causasv — Causal Feature Attribution via Asymmetric Shapley Values
causasv computes Asymmetric Shapley Values (ASV) for causal feature attribution over user-supplied DAGs. It is a Rust-first engine with Python bindings, designed for XAI workflows where feature importance should respect known causal structure.
What is ASV?
Asymmetric Shapley Values (ASV) generalize Shapley values by averaging only over topologically valid orderings of features, rather than all permutations. Given a causal DAG G and a value function v:
φ_i = (1 / |Π(G)|) Σ_{π ∈ Π(G)} [v(pre(i,π) ∪ {i}) − v(pre(i,π))]
where Π(G) is the set of all linear extensions (topological orderings) of G, and pre(i,π) is the set of features appearing before feature i in ordering π.
How ASV differs from SHAP
Standard SHAP averages over all n! feature permutations, ignoring causal structure. ASV restricts the average to permutations consistent with the causal DAG — causes always appear before their effects. This produces attributions that respect the direction of causality.
Why causal DAGs matter
When features have causal relationships, SHAP can assign attribution to a variable for effects that are actually mediated by its descendants. ASV prevents this by constraining which orderings are considered valid.
Rust example
use ;
Python example
# From a list of edges
=
# Or from a networkx DiGraph
# import networkx as nx; G = nx.DiGraph(); G.add_edge(...)
# dag = CausalDAG.from_networkx(G)
=
=
# values: dict[str, float] mapping feature name → ASV value
The Python value_fn receives a sorted list of feature names present in the coalition and must return a float.
Use explain_with_diagnostics() to get ESS, seed, and method alongside values:
=
# dict[str, float]
# float — ESS ≈ n_samples means reliable; ESS ≪ n_samples means high variance
# float — ESS / n_samples ∈ (0, 1]; close to 1 is good
# int
# int | None
# bool
# str — the method name passed in (e.g. "approx")
Use explain_adaptive() for automatic convergence detection and per-feature confidence intervals:
=
# dict[str, float]
# dict[str, float] — IS standard error per feature
# dict[str, float] — lower bound of 95% confidence interval
# dict[str, float] — upper bound of 95% confidence interval
# bool — True if rel_tol was reached before max_samples
# float — ESS / n_samples; close to 1 is good
For batched coalitions (reduced Python GIL overhead for large models), pass value_fn_batch or call explain_adaptive_batch():
# value_fn_batch receives list[list[str]] and must return list[float]
=
# Adaptive variant with batched evaluation
=
For deterministic parallel approximation, pass parallel=True with a seed:
=
# True when seed + parallel
Use explain_stability() to verify that approximate rankings are consistent across seeds:
=
# mean pairwise Kendall tau; 1.0 = perfectly stable
# dict[str, float] — small means stable estimates
# dict[str, float] — mean ASV across seeds
Use ASVEnsembleExplainer to measure sensitivity across multiple candidate DAGs:
=
=
=
=
# dict[str, float] — mean ASV across DAGs
# dict[str, float] — std across DAGs; 0 = DAG-invariant
# float — mean pairwise Kendall tau across DAG pairs
# list[dict[str, float]] — one dict per DAG
Inspect and export the DAG:
# ["education", "income", "risk_score"]
# [("education", "income"), ("income", "risk_score")]
# 'digraph {\n education -> income;\n ...\n}'
# '{"nodes":[...],"edges":[...]}'
# ["education", "income"]
# ["income", "risk_score"]
# [["education"], ["income"], ["risk_score"]]
# Restore a DAG from JSON
=
# Convert to networkx (networkx must be installed separately)
=
Use TabularExplainer for a higher-level API with sklearn-compatible models (requires numpy):
=
=
=
# values: dict[str, float] mapping feature name → ASV value
Or build the value function directly with make_tabular_value_fn for full control:
=
=
Exact vs Approximate
| Method | When to use | API |
|---|---|---|
exact |
Small DAGs (n ≤ ~8); enumerates all linear extensions | explainer.exact(value_fn) |
exact_tree |
Rooted directed trees; order-ideal DP | explainer.exact_tree(value_fn) |
exact_dag |
General DAGs, n ≤ 20; dense order-ideal DP | explainer.exact_dag(value_fn) |
exact_dag_sparse |
Sparse DAGs, n ≤ 28; BFS over valid order ideals only | explainer.exact_dag_sparse(value_fn) |
approx |
Any DAG (n > 28 or memory limit exceeded); IS sampling | explainer.approximate(value_fn, SamplingConfig::new(n)) |
auto dispatch: n ≤ 8 → exact; rooted tree → exact_tree; n ≤ 20 → exact_dag; 20 < n ≤ 28 → exact_dag_sparse; else → approx.
exact_dag_sparse visits only valid order ideals (sets where every node's parents are also present). For sparse DAGs (chains, trees, few branching points), this can be orders of magnitude fewer states than 2^n. Returns n_order_ideals, state_ratio, and memory_mb diagnostics.
The approximate estimator uses self-normalized importance sampling to correct for the bias introduced by the frontier sampler, so the efficiency axiom (Σφ_i = v(V) − v(∅)) holds exactly even for approximate results.
The result includes effective_sample_size (ESS = (Σw)² / Σw²): ESS ≈ n_samples means IS weights are uniform and the estimate is reliable; ESS ≪ n_samples indicates high weight variance.
Status
Experimental — v0.8.1. Public API may change before v1.0.
Algorithm status
| Method | Implementation | Notes |
|---|---|---|
exact |
Enumerates all linear extensions | Reference oracle; practical for n ≤ ~8 |
exact_tree |
Rooted tree validation + order-ideal DP | Efficient for trees; hook-length formula |
exact_dag |
Order-ideal DP over 2^n states | General DAGs, n ≤ 20; O(2^n × n) |
exact_dag_sparse |
BFS over valid order ideals + lazy dp_ind | Sparse DAGs, n ≤ 28; memory-bounded |
approx |
Self-normalized IS over topological orderings | Any DAG; corrects frontier-sampler bias |
The brute-force exact implementation is used as the reference oracle in tests for all other methods.
The exact_tree DP enumerates valid pre-sets via order ideals and weights each by the hook-length formula, avoiding explicit enumeration of all linear extensions. Caterpillar trees of depth 30 see orders-of-magnitude speedups over brute-force.
The exact_dag DP computes two tables over all 2^n bitmasks: dp_fwd[S] (orderings of valid order ideals S) and dp_ind[T] (linear extensions of any induced subgraph G[T]). The ASV for each node i accumulates dp_fwd[S] × dp_ind[V\(S∪{i})] × (v(S∪{i}) − v(S)) over all valid transitions. This is the order-ideal DP generalized from trees to arbitrary DAGs.
Feature matrix
| Feature | Rust | Python | Status |
|---|---|---|---|
| Exact ASV (brute-force) | ✓ | ✓ | Stable |
| Rooted-tree exact DP | ✓ | ✓ | Experimental |
| General DAG exact DP (n ≤ 20) | ✓ | ✓ | Experimental |
| Sparse exact DAG DP (n ≤ 28) | ✓ | ✓ | Experimental |
| Approximate ASV with ESS | ✓ | ✓ | Experimental |
| Adaptive approximation + CI | ✓ | ✓ | Experimental |
| Seeded deterministic parallel approx | ✓ | ✓ | Experimental |
| Batched coalition evaluation | ✓ | ✓ | Experimental |
| sklearn / NumPy helper (TabularExplainer) | — | ✓ | Experimental |
| DAG ensemble / sensitivity ASV | — | ✓ | Experimental |
| DAG structural inspection | — | ✓ | Experimental |
| Graph export (DOT / JSON / networkx) | — | ✓ | Experimental |
Paper correspondence
Beyond Shapley: Efficient Computation of Asymmetric Shapley Values
| Algorithm component | causasv |
|---|---|
| ASV definition | ✓ exact (brute-force oracle) |
| Rooted tree exact algorithm | ✓ exact_tree (order-ideal DP + hook-length formula) |
| General DAG exact DP | ✓ exact_dag (order-ideal DP, n ≤ 20) |
| Importance-sampling approximation for general DAGs | ✓ approx |
| Sparse exact DAG DP | ✓ exact_dag_sparse (BFS over order ideals, n ≤ 28) |
| Causal discovery | — out of scope |
exact_treeimplements the order-ideal enumeration + hook-length weighting for rooted directed trees.exact_dagimplements the two-table order-ideal DP for general DAGs (n ≤ 20).approximplements importance-weighted topological ordering sampling for any DAG.exactis a brute-force baseline oracle used as a correctness reference in tests.
Performance
Selected results on Apple M-series (arm64, release build), v(S) = |S|. See docs/benchmarks.md for full tables.
| DAG | n | Method | Time |
|---|---|---|---|
| Chain | 7 | exact (brute-force) |
2.7 µs |
| Balanced tree | 15 | exact_tree (DP) |
2.8 ms |
| Caterpillar | 10 | exact_tree (DP) |
170 µs |
| Chain | 16 | exact_dag (dense DP) |
5.3 ms |
| Chain | 24 | exact_dag_sparse |
15 µs |
| Two parallel chains | 20 | exact_dag (dense, 1M states) |
87.9 ms |
| Two parallel chains | 20 | exact_dag_sparse (121 states) |
91 µs (~1000×) |
| Chain | 10 | approx (1k samples) |
916 µs |
| Chain | 20 | approx serial seeded (10k) |
18.2 ms |
| Chain | 20 | approx parallel 4t seeded (10k) |
7.4 ms |
Run cargo bench to reproduce. HTML reports saved to target/criterion/.
Current limitations
- Brute-force exact ASV is exponential in the number of linear extensions; only practical for n ≤ ~8 nodes.
exact_treerequires a rooted directed tree (single root, all other nodes have in-degree 1). For general DAGs with n ≤ 20, useexact_dag. For sparse DAGs with n ≤ 28, useexact_dag_sparse. For larger DAGs, useapprox.- Python bindings provide
nodes(),edges(),to_dot(), andmake_tabular_value_fn; graph-level DOT export works but Rust-side export is not yet implemented. - No built-in causal discovery, model training, or automatic graph construction.
Compared to other tools
causasv is not a SHAP replacement or a general-purpose explainability framework.
It solves one narrow problem:
Computing Asymmetric Shapley Values over a user-supplied causal DAG.
| Tool | Focus | ASV / causal DAG |
|---|---|---|
| SHAP | General-purpose Shapley / SHAP | No — standard Shapley only |
| Captum | PyTorch model interpretability | No |
| shapr | Conditional / causal Shapley (R + Python) | Yes — broader scope, R-first |
| shapflex | ASV with causal knowledge (Python alpha) | Yes — similar concept |
| causasv | ASV over user-supplied causal DAGs | Core focus |
The main differences from shapr and shapflex: causasv is a Rust-first engine
that requires the user to supply an explicit causal DAG and a value function.
It does not perform causal discovery and does not depend on the data distribution.
Scope
- Does compute ASV (causal feature attribution) given a DAG and a value function
- Does not do causal discovery, model training, or feature selection
- Not a SHAP replacement — ASV and SHAP answer different questions
Building Python bindings
&&
Citation
Fryer, D., Strümke, I., & Nguyen, H. (2021). Shapley values for feature selection: The good, the bad, and the axioms. IEEE Access.
For the asymmetric formulation and efficient tree computation, see the paper that inspired this library:
Beyond Shapley: Efficient Computation of Asymmetric Shapley Values
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.