Skip to content

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 adjust_for names u or x_next, the action and the outcome.

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 instrument is x, u or x_next: the state reaches x_next on its own path, and the action and the outcome are what it stands between; or if it does not move u beyond what x explains, which leaves the effect not identified.

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 adjust_for names x, u or x_next, or a column twice. The standard error inverts the design's Gram matrix, which a column read twice makes singular: the error, the robustness value and the interval's E-value came out nan, nan and 1.

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 covariates names u or x_next, the action and the outcome.

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 covariates names u or x_next, the action and the outcome.

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).