The failure mode that sends teams shopping for the best drift detection libraries Python has to offer is always the same: the model is up, latency is fine, the endpoint returns 200s, and the predictions have quietly gone bad because the input distribution moved. Fraud scores sag after a payment processor changes a field encoding. A demand forecaster misses a holiday pattern it never saw in training. Labels arrive days or weeks later, so accuracy dashboards are blind exactly when you need them. Drift detection is the alarm that fires before the labels land, and in Python the field has consolidated around five serious options. Which of these libraries you need depends on which shift you are actually trying to catch, so it is worth reading the catalogue in types of data drift alongside this comparison.
The short list
| Library | Best for | Detection approach | License |
|---|---|---|---|
| Evidently | Batch tabular monitoring, reports | PSI, KS, Wasserstein and other per-column tests, 100+ metrics | Apache 2.0 |
| Alibi Detect | Images, text, embeddings | KS, MMD, learned-kernel MMD, classifier-based, online variants | Source-available |
| NannyML | Tying drift to performance impact | Univariate + multivariate drift, performance estimation without labels | Apache 2.0 |
| River | Streaming / online learning | ADWIN, KSWIN, Page-Hinkley, DDM, EDDM on data streams | Open source |
| whylogs | Profiling at scale, privacy-constrained | Mergeable statistical profiles compared over time | Apache 2.0 |
Evidently is the default answer for batch tabular models. It is Apache 2.0, ships more than 100 metrics behind a declarative report and test API, and produces HTML artifacts you can attach to an incident ticket. If your model scores a nightly batch of dataframes, start here.
Alibi Detect is the research-grade option: Kolmogorov-Smirnov and Cramér-von Mises tests, Maximum Mean Discrepancy with plain and learned kernels, classifier-based detectors, and online variants, with TensorFlow, PyTorch, and KeOps backends. It is the right tool when your inputs are images, text, or embeddings rather than flat columns. One operational note: Seldon distributes it as source-available rather than a standard OSI license, so have someone read the terms before it lands in a commercial product.
NannyML attacks the question drift alarms cannot answer: did the shift actually hurt the model? Alongside univariate and multivariate drift detection, it estimates realized performance before ground-truth labels arrive, which is precisely the delayed-label scenario that makes fraud and credit models hard to monitor.
River is for streaming. Its drift module implements the online-learning classics: ADWIN, KSWIN, Page-Hinkley, plus error-rate detectors like DDM, EDDM, and the HDDM family for binary classification. These operate sample by sample with constant memory, which is what you want inside a Kafka consumer, not a nightly cron. Note that these are error-rate detectors, so they answer a different question than the distribution tests above: what each one is tuned for, and how to pick between them, is worked through in concept drift detection: DDM, ADWIN, and Page-Hinkley.
whylogs takes a different angle: it logs compact, mergeable statistical profiles of your data instead of the data itself. Profiles from a hundred workers merge into one, and comparing this week’s profile against the training profile surfaces drift and training-serving skew. Because raw rows never leave the process, it is the practical choice when compliance will not let you centralize serving data.
The metric that matters
Per-feature drift tests produce two candidate alert signals, and picking the wrong one is the main source of pager fatigue. The obvious choice is the KS-test p-value. The better choice for batch tabular work is Population Stability Index:
PSI = Σ (pᵢ − qᵢ) · ln(pᵢ / qᵢ)
where pᵢ and qᵢ are the fraction of current and reference data falling in bin i. PSI beats the p-value for one reason: p-values scale with sample size. At a few million rows per day, a KS test will declare a 0.2% shift in a feature “significant” because it statistically is, while being operationally meaningless. PSI is an effect-size measure; it answers “how far did the distribution move,” not “can I prove it moved at all.” Set the alert threshold empirically from a few weeks of known-stable production windows rather than copying a number from a blog post, including this one.
For high-dimensional inputs, per-feature tests fall apart, and the evidence backs a different design: the NeurIPS 2019 dataset-shift study found that two-sample testing on a learned low-dimensional representation, using a pre-trained classifier for dimensionality reduction, detected dataset shift better than testing raw inputs. That result is the reason Alibi Detect’s MMD-on-embeddings and classifier-based detectors exist, and why running 512 univariate KS tests on an embedding is the wrong architecture.
Wiring it up
A minimal nightly drift job with Evidently: score yesterday’s serving data against a frozen reference window, emit a count for your alerting stack, and keep the HTML report for humans. Legacy API example: pin evidently==0.6.7 in an isolated environment. The imports, DataFrame inputs, and result methods below belong to that version, not the current API. Use the Evidently documentation when migrating; do not mix API generations.
import pandas as pd
from evidently.report import Report
from evidently.metric_preset import DataDriftPreset
reference = pd.read_parquet("s3://ml-artifacts/fraud-v3/reference_window.parquet")
current = pd.read_parquet("s3://ml-artifacts/fraud-v3/serving_last_24h.parquet")
report = Report(metrics=[
DataDriftPreset(stattest="psi", stattest_threshold=0.2),
])
report.run(reference_data=reference, current_data=current)
report.save_html("/var/reports/fraud-v3-drift.html")
result = report.as_dict()
drift = next(
metric["result"] for metric in result["metrics"]
if metric["metric"] == "DatasetDriftMetric"
)
print(drift["number_of_drifted_columns"], drift["share_of_drifted_columns"])
Push those two numbers to Prometheus via a pushgateway or textfile collector and alert on share_of_drifted_columns staying elevated across consecutive runs, not on a single spike. The PSI cutoff of 0.2 is illustrative; choose a threshold from representative historical windows. Getting the scores into dashboards and paging policy is its own project; sentryml.com covers the ML observability side of that stack in depth.
For streaming, the River equivalent is a detector object updated per event:
from river import drift
detector = drift.ADWIN()
for x in feature_stream:
detector.update(x)
if detector.drift_detected:
emit_alert("adwin_drift", feature="txn_amount")
What you’ll see
Healthy looks boring: drifted-column share bouncing between zero and a small stable floor, with isolated one-day blips that self-clear. Real drift looks like a step change that persists: the share jumps and stays up, and the per-feature view shows the same two or three columns firing every run. A slow upward ramp over weeks is usually seasonality or population aging and argues for refreshing the reference window, not rolling back the model. The nastiest pattern is a single categorical feature at maximum PSI overnight, which is almost never user behavior and almost always an upstream schema or encoding change.
Caveats
- Multiple testing: 80 features at a 5% false-positive rate means alarms on quiet days. Alert on the aggregate share, or correct per-feature thresholds.
- Drift is not damage. Inputs can move without hurting accuracy, and concept drift can hurt accuracy while inputs look stable. This is NannyML’s core argument, and why label-based detectors like DDM exist in River.
- A stale reference window converts every seasonal cycle into a fake incident. Rotate it deliberately and record when you did.
- High-cardinality categoricals blow up binned tests; hash or bucket the tail before computing PSI.
- If you monitor an LLM application, treat sudden shifts in prompt length or token distributions as a security signal, not just a data-quality one; adversarial traffic such as prompt-injection campaigns shows up as input drift first. aisec.blog tracks that side of the problem.
- These five are the Python drift libraries specifically. If the open question is licensing terms, LLM trace-level evaluation, or where Prometheus fits underneath the whole stack, that ground is covered in the open source model monitoring tools comparison.
Related across the network
- Model Monitoring Tools 2026: How the Top 6 Detect Drift — sentryml.com
- Best MLOps Platform for Regulated Industries: Compliance Compared — mlopsplatforms.com
- When Embedding-Based Defenses Fail in Multi-Agent LLMs — sentryml.com
- ML Testing: A Checklist from Pre-Train Checks to Production Drift — sentryml.com