Sampling Rate and Gaps Explained: How Often Is Often Enough
Problem statement
The sampling interval of a GPS log is usually chosen by whatever the device defaulted to, and it silently determines what your analysis can and cannot say.
Measured on 13 real GPS traces, resampled from their native ~3-second interval:
native 37.86 km
5 s 35.11 km -7.3%
10 s 33.55 km -11.4%
30 s 31.20 km -17.6%
60 s 29.67 km -21.6%
120 s 27.89 km -26.3%
Same journeys, same receiver, same code. A quarter of the distance disappears between 3-second and 2-minute logging.
The interval also is not one number. Across those traces the median gap was 3 s, the 95th percentile 11 s and the maximum 2,989 s β nearly fifty minutes with no fix at all.
Quick answer
Report the interval distribution, not the nominal rate:
import numpy as np
dt = df.groupby("track_id")["time"].diff().dt.total_seconds().dropna()
print(f"median {dt.median():.0f}s p95 {dt.quantile(0.95):.0f}s "
f"max {dt.max():,.0f}s")
for threshold in (5, 30, 60, 300):
print(f" gaps over {threshold:4d}s: {int((dt > threshold).sum()):5,} "
f"({(dt > threshold).mean():6.2%})")
median 3s p95 11s max 2,989s
gaps over 5s: 567 ( 8.85%)
gaps over 30s: 128 ( 2.00%)
gaps over 60s: 46 ( 0.72%)
gaps over 300s: 3 ( 0.05%)
Step-by-step solution
1. Work out what interval your question needs
Different questions have different requirements, and they differ by orders of magnitude:
| question | interval needed | why |
|---|---|---|
| where did this vehicle go | 30β60 s | route topology survives coarse sampling |
| how far did it travel | 1β5 s | distance is sampling-dependent |
| where did it stop | 5β30 s | a 2-minute stop needs several fixes inside it |
| how fast at a point | 1 s, or the receiver's own speed | derived speed needs a short baseline |
| did it exceed a speed limit | 1 s, plus Doppler speed | averages hide brief excesses |
| turn-by-turn behaviour | 1 s | turns take a few seconds |
"Where did it go" is remarkably robust; "how far did it go" is not.
2. Measure the interval you actually have
Nominal rates are aspirations. A device set to 1 Hz loses lock in tunnels, throttles on low battery, and may drop to event-based logging when stationary.
The distribution above has a median of 3 s and a maximum of 2,989 s. Both matter: the median sets the resolution, the tail sets where the track is fiction.
3. Treat long gaps as breaks, not as long straight lines
A 2,989-second gap interpolated as a straight line at constant speed is a fabrication. Where the receiver was off, the honest representation is a break.
GAP_SECONDS = 120
df["new_leg"] = (df["dt"] > GAP_SECONDS) | df["dt"].isna()
df["leg"] = df.groupby("track_id")["new_leg"].cumsum()
Splitting at gaps turns one impossible line into two honest ones with a hole between them.
4. Resample to a common interval before comparing datasets
Two fleets, two loggers, two mileage figures for the same driving. The only fix is to downsample both to the coarser interval and compare like with like. See How to resample and interpolate a track to a fixed interval.
5. Do not upsample to fix a coarse log
Interpolating a 60-second log to 1 Hz produces sixty points a minute, of which one is a measurement. The interpolated distance stays at 29.67 km rather than rising to 37.86 km, because interpolation on a straight line adds points without adding path.
Upsampling is useful for aligning two datasets in time. It does not recover information.
Code examples
Example 1 β an interval report with the tail made visible
import numpy as np
import pandas as pd
def interval_report(df, id_col="track_id", time_col="time",
thresholds=(2, 5, 10, 30, 60, 300, 900)):
"""The nominal rate, the real distribution, and where the holes are."""
dt = df.sort_values([id_col, time_col]).groupby(id_col)[time_col] \
.diff().dt.total_seconds().dropna()
print(f" {len(dt):,} intervals across {df[id_col].nunique()} tracks")
for q in (5, 25, 50, 75, 95, 99):
print(f" p{q:<3} {np.percentile(dt, q):9.1f} s")
print(f" max {dt.max():9.1f} s")
total = dt.sum()
for t in thresholds:
over = dt[dt > t]
print(f" over {t:5d}s: {len(over):5,} gaps "
f"({len(over) / len(dt):6.2%} of intervals, "
f"{over.sum() / total:6.2%} of elapsed time)")
return dt
The second percentage is the one people miss. A handful of long gaps can be a negligible fraction of the intervals and most of the elapsed time β meaning the track covers far less of the journey than its point count suggests.
Example 2 β splitting a track at its gaps
import numpy as np
def split_on_gaps(df, id_col="track_id", time_col="time",
max_gap_s=120, max_jump_m=None):
"""One leg per continuously observed stretch."""
df = df.sort_values([id_col, time_col]).copy()
g = df.groupby(id_col)
dt = g[time_col].diff().dt.total_seconds()
breaks = dt > max_gap_s
if max_jump_m is not None:
jump = np.hypot(g["x"].diff(), g["y"].diff())
breaks |= jump > max_jump_m
breaks |= dt.isna() # first row of each track
df["leg"] = breaks.groupby(df[id_col]).cumsum()
df["leg_id"] = df[id_col].astype(str) + "-" + df["leg"].astype(str)
legs = df.groupby("leg_id").size()
print(f" {df[id_col].nunique()} tracks -> {len(legs)} legs")
print(f" legs with fewer than 3 points: {int((legs < 3).sum())}")
return df
Splitting on a distance jump as well as a time gap catches the other failure: a short interval with an impossible displacement, which is a position error rather than a pause.
Example 3 β what interval does your question tolerate?
import numpy as np
def question_sensitivity(gdf, metric, id_col="track_id", time_col="time",
steps=(1, 5, 10, 30, 60, 120)):
"""Recompute your actual metric at several intervals and see if it moves."""
base = metric(gdf)
print(f" native: {base:.4g}")
for step in steps:
keep = []
for _, sub in gdf.groupby(id_col):
elapsed = (sub[time_col] - sub[time_col].iloc[0]) \
.dt.total_seconds().values
picked = [0]
for i in range(1, len(elapsed)):
if elapsed[i] - elapsed[picked[-1]] >= step:
picked.append(i)
keep.append(sub.iloc[picked])
value = metric(pd.concat(keep))
print(f" {step:4d}s: {value:.4g} ({value / base - 1:+.1%})")
Do this with your metric rather than with distance. Some are far more robust than others: total displacement between start and end is almost unaffected by the interval, while total path length loses a quarter.
Explanation
Why coarser sampling always shortens the path
Between two fixes the software assumes a straight line. Any deviation from straight between them is lost, and deviations exist at every scale β lane changes, gentle curves, the wobble of a walking gait.
So measured length falls monotonically as the interval grows, and never rises. It is the coastline paradox with time as the ruler: 37.86 km measured at 3 s becomes 27.89 km at 120 s.
There is no "true" length independent of the ruler. There is only length-at-an-interval, which is why the interval belongs beside the number.
Why the gap distribution is more useful than the rate
A device advertised as "1 Hz" produces intervals of 1 s most of the time and much longer occasionally, and the occasional ones concentrate exactly where the environment is difficult β urban canyons, tunnels, dense canopy.
That correlation matters. The gaps are not random missing data; they are systematically located in the places where routing, mode detection and stop detection are hardest. An analysis that ignores them is biased, not merely noisy.
Why the receiver's own speed beats derived speed
Many receivers compute speed from Doppler shift on the carrier signal, which is an independent measurement rather than a difference of two noisy positions.
Derived speed inherits the error of both endpoints, divided by the interval. With 5 m of position error and a 1-second interval, that is roughly 7 m/s of noise β which is why the highest-frequency logs contain the most implausible speeds.
If your data has a speed column from the device, prefer it, and use derived speed only as a cross-check.
Why upsampling does not help
Interpolating between fixes adds rows along the straight line already assumed. Distance along a straight line does not change when you add points to it, so the measured length is unchanged.
Upsampling is genuinely useful for one thing: putting two datasets on a common time base so they can be compared or joined. It is not a way to recover a path that was never sampled.
Edge cases or notes
- Report the interval with every distance. It is part of the measurement.
- Use the median and the 95th percentile, not the mean β one long gap dominates the mean.
- Split at long gaps rather than interpolating across them.
- A gap of zero is also a problem: two fixes in the same second give infinite derived speed.
- Gaps cluster in hard environments, so missing data is not missing at random.
- Downsample to compare, never upsample.
- Devices change rate mid-log on low battery or when stationary; check for regime changes.
- Displacement is robust, path length is not. Choose the metric that survives your sampling.
Internal links
- Trajectories explained: what makes a track different from points β the sampling-rate effect in context
- How to resample and interpolate a track to a fixed interval β the implementation
- GPS error explained: why your track wanders β the other source of bad speeds
- How to clean a GPS track: outliers, duplicates and jumps β handling the tail
- Stops, trips and segmentation explained β why stop detection needs a minimum rate
- How to calculate speed, distance and direction along a track β the derived columns
- My track has impossible speeds or teleports β the zero-interval case
- How to load GPS tracks into Python as trajectories β where to compute the report
FAQ
What GPS sampling rate should I use?
It depends on the question. Route topology survives 30β60 s; distance needs 1β5 s; stop detection needs several fixes inside the shortest stop you care about.
Why does my track distance change with the sampling rate?
Because straight lines between fixes cut every deviation shorter than the interval. Measured length fell 26% between 3-second and 2-minute sampling on real traces.
Can I recover detail by interpolating to a higher rate?
No. Interpolation adds points along the straight line already assumed, so the measured length does not change. It is useful only for aligning datasets in time.
What should I do about long gaps?
Split the track into legs at gaps above a threshold. Drawing a straight line across a 50-minute gap asserts a journey that was never recorded.
Is a mean interval useful?
Not really. One long gap dominates it. Report the median, the 95th percentile and the maximum.
Why does high-frequency GPS show more impossible speeds?
Because derived speed is a position difference divided by a short interval, so position noise is amplified. Use the receiver's Doppler speed if it is available.
How do I compare two datasets logged at different rates?
Downsample both to the coarser interval before computing anything, and state the interval alongside every distance.