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.
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.
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.
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,satandfixif the file has them. - Altitude error is roughly twice horizontal error β never derive gradient from raw GPS altitude.
Internal links
- Trajectories explained: what makes a track different from points β where derived speed comes from
- Sampling rate and gaps explained β why the interval sets the noise floor
- How to clean a GPS track: outliers, duplicates and jumps β the full cleaning sequence
- My track has impossible speeds or teleports β diagnosing the extreme values
- How to map-match a GPS track to a street network β the only fix for multipath bias
- How to split a track into trips and stops in Python β removing phantom distance
- How to calculate speed, distance and direction along a track β computing them robustly
- Coordinate precision and floating point in GIS explained β a different kind of position error
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.