statspai.mendelian¶
mendelian ¶
Mendelian Randomization methods.
Uses genetic variants as instrumental variables to estimate causal effects in epidemiological/health economics studies.
Core estimators (mr):
- IVW (inverse-variance weighted)
- MR-Egger regression
- Weighted / penalized median
Diagnostics (diagnostics):
- Cochran's Q / Rucker's Q' (heterogeneity)
- MR-Egger intercept test (directional pleiotropy)
- Leave-one-out sensitivity
- Steiger directionality test
- MR-PRESSO global test + outlier correction
- Radial MR
Frontier (frontier, v1.6):
- MR-Lap (Burgess-Davies-Thompson 2016 sample-overlap correction)
- MR-Clust (Foley et al. 2021 clustered pleiotropy)
- GRAPPLE (Wang et al. 2021 profile-likelihood MR)
- MR-cML-BIC (Xue-Shen-Pan 2021 constrained maximum likelihood)
MRResult ¶
Bases: ResultProtocolMixin
Results from Mendelian Randomization analysis.
Returned by :func:mendelian_randomization. Holds the per-method
estimates table plus heterogeneity (Cochran's Q) and pleiotropy
(MR-Egger intercept) diagnostics; render with .summary() or
.plot().
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> n_snps = 15
>>> bx = rng.uniform(0.1, 0.5, n_snps)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n_snps)
>>> snp_stats = pd.DataFrame({
... "beta_x": bx, "beta_y": by,
... "se_x": rng.uniform(0.01, 0.05, n_snps),
... "se_y": rng.uniform(0.01, 0.05, n_snps),
... })
>>> res = sp.mendelian_randomization(
... data=snp_stats, beta_exposure="beta_x", beta_outcome="beta_y",
... se_exposure="se_x", se_outcome="se_y",
... exposure_name="BMI", outcome_name="T2D", seed=0)
>>> isinstance(res, sp.MRResult)
True
>>> res.n_snps
15
>>> list(res.estimates["method"])
['IVW', 'MR-Egger', 'Weighted Median']
HeterogeneityResult
dataclass
¶
Bases: ResultProtocolMixin
Container for Cochran's Q / Rücker's Q' heterogeneity diagnostics.
Returned by :func:mr_heterogeneity. Holds the Q statistic, its
degrees of freedom and p-value, the I^2 percentage, and the
fitting method ("ivw" or "egger").
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> bx = rng.uniform(0.2, 0.5, n)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n)
>>> sy = rng.uniform(0.02, 0.05, n)
>>> het = sp.mr_heterogeneity(bx, by, sy)
>>> isinstance(het, sp.HeterogeneityResult)
True
>>> het.method
'ivw'
>>> bool(het.Q >= 0.0)
True
PleiotropyResult
dataclass
¶
Bases: ResultProtocolMixin
Container for the MR-Egger intercept (directional-pleiotropy) test.
Returned by :func:mr_pleiotropy_egger. Holds the fitted Egger
intercept, its standard error se, and the two-sided
p_value for H0: intercept == 0.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> bx = rng.uniform(0.2, 0.5, n)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n)
>>> sy = rng.uniform(0.02, 0.05, n)
>>> pleio = sp.mr_pleiotropy_egger(bx, by, sy)
>>> isinstance(pleio, sp.PleiotropyResult)
True
>>> bool(0.0 <= pleio.p_value <= 1.0)
True
LeaveOneOutResult
dataclass
¶
Bases: ResultProtocolMixin
Container for leave-one-out IVW estimates.
Returned by :func:mr_leave_one_out. Wraps a :class:pandas.DataFrame
table with one row per dropped SNP and columns dropped_snp,
estimate, se, ci_lower, ci_upper and p_value.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> bx = rng.uniform(0.2, 0.5, n)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n)
>>> sy = rng.uniform(0.02, 0.05, n)
>>> loo = sp.mr_leave_one_out(bx, by, sy)
>>> isinstance(loo, sp.LeaveOneOutResult)
True
>>> len(loo.table)
12
SteigerResult
dataclass
¶
Bases: ResultProtocolMixin
Container for the Steiger directionality test.
Returned by :func:mr_steiger. Reports whether the assumed causal
direction is supported (correct_direction), the one-sided
steiger_pvalue, the variance explained on each trait
(r2_exposure, r2_outcome), and the effective sample sizes.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> bx = rng.uniform(0.2, 0.5, n)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n)
>>> sx = rng.uniform(0.02, 0.05, n)
>>> sy = rng.uniform(0.02, 0.05, n)
>>> st = sp.mr_steiger(bx, sx, 50000, by, sy, 50000)
>>> isinstance(st, sp.SteigerResult)
True
>>> isinstance(st.correct_direction, bool)
True
RadialResult
dataclass
¶
Bases: ResultProtocolMixin
Container for radial-MR diagnostics.
Returned by :func:mr_radial. Wraps a per-SNP table (columns
snp, W, beta_hat, q_contribution), the total_Q
statistic with its Q_pvalue, and the list of Bonferroni-flagged
outliers.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> bx = rng.uniform(0.2, 0.5, n)
>>> by = 0.3 * bx + rng.normal(0, 0.02, n)
>>> sy = rng.uniform(0.02, 0.05, n)
>>> rad = sp.mr_radial(bx, by, sy)
>>> isinstance(rad, sp.RadialResult)
True
>>> len(rad.table)
12
ModeBasedResult
dataclass
¶
Bases: ResultProtocolMixin
Result container returned by :func:mr_mode.
Holds the mode-based MR point estimate, bootstrap standard error,
confidence interval, p-value, the number of SNPs, and the Silverman
bandwidth used for the kernel density. Call :meth:summary for a
formatted text report.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.2, 0.5, 25)
>>> by = 0.3 * bx + rng.normal(0, 0.01, 25)
>>> sx = rng.uniform(0.02, 0.04, 25)
>>> sy = rng.uniform(0.02, 0.04, 25)
>>> res = sp.mr_mode(bx, by, sx, sy, n_boot=200, seed=0)
>>> isinstance(res, sp.ModeBasedResult)
True
>>> res.method
'weighted-mode'
FStatisticResult
dataclass
¶
Bases: ResultProtocolMixin
Result container returned by :func:mr_f_statistic.
Summarises instrument strength across SNPs: the mean / min / max
per-SNP F-statistic, mean explained variance r2_mean, the full
per_snp_F array, and a weak_instrument_risk flag that is
True when any SNP's F falls below 10 (Staiger-Stock 1997).
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.2, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.04, 25)
>>> f = sp.mr_f_statistic(bx, sx)
>>> isinstance(f, sp.FStatisticResult)
True
>>> f.per_snp_F.shape
(25,)
MVMRResult
dataclass
¶
Bases: ResultProtocolMixin
Multivariable MR output.
Examples:
>>> import numpy as np, pandas as pd, statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx1 = rng.uniform(0.1, 0.5, 30); bx2 = rng.uniform(0.1, 0.5, 30)
>>> snp = pd.DataFrame({"beta_x1": bx1, "beta_x2": bx2,
... "beta_y": 0.4 * bx1 + rng.normal(0, 0.02, 30),
... "se_y": rng.uniform(0.01, 0.05, 30)})
>>> res = sp.mr_multivariable(snp)
>>> isinstance(res, sp.MVMRResult)
True
MediationMRResult
dataclass
¶
Bases: ResultProtocolMixin
Two-step MR output.
Examples:
>>> import numpy as np, pandas as pd, statspai as sp
>>> rng = np.random.default_rng(1)
>>> bx = rng.uniform(0.1, 0.5, 30); bm = 0.5 * bx + rng.normal(0, 0.02, 30)
>>> se = lambda: rng.uniform(0.01, 0.05, 30)
>>> snp = pd.DataFrame({"beta_x": bx, "se_x": se(), "beta_m": bm, "se_m": se(),
... "beta_y": 0.2 * bx + 0.6 * bm + rng.normal(0, 0.02, 30), "se_y": se()})
>>> res = sp.mr_mediation(snp)
>>> isinstance(res, sp.MediationMRResult)
True
MRBMAResult
dataclass
¶
Bases: ResultProtocolMixin
MR-BMA (Bayesian Model Averaging) output.
Examples:
>>> import numpy as np, pandas as pd, statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx1 = rng.uniform(0.1, 0.5, 30); bx2 = rng.uniform(0.1, 0.5, 30)
>>> snp = pd.DataFrame({"beta_x1": bx1, "beta_x2": bx2,
... "beta_y": 0.4 * bx1 + rng.normal(0, 0.02, 30),
... "se_y": rng.uniform(0.01, 0.05, 30)})
>>> res = sp.mr_bma(snp)
>>> isinstance(res, sp.MRBMAResult)
True
MRLapResult
dataclass
¶
Bases: ResultProtocolMixin
Output of :func:mr_lap.
Attributes:
| Name | Type | Description |
|---|---|---|
estimate |
float
|
Overlap-corrected causal estimate. |
se |
float
|
First-order SE (identical to IVW SE; overlap correction does not change the variance under the Burgess-Davies-Thompson 2016 approximation). |
ci_lower, ci_upper |
float
|
|
p_value |
float
|
|
estimate_ivw |
float
|
The uncorrected IVW point estimate (for comparison). |
bias_correction |
float
|
|
overlap_fraction |
float
|
|
overlap_rho |
float
|
|
f_mean |
float
|
Mean first-stage F-statistic across SNPs. |
n_snps |
int
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_lap(bx, by, sx, sy, overlap_fraction=0.5, overlap_rho=0.1)
>>> type(res).__name__
'MRLapResult'
>>> res.n_snps
25
>>> bool(np.isfinite(res.estimate))
True
MRClustResult
dataclass
¶
Bases: ResultProtocolMixin
Output of :func:mr_clust.
Attributes:
| Name | Type | Description |
|---|---|---|
cluster_estimates |
DataFrame
|
One row per cluster, columns |
assignments |
ndarray
|
Integer cluster id (0..K-1) for each SNP. |
responsibilities |
ndarray
|
(n_snps, K) matrix of posterior cluster-membership probabilities. |
K |
int
|
Selected number of clusters (including the null cluster). |
bic |
dict
|
BIC per K tried; keys are ints. |
loglik |
float
|
Final log-likelihood at the selected K. |
wald_ratios |
ndarray
|
Per-SNP Wald ratio (β_y / β_x). |
wald_se |
ndarray
|
Per-SNP Wald-ratio first-order SE. |
n_snps |
int
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_clust(bx, by, sx, sy)
>>> type(res).__name__
'MRClustResult'
>>> res.n_snps
25
>>> bool(len(res.assignments) == 25)
True
GrappleResult
dataclass
¶
Bases: ResultProtocolMixin
Output of :func:grapple.
Attributes:
| Name | Type | Description |
|---|---|---|
estimate |
float
|
Robust profile-score estimate of the causal effect β. |
se |
float
|
Sandwich standard error of the estimate (S2 Text). |
ci_lower, ci_upper |
float
|
|
p_value |
float
|
|
tau2 |
float
|
Estimated pleiotropy variance τ² (balanced pleiotropy SD² = τ²). |
loglik |
float
|
Robust objective |
tau2_se |
float
|
|
loss |
str
|
|
converged |
bool
|
|
n_snps |
int
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.grapple(bx, by, sx, sy)
>>> type(res).__name__
'GrappleResult'
>>> res.n_snps
25
>>> bool(np.isfinite(res.estimate))
True
MRcMLResult
dataclass
¶
Bases: ResultProtocolMixin
Output of :func:mr_cml.
Attributes:
| Name | Type | Description |
|---|---|---|
estimate |
float
|
cML-BIC estimate (or the BIC model average when
|
se |
float
|
Standard error from the Fisher information at the selected K (or the model-averaged SE). |
ci_lower, ci_upper, p_value |
float
|
|
K_selected |
int
|
Number of variants flagged as invalid (pleiotropic) by BIC. |
invalid_snps |
ndarray
|
Boolean mask of the K_selected variants flagged as invalid. |
path |
DataFrame
|
One row per K in [0, K_max] with columns
|
loglik |
float
|
Maximised constrained Gaussian log-likelihood at K_selected (Equation 2 including its normalising constant). |
n_snps |
int
|
|
n_samples |
int or None
|
GWAS sample size used in the BIC penalty ( |
model_average |
bool
|
Whether |
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_cml(bx, by, sx, sy, n=50_000)
>>> type(res).__name__
'MRcMLResult'
>>> res.n_snps
25
>>> bool(np.isfinite(res.estimate))
True
MRRapsResult
dataclass
¶
Bases: ResultProtocolMixin
Output of :func:mr_raps.
Attributes:
| Name | Type | Description |
|---|---|---|
estimate |
float
|
Causal effect estimate. |
se |
float
|
Asymptotic standard error: Theorem 3.3 of Zhao et al. (2020) for the
model without over-dispersion, the |
ci_lower, ci_upper |
float
|
Normal confidence interval at level |
p_value |
float
|
Two-sided normal p-value for |
tau2 |
float
|
Over-dispersion variance (0 for the model without it). |
tau2_se |
float
|
Standard error of |
loglik_robust |
float
|
Objective at the estimate, |
converged |
bool
|
Whether the estimating equations were solved within |
tuning_c |
float
|
Loss tuning constant ( |
n_snps |
int
|
Variants used, after pruning. |
loss |
str
|
|
over_dispersion |
bool
|
|
n_pruned |
int
|
Variants dropped because |
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_raps(bx, by, sx, sy)
>>> type(res).__name__
'MRRapsResult'
>>> res.n_snps
25
>>> bool(np.isfinite(res.estimate))
True
mendelian_randomization ¶
mendelian_randomization(data: Optional[DataFrame] = None, beta_exposure: Optional[str] = None, beta_outcome: Optional[str] = None, se_exposure: Optional[str] = None, se_outcome: Optional[str] = None, exposure_name: str = 'Exposure', outcome_name: str = 'Outcome', methods: Optional[List[str]] = None, alpha: float = 0.05, seed: Optional[int] = None) -> MRResult
Mendelian Randomization analysis using summary statistics.
Equivalent to R's MendelianRandomization::mr_allmethods()
and TwoSampleMR::mr().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Summary statistics with one row per SNP/instrument. |
None
|
beta_exposure
|
str
|
Column name for SNP-exposure association beta. |
None
|
beta_outcome
|
str
|
Column name for SNP-outcome association beta. |
None
|
se_exposure
|
str
|
Column name for SNP-exposure SE. |
None
|
se_outcome
|
str
|
Column name for SNP-outcome SE. |
None
|
exposure_name
|
str
|
|
'Exposure'
|
outcome_name
|
str
|
|
'Outcome'
|
methods
|
list of str
|
MR methods to use. Default: ['ivw', 'egger', 'weighted_median']. |
None
|
alpha
|
float
|
|
0.05
|
seed
|
int
|
|
None
|
Returns:
| Type | Description |
|---|---|
MRResult
|
Results with .summary(), .plot(), estimates table. |
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n_snps = 25
>>> bx = rng.uniform(0.05, 0.25, n_snps) # SNP-exposure effects
>>> by = 0.4 * bx + rng.normal(0, 0.01, n_snps) # causal effect ~ 0.4
>>> snp_stats = pd.DataFrame({
... 'beta_x': bx, 'beta_y': by,
... 'se_x': rng.uniform(0.01, 0.03, n_snps),
... 'se_y': rng.uniform(0.01, 0.03, n_snps),
... })
>>> result = sp.mendelian_randomization(
... data=snp_stats,
... beta_exposure='beta_x', beta_outcome='beta_y',
... se_exposure='se_x', se_outcome='se_y',
... exposure_name='BMI', outcome_name='T2D',
... )
>>> bool(result.n_snps == 25)
True
>>> summary_text = result.summary()
>>> ax = result.plot()
mr_egger ¶
mr_egger(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, alpha: float = 0.05) -> Dict[str, float]
MR-Egger regression.
Allows for directional pleiotropy via a non-zero intercept. Variants
are oriented to a positive exposure association before fitting, and the
standard errors use multiplicative random effects with the residual
standard error floored at 1 -- both as in MendelianRandomization and
TwoSampleMR, which this matches to machine precision.
.. versionchanged:: 1.28.0
Variants were not oriented, so the result depended on which allele
each variant was coded against: on the LDL-C / CHD example shipped
with MendelianRandomization the slope was 14% off and the
intercept -- the pleiotropy test -- 38% off. The residual standard
error was also not floored at 1, which shrinks the standard errors
below the fixed-effect ones under under-dispersion.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> n_snps = 15
>>> beta_exposure = rng.uniform(0.1, 0.5, n_snps)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n_snps)
>>> se_exposure = rng.uniform(0.01, 0.05, n_snps)
>>> se_outcome = rng.uniform(0.01, 0.05, n_snps)
>>> res = sp.mr_egger(beta_exposure, beta_outcome, se_exposure, se_outcome)
>>> sorted(res.keys())
['ci_lower', 'ci_upper', 'estimate', 'intercept', 'intercept_p', 'intercept_se', 'p_value', 'se']
>>> bool("intercept" in res)
True
References
bowden2015mendelian
mr_ivw ¶
mr_ivw(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, alpha: float = 0.05, model: str = 'default') -> Dict[str, float]
Inverse-Variance Weighted (IVW) MR estimator.
Weighted regression of the variant-outcome associations on the variant-exposure associations through the origin (equivalently, an inverse-variance-weighted mean of the Wald ratios).
model follows MendelianRandomization::mr_ivw:
"fixed"-- standard errorsqrt(1 / sum(w bx^2));"random"-- multiplicative random effects: the fixed-effect standard error timesmax(1, RSE),RSE = sqrt(Q / (n - 1));"default"-- random with more than three variants, fixed otherwise (the package's own rule; TwoSampleMR'smr_ivwis the same multiplicative random-effects form).
.. versionchanged:: 1.28.0
The standard error used to be the fixed-effect one regardless of
heterogeneity. On MendelianRandomization's own LDL-cholesterol /
CHD example (28 variants, Cochran's Q p = 3e-10) that is 0.276
against the reference's 0.530 -- about half. The point estimate is
unchanged.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> n_snps = 15
>>> beta_exposure = rng.uniform(0.1, 0.5, n_snps)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n_snps)
>>> se_exposure = rng.uniform(0.01, 0.05, n_snps)
>>> se_outcome = rng.uniform(0.01, 0.05, n_snps)
>>> res = sp.mr_ivw(beta_exposure, beta_outcome, se_exposure, se_outcome)
>>> sorted(res.keys())
['I2', 'Q', 'Q_df', 'Q_p', 'ci_lower', 'ci_upper', 'estimate', 'model',
'p_value', 'rse', 'se']
>>> bool(0.0 < res["estimate"] < 0.6)
True
References
burgess2013mendelian
mr_median ¶
mr_median(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, penalized: bool = False, n_boot: int = 1000, alpha: float = 0.05, seed: Optional[int] = None, weighting: Optional[str] = None) -> Dict[str, float]
Weighted median MR estimator.
Consistent when at least 50% of the weight comes from valid instruments.
Matches MendelianRandomization::mr_median in all three weightings.
.. versionchanged:: 1.28.0
Three corrections, found against MendelianRandomization:
- the weighted median was a step function (first ratio reaching half the weight) instead of Bowden et al.'s interpolated definition -- 0.8% on the package's LDL-C / CHD example;
- the penalty used the LOWER-tail chi-square probability, so it
up-weighted exactly the heterogeneous variants it exists to
suppress, had no
min(1, 20 q)cap, and centred on the IVW estimate rather than the weighted median -- 38% off; - the bootstrap recomputed the weights from every draw instead of holding them fixed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weighting
|
(weighted, simple, penalized)
|
|
"weighted"
|
penalized
|
bool
|
Kept for backward compatibility; equivalent to
|
False
|
n_boot
|
int
|
Parametric bootstrap for the standard error: draw |
1000
|
seed
|
int
|
Parametric bootstrap for the standard error: draw |
1000
|
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> n_snps = 15
>>> beta_exposure = rng.uniform(0.1, 0.5, n_snps)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n_snps)
>>> se_exposure = rng.uniform(0.01, 0.05, n_snps)
>>> se_outcome = rng.uniform(0.01, 0.05, n_snps)
>>> res = sp.mr_median(beta_exposure, beta_outcome, se_exposure,
... se_outcome, n_boot=200, seed=0)
>>> sorted(res.keys())
['ci_lower', 'ci_upper', 'estimate', 'p_value', 'se', 'weighting']
>>> bool(0.0 < res["estimate"] < 0.6)
True
References
bowden2016consistent
mr_plot ¶
mr_plot(result: Optional[MRResult] = None, ax: Any = None, **kwargs: Any) -> Any
Scatter plot of SNP-exposure vs SNP-outcome effects with MR lines.
mr_heterogeneity ¶
mr_heterogeneity(beta_exposure: ndarray, beta_outcome: ndarray, se_outcome: ndarray, *, method: str = 'ivw', se_exposure: Optional[ndarray] = None) -> HeterogeneityResult
Cochran's Q (IVW) or Rücker's Q' (Egger) for pleiotropic heterogeneity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beta_exposure
|
array - like
|
|
required |
beta_outcome
|
array - like
|
|
required |
se_outcome
|
array - like
|
|
required |
method
|
('ivw', 'egger')
|
|
"ivw"
|
se_exposure
|
array - like
|
Required for Rücker's Q' (Egger); if omitted we default to
first-order weights and raise for |
None
|
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> beta_exposure = rng.uniform(0.2, 0.5, n)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n)
>>> se_outcome = rng.uniform(0.02, 0.05, n)
>>> het = sp.mr_heterogeneity(beta_exposure, beta_outcome, se_outcome)
>>> het.method
'ivw'
>>> het.Q_df # n - 1 for IVW
11
>>> bool(0.0 <= het.I2 <= 100.0)
True
References
[@bowden2016assessing]
mr_pleiotropy_egger ¶
mr_pleiotropy_egger(beta_exposure: ndarray, beta_outcome: ndarray, se_outcome: ndarray) -> PleiotropyResult
Test the MR-Egger intercept for directional (unbalanced) pleiotropy.
H0: intercept == 0 (no directional pleiotropy).
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> beta_exposure = rng.uniform(0.2, 0.5, n)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n)
>>> se_outcome = rng.uniform(0.02, 0.05, n)
>>> pleio = sp.mr_pleiotropy_egger(beta_exposure, beta_outcome, se_outcome)
>>> bool(pleio.se >= 0.0)
True
>>> bool(0.0 <= pleio.p_value <= 1.0)
True
mr_leave_one_out ¶
mr_leave_one_out(beta_exposure: ndarray, beta_outcome: ndarray, se_outcome: ndarray, *, snp_ids: Optional[List[str]] = None, alpha: float = 0.05, model: str = 'default') -> LeaveOneOutResult
IVW estimate with each SNP dropped in turn.
Identifies SNPs that drive the overall estimate disproportionately.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> beta_exposure = rng.uniform(0.2, 0.5, n)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n)
>>> se_outcome = rng.uniform(0.02, 0.05, n)
>>> loo = sp.mr_leave_one_out(beta_exposure, beta_outcome, se_outcome)
>>> len(loo.table) # one row per dropped SNP
12
>>> sorted(loo.table.columns)
['ci_lower', 'ci_upper', 'dropped_snp', 'estimate', 'p_value', 'se']
mr_steiger ¶
mr_steiger(beta_exposure: ndarray, se_exposure: ndarray, n_exposure: SampleSizeInput, beta_outcome: ndarray, se_outcome: ndarray, n_outcome: SampleSizeInput, *, eaf: Optional[ndarray] = None, alternative: str = 'two-sided') -> SteigerResult
Steiger test for the direction of the causal effect (Hemani 2017).
Compares the R^2 of the SNPs on the exposure with their R^2 on the
outcome. If R^2_exposure > R^2_outcome the assumed causal
direction (exposure -> outcome) is supported. Matches
TwoSampleMR::mr_steiger with r from get_r_from_bsen.
.. versionchanged:: 1.28.0
The p-value was one-sided and computed as 1 - Phi(z), which is
exactly 0.0 for z beyond ~8.3. It is now two-sided by default, as
the method authors' TwoSampleMR reports it, computed from the upper
tail directly; pass alternative="greater" for the one-sided test.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alternative
|
('two-sided', 'greater')
|
|
"two-sided"
|
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> beta_exposure = rng.uniform(0.2, 0.5, n)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n)
>>> se_exposure = rng.uniform(0.02, 0.05, n)
>>> se_outcome = rng.uniform(0.02, 0.05, n)
>>> st = sp.mr_steiger(
... beta_exposure, se_exposure, 50000,
... beta_outcome, se_outcome, 50000,
... )
>>> isinstance(st.correct_direction, bool)
True
>>> bool(st.r2_exposure >= 0.0 and st.r2_outcome >= 0.0)
True
References
[@hemani2017orienting]
mr_radial ¶
mr_radial(beta_exposure: ndarray, beta_outcome: ndarray, se_outcome: ndarray, *, snp_ids: Optional[List[str]] = None, alpha: float = 0.05, bonferroni: bool = True) -> RadialResult
Radial IVW MR (Bowden et al. 2018).
Reparameterizes each SNP's Wald ratio as a coordinate in a
"radial" space. SNPs whose individual chi-square contribution to
the Cochran Q exceeds the threshold are flagged as outliers: the
Bonferroni level alpha / n by default, or alpha itself with
bonferroni=False, which is RadialMR::ivw_radial's rule. The
per-variant contributions and total Q match RadialMR (first-order
weights) to machine precision either way.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 12
>>> beta_exposure = rng.uniform(0.2, 0.5, n)
>>> beta_outcome = 0.3 * beta_exposure + rng.normal(0, 0.02, n)
>>> se_outcome = rng.uniform(0.02, 0.05, n)
>>> rad = sp.mr_radial(beta_exposure, beta_outcome, se_outcome)
>>> len(rad.table) # one row per SNP
12
>>> bool(0.0 <= rad.Q_pvalue <= 1.0)
True
>>> isinstance(rad.outliers, list)
True
References
[@bowden2018improving]
mr_mode ¶
mr_mode(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, method: str = 'weighted', n_boot: int = 1000, alpha: float = 0.05, seed: Optional[int] = None, phi: float = 1.0, refine: bool = False) -> ModeBasedResult
Mode-based MR estimator (Hartwig, Davey Smith & Bowden 2017).
Matches MendelianRandomization::mr_mbe(stderror = "simple") to
machine precision in both weightings: same bandwidth rule, same
512-point density grid, same bootstrap construction.
.. versionchanged:: 1.28.0
The bandwidth was a weighted-SD / IQR rule rather than Hartwig et
al.'s 0.9 min(sd, mad) n^(-1/5) on the unweighted ratios, the
mode was searched on a different grid, and the bootstrap redrew the
exposure associations and recomputed the weights instead of
resampling the ratios with the weights held fixed. On the reference
package's LDL-C / CHD example the estimate moved 7%. The standard
error is now the MAD of the bootstrap draws, as in the reference,
rather than their SD.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
('weighted', 'simple')
|
|
"weighted"
|
phi
|
float
|
Bandwidth multiplier, the reference's |
1.0
|
refine
|
bool
|
The reference estimate is a point of its 512-point density grid,
whose step is |
False
|
n_boot
|
int
|
Bootstrap replicates for SE. |
1000
|
References
hartwig2017robust
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.2, 0.5, 25)
>>> by = 0.3 * bx + rng.normal(0, 0.01, 25)
>>> sx = rng.uniform(0.02, 0.04, 25)
>>> sy = rng.uniform(0.02, 0.04, 25)
>>> res = sp.mr_mode(bx, by, sx, sy, n_boot=200, seed=0)
>>> res.n_snps
25
>>> bool(0.0 < res.estimate < 1.0) # near the true 0.3
True
mr_f_statistic ¶
mr_f_statistic(beta_exposure: ndarray, se_exposure: ndarray, *, n_samples: Optional[int] = None) -> FStatisticResult
Per-SNP F-statistic as an instrument-strength summary.
Uses the standard summary-stat approximation
F_i = (beta_i / se_i)^2, which is valid for large GWAS where
the first-stage regression is asymptotically Normal. Flags weak-IV
risk when any SNP's F falls below 10 (Staiger-Stock 1997).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_samples
|
int
|
Exposure GWAS sample size. If provided, each variant's R^2 is
.. versionchanged:: 1.41
Without |
None
|
References
staiger1997instrumental
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.2, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.04, 25) # strong instruments
>>> f = sp.mr_f_statistic(bx, sx)
>>> bool(f.f_mean > 10) # Staiger-Stock rule of thumb
True
>>> f.weak_instrument_risk
False
mr_funnel_plot ¶
mr_funnel_plot(beta_exposure: ndarray, beta_outcome: ndarray, se_outcome: ndarray, *, snp_ids: Optional[List[str]] = None, ax: Any = None) -> Any
Funnel plot of SNP-specific Wald ratios vs. precision.
An asymmetric funnel around the IVW estimate suggests directional horizontal pleiotropy. Returns the matplotlib axis.
Examples:
mr_scatter_plot ¶
mr_scatter_plot(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, ax: Any = None) -> Any
Classic MR scatter plot with IVW and MR-Egger lines.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.2, 0.5, 25)
>>> by = 0.3 * bx + rng.normal(0, 0.01, 25)
>>> sx = rng.uniform(0.02, 0.04, 25)
>>> sy = rng.uniform(0.02, 0.04, 25)
>>> ax = sp.mr_scatter_plot(bx, by, sx, sy)
>>> ax is not None
True
mr_multivariable ¶
mr_multivariable(snp_associations: DataFrame, *, outcome: str = 'beta_y', outcome_se: str = 'se_y', exposures: Optional[Sequence[str]] = None, alpha: float = 0.05, exposure_se: Optional[Sequence[str]] = None) -> MVMRResult
Multivariable Mendelian Randomization (Sanderson et al. 2019).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snp_associations
|
DataFrame
|
One row per SNP. Must contain:
|
required |
outcome
|
str
|
|
'beta_y'
|
outcome_se
|
str
|
|
'beta_y'
|
exposures
|
sequence of str
|
Exposure-beta column names. If omitted, all |
None
|
alpha
|
float
|
|
0.05
|
exposure_se
|
sequence of str
|
Columns of the variant-exposure standard errors, in the order of
|
None
|
Returns:
| Type | Description |
|---|---|
MVMRResult
|
Contains direct-effect estimates for each exposure and the
two-sample conditional F-statistics of Sanderson, Spiller & Bowden
(2021): for exposure .. versionchanged:: 1.41
|
Notes
Model:
β_Y = Σ_j α_j β_{X_j} + ε, ε ~ Normal(0, σ_Y²)
fitted by weighted least squares with weights 1 / se_y². The α_j
are the direct causal effects, holding other exposures fixed.
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n_snps = 30
>>> bx1 = rng.uniform(0.1, 0.5, n_snps)
>>> bx2 = rng.uniform(0.1, 0.5, n_snps)
>>> # True direct effects: X1 -> Y = 0.4, X2 -> Y = 0.0.
>>> by = 0.4 * bx1 + 0.0 * bx2 + rng.normal(0, 0.02, n_snps)
>>> snp = pd.DataFrame({
... "beta_x1": bx1, "beta_x2": bx2, "beta_y": by,
... "se_y": rng.uniform(0.01, 0.05, n_snps),
... })
>>> res = sp.mr_multivariable(snp, outcome="beta_y", outcome_se="se_y")
>>> isinstance(res, sp.MVMRResult)
True
>>> res.exposures
['beta_x1', 'beta_x2']
References
Sanderson, E., Davey Smith, G., Windmeijer, F. & Bowden, J. (2019). "An examination of multivariable Mendelian randomization in the single-sample and two-sample summary data settings." IJE 48(3). [@sanderson2019examination]
mr_mediation ¶
mr_mediation(snp_associations: DataFrame, *, beta_exposure: str = 'beta_x', se_exposure: str = 'se_x', beta_mediator: str = 'beta_m', se_mediator: str = 'se_m', beta_outcome: str = 'beta_y', se_outcome: str = 'se_y', exposure_name: str = 'exposure', mediator_name: str = 'mediator', outcome_name: str = 'outcome') -> MediationMRResult
Two-step MR mediation: decompose total effect into direct + indirect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snp_associations
|
DataFrame
|
One row per SNP. Must contain SNP-associations with the exposure, mediator and outcome (both beta and SE). |
required |
beta_exposure
|
str
|
|
'beta_x'
|
se_exposure
|
str
|
|
'beta_x'
|
beta_mediator
|
str
|
|
'beta_x'
|
se_mediator
|
str
|
|
'beta_x'
|
beta_outcome
|
str
|
|
'beta_y'
|
se_outcome
|
str
|
|
'beta_y'
|
Returns:
| Type | Description |
|---|---|
MediationMRResult
|
|
Notes
Two-step approach (Burgess et al. 2015):
- Step 1 — total effect (IVW): α_total = IVW(β_Y ~ β_X)
(
sp.mr_ivw's default model: multiplicative random effects). - Step 2a — exposure → mediator: α_XM = IVW(β_M ~ β_X).
- Step 2b — direct effect via MVMR: α_direct = MVMR(β_Y ~ β_X, β_M).
- Indirect: α_indirect = α_total − α_direct.
The direct effect is sp.mr_multivariable (mr_mvivw). The SE of
the indirect effect combines the SEs of steps 1 and 3 as if they were
independent (they are not; treat it as approximate).
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(1)
>>> n_snps = 30
>>> bx = rng.uniform(0.1, 0.5, n_snps)
>>> # exposure -> mediator (0.5), mediator -> outcome (0.6),
>>> # direct (0.2).
>>> bm = 0.5 * bx + rng.normal(0, 0.02, n_snps)
>>> by = 0.2 * bx + 0.6 * bm + rng.normal(0, 0.02, n_snps)
>>> snp = pd.DataFrame({
... "beta_x": bx, "se_x": rng.uniform(0.01, 0.05, n_snps),
... "beta_m": bm, "se_m": rng.uniform(0.01, 0.05, n_snps),
... "beta_y": by, "se_y": rng.uniform(0.01, 0.05, n_snps),
... })
>>> res = sp.mr_mediation(snp)
>>> isinstance(res, sp.MediationMRResult)
True
>>> bool(res.indirect_effect > 0) # positive mediated path X -> M -> Y
True
References
Burgess, S., Daniel, R. M., Butterworth, A. S. & Thompson, S. G. (2015). "Network Mendelian randomization: using genetic variants as instrumental variables to investigate mediation in causal pathways." IJE 44(2). [@burgess2015network]
mr_bma ¶
mr_bma(snp_associations: DataFrame, *, outcome: str = 'beta_y', outcome_se: str = 'se_y', exposures: Optional[Sequence[str]] = None, max_model_size: Optional[int] = None, prior_inclusion: float = 0.5, prior_sd: float = 0.5, method: str = 'bf') -> MRBMAResult
Mendelian Randomization with Bayesian Model Averaging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snp_associations
|
DataFrame
|
|
required |
outcome
|
see :func:`mr_multivariable`
|
|
'beta_y'
|
outcome_se
|
see :func:`mr_multivariable`
|
|
'beta_y'
|
exposures
|
see :func:`mr_multivariable`
|
|
'beta_y'
|
max_model_size
|
int
|
Maximum number of exposures in any one model (Zuber et al.'s
|
None
|
prior_inclusion
|
float
|
Prior probability that each exposure is in the true causal model
( |
0.5
|
prior_sd
|
float
|
Standard deviation |
0.5
|
method
|
('bf', 'bic')
|
|
'bf'
|
Returns:
| Type | Description |
|---|---|
MRBMAResult
|
|
Notes
With X = beta_X / se_Y, Y = beta_Y / se_Y and a model g,
Omega^-1 = sigma^-2 I + X_g'X_g, B = Omega X_g'Y and
log10 BF_g = -0.5 log10 det(Omega^-1) - |g| log10 sigma
- (n/2) [log10(Y'Y - B' Omega^-1 B) - log10(Y'Y)].
Before 1.30 this function returned BIC-weighted BMA under the MR-BMA
name (method='bic'); its inclusion probabilities differ from
Zuber et al.'s and it reported no averaged effect.
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(2)
>>> n_snps = 40
>>> bx1 = rng.uniform(0.1, 0.5, n_snps)
>>> bx2 = rng.uniform(0.1, 0.5, n_snps)
>>> bx3 = rng.uniform(0.1, 0.5, n_snps)
>>> by = 0.4 * bx1 + rng.normal(0, 0.02, n_snps) # only X1 is causal
>>> snp = pd.DataFrame({
... "beta_x1": bx1, "beta_x2": bx2, "beta_x3": bx3, "beta_y": by,
... "se_y": rng.uniform(0.01, 0.05, n_snps),
... })
>>> res = sp.mr_bma(snp, outcome="beta_y", outcome_se="se_y")
>>> isinstance(res, sp.MRBMAResult)
True
>>> # X1 carries the most posterior inclusion mass.
>>> bool(
... res.marginal_inclusion["beta_x1"]
... > res.marginal_inclusion["beta_x2"]
... )
True
References
Zuber, V., Colijn, J. M., Klaver, C. & Burgess, S. (2020). "Selecting likely causal risk factors from high-throughput experiments using multivariable Mendelian randomization." Nature Communications 11, 29. [@zuber2020selecting]
mr_lap ¶
mr_lap(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, overlap_fraction: float = 1.0, overlap_rho: float = 0.0, alpha: float = 0.05) -> MRLapResult
Sample-overlap-corrected inverse-variance-weighted MR.
Corrects the first-order bias introduced when the exposure and outcome GWAS share participants. Under Burgess, Davies & Thompson (2016) Eq. 8, the bias in two-sample IVW satisfies
.. math::
E[\hat\beta_{IVW}] - \beta \approx \frac{p \, \rho_{obs}}{F_{mean}}
where :math:p is the overlap fraction (share of samples appearing in
both GWAS), :math:\rho_{obs} is the phenotypic correlation of
exposure and outcome in the overlapping sub-sample, and
:math:F_{mean} is the mean first-stage F-statistic of the SNPs.
When overlap_fraction=0 (clean two-sample design) the correction
vanishes and the estimate equals standard IVW.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beta_exposure
|
ndarray
|
Per-SNP GWAS effect sizes. |
required |
beta_outcome
|
ndarray
|
Per-SNP GWAS effect sizes. |
required |
se_exposure
|
ndarray
|
Per-SNP GWAS standard errors. |
required |
se_outcome
|
ndarray
|
Per-SNP GWAS standard errors. |
required |
overlap_fraction
|
float
|
Share of the outcome sample that also appears in the exposure sample. 0 = clean two-sample; 1 = full overlap; in between = partial overlap. Must be in [0, 1]. |
1.0
|
overlap_rho
|
float
|
Phenotypic correlation between exposure and outcome measurements
in the overlapping sub-sample. In practice estimated by LD-score
regression ( |
0.0
|
alpha
|
float
|
Two-sided significance level for the CI and p-value. |
0.05
|
Returns:
| Type | Description |
|---|---|
class:`MRLapResult`
|
|
Notes
This implements the closed-form Burgess-Davies-Thompson (2016)
correction. The more elaborate MR-Lap of Mounier & Kutalik (2023)
additionally regresses bias on a non-linear function of
(F_mean, overlap_fraction) to absorb weak-instrument curvature;
that refinement is deferred to a future release. When the user-
supplied overlap_rho is obtained from LD-score regression this
simplified correction captures most of the overlap bias on
real-world GWAS scales (F > 30).
The SE is kept equal to the naive-IVW SE: the correction shifts the point estimate only. Monte-Carlo studies (Burgess 2016 Figs. 2-3) show coverage remains nominal under this conservative choice because the correction itself has second-order variance.
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_lap(bx, by, sx, sy,
... overlap_fraction=0.4, overlap_rho=0.18)
>>> type(res).__name__
'MRLapResult'
>>> bool(np.isfinite(res.estimate))
True
mr_clust ¶
mr_clust(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, K_range: Tuple[int, int] = (1, 5), include_null: bool = True, alpha: float = 0.05, seed: int = 0, max_iter: int = 200, tol: float = 1e-06) -> MRClustResult
Clustered Mendelian randomization via Gaussian mixture on Wald ratios.
Implements the MR-Clust estimator of Foley, Mason, Kirk & Burgess (2021) Bioinformatics 37(4). Assigns each SNP to one of K clusters based on its Wald ratio, where cluster 0 is a 'null' component (SNPs with zero effect on the outcome). Cluster means represent distinct causal-effect estimates — multiple non-null clusters are evidence of heterogeneous / pathway-specific pleiotropy.
Model
For SNP i in cluster k:
.. math::
r_i = \frac{\beta_{y,i}}{\beta_{x,i}}, \qquad r_i \mid z_i = k \ \sim\ \mathcal{N}(\theta_k, \tau_i^2)
with :math:\tau_i = s_{y,i} / |\beta_{x,i}| the first-order Wald
SE. When include_null=True cluster 0 is fixed at
:math:\theta_0 = 0. Parameters fitted by EM; K selected by BIC.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beta_exposure
|
ndarray
|
|
required |
beta_outcome
|
ndarray
|
|
required |
se_exposure
|
ndarray
|
|
required |
se_outcome
|
ndarray
|
|
required |
K_range
|
(int, int)
|
Inclusive range of cluster counts to try. K=1 with
|
(1, 5)
|
include_null
|
bool
|
If True the first cluster is fixed at 0 (null). |
True
|
alpha
|
float
|
For per-cluster CIs. |
0.05
|
seed
|
int
|
EM initialisation rng. |
0
|
max_iter
|
EM controls.
|
|
200
|
tol
|
EM controls.
|
|
200
|
Returns:
| Type | Description |
|---|---|
class:`MRClustResult`
|
|
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(8)
>>> bx = rng.uniform(0.1, 0.5, 25)
>>> sx = rng.uniform(0.02, 0.05, 25)
>>> sy = rng.uniform(0.02, 0.05, 25)
>>> by = 0.3 * bx + rng.normal(0, sy)
>>> res = sp.mr_clust(bx, by, sx, sy, K_range=(1, 4))
>>> type(res).__name__
'MRClustResult'
>>> res.n_snps
25
mr_cml ¶
mr_cml(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, K_max: Optional[int] = None, alpha: float = 0.05, max_iter: int = 100, tol: float = 1e-07, n: Optional[int] = None, model_average: bool = False) -> MRcMLResult
Constrained maximum-likelihood MR (cML-BIC / cML-MA-BIC).
Fits the constrained MLE of Xue, Shen & Pan (2021) for every number
of invalid instruments K = 0, ..., K_max, selects K by
BIC(K) = -2 L(K) + K log n and reports either the selected fit
(cML-BIC) or the BIC-weighted average over K (cML-MA-BIC,
model_average=True). Robust to both correlated and uncorrelated
pleiotropy under the plurality-valid assumption.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beta_exposure
|
array - like
|
Per-variant associations with the exposure and the outcome. |
required |
beta_outcome
|
array - like
|
Per-variant associations with the exposure and the outcome. |
required |
se_exposure
|
array - like
|
Their standard errors (strictly positive). |
required |
se_outcome
|
array - like
|
Their standard errors (strictly positive). |
required |
K_max
|
int
|
Largest number of invalid instruments considered. Default
|
None
|
alpha
|
float
|
Two-sided level for the normal confidence interval. |
0.05
|
max_iter
|
int
|
Maximum coordinate-descent rounds per K. |
100
|
tol
|
float
|
Stop when the change in theta between rounds is below |
1e-7
|
n
|
int
|
GWAS sample size for the BIC penalty; the paper recommends
|
None
|
model_average
|
bool
|
Report the BIC model average (Equation 5) instead of the BIC-selected fit. |
False
|
Returns:
| Type | Description |
|---|---|
MRcMLResult
|
|
Notes
Variants with negative exposure associations are sign-flipped first
(the likelihood is invariant to flipping both associations of a
variant). The data-perturbation variants (cML-*-DP) and random
restarts are not implemented: the fit uses the paper's default start
theta = 0, b = 0.
Clean-room implementation; the reference (MendelianRandomization 0.10.0, GPL-2 | GPL-3) was used only as a black box.
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(0)
>>> bx = rng.uniform(0.1, 0.5, 30)
>>> sx = np.full(30, 0.02)
>>> sy = np.full(30, 0.02)
>>> by = 0.4 * bx + rng.normal(0, sy)
>>> r = sp.mr_cml(bx, by, sx, sy, n=50_000)
>>> r.K_selected <= 2
True
References
xue2021constrained
mr_raps ¶
mr_raps(beta_exposure: ndarray, beta_outcome: ndarray, se_exposure: ndarray, se_outcome: ndarray, *, loss: Optional[str] = None, over_dispersion: bool = True, tuning_c: Optional[float] = None, alpha: float = 0.05, pruning: bool = True, niter: int = 20, tol: float = _EPS_HALF, beta_init: Optional[float] = None, tau2_init: Optional[float] = None) -> MRRapsResult
Robust Adjusted Profile Score MR (Zhao et al. 2020).
Profile-likelihood MR that accounts for measurement error in the
exposure associations (weak instruments), optionally for systematic
pleiotropy (a random-effect variance tau2) and, with a robust loss,
for a small fraction of gross pleiotropic outliers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
beta_exposure
|
array - like
|
Variant-exposure and variant-outcome associations. |
required |
beta_outcome
|
array - like
|
Variant-exposure and variant-outcome associations. |
required |
se_exposure
|
array - like
|
Their standard errors (strictly positive). |
required |
se_outcome
|
array - like
|
Their standard errors (strictly positive). |
required |
loss
|
('huber', 'tukey', 'l2')
|
Loss in the profile score. |
'huber'
|
over_dispersion
|
bool
|
Estimate the over-dispersion variance |
True
|
tuning_c
|
float
|
Loss tuning constant |
None
|
alpha
|
float
|
|
0.05
|
pruning
|
bool
|
Drop variants with |
True
|
niter
|
int
|
Maximum number of alternating |
20
|
tol
|
float
|
Convergence threshold on the change in |
``sqrt(machine eps)``
|
beta_init
|
float
|
Deprecated and ignored: the over-dispersed fit starts from the no-over-dispersion estimate and the robust fit from the L2 fit. |
None
|
tau2_init
|
float
|
Deprecated and ignored: the over-dispersed fit starts from the no-over-dispersion estimate and the robust fit from the L2 fit. |
None
|
Returns:
| Type | Description |
|---|---|
MRRapsResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
On mismatched lengths, fewer than 3 variants, non-positive standard
errors, an unknown |
Examples:
>>> import statspai as sp
>>> import numpy as np
>>> rng = np.random.default_rng(1)
>>> bx = rng.uniform(0.1, 0.4, 30)
>>> sx = np.full(30, 0.02)
>>> sy = np.full(30, 0.02)
>>> by = 0.5 * bx + rng.normal(0, 0.02, 30)
>>> r = sp.mr_raps(bx, by, sx, sy, loss="tukey")
>>> bool(abs(r.estimate - 0.5) < 0.2)
True
Notes
Clean-room implementation of the paper's estimating equations; the
reference (R mr.raps 0.4.3, GPL-3) was used only as a black box.
The (R)APS root is solved to machine precision, so it can differ from
the reference in the digits that the reference's own stopping rule
leaves undetermined (see docs/dev/cleanroom/mr_raps.md).
References
zhao2020statistical