加载中…
加载中…
Causal discovery for tabular data. In one forward pass over a whole table (rows x
variables), Axiom labels every pair of variables causes (a -> b), caused_by (b -> a),
undirected (linked, direction not identifiable), confounded (no edge, hidden common
cause) or none, with probabilities, and assembles the result into an acyclic graph.
Missing values and rows from known experiments are supported.
Treat the graph as hypotheses to check against domain knowledge: uncertain pairs are candidates for experiments; confident ones can feed causal effect identification and estimation.
3.8M parameters, fp32 safetensors (15 MB). Project page: maxpagels.com/axiom.
| recommendation | |
|---|---|
| rows | 256 or more works best; usable from ~100; below that, results get unreliable. Over 512 rows, discover averages 8 random 512-row subsets (raise n_subsets to use more of a big table). |
| variables | trained on 3-25. On simulated data it held up to 100 with no loss in accuracy; beyond 25 on real data is untested. Memory grows quickly with the variable count (~1 GB on CPU at 100, ~3 GB at 200). |
| interventions | if any rows come from experiments, label all of them: unlabeled experimental rows act like hidden causes and can produce wrong or reversed edges. |
| output | treat edges and especially directions as ranked hypotheses to check, not a verdict (see Limitations). |
pip install "git+https://huggingface.co/maxpagels/axiom-base"
from axiom import Axiom
model = Axiom.from_pretrained("maxpagels/axiom-base")
g = model.discover(df) # pandas DataFrame or (n, d) array; NaN = missing
print(g) # the relations found, strongest first
# rows from known experiments: one entry per row (None = observational),
# or an (n, d) bool mask
targets = [None, "PKC", ["PKA", "PIP3"], None, ...] # column(s) each row's experiment targeted
g = model.discover(df, interventions=targets)
discover standardizes each column as in training (do not pass raw model inputs
yourself unless you use axiom.preprocess). Tables with more than 512 rows are split into
random 512-row subsets whose predictions are averaged.
For every pair of variables a and b, Axiom gives a Relation(a, b, kind, p, probs):
kind: Axiom's conclusion about how a and b are causally linked, one of the five below.p: the model's probability for that kind.probs: the probabilities of all five kinds (they sum to 1); kind is the decision,
probs the evidence behind it.| kind | meaning | example |
|---|---|---|
causes |
a directly causes b (a -> b) | smoking -> tar in lungs |
caused_by |
b directly causes a (b -> a) | the same pair, read from the other side |
undirected |
a and b are directly linked, but the data cannot tell which way | linear-Gaussian data, where both directions fit equally well |
confounded |
no direct link, but an unmeasured variable causes both | ice cream sales and drownings (hidden cause: hot weather) |
none |
no direct link |
A kind is always read from the first variable: g.relation("PKA", "PKC").kind == "causes" and
g.relation("PKC", "PKA").kind == "caused_by" describe the same edge.
Only direct links count. In a -> b -> c, the pair a, c is none, even though a affects c,
because that effect passes through b. Drop b from the table and a -> c becomes a direct
link: what counts as direct depends on which variables you include.
g.relations # [Relation(a, b, kind, p, probs), ...]: the relations found,
# strongest first; directed ones read cause first
g.edges # [(cause, effect, p), ...]: directed edges only
[r for r in g.relations if r.kind == "confounded"] # one kind only: here, the pairs
# likely to share a hidden cause
g.relation("PKA", "PKC") # any one pair, also "none" ones: Relation(a='PKA', b='PKC',
# kind='causes', p=0.85, probs={'none': 0.0, 'causes': 0.85, ...})
g.to_frame() # the relations found as a pandas DataFrame: a, b, kind, p, p_<kind>...
g.to_frame(all_pairs=True) # every pair, plus `top`, the most probable kind
A pair only gets a kind other than none if its probability clears thresh (default 0.5;
lower it with e.g. model.discover(df, thresh=0.3)). The raw probabilities and 0/1
matrices behind all this are on g too; see help(g).
Trained only on synthetic data: every step simulates fresh random structural causal models (no dataset is seen twice).
Held-out synthetic data: fixed datasets simulated from the training distribution but never trained on, 256 rows each.
| graphs | edge AUROC | skeleton F1 | orientation accuracy | confounded-pair F1 |
|---|---|---|---|---|
| all (4-25 variables) | 0.93 | 0.75 | 0.92 | 0.28 |
| small (4-7 variables) | 0.93 | 0.76 | 0.82 | 0.10 |
| medium (10-16 variables) | 0.91 | 0.76 | 0.92 | 0.30 |
| large (19-25 variables) | 0.93 | 0.75 | 0.93 | 0.31 |
Datasets with a known causal graph; every method sees the same 500 standardized rows per draw, default settings, mean over 5 draws.
Edge detection (AUROC, 0.5 = random):
| method | Sachs | Sachs+interventions | Chamber | Chamber+interventions |
|---|---|---|---|---|
| Axiom | 0.73 | 0.74 | 0.78 | 0.77 |
| AVICI | 0.78 | 0.81 | 0.72 | 0.80 |
| PC | 0.57 | — | 0.55 | — |
| GES | 0.61 | — | 0.69 | — |
| DirectLiNGAM | 0.67 | — | 0.77 | — |
| partial corr. | 0.64 | — | 0.73 | — |
| correlation | 0.60 | — | 0.72 | — |
Direction (% of identifiable true edges pointed the right way, 50% = coin flip):
| method | Sachs | Sachs+interventions | Chamber | Chamber+interventions |
|---|---|---|---|---|
| Axiom | 30% | 37% | 70% | 97% |
| AVICI | 47% | 48% | 41% | 100% |
| PC | 50% | — | 51% | — |
| GES | 52% | — | 73% | — |
| DirectLiNGAM | 64% | — | 68% | — |
| partial corr. | 50% | — | 50% | — |
| correlation | 50% | — | 50% | — |
Standard simulated benchmarks from the literature, 5 random graphs each.
Direction (% of identifiable true edges pointed the right way, 50% = coin flip):
| method | LinGauss | LiNGAM | GP-add | MLP-SF |
|---|---|---|---|---|
| Axiom | 77% | 88% | 97% | 85% |
| AVICI | 81% | 58% | 80% | 84% |
| PC | 54% | 50% | 61% | 48% |
| GES | 49% | 46% | 56% | 69% |
| DirectLiNGAM | 43% | 97% | 42% | 37% |
| partial corr. | 50% | 50% | 50% | 50% |
| correlation | 50% | 50% | 50% | 50% |
The synthetic benchmarks pooled, under common data problems. Missing values are mean-imputed for every method except Axiom, which takes them as given.
Direction (% of identifiable true edges pointed the right way, 50% = coin flip):
| method | clean | missing | outliers | meas.noise | n=100 |
|---|---|---|---|---|---|
| Axiom | 87% | 84% | 83% | 86% | 81% |
| AVICI | 76% | 56% | 62% | 64% | 61% |
| PC | 54% | 54% | 52% | 56% | 53% |
| GES | 55% | 62% | 53% | 62% | 54% |
| DirectLiNGAM | 55% | 49% | 49% | 53% | 61% |
| partial corr. | 50% | 50% | 50% | 50% | 50% |
| correlation | 50% | 50% | 50% | 50% | 50% |
confounded output as a hint.