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
95% Wald · off by +0.726 — and the interval does not contain the truth
Back-door adjusted, conditioning on
Z
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.
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.
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
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
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
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.
| benchmark | R² with treatment | R² with outcome | bias | adjusted | 95% 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.
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.