Raw sensor data is rarely ready for analysis — it arrives in irregular intervals, contains missing values, includes noise from faulty sensors, and may use inconsistent units. This lesson covers the full journey from raw sensor readings to clean, analysis-ready data.
The IoT Data Analytics Pipeline — 7 Stages
| Stage |
Description |
IoT Example |
| 1. Collection |
Sensors generate readings at fixed intervals or on events. Data is published via MQTT or HTTP to a broker or REST API. |
ESP32 DHT22 publishes temperature every 60 s to ThingsBoard |
| 2. Ingestion |
The cloud platform receives messages and routes them to storage. May include stream processing for real-time decisions. |
ThingsBoard stores telemetry; AWS Kinesis processes streams |
| 3. Storage |
Time-series data stored in databases optimised for sequential timestamp queries. Retention policies auto-delete old data. |
InfluxDB, TimescaleDB, ThingsBoard internal storage |
| 4. Preprocessing |
Clean missing values, remove outliers, normalise scales, engineer features. Most time-consuming step in practice. |
Pandas: fillna(), drop_duplicates(), z-score normalisation |
| 5. Analysis |
Exploratory analysis: statistics, trends, correlations. Identify patterns and anomalies before applying ML. |
Mean, std, rolling average, correlation matrix, scatter plots |
| 6. Modelling |
Train machine learning models for prediction, classification, or anomaly detection. Evaluate performance. |
scikit-learn: RandomForest, SVM, Isolation Forest |
| 7. Deployment |
Trained model deployed to edge device or cloud endpoint for real-time inference on new sensor data. |
TensorFlow Lite on ESP32, ONNX on Raspberry Pi, REST API |
Retrieving IoT Data from ThingsBoard via REST API
Python — Retrieve ThingsBoard Telemetry into a Pandas DataFrame
import requests
import pandas as pd
from datetime import datetime, timedelta
TB_HOST = 'https://thingsboard.cloud'
TB_EMAIL = 'your@email.com'
TB_PASSWORD= 'yourPassword'
DEVICE_ID = 'YOUR_DEVICE_ID'
def get_token():
r = requests.post(f'{TB_HOST}/api/auth/login',
json={'username': TB_EMAIL, 'password': TB_PASSWORD})
return r.json()['token']
def get_telemetry(token, keys, hours=24):
"""Retrieve last N hours of telemetry for given keys."""
end_ms = int(datetime.now().timestamp() * 1000)
start_ms = int((datetime.now() - timedelta(hours=hours)).timestamp() * 1000)
headers = {'Authorization': f'Bearer {token}'}
params = {'keys': ','.join(keys), 'startTs': start_ms,
'endTs': end_ms, 'limit': 10000, 'agg': 'NONE'}
url = f'{TB_HOST}/api/plugins/telemetry/DEVICE/{DEVICE_ID}/values/timeseries'
return requests.get(url, headers=headers, params=params).json()
def to_dataframe(telemetry):
"""Convert ThingsBoard response to pandas DataFrame with datetime index."""
dfs = []
for key, values in telemetry.items():
df = pd.DataFrame(values) # columns: ts, value
df['ts'] = pd.to_datetime(df['ts'], unit='ms') # epoch ms → datetime
df = df.rename(columns={'value': key}).set_index('ts')
df[key] = pd.to_numeric(df[key], errors='coerce') # ensure numeric
dfs.append(df)
if not dfs:
return pd.DataFrame()
return pd.concat(dfs, axis=1).sort_index()
# Usage
token = get_token()
raw = get_telemetry(token, ['temperature', 'humidity', 'light'], hours=48)
df = to_dataframe(raw)
print(df.head())
print(f'Shape: {df.shape}, Date range: {df.index.min()} -- {df.index.max()}')
Data Preprocessing — Cleaning IoT Time-Series Data
Real IoT data is messy. Common problems: missing readings, duplicate entries (retry logic), outliers (hardware noise), irregular timestamps (clock drift), and mixed units.
1. Handling Missing Values
Python — Missing Value Detection and Imputation
# Check for missing data
print(df.isnull().sum())
print(f'Missing %: {df.isnull().sum() / len(df) * 100:.1f}%')
# Strategy 1: Forward-fill (carry last known value forward)
# Good for: slowly changing signals (temperature, humidity)
df_ffill = df.fillna(method='ffill')
# Strategy 2: Linear interpolation
# Good for: continuously varying signals with short gaps
df_interp = df.interpolate(method='time') # time-aware interpolation
# Strategy 3: Drop rows with too many missing values
# Good for: rows with > 50% missing (don't trust partial data)
df_clean = df.dropna(thresh=int(len(df.columns) * 0.5))
# Strategy 4: Fill with column median (robust to outliers)
df['temperature'] = df['temperature'].fillna(df['temperature'].median())
# For IoT: check if gap duration exceeds sensor reliability window
MAX_GAP = pd.Timedelta('2h')
gaps = df.index.to_series().diff()
large_gaps= gaps[gaps > MAX_GAP]
if not large_gaps.empty:
print(f'WARNING: {len(large_gaps)} gaps > 2 hours detected')
2. Removing Duplicates and Outliers
Python — Deduplication and Outlier Removal
# Remove exact duplicate timestamps (from MQTT retry logic)
df = df[~df.index.duplicated(keep='first')]
from scipy import stats
import numpy as np
# Z-score outlier detection (|Z| > 3 = anomaly)
def remove_zscore_outliers(series, threshold=3.0):
z_scores = np.abs(stats.zscore(series.dropna()))
mask = z_scores < threshold
cleaned = series.copy()
cleaned[series.index.isin(series.dropna().index[~mask])] = np.nan
return cleaned
df['temperature_clean'] = remove_zscore_outliers(df['temperature'])
# IQR-based outlier detection (more robust to skewed distributions)
def remove_iqr_outliers(series, factor=1.5):
Q1, Q3 = series.quantile(0.25), series.quantile(0.75)
IQR = Q3 - Q1
return series.where((series >= Q1 - factor*IQR) & (series <= Q3 + factor*IQR))
df['humidity_clean'] = remove_iqr_outliers(df['humidity'])
# Domain-based outlier detection (most reliable for IoT)
# Apply physical limits from sensor datasheets
df.loc[df['temperature'] < -40, 'temperature'] = np.nan # DHT22 min
df.loc[df['temperature'] > 80, 'temperature'] = np.nan # DHT22 max
df.loc[df['humidity'] < 0, 'humidity'] = np.nan
df.loc[df['humidity'] > 100, 'humidity'] = np.nan
3. Resampling and Normalisation
Python — Resampling Irregular Data and Normalisation
# Resample to regular 5-minute intervals
df_5min = df.resample('5T').mean() # 5T = 5-minute bins, mean aggregation
df_hourly = df.resample('1H').agg({
'temperature': 'mean', # Average temperature per hour
'humidity': 'mean',
'light': 'max', # Peak light level per hour
})
# Min-Max normalisation (scale to 0–1 range)
# Required for distance-based ML algorithms (KNN, SVM)
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
features = ['temperature', 'humidity', 'light']
df_norm = df_5min[features].copy()
df_norm[features] = scaler.fit_transform(df_norm[features].fillna(0))
# Standard normalisation (zero mean, unit variance)
# Required for algorithms that assume Gaussian distribution
from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
df_std = df_5min[features].copy()
df_std[features] = std_scaler.fit_transform(df_std[features].fillna(0))
Feature Engineering for IoT Analytics
Feature engineering transforms raw sensor readings into signals that capture underlying patterns more effectively. Good features dramatically improve model accuracy.
Python — IoT Feature Engineering
import pandas as pd, numpy as np
# 1. Temporal features — extract time-of-day patterns
df['hour'] = df.index.hour
df['day_of_week'] = df.index.dayofweek # 0=Monday, 6=Sunday
df['is_weekend'] = df['day_of_week'].isin([5,6]).astype(int)
df['is_business_hours']= ((df['hour'] >= 8) & (df['hour'] < 18)).astype(int)
# 2. Rolling statistics — capture trends and variance
df['temp_rolling_mean_1h'] = df['temperature'].rolling('1h').mean()
df['temp_rolling_std_1h'] = df['temperature'].rolling('1h').std()
df['temp_rolling_max_6h'] = df['temperature'].rolling('6h').max()
# 3. Rate of change — detect rapid temperature spikes
df['temp_delta'] = df['temperature'].diff()
df['temp_delta_rate'] = df['temp_delta'] / \
df.index.to_series().diff().dt.total_seconds() * 60 # °C per minute
# 4. Lag features — use past values as predictors
df['temp_lag_1'] = df['temperature'].shift(1) # Previous reading
df['temp_lag_12'] = df['temperature'].shift(12) # 1 hour ago (5-min data)
# 5. Derived features — sensor-specific calculated values
# Heat index: perceived temperature combining temp and humidity
T = df['temperature']; H = df['humidity']
df['heat_index'] = (-8.78469475556 + 1.61139411*T + 2.33854883889*H
- 0.14611605*T*H - 0.012308094*T**2
- 0.0164248277778*H**2 + 0.002211732*T**2*H
+ 0.00072546*T*H**2 - 0.000003582*T**2*H**2)
print(f'Features created: {df.columns.tolist()}')
Check Your Understanding — Lesson 9.1
?
1. A DHT22 sensor goes offline for 45 minutes and then reconnects. Your dataset has 45 minutes of missing temperature values. Which imputation strategy (forward-fill, linear interpolation, or drop) is most appropriate and why? What would be the risk of forward-filling a gap caused by a sensor fault rather than a network outage?
?
2. A soil moisture sensor occasionally returns 4095 (the maximum 12-bit ADC value) due to a wiring fault. Using the domain-based outlier detection approach from the lesson, write the Python code to remove these impossible values given that a real soil moisture reading ranges from 800 (saturated) to 3500 (completely dry).
?
3. Explain why resampling IoT time-series data to a regular interval (e.g. 5-minute bins) before applying machine learning is important. What problem does irregular timestamp spacing cause for most ML algorithms?