MovingPandas vs scikit-mobility: choosing a movement-analytics library
MovingPandas and scikit-mobility solve overlapping problems from opposite ends: MovingPandas is a trajectory-geometry library that models each moving object as a Trajectory on top of GeoPandas, while scikit-mobility is a mobility-science toolkit that stores points in a tabular TrajDataFrame and specializes in flows and origin-destination matrices. They share a stop-detection feature and both read from pandas, which is exactly why teams stall on the choice. The decision is not about which is better — it is about whether your dominant workload is per-track geometry or population-scale aggregation, and this page maps their feature parity so you can pick deliberately.
Why the choice is genuinely hard
The libraries overlap in the middle of the pipeline. Both ingest timestamped points, both can split a track into segments, and both can find where an object stopped — the shared capability that connects this comparison to the broader stay-point detection topic. Where they diverge is the abstraction each one optimizes. MovingPandas treats a trajectory as a first-class geometric object with a LineString, so smoothing, generalization, and turn analysis feel native. scikit-mobility treats mobility as a population phenomenon, so it reaches for matrices, generative models, and privacy risk metrics.
That divergence sits inside the wider movement pattern extraction and trajectory analysis discipline, where the same raw GPS can be modeled either as individual paths or as aggregate demand. Picking the wrong abstraction is not fatal — both interoperate through GeoPandas — but it forces you to fight the library’s grain for every downstream operation.
Feature-parity comparison
The table maps the two libraries across the dimensions that actually drive the decision. “Parity” means both do it well; the notes call out where the ergonomics differ.
| Capability | MovingPandas | scikit-mobility | Verdict |
|---|---|---|---|
| Core data model | Trajectory / TrajectoryCollection wrapping a GeoDataFrame with shapely geometry |
TrajDataFrame — a tabular pandas subclass with lat/lng/datetime/uid columns |
Geometry-first vs table-first |
| Stop / stay detection | TrajectoryStopDetector (max diameter, min duration) → stop segments & points |
stops() (spatial radius, temporal threshold) over a TrajDataFrame |
Parity; different output shape |
| Segmentation | Split by observation gap, stops, speed, or value change | Trajectory splitting by temporal/spatial gap | MovingPandas richer |
| Flows / OD matrices | Not native — export to build externally | FlowDataFrame, OD matrices, Tessellation binning |
scikit-mobility only |
| Generative & privacy models | None | Gravity, radiation, EPR models; privacy risk assessment | scikit-mobility only |
| CRS handling | Full GeoPandas CRS; to_crs() to a metric system |
Primarily lon/lat; haversine internally | MovingPandas stronger |
| Smoothing / generalization | Kalman, Douglas-Peucker, min-distance/-time generalizers | Filtering & compression utilities | MovingPandas richer |
| Visualization | hvplot/GeoViews interactive trajectory maps |
folium flow & tessellation maps |
Parity; different focus |
| Scale model | In-memory, per-object overhead per track | In-memory, vectorized over point tables | scikit-mobility lighter at population scale |
| Ecosystem base | GeoPandas, shapely, GeoViews | pandas, GeoPandas, scikit-learn, folium | Both mature |
The same task in each library
The clearest way to feel the difference is to run identical stop detection in both. A stop is where a moving object stays within a small radius for at least some minimum duration — the exact idea generalized in implementing DBSCAN for stay-point clustering in Python. The snippets below detect the same stops from the same points.
In MovingPandas, you build a Trajectory and run TrajectoryStopDetector. Note the reprojection to a metric CRS before the diameter parameter is interpreted in metres:
import geopandas as gpd
import movingpandas as mpd
import pandas as pd
from shapely.geometry import Point
def detect_stops_movingpandas(
df: pd.DataFrame,
max_diameter_m: float = 50.0,
min_duration_s: int = 120,
) -> gpd.GeoDataFrame:
"""
Detect stops with MovingPandas.
df must contain: t (datetime), lon, lat, traj_id.
Returns a GeoDataFrame of stop points with duration.
"""
if df.empty:
raise ValueError("Input DataFrame is empty.")
required = {"t", "lon", "lat", "traj_id"}
missing = required - set(df.columns)
if missing:
raise ValueError(f"Missing required columns: {missing}")
gdf = gpd.GeoDataFrame(
df.copy(),
geometry=[Point(xy) for xy in zip(df["lon"], df["lat"])],
crs="EPSG:4326",
).set_index("t")
# Reproject to a metric CRS: max_diameter is metres, so degrees are wrong.
# Web Mercator (3857) suffices for a demo; use a local UTM zone in prod.
gdf = gdf.to_crs("EPSG:3857")
tc = mpd.TrajectoryCollection(gdf, traj_id_col="traj_id")
detector = mpd.TrajectoryStopDetector(tc)
stops = detector.get_stop_points(
max_diameter=max_diameter_m,
min_duration=pd.Timedelta(seconds=min_duration_s),
)
return stops
In scikit-mobility, you build a TrajDataFrame and call stops. The library operates in lon/lat and uses haversine internally, so the radius parameter is in kilometres and no explicit projection step appears:
import pandas as pd
import skmob
from skmob.preprocessing import detection
def detect_stops_skmob(
df: pd.DataFrame,
stop_radius_km: float = 0.05,
min_minutes: float = 2.0,
) -> skmob.TrajDataFrame:
"""
Detect stops with scikit-mobility.
df must contain: datetime, lng, lat, uid.
Returns a TrajDataFrame of stops (leaving/arrival times attached).
"""
if df.empty:
raise ValueError("Input DataFrame is empty.")
required = {"datetime", "lng", "lat", "uid"}
missing = required - set(df.columns)
if missing:
raise ValueError(f"Missing required columns: {missing}")
tdf = skmob.TrajDataFrame(
df.copy(), latitude="lat", longitude="lng",
datetime="datetime", user_id="uid",
)
# stops() works in lon/lat; distances are haversine internally.
# Any speed you derive downstream must still use a metric CRS.
stops = detection.stops(
tdf,
stop_radius_factor=0.5,
minutes_for_a_stop=min_minutes,
spatial_radius_km=stop_radius_km,
leaving_time=True,
)
return stops
The results are comparable, but the surrounding code tells the story: MovingPandas made you think about CRS and geometry; scikit-mobility hid the distance math and handed you a table ready for flow aggregation.
Choose-when guidance
Pick MovingPandas when the deliverable is geometric and per-track: cleaning and smoothing individual trajectories, splitting them at gaps or stops, generalizing shapes with Douglas-Peucker, computing turn angles, or feeding a spatially-typed trajectory object design pattern. Its GeoPandas foundation means CRS-correct distance work is one to_crs away, and its TrajectoryCollection maps cleanly onto multi-track fleet data.
Pick scikit-mobility when the deliverable is aggregate and population-scale: origin-destination matrices, tessellation-based binning, gravity or radiation flow models, or privacy risk assessment on a mobility panel. Its tabular TrajDataFrame and vectorized flow tools carry large point populations more lightly than a collection of individual Trajectory objects would.
Use both when the pipeline spans geometry and aggregation — the common production case. Clean, segment, and detect stops in MovingPandas, export the stops to a plain DataFrame, then build the origin-destination matrix in scikit-mobility. GeoPandas is the neutral handoff format, so nothing is lost at the boundary.
Common mistakes and gotchas
-
Comparing stop counts without aligning parameters. MovingPandas takes a maximum diameter in metres and a minimum duration; scikit-mobility takes a spatial radius in kilometres and a minutes threshold. Diameter is roughly twice a radius, and unit mismatches make the two libraries look like they disagree when they are simply configured differently. Convert deliberately before comparing outputs.
-
Detecting stops in longitude and latitude in MovingPandas.
TrajectoryStopDetectorinterpretsmax_diameterin the units of the active CRS. Leave the data inEPSG:4326and a “50” diameter means 50 degrees — effectively no filtering. Alwaysto_crsto a metric CRS (a local UTM zone, or Web Mercator for a rough demo) before detection, consistent with the metric-CRS discipline throughout Spatiotemporal Data Foundations & Structures. -
Assuming scikit-mobility gives you metric speeds for free. Its
stopsuses haversine internally, which is fine for the stop test, but if you go on to derive speed or acceleration from its output you still owe a projection to a metric CRS. The haversine convenience does not extend to kinematic derivatives. -
Holding millions of Trajectory objects in memory. MovingPandas’ per-object model is ergonomic but carries overhead when you materialize a huge
TrajectoryCollectionall at once. Downsample and resample upstream, and process in batches, rather than loading an entire fleet-year of tracks into one collection. -
Rebuilding flows by hand in MovingPandas. If you find yourself writing origin-destination aggregation loops around a
TrajectoryCollection, that is the signal to hand off to scikit-mobility, which hasFlowDataFrameand tessellation built for exactly that. Fighting the library’s grain wastes effort a bridge would save.
Related
- Stay-Point Detection Algorithms — parent reference on the stop-detection family both libraries implement.
- Implementing DBSCAN for Stay-Point Clustering in Python — the algorithmic foundation behind both libraries’ stop detectors.
- Trajectory Object Design Patterns — how to model the trajectory abstraction that MovingPandas exposes natively.
- Movement Pattern Extraction & Trajectory Analysis — the wider analysis context these libraries operate within.
- Spatiotemporal Data Foundations & Structures — CRS and data-modeling groundwork that both libraries depend on.