How to Resample and Interpolate a Track to a Fixed Interval
Problem statement
Two datasets logged at different rates cannot be compared. The finer one reports longer journeys for identical driving, because straight lines between fixes cut fewer corners.
Measured on 13 real traces, resampled from their native ~3-second interval:
native 37.86 km
1 s 37.77 km -0.2%
2 s 36.87 km -2.6%
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%
Resampling is how you make them comparable β and the direction matters. Downsampling to a common interval works; upsampling to recover detail does not.
Quick answer
Downsample by time, not by row count:
import numpy as np
def downsample(sub, step_s):
"""Keep the first fix, then the next fix at least step_s later."""
elapsed = (sub["time"] - sub["time"].iloc[0]).dt.total_seconds().values
keep = [0]
for i in range(1, len(elapsed)):
if elapsed[i] - elapsed[keep[-1]] >= step_s:
keep.append(i)
return sub.iloc[keep]
Taking every nth row instead gives a variable interval whenever the source rate varies, which it always does β the traces measured here had a median interval of 3 s and a maximum of 2,989 s.
Step-by-step solution
1. Decide which direction you need
- Downsample to compare datasets, to reduce volume, or to match a model's time step. It loses detail, and you can quantify exactly how much.
- Upsample to align two datasets in time, or to feed a routine that requires a regular series. It does not recover detail.
Both are legitimate. Confusing them is not.
2. Downsample by elapsed time
The greedy rule above β keep a fix if it is at least step after the last kept one β produces intervals of at least step and never resamples across a gap in a misleading way.
The alternative, df.resample("30s").first(), produces a regular index with NaN in every empty bin. That is useful when you want a strictly regular series and want the gaps visible, and unhelpful when you want a smaller set of real fixes.
3. Never resample across a gap
A 2,989-second gap resampled to 10 seconds produces 299 interpolated points along a straight line that nobody travelled.
df["leg"] = (df["dt"] > gap_s).groupby(df["track_id"]).cumsum()
Split into legs first, resample within each, and leave the gaps as gaps.
4. If upsampling, interpolate positions β and mark them
resampled = (sub.set_index("time")[["x", "y"]]
.resample(f"{step}s").mean()
.interpolate(method="time"))
resampled["interpolated"] = sub.set_index("time")["x"] \
.resample(f"{step}s").count().eq(0).values
The interpolated flag is the important line. An interpolated position is a model output, and any statistic computed over the series should be able to exclude it or weight it down.
5. Report the interval with every derived number
Distance, mean speed and time-in-motion are all interval-dependent. State the interval the way you state the units.
Code examples
Example 1 β downsampling with the cost reported
import numpy as np
import pandas as pd
def downsample_tracks(df, step_s, id_col="track_id", gap_s=None):
"""Downsample every track to a minimum interval, respecting gaps."""
if gap_s:
g = df.groupby(id_col)
dt = g["time"].diff().dt.total_seconds()
df = df.copy()
df["leg"] = (dt > gap_s).groupby(df[id_col]).cumsum()
group_cols = [id_col, "leg"]
else:
group_cols = [id_col]
kept = []
for _, sub in df.sort_values(group_cols + ["time"]).groupby(group_cols):
elapsed = (sub["time"] - sub["time"].iloc[0]).dt.total_seconds().values
keep = [0]
for i in range(1, len(elapsed)):
if elapsed[i] - elapsed[keep[-1]] >= step_s:
keep.append(i)
kept.append(sub.iloc[keep])
out = pd.concat(kept, ignore_index=True)
def length(frame):
gg = frame.groupby(group_cols)
return float(np.hypot(gg["x"].diff(), gg["y"].diff()).sum())
before, after = length(df), length(out)
print(f" {len(df):,} -> {len(out):,} points ({len(out) / len(df):.1%})")
print(f" {before / 1000:.2f} km -> {after / 1000:.2f} km "
f"({after / before - 1:+.1%})")
return out
6,417 -> 2,411 points (37.6%)
37.86 km -> 33.55 km (-11.4%)
Printing both the point reduction and the distance change makes the trade-off explicit at the moment you make it.
Example 2 β upsampling to a regular series, with flags
import numpy as np
import pandas as pd
def upsample_tracks(df, step_s, id_col="track_id", gap_s=300,
method="time"):
"""Regular time series per leg, with interpolated rows marked."""
g = df.groupby(id_col)
df = df.copy()
df["leg"] = (g["time"].diff().dt.total_seconds() > gap_s) \
.groupby(df[id_col]).cumsum()
out = []
for keys, sub in df.groupby([id_col, "leg"]):
sub = sub.sort_values("time").set_index("time")
regular = sub[["x", "y"]].resample(f"{int(step_s)}s").mean()
observed = sub[["x"]].resample(f"{int(step_s)}s").count()["x"] > 0
regular = regular.interpolate(method=method, limit_area="inside")
regular["interpolated"] = ~observed.reindex(regular.index,
fill_value=False).values
regular[id_col], regular["leg"] = keys
out.append(regular.reset_index())
result = pd.concat(out, ignore_index=True)
print(f" {len(df):,} observed -> {len(result):,} rows, "
f"{result['interpolated'].mean():.1%} interpolated")
return result
limit_area="inside" stops the interpolation extending past the last real fix in a leg, which would extrapolate the journey beyond where it was observed.
Example 3 β matching two datasets to a common interval
import numpy as np
def harmonise(datasets, id_col="track_id"):
"""Downsample every dataset to the coarsest median interval present."""
intervals = {}
for name, df in datasets.items():
dt = df.groupby(id_col)["time"].diff().dt.total_seconds().dropna()
intervals[name] = float(dt.median())
print(f" {name:16} median interval {intervals[name]:6.1f}s, "
f"{len(df):,} points")
target = max(intervals.values())
print(f" harmonising everything to {target:.0f}s")
return {name: downsample_tracks(df, target, id_col=id_col)
for name, df in datasets.items()}
Choosing the coarsest is the only safe direction. Harmonising upward would upsample the coarse dataset, which changes its row count without changing its measured distance and produces two series that look comparable and are not.
Explanation
Why downsampling always shortens the track
Between two kept fixes, the path is assumed straight, so every deviation shorter than the new interval is discarded. Length therefore falls monotonically and never rises.
The measured curve is steep at first and then flattens: 2.6% lost at 2 seconds, 11.4% at 10, 26.3% at 120. The steepness depends on how sinuous the movement is β a motorway drive loses very little, a walk through a park loses a lot.
Why upsampling adds rows and not length
Interpolating between two fixes places new points on the straight line already assumed between them. The path length along a straight line does not change when you add points to it.
So an upsampled 60-second log still measures 29.67 km, not 37.86. The rows increased twenty-fold and the information did not change at all.
The legitimate uses of upsampling are alignment β putting two datasets on the same time base for joining or comparison β and satisfying a downstream tool that requires regular sampling. Neither is about recovering detail.
Why linear interpolation is usually enough
More sophisticated interpolators exist β cubic splines, Kalman smoothers, map-matched paths β and each imports assumptions.
A spline through GPS points overshoots at sharp turns, producing positions outside the corridor the vehicle occupied. A Kalman smoother assumes a motion model that may not fit. Linear interpolation assumes only that the object went roughly from A to B, which is the weakest and safest assumption.
If you need better than linear, the honest improvement is map matching, which brings in external information about where movement is possible.
Why the interval belongs in the metadata
A resampled dataset looks exactly like the original: same columns, same types, same CRS. Nothing in the rows records that its distances are 11% shorter than the source's.
Write the interval into the file's metadata or a sidecar, and into the column name if you can β length_km_at_10s is ugly and unambiguous.
Edge cases or notes
- Downsample by elapsed time, not by taking every nth row.
- Split at gaps before resampling. Resampling across a 50-minute gap manufactures a journey.
- Mark interpolated rows so downstream statistics can exclude them.
limit_area="inside"stops interpolation extrapolating past the last fix.- Harmonise downward, to the coarsest interval present.
- Upsampling does not change measured length. It cannot.
- Avoid splines for positions; they overshoot at turns.
- Record the interval with every distance and speed you report.
Internal links
- Sampling rate and gaps explained β how much detail each interval keeps
- Trajectories explained β why length depends on the ruler
- How to calculate speed, distance and direction along a track β the numbers that shift
- How to clean a GPS track: outliers, duplicates and jumps β clean before resampling
- Stops, trips and segmentation explained β segment before resampling
- How to map-match a GPS track to a street network β the principled alternative to interpolation
- How to animate movement over time in Python β where a regular series is genuinely required
- How to load GPS tracks into Python as trajectories β the input to all of this
FAQ
How do I resample a GPS track to a fixed interval?
Group by track, then keep the first fix and each subsequent fix at least the target interval after the last kept one. Split at long gaps first.
Does resampling change my track distance?
Downsampling does, always downward: measured length fell from 37.86 km to 29.67 km between 3-second and 60-second sampling on real traces. Upsampling does not change it at all.
Can I upsample to recover detail?
No. Interpolation adds points along the straight line already assumed between fixes, so it adds rows without adding path.
Should I use df.resample() or a greedy filter?
resample for a strictly regular index with visible gaps; the greedy filter when you want a smaller set of real observations at a minimum interval.
How do I compare two datasets logged at different rates?
Downsample both to the coarser median interval before computing anything. Never harmonise upward.
What interpolation method should I use?
Linear. Splines overshoot at turns and place positions outside the corridor the object occupied. If you need better, use map matching.
Should I resample before or after cleaning?
After. Cleaning uses the full-rate data to find spikes, and resampling first can drop the very fix that identifies an error.