Measurement-Mechanistic RCA
Python package for measurement-mechanistic root cause analysis from the NeurIPS paper Root Cause Analysis of Measurement and Mechanistic Anomalies.
The default installation uses Python spline expectile regression and requires no R.
Repository, executable notebook examples, and the full replication package are available at github.com/hsuhr99/measurement_mechanistic_rca.
Installation
Install the method package:
pip install measurement-mechanistic-rca
Install the optional NoGAM causal-discovery helper:
pip install "measurement-mechanistic-rca[causal-discovery]"
Install the optional R/qgam comparison backend only if R, qgam, mgcv, and rpy2 are available:
pip install "measurement-mechanistic-rca[qgam]"
Running Example: Same Root Cause, Different Anomaly Types
Consider the causal graph W <- X -> Y -> Z. We generate 500 observations
using Y = 0.8 X + noise, Z = 1.2 Y + noise, and W = -X + noise,
with independent Gaussian noise. Columns follow the node indices [X, Y, Z, W].
We inject two anomalies at Y:
- Row 0 — measurement: add 4 to the recorded Y after generating Z. The physical process is unchanged, so Z does not respond to the recording error.
- Row 1 — mechanistic: add 4 to Y before generating Z. The shift propagates to Z through its usual mechanism.
Thus both rows violate X -> Y, but only the measurement anomaly also breaks
Y -> Z. Neither changes X -> W.
1. Generate the synthetic data
import networkx as nx
import numpy as np
from measurement_mechanistic_rca import MeasurementMechanisticRCA
# Column indices: X=0, Y=1, Z=2, W=3.
names = ["X", "Y", "Z", "W"]
dag = nx.DiGraph([(0, 1), (1, 2), (0, 3)])
def make_data(n, seed, contaminated=False):
rng = np.random.default_rng(seed)
x = rng.normal(size=n)
y = 0.8 * x + rng.normal(scale=0.15, size=n)
if contaminated:
y[1] += 4.0 # Mechanism shift at Y, BEFORE generating its child Z.
z = 1.2 * y + rng.normal(scale=0.15, size=n)
w = -x + rng.normal(scale=0.15, size=n)
if contaminated:
y[0] += 4.0 # Measurement error at Y, AFTER generating Z.
return np.column_stack([x, y, z, w])
data = make_data(n=500, seed=7, contaminated=True)
Gray points show normal observations, blue the measurement anomaly, and orange the mechanistic anomaly.
2. Localize and classify the two anomalies
Use fit_explain(data, dag) to learn the mechanisms and explain the contaminated
data in one step.
np.random.seed(7)
result = MeasurementMechanisticRCA().fit_explain(data, dag)
def show_results(result):
for row in (0, 1):
cause = result.top_features(sample_idx=row, k=1)[0]
print(f"row {row}: root cause {names[cause.feature]}, type {cause.anomaly_type}")
show_results(result)
Expected output:
row 0: root cause Y, type measurement
row 1: root cause Y, type mechanistic
Both anomalies are correctly localized at Y and classified by type. The unusual Z value in row 1 is a downstream effect of the mechanism shift at Y.
To inspect the highest-ranked cause for a sample:
cause = result.top_features(sample_idx=0, k=1)[0]
print(names[cause.feature], cause.anomaly_type) # Y measurement
The result also includes the root-cause score and conditional confidence in its type:
print(cause.score)
print(cause.conditional_confidence)
Conditional confidence compares the competing type explanations while keeping
other assignments fixed. It is a cost difference, not a probability. Types are
measurement, mechanistic, uncertain when the origin is unresolved, or
both when inference with priors selects both types.
To inspect the features assigned to each anomaly type:
measurement_features = result.assignment[0]["measurement"]
mechanistic_features = result.assignment[1]["mechanistic"]
print([names[j] for j in measurement_features]) # ['Y']
print([names[j] for j in mechanistic_features]) # ['Y']
3. Train on clean data and explain future samples
Use fit to learn the normal mechanisms from clean data, then explain for
new samples. This requires prior probabilities for measurement and mechanistic
anomalies, since clean data does not reveal how often future interventions occur.
Here we set each probability to 1% per feature.
clean_data = make_data(n=500, seed=8)
new_samples = make_data(n=100, seed=9, contaminated=True)
measurement_priors = np.full(4, 0.01)
mechanistic_priors = np.full(4, 0.01)
np.random.seed(7)
model = MeasurementMechanisticRCA().fit(clean_data, dag)
future_result = model.explain(
new_samples,
measurement_priors=measurement_priors,
mechanistic_priors=mechanistic_priors,
)
show_results(future_result)
This identifies Y as the root cause for both new anomalies and returns the same
types as above. The learned mechanisms remain fixed during explain.
Optional: supplied priors for contaminated data
The same priors can also be passed to fit_explain:
np.random.seed(7)
result_with_priors = MeasurementMechanisticRCA().fit_explain(
data,
dag,
measurement_priors=measurement_priors,
mechanistic_priors=mechanistic_priors,
)
show_results(result_with_priors)
Priors can have shape (d,) for one probability per feature or (n, d) for
sample-specific probabilities. With supplied priors, the default single_root
mode assumes at most one root-cause feature per sample; prior_mode="exhaustive"
allows multiple root causes in small graphs.
Run the full example in the notebook or with python examples/running_example.py from a repository checkout.
Causal Discovery
If the causal graph is unknown, the optional NoGAM helper can learn a DAG before RCA:
from measurement_mechanistic_rca import MeasurementMechanisticRCA, discover_dag_nogam
learned_dag = discover_dag_nogam(clean_data, n_crossval=3)
result = MeasurementMechanisticRCA().fit_explain(data, learned_dag)
NoGAM is bundled through the optional causal-discovery extra and has no R dependency.
Replication Package
For the paper experiments and Docker setup, see the replication instructions.
Citation
@inproceedings{suhr:2026:rca,
title={Root Cause Analysis of Measurement and Mechanistic Anomalies},
author={Hendrik Suhr and David Kaltenpoth and Jilles Vreeken},
booktitle={Advances in Neural Information Processing Systems},
volume={39},
year={2026},
url={https://openreview.net/forum?id=2sxylzR1R2}
}
License
Original code developed for this package is released under CC BY 4.0. Bundled or adapted external code remains under the licenses of the respective upstream projects; see the source distribution's third-party notices.
Metadata
Release files for measurement-mechanistic-rca 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| measurement_mechanistic_rca-0.1.2.tar.gz | 146.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| measurement_mechanistic_rca-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 321.6 kB
Release files / measurement_mechanistic_rca-0.1.2.tar.gz
| Download URL | measurement_mechanistic_rca-0.1.2.tar.gz |
|---|---|
| Size | 146.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ed62dd8c9ee800afdd20889e3bb92e9223d67faf75296906288ef2c001e6fc06
|
|
BLAKE2b-256 checksum How to use checksums |
7ac37c76344774e5030d09eedf6fa3b8864776d6d79c635d1a5d8f5ed4a4bba1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / measurement_mechanistic_rca-0.1.2-py3-none-any.whl
| Download URL | measurement_mechanistic_rca-0.1.2-py3-none-any.whl |
|---|---|
| Size | 174.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7284cc4433832214109a6732288113d70234a2fac19d82d0c6148a795db5138d
|
|
BLAKE2b-256 checksum How to use checksums |
193ef025cc6d6ea6974f124e4c31f7114d8d3f7690aea4ed3fd17589d2d1de9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for measurement-mechanistic-rca 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.