The engineering story behind the predictions — data, features, training, deployment.
YieldGuard is an end-to-end predictive maintenance system that ingests sensor streams from industrial machines (PLCs, SCADA, IoT edge devices), engineers 256+ time-series features, and predicts equipment failure 24 hours in advance with a calibrated probability score.
The system runs entirely in the browser. The LightGBM model is exported to a compact JSON format and scored via a TypeScript tree-walker — no API round-trips, no cold starts, no data leaving the user's device.
Training data was generated by a physics-informed synthetic generator (`src/yieldguard/data/synthesizer.py`). 50 machines × 70 days × 10-minute intervals = 504,000 rows across 6 sensor channels.
Each machine follows a physical signal model with per-channel degradation physics — exponential amplitude ramps for vibration/acoustic, temperature rise via bearing friction, RPM drift. Failure events are randomized (onset timing, magnitude, duration) to avoid a trivially separable dataset.
Signal model: X(t) = μ_machine + seasonal(t) + degradation(t, t_fail) + noise(t) Degradation onset: ~48 hours before failure (randomized ±24h) Magnitude factors: vib ×2.5, temp ×1.8, aco ×2.2 (at peak) Hard negatives: 5–8 recoverable excursions per machine Label noise: 5% random flip on boundary samples Positive class: ~9.3% of total rows
FeatureEngineer(TransformerMixin) computes 256+ features per row. All temporal operations are performed inside groupby('machine_id') — never on a flat concatenated DataFrame — to prevent cross-machine data leakage.
The engineer is joblib-serialized alongside the model at training time and loaded together at inference. This guarantees the exact same feature computation between training and serving.
Rolling stats × 4 windows [6, 12, 36, 144] samples
→ mean, std, range, skew, kurt (30 per channel = 180)
EWMA × 2 spans [12, 72] + deviation (4 per channel = 24)
Lag + diff + pct_change × 4 lags (12 per channel = 72)
pct_change clipped to [-10, 10] (avoids ±∞ at near-zero)
Rate of change (raw + smoothed) (2 per channel = 12)
FFT: energy, dominant_hz, spectral_entropy (3 per channel = 18)
Cross-channel:
vibration × temperature coupling
current draw relative to RPM (power proxy)
pressure / temperature ratio (4 features)
───────────────
Total: 310 → curated to 256Cross-validation uses TimeSeriesExpandingCV with a 24-hour gap between train and validation folds. The gap prevents the model from seeing the run-up to a failure event in both sets simultaneously.
Class imbalance (~9% positive) is handled via scale_pos_weight ≈ 9.8 — no oversampling or SMOTE. The primary metric is PR-AUC (average precision), which is more informative than ROC-AUC for imbalanced data.
Optuna TPE Bayesian HPO runs 50 trials per model, maximizing PR-AUC across folds. The final model is refit on all training data using the median best_iteration across folds (rather than allowing early stopping on a combined dataset, which would cause overfitting).
Probabilities are isotonic calibrated on a held-out fold — the model's raw sigmoid output is a ranking score, not a probability; calibration makes the output honest.
Strategy: TimeSeriesExpandingCV — 5 folds, 24h gap Objective: PR-AUC (primary), ROC-AUC (secondary) Imbalance: scale_pos_weight = n_neg / n_pos ≈ 9.8 HPO: Optuna TPE, 50 trials, pruning enabled Refit: n_estimators = median(best_iteration_ across folds) Calibration: IsotonicRegression on held-out fold
The trained LightGBM model is exported via booster_.dump_model() to a compact JSON structure and served as a static file. A TypeScript tree-walker (web/lib/engine/model.ts) scores the ensemble by recursively walking split nodes, summing leaf values, applying sigmoid, then isotonic calibration.
The feature pipeline (web/lib/engine/features.ts) is a port of the Python FeatureEngineer — rolling statistics, EWMA, lag/diff/pct (clipped), ROC, and a direct DFT (FFT without scipy) for spectral features.
Export: lgb_model.booster_.dump_model() → model.json (~800 KB)
isotonic calibration params → calibration_x/y arrays
feature names + healthy baseline stats → feature_spec.json
5 demo machine series → demo_scenarios.json
TS scorer: O(n × depth) per tree — ~1000 trees × depth 6
Total inference: <50ms on modern hardware
No WASM, no server, no API — pure TypeScriptBrowser (Next.js on Vercel)
Demo page ──┐
Dashboard ──┼──► web/lib/engine/
│ features.ts — 256+ feature computation
│ model.ts — LightGBM tree scorer + calibration
│ explain.ts — risk driver ranking
│ predict.ts — orchestration
│ ▲ reads static JSON artifacts (no API)
│
└ web/lib/engine/
model.json — LightGBM dump_model() export
feature_spec.json — feature metadata + baselines
demo_scenarios.json — 5 sample machine histories
Python (offline training — source of truth)
synthesizer.py — physics-informed synthetic data
preprocessor.py — cleaning, imputation, stuck sensor detection
engineer.py — FeatureEngineer(TransformerMixin) → 256 features
trainer.py — CV + Optuna HPO + calibration
export_model.py — writes engine JSON artifacts
FastAPI (Render — optional, not on critical path)
POST /predict → PerMachineBuffer(288) → features → model → response
POST /explain → SHAP TreeExplainer (on demand)
GET /drift → PSI + KS drift report