Skip to main content

psiwatch

Current version: 0.15.0

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, Standard Deviation analysis, and — new in 0.15 — vocabulary, language and length drift for free-text columns. Pure Python. No numpy. No scipy. No pandas. No models to download.

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']
     → Categories vanished from new data: ['Personal']
     → PSI = 4.1900
     ┌ PSI:          4.1900
     ├ Chi-square:   3.8200
     ├ New cats:     ['BNPL', 'Crypto']
     └ Vanished:     ['Personal']

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

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

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

Text Drift — chatbot messages, reviews, tickets (new in 0.15)

A column of sentences used to be treated as a categorical column where every unique sentence is its own "category" — meaningless results and no explanation. psiwatch now detects free-text columns automatically and analyses them as text, with no embeddings, no models and no extra dependencies.

psiwatch compare chats_jan.csv chats_feb.csv
  [!!] message  [text]  — HIGH DRIFT
     → Vocabulary drift score 0.518 (significant; noise floor 0.007) — rising: order, help, pls; falling: delivery, blue, package
     → New words not in baseline: refund, திரும்ப, பணம், give, எனக்கு
     → 55.2% of words are unseen in the baseline (expected ~0.0%)
     ┌ Vocab drift:  0.5176  (noise floor 0.0071)
     ├ Words/value:  6.7 → 6.3
     ├ Unseen words: 0.0% expected → 55.2%
     ├ Scripts:      Latin 100%  →  Latin 87.5%, Tamil 12.5%
     ├ Rising:       order (6.5% → 13.8%), help (9.5% → 12.8%), pls (9.5% → 12.8%)
     ├ Falling:      delivery (20.5% → 0%), blue (12% → 0%), package (11.7% → 0%), price (11.7% → 0%)
     └ New words:    refund (19.2%), திரும்ப (14.7%), பணம் (14.7%), give (14.3%), scam (14%), chargeback (13.8%)

(Percentages after a word = share of messages containing it, baseline → new.)

What is checked What it catches
Vocabulary drift — Jensen-Shannon divergence (bits, 0–1) between word distributions, corrected for small-sample noise The topics people talk about changed
Rising / falling / brand-new words What changed — not just that something did
Unseen-word rate (Good-Turing corrected) New slang, new products, prompt injection, a new campaign
Script / language mix (Latin, Tamil, Devanagari, Arabic, CJK …) A wave of Tamil-script or Hindi traffic arriving at an English-only model
Length (words per value) Users switch from short queries to long pasted prompts
Structure — empty, duplicate, URL, digit, upper-case and symbol/emoji rates (MEDIUM at most) Bots, spam floods, templated or injected input

Unicode-aware: Tamil, Hindi, Bengali etc. are tokenised correctly (combining vowel signs stay attached to their word); CJK is tokenised per character.

Detection and control:

psiwatch.compare("old.csv", "new.csv")                          # text columns auto-detected
psiwatch.compare("old.csv", "new.csv", text_columns=["message"])  # force a column to text
psiwatch.compare("old.csv", "new.csv", detect_text=False)       # pre-0.15 behaviour
psiwatch compare old.csv new.csv --text-columns message
psiwatch compare old.csv new.csv --no-text-detect

A column is treated as text when it isn't numeric, has several words per value, and its values are mostly unique. Repeated multi-word categories ("New York", "Order shipped") stay categorical. Locks (psiwatch lock) store a bounded fingerprint for text columns too: word counts for the top 1,000 words plus a few scalars, not the messages.

Honest limits — read these:

  • Bag-of-words, not meaning. "refund" and "money back" are unrelated words to psiwatch. For semantic drift you need embedding-based tools (see Related Tools).
  • Small samples have low power. Under ~100 values per side, psiwatch warns and the noise-aware thresholds make it slower to flag. Fewer than 5 non-empty values → UNKNOWN.
  • Thresholds are heuristics. They were calibrated on simulated chatbot traffic (healthy samples stay PASS from ~100 values; a topic takeover reaches HIGH). Tune them on your own data with [thresholds] text_jsd_medium / text_jsd_high (default 0.005 / 0.04 bits).
  • Word lists reveal vocabulary. A lock file holds no messages, but its top-word list reflects your data's vocabulary — don't commit locks built from sensitive text to a public repo.
  • Charts can't draw Indic scripts. matplotlib (used only by --plot / --embed-chart) has no native Tamil shaping, so Tamil/Devanagari words appear as boxes in the PNG chart. Terminal, TXT, HTML and JSON reports render them correctly.
  • Speed: pure Python, roughly 6.5 s for 100k + 100k messages on a desktop CPU; expect slower on a phone.

Why psiwatch?

psiwatch is for pipelines and minimal environments where you want a drift check with nothing to install but psiwatch itself.

psiwatch 0.15.0 evidently 0.7.23 alibi-detect 0.13.0
Required dependencies 0 26 17
Wheel size (package only) ~74 KB 11.7 MB 0.4 MB
Python versions 3.8+ 3.10+ 3.9+

Dependency counts and wheel sizes are from each package's PyPI metadata (core requirements, excluding extras), checked October 2026. Wheel size excludes dependencies — installing the others pulls in numpy, pandas, scikit-learn and more; psiwatch pulls in nothing.

What psiwatch gives you: a CLI, --fail-on-drift for CI, baseline locking, directory watching, multi-snapshot trends, webhook alerts, HTML/JSON/TXT reports, Parquet/SQL/DataFrame input, and text-column drift — all pure standard-library Python, developed and tested on Termux/Android.

What psiwatch does not do (use the tools in Related Tools when you need these): model-based or embedding-based drift detection, multivariate drift (columns are analysed one at a time), a monitoring dashboard/UI, or target/prediction drift and model-quality metrics.


Install

pip install psiwatch

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

Upgrade

# via CLI (easiest)
psiwatch update

# or standard pip
pip install --upgrade psiwatch

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

# Force a column to be analysed as free text / turn text auto-detection off
psiwatch compare old.csv new.csv --text-columns message
psiwatch compare old.csv new.csv --no-text-detect

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

# Skip columns (IDs, timestamps, row numbers)
psiwatch compare old.csv new.csv --ignore-columns id,timestamp,row_num

# 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

# Suppress update banner (useful in scripts)
psiwatch compare old.csv new.csv --silent

# Send a Slack/Discord/webhook alert on drift
psiwatch compare old.csv new.csv --webhook https://hooks.slack.com/services/XXX/YYY/ZZZ

# Config files (psiwatch.toml / .psiwatchrc) in the project directory are picked up automatically
psiwatch compare old.csv new.csv

# One-line health summary (no full report — ideal for shell scripts)
psiwatch summary train.csv new.csv

# Lock training data as a statistical baseline
psiwatch lock train.csv                          # creates psiwatch.lock.json
psiwatch lock train.csv --output model.lock.json

# Check new data against the lock
psiwatch check new.csv
psiwatch check new.csv --lock model.lock.json --fail-on-drift

# Show what's stored in a lock file
psiwatch lock-info

# Track drift across a sequence of datasets over time
psiwatch trend day1.csv day2.csv day3.csv day4.csv
psiwatch trend day1.csv day2.csv day3.csv --baseline first
psiwatch trend day1.csv day2.csv day3.csv --output trend.json

# Watch a directory and check new CSV files as they arrive
psiwatch watch data/
psiwatch watch data/ --once               # one pass, exit — good for cron and CI
psiwatch watch data/ --webhook https://hooks.slack.com/services/XXX --fail-on-drift

# Upgrade 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, trend_direction
    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

Self-Upgrade

psiwatch update

Runs pip install --upgrade psiwatch under the hood — same Python environment, no extra steps. Works on Termux too.


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,
    # free-text columns (defaults shown)
    "text_jsd_medium": 0.005,        # vocabulary drift score, bits
    "text_jsd_high": 0.04,
    "text_oov_medium": 0.10,         # excess share of unseen words
    "text_oov_high": 0.25,
    "text_script_shift_medium": 0.15,  # share of values changing writing system
    "text_script_shift_high": 0.30,
    "text_structure_shift": 0.10,    # empty/duplicate/URL/digit/upper/symbol rates
})

Auto Update Check

The psiwatch CLI checks PyPI for newer versions when you run a command — never on import psiwatch. The check is cached for 24 hours (so it's not a PyPI request on every run, just every CLI invocation within the cache window) and is automatically silent in CI environments (CI=true, GITHUB_ACTIONS=true, PSIWATCH_SILENT=1).

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

To suppress from the CLI:

psiwatch compare old.csv new.csv --silent

import psiwatch and library calls like psiwatch.compare(...) never trigger this check or make any network call — it's CLI-only. If you want the check inside your own script, opt in explicitly:

from psiwatch.updater import check_for_update
import psiwatch
check_for_update(psiwatch.__version__)

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.

Trend Direction

Numeric columns include a trend direction — which way the mean moved:

Symbol Meaning
↑ Mean increased in new data
↓ Mean decreased in new data
→ Mean stable

Available in terminal output, HTML report, and in result["metrics"]["trend_direction"].


Vanished Category Detection

Categorical columns now detect categories that existed in the baseline but are completely absent from new data — not just new categories appearing.

  → Categories vanished from new data: ['Personal', 'Auto']

Trend Analysis

Track how your data drifts across a sequence of files over time.

psiwatch trend monday.csv tuesday.csv wednesday.csv thursday.csv
psiwatch trend day1.csv day2.csv day3.csv --baseline first --output trend.json

--baseline previous (default) compares each file to the one before it. --baseline first compares every file back to the first (cumulative drift from training). The report shows health score per step, per-column severity and PSI over time, and flags any column that steadily worsened across the sequence.

from psiwatch import analyze_trend

result = analyze_trend(["day1.csv", "day2.csv", "day3.csv"])
print(result["overall_health_history"])   # [97, 68, 21]
print(result["worsening_columns"])        # ["age"]

Watch Mode

Poll a directory for new CSV files and check each one against a baseline lock as it arrives.

psiwatch lock train.csv
psiwatch watch data/ --webhook https://hooks.slack.com/services/XXX

--once is designed for cron jobs and CI. Checks current directory contents and exits. psiwatch persists which files it has already checked (mtime-based, stored in <lock>.seen.json), so repeated runs only process new or modified files.

# In a cron job or CI step:
psiwatch watch data/ --once --fail-on-drift
from psiwatch import watch_directory

result = watch_directory("data/", once=True)
print(result["drifted_files"])

Webhook Alerts

Send a drift notification to Slack, Discord, or any JSON endpoint when drift is detected. The alert is skipped automatically when health score >= 80.

psiwatch compare train.csv new.csv --webhook https://hooks.slack.com/services/T/B/xxx
psiwatch check new.csv --webhook https://discord.com/api/webhooks/123/abc
psiwatch watch data/ --once --webhook https://example.com/psiwatch-alert

Format auto-detected from URL host: Slack → {"text": "..."}, Discord → {"content": "..."}, anything else → full JSON payload with health_score, summary, message.

from psiwatch import compare, send_webhook

result = compare("train.csv", "new.csv")
send_webhook("https://hooks.slack.com/services/XXX/YYY/ZZZ", result)

Config File

Store default settings in psiwatch.toml or .psiwatchrc (JSON) in your project directory. CLI flags always win over the config file.

psiwatch.toml:

psi_threshold = 0.2
ignore_columns = ["id", "timestamp"]
text_columns = ["message"]      # force free-text analysis (optional)
# detect_text = false           # turn text auto-detection off
fail_on_drift = true
webhook = "https://hooks.slack.com/services/XXX/YYY/ZZZ"

[thresholds]
mean_shift_high = 0.6

.psiwatchrc (JSON):

{
  "psi_threshold": 0.2,
  "ignore_columns": ["id", "timestamp"],
  "fail_on_drift": true
}

psiwatch auto-detects these files in the current directory (and walks up through parent directories). There is no --config flag; keep the file in your project root.


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()
Parquet file path "old.parquet" compare() (requires pandas + pyarrow)
pandas DataFrame compare(), compare_data()
Python dict {"col": [values]} compare(), compare_data()
List of dicts [{"col": val}, ...] compare()
Plain Python list compare_columns()
SQL query + DB-API connection psiwatch.loader.load_sql() → compare_data()

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
Trend Direction Which way the mean moved (↑ ↓ →)

Categorical columns — city, grade, status, loan type

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

Text columns — messages, reviews, tickets, search queries

Method What it detects
Vocabulary drift (noise-corrected JSD) Word distribution changed
Rising / falling / new words Which words drove the change
Unseen-word rate (Good-Turing corrected) Words never seen in the baseline
Script / language mix Writing system shifted (e.g. Latin → Tamil)
Length shift Words per value changed
Structure rates Empty, duplicate, URL, digit, upper-case, symbol/emoji rates

See Text Drift above for details and limits.


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)
  • Categories vanished — Personal, Auto no longer in new 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/
├── .github/workflows/
│   └── ci.yml            ← pytest on Python 3.8–3.13 + build/version check
├── src/psiwatch/
│   ├── __init__.py      ← public API + DriftDetected exception
│   ├── loader.py        ← CSV, Parquet, SQL, dict, list, DataFrame input
│   ├── adapt.py          ← learn-thresholds: per-column learned PSI thresholds
│   ├── viz.py            ← optional matplotlib chart export (psiwatch[charts])
│   ├── analyzer.py      ← PSI, mean/std, chi-square, percentiles, trend, baseline summaries
│   ├── text.py          ← free-text drift: vocabulary JSD, new words, scripts, length, structure
│   ├── reporter.py      ← terminal, HTML, JSON, TXT output (HTML-escaped)
│   ├── updater.py       ← PyPI version check (24h cached) + self-upgrade — CLI-triggered only
│   ├── locker.py        ← baseline locking (lock / check / lock-info) — stores fingerprints, not raw data
│   ├── trend.py         ← multi-file drift trend analysis (HTML-escaped)
│   ├── watcher.py       ← directory polling with mtime-based state
│   ├── webhook.py       ← Slack/Discord/generic webhook alerts
│   ├── config.py        ← psiwatch.toml / .psiwatchrc config loader
│   └── cli.py           ← psiwatch CLI
├── samples/
│   ├── train.csv        ← example baseline dataset
│   └── new.csv          ← example drifted dataset
├── tests/
│   ├── test_analyzer.py
│   ├── test_locker.py
│   ├── test_reporter.py
│   ├── test_text.py
│   ├── test_trend.py
│   ├── test_updater.py
│   ├── test_webhook.py
│   └── test_config.py
├── pyproject.toml
└── README.md

Run Tests

pip install -e ".[dev]"
pytest
============================== 93 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
subprocess Self-upgrade (psiwatch update)
datetime Report timestamps
re, unicodedata, collections, bisect Text tokenisation and word counts (0.15+)

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


Changelog

v0.15.0

  • Added: free-text column drift. Columns of sentences (chat messages, reviews, tickets, queries) are auto-detected and analysed for vocabulary drift (noise-corrected Jensen-Shannon divergence), rising/falling/brand-new words, unseen-word rate (Good-Turing corrected), script/language mix, length, and structure (empty, duplicate, URL, digit, upper-case, symbol rates). Pure Python, zero dependencies, no models. Works in compare, analyze, summary, lock/check, trend, --plot and --embed-chart.
    • --text-columns a,b / text_columns=[...] forces text analysis; --no-text-detect / detect_text=False restores pre-0.15 behaviour (text treated as categorical).
    • New thresholds: text_jsd_medium/high, text_oov_medium/high, text_script_shift_medium/high, text_structure_shift. New config keys: text_columns, detect_text.
    • Behaviour change: a free-text column that used to be reported as [categorical] is now reported as [text].
    • Lock files gain a text column type (bounded: top-1,000 word counts + scalars). Locks containing text columns cannot be read by psiwatch < 0.15.
  • Fixed (README): removed the non-existent --config flag; replaced the competitor comparison (unverified "No" claims and wrong sizes) with dependency counts and wheel sizes measured from PyPI metadata; corrected the package size (~74 KB wheel, not ~15 KB); replaced stale hand-rolled test output with the real pytest summary.
  • Tests: 93 (53 existing + 40 new, including noise-calibration tests that require healthy data to stay PASS).

v0.14.0

  • Added: psiwatch.viz.plot_drift() — real baseline-vs-new histogram overlay charts (numeric columns) and category frequency comparisons (categorical columns), saved as a PNG/PDF/SVG. Built from the exact same binned histogram data PSI itself uses — not an approximation from summary stats.
    • psiwatch compare old.csv new.csv --plot drift.png
    • psiwatch compare old.csv new.csv --plot drift.png --plot-style seaborn-v0_8-darkgrid
    • Python: from psiwatch.viz import plot_drift; plot_drift("old.csv", "new.csv", output="drift.png")
    • Customization: title= and dpi= params (plot_drift(..., title="Q3 Sales Drift", dpi=200)) for presentation-ready output.
    • Requires matplotlib — install with pip install psiwatch[charts] or pip install matplotlib. This is the one deliberate exception to psiwatch's zero-dependency core: matplotlib is imported lazily inside plot_drift()/plot_drift_bytes() only, never at module load, so import psiwatch and every other feature remain fully dependency-free regardless of whether matplotlib is installed.
    • No seaborn dependency, by design: modern matplotlib ships several seaborn-derived style sheets built in (seaborn-v0_8, seaborn-v0_8-darkgrid, etc. — see --plot-style), so you get seaborn's visual look without psiwatch importing seaborn itself. If you want actual seaborn-specific plot types for your own custom analysis, install seaborn yourself and work with psiwatch's result data directly — psiwatch doesn't broker that.
  • Added: --embed-chart — embeds the drift chart directly into an HTML report as an inline base64 image, instead of a separate chart file to keep track of alongside the report.
    • psiwatch compare old.csv new.csv --output report.html --embed-chart
    • Python: psiwatch.compare(old, new, output="report.html", embed_chart=True)
    • Silently ignored (no error) if --output isn't .html, or if no --output is given at all — only raises if you actually requested embedding and matplotlib is missing.
    • Uses a new psiwatch.viz.plot_drift_bytes() that returns PNG bytes in memory rather than writing a file, sharing the same chart-building logic as plot_drift() (no duplicated drawing code between the file and embedded paths).

v0.13.0

  • Added: psiwatch learn-thresholds — learns a per-column PSI threshold from a sequence of historical "normal" snapshots, instead of using one fixed global threshold for every column. Columns with naturally higher variance get a more lenient learned threshold; naturally stable columns keep a tight one. Uses mean + sensitivity*std over historical PSI (default sensitivity 3.0), clamped between 0.125 and 0.75 so it can never become dangerously lenient or impractically strict. Saves to a JSON file (default psiwatch_thresholds.json).
    • psiwatch learn-thresholds day1.csv day2.csv ... --output thresholds.json
    • psiwatch learn-thresholds --dir history/ --output thresholds.json
    • psiwatch compare new_base.csv new_data.csv --thresholds-file thresholds.json
    • Scope note: only the PSI threshold is learned/adapted. mean_shift_high/std_shift_high and other checks still use the global default — severity is the worst of all checks combined, so a column can still be flagged HIGH via a real mean/std shift even when its learned PSI threshold says PSI itself is within normal historical range.
    • Data size note: PSI is sensitive to sample size — snapshots under ~500 rows can produce learned thresholds that don't make intuitive sense (a tightly-distributed column can appear noisier than a widely-distributed one purely from bin-edge sensitivity at low N). learn-thresholds warns when any snapshot is under 500 rows.
  • Added: runnable Java and JavaScript examples (docs/examples/) showing how to call the psiwatch CLI as a subprocess and parse its JSON report — no psiwatch code changes needed, since this already worked for any language capable of running a subprocess and parsing JSON. See docs/java-interop.md.

v0.12.2

  • Added: Parquet file support — compare(), analyze(), and the CLI compare command now accept .parquet/.pq file paths anywhere a CSV path is accepted, auto-detected by extension. Requires pandas + pyarrow to be installed (optional — psiwatch's core stays zero-dependency).
  • Added: load_sql(query, connection) in psiwatch.loader — run a SQL query against any DB-API connection you already have open (sqlite3, psycopg2, pymysql, SQLAlchemy, etc.) and feed the result straight into compare_data(). psiwatch does not bundle or require any DB driver — bring your own connection.
  • Fixed: values like "NaN", "inf", "-Infinity" were silently accepted by float() and could crash compare()/analyze() downstream during PSI binning. cast_numeric() now explicitly rejects NaN/infinity, treating them the same as any other unparseable value.
  • Fixed: numeric columns with non-numeric/garbage values (including the NaN/inf case above) were silently dropped with no indication anywhere in the report — new_count would just be smaller than expected. A warning now reports exactly how many values (and what %) were excluded, on both the baseline and new side.

v0.12.1

  • Fixed: package metadata in pyproject.toml — corrected author name/email and switched license to the SPDX-string format expected by current packaging tooling. No code changes.

v0.12.0 — security & bug-fix release (no new features)

  • Fixed: psiwatch lock was storing the entire raw baseline dataset inside the lock file (under values_sample) instead of a statistical fingerprint — a 10,000-row baseline produced a multi-MB lock file containing your original training data. Lock files now store mean/std/percentiles plus a 10-bin histogram (numeric) or category frequencies (categorical) — bounded size regardless of dataset size, and no raw rows. Lock files created before this fix are detected and rejected with a message to re-run psiwatch lock.
  • Fixed: HTML reports (to_html(), to_html_trend()) interpolated column names, category values, and source filenames directly into the page — and into an inline <script> block for the trend chart — with no escaping. A column name or category value containing <script>...</script> would execute when the report was opened in a browser. All interpolated content is now HTML-escaped, with an additional guard against </script> breakout in the chart's JSON payload.
  • Fixed: import psiwatch made a network call to PyPI on every import (the update-check banner), even inside training pipelines, notebooks, or CI steps that never touch the CLI. The check now only runs from the psiwatch CLI itself; plain import psiwatch makes zero network calls. compare()'s silent_update parameter is now a documented no-op (kept so existing calls don't break) since the check it used to suppress no longer happens at that call site.
  • Test suite converted from a standalone script with a hand-rolled pass/fail counter (no real asserts, never run by CI) into a real pytest suite across 6 files, including dedicated regression tests for all three fixes above. Added .github/workflows/ci.yml running the suite on Python 3.8–3.13 plus a package-build and version-consistency check on every push and pull request.

v0.11.0

  • psiwatch trend — track drift across a sequence of datasets over time; detect worsening columns
  • psiwatch watch — poll a directory for new CSV files and check each against a lock baseline; persists seen-file state across --once runs (cron/CI-safe)
  • --webhook URL — send Slack, Discord, or generic JSON alert on any drift detection (compare, check, summary, watch)
  • Config file support — drop a psiwatch.toml or .psiwatchrc (JSON) in your project directory to set default thresholds, columns, webhook, etc.; CLI flags always override
  • analyze_trend() Python API — full programmatic access to trend result dict including worsening_columns and column_history
  • watch_directory() Python API — embed directory watching in your own scripts
  • send_webhook() Python API — post drift alerts to any endpoint from Python
  • load_config() Python API — load and apply config files programmatically
  • Webhook skips automatically when health score ≥ 80 (drift-only alerting by default)
  • --once flag on watch — single-pass mode for cron jobs and CI pipelines

v0.10.1

  • result["summary"] in analyze() — high_count, medium_count, pass_count, drifted_columns, stable_columns, total_columns
  • Sample size warning — fires when baseline and new data differ by more than 10x (PSI unreliable at extreme size ratios)
  • --ignore-columns / -x flag — skip columns by name (IDs, timestamps, row numbers)
  • psiwatch summary command — one-line health score for shell scripts without a full report
  • psiwatch lock / check / lock-info — baseline locking: save a statistical fingerprint of training data, ship it with your model, check against it in CI without the original CSV

v0.10.0

  • psiwatch update CLI command — self-upgrade without leaving the terminal
  • Trend direction (↑ ↓ →) — numeric columns now show which way the mean moved
  • Vanished category detection — categories missing from new data flagged explicitly
  • Version banner fixed — fixed-width box, never misaligns on any version string length
  • CI detection — banner auto-suppressed when CI=true or GITHUB_ACTIONS=true
  • --silent CLI flag — suppress update banner in scripts
  • silent_update param in compare() — same for programmatic use
  • JSON output now includes source_info field
  • pyproject.toml classifiers expanded — Python 3.8–3.13, better discoverability
  • vanished_categories in all output formats (terminal, HTML, TXT, JSON)

v0.9.0 (previous)

  • 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
  • Auto version check against PyPI, 24h cached
  • Health score hard-cap — any HIGH column caps score at ≤50
  • Missing column warnings — schema mismatches shown explicitly
  • Mixed-type column warnings — columns 50-80% numeric now warn
  • Timestamp + source in all reports
  • Chi-square O(n²) → O(n)

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

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.

Metadata

Release files for psiwatch 0.15.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for psiwatch 0.15.0
File Size Uploaded
psiwatch-0.15.0.tar.gz 106.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for psiwatch 0.15.0
File Interpreter ABI Platform
psiwatch-0.15.0-py3-none-any.whl Python 3 none any Details

Total release size: 181.8 kB

Release files / psiwatch-0.15.0.tar.gz

Download URL psiwatch-0.15.0.tar.gz
Size 106.1 kB
Tags Source
SHA-256 checksum
How to use checksums
e62249f4125e605b479a85b7bf37abb11fb4f75d447df5524a60caec8a2a2fe3
BLAKE2b-256 checksum
How to use checksums
e616467da01e003f5a3dd5e6f3c992d5e6845bfeaecdb8a0a436caf8ced71f47
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-requests/2.34.2

Release files / psiwatch-0.15.0-py3-none-any.whl

Download URL psiwatch-0.15.0-py3-none-any.whl
Size 75.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c76a67e70c4e8aa82210ca25879d444235c9021a8226f0795fd021acd889d2c6
BLAKE2b-256 checksum
How to use checksums
f8b23c86470cae6717de9d1146c94ff664117f4b21e802966e1bc12c90765ead
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via python-requests/2.34.2

Release history Release notifications | RSS feed

This release

0.15.0 This release

2 release files

0.14.0

2 release files

0.13.0

2 release files

0.12.2

2 release files

0.12.1

2 release files

0.12.0

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page