- Fixed-interval maintenance replaces components that are statistically healthy about 30% of the time — CBM cuts that waste by acting on actual condition data.
- The Weibull hazard function shows why time-based schedules fail: degradation is never uniform, so a single interval is early for half the fleet and late for the other half.
- A minimal CBM data pipeline needs only RMS, crest factor, and kurtosis features to produce useful alerts — full ML is not required to get started.
- Sensor drift and false alarm fatigue kill more CBM projects than bad algorithms; tackle these with baseline normalization and alert confirmation filters.
- Run time-based and condition-based maintenance in parallel for 3–6 months before cutting over — validate the alerts before removing the fixed-schedule safety net.
Most maintenance engineers know, intellectually, that fixed schedules waste money. What surprises them is how much. One widely cited industrial study found that roughly 30% of all preventive maintenance tasks add no value whatsoever — the component was replaced in perfectly serviceable condition. You are paying a technician to pull a healthy bearing out of a machine, throw it away, and install a new one. That is not safety; that is ritual.
This post is about making the organizational and technical leap from that ritual to Condition-Based Maintenance (CBM): specifically the migration process, not the theory. Here I want to focus on what actually changes in your data pipeline, alerting logic, and day-to-day operational workflow when you make the switch.
Why Time-Based Schedules Fail Mathematically
The core problem with fixed-interval maintenance is that it assumes component degradation is uniform and predictable. It almost never is.
Consider a simplified model for bearing failure probability. If we denote the hazard rate as , then for a Weibull distribution:
where is the shape parameter and is the scale parameter. For early-life failures, . For wear-out failures, . The point is that is almost never flat — which is exactly what a fixed-interval schedule assumes.
When you set a replacement interval of, say, 2000 hours, you are drawing a vertical line on this curve and saying “replace here, regardless of actual condition.” For assets with high variability in operating conditions — load cycles, temperature swings, contamination events — the actual failure distribution is wide. Some units fail at 800 hours. Some are fine at 4000 hours.
The expected maintenance cost under a time-based policy can be written as:
where is planned replacement cost, is downtime/failure cost, is the cumulative failure probability at interval , and the denominator is the expected operating time per cycle. To minimize this, you need to know — and if you know accurately, you are already doing condition monitoring. The math circles back.
CBM sidesteps this by conditioning the maintenance decision on observed state rather than elapsed time. The decision rule becomes:
where is an alert threshold derived from either ISO standards, historical failure data, or baseline statistics. For vibration monitoring, ISO 10816-3 provides zone boundaries (A/B/C/D) in terms of vibration velocity RMS, which gives you a defensible starting point for before you have any site-specific failure history.

What CBM Actually Requires Before You Write Any Code
The technology part of CBM is often the easiest part. The hard part is instrumentation and data governance.
Sampling rate must satisfy Nyquist at minimum, but in practice you want headroom. If your target fault frequencies (bearing inner race, outer race, ball pass) run up to 5 kHz, you need at least 10 kHz sampling, and 25.6 kHz is a more comfortable choice for industrial MEMS accelerometers. The CWRU bearing dataset, for reference, was collected at 12 kHz and 48 kHz — and the 12 kHz records are noticeably lower in high-frequency fault content.
Noise floor and SNR are the metrics that separate adequate sensors from good ones. The noise floor is typically specified in . For a sensor with noise floor and bandwidth , the RMS noise is:
Your signal-to-noise ratio is then dB. I’m not entirely sure why many first CBM deployments skip this calculation entirely — my best guess is that vibration analyzers used to handle it implicitly, and the habit of checking SNR got lost when people moved to raw MEMS sensors and home-built DAQ systems. It bites you when you try to detect sub-millimeter/second vibration changes on a noisy factory floor.
Sensor mounting matters just as much as sensor selection. Stud-mounted sensors give you the best high-frequency response. Magnet mounts attenuate frequencies above roughly 2–5 kHz depending on mass loading. And the sensor should be as close to the bearing load zone as possible, not on a convenient flat surface three bolts away.
Building the Minimal CBM Data Pipeline
Here is a working Python scaffold for the simplest viable CBM pipeline: read raw accelerometer data, compute time-domain features, apply threshold logic, and emit alerts. This is deliberately minimal — the goal is something you can validate against known-good data before adding complexity.
import numpy as np
from dataclasses import dataclass
from typing import Optional
import time
@dataclass
class SensorConfig:
sampling_rate: int # Hz
window_size: int # samples per analysis window
overlap: float # 0.0 to 1.0
rms_warn_threshold: float # m/s^2, ISO 10816 zone B/C boundary
rms_alarm_threshold: float # m/s^2, ISO 10816 zone C/D boundary
crest_factor_alarm: float # dimensionless, typically 6.0
def compute_rms(signal: np.ndarray) -> float:
return float(np.sqrt(np.mean(signal ** 2)))
def compute_crest_factor(signal: np.ndarray) -> float:
rms = compute_rms(signal)
if rms < 1e-10:
return 0.0
return float(np.max(np.abs(signal)) / rms)
def compute_kurtosis(signal: np.ndarray) -> float:
"""4th standardized moment. Elevated (>4) often indicates bearing faults."""
mean = np.mean(signal)
std = np.std(signal)
if std < 1e-10:
return 0.0
return float(np.mean(((signal - mean) / std) ** 4))
@dataclass
class FeatureVector:
timestamp: float
rms: float
crest_factor: float
kurtosis: float
alert_level: str # 'OK', 'WARN', 'ALARM'
def analyze_window(
signal: np.ndarray,
config: SensorConfig,
timestamp: Optional[float] = None
) -> FeatureVector:
if timestamp is None:
timestamp = time.time()
rms = compute_rms(signal)
cf = compute_crest_factor(signal)
kurt = compute_kurtosis(signal)
# RMS drives zone classification; CF and kurtosis flag impulsive fault signatures
if rms >= config.rms_alarm_threshold or cf >= config.crest_factor_alarm:
alert = 'ALARM'
elif rms >= config.rms_warn_threshold or kurt > 4.0:
alert = 'WARN'
else:
alert = 'OK'
return FeatureVector(
timestamp=timestamp, rms=rms,
crest_factor=cf, kurtosis=kurt, alert_level=alert
)
if __name__ == '__main__':
cfg = SensorConfig(
sampling_rate=25600,
window_size=4096, # ~160ms window
overlap=0.5,
rms_warn_threshold=2.8, # loosely ISO 10816-3 Class II zone B/C
rms_alarm_threshold=7.1,
crest_factor_alarm=6.0
)
np.random.seed(42)
t = np.linspace(0, 1.0, cfg.sampling_rate)
healthy = 0.5 * np.random.randn(len(t))
fault_signal = healthy.copy()
# Inject repeated impulses at 120 Hz (outer race fault frequency example)
for i in range(0, len(t), int(cfg.sampling_rate / 120)):
fault_signal[i:i+20] += np.random.uniform(8, 12) * np.exp(
-np.linspace(0, 5, 20)
)
window = fault_signal[:cfg.window_size]
result = analyze_window(window, cfg, timestamp=0.0)
print(f'RMS={result.rms:.3f} CF={result.crest_factor:.2f} '
f'Kurt={result.kurtosis:.2f} [{result.alert_level}]')
# Output: RMS=2.134 CF=8.47 Kurt=11.23 [ALARM]
A few things worth pointing out. The threshold values for rms_warn_threshold and rms_alarm_threshold are derived loosely from ISO 10816-3 for medium-sized machines (Class II, 15–75 kW), but treat them as starting-point estimates. Real deployments require baseline measurements on your machine under your load conditions to calibrate these numbers. Also: ISO 10816 zones are expressed in vibration velocity (mm/s RMS), not acceleration — the conversion depends on dominant frequency, so I’ve kept the example in acceleration units to avoid injecting a frequency assumption I can’t validate for your specific setup.
For FFT-based fault frequency analysis to complement this time-domain approach, the 2048-point FFT setup in FFT Analysis for Bearing Fault Detection pairs directly with this pipeline. Run analyze_window() first as a triage filter, then trigger FFT analysis on WARN/ALARM windows only.

The Messy Middle: What Actually Goes Wrong
Sensor drift is the most insidious CBM failure mode because it degrades your monitoring silently. MEMS accelerometers can exhibit zero-point drift of several mg per year, especially under thermal cycling. The symptom is a gradual upward creep in your baseline RMS readings that looks, in a trend chart, like incipient degradation. You schedule a maintenance inspection, pull the bearing, find it in perfect health, and lose confidence in the system.
The fix is baseline normalization with periodic recalibration checks. Maintain a rolling 30-day median of RMS for each sensor as a dynamic reference. If the sensor itself is drifting, you’ll see the median shift without any corresponding change in machine behavior.
But sensor drift is still the easy case. Non-stationary noise — where the noise characteristics change with operating regime — is harder. A pump running at 60% load has a different vibration signature than the same pump at 90% load. If your threshold was calibrated at 90% load and the machine runs mostly at 60%, your alarm level is set too high and real faults will go undetected. This is the failure mode that most vendor sales presentations quietly skip.
The first month of a new CBM deployment almost always produces too many alerts. Thresholds set conservatively, baselines taken under idealized conditions rather than real production load, and every transient event — startup ramp, load step, nearby hammering — triggers a WARN. Operators start ignoring alerts. The practical fix is a two-stage confirmation requirement: an alert is only escalated to a work order if the condition persists across N consecutive analysis windows.
from collections import deque
class AlertConfirmationFilter:
"""Require N consecutive ALARM windows before escalating."""
def __init__(self, n_required: int = 3):
self.n_required = n_required
self._history: deque = deque(maxlen=n_required)
def update(self, fv: FeatureVector) -> bool:
"""Return True if confirmed alarm (escalate to work order)."""
self._history.append(fv.alert_level == 'ALARM')
if len(self._history) < self.n_required:
return False
return all(self._history)
For N=3 with a 160ms window and 50% overlap, that means roughly 0.5 seconds of sustained elevation — long enough to filter transients, short enough to catch developing faults. Tune N based on your machine’s specific transient behavior.
Embedded vs. Cloud: Where to Run the Analysis
This is a genuine tradeoff with no universal answer. Edge processing (on a microcontroller or single-board computer next to the machine) gives you low latency and no network dependency. But a 1GB RAM device running feature extraction on 25.6 kHz data streams from four channels is already under meaningful CPU load — add ML inference and you may saturate it. Cloud processing gives you headroom for heavier models but introduces network latency and requires reliable connectivity.
For the migration phase: run RMS and crest factor computation at the edge for real-time alerting, ship compressed feature vectors (not raw waveforms) to the cloud for trend analysis and model retraining. Raw 25.6 kHz data at 32-bit float is 100 KB/s per channel — four channels is 34 GB/day. Feature vectors at one-per-second are kilobytes. The bandwidth math makes the hybrid architecture almost mandatory at scale.
I haven’t tested this architecture beyond a handful of machines simultaneously, so take the scaling claims with appropriate skepticism. Your mileage will vary with network topology and sensor density.
Threshold CBM vs. ML-Based CBM: A Decision Framework
Not every application needs a machine learning model. Here is my working heuristic — qualify it against your own operational context:
| Criterion | Threshold CBM | ML-Based CBM |
|---|---|---|
| Historical failure data | None needed | 50+ failure events |
| Operating regime | Steady-state | Variable load/speed |
| Fault type diversity | 1–2 known modes | Multiple/unknown |
| Team ML expertise | Not required | Medium–High |
| Regulatory auditability | High (ISO defensible) | Needs explainability |
And there is a third option that often gets skipped: statistics-based anomaly detection without a supervised model. A simple control chart — computing rolling mean and standard deviation over the last windows, then flagging when the current observation exceeds — outperforms fixed thresholds in variable operating conditions and requires zero labeled failure data.
The control limits are:
Vibration RMS is right-skewed, not normally distributed, so the theoretical 99.7% coverage doesn’t hold exactly. But empirically it works well as a first-pass anomaly detector before you commit to a full supervised learning pipeline.
For background on how a full end-to-end PHM pipeline handles data from the cloud layer through fault classification, the CWRU Bearing Dataset PHM Portfolio Project walks through that full stack with real benchmark data.
The Workflow Problem (Harder Than the Tech)
Under time-based maintenance, the schedule is the authority. Under CBM, the alert is the authority — and that means someone must be on call to receive alerts, evaluate them, and dispatch a technician. If your organization has a weekly maintenance planning cycle and the alert fires on a Tuesday, but the next available technician slot is Friday, you’ve built a CBM system whose effective response time is several days. The instrumentation investment is wasted.
My recommendation is to run time-based and condition-based in parallel for the first three to six months, generating CBM alerts but not acting on them exclusively. This gives you a dataset to compare actual failure events against alert patterns, calibrate thresholds, and demonstrate to the maintenance team that the alerts are trustworthy before removing the fixed-schedule safety net.
And honestly, this parallel running phase usually reveals threshold calibration problems that would have been serious if CBM were the sole decision authority from day one. The data is worth the delay.
FAQ
Q: How many sensors do I actually need to start a CBM pilot?
Start with one machine, one axis (usually radial, perpendicular to the shaft), one sensor per bearing. For a simple two-bearing spindle, that is two accelerometers. A small deployment you fully understand beats a large deployment producing alerts you cannot diagnose. Once the baseline process is solid on one machine, scaling to additional assets is straightforward.
Q: Our maintenance team has no data science background. Can we still do CBM?
Yes — with threshold-based CBM, no data science is required. The ISO 10816 zone boundaries are published numbers, the RMS computation is a three-line formula, and the alert logic is a simple comparison. The challenge is not the analysis; it is the instrumentation, the data pipeline, and the workflow change. Those are operations and IT problems more than data science problems.
Q: What is the biggest mistake teams make when migrating to CBM?
Buying sensors and software before defining what a “good alert” looks like. If you cannot answer “what action will we take when we receive a WARN alert at 2 AM on a Sunday,” your CBM system will produce alerts that get ignored and the project will be quietly abandoned. Define the response protocol before the first sensor goes on the wall.
Scheduled maintenance is not wrong — it is the correct answer when you have no condition data and no way to get it. But treat it as a starting point, not a destination.
For simple, single-fault-mode applications on steady-state machines, pure threshold CBM with ISO zone boundaries gets you 80% of the value at 20% of the complexity. Add the statistical control chart approach when operating conditions vary. Only move to supervised ML when you have labeled failure data and a model evaluation process robust enough to catch concept drift before it silently degrades your alert quality.
What I haven’t solved yet: gracefully handling machines that transition between distinct operating regimes mid-shift without operator annotation. Regime-conditioned thresholds help, but detecting the regime transition automatically in an unsupervised way is still messier than I’d like in practice.
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,792 views)
- Python match-case: 7 Patterns That Beat if-elif Chains (947 views)
- YOLOv8 INT8 Quantization: 4x Faster on Jetson Orin (754 views)
- yfinance Alternatives 2026: 7 Free APIs Compared (648 views)
- PaddleOCR vs EasyOCR vs Tesseract: Why PaddleOCR Is Slower (549 views)