Robust anomaly detection on MAD and the Modified Z-Score, with seasonal baselines
Project description
madwatch
Robust anomaly detection that doesn't panic at paydays.
madwatch scores time-series values with the Modified Z-Score over MAD
(Median Absolute Deviation) instead of mean/standard deviation. Medians don't
care about your whale transaction: one huge outlier can't inflate the baseline
and mask the next real anomaly.
Install
pip install madwatch # core, numpy only
pip install 'madwatch[cli]' # + CLI, pandas, matplotlib
Quickstart
from madwatch import RollingDetector
det = RollingDetector(window=40, threshold=3.5, min_samples=10)
for value in stream:
score = det.update(value)
if score.is_anomaly:
alert(value, score.z)
Why MAD?
Standard deviation has a design flaw for anomaly detection: the anomaly you are
trying to catch is inside the calculation. One spike inflates sigma, the
threshold stretches, and the next three real anomalies walk through undetected.
MAD is median-based, so a single outlier in the window barely moves the
baseline. The 0.6745 constant makes the score comparable to a classic z-score
on normal data, so the usual "flag at 3.5" rule still reads naturally.
Longer version: Why MAD instead of standard deviation?
Seasonal baselines
Mondays don't look like Saturdays and 9 AM doesn't look like midnight. Comparing
a value against a global baseline produces false alarms at every weekly rhythm.
SeasonalBaseline buckets history by day-of-week and/or hour and scores each
point against its own bucket:
from madwatch import SeasonalBaseline
sb = SeasonalBaseline(granularity="dow_hour").fit(timestamps, values)
z = sb.score(new_timestamps, new_values)
In production use on financial streams, this combination cut false positives by roughly 60% compared to a naive z-score.
CLI
madwatch data.csv --column amount --window 40 --threshold 3.5 --plot out.png
madwatch data.csv --column amount --timestamp ts --seasonal dow_hour
where value z
2026-02-07T12:00:00 512.00 9.41
1 anomalies in 312 points
API
| Name | What it does |
|---|---|
mad(x) |
Median Absolute Deviation of an array |
modified_zscore(x, scale=0.6745) |
Per-element robust z-score |
RollingDetector(window, threshold, min_samples) |
Streaming detection over a trailing window |
SeasonalBaseline(granularity) |
Per-bucket (dow/hour) baselines with global fallback |
Behavior notes: constant windows (MAD = 0) score z = 0; the detector stays
silent until min_samples values have arrived; NaN input raises ValueError
(the CLI skips NaN rows with a warning).
Development
uv venv && uv pip install -e '.[dev]'
pytest
ruff check src tests
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file madwatch-0.1.0.tar.gz.
File metadata
- Download URL: madwatch-0.1.0.tar.gz
- Upload date:
- Size: 64.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bfecc93a4641c9c62ad53d10c9aa3be993a41890435802420f53a34d9db654da
|
|
| MD5 |
9163d56a116cc3cbe1d4f07724544727
|
|
| BLAKE2b-256 |
1bac7998719fc6624b0d0204939dd19adf88023f7297611567beff1a05d28ec7
|
Provenance
The following attestation bundles were made for madwatch-0.1.0.tar.gz:
Publisher:
release.yml on efekckk/madwatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
madwatch-0.1.0.tar.gz -
Subject digest:
bfecc93a4641c9c62ad53d10c9aa3be993a41890435802420f53a34d9db654da - Sigstore transparency entry: 1802593856
- Sigstore integration time:
-
Permalink:
efekckk/madwatch@559021e89fda1392785c6b94753a7f19198c4aeb -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/efekckk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@559021e89fda1392785c6b94753a7f19198c4aeb -
Trigger Event:
push
-
Statement type:
File details
Details for the file madwatch-0.1.0-py3-none-any.whl.
File metadata
- Download URL: madwatch-0.1.0-py3-none-any.whl
- Upload date:
- Size: 8.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67213c02ecd4cc3bef9c35d996c74122d527c5f02b63d066650e71365a63ade1
|
|
| MD5 |
9e4c7e6268a320329545cd90e802d879
|
|
| BLAKE2b-256 |
e551773b2b43ebaa68fb7e7bcf86371da27f8a83c75bf6c606bb6444cfd06a61
|
Provenance
The following attestation bundles were made for madwatch-0.1.0-py3-none-any.whl:
Publisher:
release.yml on efekckk/madwatch
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
madwatch-0.1.0-py3-none-any.whl -
Subject digest:
67213c02ecd4cc3bef9c35d996c74122d527c5f02b63d066650e71365a63ade1 - Sigstore transparency entry: 1802594847
- Sigstore integration time:
-
Permalink:
efekckk/madwatch@559021e89fda1392785c6b94753a7f19198c4aeb -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/efekckk
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@559021e89fda1392785c6b94753a7f19198c4aeb -
Trigger Event:
push
-
Statement type: