"You promised me 100 samples per second. You delivered 94, at moments of your own choosing, and called it a time series."
A Disappointed AI Agent
The Big Picture
Every model in this book quietly assumes its input arrived on a clean, uniform grid: sample \(n\) at time \(t_0 + n/f_s\), no gaps, no surprises. Real sensor streams break that assumption constantly. A Bluetooth wearable drops radio packets in a crowded room. A Controller Area Network (CAN) bus buffer overflows during a burst. An operating system schedules your acquisition thread late, so the timestamp attached to a sample no longer matches when the physical measurement was actually taken. These are not exotic edge cases; they are the default condition of field data. This section gives you a vocabulary for the three dominant defects (missing samples, timing jitter, and packet loss), the tools to measure them from timestamps alone, and the discipline to mark them explicitly rather than let interpolation launder corruption into confident-looking numbers.
This section builds on the ideal sampling model and Nyquist limit from Section 3.1 and the timestamping and clock concepts from Section 3.3, since a corrupted timestamp is itself a form of missing information. Reasoning about why data goes missing draws on the probability primer in Chapter 4.
A taxonomy of temporal defects
The three failure modes differ in cause even when they look alike in a plot. Missing samples means the underlying acquisition produced fewer readings than the nominal rate implies. An analog-to-digital converter (ADC) skipped a conversion, a sensor entered a fault state, or a firmware watchdog reset the chip mid-stream. The timeline has a hole. Jitter means the samples exist but their spacing is irregular: the intended interval is \(T_s = 1/f_s\), yet the actual interval \(\Delta t_n = t_n - t_{n-1}\) fluctuates. We summarize jitter by the standard deviation of those intervals, \(\sigma_{\Delta t}\), and by the peak-to-peak spread. Packet loss is a transport-layer phenomenon. The sensor measured correctly, but the bytes never reached your process: a corrupted radio frame, a dropped User Datagram Protocol (UDP) datagram, or a ring buffer that overwrote unread data. From the consumer's side packet loss looks like missing samples. Yet it arrives in characteristic bursts, and the protocol layer can often recover it, which pure sensor faults cannot. In short: a missing sample you have flagged is still data, while a missing sample you have hidden is a lie your model will believe. Figure 3.4.2 illustrates the taxonomy of temporal defects: missing samples vs jitter vs packet loss.
Checkpoint
So far: three defects, one rule. Missing samples are absent readings, jitter is irregular sample spacing, and packet loss is data that was measured but never delivered; whichever you face, carry the true timestamps and mark every hole rather than hiding it.
Key Insight
The dangerous move is treating an irregular stream as if it were regular. If you feed jittered samples into a Fast Fourier Transform (FFT) or a fixed-stride convolution assuming uniform \(T_s\), you are lying to the model about time. A sample tagged \(t_n\) but processed as if it landed at \(nT_s\) injects a phase error of \(2\pi f (t_n - nT_s)\) at frequency \(f\). Small timing errors become large spectral errors at high frequencies. Always carry the true timestamps, not just an index.
Why the pattern of missingness matters
Naming which of the three defects you face tells you what broke, but not how badly it will hurt; that severity is set by the pattern of the gaps, not merely by their presence. Borrowing the standard statistical taxonomy, missing data is missing completely at random (MCAR) when the dropout is independent of everything, missing at random (MAR) when it depends on observed covariates, and missing not at random (MNAR) when it depends on the unobserved value itself. This distinction is not academic. A pulse oximeter that drops readings precisely when the patient moves (motion also being what makes the reading dangerous) is MNAR: the gaps are correlated with the events you care most about. Imputing those gaps with the local mean will systematically erase the very episodes your model is meant to catch, and no amount of downstream accuracy on clean segments will reveal the bias. (This is why a 2 percent MNAR loss landing on the events you care about can wreck a detector that shrugs off a 20 percent MCAR loss spread across quiet stretches.) Characterizing whether your gaps are MCAR, MAR, or MNAR is a prerequisite for choosing any repair strategy, and it directly shapes the leakage-safe splits you will build in Chapter 5.
Mental Model
Think of an end-of-course feedback survey that students fill out voluntarily. If a few random students forget, the missing forms tell you nothing special (MCAR). If evening-section students reply less because they are tired, you can still correct using the section label you already recorded (MAR). But if the students who fail the course are precisely the ones who never submit the survey, then the missing responses are tied to the very quantity you wanted to measure, satisfaction, and averaging the forms you did get will read far too rosy (MNAR). The danger is not the count of blank forms; it is that their blankness is caused by the hidden value itself, so no reweighting on what you observed can undo the bias.
Common Misconception
The misconception is "as long as the delivery ratio (the fraction of expected samples that actually arrived) is high, the missing data is harmless." Wrongness of missing data is governed by which samples are gone, not merely how many: a 2 percent MNAR loss concentrated on exactly the high-acceleration events a fall detector cares about can be far more damaging than a 20 percent MCAR loss spread uniformly across quiet stretches.
Practical Example: A wrist wearable in a crowded gym
A fitness band streams a 50 Hz accelerometer over Bluetooth Low Energy (BLE) to a phone. In a quiet room the phone receives a clean 20 ms cadence. Walk onto a gym floor packed with other 2.4 GHz radios and the picture changes: the BLE link layer retransmits, the connection interval stretches, and whole notification packets (each carrying a batch of samples) are lost. The phone now sees bursts of 40 to 60 missing samples every few seconds, plus jitter of several milliseconds on the packets that do arrive. A naive activity classifier trained on lab data typically collapses, not because the motion changed, but because its input grid quietly developed holes. The fix is not a better model first; it is measuring the loss, marking it, and making the model gap-aware, in that order.
Real-World Application: Apple Watch fall detection
The Apple Watch fall-detection pipeline consumes accelerometer and gyroscope streams over the same power-managed sensor buses that drop and re-batch samples to save energy, so it cannot assume a clean grid. Its on-device model reasons over timestamped windows and treats a fall as a short, high-jerk transient (jerk being the rate of change of acceleration, so a sharp impact spikes it) followed by inactivity, which means a dropout landing on the impact (an MNAR loss on exactly the event of interest) would be catastrophic. That is why such designs typically lean on redundant high-rate sensing and explicit gap handling rather than trusting a raw delivery ratio.
Measuring defects from timestamps
A classifier that quietly ingests a corrupted stream does not throw an error; it just ships wrong predictions in the field, and every repair choice and quality gate downstream inherits its trust from whether you quantified the damage first. You cannot repair what you have not measured. Given a vector of arrival timestamps, three numbers tell you almost everything: the distribution of inter-sample intervals \(\Delta t_n\), the count and duration of gaps (intervals exceeding a threshold such as \(1.5\,T_s\)), and the delivery ratio (received samples divided by expected samples over a window). The code below computes all three and, crucially, produces an explicit boolean missingness mask on a uniform reference grid rather than silently filling the holes. Figure 3.4.1 shows this alignment: jittered arrival timestamps are snapped to the nearest slot of a uniform grid, and the slots that no timestamp reaches become explicit holes in the boolean mask.
True (observed, teal); the two slots the dropout burst left untouched are marked False (holes, dashed red). The mask, not an interpolated fill, is what travels downstream.The delivery ratio is precisely the count of samples you actually received divided by the count the nominal rate says you should have received over the same interval, \(N_\text{obs} / N_\text{expected}\) where \(N_\text{expected} = \lfloor (t_{-1} - t_0)/T_s \rfloor + 1\); it is the single scalar that tells you how much of the promised timeline reached your process. It matters because it is a cheap, unit-free health gauge you can compute continuously and alarm on before any modeling begins. The mechanism is a simple ratio, so its one weakness is bluntness: a delivery ratio hides where the losses fell, which is why you pair it with the gap count and the boolean mask rather than trusting it alone. Reach for the delivery ratio when you need a fast per-window quality score for dashboards or gating; reach for the full mask when a downstream stage must know exactly which slots are empty.
import numpy as np
def profile_stream(timestamps, fs_nominal):
"""Diagnose jitter, gaps, and delivery ratio from arrival timestamps."""
t = np.asarray(timestamps, dtype=float)
dt = np.diff(t)
Ts = 1.0 / fs_nominal
jitter_std = dt.std() # timing irregularity, seconds
gap_mask = dt > 1.5 * Ts # intervals that skipped samples
n_missing = int(np.round((dt[gap_mask] / Ts - 1).sum()))
expected = int(np.round((t[-1] - t[0]) / Ts)) + 1
delivery_ratio = len(t) / expected
# Build a uniform grid and a mask marking which grid slots were observed.
grid = t[0] + np.arange(expected) * Ts
idx = np.clip(np.round((t - t[0]) / Ts).astype(int), 0, expected - 1)
observed = np.zeros(expected, dtype=bool)
observed[idx] = True # True = real sample, False = hole
return dict(jitter_std=jitter_std, n_missing=n_missing,
delivery_ratio=delivery_ratio, grid=grid, observed=observed)
# Simulate a 50 Hz stream with jitter and a 1-second dropout burst.
rng = np.random.default_rng(0)
Ts = 1 / 50
clean = np.arange(0, 10, Ts)
jittered = clean + rng.normal(0, 0.002, clean.size) # 2 ms RMS jitter
kept = jittered[(jittered < 4.0) | (jittered > 5.0)] # drop 1 s of packets
p = profile_stream(kept, fs_nominal=50)
print(f"jitter RMS : {p['jitter_std']*1e3:5.1f} ms")
print(f"missing count: {p['n_missing']}")
print(f"delivery : {p['delivery_ratio']*100:5.1f} %")
profile_stream: profiling a jittered, lossy sensor stream from timestamps alone. The function returns the jitter RMS, missing count, delivery ratio, and a boolean observed mask aligned to a uniform grid, so downstream code can distinguish a real zero from an absent sample instead of guessing.Running it on the simulated stream reports roughly 2 ms of jitter, about 50 missing samples across the one-second dropout, and a delivery ratio near 90 percent. The mask is the deliverable that matters: it travels with the data and lets every later stage decide, explicitly, how to treat the holes.
Step-Through: profile_stream on five timestamps
Trace the profiler with a tiny stream at a nominal 10 Hz, so \(T_s = 0.1\) s. Arrival timestamps: \(t = [0.00, 0.10, 0.20, 0.50, 0.60]\) seconds.
- Intervals. \(\Delta t = \text{diff}(t) = [0.10, 0.10, 0.30, 0.10]\) s. The third gap is triple the nominal spacing.
- Jitter. The mean interval is \(0.15\) s, and \(\sigma_{\Delta t} = \sqrt{\tfrac{1}{4}(0.05^2 + 0.05^2 + 0.15^2 + 0.05^2)} = \sqrt{0.0075} \approx 0.0866\) s, so
jitter_stdprints as 86.6 ms (large here only because the gap inflates the spread). - Gap mask. The threshold is \(1.5\,T_s = 0.15\) s. Only \(0.30 > 0.15\), so
gap_mask = [False, False, True, False]. - Missing count. \(n\_missing = \text{round}(0.30/0.1 - 1) = \text{round}(2) = 2\): two samples vanished inside that one gap.
- Expected and delivery. \(\text{expected} = \text{round}((0.60 - 0.00)/0.1) + 1 = 7\), so
delivery_ratio = 5 / 7 = 0.714, about 71.4 percent. - Grid and mask. The grid is \([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6]\). Snapped indices are \([0, 1, 2, 5, 6]\), giving
observed = [True, True, True, False, False, True, True]. The twoFalseslots at \(0.3\) and \(0.4\) s are exactly the two missing samples the count predicted.
Library Shortcut
The uniform-grid alignment and mask construction above (roughly 12 lines of index arithmetic) collapses to two lines with pandas. Index your series by a DatetimeIndex, then s.resample("20ms").asfreq() snaps to a regular grid and inserts NaN at every hole; s.resample("20ms").asfreq().isna() is your missingness mask. pandas also handles time-zone-aware timestamps, leap-second edge cases, and the off-by-one grid boundaries that the hand-rolled version glosses over. Reach for the hand-rolled path only on a microcontroller where pandas will not fit.
Repair strategies, and when to trust them
Once holes are marked, you have four broad options, in ascending order of assumption. Leave them: many models, from the Kalman filter to modern attention, can consume a mask and skip absent inputs. A Kalman update, covered in Chapter 9, handles a missing measurement by running the prediction step alone and letting uncertainty grow. Interpolate: linear or spline fills are cheap and adequate for short gaps in slowly varying signals, but they fabricate high-frequency content and will fool a spectral feature extractor. Filter-based reconstruction: model-aware smoothing, the subject of Chapter 6, respects the signal's bandwidth when bridging gaps. Learned imputation: powerful, but it can hallucinate plausible values and, worse, leak information if fit on data that overlaps your evaluation set. The golden rule: interpolate for visualization freely, but never let an imputed value enter a training label or a safety-critical decision without a flag that says it was invented.
Research Frontier
The methods here treat the irregular grid as a defect to patch. A newer line treats continuous time as the native representation and never snaps to a grid at all. ContiFormer (Chen et al., NeurIPS 2023) fuses neural ordinary differential equations with the attention mechanism so that a Transformer can attend across genuinely continuous, irregularly sampled timestamps, learning the dynamics between observed points rather than imputing them first. On the standard irregular-time-series benchmarks it outperforms both discrete Transformers fed interpolated inputs and earlier neural-ODE models, which points toward a future where the missingness mask is an input the model reasons over natively instead of a hole you fill before training.
Exercise
Take a 100 Hz inertial measurement unit (IMU) recording (or synthesize one). Delete samples under three regimes: MCAR (drop 10 percent uniformly at random), bursty (drop five contiguous 200 ms windows), and MNAR (drop samples whenever \(|a| > 2g\)). For each, compute the root mean square (RMS) error of linear interpolation against the ground truth and the resulting shift in the signal's spectral centroid. Which regime does interpolation handle worst, and why does the MNAR case damage a fall-detection feature far more than its raw RMS error suggests?
Try It: Profile and repair a lossy stream in 20 minutes
Build the whole measure-then-repair loop end to end on your laptop with only NumPy, SciPy, and Matplotlib.
- Synthesize ground truth:
t = np.arange(0, 20, 1/100)andx = np.sin(2*np.pi*3*t) + 0.3*np.sin(2*np.pi*17*t), a 100 Hz signal with a 3 Hz and a 17 Hz component. - Corrupt it: add 3 ms Gaussian jitter to
t, then delete every sample falling inside three 300 ms windows to simulate bursty packet loss. Keep the surviving(t, x)pairs. - Profile the survivors with the
profile_streamfunction from this section and print the jitter RMS, missing count, and delivery ratio. - Repair two ways on the uniform grid: fill the holes with
numpy.interp(linear) and again withscipy.interpolate.CubicSpline. Overlay both against the ground truth and compute the RMS error of each. - Take an FFT of the original, the linear fill, and the spline fill, and check what happened to the 17 Hz peak. Confirm for yourself that the interpolation smeared the high-frequency component even where the RMS error looked small.
Self-Check
- A stream has a delivery ratio of 98 percent but a jitter RMS of 8 ms at a nominal 100 Hz. Is this stream safe to feed directly into an FFT? Justify your answer in terms of phase error.
- Why is a burst of packet loss on a BLE link often recoverable in a way that a sensor brownout (a brief supply-voltage dip that resets or stalls the sensor mid-measurement) is not?
- You impute a gap with the segment mean and your model's accuracy improves. Give one reason this could indicate a problem rather than a success.
The missing packets between here and Mars
Missing samples are not just a wearable annoyance; on deep-space links they are severe enough to have reshaped how spacecraft talk to Earth. Deep-space links to Mars carry one-way light-time delays of roughly three to twenty-two minutes, and packets are routinely lost to solar noise and antenna scheduling, so the Deep Space Network relies on the Consultative Committee for Space Data Systems (CCSDS) File Delivery Protocol (CFDP) and related store-and-forward standards precisely because a naive stream would be full of holes. The counterintuitive lesson: at interplanetary distances engineers stopped trying to guarantee a uniform sample grid at all and instead ship self-describing packets, each stamped with its own time, so a receiver can reconstruct order and mark gaps no matter which datagrams survive the 200-million-kilometer trip. It is the same "carry the true timestamp, mark the hole" discipline this section preaches, just with a worse worst-case ping.
Lab: Break a real IMU stream and watch a classifier degrade
Goal. Feel, empirically, how the pattern of missingness (not just its amount) governs downstream harm.
Tools. Python with NumPy, SciPy, scikit-learn, and Matplotlib, plus a labeled human-activity IMU dataset such as UCI HAR or PAMAP2 (both free downloads of accelerometer and gyroscope recordings with activity labels).
Steps. Load one subject's accelerometer channel and train a small random-forest activity classifier on clean windows to get a baseline accuracy. Then inject missingness three ways at a matched 10 percent loss: (1) MCAR, drop samples uniformly at random; (2) bursty, delete a few contiguous 300 ms windows; (3) MNAR, drop samples whenever the acceleration magnitude exceeds a high threshold. Repair each corrupted stream with linear interpolation on a uniform grid, re-extract features, and re-score the frozen classifier.
What to vary. Sweep the loss fraction from 2 to 30 percent and, separately, the burst length. What to observe. Plot accuracy versus loss fraction for all three regimes on one axis. You should see the MCAR curve stay nearly flat while the MNAR curve falls off a cliff at a loss fraction where MCAR is still harmless, making concrete the section's claim that which samples vanish dominates how many.
What's Next
Section 3.5 turns from the timing of samples to their values: how quantization and compression trade bits for fidelity, why an 8-bit ADC and a lossy codec each impose their own noise floor, and how to reason about the distortion budget before it silently caps what any downstream model can perceive.