Spaces:
Sleeping
title: Real-Time Anomaly Detection
emoji: 📡
colorFrom: blue
colorTo: indigo
sdk: docker
app_port: 7860
pinned: false
Real-Time Anomaly Detection on Time-Series Streams
Live demo: anomaly.zeetay.dev
Online anomaly detection on a multivariate sensor stream with concept drift handling. Each observation is scored once as it arrives. No batch retraining.
Problem
Sensor distributions shift over time. A static model will either miss anomalies in the new regime or start flagging normal post-drift readings as anomalous. This project fits a baseline during a stable warmup window, then scores every subsequent observation against it while monitoring for distribution shifts.
Approach
- Synthetic stream: Three-phase generator (normal baseline, gradual drift, injected anomalies) with configurable rates and ground truth labels.
- Online scorer: Mahalanobis distance from a frozen Phase A baseline. Uses the full sensor covariance matrix, so it catches contextual anomalies (anti-correlated sensor readings) that per-feature z-score methods miss entirely.
- Drift detection: ADWIN watches a normalized composite sensor signal. When the distribution shifts (Phase B), ADWIN fires. The anomaly baseline is kept since it was calibrated before drift started.
- Running metrics: Precision, recall, and F1 are updated after every observation against ground truth labels.
- Real-time UI: FastAPI + WebSocket streams each scored observation to a dashboard with rolling charts, anomaly/drift markers, and live counters. New visitors get a replay of the last 200 points.
Results (offline benchmark)
Evaluated on Phase C (300 obs, 10% anomaly rate, seed=42):
| Method | Precision | Recall | F1 | Mode |
|---|---|---|---|---|
| Mahalanobis + ADWIN | 0.833 | 1.000 | 0.909 | online |
| Isolation Forest | 0.476 | 1.000 | 0.645 | batch |
| Z-Score (max |z|>2.5) | 0.450 | 0.600 | 0.514 | batch |
Z-Score's recall is limited by contextual anomalies where no single sensor exceeds the 2.5σ threshold. Mahalanobis catches these via the covariance term.
Tech stack
- Python 3.10+
- River - online drift detection (ADWIN)
- numpy / scikit-learn - Mahalanobis scoring, Isolation Forest baseline
- FastAPI - REST + WebSocket, static dashboard
- asyncio - async stream and pipeline
- pytest - tests for stream, detector, pipeline, and server
Setup and run
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux/macOS
pip install -r requirements.txt
python run.py
Open http://localhost:8000/. The pipeline starts on first connection and loops automatically.
Deploy
- Frontend: anomaly.zeetay.dev — Netlify, auto-deploys from
main - Backend: huggingface.co/spaces/zeetay/anomaly-detection — HF Spaces (Docker)
The frontend is static files served by Netlify. The backend is a FastAPI + WebSocket server running in a Docker container on HF Spaces. dashboard.js connects directly to the HF Space WebSocket from any non-HF host.
Docker locally: docker build -t anomaly . && docker run -p 7860:7860 anomaly
Tests
pytest
All tests use the synthetic stream and need no external services.
Project layout
| Path | Role |
|---|---|
src/stream/ |
Observation model, async synthetic generator (phases A/B/C) |
src/detector/ |
Anomaly scorer (Mahalanobis), drift detector (ADWIN), running metrics |
src/pipeline/ |
Async pipeline: stream, score, broadcast, replay buffer |
src/server/ |
FastAPI app, WebSocket, /stats, /metrics (Prometheus), static dashboard |
tests/ |
Pytest for stream, detector, pipeline, dashboard |
Design notes
- Mahalanobis over Z-Score: Z-Score tests each sensor independently. Mahalanobis uses the covariance matrix, so readings that look moderate in isolation but are unusual as a combination (e.g., temp up + pressure down when they normally move together) score high.
- ADWIN on sensor signal, not anomaly scores: Watching the normalized sensor deviation catches sustained distribution shifts without false-firing on isolated anomaly spikes.
- Replay on connect: New clients get the last 200 messages immediately, then live updates.
- Start on first connect: The pipeline only runs while at least one client is connected. Counters reset each cycle.