Asset health scoring
Effort: ~20–30 minutes · You'll build: a composite 0–100 health index from several signals, with a healthy/watch/critical classification · Stack: the SDK plus a little arithmetic — no model training.
A machine rarely fails on one signal. Vibration is creeping up, the bearing runs a little hot, oil pressure sags at load — each is fine alone, but together they tell a story. A health score rolls those signals into one comparable number so an operator can rank a whole fleet at a glance and a dashboard can show green/amber/red. It's the lightweight cousin of predictive maintenance: no model to train, just a transparent, tunable index.
No background needed. Skim the gentle primer for the ideas in plain language — model, feature, training, and the algorithm itself — and use Generate sample data for a sandbox to run this against.
This reads several pump signals. Generate a sandbox
first — section A ingests pump_07_bearing_temp_c, pump_07_oil_pressure_kpa and a
stand-in pump_07_vibration_anomaly (or run predictive maintenance
to produce the real one).
1. Pull the latest value of each signal
Take the most recent reading (or short average) of each contributing series for the asset.
- Python
- Java
- Rust
import datahub_sdk, pandas as pd
client = datahub_sdk.DataHubClient.from_env()
def latest(external_id):
rf = datahub_sdk.RetrieveFilter(
ts=external_id,
start=pd.Timestamp.now(tz="UTC") - pd.Timedelta(minutes=15),
end=pd.Timestamp.now(tz="UTC"),
aggregates=["avg"], granularity="15m")
pts = client.timeseries.retrieve_datapoints(rf)[0].get_datapoints()
return float(pts[-1].average)
signals = {
"vibration": latest("pump_07_vibration_anomaly"), # 0..~1, from the anomaly model
"bearing_temp": latest("pump_07_bearing_temp_c"),
"oil_pressure": latest("pump_07_oil_pressure_kpa"),
}
// The same retrieve, per signal; the scoring arithmetic below is shown in Python.
var filter = new RetrieveFilter();
filter.setExternalId("pump_07_bearing_temp_c");
filter.setStart(ZonedDateTime.now().minusMinutes(15));
filter.setEnd(ZonedDateTime.now());
filter.setAggregates(List.of("avg"));
filter.setGranularity("15m");
var request = new DataRetriever<RetrieveFilter>();
request.setItems(List.of(filter));
var pts = client.timeseries().retrieve(request).getItems().get(0).getDatapoints();
double bearingTemp = Double.parseDouble(pts.get(pts.size() - 1).getValue());
// The same retrieve, per signal; the scoring arithmetic below is shown in Python.
use dataplatform_rust_sdk::generic::{DataWrapper, RetrieveFilter};
use chrono::Utc;
let filter = RetrieveFilter {
external_id: Some("pump_07_bearing_temp_c".into()),
start: Some(Utc::now() - chrono::Duration::minutes(15)),
end: Some(Utc::now()),
aggregates: Some(vec!["avg".into()]),
granularity: Some("15m".into()),
..Default::default()
};
let s = api.time_series
.retrieve_datapoints(&DataWrapper::from(vec![filter])).await?
.get_items().remove(0);
2. Normalise each signal to a "badness" in 0–1
Every signal lives on its own scale, so map each to a common 0 (fine) → 1 (alarm) range against its healthy and limit values. A reading at or below healthy scores 0; at or above the limit scores 1; in between, it ramps linearly.
def badness(value, healthy, limit):
if limit == healthy:
return 0.0
return max(0.0, min(1.0, (value - healthy) / (limit - healthy)))
LIMITS = { # (healthy, limit) per signal
"vibration": (0.05, 0.30),
"bearing_temp": (60.0, 90.0),
"oil_pressure": (350.0, 250.0), # inverted: lower is worse
}
parts = {k: badness(v, *LIMITS[k]) for k, v in signals.items()}
3. Weight, combine, and classify
Weight the signals by how much each matters for this asset class, combine into a 0–100 score (100 = perfect health), and bucket it.
WEIGHTS = {"vibration": 0.5, "bearing_temp": 0.3, "oil_pressure": 0.2}
badness_total = sum(parts[k] * WEIGHTS[k] for k in parts)
score = round(100 * (1 - badness_total), 1)
band = "healthy" if score >= 80 else "watch" if score >= 60 else "critical"
4. Publish the score and flag the bad ones
Write the score back as its own series — now you can chart, rank and subscribe to asset
health like any other signal — and raise an event when an asset drops to critical.
client.timeseries.create([datahub_sdk.TimeSeries(
external_id="pump_07_health_score", name="Pump 07 health score", unit="score", value_type="float")])
client.timeseries.insert_from_lists(
timestamps=[pd.Timestamp.now(tz="UTC")], values=[score], ts="pump_07_health_score")
if band == "critical":
client.events.create([datahub_sdk.Event(
external_id=f"health_critical_pump_07_{int(pd.Timestamp.now().timestamp())}",
type="health_critical", status="open",
event_time=pd.Timestamp.now(tz="UTC"),
metadata={"asset": "pump_07", "score": str(score),
"worst_signal": max(parts, key=parts.get)})])
Run it across the fleet on a schedule and you have a single ranked health view —
worst_signal in the metadata tells maintenance why each asset is red.
Where to take it further
- Feed it the model. Swap the hand-set vibration limits for the anomaly score as a direct input.
- Trend the score. A falling health score over days is itself a predictor — forecast it like any other series.
- Roll up the graph. Average child scores up a site graph for a line- or plant-level health number.
Further reading
- Normalising signals to a common scale — Feature scaling
- The ideas in plain language — Machine learning, gently
See also
- Query & aggregate — pulling the contributing signals.
- Predictive maintenance — a learned input to the score.
- Turn readings into events — flagging critical assets.