Detecting stops¶

No description has been provided for this image

Binder IPYNB HTML

There are no definitive answers when it comes to detecting / extracting stops from movement trajectories. Due to tracking inaccuracies, movement speed rarely goes to true zero. GPS tracks, for example, tend to keep moving around the object's stop location.

Suitable stop definitions are also highly application dependent. For example, an application may be interested in analyzing trip purposes. To do so, analysts would be interested in stops that are longer than, for example, 5 minutes and may try to infer the purpose of the stop from the stop location and time. Shorter stops, such as delays at traffic lights, however would not be relevant for this appication.

In the MovingPandas TrajectoryStopDetector implementation, a stop is detected if the movement stays within an area of specified size for at least the specified duration.

In [1]:
import pandas as pd
import geopandas as gpd
import movingpandas as mpd
import shapely as shp
import hvplot.pandas
import matplotlib.pyplot as plt
import folium

from geopandas import GeoDataFrame, read_file
from shapely.geometry import Point, LineString, Polygon
from datetime import datetime, timedelta
from holoviews import opts

import warnings

warnings.filterwarnings("ignore")

plot_defaults = {"linewidth": 5, "capstyle": "round", "figsize": (9, 3), "legend": True}
opts.defaults(
    opts.Overlay(active_tools=["wheel_zoom"], frame_width=500, frame_height=400)
)

mpd.show_versions()
MovingPandas 0.23.0

SYSTEM INFO
-----------
python     : 3.10.19 | packaged by conda-forge | (main, Jan 26 2026, 23:45:08) [GCC 14.3.0]
executable : /home/anita/miniforge3/envs/mpd-ex/bin/python
machine    : Linux-6.8.0-134-generic-x86_64-with-glibc2.39

PROJ INFO
-----------
PROJ       : 9.6.2
PROJ data dir: /home/anita/miniforge3/envs/mpd-ex/share/proj

PYTHON DEPENDENCIES
-------------------
numpy      : 1.23.1
geopandas  : 1.0.1
geopy      : 2.4.1
geoviews   : 1.15.1
holoviews  : 1.22.1
hvplot     : 0.12.2
mapclassify: 2.8.1
matplotlib : 3.10.8
pandas     : 2.3.3
pyproj     : 3.7.1
shapely    : 2.1.2
stonesoup  : 1.8
In [2]:
gdf = read_file("../data/geolife_small.gpkg")
tc = mpd.TrajectoryCollection(gdf, "trajectory_id", t="t")

Stop detection with a single Trajectory¶

In [3]:
my_traj = tc.trajectories[0]
my_traj
Out[3]:

Trajectory 1 (No. rows: 466 | Length: 6.2 km)

Start: 2008-12-11 04:42:14End: 2008-12-11 05:15:46Duration: 0:33:32
Bounds: (116.385602, 39.862378, 116.393553, 39.898723)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (int64), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:42:14 1 1 1 19 POINT (116.3913 39.89857)
2008-12-11 04:42:16 2 2 1 19 POINT (116.39132 39.89862)
2008-12-11 04:43:26 3 3 1 19 POINT (116.39093 39.89861)
2008-12-11 04:43:32 4 4 1 19 POINT (116.39083 39.89864)
2008-12-11 04:43:47 5 5 1 19 POINT (116.38941 39.89872)
In [4]:
traj_plot = my_traj.hvplot(
    title="Trajectory {}".format(my_traj.id),
    line_width=7.0,
    tiles="CartoLight",
    color="slategray",
)
traj_plot
Out[4]:
In [5]:
detector = mpd.TrajectoryStopDetector(my_traj)

Stop duration¶

In [6]:
%%time
stop_time_ranges = detector.get_stop_time_ranges(
    min_duration=timedelta(seconds=60), max_diameter=100
)
CPU times: user 104 ms, sys: 60 μs, total: 104 ms
Wall time: 104 ms
In [7]:
for x in stop_time_ranges:
    print(x)
Traj 1: 2008-12-11 04:42:14 - 2008-12-11 04:43:32 (duration: 0 days 00:01:18)
Traj 1: 2008-12-11 04:43:52 - 2008-12-11 04:47:40 (duration: 0 days 00:03:48)
Traj 1: 2008-12-11 04:50:06 - 2008-12-11 04:51:23 (duration: 0 days 00:01:17)
Traj 1: 2008-12-11 04:54:50 - 2008-12-11 04:55:55 (duration: 0 days 00:01:05)
Traj 1: 2008-12-11 05:02:03 - 2008-12-11 05:06:34 (duration: 0 days 00:04:31)
Traj 1: 2008-12-11 05:07:19 - 2008-12-11 05:08:31 (duration: 0 days 00:01:12)
Traj 1: 2008-12-11 05:11:17 - 2008-12-11 05:13:38 (duration: 0 days 00:02:21)
Traj 1: 2008-12-11 05:13:51 - 2008-12-11 05:15:46 (duration: 0 days 00:01:55)

Stop points¶

In [8]:
%%time
stop_points = detector.get_stop_points(
    min_duration=timedelta(seconds=60), max_diameter=100
)
CPU times: user 118 ms, sys: 965 μs, total: 119 ms
Wall time: 119 ms
In [9]:
stop_points
Out[9]:
geometry start_time end_time traj_id duration_s
stop_id
1_2008-12-11 04:42:14 POINT (116.39112 39.89862) 2008-12-11 04:42:14 2008-12-11 04:43:32 1 78.0
1_2008-12-11 04:43:52 POINT (116.3907 39.89838) 2008-12-11 04:43:52 2008-12-11 04:47:40 1 228.0
1_2008-12-11 04:50:06 POINT (116.38932 39.88923) 2008-12-11 04:50:06 2008-12-11 04:51:23 1 77.0
1_2008-12-11 04:54:50 POINT (116.39232 39.87849) 2008-12-11 04:54:50 2008-12-11 04:55:55 1 65.0
1_2008-12-11 05:02:03 POINT (116.39293 39.86384) 2008-12-11 05:02:03 2008-12-11 05:06:34 1 271.0
1_2008-12-11 05:07:19 POINT (116.39024 39.86383) 2008-12-11 05:07:19 2008-12-11 05:08:31 1 72.0
1_2008-12-11 05:11:17 POINT (116.38596 39.86519) 2008-12-11 05:11:17 2008-12-11 05:13:38 1 141.0
1_2008-12-11 05:13:51 POINT (116.38603 39.86538) 2008-12-11 05:13:51 2008-12-11 05:15:46 1 115.0
In [10]:
stop_point_plot = traj_plot * stop_points.hvplot(
    geo=True, size="duration_s", color="deeppink"
)
stop_point_plot
Out[10]:
In [11]:
stop_points_gdf = gpd.GeoDataFrame(stop_points, geometry="geometry", crs="EPSG:4326")
In [12]:
m = my_traj.explore(
    color="blue",
    style_kwds={"weight": 4},
    name="Trajectory",
)

stop_points_gdf.explore(
    m=m,
    color="red",
    style_kwds={
        "style_function": lambda x: {"radius": x["properties"]["duration_s"] / 10}
    },
    name="Stop points",
)

folium.TileLayer("OpenStreetMap").add_to(m)
folium.LayerControl().add_to(m)

m
Out[12]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Stop segments¶

In [13]:
%%time
stop_segments = detector.get_stop_segments(
    min_duration=timedelta(seconds=60), max_diameter=100
)
CPU times: user 107 ms, sys: 16 μs, total: 107 ms
Wall time: 107 ms
In [14]:
stop_segments
Out[14]:

TrajectoryCollection with 8 trajectories

Trajectory 1_2008-12-11 04:42:14 (No. rows: 4 | Length: 46.7 m)

Start: 2008-12-11 04:42:14End: 2008-12-11 04:43:32Duration: 0:01:18
Bounds: (116.390833, 39.898573, 116.391317, 39.898635)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:42:14 1 1 1_2008-12-11 04:42:14 19 POINT (116.3913 39.89857)
2008-12-11 04:42:16 2 2 1_2008-12-11 04:42:14 19 POINT (116.39132 39.89862)
2008-12-11 04:43:26 3 3 1_2008-12-11 04:42:14 19 POINT (116.39093 39.89861)
2008-12-11 04:43:32 4 4 1_2008-12-11 04:42:14 19 POINT (116.39083 39.89864)

Trajectory 1_2008-12-11 04:43:52 (No. rows: 14 | Length: 132.5 m)

Start: 2008-12-11 04:43:52End: 2008-12-11 04:47:40Duration: 0:03:48
Bounds: (116.390193, 39.897837, 116.390913, 39.898437)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:43:52 7 7 1_2008-12-11 04:43:52 19 POINT (116.39061 39.89784)
2008-12-11 04:45:21 8 8 1_2008-12-11 04:43:52 19 POINT (116.39071 39.8979)
2008-12-11 04:45:25 9 9 1_2008-12-11 04:43:52 19 POINT (116.39072 39.89801)
2008-12-11 04:45:30 10 10 1_2008-12-11 04:43:52 19 POINT (116.39075 39.89811)
2008-12-11 04:45:35 11 11 1_2008-12-11 04:43:52 19 POINT (116.3908 39.89819)

Trajectory 1_2008-12-11 04:50:06 (No. rows: 10 | Length: 101.3 m)

Start: 2008-12-11 04:50:06End: 2008-12-11 04:51:23Duration: 0:01:17
Bounds: (116.388975, 39.8892, 116.389997, 39.889452)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:50:06 98 98 1_2008-12-11 04:50:06 19 POINT (116.38898 39.88945)
2008-12-11 04:51:08 99 99 1_2008-12-11 04:50:06 19 POINT (116.38902 39.88936)
2008-12-11 04:51:11 100 100 1_2008-12-11 04:50:06 19 POINT (116.38909 39.88928)
2008-12-11 04:51:13 101 101 1_2008-12-11 04:50:06 19 POINT (116.38917 39.88924)
2008-12-11 04:51:15 102 102 1_2008-12-11 04:50:06 19 POINT (116.38927 39.88922)

Trajectory 1_2008-12-11 04:54:50 (No. rows: 11 | Length: 119.2 m)

Start: 2008-12-11 04:54:50End: 2008-12-11 04:55:55Duration: 0:01:05
Bounds: (116.391832, 39.878137, 116.392557, 39.878818)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:54:50 211 211 1_2008-12-11 04:54:50 19 POINT (116.39256 39.87882)
2008-12-11 04:54:56 212 212 1_2008-12-11 04:54:50 19 POINT (116.39255 39.87872)
2008-12-11 04:55:03 213 213 1_2008-12-11 04:54:50 19 POINT (116.39254 39.87862)
2008-12-11 04:55:08 214 214 1_2008-12-11 04:54:50 19 POINT (116.39249 39.87853)
2008-12-11 04:55:13 215 215 1_2008-12-11 04:54:50 19 POINT (116.3924 39.87848)

Trajectory 1_2008-12-11 05:02:03 (No. rows: 13 | Length: 117.3 m)

Start: 2008-12-11 05:02:03End: 2008-12-11 05:06:34Duration: 0:04:31
Bounds: (116.392508, 39.8634, 116.393137, 39.864008)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 05:02:03 355 355 1_2008-12-11 05:02:03 19 POINT (116.39314 39.8634)
2008-12-11 05:02:16 356 356 1_2008-12-11 05:02:03 19 POINT (116.39308 39.86348)
2008-12-11 05:02:21 357 357 1_2008-12-11 05:02:03 19 POINT (116.39308 39.86357)
2008-12-11 05:02:32 358 358 1_2008-12-11 05:02:03 19 POINT (116.39307 39.86366)
2008-12-11 05:02:37 359 359 1_2008-12-11 05:02:03 19 POINT (116.39302 39.86374)

... and 3 more trajectories

In [15]:
stop_segment_plot = stop_point_plot * stop_segments.hvplot(
    line_width=7.0, tiles=None, color="orange"
)
stop_segment_plot
Out[15]:
In [16]:
m = my_traj.explore(
    color="blue",
    tooltip="trajectory_id",
    popup=True,
    style_kwds={"weight": 4},
    name="Trajectory",
)

stop_segments.explore(
    m=m,
    color="orange",
    tooltip="trajectory_id",
    popup=True,
    style_kwds={"weight": 4},
    name="Stop segments",
)

stop_points_gdf.explore(
    m=m,
    color="red",
    tooltip="stop_id",
    popup=True,
    marker_kwds={"radius": 3},
    name="Stop points",
)

folium.TileLayer("CartoDB positron").add_to(m)
folium.LayerControl().add_to(m)

m
Out[16]:
Make this Notebook Trusted to load map: File -> Trust Notebook

Split at stops¶

In [17]:
%%time
split = mpd.StopSplitter(my_traj).split(
    min_duration=timedelta(seconds=60), max_diameter=100
)
CPU times: user 109 ms, sys: 5 μs, total: 109 ms
Wall time: 109 ms
In [18]:
split
Out[18]:

TrajectoryCollection with 7 trajectories

Trajectory 1_2008-12-11 04:43:32 (No. rows: 4 | Length: 264.3 m)

Start: 2008-12-11 04:43:32End: 2008-12-11 04:43:52Duration: 0:00:20
Bounds: (116.38941, 39.897837, 116.390833, 39.898723)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:43:32 4 4 1_2008-12-11 04:43:32 19 POINT (116.39083 39.89864)
2008-12-11 04:43:47 5 5 1_2008-12-11 04:43:32 19 POINT (116.38941 39.89872)
2008-12-11 04:43:50 6 6 1_2008-12-11 04:43:32 19 POINT (116.39052 39.89793)
2008-12-11 04:43:52 7 7 1_2008-12-11 04:43:32 19 POINT (116.39061 39.89784)

Trajectory 1_2008-12-11 04:47:40 (No. rows: 79 | Length: 1.1 km)

Start: 2008-12-11 04:47:40End: 2008-12-11 04:50:06Duration: 0:02:26
Bounds: (116.38822, 39.889452, 116.390193, 39.898437)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:47:40 20 20 1_2008-12-11 04:47:40 19 POINT (116.39019 39.89844)
2008-12-11 04:47:42 21 21 1_2008-12-11 04:47:40 19 POINT (116.39013 39.89838)
2008-12-11 04:47:44 22 22 1_2008-12-11 04:47:40 19 POINT (116.39003 39.89834)
2008-12-11 04:47:46 23 23 1_2008-12-11 04:47:40 19 POINT (116.38993 39.89829)
2008-12-11 04:47:48 24 24 1_2008-12-11 04:47:40 19 POINT (116.38984 39.89824)

Trajectory 1_2008-12-11 04:51:23 (No. rows: 105 | Length: 1.4 km)

Start: 2008-12-11 04:51:23End: 2008-12-11 04:54:50Duration: 0:03:27
Bounds: (116.389997, 39.878818, 116.392755, 39.889492)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:51:23 107 107 1_2008-12-11 04:51:23 19 POINT (116.39 39.88924)
2008-12-11 04:51:24 108 108 1_2008-12-11 04:51:23 19 POINT (116.3901 39.88926)
2008-12-11 04:51:26 109 109 1_2008-12-11 04:51:23 19 POINT (116.39027 39.88926)
2008-12-11 04:51:28 110 110 1_2008-12-11 04:51:23 19 POINT (116.39043 39.88927)
2008-12-11 04:51:30 111 111 1_2008-12-11 04:51:23 19 POINT (116.39059 39.88927)

Trajectory 1_2008-12-11 04:55:55 (No. rows: 135 | Length: 1.9 km)

Start: 2008-12-11 04:55:55End: 2008-12-11 05:02:03Duration: 0:06:08
Bounds: (116.391545, 39.862378, 116.393553, 39.878137)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 04:55:55 221 221 1_2008-12-11 04:55:55 19 POINT (116.39183 39.87814)
2008-12-11 04:55:56 222 222 1_2008-12-11 04:55:55 19 POINT (116.39174 39.87804)
2008-12-11 04:55:57 223 223 1_2008-12-11 04:55:55 19 POINT (116.39171 39.8779)
2008-12-11 04:55:58 224 224 1_2008-12-11 04:55:55 19 POINT (116.39166 39.87782)
2008-12-11 04:55:59 225 225 1_2008-12-11 04:55:55 19 POINT (116.39161 39.87773)

Trajectory 1_2008-12-11 05:06:34 (No. rows: 16 | Length: 145.5 m)

Start: 2008-12-11 05:06:34End: 2008-12-11 05:07:19Duration: 0:00:45
Bounds: (116.390843, 39.863797, 116.392508, 39.863978)CRS: EPSG:4326
Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32)
Data preview
id sequence trajectory_id tracker geometry
t
2008-12-11 05:06:34 367 367 1_2008-12-11 05:06:34 19 POINT (116.39251 39.86398)
2008-12-11 05:06:37 368 368 1_2008-12-11 05:06:34 19 POINT (116.39239 39.86398)
2008-12-11 05:06:40 369 369 1_2008-12-11 05:06:34 19 POINT (116.39227 39.86397)
2008-12-11 05:06:43 370 370 1_2008-12-11 05:06:34 19 POINT (116.39216 39.86397)
2008-12-11 05:06:46 371 371 1_2008-12-11 05:06:34 19 POINT (116.39206 39.86397)

... and 2 more trajectories

In [19]:
split.to_traj_gdf()
Out[19]:
trajectory_id start_t end_t geometry length direction
0 1_2008-12-11 04:43:32 2008-12-11 04:43:32 2008-12-11 04:43:52 LINESTRING (116.39083 39.89864, 116.38941 39.8... 264.335588 192.205758
1 1_2008-12-11 04:47:40 2008-12-11 04:47:40 2008-12-11 04:50:06 LINESTRING (116.39019 39.89844, 116.39013 39.8... 1070.704027 185.938120
2 1_2008-12-11 04:51:23 2008-12-11 04:51:23 2008-12-11 04:54:50 LINESTRING (116.39 39.88924, 116.3901 39.88926... 1378.618493 169.324962
3 1_2008-12-11 04:55:55 2008-12-11 04:55:55 2008-12-11 05:02:03 LINESTRING (116.39183 39.87814, 116.39174 39.8... 1909.066931 176.111541
4 1_2008-12-11 05:06:34 2008-12-11 05:06:34 2008-12-11 05:07:19 LINESTRING (116.39251 39.86398, 116.39239 39.8... 145.517971 262.952795
5 1_2008-12-11 05:08:31 2008-12-11 05:08:31 2008-12-11 05:11:17 LINESTRING (116.38974 39.86382, 116.38967 39.8... 439.686682 300.319621
6 1_2008-12-11 05:13:38 2008-12-11 05:13:38 2008-12-11 05:13:51 LINESTRING (116.38587 39.86544, 116.38584 39.8... 16.921893 349.846256
In [20]:
split.explore(
    column="trajectory_id", tiles="CartoDB dark_matter", style_kwds={"weight": 4}
)
Out[20]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [21]:
stop_segment_plot + split.hvplot(
    title="Trajectory {} split at stops".format(my_traj.id),
    line_width=7.0,
    tiles="CartoLight",
)
Out[21]:

Stop Detection for TrajectoryCollections¶

The process is the same as for individual trajectories.

In [22]:
%%time
detector = mpd.TrajectoryStopDetector(tc)
stop_points = detector.get_stop_points(
    min_duration=timedelta(seconds=120), max_diameter=100
)
len(stop_points)
CPU times: user 1.41 s, sys: 9 μs, total: 1.41 s
Wall time: 1.41 s
Out[22]:
31
In [23]:
ax = tc.plot(figsize=(7, 7))
stop_points.plot(ax=ax, color="red")
Out[23]:
<Axes: >
No description has been provided for this image
In [24]:
m = tc.explore(
    column="trajectory_id",
    tooltip="trajectory_id",
    popup=True,
    style_kwds={"weight": 4},
    name="Trajectories",
)

stop_points.explore(
    m=m,
    color="red",
    tooltip="stop_id",
    popup=True,
    marker_kwds={"radius": 5},
    name="Stop points",
)

folium.TileLayer("CartoDB positron").add_to(m)
folium.LayerControl().add_to(m)

m
Out[24]:
Make this Notebook Trusted to load map: File -> Trust Notebook
In [ ]: