Splitting trajectories¶
Gaps are quite common in trajectories. For example, GPS tracks may contain gaps if moving objects enter tunnels where GPS reception is lost. In other use cases, moving objects may leave the observation area for longer time before returning and continuing their recorded track.
Depending on the use case, we therefore might want to split trajectories at observation gaps that exceed a certain minimum 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
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")
In [3]:
my_traj = tc.trajectories[1]
print(my_traj)
Trajectory 2 (2009-06-29 07:02:25 to 2009-06-29 11:13:12) | Size: 897 | Length: 38.8 km Bounds: (116.319212, 39.971703, 116.592616, 40.082514) LINESTRING (116.590957 40.071961, 116.590905 40.072007, 116.590879 40.072027, 116.590915 40.072004,
In [4]:
my_traj.plot(column="speed", vmax=20, **plot_defaults)
Out[4]:
<Axes: >
ObservationGapSplitter¶
Split the trajectory where then are no observations for at least two minutes:
In [5]:
split = mpd.ObservationGapSplitter(my_traj).split(gap=timedelta(minutes=2))
split
Out[5]:
TrajectoryCollection with 5 trajectories
Trajectory 2_0 (No. rows: 149 | Length: 2.4 km)
| Start: 2009-06-29 07:02:25 | End: 2009-06-29 07:13:55 | Duration: 0:11:30 |
| Bounds: (116.586342, 40.07196, 116.592616, 40.082514) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 07:02:25 | 1556 | 1090 | 2_0 | 0 | POINT (116.59096 40.07196) |
| 2009-06-29 07:02:30 | 1557 | 1091 | 2_0 | 0 | POINT (116.5909 40.07201) |
| 2009-06-29 07:02:35 | 1558 | 1092 | 2_0 | 0 | POINT (116.59088 40.07203) |
| 2009-06-29 07:02:40 | 1559 | 1093 | 2_0 | 0 | POINT (116.59092 40.072) |
| 2009-06-29 07:02:45 | 1560 | 1094 | 2_0 | 0 | POINT (116.59096 40.07198) |
Trajectory 2_1 (No. rows: 3 | Length: 36.2 m)
| Start: 2009-06-29 07:16:55 | End: 2009-06-29 07:17:05 | Duration: 0:00:10 |
| Bounds: (116.586843, 40.07929, 116.586902, 40.079608) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 07:16:55 | 1705 | 1239 | 2_1 | 0 | POINT (116.5869 40.07961) |
| 2009-06-29 07:17:00 | 1706 | 1240 | 2_1 | 0 | POINT (116.58689 40.07936) |
| 2009-06-29 07:17:05 | 1707 | 1241 | 2_1 | 0 | POINT (116.58684 40.07929) |
Trajectory 2_2 (No. rows: 619 | Length: 33.8 km)
| Start: 2009-06-29 07:29:35 | End: 2009-06-29 08:20:15 | Duration: 0:50:40 |
| Bounds: (116.319212, 39.971703, 116.587085, 40.079854) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 07:29:35 | 1708 | 1242 | 2_2 | 0 | POINT (116.58703 40.07951) |
| 2009-06-29 07:29:40 | 1709 | 1243 | 2_2 | 0 | POINT (116.58704 40.07951) |
| 2009-06-29 07:29:45 | 1710 | 1244 | 2_2 | 0 | POINT (116.58703 40.07952) |
| 2009-06-29 07:29:50 | 1711 | 1245 | 2_2 | 0 | POINT (116.587 40.07953) |
| 2009-06-29 07:29:55 | 1712 | 1246 | 2_2 | 0 | POINT (116.58704 40.07947) |
Trajectory 2_3 (No. rows: 120 | Length: 1.6 km)
| Start: 2009-06-29 10:57:17 | End: 2009-06-29 11:06:52 | Duration: 0:09:35 |
| Bounds: (116.319598, 39.999772, 116.327485, 40.007696) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 10:57:17 | 2327 | 1861 | 2_3 | 0 | POINT (116.3197 40.00751) |
| 2009-06-29 10:57:22 | 2328 | 1862 | 2_3 | 0 | POINT (116.31971 40.00759) |
| 2009-06-29 10:57:27 | 2329 | 1863 | 2_3 | 0 | POINT (116.31964 40.00769) |
| 2009-06-29 10:57:32 | 2330 | 1864 | 2_3 | 0 | POINT (116.3196 40.0077) |
| 2009-06-29 10:57:37 | 2331 | 1865 | 2_3 | 0 | POINT (116.31965 40.00764) |
Trajectory 2_4 (No. rows: 5 | Length: 574.5 m)
| Start: 2009-06-29 11:09:12 | End: 2009-06-29 11:10:07 | Duration: 0:00:55 |
| Bounds: (116.323486, 40.000248, 116.327222, 40.001014) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 11:09:12 | 2447 | 1981 | 2_4 | 0 | POINT (116.32636 40.00025) |
| 2009-06-29 11:09:47 | 2448 | 1982 | 2_4 | 0 | POINT (116.32349 40.00037) |
| 2009-06-29 11:09:57 | 2449 | 1983 | 2_4 | 0 | POINT (116.32513 40.00057) |
| 2009-06-29 11:10:02 | 2450 | 1984 | 2_4 | 0 | POINT (116.32688 40.00087) |
| 2009-06-29 11:10:07 | 2451 | 1985 | 2_4 | 0 | POINT (116.32722 40.00101) |
In [6]:
split.to_traj_gdf()
Out[6]:
| trajectory_id | start_t | end_t | geometry | length | direction | |
|---|---|---|---|---|---|---|
| 0 | 2_0 | 2009-06-29 07:02:25 | 2009-06-29 07:13:55 | LINESTRING (116.59096 40.07196, 116.5909 40.07... | 2367.642888 | 345.820618 |
| 1 | 2_1 | 2009-06-29 07:16:55 | 2009-06-29 07:17:05 | LINESTRING (116.5869 40.07961, 116.58689 40.07... | 36.188090 | 188.079866 |
| 2 | 2_2 | 2009-06-29 07:29:35 | 2009-06-29 08:20:15 | LINESTRING (116.58703 40.07951, 116.58704 40.0... | 33766.853732 | 250.896081 |
| 3 | 2_3 | 2009-06-29 10:57:17 | 2009-06-29 11:06:52 | LINESTRING (116.3197 40.00751, 116.31971 40.00... | 1613.488553 | 140.473858 |
| 4 | 2_4 | 2009-06-29 11:09:12 | 2009-06-29 11:10:07 | LINESTRING (116.32636 40.00025, 116.32349 40.0... | 574.481893 | 40.696636 |
In [7]:
fig, axes = plt.subplots(nrows=1, ncols=len(split), figsize=(19, 4))
for i, traj in enumerate(split):
traj.plot(ax=axes[i], linewidth=5.0, capstyle="round", column="speed", vmax=20)
StopSplitter¶
Split the trajectory where observations stay within 30 meters for at least 1 minute. Discard created trajectories that are shorter than 500 meters long:
In [8]:
split = mpd.StopSplitter(my_traj).split(
max_diameter=30, min_duration=timedelta(minutes=1), min_length=500
)
split
Out[8]:
TrajectoryCollection with 4 trajectories
Trajectory 2_2009-06-29 07:06:55 (No. rows: 75 | Length: 1.9 km)
| Start: 2009-06-29 07:06:55 | End: 2009-06-29 07:12:55 | Duration: 0:06:00 |
| Bounds: (116.586342, 40.074197, 116.592583, 40.082514) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 07:06:55 | 1617 | 1151 | 2_2009-06-29 07:06:55 | 0 | POINT (116.59258 40.0742) |
| 2009-06-29 07:07:00 | 1618 | 1152 | 2_2009-06-29 07:06:55 | 0 | POINT (116.59254 40.0743) |
| 2009-06-29 07:07:05 | 1619 | 1153 | 2_2009-06-29 07:06:55 | 0 | POINT (116.59249 40.07455) |
| 2009-06-29 07:07:10 | 1620 | 1154 | 2_2009-06-29 07:06:55 | 0 | POINT (116.59246 40.07488) |
| 2009-06-29 07:07:15 | 1621 | 1155 | 2_2009-06-29 07:06:55 | 0 | POINT (116.59244 40.07526) |
Trajectory 2_2009-06-29 07:29:40 (No. rows: 400 | Length: 29.8 km)
| Start: 2009-06-29 07:29:40 | End: 2009-06-29 08:02:30 | Duration: 0:32:50 |
| Bounds: (116.328591, 39.971703, 116.587085, 40.079854) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 07:29:40 | 1709 | 1243 | 2_2009-06-29 07:29:40 | 0 | POINT (116.58704 40.07951) |
| 2009-06-29 07:29:45 | 1710 | 1244 | 2_2009-06-29 07:29:40 | 0 | POINT (116.58703 40.07952) |
| 2009-06-29 07:29:50 | 1711 | 1245 | 2_2009-06-29 07:29:40 | 0 | POINT (116.587 40.07953) |
| 2009-06-29 07:29:55 | 1712 | 1246 | 2_2009-06-29 07:29:40 | 0 | POINT (116.58704 40.07947) |
| 2009-06-29 07:30:00 | 1713 | 1247 | 2_2009-06-29 07:29:40 | 0 | POINT (116.58704 40.07945) |
Trajectory 2_2009-06-29 08:06:55 (No. rows: 246 | Length: 5.0 km)
| Start: 2009-06-29 08:06:55 | End: 2009-06-29 11:04:22 | Duration: 2:57:27 |
| Bounds: (116.319212, 39.984768, 116.327176, 40.008284) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 08:06:55 | 2167 | 1701 | 2_2009-06-29 08:06:55 | 0 | POINT (116.32311 39.98477) |
| 2009-06-29 08:07:00 | 2168 | 1702 | 2_2009-06-29 08:06:55 | 0 | POINT (116.32328 39.98481) |
| 2009-06-29 08:07:05 | 2169 | 1703 | 2_2009-06-29 08:06:55 | 0 | POINT (116.32345 39.98481) |
| 2009-06-29 08:07:10 | 2170 | 1704 | 2_2009-06-29 08:06:55 | 0 | POINT (116.32362 39.98481) |
| 2009-06-29 08:07:15 | 2171 | 1705 | 2_2009-06-29 08:06:55 | 0 | POINT (116.32381 39.9848) |
Trajectory 2_2009-06-29 11:06:17 (No. rows: 14 | Length: 777.9 m)
| Start: 2009-06-29 11:06:17 | End: 2009-06-29 11:13:12 | Duration: 0:06:55 |
| Bounds: (116.323486, 39.99997, 116.327485, 40.001014) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | |
|---|---|---|---|---|---|
| t | |||||
| 2009-06-29 11:06:17 | 2439 | 1973 | 2_2009-06-29 11:06:17 | 0 | POINT (116.32744 39.99997) |
| 2009-06-29 11:06:22 | 2440 | 1974 | 2_2009-06-29 11:06:17 | 0 | POINT (116.32746 40.00004) |
| 2009-06-29 11:06:27 | 2441 | 1975 | 2_2009-06-29 11:06:17 | 0 | POINT (116.32748 40.00009) |
| 2009-06-29 11:06:32 | 2442 | 1976 | 2_2009-06-29 11:06:17 | 0 | POINT (116.3274 40.0002) |
| 2009-06-29 11:06:37 | 2443 | 1977 | 2_2009-06-29 11:06:17 | 0 | POINT (116.32727 40.00035) |
In [9]:
split.to_traj_gdf()
Out[9]:
| trajectory_id | start_t | end_t | geometry | length | direction | |
|---|---|---|---|---|---|---|
| 0 | 2_2009-06-29 07:06:55 | 2009-06-29 07:06:55 | 2009-06-29 07:12:55 | LINESTRING (116.59258 40.0742, 116.59254 40.07... | 1862.857529 | 332.413430 |
| 1 | 2_2009-06-29 07:29:40 | 2009-06-29 07:29:40 | 2009-06-29 08:02:30 | LINESTRING (116.58704 40.07951, 116.58703 40.0... | 29770.503003 | 244.569703 |
| 2 | 2_2009-06-29 08:06:55 | 2009-06-29 08:06:55 | 2009-06-29 11:04:22 | LINESTRING (116.32311 39.98477, 116.32328 39.9... | 5026.886838 | 11.661788 |
| 3 | 2_2009-06-29 11:06:17 | 2009-06-29 11:06:17 | 2009-06-29 11:13:12 | LINESTRING (116.32744 39.99997, 116.32746 40.0... | 777.883748 | 1.430923 |
In [10]:
fig, axes = plt.subplots(nrows=1, ncols=len(split), figsize=(19, 4))
for i, traj in enumerate(split):
traj.plot(ax=axes[i], linewidth=5.0, capstyle="round", column="speed", vmax=20)
SpeedSplitter¶
Split the trajectory where the speed is below one meters per second for at least five minutes:
In [11]:
split = mpd.SpeedSplitter(my_traj).split(speed=1, duration=timedelta(minutes=5))
split
Out[11]:
TrajectoryCollection with 3 trajectories
Trajectory 2_0 (No. rows: 85 | Length: 2.5 km)
| Start: 2009-06-29 07:02:25 | End: 2009-06-29 07:17:05 | Duration: 0:14:40 |
| Bounds: (116.586342, 40.07196, 116.592604, 40.082514) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32), speed (float64) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | speed | |
|---|---|---|---|---|---|---|
| t | ||||||
| 2009-06-29 07:02:25 | 1556 | 1090 | 2_0 | 0 | POINT (116.59096 40.07196) | 1.352991 |
| 2009-06-29 07:02:30 | 1557 | 1091 | 2_0 | 0 | POINT (116.5909 40.07201) | 1.352991 |
| 2009-06-29 07:02:50 | 1561 | 1095 | 2_0 | 0 | POINT (116.59101 40.07196) | 1.008940 |
| 2009-06-29 07:03:20 | 1568 | 1102 | 2_0 | 0 | POINT (116.59087 40.0721) | 2.927362 |
| 2009-06-29 07:03:25 | 1569 | 1103 | 2_0 | 0 | POINT (116.59069 40.07225) | 4.457305 |
Trajectory 2_1 (No. rows: 536 | Length: 33.7 km)
| Start: 2009-06-29 07:29:55 | End: 2009-06-29 08:20:15 | Duration: 0:50:20 |
| Bounds: (116.319212, 39.971703, 116.587085, 40.079854) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32), speed (float64) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | speed | |
|---|---|---|---|---|---|---|
| t | ||||||
| 2009-06-29 07:29:55 | 1712 | 1246 | 2_1 | 0 | POINT (116.58704 40.07947) | 1.443824 |
| 2009-06-29 07:30:20 | 1718 | 1252 | 2_1 | 0 | POINT (116.58705 40.0795) | 1.334180 |
| 2009-06-29 07:30:25 | 1719 | 1253 | 2_1 | 0 | POINT (116.58707 40.07957) | 1.646357 |
| 2009-06-29 07:30:30 | 1720 | 1254 | 2_1 | 0 | POINT (116.58708 40.07964) | 1.521706 |
| 2009-06-29 07:30:35 | 1721 | 1255 | 2_1 | 0 | POINT (116.58704 40.07976) | 2.782868 |
Trajectory 2_2 (No. rows: 98 | Length: 2.2 km)
| Start: 2009-06-29 10:57:22 | End: 2009-06-29 11:10:07 | Duration: 0:12:45 |
| Bounds: (116.319636, 39.999772, 116.327485, 40.007687) | CRS: EPSG:4326 | |
| Columns: id (int64), sequence (int64), trajectory_id (object), tracker (int32), speed (float64) | ||
Data preview
| id | sequence | trajectory_id | tracker | geometry | speed | |
|---|---|---|---|---|---|---|
| t | ||||||
| 2009-06-29 10:57:22 | 2328 | 1862 | 2_2 | 0 | POINT (116.31971 40.00759) | 1.628377 |
| 2009-06-29 10:57:27 | 2329 | 1863 | 2_2 | 0 | POINT (116.31964 40.00769) | 2.541564 |
| 2009-06-29 10:57:37 | 2331 | 1865 | 2_2 | 0 | POINT (116.31965 40.00764) | 1.436148 |
| 2009-06-29 10:57:47 | 2333 | 1867 | 2_2 | 0 | POINT (116.31968 40.00756) | 1.222813 |
| 2009-06-29 10:57:52 | 2334 | 1868 | 2_2 | 0 | POINT (116.3199 40.00748) | 4.295232 |
In [12]:
split.to_traj_gdf()
Out[12]:
| trajectory_id | start_t | end_t | geometry | length | direction | |
|---|---|---|---|---|---|---|
| 0 | 2_0 | 2009-06-29 07:02:25 | 2009-06-29 07:17:05 | LINESTRING (116.59096 40.07196, 116.5909 40.07... | 2521.655180 | 336.756427 |
| 1 | 2_1 | 2009-06-29 07:29:55 | 2009-06-29 08:20:15 | LINESTRING (116.58704 40.07947, 116.58705 40.0... | 33662.308829 | 250.906244 |
| 2 | 2_2 | 2009-06-29 10:57:22 | 2009-06-29 11:10:07 | LINESTRING (116.31971 40.00759, 116.31964 40.0... | 2231.112048 | 138.778458 |
In [13]:
fig, axes = plt.subplots(nrows=1, ncols=len(split), figsize=(19, 4))
for i, traj in enumerate(split):
traj.plot(ax=axes[i], linewidth=5.0, capstyle="round", column="speed", vmax=20)
In [ ]: