Skip to content

Preprocessing

Clean, segment, thin and summarise trajectories, detect stops and places, and convert positions to H3 cells or origin-destination flows. Every function accepts any eager dataframe and returns the same backend. For a worked example on real GPS data, see Clean noisy GPS data and find stops; for the ideas behind the choices, see Cleaning and segmentation.

Clean and reduce

API Description Example
filter Filter trajectory noise by removing outlier points using a named algorithm. Remove implausible jumps
compress Compress trajectory by collapsing nearby points into single representative points. Reduce the number of points
simplify Simplify a trajectory by dropping points using a named algorithm. Reduce the number of points
segment Partition a trajectory into segments using a named algorithm. Split each trace at recording gaps

Stops and places

API Description Example
stay_locations Detect stay locations (stops) in trajectory data. Detect stops
cluster Cluster stop locations with H3 cells and connected components. Group stops into locations
identify_locations Label each user's recurring Locations as "home", "work", or "other". Functional forms
create_activity_flag Flag each staypoint as a genuine "activity" (vs. a transient stop) by dwell time. Functional forms

Trips and travel modes

API Description Example
trajectory_to_trips Build a fastmob.Trips row for each consecutive ping pair. Common part of commuters and links
trajectory_to_od Build a long-format Origin-Destination matrix directly from a raw trajectory. From check-ins to flows
predict_transport_mode Classify each tripleg's transport mode from its average speed. Functional forms
calculate_modal_split Aggregate a mode-labeled Triplegs table into a modal-split table. Functional forms

H3 cells

API Description Example
latlng_to_h3 Convert each row's (lat, lng) to an H3 cell index, Rust-accelerated. Homes and visits per place
h3_to_latlng Convert each H3 cell to its geographic center coordinates. Common part of commuters and links

Phone records

API Description Example
cdr_to_trips_df Derive a CDR trips dataframe from cdr_to_visitation_df output. Mobile-phone records
cdr_to_visitation_df Convert raw CDR records into a NetMob-like visitation dataframe. Mobile-phone records

filter(traj, max_speed_kmh=500.0, include_loops=False, speed_kmh=5.0, max_loop=6, ratio_max=0.25, *, datetime_col=None, lat_col=None, lng_col=None, uid_col=None, is_sorted=False, method='speed', **method_kwargs)

Filter trajectory noise by removing outlier points using a named algorithm.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
max_speed_kmh float

Parameters for the default method="speed" algorithm; unused by every other method. See the method="speed" description below.

500.0
include_loops bool

Parameters for the default method="speed" algorithm; unused by every other method. See the method="speed" description below.

False
speed_kmh float

Parameters for the default method="speed" algorithm; unused by every other method. See the method="speed" description below.

5.0
max_loop int

Parameters for the default method="speed" algorithm; unused by every other method. See the method="speed" description below.

6
ratio_max float

Parameters for the default method="speed" algorithm; unused by every other method. See the method="speed" description below.

0.25
datetime_col str | None

Explicit column name overrides; auto-detected when None.

None
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
uid_col str | None

Explicit column name overrides; auto-detected when None.

None
is_sorted

Whether the trajectory is already sorted by user and time, with nulls and NaNs removed. Setting this to True can speed up processing but may lead to incorrect results if the data is not properly preprocessed.

False
method str

Name of the outlier-detection algorithm to run. One of "speed" (default, backward-compatible with every prior filter() call that never passed method), "hampel", "greedy", "smart_greedy", or "zheng".

'speed'
**method_kwargs Any

Method-specific parameters for method != "speed", forwarded to the matching FILTER_METHODS[method] preparer.

  • speed: uses max_speed_kmh, include_loops, speed_kmh, max_loop, ratio_max above (unchanged from every prior release).
  • hampel: window_size (default 5) — centered rolling window length, in points, over the per-user consecutive-point speed (km/h) series; n_sigma (default 3.0) — outlier threshold as a multiple of 1.4826 * MAD(window).
  • greedy: max_speed_kmh (default 100.0) — maximum physically-consistent speed, in km/h, between each point and the immediately preceding point.
  • smart_greedy: max_speed_kmh (default 100.0) — same consistency threshold as greedy, but keeps the single longest run of mutually consistent points rather than testing fixed adjacent pairs.
  • zheng: max_speed_kmh (default 100.0) — consistency threshold as above; min_seg_size (default 1) — minimum consecutive-consistent-run length required to keep a segment (shorter runs are dropped in full).
{}

Returns:

Type Description
DataFrame

Filtered trajectory in the same backend as input.

Examples:

>>> import pandas as pd
>>> import fastmob
>>> url = fastmob.data.BRIGHTKITE_SAMPLE
>>> df = pd.read_csv(
...     url,
...     sep="\t",
...     header=0,
...     nrows=5000,
...     names=["uid", "datetime", "lat", "lng", "location id"],
... )
>>> df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
>>> df["location_id"] = df["location id"].astype("string")
>>> df = df.dropna(subset=["uid", "datetime", "lat", "lng"])[
...     ["uid", "datetime", "lat", "lng", "location_id"]
... ]
>>> print(df.head().to_string(index=False))
 uid                  datetime       lat         lng                              location_id
   0 2010-10-16 06:02:04+00:00 39.891383 -105.070814         7a0f88982aa015062b95e3b4843f9ca2
   0 2010-10-16 03:48:54+00:00 39.891077 -105.068532         dd7cd3d264c2d063832db506fba8bf79
   0 2010-10-14 18:25:51+00:00 39.750469 -104.999073 9848afcc62e500a01cf6fbf24b797732f8963683
   0 2010-10-14 00:21:47+00:00 39.752713 -104.996337         2ef143e12038c870038df53e0478cefc
   0 2010-10-13 23:31:51+00:00 39.752508 -104.996637         424eb3dd143292f9e013efa00486c907
>>> from fastmob.preprocessing import filter
>>> filtered = filter(df, max_speed_kmh=500.0)
>>> print(len(filtered))
4898
>>> print(filtered.head().to_string(index=False))
 uid                  datetime       lat         lng                      location_id
   0 2009-05-25 20:56:10+00:00 37.774929 -122.419415 ee81ef22a22411ddb5e97f082c799f59
   0 2009-05-25 21:35:28+00:00 37.600747 -122.382376 248b82709e6c11ddbf68003048c0801e
   0 2009-05-25 21:37:44+00:00 37.600747 -122.382376 248b82709e6c11ddbf68003048c0801e
   0 2009-05-25 21:42:47+00:00 37.600747 -122.382376 248b82709e6c11ddbf68003048c0801e
   0 2009-05-25 22:13:23+00:00 37.615223 -122.389979 be2f1e669cc111dd9a50003048c0801e
References

Worked example: Remove implausible jumps


compress(traj, spatial_radius_km=0.2, *, datetime_col=None, lat_col=None, lng_col=None, uid_col=None)

Compress trajectory by collapsing nearby points into single representative points.

All points within spatial_radius_km from an initial point are collapsed into one point with median lat/lng and the initial point's timestamp.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
spatial_radius_km float

Minimum distance (km) between consecutive output points.

0.2
datetime_col str | None

Explicit column name overrides; auto-detected when None.

None
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
uid_col str | None

Explicit column name overrides; auto-detected when None.

None

Returns:

Type Description
DataFrame

Compressed trajectory in the same backend as input.

Examples:

>>> import pandas as pd
>>> import fastmob
>>> url = fastmob.data.BRIGHTKITE_SAMPLE
>>> df = pd.read_csv(
...     url,
...     sep="\t",
...     header=0,
...     nrows=5000,
...     names=["uid", "datetime", "lat", "lng", "location id"],
... )
>>> df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
>>> df["location_id"] = df["location id"].astype("string")
>>> df = df.dropna(subset=["uid", "datetime", "lat", "lng"])[
...     ["uid", "datetime", "lat", "lng", "location_id"]
... ]
>>> print(df.head().to_string(index=False))
 uid                  datetime       lat         lng                              location_id
   0 2010-10-16 06:02:04+00:00 39.891383 -105.070814         7a0f88982aa015062b95e3b4843f9ca2
   0 2010-10-16 03:48:54+00:00 39.891077 -105.068532         dd7cd3d264c2d063832db506fba8bf79
   0 2010-10-14 18:25:51+00:00 39.750469 -104.999073 9848afcc62e500a01cf6fbf24b797732f8963683
   0 2010-10-14 00:21:47+00:00 39.752713 -104.996337         2ef143e12038c870038df53e0478cefc
   0 2010-10-13 23:31:51+00:00 39.752508 -104.996637         424eb3dd143292f9e013efa00486c907
>>> from fastmob.preprocessing import compress
>>> compressed = compress(df, spatial_radius_km=0.2)
>>> print(len(compressed))
3173
>>> print(compressed.head().to_string(index=False))
 uid                  datetime       lat         lng                      location_id
   0 2009-05-25 20:56:10+00:00 37.774929 -122.419415 ee81ef22a22411ddb5e97f082c799f59
   0 2009-05-25 21:35:28+00:00 37.600747 -122.382376 248b82709e6c11ddbf68003048c0801e
   0 2009-05-25 22:13:23+00:00 37.615223 -122.389979 be2f1e669cc111dd9a50003048c0801e
   0 2009-05-26 02:21:12+00:00 39.878664 -104.682105 e12721ce84e911dd8019003048c0801e
   0 2009-05-26 04:59:44+00:00 39.739154 -104.984703 ee8b1d0ea22411ddb074dbd65f1665cf
References

Worked example: Reduce the number of points


simplify(traj, method='douglas_peucker', *, datetime_col=None, lat_col=None, lng_col=None, uid_col=None, is_sorted=False, **method_kwargs)

Simplify a trajectory by dropping points using a named algorithm.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
method str

Name of the simplification algorithm to run. One of "douglas_peucker", "top_down_time_ratio", "min_distance", "min_time_delta", "max_distance", "chan_chin", or "imai_iri".

'douglas_peucker'
datetime_col str | None

Explicit column name overrides; auto-detected when None.

None
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
uid_col str | None

Explicit column name overrides; auto-detected when None.

None
is_sorted bool

Whether the trajectory is already sorted by user and time. Setting this to True can speed up processing but may lead to incorrect results if the data is not properly preprocessed.

False
**method_kwargs Any

Method-specific parameters, forwarded to the matching SIMPLIFY_METHODS[method] preparer. See each algorithm below for its accepted keyword(s) and default.

  • douglas_peucker: epsilon_km (default 0.001) — maximum perpendicular distance, in km, a dropped point may deviate from the simplified line.
  • top_down_time_ratio: epsilon_km (default 0.001) — maximum spatiotemporal distance, in km, between a point and its time-interpolated position on the simplified line.
  • min_distance: min_distance_km (default 0.01) — minimum distance, in km, required between consecutive kept points.
  • min_time_delta: min_time_delta_s (default 60.0) — minimum time, in seconds, required between consecutive kept points.
  • max_distance: epsilon_km (default 0.001) — maximum perpendicular distance, in km, allowed in this single-pass streaming approximation of Douglas-Peucker.
  • chan_chin: epsilon_km (default 0.001) — maximum perpendicular distance, in km, allowed between an original point and its nearest retained segment.
  • imai_iri: epsilon_km (default 0.001) — same tolerance as chan_chin, but searching for the true minimum number of retained segments.
{}

Returns:

Type Description
DataFrame

Simplified trajectory (row subset) in the same backend as input.

Examples:

>>> import pandas as pd
>>> import fastmob
>>> url = fastmob.data.BRIGHTKITE_SAMPLE
>>> df = pd.read_csv(
...     url,
...     sep="\t",
...     header=0,
...     nrows=5000,
...     names=["uid", "datetime", "lat", "lng", "location id"],
... )
>>> df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
>>> df = df.dropna(subset=["uid", "datetime", "lat", "lng"])[
...     ["uid", "datetime", "lat", "lng"]
... ]
>>> from fastmob.preprocessing import simplify
>>> simplified = simplify(df, method="douglas_peucker", epsilon_km=0.1)
>>> len(simplified) <= len(df)
True
References
  • [DP1973] Douglas, D., & Peucker, T. (1973). Algorithms for the reduction of the number of points required to represent a digitized line or its caricature. The Canadian Cartographer 10(2), 112-122.
  • [MdB2004] Meratnia, N., & de By, R.A. (2004). Spatiotemporal compression techniques for moving point objects. EDBT 2004.
  • [CC1996] Chan, W. S., & Chin, F. (1996). Approximation of polygonal curves with minimum number of line segments or minimum error. International Journal of Computational Geometry & Applications, 6(1).
  • [II1988] Imai, H., & Iri, M. (1988). Polygonal approximations of a curve — formulations and algorithms. Computational Morphology.

Worked example: Reduce the number of points


segment(traj, method='value_change', *, datetime_col=None, lat_col=None, lng_col=None, uid_col=None, is_sorted=False, **method_kwargs)

Partition a trajectory into segments using a named algorithm.

Unlike simplify/filter (which drop rows), segment never changes row count: it adds an integer segment_id column aligned 1:1 with the input, restarting at 0 for each user. Every shipped method is ported from MovingPandas' trajectory_splitter.py module, adapted to fastmob's hard row-preservation contract — where MovingPandas' splitters drop short/parked/gap rows entirely (e.g. a min_length cutoff, or every row strictly inside a detected stop), fastmob folds those rows forward into a neighboring segment (or, for stop, gives them their own bracketed segment) instead of dropping them. This is a deliberate, documented semantic difference, not a bug — see each method's Rust kernel docstring for the exact adaptation.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
method str

Name of the segmentation algorithm to run. One of "angle_change", "observation_gap", "speed", "stop", "value_change" (default), or "temporal".

'value_change'
datetime_col str | None

Explicit column name overrides; auto-detected when None.

None
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
uid_col str | None

Explicit column name overrides; auto-detected when None.

None
is_sorted bool

Whether the trajectory is already sorted by user and time. Setting this to True can speed up processing but may lead to incorrect results if the data is not properly preprocessed.

False
**method_kwargs Any

Method-specific parameters, forwarded to the matching SEGMENT_METHODS[method] preparer.

  • angle_change: min_angle (default 45.0) — minimum bearing change, in degrees, between the running reference heading and a point's incoming heading required to start a new segment; min_speed_kmh (default 0.0) — minimum incoming speed, in km/h, for a point to be evaluated at all.
  • observation_gap: gap_s (default 3600.0) — time gap, in seconds, above which a new segment starts.
  • speed: speed_kmh (default 0.0) and max_speed_kmh (default inf) — a point is "moving" when its incoming speed, in km/h, falls in [speed_kmh, max_speed_kmh]; duration_s (default 300.0) — minimum duration, in seconds, a contiguous non-moving run must span to bracket a new segment (shorter non-moving runs are folded into the surrounding segment).
  • stop: stop_radius_km (default 0.2), minutes_for_a_stop (default 20.0), no_data_for_minutes (default 1e12), min_speed_kmh (default None, disabling end-trimming) — same stop-detection parameters as stay_locations; every row inside a detected stop's window becomes its own segment, bracketed by the moving segments before and after it.
  • value_change: col_name (required) — name of the column whose consecutive-value changes start a new segment.
  • temporal: mode (default "day") — one of "hour", "day", "month", "year"; truncates the datetime column to that bucket length and starts a new segment on every bucket change (delegates to the same kernel as value_change).
{}

Returns:

Type Description
DataFrame

Input trajectory with an added segment_id column, in the same backend as input.

Examples:

>>> import pandas as pd
>>> import fastmob
>>> url = fastmob.data.BRIGHTKITE_SAMPLE
>>> df = pd.read_csv(
...     url,
...     sep="\t",
...     header=0,
...     nrows=5000,
...     names=["uid", "datetime", "lat", "lng", "location id"],
... )
>>> df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
>>> df = df.dropna(subset=["uid", "datetime", "lat", "lng"])[
...     ["uid", "datetime", "lat", "lng"]
... ]
>>> from fastmob.preprocessing import segment
>>> segmented = segment(df, method="observation_gap", gap_s=3600.0)
>>> len(segmented) == len(df)
True
>>> "segment_id" in segmented.columns
True
References

Worked example: Split each trace at recording gaps


stay_locations(traj, minutes_for_a_stop=20.0, spatial_radius_km=0.2, leaving_time=True, no_data_for_minutes=1000000000000.0, min_speed_kmh=None, include_last=True, *, datetime_col=None, lat_col=None, lng_col=None, uid_col=None)

Detect stay locations (stops) in trajectory data.

A stop is detected when the individual stays within spatial_radius_km for at least minutes_for_a_stop minutes. Stop coordinates are median lat/lng.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
minutes_for_a_stop float

Minimum duration (minutes) to qualify as a stop.

20.0
spatial_radius_km float

Radius (km) within which points are grouped into a stop.

0.2
leaving_time bool

If True, add a 'leaving_datetime' column with the departure time.

True
no_data_for_minutes float

Gap threshold (minutes) above which data is treated as missing.

1000000000000.0
min_speed_kmh float | None

If set, trim trailing high-speed points from the end of each stop.

None
include_last bool

Whether to close and emit the still-open stay at the end of a user's track, i.e. one the user hadn't "stepped out of" yet when tracking ended. Matches trackintel's generate_staypoints(include_last=...), except trackintel defaults this to False.

True
datetime_col str | None

Explicit column name overrides; auto-detected when None.

None
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
uid_col str | None

Explicit column name overrides; auto-detected when None.

None

Returns:

Type Description
DataFrame

Stop locations in the same backend as input. Schema: [uid_col, lat_col, lng_col, datetime_col, (leaving_datetime)]

Examples:

>>> import pandas as pd
>>> import fastmob
>>> url = fastmob.data.BRIGHTKITE_SAMPLE
>>> df = pd.read_csv(
...     url,
...     sep="\t",
...     header=0,
...     nrows=5000,
...     names=["uid", "datetime", "lat", "lng", "location id"],
... )
>>> df["datetime"] = pd.to_datetime(df["datetime"], errors="coerce")
>>> df["location_id"] = df["location id"].astype("string")
>>> df = df.dropna(subset=["uid", "datetime", "lat", "lng"])[
...     ["uid", "datetime", "lat", "lng", "location_id"]
... ]
>>> print(df.head().to_string(index=False))
 uid                  datetime       lat         lng                              location_id
   0 2010-10-16 06:02:04+00:00 39.891383 -105.070814         7a0f88982aa015062b95e3b4843f9ca2
   0 2010-10-16 03:48:54+00:00 39.891077 -105.068532         dd7cd3d264c2d063832db506fba8bf79
   0 2010-10-14 18:25:51+00:00 39.750469 -104.999073 9848afcc62e500a01cf6fbf24b797732f8963683
   0 2010-10-14 00:21:47+00:00 39.752713 -104.996337         2ef143e12038c870038df53e0478cefc
   0 2010-10-13 23:31:51+00:00 39.752508 -104.996637         424eb3dd143292f9e013efa00486c907
>>> from fastmob.preprocessing import stay_locations
>>> stops = stay_locations(df, spatial_radius_km=0.2, minutes_for_a_stop=20.0)
>>> print(len(stops))
3029
>>> print(stops.head().to_string(index=False))
      lat         lng            datetime  uid    leaving_datetime
37.774929 -122.419415 2009-05-25 20:56:10    0 2009-05-25 21:35:28
37.600747 -122.382376 2009-05-25 21:35:28    0 2009-05-25 22:13:23
37.615223 -122.389979 2009-05-25 22:13:23    0 2009-05-26 02:21:12
39.878664 -104.682105 2009-05-26 02:21:12    0 2009-05-26 04:59:44
39.739154 -104.984703 2009-05-26 04:59:44    0 2009-05-26 16:43:59
References

Worked example: Detect stops


cluster(traj, cluster_radius_km=0.1, min_samples=1, *, h3_resolution=None, datetime_col=None, lat_col=None, lng_col=None, uid_col=None, n_jobs=None)

Cluster stop locations with H3 cells and connected components.

Points are assigned to an H3 cell. Cells containing fewer than min_samples points are noise; active cells touching at an edge are joined into a cluster. cluster_radius_km selects a calibrated grid resolution for compatibility with the previous DBSCAN API. Pass h3_resolution (0--15) to control the grid explicitly; it takes precedence over cluster_radius_km. n_jobs is accepted for API compatibility and has no effect.

Worked example: Group stops into locations


identify_locations(locations, staypoints, method='freq', night_start_hour=22, night_end_hour=6, work_start_hour=8, work_end_hour=18)

Label each user's recurring Locations as "home", "work", or "other".

Parameters:

Name Type Description Default
locations Locations

Locations previously generated from staypoints (via Staypoints.generate_user_locations), so location_id values line up.

required
staypoints Staypoints

Must carry a location_id column (the enriched Staypoints returned alongside locations).

required
method str

Only "freq" is implemented: per user, the location with the most total night-time dwell (night_start_hour..night_end_hour, wrapping midnight) is labeled "home"; the location (other than home) with the most total weekday daytime dwell (work_start_hour..work_end_hour, Mon-Fri) is labeled "work". Every other location is "other".

'freq'
night_start_hour int

Hour-of-day boundaries (0-23) used by the "freq" heuristic.

22
night_end_hour int

Hour-of-day boundaries (0-23) used by the "freq" heuristic.

6
work_start_hour int

Hour-of-day boundaries (0-23) used by the "freq" heuristic.

8
work_end_hour int

Hour-of-day boundaries (0-23) used by the "freq" heuristic.

18

Returns:

Type Description
Locations

A copy of locations with an added purpose column.

References
  • Martin, F. et al. (2023) trackintel: location_identifier(method="FREQ").
  • transbigdata: mobile_identify_home/mobile_identify_work.

Worked example: Functional forms


create_activity_flag(staypoints, method='time_threshold', time_threshold_min=15.0)

Flag each staypoint as a genuine "activity" (vs. a transient stop) by dwell time.

Parameters:

Name Type Description Default
staypoints Staypoints or dataframe

Must have started_at/finished_at columns (or the Staypoints wrapper's own resolved column names).

required
method str

Only "time_threshold" is implemented.

'time_threshold'
time_threshold_min float

Minimum dwell time, in minutes, to count as an activity. Default 15.0.

15.0

Returns:

Type Description
Staypoints or dataframe

Input with an added boolean activity column.

References
  • Martin, F. et al. (2023) trackintel: create_activity_flag.

Worked example: Functional forms


trajectory_to_trips(traj, resolution=9, *, uid_col=None, datetime_col=None, lat_col=None, lng_col=None, drop_self_loops=True, presorted=False)

Build a fastmob.Trips row for each consecutive ping pair.

Each user's fixes are sorted chronologically, tessellated to H3, then paired with that user's next fix. Same-cell pairs are dropped by default, matching trajectory_to_od. The returned Trips contain origin_location_id and destination_location_id for direct sparse CPC comparisons, alongside trip IDs and the pair's start/finish times.

This represents a consecutive-fix movement pair, not the activity-to- activity trips built from triplegs and staypoints.

presorted=True skips the internal chronological sort, trusting that rows are already ordered by [uid_col, datetime_col] (or just datetime_col when there is no uid_col) -- see trajectory_to_od for the full caveat.

Worked example: Common part of commuters and links


trajectory_to_od(traj, resolution=9, *, uid_col=None, datetime_col=None, lat_col=None, lng_col=None, drop_self_loops=True, presorted=False)

Build a long-format Origin-Destination matrix directly from a raw trajectory.

Tessellates each point into an H3 cell (via fastmob.preprocessing.latlng_to_h3), sorts each user chronologically, and pairs each point's cell with the next point's cell (per user) to derive one trip per consecutive fix. Pairing and counting are fused into a single Rust pass (see od_edge_counts_presorted) rather than materializing every paired trip before counting.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
resolution int

H3 resolution, 0-15.

9
uid_col str | None

Explicit column name overrides; auto-detected when None. When uid_col cannot be found, the whole frame is treated as one user.

None
datetime_col str | None

Explicit column name overrides; auto-detected when None. When uid_col cannot be found, the whole frame is treated as one user.

None
lat_col str | None

Explicit column name overrides; auto-detected when None. When uid_col cannot be found, the whole frame is treated as one user.

None
lng_col str | None

Explicit column name overrides; auto-detected when None. When uid_col cannot be found, the whole frame is treated as one user.

None
drop_self_loops bool

When True (default), consecutive fixes that land in the same cell are not counted as a trip -- matches how "did the user actually move somewhere" trip derivation is normally defined. Set False to also count same-cell "stayed put" pairs (as MoveInside traffic).

True
presorted bool

When True, skip the internal chronological sort and trust that rows are already ordered by [uid_col, datetime_col] (or just datetime_col when there is no uid_col). Rows are still cleaned (nulls dropped, coordinates cast) but not physically reordered. Passing already-interleaved-by-user data with this flag set silently produces wrong pairings -- only set it when the caller can guarantee the ordering.

False

Returns:

Type Description
DataFrame

Long-format OD counts with columns ["origin", "destination", "count"], sorted by ["origin", "destination"] -- the same shape fastmob.measures.collective.od.od_matrix returns. Callers that want the wide pivoted form used by some report code can .pivot(on="destination", index="origin", values="count") this themselves.

Examples:

>>> import pandas as pd
>>> from fastmob.preprocessing import trajectory_to_od
>>> traj = pd.DataFrame(
...     {
...         "uid": ["u1", "u1", "u1"],
...         "datetime": pd.to_datetime(["2020-01-01 00:00", "2020-01-01 01:00", "2020-01-01 02:00"]),
...         "lat": [37.7793, 37.6007, 37.6007],
...         "lng": [-122.4194, -122.3824, -122.3824],
...     }
... )
>>> result = trajectory_to_od(traj, resolution=7)
>>> len(result)
1

Worked example: From check-ins to flows


predict_transport_mode(triplegs, method='simple-coarse', categories=None, *, speed_col='mean_speed_kmh')

Classify each tripleg's transport mode from its average speed.

Parameters:

Name Type Description Default
triplegs Triplegs or dataframe

A fastmob.core.Triplegs instance, or any Narwhals-compatible dataframe with a speed_col column.

required
method str

Only "simple-coarse" (speed-threshold binning) is implemented.

'simple-coarse'
categories dict[float, str]

Mapping of upper-bound speed threshold (km/h) to mode label, evaluated in ascending order (the last key should be math.inf to catch every remaining row). Defaults to DEFAULT_MODE_CATEGORIES.

None
speed_col str

Column holding each row's average speed, km/h. Default "mean_speed_kmh" (Triplegs' own column).

'mean_speed_kmh'

Returns:

Type Description
Triplegs or dataframe

Input with an added mode column, in the same type/backend as input.

References
  • Martin, F. et al. (2023) trackintel: predict_transport_mode.

Worked example: Functional forms


calculate_modal_split(triplegs, freq=None, metric='count', per_user=False, normalize=False)

Aggregate a mode-labeled Triplegs table into a modal-split table.

Parameters:

Name Type Description Default
triplegs Triplegs or dataframe

Must already have a mode column (see predict_transport_mode).

required
freq str

Time-bucket the split by started_at: one of "hour", "day", "week", "month". None (default): no time bucketing.

None
metric str

"count" (default, number of triplegs), "distance" (sum of length_km), or "duration" (sum of duration_s).

'count'
per_user bool

When True, split per user as well (requires a uid column). Default False.

False
normalize bool

When True, normalize each row-group (excluding mode itself) to sum to 1. Default False.

False

Returns:

Type Description
dataframe

One row per (mode [, uid][, time bucket]), with a value column.

Worked example: Functional forms


latlng_to_h3(traj, resolution=9, *, lat_col=None, lng_col=None, output_col='h3_cell')

Convert each row's (lat, lng) to an H3 cell index, Rust-accelerated.

Rows with a non-finite or out-of-range coordinate (or a real Arrow null, for Polars/PyArrow input) get a null cell rather than failing the whole batch: NaN/inf coordinates in a NumPy-backed frame map to the reserved sentinel 2**64 - 1 (not a valid H3 index), while an Arrow-backed frame gets a genuine null slot in the output column.

Parameters:

Name Type Description Default
traj Any

Trajectory dataframe; any Narwhals-compatible eager backend.

required
resolution int

H3 resolution, 0-15.

9
lat_col str | None

Explicit column name overrides; auto-detected when None.

None
lng_col str | None

Explicit column name overrides; auto-detected when None.

None
output_col str

Name of the new column holding the H3 cell index.

'h3_cell'

Returns:

Type Description
DataFrame

traj with output_col added, in the same backend as input.

Examples:

>>> import pandas as pd
>>> from fastmob.preprocessing import latlng_to_h3
>>> df = pd.DataFrame({"lat": [37.769377], "lng": [-122.388519]})
>>> result = latlng_to_h3(df, resolution=9)
>>> hex(result["h3_cell"].iloc[0])
'0x89283082e73ffff'

Worked example: Homes and visits per place


h3_to_latlng(traj, *, h3_col='h3_cell', lat_col='center_lat', lng_col='center_lng')

Convert each H3 cell to its geographic center coordinates.

Null or invalid H3 cells produce null latitude and longitude values.

Parameters:

Name Type Description Default
traj Any

Dataframe containing an H3 cell column; any Narwhals-compatible eager backend.

required
h3_col str

Name of the input column containing native unsigned integer H3 cell indexes.

'h3_cell'
lat_col str

Names of the output columns containing the cell-center latitude and longitude.

'center_lat'
lng_col str

Names of the output columns containing the cell-center latitude and longitude.

'center_lng'

Returns:

Type Description
DataFrame

traj with the two centroid columns added, in the same backend as input.

Examples:

>>> import pandas as pd
>>> from fastmob.preprocessing import h3_to_latlng, latlng_to_h3
>>> cells = latlng_to_h3(pd.DataFrame({"lat": [37.769377], "lng": [-122.388519]}))
>>> result = h3_to_latlng(cells)
>>> round(result["center_lat"].iloc[0], 5)
37.76929

Worked example: Common part of commuters and links


cdr_to_trips_df(visitation_df, user_id_column='user_id', avg_speed_kmh=50.0, circuity=1.2)

Derive a CDR trips dataframe from cdr_to_visitation_df output.

Parameters:

Name Type Description Default
visitation_df Any

Visitation dataframe produced by cdr_to_visitation_df.

required
user_id_column str

Column containing user IDs.

'user_id'
avg_speed_kmh float

Assumed average travel speed used to estimate trip durations.

50.0
circuity float

Multiplier applied to straight-line travel distance.

1.2

Returns:

Type Description
DataFrame

NetMob-like trips dataframe in the caller's original backend.

Examples:

>>> import pandas as pd
>>> from fastmob.preprocessing import cdr_to_trips_df, cdr_to_visitation_df
>>> cdr = pd.DataFrame(
...     {
...         "user_id": ["u1", "u1", "u1", "u1"],
...         "timestamp": pd.to_datetime(
...             ["2020-01-01 08:00", "2020-01-01 08:15", "2020-01-01 09:00", "2020-01-01 10:00"]
...         ),
...         "venueId": ["home", "home", "work", "home"],
...         "lat": [0.0, 0.0, 0.01, 0.0],
...         "long": [0.0, 0.0, 0.01, 0.0],
...     }
... )
>>> visits = cdr_to_visitation_df(cdr)
>>> result = cdr_to_trips_df(visits)
>>> preview = result[["user_id", "trip_number", "origin_area", "destination_area", "duration_minutes"]]
>>> print(preview.round({"duration_minutes": 2}).to_string(index=False))
user_id  trip_number origin_area destination_area  duration_minutes
     u1            1        home             work              2.26
     u1            2        work             home              2.26

Worked example: Mobile-phone records


cdr_to_visitation_df(trajectory_df, user_id_column='user_id', timestamp_column='timestamp', venue_column='venueId', lat_column='lat', lon_column='long', avg_speed_kmh=50.0, circuity=1.2)

Convert raw CDR records into a NetMob-like visitation dataframe.

Consecutive records at the same venue for the same user are collapsed into one stay. The stay end time is estimated from approximate travel time to the next stay, clamped to the observed gap between venue changes.

Parameters:

Name Type Description Default
trajectory_df Any

Raw CDR-like records in any Narwhals-compatible eager dataframe.

required
user_id_column str

Column containing user IDs.

'user_id'
timestamp_column str

Column containing observation timestamps.

'timestamp'
venue_column str

Column containing venue or cell-tower IDs.

'venueId'
lat_column str

Latitude column.

'lat'
lon_column str

Longitude column.

'long'
avg_speed_kmh float

Assumed average travel speed used to estimate stay end times.

50.0
circuity float

Multiplier applied to straight-line travel distance.

1.2

Returns:

Type Description
DataFrame

NetMob-like visitation dataframe in the caller's original backend.

Examples:

>>> import pandas as pd
>>> from fastmob.preprocessing import cdr_to_visitation_df
>>> cdr = pd.DataFrame(
...     {
...         "user_id": ["u1", "u1", "u1", "u1"],
...         "timestamp": pd.to_datetime(
...             ["2020-01-01 08:00", "2020-01-01 08:15", "2020-01-01 09:00", "2020-01-01 10:00"]
...         ),
...         "venueId": ["home", "home", "work", "home"],
...         "lat": [0.0, 0.0, 0.01, 0.0],
...         "long": [0.0, 0.0, 0.01, 0.0],
...     }
... )
>>> result = cdr_to_visitation_df(cdr)
>>> preview = result[["user_id", "area", "start_timestamp", "end_timestamp", "duration_minutes"]]
>>> print(preview.round({"duration_minutes": 2}).to_string(index=False))
user_id area     start_timestamp              end_timestamp  duration_minutes
     u1 home 2020-01-01 08:00:00 2020-01-01 08:57:44.132898             57.74
     u1 work 2020-01-01 09:00:00 2020-01-01 09:57:44.132898             57.74
     u1 home 2020-01-01 10:00:00                        NaT               NaN

Worked example: Mobile-phone records