Introduction
formative makes causal effect estimation more accessible. Causal estimation is full of statistical jargon that hinders adoption; formative’s goal is to lower that barrier. Attempting to do causal estimation is better than simply comparing averages or correlations.
Every analysis follows four steps.
1. Encode your causal assumptions as a DAG
Before any data is touched, you declare which variables are assumed to cause which. This makes your identification assumptions explicit and machine-readable. formative uses the DAG to determine which variables to control for (or how to use an instrument), and to check whether the data can support identification given those assumptions. Remember, the DAG is not a data model, but your assumptions about the data generating process. A partial DAG is better than no DAG.
from formative.causal import DAG
dag = DAG()
dag.assume("proximity").causes("education")
dag.assume("ability").causes("education", "income")
dag.assume("education").causes("income")
2. Choose an estimator
Pass the DAG to an estimator along with your treatment and outcome. The estimator reads the DAG to determine which variables to control for (or how to use an instrument), then fits the model. If the data cannot support identification given the DAG, an error is raised before estimation runs.
Choosing the right estimator for your causal question is crucial; not all methods are possible given your problem. See https://getformative.dev for an online wizard to help you choose.
from formative.causal import IV2SLS
result = IV2SLS(
dag,
treatment="education",
outcome="income",
instrument="proximity"
).fit(df)
After you have obtained the result object, you can print a summary of the estimate and its assumptions. The assumptions are marked as testable or untestable depending on whether formative can check them. Most assumptions in causal inference are untestable by nature — things you must argue for based on domain knowledge and theory.
print(result.summary())
IV (2SLS) Causal Effect: education → income
Instrument: proximity
──────────────────────────────────────────────────
IV estimate : 1.9643
Unadjusted estimate : 2.2598 (no controls)
Confounding bias : +0.2955
Std. error : 0.0377
95% CI : [1.8905, 2.0381]
p-value : 0.0000
Assumptions
┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
[ testable ] Relevance: the instrument strongly affects treatment
[ untestable ] Exclusion restriction: instrument only affects outcome through treatment
[ untestable ] Independence: instrument is uncorrelated with unobserved confounders
[ untestable ] Monotonicity: instrument affects treatment in same direction for everyone
[ untestable ] Stable Unit Treatment Value Assumption (SUTVA)
executive_summary() gives a plain-English version of the result, useful for
communicating with non-technical audiences.
3. Refute
Once you have a result, run statistical checks that probe whether its assumptions hold in the data. Each check returns a clear pass or fail — but no set of tests can guarantee validity. Use them as diagnostics, not proof. Causal inference is a judgment call, not a mathematical certainty.
report = result.refute(df)
print(report.summary())
IV Refutation Report: education → income
Instrument: proximity
──────────────────────────────────────────────────
[PASS] First-stage F-statistic: F = 911.22 (threshold: F ≥ 10)
[PASS] Random common cause: estimate shifted by 0.0001 (≤ 1 SE = 0.0377)
All checks passed.
4. Decide
An estimate is rarely the end goal — the question is whether to act on it. Pass the cost per unit of treatment applied and the value of one unit of improvement in the outcome, and formative turns the estimate and its uncertainty into a treat/don’t-treat call. Suppose a unit of education costs $8 and a unit of income is worth $15:
decision = result.decide(cost=8, benefit=15)
print(decision)
Decision Analysis: education → income
──────────────────────────────────────────────────
Cost per unit of treatment : 8.0000
Benefit per unit of outcome : 15.0000
Net benefit (point estimate) : +21.4647
Net benefit 95% CI : [+20.3574, +22.5720]
Optimal decision : treat
Decision confidence : 100.0%
Robust to estimation error : Yes — decision is stable across 95% CI
Depending on the estimator, decisions can go beyond a single call for everyone: per-group decisions via an effect modifier, or a learned treatment rule that decides whom to treat. See Decisions for the full decision layer.
What’s supported
Every estimator follows the same four steps — DAG in, fit(), refute(),
decide() — but they target different estimands and support different levels
of decision granularity:
Estimator |
Estimand |
Heterogeneous effects |
Decisions |
|---|---|---|---|
|
ATE |
|
decide for all, decide per group, learn a policy |
|
ATE |
|
decide for all, decide per group |
|
LATE |
— |
decide for all |
|
ATT |
— |
decide for all |
|
ATT |
— |
decide for all |
|
LATE at the cutoff |
— |
decide for all |
All estimators support summary(), executive_summary(), and refute(),
and every decision report can be handed to the game-theoretic rules in
formative.game via to_outcomes(). See Estimands for what the
estimands mean and Estimators for the estimators themselves.