Skip to main content

Dataset drift detection for ML pipelines.

Project description

psiwatch

Zero-dependency Python library for dataset drift detection in ML pipelines.

Detect covariate drift, distribution shift, and data quality degradation between two datasets — using PSI, Chi-Square, Mean Shift, and Standard Deviation analysis. Pure Python. No numpy. No scipy. No pandas required.

PyPI Downloads License Python Zero Dependencies


The Problem

You train a model on historical data. Weeks later, predictions go wrong — silently. No errors. No alerts.

The cause is data drift. Production data no longer looks like training data:

  • Customer ages shifted
  • New cities or categories appeared
  • Salary distributions changed
  • Credit scores dropped

Most teams discover this after the model has already failed.


The Solution

pip install psiwatch
psiwatch compare train.csv production.csv
══════════════════════════════════════════════════════════════
  PSIWATCH REPORT
  baseline: train.csv  →  new: production.csv
  Generated: 2026-06-14 09:00:00
══════════════════════════════════════════════════════════════

  [!!] credit_score  [numeric]  — HIGH DRIFT
     → Mean shifted by 2.20 std devs (752.00 → 624.00)
     → PSI = 11.1200 (significant drift)
     ┌ Mean:    752.00 → 624.00
     ├ Std:     48.00 → 71.00
     ├ PSI:     11.1200
     ├ Min:     600.00 → 420.00
     ├ P25:     720.00 → 560.00
     ├ Median:  750.00 → 620.00
     ├ P75:     790.00 → 690.00
     └ Max:     850.00 → 800.00

  [!!] loan_type  [categorical]  — HIGH DRIFT
     → New categories found: ['BNPL', 'Crypto']
     → PSI = 4.1900
     ┌ PSI:         4.1900
     ├ Chi-square:  3.8200
     └ New cats:    ['BNPL', 'Crypto']

  [OK] customer_id  [categorical]  — STABLE
     → No drift detected

──────────────────────────────────────────────────────────────
  HIGH: 2   MEDIUM: 0   PASS: 1

  [!!] Drift Health Score: 11/100  (Significant Drift)
══════════════════════════════════════════════════════════════

Why psiwatch?

Most drift detection tools require heavy dependencies and are built for Jupyter notebooks — not pipelines or minimal environments.

Feature psiwatch evidently alibi-detect
Dependencies Zero Heavy Heavy
Install size ~15KB ~50MB+ ~100MB+
Works on Termux/Android Yes No No
CLI tool Yes No No
CI/CD --fail-on-drift flag Yes No No
pandas DataFrame support Yes Yes Yes
Pure Python Yes No No
Auto update check Yes No No
HTML reports Yes Yes No

Install

pip install psiwatch

Works on Windows, Mac, Linux, VPS, Google Colab, Jupyter, and Termux on Android.


Quickstart

git clone https://github.com/tharunstryker/psiwatch
cd psiwatch
pip install -e .
psiwatch compare samples/train.csv samples/new.csv

CLI

# Compare two CSV files
psiwatch compare old.csv new.csv

# Save as HTML report
psiwatch compare old.csv new.csv --output report.html

# Save as JSON for pipelines
psiwatch compare old.csv new.csv --output report.json

# Save as plain text
psiwatch compare old.csv new.csv --output report.txt

# Compare specific columns only
psiwatch compare old.csv new.csv --columns age,score,city

# Set custom PSI threshold
psiwatch compare old.csv new.csv --psi-threshold 0.15

# Fail with exit code 1 if drift detected (for CI/CD)
psiwatch compare old.csv new.csv --fail-on-drift

# Upgrade psiwatch to latest version
psiwatch update

# Show installed version
psiwatch version

Python Library

CSV files

import psiwatch

psiwatch.compare("old.csv", "new.csv")
psiwatch.compare("old.csv", "new.csv", output="report.html")
psiwatch.compare("old.csv", "new.csv", columns=["age", "score"])

pandas DataFrames

import pandas as pd
import psiwatch

old_df = pd.read_csv("train.csv")
new_df = pd.read_csv("production.csv")

psiwatch.compare(old_df, new_df)
psiwatch.compare(old_df, new_df, output="report.html")

pandas is optional — psiwatch works without it. Only imported when a DataFrame is passed.

Python dicts

psiwatch.compare_data(
    old={"age": [22, 23, 21], "city": ["Chennai", "Delhi", "Mumbai"]},
    new={"age": [28, 30, 29], "city": ["Chennai", "Bangalore", "Hyderabad"]}
)

List of dicts (JSON records)

old_records = [{"age": 22, "city": "Chennai"}, {"age": 23, "city": "Delhi"}]
new_records = [{"age": 28, "city": "Mumbai"}, {"age": 30, "city": "Pune"}]

psiwatch.compare(old_records, new_records)

Single list (one column)

psiwatch.compare_columns([22, 23, 21], [28, 30, 29], name="age")

Raw results (no print)

result = psiwatch.analyze("old.csv", "new.csv")

print(result["health_score"])         # 0-100

for col, data in result["columns"].items():
    print(col, data["severity"])      # HIGH / MEDIUM / PASS
    print(col, data["metrics"])       # PSI, mean, std, chi-square, percentiles
    print(col, data.get("warnings"))  # mixed-type or schema warnings

CI/CD — Fail on Drift

Block deployments when data drifts. psiwatch exits with code 1 if health_score < 80.

GitHub Actions

- name: Check data drift
  run: psiwatch compare train.csv production.csv --fail-on-drift

Python

import psiwatch
from psiwatch import DriftDetected

try:
    psiwatch.compare("train.csv", "new.csv", fail_on_drift=True)
except DriftDetected as e:
    print(f"Drift detected: {e}")
    # send alert, stop deploy, log to monitoring

Custom Thresholds

# Shortcut — set HIGH boundary, medium auto-scales to 40%
psiwatch.compare("old.csv", "new.csv", psi_threshold=0.15)

# Full control
psiwatch.compare("old.csv", "new.csv", thresholds={
    "psi_medium": 0.05,
    "psi_high": 0.15,
    "mean_shift_medium": 0.2,
    "mean_shift_high": 0.5,
    "std_shift_medium": 0.2,
    "std_shift_high": 0.5,
    "category_share_shift": 0.10,
    "chi_square_medium": 0.5,
})

Auto Update Check

psiwatch checks PyPI for newer versions on every run. Check is cached for 24 hours — won't spam on every import.

  ╔══════════════════════════════════════════════════╗
  ║  psiwatch update available: 0.9.0 → 0.10.0      ║
  ║  Run: pip install --upgrade psiwatch             ║
  ╚══════════════════════════════════════════════════╝

To suppress in production/CI:

import psiwatch
psiwatch.compare("old.csv", "new.csv", silent_update=True)

Dataset Warnings

psiwatch warns instead of failing silently when your datasets have schema mismatches.

  [WARN]
     ⚠  Columns only in baseline (skipped): ['old_feature', 'legacy_col']
     ⚠  Columns only in new data (skipped): ['new_feature']
     ⚠  Column 'income' is 72% numeric — treated as categorical. Cast to float if intended as numeric.

Output Formats

Format Command Use case
Terminal default Quick checks during development
HTML --output report.html Sharing with team, presentations
JSON --output report.json CI/CD pipelines, automation, dashboards
TXT --output report.txt Server logs, plain text reports

All outputs include: timestamp, source file names, per-column metrics, health score.


Input Modes

Input Works with
CSV file path "old.csv" compare()
pandas DataFrame compare(), compare_data()
Python dict {"col": [values]} compare(), compare_data()
List of dicts [{"col": val}, ...] compare()
Plain Python list compare_columns()

Detection Methods

Numeric columns — age, score, salary, credit score

Method What it detects
Mean Shift Average moved significantly
Std Deviation Shift Spread of values changed
PSI Overall distribution shape changed
Percentiles Min, P25, Median, P75, Max compared

Categorical columns — city, grade, status, loan type

Method What it detects
New Category Detection Values that never existed in training data
Frequency Distribution Shift Category proportions changed
PSI Overall distribution changed
Chi-Square Frequency mismatch is statistically significant

PSI Reference

PSI (Population Stability Index) is the industry standard metric for monitoring production data drift.

PSI Status Action
< 0.10 Stable Model is fine
0.10 – 0.25 Moderate Drift Monitor closely, investigate
> 0.25 Significant Drift Retrain your model

Drift Health Score

Every report includes a single 0–100 score.

Score Status Meaning
80–100 Stable Data is stable, model likely fine
50–79 Moderate Drift Some columns changed — investigate
0–49 Significant Drift Major shifts — retrain

Important: if any column is HIGH severity, the score is hard-capped at ≤50 — one bad column in a 20-column dataset does not average away into "Healthy".


Real World Example — Banking Data

psiwatch compare bank_2023.csv bank_2026.csv

What psiwatch caught:

  • Credit scores dropped from 752 → 624 — riskier customers
  • Salaries dropped from 63k → 45k — lower income applicants
  • Loan amounts jumped from 500k → 800k — borrowing more, earning less
  • New loan types appeared — BNPL, Crypto (never in training data)
  • New statuses appeared — Defaulted, Frozen
  • Branches completely changed — 5 old cities gone, 5 new cities added

Health Score: 11/100 — a model trained on 2023 data would be completely blind to all of this.


Project Structure

psiwatch/
├── src/psiwatch/
│   ├── __init__.py      ← public API + DriftDetected exception
│   ├── loader.py        ← CSV, dict, list, DataFrame input
│   ├── analyzer.py      ← PSI, mean/std, chi-square, percentiles
│   ├── reporter.py      ← terminal, HTML, JSON, TXT output
│   ├── updater.py       ← PyPI version check (24h cached)
│   └── cli.py           ← psiwatch CLI
├── samples/
│   ├── train.csv        ← example baseline dataset
│   └── new.csv          ← example drifted dataset
├── tests/
│   └── test_analyzer.py
├── pyproject.toml
└── README.md

Run Tests

python tests/test_analyzer.py
Running psiwatch tests...

PASS numeric high drift detected
PASS numeric no drift — PASS
PASS categorical new category detected
PASS categorical no drift — PASS
PASS full analyze — health score: 0/100
PASS health score clean data: 100/100
PASS column filter works

All tests passed.

Zero Dependencies

psiwatch uses only Python's standard library:

Module Used for
csv File reading
math Statistical calculations
json JSON output + version cache
os File operations
argparse CLI interface
urllib PyPI version check
datetime Report timestamps

No pip conflicts. No install failures. If Python runs, psiwatch runs.


Changelog

v0.10.0

  • pandas DataFrame support — pass DataFrames directly to compare()
  • List of dicts input — [{"age": 22, "city": "Chennai"}, ...] supported
  • --fail-on-drift CLI flag — exit code 1 when drift detected, for CI/CD pipelines
  • DriftDetected exception — catch in Python for custom alerting logic
  • updater.py — auto version check against PyPI, 24h cached, silent in CI
  • Health score hard-cap — any HIGH column caps score at ≤50 (was averaging, now accurate)
  • Missing column warnings — schema mismatches shown explicitly, not silently dropped
  • Mixed-type column warnings — columns 50-80% numeric now warn before type decision
  • Timestamp + source in all reports — HTML, TXT, terminal all show when and what was compared
  • Chi-square O(n²) → O(n) — pre-computed count dicts, fast on large datasets

v0.2.0

  • Custom threshold configuration (psi_threshold, thresholds dict)
  • Column filtering (columns parameter)
  • HTML report output
  • analyze() function for programmatic access

v0.1.0

  • Initial release
  • CSV comparison via CLI and Python API
  • PSI, Mean Shift, Std Shift, Chi-Square, New Category detection
  • Terminal, JSON, TXT output
  • Zero dependencies

Related Tools

If you need heavier drift detection with statistical testing frameworks:

Use psiwatch when you need something lightweight, fast, and dependency-free.


License

MIT © 2026 Tharun · Naeris

Built entirely on Android using Termux. No laptop. No PC. No IDE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

psiwatch-0.10.0.tar.gz (25.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

psiwatch-0.10.0-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

Details for the file psiwatch-0.10.0.tar.gz.

File metadata

  • Download URL: psiwatch-0.10.0.tar.gz
  • Upload date:
  • Size: 25.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.34.2

File hashes

Hashes for psiwatch-0.10.0.tar.gz
Algorithm Hash digest
SHA256 4eeb7ef160ec6d30f730722fcba6306162dcb6eee8040aa85b94ac187bdc7afe
MD5 c260c74652bcc5e2ce985002e35c1785
BLAKE2b-256 b23ab23b72d1ea8d04585837d48cbadb7031f21db6e6966f5da00c29c24248cd

See more details on using hashes here.

File details

Details for the file psiwatch-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: psiwatch-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 21.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.34.2

File hashes

Hashes for psiwatch-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0b3c718716796b38cea193daac540d2beae77d0c162304497c06b8125b3ab6ec
MD5 16ec8ab804401bd97361c104611b6aae
BLAKE2b-256 54cc73a38f835c7c48fa9fbb0c1c71ec1264f44a95f94fb12fb63dfd6940d36e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page