statspai.question¶
question ¶
Estimand-first causal question DSL (sp.causal_question).
The article emphasizes "causal question precedes statistical model" as the common foundation of all three causal-inference schools: econometrics' identification, epidemiology's target trial protocol, and ML's estimand-aware learning.
This module lets a user declare a causal question in one place, then automatically:
- Identify the appropriate research design (IV / DiD / RD / backdoor).
- Suggest the right StatsPAI estimator.
- Run the analysis and attach diagnostics + sensitivity.
- Produce a reproducible Methods paragraph.
import statspai as sp df = sp.dgp_did(n_units=100, n_periods=10, seed=0) df["first_treat"] = df["first_treat"].fillna(0).astype(int) q = sp.causal_question( ... treatment="first_treat", # first treatment period, 0 = never ... outcome="y", ... estimand="ATT", ... design="did", ... data=df, ... time_structure="panel", ... time="time", ... id="unit", ... ) q.identify().estimator 'did' r = q.estimate() r.estimand 'ATT' print(q.report().splitlines()[0])
Causal Question¶
CausalQuestion
dataclass
¶
Pre-registered causal question declaration.
Fields map directly onto the Target Trial Protocol (Hernán 2016) and the "PICOTS + identification" rubric the article describes.
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 400
>>> d = (rng.uniform(size=n) < 0.5).astype(int)
>>> y = 1.0 + 0.5 * d + rng.normal(size=n)
>>> df = pd.DataFrame({"treat": d, "outcome": y})
>>> q = sp.causal_question("treat", "outcome", data=df, design="rct")
>>> isinstance(q, sp.CausalQuestion)
True
>>> q.identify().estimator
'regress'
save ¶
Save the question to a pre-registration file.
See :func:statspai.question.preregister.preregister for details.
load
classmethod
¶
Load a CausalQuestion from a preregistration file.
identify ¶
identify() -> IdentificationPlan
Choose an estimator based on the declared design / estimand.
estimate ¶
estimate(*, strict: bool = False, **kwargs: Any) -> EstimationResult
Execute the identification plan against self.data.
The declaration constrains the result: when the design cannot deliver
the declared estimand (the plan falls back, e.g. CATE -> ATE), the
result records it under deviations, a StatsPAIWarning is
raised, and strict=True refuses instead. Rows dropped from the
declared data and estimator keywords passed here are recorded too.
paper ¶
paper(*, fmt: str = 'markdown', output_path: Optional[str] = None, dag: Any = None, include_robustness: bool = True, cite: bool = True, reviewer_mode: bool = False) -> Any
Build a full :class:PaperDraft from this declared question.
Convenience wrapper around :func:statspai.paper_from_question.
Calls identify() and estimate() on demand, then assembles
a Question / Data / Identification / Estimator / Results /
Robustness / References draft. Renders to markdown by default;
pass fmt='qmd' for a Quarto document with statspai
provenance and an auto-appended Reproducibility appendix.
Examples:
>>> import os, tempfile
>>> import statspai as sp
>>> df = sp.dgp_did(n_units=100, n_periods=10, seed=0)
>>> df["first_treat"] = df["first_treat"].fillna(0).astype(int)
>>> q = sp.causal_question("first_treat", "y", data=df, design="did",
... estimand="ATT", time="time", id="unit")
>>> draft = q.paper(fmt='qmd')
>>> list(draft.sections)[:3]
['Question', 'Data', 'Identification']
>>> draft.write(os.path.join(tempfile.mkdtemp(), "paper.qmd"))
EstimationResult
dataclass
¶
Bases: ResultProtocolMixin
Unified view of a causal-question estimate.
Thin wrapper that preserves the underlying estimator's full result
object while exposing a canonical estimate / se / ci interface.
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 400
>>> d = (rng.uniform(size=n) < 0.5).astype(int)
>>> y = 1.0 + 0.5 * d + rng.normal(size=n)
>>> df = pd.DataFrame({"treat": d, "outcome": y})
>>> q = sp.causal_question("treat", "outcome", data=df, design="rct")
>>> res = q.estimate()
>>> isinstance(res, sp.EstimationResult)
True
>>> res.estimator
'regress'
>>> bool(res.ci[0] < res.estimate < res.ci[1])
True
IdentificationPlan
dataclass
¶
Output of :meth:CausalQuestion.identify.
Describes which estimator is planned, why it is identifying, and which assumptions the user must defend.
Examples:
>>> import statspai as sp
>>> q = sp.causal_question("treat", "outcome", design="rct")
>>> plan = q.identify()
>>> isinstance(plan, sp.IdentificationPlan)
True
>>> plan.estimator
'regress'
>>> bool("random assignment" in plan.assumptions)
True
load_preregister ¶
load_preregister(filename: Union[str, Path]) -> CausalQuestion
Load a pre-registration file back into a :class:CausalQuestion.
Deviations and metadata are preserved on the returned object via
the .notes field (concatenated).
Examples:
>>> import statspai as sp
>>> import os, tempfile
>>> q = sp.causal_question(treatment="policy", outcome="employment",
... estimand="ATT", design="did")
>>> path = os.path.join(tempfile.mkdtemp(), "pap.yaml")
>>> _ = sp.preregister(q, path)
>>> q2 = sp.load_preregister(path)
>>> q2.treatment
'policy'
causal_question ¶
causal_question(treatment: str, outcome: str, *, data: Optional[DataFrame] = None, population: str = '', estimand: str = 'ATE', design: str = 'auto', time_structure: str = 'cross_section', time: Optional[str] = None, id: Optional[str] = None, covariates: Optional[Sequence[str]] = None, instruments: Optional[Sequence[str]] = None, running_variable: Optional[str] = None, cutoff: Optional[float] = None, cohort: Optional[str] = None, notes: str = '', engine: str = 'ols') -> CausalQuestion
Declare a causal question (see :class:CausalQuestion).
Supported design values
Classical / quasi-experimental:
- 'rct' — randomised assignment; OLS ATE.
- 'iv' / 'natural_experiment' — 2SLS / LATE.
- 'regression_discontinuity' — local polynomial RD.
- 'did' / 'event_study' — difference-in-differences.
- 'synthetic_control' — convex-hull weighting.
- 'longitudinal_observational' — MSM / g-formula.
- 'selection_on_observables' — AIPW (default).
ML-based selection-on-observables (v1.13+):
- 'dml' — Double/debiased ML for ATE / LATE
[chernozhukov2018double].
- 'tmle' — Targeted Maximum Likelihood with Super Learner
[vanderlaan2006targeted].
- 'metalearner' — S/T/X/R/DR-Learner for tau(x);
population ATE summary via AIPW influence function
[kunzel2019metalearners; nie2021quasi].
- 'causal_forest' — honest random forest for tau(x)
[athey2019generalized; wager2018estimation]; population
ATE inference uses cross-fit AIPW
[vanderlaan2003unified; chernozhukov2018double].
All bib keys above resolve in paper.bib.
Examples:
>>> import numpy as np, pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 400
>>> d = (rng.uniform(size=n) < 0.5).astype(int)
>>> x = rng.normal(size=n)
>>> y = 1.0 + 0.5 * d + 0.3 * x + rng.normal(size=n)
>>> df = pd.DataFrame({"treat": d, "outcome": y, "x": x})
>>> q = sp.causal_question("treat", "outcome", data=df,
... design="rct", covariates=["x"])
>>> plan = q.identify()
>>> plan.estimator
'regress'
>>> res = q.estimate()
>>> res.estimand
'ATE'