GPS Error Explained: Why Your Track Wanders

Problem statement

A GPS track of someone standing still is not a point. It is a cloud a few metres across, drifting slowly. A track along a straight road is not straight. And the derived speeds contain values nothing on that road could reach.

Measured on 13 real GPS traces whose median speed was 4.7 km/h β€” walking pace:

p50        1.32 m/s      4.7 km/h
p90        6.12 m/s     22.0 km/h
p99       21.10 m/s     76.0 km/h
p99.9     61.68 m/s    222.0 km/h
max      175.14 m/s    630.5 km/h

Nobody walked at 630 km/h. That value is the ratio of a position error to a short time interval, and understanding which parts of the distribution are movement and which are error is the whole of GPS data cleaning.

Quick answer

Position error divided by a short interval produces a large speed error:

# a rough noise floor for derived speed
position_error_m = 5.0          # typical consumer horizontal error
interval_s = 1.0
speed_noise = position_error_m * (2 ** 0.5) / interval_s
print(f"~{speed_noise:.1f} m/s of speed noise from position error alone")
~7.1 m/s of speed noise from position error alone

Seven metres per second is 25 km/h. At one-second sampling, that noise floor sits above the actual speed of a pedestrian β€” which is why high-frequency pedestrian tracks look erratic and low-frequency ones look smooth.

A five metre position error producing seven metres per second of speed noise at one second sampling and 0.6 at ten seconds.
The same position error becomes a different amount of speed error depending only on the interval.

Step-by-step solution

1. Know the error sources and which ones you can fix

  • Multipath β€” the signal bounces off a building before reaching the receiver, so the computed range is too long. This is the dominant urban error and it is biased, not random: reflections make you appear further from the satellite, and in a street canyon they push consistently in one direction.
  • Satellite geometry (DOP) β€” satellites clustered in one part of the sky give a poorly conditioned solution. Recorded as HDOP or PDOP where the format supports it.
  • Atmospheric delay β€” ionospheric and tropospheric refraction, largely corrected by modern receivers and by differential methods.
  • Receiver noise β€” the irreducible floor, a metre or two.
  • Cold start and reacquisition β€” the first fixes after losing lock are much worse than the steady state.

Only the last two behave like random noise. The rest are correlated in time and space, which is why averaging does not remove them.

2. Expect error to be worst where the analysis is hardest

Multipath is worst among tall buildings. Signal loss is worst in tunnels and under canopy. Reacquisition error follows every gap.

So error concentrates in dense urban areas, exactly where you want to know which street someone used. The measured error is not spread evenly over the track; it is concentrated at the interesting parts.

3. Separate error from movement using physics

The measurement above shows the shape of the problem. A pedestrian dataset with a median of 4.7 km/h contains 0.2% of intervals implying over 200 km/h.

Filtering by a plausible maximum speed is the first line of defence:

speed <= 200 km/h: removed  13 points, length 37.69 km  (-0.44%)
speed <= 100 km/h: removed  42 points, length 37.27 km  (-1.57%)
speed <=  50 km/h: removed 121 points, length 36.19 km  (-4.42%)
speed <=  29 km/h: removed 333 points, length 35.27 km  (-6.84%)

A generous threshold removes almost nothing and costs almost nothing. A tight one removes real movement β€” this dataset contains genuine vehicle travel, so a 29 km/h cap is deleting data rather than noise.

4. Prefer a threshold you can justify

Set the cap from the mode of transport, not from the histogram. A pedestrian study can cap at 10 m/s; a mixed-mode dataset cannot, because the same file contains walking and driving.

Where modes are mixed, filter on implausibility β€” a speed nothing achieves, like 200 km/h on urban streets β€” and handle mode separation as its own step.

5. Understand what smoothing does and does not fix

A moving average or Kalman filter reduces uncorrelated noise. Multipath is correlated over seconds to minutes, so smoothing reduces its visible jitter while leaving its bias in place.

The result is a smooth track that is confidently in the wrong street. For street-level accuracy, map matching against a road network is the tool that actually helps β€” see How to map-match a GPS track to a street network.

Random receiver noise scattering around the true position against multipath bias displacing the track consistently to one side of a street.
Smoothing removes the scatter and keeps the bias. Only external information β€” a road network β€” removes the bias.

Code examples

Example 1 β€” an error-aware speed filter

import numpy as np


def flag_implausible(df, id_col="track_id", max_speed_ms=56.0,
                     max_accel_ms2=10.0):
    """Flag rather than delete, so the decision stays visible."""
    df = df.sort_values([id_col, "time"]).copy()
    g = df.groupby(id_col)

    df["dt"] = g["time"].diff().dt.total_seconds()
    df["dist"] = np.hypot(g["x"].diff(), g["y"].diff())
    df["speed"] = df["dist"] / df["dt"].replace(0, np.nan)
    df["accel"] = df.groupby(id_col)["speed"].diff() / df["dt"].replace(0, np.nan)

    df["flag_speed"] = df["speed"] > max_speed_ms
    df["flag_accel"] = df["accel"].abs() > max_accel_ms2
    df["flag_zero_dt"] = df["dt"] == 0
    df["flag"] = df[["flag_speed", "flag_accel", "flag_zero_dt"]].any(axis=1)

    for name in ("flag_speed", "flag_accel", "flag_zero_dt"):
        print(f"  {name:14} {int(df[name].sum()):6,} "
              f"({df[name].mean():6.3%})")
    print(f"  {'any flag':14} {int(df['flag'].sum()):6,} "
          f"({df['flag'].mean():6.3%})")
    return df

Acceleration catches something speed alone misses: a single displaced point produces one very high speed into it and one very high speed out of it, so the acceleration between them is enormous. A genuine acceleration to 30 m/s takes several seconds.

Example 2 β€” a spike filter that removes the point, not the track

import numpy as np


def remove_spikes(df, id_col="track_id", max_speed_ms=56.0, max_passes=5):
    """Iteratively drop points whose removal makes the track plausible."""
    df = df.sort_values([id_col, "time"]).reset_index(drop=True).copy()

    for step in range(max_passes):
        g = df.groupby(id_col)
        dt = g["time"].diff().dt.total_seconds()
        dist = np.hypot(g["x"].diff(), g["y"].diff())
        speed = dist / dt.replace(0, np.nan)

        bad = speed > max_speed_ms
        if not bad.any():
            print(f"  converged after {step} pass(es)")
            break

        # a spike shows as a high speed in AND out; drop the middle point
        next_bad = bad.shift(-1, fill_value=False)
        spike = bad & next_bad
        drop = spike if spike.any() else bad
        print(f"  pass {step + 1}: dropping {int(drop.sum()):,} points")
        df = df[~drop].reset_index(drop=True)

    return df

Iterating matters. Removing one spike changes the speeds of its neighbours, which can reveal a second spike that was hidden by the first.

Example 3 β€” using the recorded quality fields

import numpy as np


def quality_filter(df, hdop_max=5.0, min_sats=4, fix_types=("3d",)):
    """If the format recorded quality, use it rather than inferring it."""
    mask = np.ones(len(df), bool)
    report = {}

    if "hdop" in df:
        ok = df["hdop"] <= hdop_max
        report["hdop"] = float((~ok).mean()); mask &= ok.fillna(True)
    if "sat" in df:
        ok = df["sat"] >= min_sats
        report["satellites"] = float((~ok).mean()); mask &= ok.fillna(True)
    if "fix" in df:
        ok = df["fix"].isin(fix_types)
        report["fix_type"] = float((~ok).mean()); mask &= ok.fillna(True)

    if not report:
        print("  no quality fields in this file β€” falling back to physics")
    for key, share in report.items():
        print(f"  {key:12} would remove {share:6.2%}")
    print(f"  keeping {mask.mean():.2%} of points")
    return df[mask]

GPX supports hdop, sat and fix elements, and most exporters omit them. When they are present they are better evidence than any physical filter, because they describe the solution rather than its consequences.

Explanation

Why position error becomes large speed error

Derived speed is the distance between two noisy positions divided by the time between them. Each position carries an independent error of roughly Οƒ, so their difference carries about Οƒβˆš2, and dividing by a short interval multiplies it.

At Οƒ = 5 m and a 1-second interval that is about 7 m/s β€” 25 km/h of noise on a measurement whose true value might be 1.4 m/s.

Two practical consequences. Derived speed at high sampling rates is dominated by noise, so smooth it or use the receiver's Doppler speed. And the noise floor scales as 1/interval, which is why coarse logs give cleaner-looking speeds β€” they are averaging over more real movement.

Why multipath is worse than noise

Random noise averages out. Multipath does not: a reflection always makes the measured range longer than the true one, and in a street canyon the same buildings reflect the same way for as long as you are there.

The result is a track that is smoothly, confidently and consistently displaced β€” often onto the parallel street. No amount of filtering fixes it, because the data contains no information about the true position.

This is why map matching exists. It brings in a road network as external evidence and asks which sequence of roads best explains the observations, which can recover a displacement that is systematically wrong.

Why standing still produces the worst-looking data

When a receiver is stationary, all of the apparent movement is error. The track becomes a wandering blob a few metres across, generating a stream of small, meaningless displacements and headings.

Two consequences. Stationary periods inflate total distance β€” hundreds of tiny displacements accumulate into hundreds of metres of phantom travel. And they produce meaningless headings, which corrupt any turn or direction analysis.

Detecting and collapsing stops before computing distance is therefore not a refinement; it is a correctness fix. See How to split a track into trips and stops in Python.

Why the accuracy figure on the box is optimistic

Quoted accuracies are typically 50th-percentile values under open-sky conditions with a good satellite constellation. Real deployments are the tail: urban canyons, in a pocket, under a windscreen, after a cold start.

Design filters for the tail. The measurement above had a 99.9th-percentile derived speed of 222 km/h in a pedestrian dataset β€” the tail is where the work is.

Speed caps removing 0.44 percent of distance at 202 km/h and 6.84 percent at 29 km/h.
Removing the impossible is nearly free. Removing the merely surprising costs real movement.

Edge cases or notes

  • Speed noise scales as 1/interval. High-rate logs look worse and are not.
  • Multipath is biased, not random, so smoothing cannot remove it.
  • Stationary periods generate phantom distance. Detect stops before summing.
  • Reacquisition after a gap gives the worst fixes. Consider dropping the first few.
  • Flag rather than delete where you can, so the choice is auditable.
  • Filter on acceleration as well as speed to catch single displaced points.
  • Use hdop, sat and fix if the file has them.
  • Altitude error is roughly twice horizontal error β€” never derive gradient from raw GPS altitude.

FAQ

Why does my GPS track wander when I am standing still?

Because all apparent movement is error. Receiver noise and multipath produce a drifting cloud a few metres across, which accumulates into phantom distance if you sum it.

Why do I see impossible speeds in walking data?

Position error divided by a short interval. With 5 m error and 1-second sampling the noise floor for derived speed is about 7 m/s β€” above walking pace.

What speed threshold should I filter at?

One justified by the mode of transport, and generous. On real traces, capping at 200 km/h removed 13 points and 0.44% of the distance; capping at 29 km/h removed 333 points and 6.8%, much of it real vehicle travel.

Does smoothing fix GPS error?

Only the random part. Multipath is correlated and biased, so smoothing produces a smooth track in the wrong street. Map matching is the fix for that.

Why is my track in the wrong street?

Multipath in an urban canyon. Reflected signals lengthen the measured range consistently, displacing the solution β€” often onto a parallel road.

Should I use the altitude from GPS?

Not for gradients. Vertical error is roughly twice horizontal error, so derived slopes are dominated by noise. Sample a DEM at the track positions instead.

What are HDOP and PDOP?

Dilution of precision: how well the satellite geometry conditions the position solution. Lower is better; above about 5 the fix is weak.