Skip to main content

sensor-anomaly

Spot abnormal behaviour across many industrial sensor channels at once, including the faults that are only visible between channels and the dead sensors that no outlier test will ever flag.

Install

pip install sensor-anomaly

Quickstart

import numpy as np, pandas as pd, sensor_anomaly

rng = np.random.default_rng(0)
df = pd.DataFrame({"temp": rng.normal(70, 1, 500), "flow": rng.normal(12, 0.5, 500)})
df.loc[300:340, "flow"] = 0.0          # this sensor died and now reads zero
report = sensor_anomaly.detect(df)
print(report.summary())

That is the whole thing. report.summary() prints which channels misbehaved, the events worth looking at, and a plain-words note for every fault code it used.

What it checks

Three passes over the same table, all landing in one report:

  • Each channel on its own - a robust z-score of the rolling residual. Each reading is judged against its own recent level, so a slow drift or a daily cycle does not bury the result in false alarms, and a handful of genuine spikes cannot inflate the threshold the way a standard deviation would.
  • All channels together - an IsolationForest over the standardized channels and over each channel's residual against what the others predict for it. This is the pass that catches a fault where no single reading is odd: flow falling while current climbs, both still inside their normal ranges. That broken correlation is invisible per channel. The cut is set from how far a reading strays from what its neighbours predict, corrected for how many readings the table contains, so sensitivity means "the chance a whole clean table trips at all" rather than a chance per row. Redundant sensors that all track one process variable - the normal case in a plant - stay quiet.
  • Sensor faults that are not outliers at all - a dead sensor is not unusual, it is boringly consistent:
    • flatline - the reading stopped changing for a long run
    • stuck-at-zero - it sat at exactly zero
    • railed-at-min / railed-at-max - pinned against its measurement limit
    • missing-burst - a run of consecutive samples went missing
    • step-change - the level jumped and stayed there (a recalibration or a swap)
    • all-missing / no-numeric-data - a failed sensor, reported rather than crashed on

It also handles the awkward cases without argument: channels full of gaps, an all-NaN channel, a channel that never moves, a table with a header and no rows, and a single-channel table (the cross-channel pass is skipped and the report says so). Your DataFrame is never modified.

A clean table says so. At sensitivity=3.0 a 3-sigma test flags about 0.27% of perfectly normal readings by definition; the report counts those as the noise floor, says so in its notes, and leaves report.anomalous False.

IsolationForest cannot read a missing value, so each channel is imputed with its own median before fitting rather than silently dropping every row that has a gap. The report says how many readings were filled and in which channels.

API

detect(df, *, time=None, channels=None, sensitivity=3.0, contamination="auto", random_state=0) -> SensorReport

argument meaning
df wide DataFrame, one column per channel, or a path to .csv / .tsv / .parquet
time timestamp column. Left as None, an obvious one is detected, reported, and never scored as a channel
channels which columns are sensors. Left as None, every numeric column is used and anything skipped is named in the notes
sensitivity per-channel threshold in robust sigmas. Higher flags less. Try 4-5 on noisy plant data
contamination "auto", or the share of rows you expect to be anomalous
random_state seed, so the same table always gives the same answer

SensorReport

member what it gives you
.channels dict[name -> ChannelResult]
.joint row positions flagged by the cross-channel model
.events merged Event records, worst first
.worst_channels channel names, worst first: failed, then faulty, then by rate
.anomalous one boolean to gate an alarm on: False when every flag is inside the false-positive rate sensitivity itself predicts
.quiet the other side of the same judgement: True when the flags are noise
.n_events, .n_rows, .method, .failed_channels at a glance
.summary() the human report, plain ASCII, safe to pipe anywhere
.to_dict() JSON-safe dict of everything
.to_frame() one row per channel: status, anomalies, rate, peak score, faults
.events_frame() one row per event
.notes what was skipped, assumed, imputed or fallen back to

ChannelResult holds .anomalies, .rate, .score_max, .faults, .status, .n_valid, .n_missing and .notes. Event holds .start, .end, .channels, .kind, .severity, .level and .describe().

report.channels["flow"].faults          # ['flatline', 'stuck-at-zero', 'step-change']
report.worst_channels[0]                # 'flow'
report.channels_with("flatline")        # ['flow']
[e.describe() for e in report.events]   # one readable line each
report.events_frame()                   # a DataFrame of the same thing

Same settings across many tables:

detector = sensor_anomaly.Detector(sensitivity=4.0, time="timestamp")
report = detector.detect(batch)

CLI

sensor-anomaly plant.csv
sensor-anomaly plant.csv --time timestamp --sensitivity 4
sensor-anomaly plant.csv --channels temp,flow,vibration
sensor-anomaly plant.csv --json > findings.json
sensor-anomaly plant.csv --output channels.csv

--output writes the per-channel table as .csv / .tsv / .parquet, or the whole report when the path ends in .json. Run sensor-anomaly --help for the rest.

License

MIT

Release files for sensor-anomaly 0.1.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 sensor-anomaly 0.1.0
File Size Uploaded
sensor_anomaly-0.1.0.tar.gz 42.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sensor-anomaly 0.1.0
File Interpreter ABI Platform
sensor_anomaly-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 79.7 kB

Release files / sensor_anomaly-0.1.0.tar.gz

Download URL sensor_anomaly-0.1.0.tar.gz
Size 42.5 kB
Tags Source
SHA-256 checksum
How to use checksums
5fee0335cf093a91b919242e9aaa28df9379825080dd2c0b581c9a507b4ad4aa
BLAKE2b-256 checksum
How to use checksums
dbb4dfe408c0e1da547181c78df00662077e7584ecf2deb69972a8435671671a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / sensor_anomaly-0.1.0-py3-none-any.whl

Download URL sensor_anomaly-0.1.0-py3-none-any.whl
Size 37.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
51055da0d9225ea124a0f04be5217dfb4217585a211c459c203d2688e786217c
BLAKE2b-256 checksum
How to use checksums
20124fa90376c238de772f8a0bb5cca9e3dc11c51f23a61ee139a3f006dff5b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.1.0 This release

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