6 Edge Data: Sampling and Statistical Compression
6.1 Overview
This first route protects signal evidence through sampling and compares lossless, statistical, and transform reduction.
This is part 1 of 2. Continue with Edge Data: Semantic Compression and Pipelines for the second focused route.
6.2 Start With the Story
Keep the Shape Before Shrinking the Data
Picture a motor whose vibration changes faster than a slow sensor check can see. If the device measures too rarely, no later data trick can rebuild the missing motion.
Sampling means taking readings at chosen times. The sampling rate is how many readings are taken in one second. Start with the fastest change that matters. Measure often enough to show that change before you remove or combine any values.
Compression means using fewer bits to carry useful information. First decide what the receiver must still know. Exact records may need a lossless method, which can rebuild every bit. Trends may allow summaries. Alarms may allow the device to send only a named event plus enough context to check it.
Compare the smaller result with the original. Test normal work, sharp changes, noise, and a lost link. Record the method, settings, time window, and error.
Start with one short raw record. Mark the fastest useful change. Mark the slow trend. Mark known noise. Mark any gap. Choose the rate. Take the sample. Check the shape. Keep the time. Keep the unit. Keep the test state.
Now make a smaller copy. Count its bits. Check its shape. Check its peak. Check its mean. Check its alarm. Rebuild it when the method allows that. Measure the error. Repeat with a sharp event. Repeat with a quiet event. Repeat after a restart.
Test the full field path. Fill the local store. Remove the link. Keep taking samples. Restore the link. Send old data in order. Mark repeats. Mark lost blocks. Check the far copy. Check the power used. Check the time needed.
Write one clear rule for use. State which facts may change. State which facts must not change. State the largest allowed error. State who owns the setting. State what change forces a new test. Keep the raw test set so a later method can be judged on the same evidence.
The simple rule has limits. Less data saves power and link use, but it can also hide faults.
Use Practitioner to choose rates and reduction steps. Use Under the Hood for signal, transform, battery, and error limits.
Picture an IoT team using the ideas in Edge Sampling and Compression during a live operations review. A device has produced messy evidence, an analytic step is about to change an alert or control decision, and someone has to explain why the result should be trusted.
Read this page as that path from sensor evidence to accountable action. Start with what the system observes, keep the model or data treatment visible, and finish with the check that would convince an operator, maintainer, or auditor to act.
6.3 Learning Objectives
By the end of this chapter, you will be able to:
- Apply Nyquist Theorem: Calculate appropriate sampling rates for different sensor types
- Implement Data Reduction Techniques: Use aggregation, compression, event-based reporting, and delta encoding
- Select Compression Algorithms: Choose optimal algorithms based on data type and edge device constraints
- Avoid Common Pitfalls: Prevent sampling aliasing, buffer overflow, and rate mismatch errors
Edge sampling and compression reduce the amount of data IoT devices need to transmit. Think of sending a friend the highlights of a movie instead of the entire film. By transmitting only important changes or compressed summaries, devices save battery power and network bandwidth while preserving the information that matters most.
6.4 Prerequisites
Before diving into this chapter, you should be familiar with:
- Edge Data Acquisition: Architecture: Understanding device categories and data generation patterns
- Basic signal processing concepts: Familiarity with frequency and time-domain representations
- Python programming: Code examples use Python for data processing
Core Concept: Transform raw sensor data into actionable information at the source - send summaries, statistics, and alerts rather than every reading.
Why It Matters: Transmitting data costs 10-100x more energy than processing it locally. A sensor sending 1000 samples/minute to the cloud uses 100x more bandwidth than one sending minute-averages - with identical analytical value for most applications.
Key Takeaway: Apply the 90% rule - if 90% of your data is “normal” readings that will never be analyzed individually, aggregate them locally. Send statistical summaries (min, max, mean, std) at lower frequency, and only transmit raw data when anomalies are detected. This extends battery life from days to years.
6.5 Nyquist Sampling Rate
Key Concepts
- Adaptive sampling: Dynamically adjusting the sensor sampling rate based on signal variance or event rate — increasing frequency when the signal changes rapidly and decreasing it during quiet periods.
- Nyquist-Shannon theorem: The fundamental sampling principle stating that a signal must be sampled at least twice its highest frequency component to be reconstructed accurately — the minimum sampling rate for any IoT sensor.
- Delta encoding: A compression technique transmitting only the change between consecutive readings rather than absolute values, highly effective for slowly varying sensors.
- Run-length encoding (RLE): Compressing sequences of identical values into a count-value pair — very effective for binary event streams or sensors with frequent identical readings.
- Lossless compression: Compression that allows perfect reconstruction of the original data — required for financial billing data, safety-critical readings, and regulatory compliance.
- Lossy compression: Compression that discards some information to achieve higher compression ratios — acceptable for analytics workloads where small accuracy loss is tolerable.
- First set the sampling rate from the highest frequency that matters, not from the most convenient device rate.
- Then reduce edge traffic with aggregation, event reporting, delta encoding, and outage-aware buffering.
- Next compare lossless, statistical, FFT, and semantic compression against the evidence the cloud still needs.
- Finally test the factory vibration case, choose a strategy, and use the quizzes to check that fidelity, bandwidth, and memory stay aligned.
Checkpoints recap the decision path. Deep-dive and collapsed callouts keep calculations and implementation details available without replacing the main sampling-and-compression flow.
To accurately capture a signal, the sampling rate must be at least twice the highest frequency component of interest:
Practical examples:
| Signal Type | Max Frequency | Min Sample Rate | Typical Rate |
|---|---|---|---|
| Temperature | 0.1 Hz (slow changes) | 0.2 Hz | 1 sample/minute |
| Vibration | 500 Hz | 1 kHz | 2-5 kHz |
| Audio | 20 kHz | 40 kHz | 44.1 kHz |
| Motion (IMU) | 50 Hz | 100 Hz | 100-200 Hz |
The mistake: Sampling signals below the Nyquist rate (2x the highest frequency), causing phantom patterns (aliasing) that don’t exist in the original signal.
Symptoms:
- Vibration analysis shows unexpected low-frequency patterns
- Motor speed readings fluctuate despite constant RPM
- Temperature data shows oscillations that don’t match physical reality
- Frequency analysis reveals false peaks at wrong frequencies
- Bearing fault detection produces false positives
Why it happens: Engineers apply “common sense” sampling rates without frequency analysis. Underestimating signal bandwidth - a 60 Hz motor generates harmonics at 120 Hz, 180 Hz, etc. Cost pressure drives lower sampling rates. Copy-pasting configurations between different sensor types.
The fix: Always sample at >2x the highest frequency of interest:
| Signal Type | Max Frequency | Minimum Sample Rate | Recommended |
|---|---|---|---|
| Room temperature | 0.01 Hz | 0.02 Hz | 1/minute |
| HVAC response | 0.1 Hz | 0.2 Hz | 1/second |
| Motor vibration | 500 Hz | 1 kHz | 2.5 kHz |
| Bearing analysis | 5 kHz | 10 kHz | 25 kHz |
Prevention: Perform frequency analysis on representative signals before deployment. Use anti-aliasing filters (low-pass hardware filters) before the ADC. When in doubt, oversample then downsample digitally with proper filtering.
What sampling rate is needed to detect bearing faults in a 1800 RPM motor?
Given:
- Motor speed: 1800 RPM = 30 Hz (revolutions per second)
- Bearing has 8 rolling elements
- Bearing fault frequency: Rolling element passes once per revolution = Hz
- Harmonics: 2nd harmonic at 480 Hz, 3rd at 720 Hz
- Highest frequency of interest: 3rd harmonic = 720 Hz
Nyquist calculation:
Practical safety factor (2.5x above Nyquist):
If sampled at only 100 Hz (common mistake):
- Nyquist frequency = 50 Hz, but bearing fault is at 240 Hz
- 240 Hz aliases to Hz
- Result: False 40 Hz pattern appears instead of the real 240 Hz bearing fault
- Impact: Bearing failure goes undetected until catastrophic failure
Memory and bandwidth check:
- 4 kHz sampling × 2 bytes per sample = 8 kB/s raw data
- 1-second FFT windows → 8 kB per analysis
- Send top 10 frequency peaks → 120 bytes per second (67× reduction)
- Conclusion: Edge FFT compression mandatory for battery-powered vibration monitoring
6.6 Nyquist Rate Calculator
Use this calculator to explore how motor speed, harmonic order, and safety factor affect the required sampling rate and resulting data volume.
6.7 Interactive: Nyquist Sampling Animation
Checkpoint: Sampling Without Aliasing
You now know:
- Nyquist starts from the highest frequency of interest, so a 60 Hz motor with 4th-harmonic faults needs a 480 Hz minimum sample rate.
- The bearing example turns 1800 RPM into 30 Hz, then follows harmonics to 720 Hz before applying the 1440 Hz minimum and practical safety margin.
- The calculator and animation are sanity checks for the same rule: low sample rates create false low-frequency patterns, not trustworthy compression.
6.8 Edge Data Reduction Techniques
Before transmitting data to the cloud, edge devices can apply several reduction strategies:
- Aggregation: Compute statistics over time windows (mean, min, max, variance)
- Compression: Apply lossless (ZIP) or lossy (threshold-based) compression
- Event-based reporting: Only transmit when values exceed thresholds
- Delta encoding: Send only changes from previous values
# Example: Edge aggregation for temperature sensor
class EdgeAggregator:
def __init__(self, window_size=60): # 60 samples = 1 minute at 1 Hz
self.window_size = window_size
self.buffer = []
def add_sample(self, value):
self.buffer.append(value)
if len(self.buffer) >= self.window_size:
return self.compute_summary()
return None
def compute_summary(self):
summary = {
"min": min(self.buffer),
"max": max(self.buffer),
"mean": sum(self.buffer) / len(self.buffer),
"samples": len(self.buffer)
}
self.buffer = []
return summary
# Usage: Send 1 summary per minute instead of 60 raw samples
aggregator = EdgeAggregator(window_size=60)
for temp_reading in sensor_stream:
summary = aggregator.add_sample(temp_reading)
if summary:
transmit_to_cloud(summary) # 60x bandwidth reduction
Battery life impact of edge aggregation vs raw transmission:
Scenario: Temperature sensor with 2500 mAh battery, 1 reading/minute
Option A: Raw transmission (every reading sent):
- LoRa TX: 120 mA for 1.5s per transmission
- Transmissions per hour: 60
- TX energy per hour:
- Plus sleep: 0.01 mAh/hour
- Total: 3.01 mAh/hour → battery life = 2500 / 3.01 = 831 hours = 35 days
Option B: Edge aggregation (1 summary every 15 minutes):
- Transmissions per hour: 4
- TX energy per hour:
- Plus sleep: 0.01 mAh/hour
- Total: 0.21 mAh/hour → battery life = 2500 / 0.21 = 11,905 hours = 496 days = 1.4 years
Battery life improvement: longer
Data volume comparison:
- Raw: 60 packets/hour × 20 bytes = 1.2 kB/hour = 28.8 kB/day
- Aggregated: 4 packets/hour × 32 bytes (min/max/mean/count) = 128 bytes/hour = 3.1 kB/day
- Bandwidth reduction:
Key insight: Transmission dominates power budget. Aggregation provides 14× battery improvement with minimal information loss for trend analysis.
6.9 Battery vs Aggregation Calculator
Adjust the parameters below to see how edge aggregation affects battery life and bandwidth for a LoRa-connected sensor.
The mistake: Combining data from sensors with different sampling rates without proper resampling, leading to incorrect correlations and temporal misalignment.
Symptoms:
- Correlation analysis shows unexpected null or spurious relationships
- Merged datasets have many NaN/missing values at certain timestamps
- Time-series plots show “jagged” or misaligned signals
- ML models perform poorly despite good individual sensor data
Why it happens: Teams often assume all sensors operate at the same rate. A temperature sensor at 1 Hz combined with a vibration sensor at 100 Hz creates 99 missing values per temperature reading. Naive timestamp matching drops 99% of vibration data.
The fix: Use proper resampling/interpolation before combining:
# Resample high-frequency data to match low-frequency
vibration_1hz = vibration_100hz.resample('1S').mean()
# Or upsample low-frequency with interpolation
temp_100hz = temp_1hz.resample('10ms').interpolate(method='linear')
# Then merge on aligned timestamps
merged = pd.merge_asof(vibration_1hz, temp_1hz, on='timestamp', tolerance=pd.Timedelta('500ms'))
Prevention: Document sampling rates in sensor metadata. Create a data alignment layer that resamples all sources to a common time base before analysis.
-
Wrong: Average traffic is enough to size the buffer. Outages and bursts can fill it and erase readings.
The mistake: Configuring edge device buffers without considering worst-case scenarios, causing data loss during network outages or traffic spikes.
Symptoms:
- Gaps in time-series data after network recovery
- “Buffer full, dropping oldest data” warnings in device logs
- Critical events missing during high-activity periods
- Inconsistent data counts between edge and cloud
- Post-incident analysis reveals missing sensor readings
Why it happens: Buffer sizes calculated for average conditions, not peak loads. Network outage duration underestimated. Sensor burst rates during events (motion, vibration) exceed steady-state assumptions. Memory constraints on edge devices force small buffers.
The fix: Size buffers for worst-case, not average:
# Buffer sizing calculation
samples_per_second = 10
max_outage_duration_seconds = 3600 # 1 hour
safety_margin = 1.5
min_buffer_size = samples_per_second * max_outage_duration_seconds * safety_margin
# = 10 * 3600 * 1.5 = 54,000 samples
# If memory-constrained, implement tiered retention:
# - Last 5 minutes: Full resolution
# - 5-60 minutes: 10x downsampled
# - Beyond 60 minutes: Statistical summary only
Prevention: Monitor buffer utilization as a health metric. Alert at 70% capacity. Implement graceful degradation (reduce resolution before dropping data). Test with simulated network outages lasting 2x your expected maximum.
Checkpoint: Reduce Before Transmit
You now know:
- Aggregation can replace 60 raw samples with 1 summary when min, max, mean, and count preserve the decision evidence.
- Transmission dominates many edge power budgets: the worked LoRa case moves from 35 days to 496 days by sending 4 summaries per hour.
- Buffer plans need peak-load math, including the 54,000-sample outage calculation, before teams trust compressed or tiered retention data.
6.10 Compression Algorithms Deep Dive
Edge devices face a fundamental trade-off: transmit less data (save power, bandwidth, cost) while preserving information needed for downstream analytics. This deep dive compares compression techniques across three dimensions: compression ratio, computational cost, and information preservation.
6.11 Compression Algorithm Categories
| Category | Compression Ratio | CPU Cost | Information Loss | Best For |
|---|---|---|---|---|
| Lossless | 2:1 - 4:1 | Medium | None | Critical data, audit logs |
| Lossy Statistical | 10:1 - 100:1 | Low | Controlled | Trend analysis, dashboards |
| Lossy Transform | 50:1 - 500:1 | High | Controlled | Pattern detection, ML features |
| Semantic | 100:1 - 1000:1 | Low-Medium | Significant | Event detection, alerts |
6.12 Lossless Compression: DEFLATE/GZIP
Standard lossless compression works well for structured IoT data:
import gzip
import json
def compress_batch(readings: list[dict]) -> bytes:
"""
Compress a batch of sensor readings losslessly.
Typical compression: 3-5x for JSON sensor data.
"""
json_str = json.dumps(readings)
compressed = gzip.compress(json_str.encode('utf-8'), compresslevel=6)
return compressed
# Example: 100 temperature readings
readings = [{"ts": 1704067200 + i, "v": 22.5 + (i % 10) * 0.1} for i in range(100)]
raw_size = len(json.dumps(readings).encode()) # ~4,500 bytes
compressed_size = len(compress_batch(readings)) # ~1,200 bytes
# Compression ratio: 3.75:1
Performance characteristics:
| Metric | GZIP Level 1 | GZIP Level 6 | GZIP Level 9 |
|---|---|---|---|
| Compression ratio | 2.5:1 | 3.5:1 | 4:1 |
| Compress speed | 50 MB/s | 20 MB/s | 5 MB/s |
| Decompress speed | 100 MB/s | 100 MB/s | 100 MB/s |
| Edge CPU impact | Low | Medium | High |
When to use: Audit trails, compliance data, any data that may be queried in original form. Do not use for real-time streams on constrained MCUs.
6.13 Lossy Statistical: Aggregation Windows
Compute statistics over time windows, discard raw samples:
import statistics
from dataclasses import dataclass
from typing import Optional
@dataclass
class AggregatedWindow:
timestamp: int # Window start
count: int # Number of samples
mean: float
min_val: float
max_val: float
std_dev: float
p95: Optional[float] = None # Optional percentile
class WindowAggregator:
def __init__(self, window_seconds: int = 60):
self.window_seconds = window_seconds
self.buffer: list[float] = []
self.window_start: Optional[int] = None
def add_sample(self, timestamp: int, value: float) -> Optional[AggregatedWindow]:
if self.window_start is None:
self.window_start = timestamp
# Check if window complete
if timestamp - self.window_start >= self.window_seconds:
result = self._compute_aggregate()
self.buffer = [value]
self.window_start = timestamp
return result
self.buffer.append(value)
return None
def _compute_aggregate(self) -> AggregatedWindow:
sorted_buffer = sorted(self.buffer)
p95_idx = int(len(sorted_buffer) * 0.95)
return AggregatedWindow(
timestamp=self.window_start,
count=len(self.buffer),
mean=statistics.mean(self.buffer),
min_val=min(self.buffer),
max_val=max(self.buffer),
std_dev=statistics.stdev(self.buffer) if len(self.buffer) > 1 else 0,
p95=sorted_buffer[p95_idx] if p95_idx < len(sorted_buffer) else None
)
# Example: 1 Hz sensor, 60-second windows
# Input: 60 samples x 8 bytes = 480 bytes
# Output: 1 aggregate x 48 bytes = 48 bytes
# Compression ratio: 10:1
# Preserved: Trend (mean), anomaly detection (min/max/std), health (count)
Information loss analysis:
| What’s Preserved | What’s Lost |
|---|---|
| Average value (trend) | Individual sample timing |
| Min/max (bounds) | Exact sequence of values |
| Standard deviation (stability) | Sub-window patterns |
| Sample count (health) | Correlation with other sensors at sample level |
When to use: Temperature, humidity, air quality - any slowly changing signal where trends matter more than exact samples.
6.14 Lossy Transform: FFT-Based Compression
Transform to frequency domain, keep only significant components:
import numpy as np
from dataclasses import dataclass
@dataclass
class FFTCompressed:
timestamp: int
sample_rate: float
duration: float
frequencies: list[float] # Top N frequency components
magnitudes: list[float] # Corresponding magnitudes
phases: list[float] # Phase angles for reconstruction
def fft_compress(samples: np.ndarray, sample_rate: float,
timestamp: int, top_n: int = 10) -> FFTCompressed:
"""
Compress time-series data using FFT, keeping top N frequency components.
Typical compression: 100:1 to 500:1 depending on signal complexity.
Best for: Vibration, audio, periodic signals.
"""
# Compute FFT
fft_result = np.fft.rfft(samples)
freqs = np.fft.rfftfreq(len(samples), 1/sample_rate)
# Get magnitudes and find top N (excluding DC component)
magnitudes = np.abs(fft_result[1:]) # Skip DC
phases = np.angle(fft_result[1:])
freqs = freqs[1:]
# Select top N by magnitude
top_indices = np.argsort(magnitudes)[-top_n:]
return FFTCompressed(
timestamp=timestamp,
sample_rate=sample_rate,
duration=len(samples) / sample_rate,
frequencies=freqs[top_indices].tolist(),
magnitudes=magnitudes[top_indices].tolist(),
phases=phases[top_indices].tolist()
)
def fft_decompress(compressed: FFTCompressed, num_samples: int) -> np.ndarray:
"""
Reconstruct signal from FFT components (lossy reconstruction).
"""
t = np.linspace(0, compressed.duration, num_samples)
signal = np.zeros(num_samples)
for freq, mag, phase in zip(compressed.frequencies,
compressed.magnitudes,
compressed.phases):
signal += mag * np.cos(2 * np.pi * freq * t + phase)
return signal
# Example: Vibration sensor, 1 second at 1000 Hz
samples = np.sin(2*np.pi*50*np.linspace(0, 1, 1000)) # 50 Hz signal
samples += 0.3 * np.sin(2*np.pi*150*np.linspace(0, 1, 1000)) # 150 Hz harmonic
# Input: 1000 samples x 4 bytes = 4000 bytes
# Output: 10 freq-mag-phase tuples x 12 bytes = 120 bytes + 20 bytes metadata
# Compression ratio: ~30:1
compressed = fft_compress(samples, 1000.0, 1704067200, top_n=10)
reconstructed = fft_decompress(compressed, 1000)
# Reconstruction error for this example: ~5% RMS
# Bearing fault detection: Still works (frequency peaks preserved)
When to use: Vibration analysis, acoustic monitoring, any signal where frequency content matters more than exact waveform.
6.15 Continue to Part 2
Continue with Edge Data: Semantic Compression and Pipelines.
