Staypoint detection with GeoLife
A reproducible comparison of staypoint detection with fastmob and GeoPandas-oriented trackintel on the full GeoLife corpus. We use the same trajectory and thresholds for both implementations, then report comparable rather than byte-identical outputs because the detectors differ at some edges.
1. Load a real trajectory dataset
GeoLife contains timestamped GPS fixes for multiple users. The full public corpus contains nearly 25 million position fixes; expect the download and benchmark to take time.
!wget -O geolife.zip "https://download.microsoft.com/download/F/4/8/F4894AA5-FDBC-481E-9285-D5F8C4C4F039/Geolife%20Trajectories%201.3.zip"
!unzip -q geolife.zip -d geolife
2. Import the tools
fastmob accepts ordinary dataframe columns. We normalize timestamps to UTC before running the detector.
import fastmob
import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd
import trackintel as ti
3. Set the detection contract
A staypoint is a stop lasting at least minutes inside a radius_m radius. gap_minutes separates observations when a device stops reporting.
4. Prepare the trajectory
fastmob accepts ordinary dataframe columns. We normalize timestamps to UTC before running the detector.
tdf = fastmob.io.load_geolife_trajectories("/content/geolife/Geolife Trajectories 1.3").to_pandas()
tdf
5. Benchmark fastmob
This timed cell measures the complete staypoint operation. Repeat it after changing the slice or thresholds; the first run may include warm-up costs.
%%timeit
fastmob_result = fastmob.stay_locations(
tdf,
minutes_for_a_stop=minutes,
spatial_radius_km=radius_m / 1000,
no_data_for_minutes=gap_minutes,
leaving_time=True,
)
del fastmob_result # Save system RAM
2.15 s ± 439 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
6. Build the GeoPandas-style baseline
trackintel works with a Positionfixes GeoDataFrame, so we create point geometry from the same latitude and longitude columns.
df = tdf.rename(columns={"uid": "user_id", "datetime": "tracked_at"})
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df["lng"], df["lat"]),
crs="EPSG:4326"
)
pfs = ti.Positionfixes(gdf)
del df, gdf
7. Benchmark the GeoPandas-style baseline
This uses trackintel's sliding-window detector with equivalent distance, time, gap, and trailing-stop settings.
%%timeit
_, trackintel_result = pfs.generate_staypoints(
method='sliding', dist_threshold=radius_m, time_threshold=minutes,
gap_threshold=gap_minutes, include_last=True, n_jobs=-1,
)
del trackintel_result # Save System RAM
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
2min 50s ± 4.65 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
8. Compare the detected stops
The plot below shows one user's staypoints from both methods. Similar locations indicate comparable detection behavior; differences usually come from implementation details or edge handling.
fastmob_result = fastmob.stay_locations(
tdf,
minutes_for_a_stop=minutes,
spatial_radius_km=radius_m / 1000,
no_data_for_minutes=gap_minutes,
leaving_time=True,
)
del tdf
trackintel_result = pfs.generate_staypoints(
method='sliding', dist_threshold=radius_m, time_threshold=minutes,
gap_threshold=gap_minutes, include_last=True, n_jobs=-1,
)
del pfs
/usr/local/lib/python3.13/dist-packages/trackintel/preprocessing/positionfixes.py:112: UserWarning: 1912 duplicates were dropped from your positionfixes. Dropping duplicates is recommended but can be prevented using the 'exclude_duplicate_pfs' flag.
warnings.warn(warn_str)
user_id = "001"
fm_full = fastmob_result
fm = fm_full[fm_full["uid"].astype(str).str.zfill(3) == user_id]
ti_full = trackintel_result[1]
ti_stays = ti_full[ti_full["user_id"].astype(str).str.zfill(3) == user_id]
fig, axes = plt.subplots(1, 2, figsize=(14, 6), sharex=True, sharey=True)
axes[0].scatter(fm["lng"], fm["lat"], s=18, alpha=0.7)
axes[0].set_title(f"Fastmob — user 000 ({len(fm)} stays)")
axes[1].scatter(ti_stays.geometry.x, ti_stays.geometry.y, s=18, alpha=0.7)
axes[1].set_title(f"Trackintel — user 000 ({len(ti_stays)} stays)")
for ax in axes:
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
