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
Library Selection by Task Type Timestamped GPS points branch into two workloads: per-trajectory geometry work favors MovingPandas, while population flows and origin-destination modeling favor scikit-mobility. Both meet at a shared GeoPandas exchange layer. Timestamped GPS points Per-trajectory geometry smoothing, segmentation, stops, turns Population flows OD matrices, tessellation, models MovingPandas scikit-mobility bridge via GeoPandas

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:

PYTHON
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:

PYTHON
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. TrajectoryStopDetector interprets max_diameter in the units of the active CRS. Leave the data in EPSG:4326 and a “50” diameter means 50 degrees — effectively no filtering. Always to_crs to 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 stops uses 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 TrajectoryCollection all 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 has FlowDataFrame and tessellation built for exactly that. Fighting the library’s grain wastes effort a bridge would save.

Back to Stay-Point Detection Algorithms