Apply ARIMA Models: Use forecasting-based detection for temporal anomalies
Implement Exponential Smoothing: Detect level shifts and trend changes efficiently
Use STL Decomposition: Separate trend, seasonality, and residuals to isolate anomalies
Select Appropriate Methods: Choose time-series techniques based on data patterns
In 60 Seconds
Time-series anomaly detection methods such as ARIMA, exponential smoothing, and STL decomposition detect contextual anomalies that are only unusual given their temporal context — a temperature of 30°C is normal in summer but anomalous in winter. The core technique is to forecast the expected value, then flag readings whose forecast error exceeds a learned threshold.
For Beginners: Time-Series Anomaly Detection
Time-series anomaly detection spots unusual patterns in data that changes over time. Think of monitoring a heartbeat – a steady rhythm is normal, but unexpected spikes or pauses are concerning. For IoT sensors, this means learning what normal daily and weekly patterns look like so the system can automatically detect when something goes wrong.
Core Concept: Time-series methods exploit temporal patterns - anomalies are points where actual values significantly differ from what the model predicted based on historical patterns.
Why It Matters: IoT data is inherently temporal. A temperature of 30°C might be normal at 2 PM but anomalous at 2 AM. Time-series methods capture these contextual patterns that simple statistical methods miss.
Key Takeaway: Use ARIMA when data has clear trends/seasonality, exponential smoothing for detecting level shifts, and STL decomposition when you need to separate seasonal patterns from anomalies.
14.2 Prerequisites
Before diving into this chapter, you should be familiar with:
Anomaly Types: Understanding contextual anomalies that depend on temporal context
~15 min | Advanced | P10.C01.U03
Key Concepts
ARIMA: AutoRegressive Integrated Moving Average — a forecasting model capturing autocorrelation (AR), removing non-stationarity via differencing (I), and smoothing noise (MA); residuals outside a threshold indicate anomalies.
STL decomposition: Seasonal-Trend decomposition using Loess — separates a time series into trend, seasonal, and residual components; anomalies appear as spikes in the residual component.
Exponential smoothing: A forecasting technique weighting recent observations more heavily; Holt-Winters triple exponential smoothing handles both trend and seasonal patterns.
Stationarity: A property of a time series where mean and variance are constant over time; required by ARIMA, achieved via differencing to remove trends.
Autocorrelation: The correlation of a time series with its own past values at different lags; high autocorrelation enables good forecasting.
Concept drift: A shift in the statistical properties of the time series over time that degrades forecast accuracy; requires periodic model retraining or adaptive algorithms.
Forecast error threshold: The maximum allowable deviation between a forecasted value and the observed value before a reading is classified as anomalous.
14.3 Introduction
IoT sensor data is inherently temporal – values are meaningful in sequence, not isolation. While statistical methods (Z-score, IQR) treat each reading independently, time-series methods leverage the sequential structure to detect anomalies that only become apparent in temporal context. This chapter covers three complementary approaches: ARIMA for forecast-based detection, exponential smoothing for level-shift detection, and STL decomposition for separating seasonal patterns from true anomalies.
How It Works
Each method takes a different approach to exploiting temporal patterns:
ARIMA Forecasting Mechanism:
Model learns patterns: ARIMA captures trends (gradual increase/decrease), seasonality (daily/weekly cycles), and autocorrelation (current value depends on past values)
Predict next value: Based on recent history, model forecasts what the next reading should be
Calculate forecast error: Residual = actual value - predicted value
Threshold on residuals: Large residuals indicate anomalies – values that deviate from expected temporal trajectory
Why this works: Normal sensor patterns have temporal structure. ARIMA learns time-dependent patterns and flags values that violate the expected trajectory – even if the absolute value appears “normal” in isolation.
STL Decomposition Mechanism:
Separate components: Split time series into three additive parts:
Trend: Long-term increase/decrease (e.g., sensor drift over months)
Seasonal: Regular repeating patterns (e.g., daily temperature cycle)
Residual: What remains after removing trend and seasonal (noise + anomalies)
Detect in residuals: Anomalies appear as spikes in the residual component
Context preservation: By isolating seasonal patterns, STL prevents flagging normal daily variations as anomalies
Why this works: IoT data has strong seasonal patterns (HVAC follows occupancy, solar panels follow sun). STL removes these known patterns, leaving only true deviations.
Exponential Smoothing (EWMA) Mechanism:
Weighted average: Recent values get higher weight, old values get lower weight (exponentially decaying)
Track baseline: EWMA represents current “expected” value based on recent trend
Detect level shifts: When actual value suddenly departs from EWMA baseline by >threshold, flag anomaly
Why this works: EWMA adapts quickly to gradual changes (avoiding false alarms from concept drift) while detecting sudden level shifts (actual anomalies). The smoothing parameter (α) controls responsiveness.
Core Concept: Model time series as a function of past values and past errors to forecast future values. Anomalies are points where actual value significantly differs from forecast.
When ARIMA Excels:
Data has clear trends or seasonal patterns
You need to forecast next value to compare against
Anomalies are deviations from expected trajectory
ARIMA Components:
AR (AutoRegressive): Value depends on previous values
I (Integrated): Differencing to make series stationary
MA (Moving Average): Value depends on previous errors
IoT Application Example:
from statsmodels.tsa.arima.model import ARIMAimport numpy as npclass ARIMADetector:def__init__(self, order=(1,1,1), threshold=3.0):""" order: (p, d, q) for ARIMA model threshold: number of std deviations for anomaly """self.order = orderself.threshold = thresholdself.history = []self.model =Nonedef train(self, historical_data):"""Train ARIMA model on historical data"""self.history =list(historical_data)self.model = ARIMA(self.history, order=self.order)self.model_fit =self.model.fit()def predict_and_detect(self, new_value):""" Predict next value and check if new_value is anomalous """iflen(self.history) <10:self.history.append(new_value)returnFalse, None, None# Forecast next value forecast =self.model_fit.forecast(steps=1)[0]# Calculate residual error residuals =self.model_fit.resid std_error = np.std(residuals)# Check if new value is anomalous error =abs(new_value - forecast) is_anomaly = error > (self.threshold * std_error)# Update history and retrain (in practice, retrain periodically)self.history.append(new_value)return is_anomaly, forecast, error# Example: Power consumption monitoring with daily patterns# Historical data: 24 hours of power usage (kW)historical_power = [12, 10, 9, 8, 8, 9, 15, 25, 30, 28, 26, 27, # Day pattern28, 29, 27, 25, 30, 35, 28, 22, 18, 15, 13, 11# Evening/night]detector = ARIMADetector(order=(2,1,2), threshold=2.5)detector.train(historical_power)# Next hour: should be ~12 kW, but reads 55 kW (anomaly)anomaly, forecast, error = detector.predict_and_detect(55)print(f"Expected: {forecast:.1f} kW")print(f"Actual: 55.0 kW")print(f"Error: {error:.1f} kW")print(f"Anomaly: {anomaly}")# Output: Expected: 12.3 kW, Actual: 55.0 kW, Error: 42.7 kW, Anomaly: True
Try It: ARIMA Forecast-Based Anomaly Detection
Adjust the ARIMA parameters and anomaly threshold to see how forecast-based detection works on power consumption data with a daily pattern. Inject an anomaly value and observe whether the detector flags it.
Where \(\alpha\) is the smoothing factor (\(0 < \alpha < 1\)):
\(\alpha = 0.1\): Heavily smoothed, slow to react
\(\alpha = 0.9\): Light smoothing, fast to react
Use Case: Detecting sudden jumps in vibration, temperature, or current draw.
Interactive: EWMA Alpha Explorer
Adjust the smoothing factor to see how EWMA responds to a step change from a baseline of 22°C to 30°C. Watch how different alpha values trade off detection speed against noise sensitivity.
The EWMA response time to a step change is controlled by \(\alpha\). For a sudden shift of magnitude \(\Delta\), the EWMA reaches 63% of the new value after:
And 95% after approximately \(3 \times t_{63\%}\) samples.
Example: Temperature sensor at 1 sample/minute experiences step from 22°C to 30°C (\(\Delta = 8\)°C, so 95% level = 29.6°C): - \(\alpha = 0.1\): Reaches 95% after \(3 \times 9.5 \approx 29\) minutes - \(\alpha = 0.3\): Reaches 95% after \(3 \times 2.8 \approx 8\) minutes - \(\alpha = 0.7\): Reaches 95% after \(3 \times 0.83 \approx 2.5\) minutes
Trade-off: Higher \(\alpha\) detects changes faster but also generates more false alarms from noise. For IoT with 1% sensor noise, \(\alpha = 0.2\) balances 14-minute detection with 2% false alarm rate.
14.6 Seasonal Decomposition (STL)
Core Concept: Separate time series into Trend, Seasonal, and Residual components (as described in the mechanism above). Anomalies appear as spikes in the residual – the signal that remains after removing all expected patterns.
Implementation:
from statsmodels.tsa.seasonal import seasonal_decomposeimport pandas as pdimport numpy as np# Temperature data with daily seasonalitytimestamps = pd.date_range('2024-01-01', periods=168, freq='H') # 1 weektemps = [20+5*np.sin(2*np.pi*i/24) + np.random.normal(0, 0.5)for i inrange(168)]# Inject anomaly at hour 100temps[100] =35# Sudden spikedf = pd.DataFrame({'timestamp': timestamps, 'temperature': temps})df.set_index('timestamp', inplace=True)# Decompose into trend, seasonal, residualdecomposition = seasonal_decompose(df['temperature'], model='additive', period=24)# Anomalies are large residualsresiduals = decomposition.residthreshold =3* residuals.std()anomalies =abs(residuals) > thresholdanomaly_indices = df.index[anomalies]print(f"Anomalies detected at: {anomaly_indices.tolist()}")# Output: Anomalies detected at: [Timestamp('2024-01-05 04:00:00')]# (Hour 100 = day 5, 4:00 AM -- the injected spike)
Try It: STL Decomposition Explorer
Explore how STL (Seasonal-Trend-Loess) decomposition separates a temperature signal into trend, seasonal, and residual components. Adjust the seasonal amplitude, inject an anomaly spike, and see how the anomaly appears clearly in the residual – even when it is hidden in the raw signal.
14.7 Worked Example: HVAC Energy Anomaly Detection with STL
Worked Example: Detecting HVAC Faults via Power Consumption Anomalies
Scenario: Siemens Building Technologies monitors HVAC systems across a 15-story office building in Frankfurt, Germany. Each floor has 4 air handling units (AHUs), totaling 60 AHUs. The facility manager wants automatic detection of AHU faults before they cause comfort complaints or energy waste.
Given:
60 AHUs, each with a power meter sampling every 5 minutes (288 readings/day)
Normal AHU power: 2.5-8.0 kW depending on time of day and season
Historical data: 6 months (26 weeks), confirmed faults logged by maintenance team
Known fault types: stuck damper (sustained high power), refrigerant leak (gradual efficiency loss), fan belt slip (erratic power spikes)
Step 1 – Apply STL decomposition to separate normal patterns:
For a single AHU (Floor 7, Unit B), decompose 6 months of data:
Component
What It Captures
Typical Range
Trend
Gradual seasonal shift (summer = higher baseline)
3.5 kW (winter) to 5.2 kW (summer)
Daily seasonality
Business-hours cycle
+/-2.5 kW swing
Weekly seasonality
Weekend reduction
-1.5 kW on Sat/Sun
Residual
Unexplained variation – anomalies live here
+/-0.3 kW normally
Step 2 – Establish residual thresholds from clean history:
From the first 4 months (no known faults): - Residual mean: 0.0 kW (by construction) - Residual standard deviation: 0.28 kW - Anomaly threshold: |residual| > 3 x 0.28 = 0.84 kW
Over 60 AHUs with an average of 8 faults/year: estimated annual savings of EUR 15,000-25,000 in energy and maintenance costs.
Result: STL decomposition detects stuck dampers 7x faster than fixed thresholds and catches refrigerant leaks 5 weeks earlier than complaint-driven maintenance. The residual-based approach works because it isolates faults from the expected daily/weekly/seasonal patterns that would otherwise mask gradual degradation.
Key Insight: For HVAC anomaly detection, the biggest win is not catching sudden failures (those are obvious) but detecting gradual efficiency losses that waste energy silently. A 10% refrigerant leak increases energy consumption by 20% but the absolute power change is small enough to hide within normal daily variation. STL decomposition separates the signal from the noise.
14.8 Method Comparison
Comparison of Time-Series Methods:
Method
Strength
Limitation
Best IoT Use Case
ARIMA
Captures complex temporal patterns
Requires stationarity, computationally expensive
Predictable systems (HVAC, production lines)
Exponential Smoothing
Fast, simple, adapts to level changes
Misses complex patterns
Real-time edge detection (motor current)
STL Decomposition
Handles seasonality explicitly
Needs full cycles of data (>=2 seasons)
Environmental monitoring (temp, humidity)
Pitfall: Using Static Thresholds for Dynamic Systems
The Mistake: Setting fixed anomaly detection thresholds (e.g., “alert if temperature > 80°C”) based on initial observations, then leaving them unchanged as the system operates over months or years.
Why It Happens: Static thresholds are simple to implement and understand. Initial calibration produces reasonable results, creating false confidence. Gradual changes in “normal” baseline go unnoticed, and recalibration requires effort that gets deprioritized.
The Fix: Implement adaptive thresholds that evolve with system behavior:
Rolling baseline: Calculate thresholds based on recent history (e.g., last 7 days) rather than historical constants. Use exponentially weighted moving averages (EWMA) with decay factor 0.94-0.99 depending on expected change rate.
Contextual thresholds: Maintain separate thresholds for different operating modes - a motor at full load has different “normal” vibration than at idle. Use clustering to automatically discover operating modes from historical data.
Scheduled recalibration: For slow-drifting systems, automatically recalibrate monthly using unsupervised methods (rebuild IQR bounds, recompute seasonal decomposition).
Anomaly rate monitoring: If your system suddenly detects 10x more anomalies than baseline, investigate whether something changed in the environment OR if your thresholds have drifted out of calibration.
A factory motor that operated at 75°C for 2 years may run at 82°C after maintenance - that’s not an anomaly, it’s a new normal.
For Kids: Meet the Sensor Squad!
Sammy the Sensor had a mystery to solve. He was watching the temperature in a smart greenhouse, and something strange was happening.
“Max!” Sammy called. “The temperature is 28 degrees. Is that normal?”
Max the Microcontroller checked the clock. “It is 2 PM on a sunny day. 28 degrees is perfectly normal for this time!”
Later that night, Sammy reported 28 degrees again. Max jumped up. “Wait – it is 2 AM now! 28 degrees in the middle of the night? That is NOT normal! Something is wrong!”
Lila the LED was confused. “But the number is the same! How can 28 degrees be normal at one time and weird at another?”
“That is what makes time-series detection so cool,” Max explained. “It is not just about the NUMBER – it is about WHEN the number happens. I keep a mental calendar of what temperatures should look like at each hour. During the day, it is warm. At night, it should cool down. When it does not follow the pattern, I know something is off.”
They investigated and found that a heating vent was stuck open! Without time-series awareness, they would have missed it entirely because 28 degrees looked perfectly fine on its own.
Bella the Battery added: “It is like if you saw someone wearing a winter coat. Normal in January, weird in July – same coat, different context!”
Key lesson: Time-series methods detect anomalies based on WHEN something happens, not just what the value is. A normal value at the wrong time can be just as important as an obviously strange reading!
Try It: Exponential Smoothing Anomaly Detector
Build an EWMA-based anomaly detector in pure Python (no external libraries). This demonstrates how exponential smoothing catches level shifts and spikes in IoT sensor data.
import mathimport randomclass EWMADetector:"""Exponentially Weighted Moving Average anomaly detector."""def__init__(self, alpha=0.1, threshold_std=3.0):self.alpha = alphaself.threshold_std = threshold_stdself.ewma =Noneself.ewma_var =None# Track variance for adaptive thresholdself.count =0def update(self, value):"""Process new value, return (is_anomaly, ewma, threshold)."""ifself.ewma isNone:self.ewma = valueself.ewma_var =0.0self.count =1returnFalse, value, 0.0self.count +=1# Predict: current EWMA is our forecast forecast =self.ewma error = value - forecast# Update EWMAself.ewma =self.alpha * value + (1-self.alpha) *self.ewma# Update variance estimate (EWMA of squared errors)self.ewma_var = (self.alpha * error**2+ (1-self.alpha) *self.ewma_var) std = math.sqrt(self.ewma_var) ifself.ewma_var >0else1.0# Anomaly if error exceeds threshold threshold =self.threshold_std * std is_anomaly =abs(error) > threshold andself.count >10return is_anomaly, forecast, threshold# === Simulate IoT temperature sensor with anomalies ===random.seed(42)# Normal: ~22C with small noise, daily cyclereadings = []for hour inrange(72): # 3 days, hourly daily_cycle =3* math.sin(2* math.pi * (hour %24) /24) noise = random.gauss(0, 0.3) temp =22+ daily_cycle + noise readings.append(("normal", temp))# Inject anomaliesanomaly_hours = {24: ("spike", 35.0), # Heater malfunction at hour 2448: ("drop", 8.0), # Window left open at hour 4860: ("gradual", None), # Gradual drift starts at hour 60}for i, (label, temp) inenumerate(readings):if i in anomaly_hours: atype, aval = anomaly_hours[i]if atype =="spike": readings[i] = ("SPIKE", aval)elif atype =="drop": readings[i] = ("DROP", aval)if i >=60: # Gradual drift drift =0.5* (i -60) old_label, old_temp = readings[i]if old_label =="normal": readings[i] = ("drift"if drift >2else"normal", old_temp + drift)# Run detector with two different alpha valuesprint("=== EWMA Anomaly Detection: Alpha Comparison ===\n")for alpha in [0.1, 0.3]: detector = EWMADetector(alpha=alpha, threshold_std=3.0) detected = []print(f"Alpha = {alpha} ({'slow response'if alpha <0.2else'fast response'})")print(f"{'Hour':>4}{'Label':>7}{'Value':>7}{'EWMA':>7} "f"{'Thresh':>7}{'Alert':>6}")print("-"*46)for hour, (label, value) inenumerate(readings): is_anomaly, ewma, threshold = detector.update(value)if is_anomaly: detected.append((hour, label, value))# Print key momentsif is_anomaly or hour %12==0or label !="normal": alert ="***"if is_anomaly else""print(f"{hour:4d}{label:>7}{value:7.1f}{ewma:7.1f} "f"{threshold:7.1f}{alert:>6}")print(f" Total anomalies detected: {len(detected)}") true_pos =sum(1for _, l, _ in detected if l !="normal") false_pos =sum(1for _, l, _ in detected if l =="normal")print(f" True positives: {true_pos}")print(f" False positives: {false_pos}\n")print("Key insight: Lower alpha (0.1) has smoother baseline but ""slower response to real changes.\n""Higher alpha (0.3) catches anomalies faster but may ""generate more false positives.\n""Choose alpha based on how quickly 'normal' changes in your system.")
What to Observe:
The spike at hour 24 (35C vs ~22C baseline) is caught immediately by both alpha values
The drop at hour 48 (8C) is also detected clearly – any sudden deviation triggers the detector
The gradual drift starting at hour 60 is harder to catch: alpha=0.1 detects it later because EWMA adapts slowly
Alpha=0.3 catches drift sooner but may have more false positives from the daily temperature cycle
The trade-off between sensitivity and specificity is controlled by alpha and threshold_std
Try It: EWMA Alpha Comparison Dashboard
Compare two EWMA detectors side by side with different alpha values. Inject a spike, a drop, and a gradual drift into a simulated temperature signal and see how detection speed and false alarm rates differ. This directly illustrates the sensitivity-specificity trade-off.
Smart Grid - Power consumption patterns with diurnal cycles
Interactive Quiz: Match Concepts
Interactive Quiz: Sequence the Steps
Common Pitfalls
1. Applying ARIMA to non-stationary data without differencing
ARIMA requires a constant mean. A gradually rising temperature baseline will cause ARIMA to systematically underestimate future values, generating endless false positives. Always apply the Augmented Dickey-Fuller test first.
2. Using a fixed forecast-error threshold across seasons
A threshold calibrated on summer data will trigger false alarms in winter when natural variance is higher. Fit separate thresholds per season or use adaptive percentile-based bounds.
3. Ignoring computational cost of ARIMA on embedded devices
ARIMA fitting is expensive and requires matrix operations unsuitable for microcontrollers. Use exponential smoothing (O(1) memory) at the edge; reserve ARIMA for gateway or cloud tiers.
4. Not accounting for missing data in time-series models
Sensor dropouts break the equal-time-step assumption of ARIMA. Impute missing values before fitting any time-series model to avoid corrupted parameter estimates.
Label the Diagram
14.9 Summary
Time-series methods excel at detecting contextual anomalies where temporal patterns matter:
ARIMA: Forecast-based detection for complex temporal patterns
Exponential Smoothing: Fast level-shift detection at the edge
STL Decomposition: Separates seasonality to isolate true anomalies
Key Takeaway: Use time-series methods when “normal” depends on when the reading occurred. Statistical methods miss these contextual patterns.
14.10 What’s Next
If you want to…
Read this
Apply statistical methods for non-seasonal anomalies