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%)
A GPS interval distribution with a median of 3 seconds, a 95th percentile of 11 seconds and a long tail out to 2,989 seconds.
The nominal rate describes the median. The tail is what breaks the analysis.

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.

A long GPS gap drawn as a straight line against the same gap represented as a break between two track legs.
A 50-minute gap drawn as a straight line asserts a journey nobody recorded.

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.

Four trajectory metrics rated for robustness to the sampling interval, with path length the least robust.
Route topology survives coarse sampling. Distance does not, and speed least of all.

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.

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.