chc.causal¶
Stability tier: evolving.
Confounded offline identification: why the control-effect residual needs the adjustment set.
A minimal linear demonstration of the CHC causal claim. The historical action
was chosen by a behaviour policy correlated with a covariate z that also drives the outcome.
Fitting the effect of u without adjusting for z is confounded (the estimate can flip sign);
conditioning the residual on the adjustment set recovers the true interventional effect.
Sequential-ignorability setting with history H = (x, z): with z in the adjustment set the
backdoor path u <- z -> x' is blocked and the effect is identified; omit z and it is not.
When z is latent, an instrument w identifies the effect via 2SLS; a Cinelli-Hazlett
robustness value bounds how much hidden confounding a control decision could tolerate. Double ML
recovers the effect under nonlinear confounding via cross-fitted residualisation.
ConfoundedLinearSystem
dataclass
¶
One-step transition x' = a·x + b_true·u + c·z + noise logged under u = kappa·z + eta.
z is an observed confounder: it drives both the historical action (via kappa) and the
outcome (via c). b_true is the causal effect we ultimately want for control.
The defaults are tuned so the naive (unadjusted) estimate of b flips sign.
sample(n, key)
¶
Draw n transitions as columns x, z, u, x_next, w (w = instrument).
estimate_control_effect(data, adjust_for=())
¶
Estimate ∂x_next/∂u from logged data, adjusting for the named covariates.
Regresses x_next on [x, u, *adjust_for]. With adjust_for=("z",) (the correct
adjustment set) the u coefficient is causal; with adjust_for=() it stays confounded.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
estimate_effect_iv(data, instrument='w')
¶
Two-stage least squares for ∂x_next/∂u using an instrument for a latent confounder.
Stage 1 regresses u on [x, instrument]; stage 2 regresses x_next on [x, û]. The
instrument must drive u, be independent of the confounder, and affect x_next only via
u — then the effect is recovered even when the confounder z is unobserved.
The log can check the first of the three, and the estimate does: the instrument must move
u beyond what x and a constant explain. The product of the two beyond them must be more
than 64 eps of each one's own size could make of it, read the same in any units and at any
level. Short of that, 2SLS's moment has no rank, û is collinear with x and the
constant, and least squares returned its minimum-norm coefficient: on 40,000 rows of
ConfoundedLinearSystem with gamma=1, where the truth is 1.0, -0.0032 for an
instrument of zeros and 0.63 for an affine copy of x where u moves with x. A weak
instrument keeps the rank and is estimated: a column of noise drawn apart from u read 0.263
there, its partial correlation with u given x 0.0087 where the log's own instrument's
is 0.53 (chc.estimators.IV2SLS reports it). No inference here is robust to a weak
instrument.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
sensitivity_analysis(data, adjust_for=(), q=1.0)
¶
Effect estimate plus its Cinelli-Hazlett robustness value.
The robustness value is the R^2 an unobserved confounder would need with both u and
x_next to reduce the estimated effect by q*100% (toward zero). High RV = robust;
a controller can ship this bound on how much hidden confounding its decision could tolerate.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
e_value(standardized_effect, std_error=None)
¶
VanderWeele-Ding E-value: the confounding strength needed to explain the effect away.
The E-value is the minimum association (on the risk-ratio scale) an unmeasured confounder would
need with both treatment and outcome, beyond the measured covariates, to reduce the estimate
to the null. Larger = more robust. For a standardised (Cohen's d-scale) effect it maps
d -> RR = exp(0.91 * |d|) (VanderWeele & Ding, 2017) then E = RR + sqrt(RR (RR - 1)).
std_error (same standardised scale) adds e_value_ci for the 95% confidence limit nearest
the null -- 1.0 when the interval covers the null, i.e. no confounding need be invoked.
estimate_effect_dml(data, covariates=('x', 'z'), degree=3, folds=2, ridge=0.01, seed=0)
¶
Double / debiased ML estimate of ∂x_next/∂u via cross-fitted residual-on-residual.
Partials flexible (polynomial-ridge) predictions of x_next and u out of the covariates,
then regresses the residuals. This is Neyman-orthogonal, so it recovers the effect even under
nonlinear confounding, where the linear estimate_control_effect adjustment is biased.
The covariates are centred and scaled before the polynomial basis is built, and the intercept
takes no ridge, so ridge reads the same in any units of the covariates and at any level of
them, of the action and of the outcome.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
dml_point_and_se(data, covariates=('x', 'z'), degree=3, folds=2, ridge=0.01, seed=0)
¶
DML point estimate theta and its heteroskedastic influence-function standard error.
For the partially-linear score psi = u_res*(y_res - theta*u_res) the estimator is
theta = <u_res, y_res>/<u_res, u_res> and the (robust, HC-style) SE is
sqrt(sum u_res^2 * eps^2) / sum u_res^2 with eps = y_res - theta*u_res -- the sandwich
variance of the Neyman-orthogonal moment. Ships EconML-grade uncertainty with the cross-fit
effect (a 95% CI is theta +/- 1.96*se). The nuisances are those of
estimate_effect_dml, so ridge reads the same in any units of the covariates and at
any level of them, of the action and of the outcome.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
refute_effect(data, adjust_for=('z',), subset_fraction=0.5, seed=0)
¶
DoWhy-style refutation tests for the adjusted effect estimate (a robustness gate).
- placebo: permute the treatment — the effect must collapse toward 0 (else it is spurious);
- random common cause: add an irrelevant covariate — the effect must stay stable;
- subset: re-estimate on a random subsample — the effect must stay stable.
Returns the estimates and passes (placebo near 0, the others near the original).