Skip to content

Launch Binder

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.

%pip install -q fastmob trackintel

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.

# hyperparameters
minutes = 20
radius_m = 100
gap_minutes = 15

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
tdf["datetime"] = pd.to_datetime(tdf['datetime'], errors="coerce", utc=True)
tdf.info()

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()

png