axiom

Pillar 01 · axiom.identify · axiom.diagnose

Can the data answer it?

Every estimator returns a number. The question that decides whether the number means anything comes first: given how the world is wired, is this effect recoverable from what you measured? axiom answers that from the graph, before any data is touched — and when the answer is no, it says which variable is missing rather than returning a number anyway.

The good case

A route exists, so take it

A treatment X, an outcome Y, and a common cause Z that drives both. The graph is X -> Y, Z -> X, Z -> Y, and everything in it is measured. axiom searches for a licensed route and finds the back door.

Three estimates of the same effect

Same data, same 5,000 rows. The only difference is whether the confounder was conditioned on.

axiom.identify.ols(frame, "Y", "X", covariates=…) · n = 5,000 · 95% Wald · seed 0

Naive estimate, ignoring Z

2.726
2.693 2.758

95% Wald · off by +0.726 — and the interval does not contain the truth

Back-door adjusted, conditioning on Z

2.013
1.985 2.040

95% Wald · off by +0.013 against a truth of 2.0

The bias does not shrink with n

Collecting ten times the data would tighten the naive interval around 2.73, not move it toward 2.0. Confounding is not noise, and no sample size fixes it.

What the verdict carries

A verdict is not a boolean. It names the route, the set to condition on, and every assumption that route rests on — each one with a state, so you can see what has actually been checked and what is merely being assumed.

status identified via the backdoor route licensed
adjust for Z — the minimal sufficient set. axiom also enumerates the alternatives: ['Z'] 1 set
assumptions none beyond the graph. With every node measured and a back door available, there is nothing further to assume — so axiom lists nothing rather than padding the verdict. The graph itself is still your claim to defend, and it is hashed into the verdict. 0 lines

The honest case

No route exists, and axiom says so

Now the same structure with Z never measured — the observed columns are X, Y. There is nothing to condition on. A library that hands back a coefficient here is not lying about the arithmetic; it is answering a different question than the one you asked.

status downgraded — route backdoor_unmeasured not licensed
needs Z, which is not in your data unmeasured
if you fit anyway the regression returns 2.726 against a truth of 2.0 +0.726
no route at all for X -> Y, X <-> Y the verdict is blocked: “no back-door, front-door, or instrumental route exists in the graph” blocked

Being told no is the feature

Three outcomes, three different things to do next. Identified means adjust and proceed. Downgraded means the route exists on paper but a variable it needs is missing — so either go and measure it, find an instrument, or bound the damage with a sensitivity analysis. Blocked means no observational route exists in this graph at all, and the only way forward is to intervene: run the experiment.

That last branch is the bridge to the next pillar. When identification fails, the answer is not a cleverer estimator — it is a design.

The rescue

An instrument puts it back in reach

If something shifts the treatment without touching the outcome except through it, the effect becomes recoverable again. axiom finds the instrument in the graph, runs two-stage least squares, and checks the relevance condition rather than assuming it.

OLS versus 2SLS on a confounded treatment

The instrument is Z, found by axiom from the graph — not nominated by hand.

axiom.identify.two_stage_least_squares(frame, "Y", "X", instruments=[Z]) · n = 5,000 · seed 1

route axiom picked instrument from the graph structure downgraded
relevance first-stage F of 2,215 — checked, not assumed. A weak instrument is its own bias. satisfied
recovery 2SLS lands -0.001 from the truth where OLS is off by +0.347 1.999

Exclusion — the assumption that the instrument affects the outcome only through the treatment — is not testable from data, and axiom does not pretend otherwise. It goes into the verdict as an unverified assumption, which is where a reviewer can find it.

When you cannot fix it

How strong would the confounder have to be?

Sometimes there is no instrument and no way to measure the missing variable. The remaining honest move is to say how bad the problem would have to be to change your mind — and then let a reader judge whether a confounder that strong is plausible in their setting.

The estimate under every strength of confounder

Both axes are partial R², the share of variance an unobserved confounder would explain. Darker is a larger surviving estimate.

axiom.diagnose.bias_bounds(estimate, se, df, r2_yz_dx, r2_dz_x, mass=0.95) — Cinelli–Hazlett bounds over a 21×21 grid

Robustness value

86.2%

A confounder would have to explain 86.2% of the residual variance in both the treatment and the outcome to wipe the effect out entirely. That is an enormous confounder — which is the point of quoting the number.

To produce the bias that is actually there

45.6%

The real confounding in this simulation moves the estimate by +0.726. A confounder explaining 45.6% of the residual variance in each would do that much damage.

Bounding the damage at a stated confounder strength

Pick a benchmark — “no worse than the confounder I already measured”, say — and axiom returns the adjusted estimate and interval under exactly that assumption. Here the benchmark is the confounder the simulation really has, and two weaker versions of it.

Adjusted estimate under a hypothetical confounder of each strength. Truth is 2.0; the unadjusted estimate is 2.726.
benchmarkR² with treatmentR² with outcome biasadjusted95% interval
the confounder that is actually there 0.390 0.578 0.715 2.011 [1.984, 2.038]
half as strong 0.195 0.289 0.311 2.415 [2.384, 2.445]
a quarter as strong 0.098 0.145 0.147 2.579 [2.547, 2.610]

The first row lands on 2.011 against a truth of 2.0 — the benchmark recovers the right answer, because in a simulation we know exactly what the confounder is. In real work you do not, which is why the output is a curve over strengths rather than a single corrected number.

Tipping point

Given a decision threshold of 1.0, axiom also reports where the decision flips. At zero bias the call is True with probability 1.00; it only reverses once bias reaches 1.8. The confounding actually present moves it by 0.726 — so on this decision, at this threshold, the confounder is not large enough to matter.

What is in axiom.identify

65 public symbols. Graph construction and parsing, role assignment, all three route families, adjustment-set enumeration, transport verdicts across populations, and the linear estimators that go with each route.

Browse every symbol →

Next: what to measure

Identification tells you whether the data you have can answer the question. When it cannot — or when the interval is too wide to act on — the next question is what experiment would fix that, at what size, and whether it is worth running at all.

Go to Design →