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

RCT

ATE

effect_modifier

decide for all, decide per group, learn a policy

OLSObservational

ATE

effect_modifier

decide for all, decide per group

IV2SLS

LATE

decide for all

PropensityScoreMatching

ATT

decide for all

DiD

ATT

decide for all

RDD

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.