- Rule-based systems fail when you add new machines, change operating conditions, or encounter unforeseen failure modes — ML adapts by learning thresholds from data instead of hardcoding them.
- The 4-week migration runs ML predictions parallel to existing rules: extract 20-30 time/frequency features, train Random Forest with class balancing, deploy alongside legacy thresholds for validation.
- Optimize for lead time (7-14 days) instead of accuracy — use hybrid logic that combines hard safety rules, ML early warnings, and rule-based fallbacks for edge cases.
- Start with Random Forest on engineered features before LSTMs — simpler models fail predictably, give feature importances, and run on edge devices under 10ms latency.
The Threshold That Breaks Rule-Based Systems
Your vibration alarm triggered 47 times last month. Twelve were false positives. Three real failures slipped through because they didn’t cross your fixed 10mm/s RMS threshold.
Rule-based condition monitoring works until it doesn’t. The moment you add a second machine model, change bearing suppliers, or shift production schedules, those carefully tuned thresholds become guesses. I’ve seen teams spend weeks adjusting rules only to have them break again when ambient temperature changed by 10°C.
Machine learning doesn’t eliminate thresholds — it learns them from data. The migration isn’t trivial, but it’s methodical. Here’s what actually changes when you move from “if vibration > X then alert” to models that predict failures weeks in advance.

What Rule-Based Systems Actually Do Well
Before ripping out your existing logic, understand where rules still win.
They’re deterministic. When the RMS velocity exceeds 10mm/s on a pump bearing, you get an alert. Every time. No training data required, no model drift, no black box explanations to skeptical engineers.
They’re fast. A simple threshold check runs in microseconds on an edge device. No GPU, no cloud latency, no batch inference pipelines.
And they fail predictably. When your rule misses a fault, you know exactly why — the signal didn’t cross the line. When a neural network misses it, you’re debugging gradients and activation maps.
The problem isn’t that rules are bad. It’s that they don’t scale.
Add ten machines with different load profiles and you’re managing hundreds of thresholds. Seasonal temperature variations? Multiply that by four. New failure modes that don’t fit your existing bands? Start over.
ML trades determinism for adaptability. Instead of “alert if “, you get “alert if “. More flexible, more accurate under variation, but also more complex to deploy.
The 4-Week Migration Plan I Actually Use
This isn’t a complete rewrite. You keep your existing monitoring running while building the ML layer in parallel.
Week 1: Baseline data collection. Pull historical sensor logs that include both normal operation and known failures. You need at least 3-6 months of data with clear failure timestamps. The CWRU bearing dataset works for proof-of-concept, but real migration requires your actual machines — different housings, different lubricants, different failure signatures.
Label your data honestly. If you’re not sure when degradation started, mark it as uncertain. I’ve seen teams label data as “healthy” up until catastrophic failure, then wonder why their model can’t predict anything. Bearings don’t go from perfect to seized in one sample.
import pandas as pd
import numpy as np
from scipy import signal
from sklearn.preprocessing import StandardScaler
# Load raw vibration data (assume 25.6 kHz sampling rate)
df = pd.read_parquet('pump_bearing_2024.parquet')
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp')
# Known failure event: 2024-11-03 14:22:00
failure_time = pd.Timestamp('2024-11-03 14:22:00')
# Label degradation window (conservative 7-day lead time)
df['label'] = (df['timestamp'] > failure_time - pd.Timedelta(days=7)) & \
(df['timestamp'] <= failure_time)
df['label'] = df['label'].astype(int)
print(f"Healthy samples: {(df['label']==0).sum()}")
print(f"Degraded samples: {(df['label']==1).sum()}")
Expect severe class imbalance. Healthy operation dominates. If you have 99.5% normal data, your “always predict healthy” baseline gets 99.5% accuracy and teaches you nothing.
Week 2: Feature engineering. Your rule-based system probably used RMS, peak-to-peak, and maybe crest factor. ML models benefit from richer features, but diminishing returns kick in fast.
Time domain: RMS, kurtosis (), skewness, peak-to-peak. Frequency domain: FFT bins at bearing fault frequencies (BPFO, BPFI, BSF, FTF), spectral entropy .
I typically extract 20-30 features per sensor. More than 50 and you’re chasing noise unless you have massive datasets.
def extract_features(window, fs=25600):
"""Extract time and frequency features from vibration window."""
features = {}
# Time domain
features['rms'] = np.sqrt(np.mean(window**2))
features['peak'] = np.max(np.abs(window))
features['kurtosis'] = ((window - window.mean())**4).mean() / window.std()**4
features['skewness'] = ((window - window.mean())**3).mean() / window.std()**3
features['crest_factor'] = features['peak'] / features['rms']
# Frequency domain
f, psd = signal.welch(window, fs=fs, nperseg=1024)
features['spectral_entropy'] = -np.sum(psd * np.log(psd + 1e-12))
# Bearing fault frequencies (example: 1800 RPM, 8-ball bearing)
# BPFO = (N_balls / 2) * (RPM / 60) * (1 + (d_ball / d_pitch) * cos(alpha))
# Simplified example: BPFO ~ 107 Hz
bpfo_idx = np.argmin(np.abs(f - 107))
features['bpfo_power'] = psd[bpfo_idx-2:bpfo_idx+3].sum() # 5-bin window
return features
# Rolling window feature extraction (1-second windows, 50% overlap)
window_size = 25600 # 1 second at 25.6 kHz
step = 12800 # 50% overlap
feature_list = []
for i in range(0, len(df) - window_size, step):
window_data = df.iloc[i:i+window_size]['acceleration'].values
window_label = df.iloc[i+window_size]['label'] # label from end of window
feats = extract_features(window_data)
feats['label'] = window_label
feature_list.append(feats)
feature_df = pd.DataFrame(feature_list)
print(feature_df.head())
This is where rule-based and ML diverge. Your old system looked at one feature (RMS). The ML model sees all of them simultaneously and learns which combinations matter.
Week 3: Model training and validation. Start simple. Random Forest before LSTM. Logistic regression before Transformer. Complex models fail in complex ways, and you need to understand baseline performance first.
For initial deployment, I’d use Random Forest or Gradient Boosting (XGBoost, LightGBM). They handle tabular features well, train fast, and give feature importances that help you explain predictions to operators.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score, precision_recall_curve
import matplotlib.pyplot as plt
# Split data (time-aware — don't shuffle!)
X = feature_df.drop(columns=['label'])
y = feature_df['label']
# Use first 70% for training, last 30% for test (maintains temporal order)
split_idx = int(0.7 * len(X))
X_train, X_test = X[:split_idx], X[split_idx:]
y_train, y_test = y[:split_idx], y[split_idx:]
print(f"Train: {len(X_train)}, Test: {len(X_test)}")
print(f"Test failure rate: {y_test.mean():.3f}")
# Handle class imbalance with class weights
model = RandomForestClassifier(
n_estimators=200,
max_depth=15,
class_weight='balanced', # upweight minority class
random_state=42,
n_jobs=-1
)
model.fit(X_train, y_train)
# Evaluate
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1]
print(classification_report(y_test, y_pred))
print(f"ROC-AUC: {roc_auc_score(y_test, y_proba):.3f}")
# Feature importances
importances = pd.DataFrame({
'feature': X.columns,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
print("\nTop 5 features:")
print(importances.head())
Don’t shuffle your train/test split. PHM is a time-series problem — you’re predicting the future, not randomly held-out samples. If you shuffle, you leak future information into training and get falsely optimistic metrics.
My best guess is that kurtosis and BPFO power will dominate feature importance for bearing faults, but every machine is different. If spectral entropy ranks high, you might have non-stationary noise issues that a simple threshold would miss entirely.
Week 4: Parallel deployment. Keep your rule-based alerts running. Add ML predictions as a second channel. Log both outputs side-by-side for a month.
# Production inference loop (simplified)
def predict_health(vibration_window, model, scaler):
"""Run ML model inference on new sensor data."""
features = extract_features(vibration_window)
X_new = pd.DataFrame([features])
# Apply same scaling as training
X_scaled = scaler.transform(X_new)
# Get probability of failure
prob = model.predict_proba(X_scaled)[0, 1]
return prob
# Legacy rule
def rule_based_check(vibration_window):
rms = np.sqrt(np.mean(vibration_window**2))
return rms > 10.0 # mm/s threshold
# Parallel monitoring
for new_window in realtime_stream:
ml_prob = predict_health(new_window, model, scaler)
rule_alert = rule_based_check(new_window)
# Log both for comparison
logger.info(f"ML prob: {ml_prob:.3f}, Rule alert: {rule_alert}")
# Conservative strategy: alert if EITHER triggers
if ml_prob > 0.7 or rule_alert:
send_maintenance_alert()
This parallel phase is critical. You’ll find edge cases where the model fails (sensor drift, new failure modes, data pipeline bugs). You’ll also find cases where the model catches degradation 2-3 weeks earlier than your threshold did.
Where ML Models Fail (and How to Catch It)
The first time my ML-based system missed a failure, it was because of something the rule-based system would’ve caught: a sensor cable came loose and the signal dropped to near-zero. The model, trained on normal vibration patterns, saw low-amplitude signals and predicted “healthy.”
Rule-based systems fail gracefully when the world changes slightly. ML models fail catastrophically when the world changes in ways they’ve never seen.
Sensor drift. If your accelerometer bias drifts by 0.5 m/s² over six months, your trained model’s RMS feature shifts. Sudden drop in prediction confidence or rise in false alarms? Check sensor calibration dates.
Concept drift. You trained on summer data (20-30°C ambient). Winter arrives (5-15°C). Bearing clearances change, lubricant viscosity changes, vibration baselines shift. The model’s learned decision boundary doesn’t move.
Fix: retrain quarterly with recent data, or use online learning methods (though those add complexity).
New failure modes. Your model learned inner race defects, outer race defects, and ball defects. A lubrication failure presents completely different signatures (high-frequency noise, temperature rise). The model has no idea what to do.
Fix: keep the rule-based temperature threshold as a safety net.
Class imbalance extremes. If you have 10,000 hours of healthy data and 2 hours of degraded data, even SMOTE and class weights struggle. The model memorizes “predict healthy” because it’s right 99.98% of the time.
Fix: oversample degraded windows aggressively, or switch to anomaly detection (train only on healthy data, flag deviations). I covered the VAE approach in another post — it works when you literally have zero labeled failures.

The Metrics That Actually Matter
Accuracy is a trap. With 99% healthy data, predicting “always healthy” gives 99% accuracy.
Precision and recall are better, but you need to pick one to optimize. High precision = fewer false alarms, but you might miss some failures. High recall = catch all failures, but operators get alert fatigue from false positives.
For critical assets (turbines, compressors), optimize recall. Missing a failure costs millions. For non-critical pumps, optimize precision — you don’t want maintenance chasing ghosts.
The metric I actually use: lead time. How many days before failure does the model trigger an alert? Rule-based RMS thresholds typically give 1-3 days. A good ML model should give 7-14 days.
# Calculate lead time for each detected failure
def compute_lead_time(predictions, failure_timestamp, threshold=0.7):
"""Find first prediction above threshold before failure."""
alerts = predictions[predictions['prob'] > threshold]
alerts_before_failure = alerts[alerts['timestamp'] < failure_timestamp]
if len(alerts_before_failure) == 0:
return None # missed detection
first_alert = alerts_before_failure.iloc[0]['timestamp']
lead_time = (failure_timestamp - first_alert).total_seconds() / 86400 # days
return lead_time
# Example: 11.3 days lead time means you can schedule maintenance next week
# vs rule-based 2-day warning that forces emergency shutdown
Real-Time Inference on Edge vs Cloud
Your rule-based system probably runs on a PLC or edge gateway. Can the ML model do the same?
Depends on the model. Random Forest with 200 trees and 30 features? Runs fine on a Raspberry Pi 4 — inference latency under 10ms. LSTM with 3 layers and 128 units? You’ll need a more powerful edge device or cloud offloading.
I’ve deployed RF models on ARM Cortex-M7 microcontrollers (480 MHz, 1MB RAM) using quantized decision tree inference — works if you keep the model small (50 trees, max depth 10). Anything bigger, move to edge servers (NVIDIA Jetson, Intel NUC) or batch-process in the cloud.
Cloud pros: unlimited compute, easy retraining, centralized monitoring. Cloud cons: latency (100-500ms round-trip), dependency on network, cost at scale (1000 sensors × 1 Hz × cloud API calls = expensive).
I’d run inference on-edge for real-time alerts, send feature vectors (not raw data) to cloud for model updates and long-term RUL tracking.
When to Keep Rules and When to Trust the Model
Don’t throw out your rule-based system. Combine them.
Use rules for: Hard safety limits (bearing temperature > 120°C, immediate shutdown), sanity checks (sensor values within physical bounds), fallback when ML confidence is low.
Use ML for: Early degradation detection (days-to-weeks lead time), multi-sensor fusion (vibration + temperature + current), adapting to operational variability (load changes, environmental shifts).
Example hybrid logic:
def should_alert(sensor_data, ml_model):
# Hard rule: emergency shutdown
if sensor_data['temperature'] > 120:
return ('CRITICAL', 'Temperature exceeded safe limit')
# Sanity check: sensor failure
if sensor_data['vibration_rms'] < 0.1: # too quiet, likely sensor fault
return ('WARNING', 'Possible sensor malfunction')
# ML prediction
ml_prob = ml_model.predict_proba(sensor_data)[0, 1]
if ml_prob > 0.85:
return ('ALERT', f'High failure probability: {ml_prob:.2f}')
elif ml_prob > 0.7:
return ('WATCH', f'Elevated risk: {ml_prob:.2f}')
else:
# Fallback rule for known patterns ML might miss
if sensor_data['vibration_rms'] > 10.0: # old threshold
return ('ALERT', 'RMS threshold exceeded (rule-based fallback)')
return ('OK', None)
This catches sensor failures, respects hard safety limits, and uses ML for nuanced predictions. The rule-based fallback at the end ensures you don’t regress from your original system’s capabilities.
FAQ
Q: How much historical data do I need to train a PHM model?
For supervised learning (labeled failures), you need at least 3-5 failure examples to learn patterns, ideally 10+. For each failure, collect data starting from the last known-good state through to failure — typically weeks to months per asset. If you don’t have labeled failures, switch to unsupervised anomaly detection (train only on healthy data). I’m not entirely sure if 3 failures is statistically enough for generalization, but I’ve seen working models with that few when failure modes are consistent.
Q: Can I use transfer learning from public datasets like CWRU to my machines?
Sort of. Pre-train on CWRU to learn general bearing fault features, then fine-tune on your data. Don’t deploy a CWRU-trained model directly — bearing geometry, housing resonances, and operating speeds differ. I’ve had mixed results: works OK for same bearing type at similar RPM, fails completely when machine configurations diverge. Take this with a grain of salt — I haven’t tested it at scale across dozens of machine types.
Q: What’s the minimum sampling rate for vibration-based ML models?
Depends on bearing speed and defect frequencies. Rule of thumb: sample at , where is the highest fault frequency you care about. For a 1800 RPM bearing, BPFO might be ~100 Hz, so 10 kHz is safe. Slower machines (300 RPM fans) can work with 2-5 kHz. The IMS and CWRU datasets use 12-50 kHz — if you’re much lower, you’ll miss high-frequency early indicators.
Closing Thoughts
Start with Random Forest on engineered features. It’s boring, but it works and you can explain it to skeptical engineers. Once that’s running reliably for 3-6 months, consider LSTM or Transformer models for sequence modeling — they capture temporal degradation trends that feature-based methods miss, but they’re harder to debug when they fail.
The real bottleneck isn’t the algorithm. It’s labeling failures accurately, keeping sensors calibrated, and convincing maintenance teams to trust a model over their decades of experience. The technical migration takes 4 weeks. The organizational migration takes 4 months.
I’m still figuring out how to handle concept drift gracefully without constant retraining. Online learning methods exist (incremental Random Forest, streaming gradient descent), but they add complexity and can amplify noise if not carefully tuned. If you’ve deployed adaptive models in production PHM systems, I’d be curious to hear what actually worked.
Did you find this helpful?
Your support keeps this blog running and ad-free content coming.
☕ Buy me a coffeeMost Popular Posts
- Custom Metaclass in Python: 43% Faster Validation (12,794 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (763 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (652 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (549 views)