machine-health
One continuously updated 0-100 health score per machine, built from every sensor you have and the limits you already know, with the reasoning attached so you can act on it.
Install
pip install machine-health
Quickstart
import numpy as np, pandas as pd
from machine_health import score
rng = np.random.default_rng(0)
temp = np.r_[rng.normal(60, 1, 800), rng.normal(63, 1.5, 200)] # the last fifth runs hot
vib = np.r_[rng.normal(0.2, 0.02, 800), rng.normal(0.24, 0.025, 200)]
result = score(pd.DataFrame({"temp": temp, "vibration": vib}), rules={"temp": {"max": 70, "warn_max": 65}})
print(result.summary())
machine health: 78.2 / 100 (grade C, trend degrading)
window: 800 row(s), baseline: 200 row(s)
channels (2): temp, vibration
components (score, weight, points lost):
stability 68.3 weight 0.30 -9.50
compliance 90.0 weight 0.30 -3.00
anomaly 53.8 weight 0.20 -9.25
availability 100.0 weight 0.20 0.00
points lost by channel:
temp -16.19
vibration -5.57
violations (1):
[warning] temp above warn_max=65 on 10 of 800 rows (1.2%), worst 66.73
result.value is the number to put on a dashboard; everything else on the object says where
it came from. The points lost are split two ways - result.component_penalties (stability,
compliance, anomaly, availability) and result.contributors (temp, vibration) - and each
split adds up to exactly result.penalty, which is 100 - result.value. The printed figures
above are those two splits rounded to two decimals.
What it scores
Four components, each reported separately on its own 0-100 scale, each measured against a baseline period of rows you call healthy (by default the first 20% of the data):
- stability - how far each channel has moved from the way it behaved in the baseline, in
either of the two ways it can move, whichever costs more. It swings more: a channel
swinging
4xmore than it used to scores 0, and varying less than baseline is never penalised. Or it has settled at another level: a move of up to one baseline spread is free, 1000 spreads scores 0, logarithmic in between. The second half matters because a channel that steps to a steady wrong value swings exactly as much as it always did. - compliance - whether your own limits hold. Hard limits (
min/max) are critical, soft limits (warn_min/warn_max) are warnings, and a single breach always costs something, so one bad reading in a million rows does not vanish into the average. - anomaly - the share of readings that are outliers against the baseline (robust z above 3.5, median and MAD based). A channel with 10% or more outlying readings scores 0.
- availability - missing readings, plus a penalty for a channel that used to move and is now flatlined, which is what a dead sensor looks like.
The components are combined with weights (stability 0.30, compliance 0.30, anomaly 0.20, availability 0.20 by default) into value = 100 - sum(weight * points lost), so the score is
always between 0 and 100.
Grades are fixed bands, not a curve: A >= 90, B >= 80, C >= 70, D >= 60, F below 60.
result.ok is True when the grade is C or better and no hard limit was broken.
A component that cannot be measured is not guessed. With no rules there is no compliance
score; with a one-row baseline there is no stability score. The weight of a component that
could not be measured is shared out over the ones that could, result.unmeasured names it,
result.to_dict()["components"] reports it as null rather than a free 100, and a note in
result.notes says so. That is why an all-NaN table scores 0 on availability alone instead
of inheriting three free 100s.
Without rules the score is statistical, not absolute. It knows only how the machine
behaves now compared with its own healthy period - nothing about what its readings mean.
That is enough to catch extra noise, outliers, a level shift and dead sensors, but a channel
whose baseline was never healthy in the first place will look fine. Pass your own limits
(rules={"temp": {"max": 80}}) whenever you have them; they are the only part of the score
that knows what the numbers are supposed to be.
API
score(df, *, time=None, channels=None, rules=None, weights=None, baseline=None) -> MachineScore
df- a DataFrame or a path to a.csv/.tsv/.parquetfile; one row per reading, one column per channel.time- name of the timestamp column. Rows are sorted by it, violations report when they first happened, andresult.whencarries the newest timestamp.channels- which columns to score; defaults to every numeric column. A channel a rule names is always included.rules-{"temp": {"max": 80}}, aRule, or a list of either. A rule naming a channel that is not in the data raisesValueErrorlisting the channels that are.weights-{"compliance": 0.5}; missing names keep their default. Weights that do not sum to 1 are normalized and a note is recorded.baseline- the healthy period: a DataFrame or file path, a fraction (0.3), a row count (500), or a boolean mask over the rows. Default: the first 20% of the rows.
HealthScorer(time=..., channels=..., rules=..., weights=..., baseline=...) is the same thing
as an object, when you want to keep the settings and call .score(df) repeatedly.
MachineScore
.value- 0-100;.grade-"A"to"F";.penalty-100 - value.components-{"stability": 68.3, "compliance": 90.0, ...}, each 0-100.weights- the weight each component actually got;.component_penalties- points each cost.contributors-{channel: penalty points}, summing to.penalty;.top_contributors(n).violations- list ofViolation(channel, limit, value, severity, count, n_rows, fraction, worst, first_time), critical first;.critical_violations.trend-"improving","stable"or"degrading": the two halves of the window compared by how far each sits from the baseline (level shift, extra spread, missing readings and breached rules, in baseline spreads). It compares the halves with each other, not with the score, so a half that straddles a step never outranks the steady bad level that follows it.unmeasured- components that could not be scored at all;.notes- everything the scorer decided for you;.channels,.n_rows,.n_baseline_rows,.when,.ok.summary()- the text above;.to_dict()- JSON-safe, including the grade bands
Rule
from machine_health import Rule, score
score(df, rules=[Rule("temp", max=80, warn_max=75), Rule("vibration", max=2.5, weight=2.0)])
Rule(channel, min=None, max=None, warn_min=None, warn_max=None, weight=1.0). weight sets
how much the rule counts against the other rules inside the compliance component.
HealthMonitor
from machine_health import HealthMonitor
monitor = HealthMonitor(healthy_df, rules={"temp": {"max": 70}}, time="ts")
for batch in incoming_batches:
result = monitor.update(batch) # same baseline every time, so scores are comparable
print(monitor.history) # one row per update
print([a.summary() for a in monitor.alerts])
HealthMonitor(baseline_df, rules=None, weights=None, *, time=None, channels=None) keeps the
baseline fixed and scores each batch exactly the way score() does.
.update(batch)- aMachineScore. An empty batch returns the previous score unchanged with a note instead of raising. From the second update on,.trendcompares the batch with the mean of the previous three..history- DataFrame:update, when, value, grade, trend, stability, compliance, anomaly, availability, rows, violations. A component that was not measured isNaNin its column (with no rules, the wholecompliancecolumn is), never a free 100.alerts-Alert(when, severity, message), raised when the grade drops a level (critical for two levels or a drop to F) or a rule is violated.baseline_score- the baseline scored against itself, the starting point for comparisons.last_score,.summary(),.to_dict()
CLI
machine-health telemetry.csv
machine-health telemetry.csv --time ts --rule "temp:max=80,warn_max=75" --rule "vibration:max=2.5"
machine-health telemetry.csv --json --output health.json
machine-health telemetry.csv --baseline 0.3 --weight compliance=0.5 --fail-under 70
--rule is repeatable and takes channel:key=number pairs (min, max, warn_min,
warn_max, weight); --rules FILE reads the same thing from JSON. --baseline takes a
fraction, a row count or a path to a table of healthy rows. The exit status is 0 unless
--fail-under SCORE is given and the score is below it (2 on a bad argument or unreadable
file), so it drops straight into a shell check.
License
MIT
Metadata
Release files for machine-health 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| machine_health-0.1.0.tar.gz | 39.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| machine_health-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 72.4 kB
Release files / machine_health-0.1.0.tar.gz
| Download URL | machine_health-0.1.0.tar.gz |
|---|---|
| Size | 39.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
893dcccf9fce87478d363293c31c3028516d213cb2fdbb08cc58157f6f7c594e
|
|
BLAKE2b-256 checksum How to use checksums |
aa79ca7cd8811df3cd960f070d4027cbd418333cb89b7d9cf5a3d9d2ce2e1dc4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / machine_health-0.1.0-py3-none-any.whl
| Download URL | machine_health-0.1.0-py3-none-any.whl |
|---|---|
| Size | 32.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
de9a5962b36d9e6c8244b3e7b3f9ccc2e73d1b738669f70c3cc6d9e8fb43ce0f
|
|
BLAKE2b-256 checksum How to use checksums |
ad5c9b9815219a19730bd62eee78dff485be1bd174db7798ccd5339aa99ec85e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|