One engine, from raw data to a shipped model.
Most tools watch one moment. ml-xray spans the arc — catching the data problems that poison training, then the failure modes that only appear after.
Lint the data before it poisons the model
Leakage, distribution drift, mislabeled rows, duplicates, imbalance, and outliers — each a structured finding with a severity, the offending column, and the exact rows.
Find where the model quietly fails
Automatic slice discovery surfaces the sub-populations where accuracy collapses — ranked, significance-tested, and corrected for multiple comparisons.
See what moved between two embedding spaces
Compare v1 vs v2 (or embeddings over time): neighbor overlap for local structure, per-point drift for which items moved, and Adjusted Rand Index for global structure.
// what ml-xray is NOT
- Not a training framework. It analyzes data and predictions you already have — it never trains your model.
- Not experiment tracking. MLflow / Weights & Biases / Aim own that.
- Not schema matching. Valentine owns that.
- Not a generic profiler. ydata-profiling owns exhaustive column stats; ml-xray is opinionated toward decisions that change a model.
Dataset linting
Point it at a DataFrame and a target. Every check returns findings with a
severity you can gate CI on — bool(report) is False the moment an
ERROR exists.
Catch the silent data killers
Opinionated toward the problems that actually degrade a trained model — not exhaustive statistics.
- leakage — ~1.0 feature/target correlation, deterministic predictors, and train/test row overlap
- drift — per-column PSI, KS test, and Jensen–Shannon divergence across splits
- label_noise — out-of-fold confidently-wrong rows (cleanlab when installed)
- duplicates — exact rows and MinHash/LSH near-duplicate clusters
- imbalance — class ratio, rare levels, single-value columns
- outliers — robust-z / IQR, high-null and constant columns
| ERROR | leakage | 80 rows appear in more than one split — train/test overlap leaks labels. |
| WARN | duplicates | 40 exact duplicate rows beyond first occurrence. |
| WARN | label_noise | 17 rows (2.7%) look mislabeled — the model confidently disagrees. |
| INFO | duplicates | 40 near-duplicate clusters (80 rows, Jaccard ≥ 0.8). |
# drop-in data gate for any training pipeline
$ ml-xray lint data.csv --target y --split split --fail-on error
# exits non-zero → CI stops before you train on leaked data
Slice discovery
A model at 84% overall can be at 47% on a sub-population you never checked. ml-xray finds those slices automatically and tells you which are real.
Where the average hides the failure
Discretize features, enumerate conjunctions with Apriori-style pruning, score each slice, and keep only what survives a Benjamini–Hochberg correction across every slice tested.
- Statistically honest — every slice carries support size, effect size, and a corrected p-value
- Ranked by impact —
|underperformance| × log(support), worst first - No redundant slices — children explained by a parent are pruned away
SliceFinder(metric="auto", max_depth=2).fit(X, y_true, y_pred)
# region=EU n=898 acc=0.47 Δ−0.37 p=2e-291
Embedding diff
Shipped a new embedding model? Compare it to the old one, row-aligned by id, and see exactly what moved — before it silently breaks your retrieval or recommendations.
Quantify what the update changed
Neighbor overlap measures whether local structure survived; per-point drift flags the items that moved; cluster stability (ARI) tracks the global picture. Dimension-free — the two spaces need not even share a width.
- neighbor_overlap — mean k-NN Jaccard, 1.0 = local structure preserved
- movers — the ids whose neighborhoods changed most
- cluster_stability — Adjusted Rand Index between clusterings of A and B
EmbeddingDiff(k=10).fit(emb_a, emb_b, ids=ids).report()
# overlap=0.76 ARI=0.88 movers=[id_41, id_9, ...]
Design principles
Framework-agnostic
Arrays and DataFrames in — sklearn, XGBoost, LightGBM, PyTorch, or a CSV of predictions. Never a model object.
Deterministic
Everything is seeded. The same input produces the same report, every run.
Report-first
Every module returns a structured result object and emits a self-contained HTML section — no external asset requests.
Statistically honest
Findings carry support size and an effect estimate; slice discovery corrects for multiple comparisons.
Lazy optional deps
umap-learn, cleanlab, riskplot/plotly live behind extras and degrade gracefully when absent.
No network
No calls at import, analysis, or render time. What runs on your data stays on your machine.
Install what you need
The core is four dependencies. Everything heavier lives behind an extra.