statspai.mediation¶
mediation ¶
Causal Mediation Analysis module for StatsPAI.
Implements modern causal mediation analysis following Imai, Keele, and Tingley (2010), decomposing total treatment effects into: - Average Causal Mediation Effect (ACME): indirect effect through mediator - Average Direct Effect (ADE): direct effect not through mediator
Also supports the classical Baron-Kenny approach for comparison.
References
Imai, K., Keele, L., and Tingley, D. (2010). "A General Approach to Causal Mediation Analysis." Psychological Methods, 15(4), 309-334. [@imai2010general]
Baron, R.M. and Kenny, D.A. (1986). "The Moderator-Mediator Variable Distinction in Social Psychological Research." Journal of Personality and Social Psychology, 51(6), 1173-1182. [@baron1986moderator]
FourWayResult
dataclass
¶
Bases: ResultProtocolMixin
Result container for :func:four_way_decomposition.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> treat = np.tile([0.0, 1.0], 50)
>>> mediator = 0.5 + 0.4 * treat + np.repeat(np.linspace(-1, 1, 50), 2)
>>> y = 1 + 2 * treat + 3 * mediator + 4 * treat * mediator
>>> df = pd.DataFrame({"y": y, "a": treat, "m": mediator})
>>> res = sp.four_way_decomposition(df, y="y", treat="a", mediator="m")
>>> type(res).__name__
'FourWayResult'
>>> round(float(res.total_effect), 1)
6.8
MediationAnalysis ¶
Causal Mediation Analysis estimator.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 300
>>> treat = rng.binomial(1, 0.5, n)
>>> x = rng.normal(0, 1, n)
>>> m = 0.4 * treat + 0.3 * x + rng.normal(0, 1, n)
>>> y = 0.5 * treat + 0.6 * m + 0.2 * x + rng.normal(0, 1, n)
>>> df = pd.DataFrame({"treat": treat, "x": x, "m": m, "y": y})
>>> ma = sp.MediationAnalysis(df, y="y", treat="treat", mediator="m",
... covariates=["x"], n_boot=100, seed=0)
>>> res = ma.fit()
>>> res.estimand
'ACME'
>>> res.method
'Causal Mediation Analysis'
MediateSensitivityResult
dataclass
¶
Bases: ResultProtocolMixin
plot ¶
plot(ax: Any = None, *, fill: bool = True, annotate: bool = True, figsize: Any = (7.0, 4.5), **kwargs: Any) -> Any
Publication-style sensitivity plot.
Shows ACME(ρ) vs the mediator-outcome confounder strength ρ,
with a coloured fill for the region of nullability (any ρ in
[ρ_at_zero, 1] flips the ACME sign), the ρ at which the
ACME crosses zero (annotated), and reference lines at ρ=0
(i.e. sequential-ignorability) and ACME=0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
matplotlib Axes
|
|
None
|
fill
|
bool
|
Fill the {ACME(ρ) > 0} region in light blue and the {ACME(ρ) < 0} region in light red, à la sensemakr. |
True
|
annotate
|
bool
|
Annotate ρ_at_zero, baseline ACME, and an interpretive note. |
True
|
four_way_decomposition ¶
four_way_decomposition(data: DataFrame, y: str, treat: str, mediator: str, covariates: Optional[Sequence[str]] = None, a0: float = 0.0, a1: float = 1.0, m0: float = 0.0, vce: str = 'ols', yreg: str = 'linear', mreg: str = 'linear', covariate_values: Optional[Mapping[str, float]] = None) -> FourWayResult
Parametric four-way decomposition of TE = CDE + INT_ref + INT_med + PIE.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
|
required |
treat
|
str
|
|
required |
mediator
|
str
|
|
required |
covariates
|
sequence of str
|
Entered linearly in both the outcome and the mediator model. |
None
|
a0
|
float
|
Reference and comparison levels of the treatment (default 0, 1). |
0.0
|
a1
|
float
|
Reference and comparison levels of the treatment (default 0, 1). |
0.0
|
m0
|
float
|
Mediator reference level at which CDE is evaluated. |
0.0
|
vce
|
('ols', 'ml')
|
Variance used for linear models (a linear outcome, a normal
mediator). |
'ols'
|
yreg
|
('linear', 'logistic', 'poisson')
|
Outcome model, with the exposure-mediator interaction. For
|
'linear'
|
mreg
|
('linear', 'logistic')
|
Mediator model: normal linear regression, or logistic regression for a 0/1 mediator. |
'linear'
|
covariate_values
|
mapping
|
Values of the covariates at which the decomposition is evaluated
( |
None
|
Returns:
| Type | Description |
|---|---|
FourWayResult
|
|
Notes
Every component is a closed-form function of the two regressions
(mediation/_four_way_engine.py); the Jacobian for the delta method
is computed by complex-step differentiation, so it carries no
truncation error. Against Stata med4way 2.3.2 (vce='ml') the
estimates agree to 1e-8 and the standard errors to 1e-7 for every
outcome / mediator model (tests/reference_parity/
test_four_way_nonlinear_parity.py). With vce='ols' the estimates
equal CMAverse's; its standard errors leave out the mediator
variance's sampling error.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> treat = np.tile([0.0, 1.0], 50)
>>> mediator = 0.5 + 0.4 * treat + np.repeat(np.linspace(-1, 1, 50), 2)
>>> y = 1 + 2 * treat + 3 * mediator + 4 * treat * mediator
>>> df = pd.DataFrame({"y": y, "a": treat, "m": mediator})
>>> res = sp.four_way_decomposition(df, y="y", treat="a", mediator="m")
>>> round(float(res.cde), 1)
2.0
>>> round(float(res.pie), 1)
1.2
mediate_interventional ¶
mediate_interventional(data: DataFrame, y: str, treat: str, mediator: str, covariates: Optional[List[str]] = None, tv_confounders: Optional[List[str]] = None, n_mc: int = 500, n_boot: int = 500, alpha: float = 0.05, pvalue_method: str = 'bootstrap_sign', seed: int = 42) -> CausalResult
Interventional (in)direct effects (VanderWeele, Vansteelandt, Robins 2014).
Decomposes the total effect into:
- IIE (Interventional Indirect Effect): E[Y(1, G_{M|1})] - E[Y(1, G_{M|0})], i.e. the effect of shifting M's post-treatment distribution from its D=0 draw to its D=1 draw, while holding D fixed at 1.
- IDE (Interventional Direct Effect): E[Y(1, G_{M|0})] - E[Y(0, G_{M|0})].
- Total = IIE + IDE = E[Y(1, G_{M|1})] - E[Y(0, G_{M|0})].
Here :math:G_{M|d} is the random draw from the marginal
distribution of :math:M under treatment :math:D = d (integrated
over covariates).
Interventional effects are identified under the standard mediation
assumptions minus the cross-world independence requirement —
which makes them valid even when there are treatment-induced
mediator-outcome confounders (tv_confounders). Natural effects
are not generally identified in that case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
Outcome variable. |
required |
treat
|
str
|
Binary treatment (0/1). |
required |
mediator
|
str
|
Mediator variable. |
required |
covariates
|
list of str
|
Baseline (pre-treatment) covariates. |
None
|
tv_confounders
|
list of str
|
Treatment-induced mediator-outcome confounders (variables
affected by D that confound the M-Y relationship). These enter
the outcome model but not the M-marginalization; each is
modelled linearly on |
None
|
n_mc
|
int
|
Monte Carlo draws of M for the stochastic intervention. |
500
|
n_boot
|
int
|
Nonparametric bootstrap replications. |
500
|
alpha
|
float
|
Significance level. |
0.05
|
pvalue_method
|
(bootstrap_sign, wald)
|
How the per-effect p-value is computed:
|
'bootstrap_sign'
|
seed
|
int
|
Random seed. |
42
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 300
>>> treat = rng.binomial(1, 0.5, n)
>>> x = rng.normal(0, 1, n)
>>> m = 0.4 * treat + 0.3 * x + rng.normal(0, 1, n)
>>> y = 0.5 * treat + 0.6 * m + 0.2 * x + rng.normal(0, 1, n)
>>> df = pd.DataFrame({"treat": treat, "x": x, "m": m, "y": y})
>>> res = sp.mediate_interventional(
... df, y="y", treat="treat", mediator="m", covariates=["x"],
... n_mc=100, n_boot=100, seed=0,
... )
>>> res.estimand
'IIE'
>>> res.method
'Interventional Mediation Analysis'
>>> len(res.detail["effect"]) # IIE, IDE, Total
3
Notes
Linear outcome model assumption. The current implementation hard-codes an OLS outcome regression Y ~ D + M + X_base + X_tv. This permits the Monte-Carlo integration to collapse analytically via the linearity of OLS predictions in the treatment-induced confounder block (X_tv), giving an O(n_mc + n) cost instead of O(n × n_mc). Non-linear outcome models (gradient boosting, neural nets, etc.) would break this vectorisation and are not currently supported — passing a custom learner is not exposed via the API.
References
VanderWeele, T.J., Vansteelandt, S. and Robins, J.M. (2014). "Effect decomposition in the presence of an exposure-induced mediator-outcome confounder." Epidemiology, 25(2), 300-306. [@vanderweele2014effect]
mediate_paths ¶
mediate_paths(data: DataFrame, y: str, treat: str, mediators: Sequence[Union[str, Sequence[str]]], covariates: Optional[List[str]] = None, interaction: bool = True, outcome_model: str = 'linear', ps: bool = False, decomposition: str = 'type1', n_boot: int = 500, alpha: float = 0.05, seed: int = 42) -> CausalResult
Path-specific effects of a binary treatment through ordered mediators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
y
|
str
|
Outcome. |
required |
treat
|
str
|
Binary treatment (0/1). |
required |
mediators
|
sequence
|
The mediators in causal order, |
required |
covariates
|
list of str
|
Pre-treatment confounders, entered linearly in every model. |
None
|
interaction
|
bool
|
Include treatment x mediator interactions in the outcome models. |
True
|
outcome_model
|
('linear', 'logistic')
|
Model for |
'linear'
|
ps
|
bool
|
Reweight with a logistic propensity score of the treatment on the covariates (the imputation-based weighting estimator) instead of the pure imputation estimator. |
False
|
decomposition
|
('type1', 'type2')
|
Which decomposition is the headline (both are in |
'type1'
|
n_boot
|
int
|
Nonparametric bootstrap replications (units resampled, every model refitted) for the standard errors and percentile intervals. |
500
|
alpha
|
float
|
|
0.05
|
seed
|
int
|
|
42
|
Returns:
| Type | Description |
|---|---|
CausalResult
|
|
Notes
Identification needs no unmeasured confounding of treatment-outcome, treatment-mediator and mediator-outcome relations given the covariates and earlier mediators, and no other post-treatment confounder of the mediators (an exposure-induced confounder has to be entered as an earlier mediator). The direct and path-specific effects always sum to the total effect.
References
[@zhou2023tracing]
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 2000
>>> a = rng.integers(0, 2, n)
>>> m1 = 0.6 * a + rng.normal(size=n)
>>> m2 = 0.5 * a + 0.7 * m1 + rng.normal(size=n)
>>> y = 0.4 * a + 0.5 * m1 + 0.8 * m2 + rng.normal(size=n)
>>> df = pd.DataFrame({"y": y, "a": a, "m1": m1, "m2": m2})
>>> r = sp.mediate_paths(df, y="y", treat="a", mediators=["m1", "m2"],
... interaction=False, n_boot=50)
>>> sorted(r.model_info["type1"])
['direct', 'via_M1', 'via_M2']
mediate_sensitivity ¶
mediate_sensitivity(data: DataFrame, y: str, treat: str, mediator: str, covariates: Optional[list] = None, rho_range: tuple = (-0.9, 0.9), n_grid: int = 41, *, conf_level: float = 0.95, eps: Optional[float] = None, max_iter: int = 10000) -> MediateSensitivityResult
Sensitivity analysis for causal mediation (linear models).
For each rho on a grid, the mediator model M ~ T + X and the
outcome model Y ~ T + M + X are re-estimated jointly with their
error correlation fixed at rho, and the ACME a_T * b_M is
reported with a delta-method confidence interval (Imai, Keele &
Yamamoto 2010, Section 5). rho = 0 is sequential ignorability.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Analysis data; rows with a missing value in a used column are dropped. |
required |
y
|
str
|
Outcome column (linear model). |
required |
treat
|
str
|
Treatment column. With a 0/1 treatment the ACME is the effect of moving from control to treatment; with a numeric treatment it is the effect per unit. |
required |
mediator
|
str
|
Mediator column (linear model). |
required |
covariates
|
list of str
|
Pre-treatment covariates entering both models. |
None
|
rho_range
|
tuple of float
|
|
(-0.9, 0.9)
|
n_grid
|
int
|
Number of equally spaced |
41
|
conf_level
|
float
|
Level of the normal-approximation interval at each |
0.95
|
eps
|
float
|
Stopping tolerance of the iterated FGLS: iteration stops once the
sum of squared changes of the coefficients is below |
None
|
max_iter
|
int
|
Maximum FGLS steps per |
10000
|
Returns:
| Type | Description |
|---|---|
MediateSensitivityResult
|
|
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> t = rng.binomial(1, 0.5, 400)
>>> m = 0.5 * t + rng.normal(size=400)
>>> df = pd.DataFrame({"t": t, "m": m,
... "y": t + 0.8 * m + rng.normal(size=400)})
>>> res = sp.mediate_sensitivity(df, y="y", treat="t", mediator="m",
... n_grid=19)
>>> float(round(res.rho_grid[0], 2)), res.rho_at_zero is not None
(-0.9, True)
References
[@imai2010identification], [@imai2010general]
Clean-room implementation; the reference (R mediation 4.5.1,
medsens, GPL >= 2) was used only as a black box.