processing.tours
processing.tours.aggregation_helpers
Tour aggregation helper functions for tour extraction.
This module contains functions for: - Flagging each trip end against its own tour's anchor - Aggregating trips to tour-level records and classifying them - Assigning half-tour classification
The primary destination and tour purpose come from destination_helpers.
add_tour_anchor_flags
add_tour_anchor_flags(linked_trips: pl.DataFrame) -> pl.DataFrame
Flag each trip end as being at its own tour's anchor.
A tour's anchor is the place it is expected to leave from and come back to:
home for a home-based tour, and -- for a subtour -- whatever the containing
tour anchored on (_anchor_location_type, set by
:func:~processing.tours.detection_helpers.expand_anchor_periods).
Collapsing both cases into one pair of columns is what lets tour classification and validation ask "did this tour reach its anchor?" without first knowing which anchor applies. Before this existed, the anchor was assumed to be home, so an at-work subtour -- which by definition never touches home -- could never be classified COMPLETE and was dropped by every downstream gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trips with |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
The trips with |
aggregate_tour_attributes
aggregate_tour_attributes(
linked_trips: pl.DataFrame,
config: TourConfig,
match: MatchConfig,
) -> tuple[pl.DataFrame, pl.DataFrame]
Aggregate trip data to tour-level records with attributes.
Calculates tour attributes from trip data: - Tour purpose: Highest priority destination, with duration tie-breaker (When priorities equal, selects trip with longest activity duration) - Tour mode: Highest priority trip mode - Timing: First departure and last arrival - Counts: Number of trips and stops
IMPORTANT: Work-based subtours are aggregated separately with their own tour_id (which includes subtour_num in the last 2 digits). The final output includes both home-based tours and work-based subtours as separate records.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Linked trips with tour_id |
required |
config
|
TourConfig
|
TourConfig object with priority settings |
required |
match
|
MatchConfig
|
The habitual-location match rule, for returns to the destination |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Tuple of: (enhanced_linked_trips, tours) |
pl.DataFrame
|
|
processing.tours.destination_helpers
Tour purpose, primary destination, and when the tour was there.
This module contains functions for: - Choosing each tour's primary destination and so its purpose - Timing the arrival at and departure from that destination
calculate_tour_purpose_and_destination
calculate_tour_purpose_and_destination(
linked_trips: pl.DataFrame, config: TourConfig
) -> tuple[pl.DataFrame, pl.DataFrame]
Calculate tour purpose and primary destination from trip data.
Determines tour purpose from the highest priority non-last trip, with activity duration as a tie-breaker. Returns enhanced trip data with purpose priorities and primary destination coordinates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trip data with tour_num and subtour_num |
required |
config
|
TourConfig
|
TourConfig with purpose hierarchy |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Tuple of (enhanced_linked_trips, tour_purp_and_coords): |
pl.DataFrame
|
|
tuple[pl.DataFrame, pl.DataFrame]
|
|
calculate_destination_times
calculate_destination_times(
linked_trips: pl.DataFrame, buffer_meters: float
) -> pl.DataFrame
Calculate arrival and departure times at primary destination.
A trip arrives at or departs from the primary destination when its end is within the habitual-location buffer of it.
Candidate trips are the ones that could be the destination -- the same
_is_not_a_destination test purpose selection uses, so the two cannot
disagree about which trip the destination is. Selecting on "not the last
trip" instead assumed every tour returns to its anchor: a one-trip tour's
only trip is also its last, so it matched no candidate rule and came out
with no destination at all, while purpose selection had already given it one.
dest_depart_time stays null when nothing departed the destination. A
tour cut by the diary edge arrived somewhere and stopped being observed;
inventing a departure would put a time on an event that did not happen.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Enhanced trip data with primary destination coordinates |
required |
buffer_meters
|
float
|
The habitual-location buffer: a trip end this close to the primary destination is at it. |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
DataFrame with dest_arrive_time, dest_depart_time, and |
pl.DataFrame
|
dest_linked_trip_id per tour_id |
processing.tours.detection_helpers
Tour detection helper functions for tour extraction.
This module contains functions for: - Identifying home-based tour boundaries - Expanding anchor location periods (work, school) - Detecting anchor-based subtours (work-based, school-based)
identify_home_based_tours
identify_home_based_tours(
linked_trips: pl.DataFrame, check_multiday_gaps: bool = False
) -> pl.DataFrame
Identify home-based tours from classified trip data.
Creates tour boundaries for sequences of trips, classifying each tour by whether it starts/ends at home using TourCategory enum: - COMPLETE: Starts at home, ends at home - PARTIAL_END: Starts at home, doesn't end at home - PARTIAL_START: Doesn't start at home, ends at home - PARTIAL_BOTH: Neither starts nor ends at home
Tours are identified by detecting: 1. Departures from home (o_is_home=True, d_is_home=False) 2. Returns to home (o_is_home=False, d_is_home=True) 3. Multi-day gaps (if check_multiday_gaps=True)
Tours can be filtered downstream using the tour_category column: - Filter to TourCategory.COMPLETE for legacy compatibility - Include partial tours for more comprehensive analysis
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Classified linked trips with location type flags |
required |
check_multiday_gaps
|
bool
|
Whether to check for multi-day gaps |
False
|
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Linked trips with tour_id, tour_category |
expand_anchor_periods
expand_anchor_periods(linked_trips: pl.DataFrame) -> pl.DataFrame
Expand work/school anchor periods for tours using the anchor flags.
For tours whose trips visit a person's work or school location, this finds the first arrival and last departure at that anchor, so subtours are only detected WITHIN the anchor period, not during travel to/from it. For example: Home -> Work -> Lunch -> Work -> Errand -> Home Only Lunch (between the two work visits) is a subtour, not Errand.
The anchor flags _o_at_work/_d_at_work (and _at_school) come from
add_anchor_flags: a trip end is at the work anchor when it is at a
reported workplace, or -- on a day no reported workplace was visited -- at
an observed one. School works the same way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Classified trips with tour_num and anchor flags (_o_at_work, _d_at_work, _o_at_school, _d_at_school). |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Trips with _anchor_period_start_trip_num, _anchor_period_end_trip_num, |
pl.DataFrame
|
and _anchor_location_type. Anchor flags are retained for subtour detection. |
detect_anchor_based_subtours
detect_anchor_based_subtours(
linked_trips: pl.DataFrame,
) -> pl.DataFrame
Detect anchor-based subtours using hybrid loop approach.
LEGACY REFERENCE: 03a-tour_extract_week.py lines 578-600 MATCHES LEGACY: Only detects subtours within expanded anchor periods
This is a hybrid approach that uses fast Polars vectorized operations to detect home-based tours, expand anchor periods, then this function uses a slower but more flexible/understandable loop-based approach to detect subtours. The computational cost is acceptable since subtours are relatively rare compared to overall trips/tours. Perhaps future versions could optimize this further.
It loops over tours with anchor periods and detects subtours by finding leave/return patterns. Uses the anchor_period markers from expand_anchor_periods() to know where to look for subtours.
A subtour is detected when: 1. Trip leaves an anchor location (o at anchor, d not) 2. A later trip returns to that same anchor location (o not, d at it) 3. Both trips are WITHIN the expanded anchor period
This prevents false subtour detection on trips to/from home.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trips with anchor period markers |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Trips with subtour_id assigned to subtour trips |
processing.tours.extraction
Tour building module for travel diary survey processing.
This module implements a hierarchical tour extraction algorithm that processes linked trip data to identify and classify tours and subtours based on spatial and temporal patterns.
Algorithm
The tour building process runs these steps in order:
1. Location Classification
- Says which of the person's habitual locations (built by the
detect_habitual_locationsstep) each trip origin/destination is at - Classifies each trip origin/destination as HOME, WORK, SCHOOL, or OTHER from the location it is at; any of the person's homes bounds tours
- Adds boolean flags: o_is_home, d_is_home, o_is_work, d_is_work, etc.
2. Home-Based Tour Identification
- Sorts trips by person, day, and departure time
- Identifies tour boundaries by detecting:
- Departures from home (o_is_home=True, d_is_home=False)
- Returns to home (o_is_home=False, d_is_home=True)
- Day boundaries (first trip of person-day)
- Numbers the tours within each person-day (
tour_num)
3. Anchor Period Expansion (CRITICAL for subtours)
- For tours visiting usual anchor locations (work, school), expands the "at anchor" period by finding first arrival and last departure
- Uses pure Polars window functions to identify anchor periods
- Prevents subtours from being detected during travel to/from anchor
- Generalizable: supports work, school, or future anchor types
4. Anchor-Based Subtour Detection
- Within expanded anchor periods, identifies subtours by detecting:
- Departures from an anchor (o_at_anchor=True, d_at_anchor=False)
- Returns to that same anchor (o_at_anchor=False, d_at_anchor=True)
- Numbers the subtours within each tour (
subtour_num); work- and school-based subtours alike
5. Tour IDs and Attribute Aggregation
tour_idis theday_idfollowed by four digits,tour_num * 1000 + subtour_num * 10; a subtour'sparent_tour_idis its tour's id withsubtour_num0-
Computes tour-level attributes from constituent trips:
- tour_purpose: the primary destination's purpose, chosen by score
(activity duration and purpose, the default) or by the
person-category hierarchy (
tour_purpose_method) - tour_mode: Highest priority travel mode (from configurable mode hierarchy)
- origin_depart_time: First trip's departure time
- dest_arrive_time: Last trip's arrival time
- trip_count: Number of trips in tour
- tour_purpose: the primary destination's purpose, chosen by score
(activity duration and purpose, the default) or by the
person-category hierarchy (
-
Assigns each trip's
tour_direction: outbound (before the primary destination) or inbound (after it). A subtour has two halves too; subtour membership is read fromparent_tour_id, not the direction
6. Joint Tour Identification
- If joint_trips data provided, identifies tours where all trips involve same group of travelers
- Assigns joint_tour_id to tours with stable participant groups
- Links tour-level joint travel to trip-level joint travel
7. Tour Data Quality
- Stamps each tour's
tour_data_quality: valid, or the first reason it is not a valid round trip (no destination, a spatial gap, another home, split across days, at the diary's edge). Nothing is corrected - Adds tour_id and joint_tour_id to unlinked_trips for reference
8. Joint Tours Table
- Collapses the member tours of each joint tour into
joint_tours
Edge Case Handling is performed including
- Incomplete tours (no return home at end of day)
- Multi-day tours (spanning survey boundaries)
- Missing work/school locations (null coordinates)
- Non-sequential trip chains (spatial gaps)
- Hierarchical tour structure: Home-based tours → work- and school-based subtours
- Location matching allows for GPS/geocoding error through one buffer
- Tour purpose reflects primary activity, not intermediate stops
- Extensible design allows further anchor types
extract_tours
extract_tours(
persons: pl.DataFrame,
unlinked_trips: pl.DataFrame,
linked_trips: pl.DataFrame,
habitual_locations: pl.DataFrame,
days: pl.DataFrame,
extraction: TourConfig,
habitual_match: MatchConfig,
joint_trips: pl.DataFrame | None = None,
) -> dict[str, pl.DataFrame]
Extract hierarchical tour structures from linked trip data.
Builds tour and subtour structures from linked trip sequences using spatial and temporal patterns. See module docstring for complete algorithm description.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
persons
|
pl.DataFrame
|
Person attributes, for the person category purpose priority is ranked by. |
required |
unlinked_trips
|
pl.DataFrame
|
Individual trip segments. Will receive tour_id assignment. |
required |
linked_trips
|
pl.DataFrame
|
Journey records with coordinates and timing. Required columns: person_id, day_id, o_lon, o_lat, d_lon, d_lat, depart_time, arrive_time. |
required |
habitual_locations
|
pl.DataFrame
|
The person's habitual locations, from
|
required |
days
|
pl.DataFrame
|
Days with |
required |
extraction
|
TourConfig
|
Tour-building rules. See |
required |
habitual_match
|
MatchConfig
|
Whether a trip end is at a habitual location, and the
distance within which it returns to a tour's primary destination.
The tours' own setting, separate from |
required |
joint_trips
|
pl.DataFrame | None
|
Optional joint trip aggregations. If provided, enables joint tour identification based on stable participant groups. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, pl.DataFrame]
|
Dictionary containing:
|
processing.tours.joint_tour_helpers
Joint tour identification for linking individual tours taken together.
This module identifies which individual tours are "joint" - where all trips throughout the tour involve the same stable group of 2+ household members. Joint tours are linked via joint_tour_id while maintaining individual tour records in the canonical data model.
Algorithm:
- Find eligible tours: every trip in the tour is joint, and there are 2+ trips
- Count the tour's stable group: participants present on ALL of its joint trips
- Keep tours whose stable group has 2+ people
- Assign joint_tour_id per occasion: tours sharing the same set of joint_trip_ids are the same outing and get the same id
- Drop singletons: an id held by only one person is not a joint tour
Every step works off linked_trips' existing (person_id, tour_id,
joint_trip_id) relation, so participation stays a row-per-fact join rather than
being collapsed into list columns.
Example:
Person A, B, C depart together (joint_trip_id=1001): - Trip 1: A,B,C -> shopping (joint_trip_id=1001) - Trip 2: A,B,C -> home (joint_trip_id=1001) Result: A, B, C all get same joint_tour_id
Person A, B, C depart, C drops off: - Trip 1: A,B,C -> school (joint_trip_id=1001) - Trip 2: A,B -> home (joint_trip_id=1002) Result: A and B get joint_tour_id (stable pair), C gets NULL
The same pair on two separate outings get two different joint_tour_ids, because the outings have different joint_trip_ids.
MIN_TRIPS_FOR_JOINT_TOUR
module-attribute
MIN_TRIPS_FOR_JOINT_TOUR = 2
MIN_JOINT_TOUR_PARTICIPANTS
module-attribute
MIN_JOINT_TOUR_PARTICIPANTS = 2
identify_joint_tours
identify_joint_tours(
linked_trips: pl.DataFrame, tours: pl.DataFrame
) -> tuple[pl.DataFrame, pl.DataFrame]
Identify joint tours by analyzing stable participant groups.
Tours are considered joint if all constituent trips involve the same stable group of 2+ household members throughout the entire tour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trip data with tour_id and joint_trip_id |
required |
tours
|
pl.DataFrame
|
Tour-level records to update with joint_tour_id |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Tuple of (updated_linked_trips, updated_tours) with joint_tour_id |
pl.DataFrame
|
column added |
build_joint_tours_table
build_joint_tours_table(
tours: pl.DataFrame, days: pl.DataFrame
) -> pl.DataFrame
Build the canonical joint_tours table from tours carrying a joint id.
One row per joint_tour_id, mirroring how build_joint_trips_table
collapses member trips into a joint trip. The member tours stay authoritative
for everything tour-shaped; this table exists so the group itself can be
counted, validated and weighted once.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tours
|
pl.DataFrame
|
Canonical tours, after |
required |
days
|
pl.DataFrame
|
Days with |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
One row per joint tour with |
pl.DataFrame
|
and, where the member tours carry it, |
processing.tours.location_helpers
Location classification helper functions for tour extraction.
This module contains functions for: - Classifying trip origins/destinations as HOME, WORK, SCHOOL, or OTHER from the habitual location each end is at - Per-day work/school anchor flags
classify_trip_locations
classify_trip_locations(
linked_trips: pl.DataFrame,
habitual_locations: pl.DataFrame,
config: MatchConfig,
days: pl.DataFrame,
) -> pl.DataFrame
Classify trip origins and destinations against a person's habitual locations.
Whether an end is at a location is decided once, by match_trip_ends:
within the buffer, with a purpose that agrees, where a day's stated start
and end at home also count at its first origin and last destination. This
only reads that answer. Purpose alone never makes an end home, work or
school.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trip data with o/d coordinates and purpose codes. |
required |
habitual_locations
|
pl.DataFrame
|
Habitual locations. |
required |
config
|
MatchConfig
|
Matching rules. |
required |
days
|
pl.DataFrame
|
Days with |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Trips with added columns: |
pl.DataFrame
|
|
pl.DataFrame
|
|
pl.DataFrame
|
|
add_anchor_flags
add_anchor_flags(linked_trips: pl.DataFrame) -> pl.DataFrame
Say which workplace or school each trip end is anchored at, per day.
A tour anchor is the place a person is based for the day, and the rule is the same for work and school: a trip end is at the anchor if it is at one of the person's reported locations of that kind, or — on days they did not visit a reported one at all — at one of their observed ones. So someone can be based at a different office on different days, while a stop near the usual office on an office day stays a subtour.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
linked_trips
|
pl.DataFrame
|
Trips from |
required |
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Trips with |
pl.DataFrame
|
telling anchors apart) and |
pl.DataFrame
|
per-source matches dropped. |
processing.tours.priority_utils
Priority calculation utilities for tour purpose and mode selection.
This module provides pure helper functions for calculating priority values used in tour purpose and mode aggregation. These functions are stateless and can be used across different tour processing contexts.
add_purpose_priority_column
add_purpose_priority_column(
df: pl.DataFrame,
config: TourConfig,
alias: str = "purpose_priority",
) -> pl.DataFrame
Add purpose priority column to dataframe.
Maps person_category (PersonCategory string) → purpose priority value. Purpose priority is determined by the nested mapping in the TourConfig: Args: df: DataFrame with person_category and d_purpose_category columns config: TourConfig with priority mappings alias: Column name for the priority values
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
DataFrame with added purpose_priority column |
add_mode_priority_column
add_mode_priority_column(
df: pl.DataFrame,
mode_hierarchy: list,
alias: str = "mode_priority",
) -> pl.DataFrame
Add mode priority column based on mode hierarchy.
Maps ModeType values to priority integers based on their position in the mode hierarchy list (later in list = higher priority).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
pl.DataFrame
|
DataFrame with mode_type column |
required |
mode_hierarchy
|
list
|
Ordered list of ModeType enums (ascending priority) |
required |
alias
|
str
|
Column name for the priority values |
'mode_priority'
|
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
DataFrame with added mode_priority column |
add_activity_duration_column
add_activity_duration_column(
df: pl.DataFrame,
default_minutes: float = 240.0,
alias: str = "activity_duration",
) -> pl.DataFrame
Add activity duration column to dataframe.
Calculates time spent at each destination as the time between arrival and the next trip's departure. For the last trip of the day, uses the provided default duration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
pl.DataFrame
|
DataFrame with arrive_time, depart_time, person_id, day_id columns |
required |
default_minutes
|
float
|
Default duration for last trip of day (default: 240) |
240.0
|
alias
|
str
|
Column name for the duration values |
'activity_duration'
|
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
DataFrame with added activity_duration column (in minutes) |
add_purpose_score_column
add_purpose_score_column(
df: pl.DataFrame,
config: TourConfig,
purpose_col: str = "d_purpose_category",
duration_col: str = "_activity_duration",
person_category_col: str = "person_category",
alias: str = "purpose_score",
) -> pl.DataFrame
Add a duration-weighted purpose score used to pick the tour purpose.
Each candidate activity scores W * x / (x + h) where x is its
activity duration, W is the purpose's ceiling weight for the person's
category (config.purpose_score_weights) and h is the purpose's
half-saturation duration (config.purpose_score_halfmax). The score rises
with duration toward W, reaching W / 2 at x = h, so a long
discretionary activity can outscore a brief mandatory one. Highest score
wins.
Purposes with no configured weight (e.g. HOME) get a null score and are never selected. The lookups are vectorized joins rather than per-row evaluation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
pl.DataFrame
|
Trip data with the purpose, duration and person-category columns. |
required |
config
|
TourConfig
|
TourConfig carrying the weight and half-max tables. |
required |
purpose_col
|
str
|
Column holding the purpose category value. |
'd_purpose_category'
|
duration_col
|
str
|
Column holding the activity duration in minutes. |
'_activity_duration'
|
person_category_col
|
str
|
Column holding the PersonCategory string. |
'person_category'
|
alias
|
str
|
Name for the score column to add. |
'purpose_score'
|
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
DataFrame with the score column added. |
processing.tours.tour_configs
Configuration models for tour building parameters.
ALGORITHM DESIGN NOTES:
This module implements a flexible tour extraction algorithm with configurable behavior for different use cases. Key design decisions:
-
LOCATION CLASSIFICATION: A trip end is at a habitual location when it is within one buffer of it AND its purpose agrees (see processing.habitual_locations).
-
TOUR COMPLETENESS: Incomplete tours are allowed (e.g., missing return home), but are flagged and categorized accordingly in output so they can be handled downstream.
-
PURPOSE PRIORITY: Configurable hierarchies by person type. By default, work_related has same priority as work. Duration tie-breaking can be enabled via config.
-
MODE HIERARCHY: Ordered list where later = higher priority. Single tour mode by default, half-tour modes can be enabled via config.
-
WORK-BASED SUBTOURS: By default detects all work departures/returns. Can restrict to usual workplace only via config.detect_usual_workplace.
OUTPUT FORMAT:
Returns tour-level and trip-level DataFrames with enum types and minute-based times. Downstream formatters can transform to Daysim format (integer codes, HHMM times, tour reordering, etc.) as needed.
TourConfig
pydantic-model
Configuration model for tour building parameters.
This config uses Pydantic for validation and provides type-safe access
to tour building parameters including mode hierarchies and purpose
priorities. The one distance that decides whether a trip end is at a place
is not here: extract_tours takes it as its own MatchConfig.
Config:
extra:forbid
Fields:
-
mode_hierarchy(list[ModeType]) -
purpose_priority_by_personcat(dict[str, dict[PurposeCategory, int]]) -
tour_purpose_method(Literal['score', 'hierarchy']) -
purpose_score_weights(dict[str, dict[PurposeCategory, float]]) -
purpose_score_halfmax(dict[PurposeCategory, float]) -
check_multiday_gaps(bool) -
detect_usual_workplace(bool) -
spatial_gap_threshold_meters(float) -
default_activity_duration_minutes(float)
mode_hierarchy
pydantic-field
mode_hierarchy: list[ModeType] = [
ModeType.WALK,
ModeType.BIKE,
ModeType.BIKESHARE,
ModeType.SCOOTERSHARE,
ModeType.CAR,
ModeType.CARSHARE,
ModeType.TAXI,
ModeType.TNC,
ModeType.SHUTTLE,
ModeType.SCHOOL_BUS,
ModeType.FERRY,
ModeType.TRANSIT,
ModeType.LONG_DISTANCE,
]
Ordered list of mode types by priority - later in list = higher priority
purpose_priority_by_personcat
pydantic-field
purpose_priority_by_personcat: dict[
str, dict[PurposeCategory, int]
] = {
PersonCategory.WORKER: {
PurposeCategory.WORK: 1,
PurposeCategory.WORK_RELATED: 1,
PurposeCategory.SCHOOL: 2,
PurposeCategory.SCHOOL_RELATED: 2,
PurposeCategory.ESCORT: 3,
PurposeCategory.SHOP: 4,
PurposeCategory.MEAL: 4,
PurposeCategory.SOCIALREC: 4,
PurposeCategory.ERRAND: 4,
PurposeCategory.CHANGE_MODE: 5,
PurposeCategory.OVERNIGHT: 5,
PurposeCategory.OTHER: 5,
PurposeCategory.MISSING: 5,
PurposeCategory.PNTA: 5,
PurposeCategory.NOT_IMPUTABLE: 5,
},
PersonCategory.STUDENT: {
PurposeCategory.SCHOOL: 1,
PurposeCategory.SCHOOL_RELATED: 1,
PurposeCategory.WORK: 2,
PurposeCategory.WORK_RELATED: 2,
PurposeCategory.ESCORT: 3,
PurposeCategory.SHOP: 4,
PurposeCategory.MEAL: 4,
PurposeCategory.SOCIALREC: 4,
PurposeCategory.ERRAND: 4,
PurposeCategory.CHANGE_MODE: 5,
PurposeCategory.OVERNIGHT: 5,
PurposeCategory.OTHER: 5,
PurposeCategory.MISSING: 5,
PurposeCategory.PNTA: 5,
PurposeCategory.NOT_IMPUTABLE: 5,
},
PersonCategory.OTHER: {
PurposeCategory.WORK: 1,
PurposeCategory.WORK_RELATED: 1,
PurposeCategory.SCHOOL: 2,
PurposeCategory.SCHOOL_RELATED: 2,
PurposeCategory.ESCORT: 3,
PurposeCategory.SHOP: 4,
PurposeCategory.MEAL: 4,
PurposeCategory.SOCIALREC: 4,
PurposeCategory.ERRAND: 4,
PurposeCategory.CHANGE_MODE: 5,
PurposeCategory.OVERNIGHT: 5,
PurposeCategory.OTHER: 5,
PurposeCategory.MISSING: 5,
PurposeCategory.PNTA: 5,
PurposeCategory.NOT_IMPUTABLE: 5,
},
}
Priority order for determining tour purpose by person category (lower = higher priority). All non-HOME purposes must be explicitly defined. Used only when tour_purpose_method='hierarchy'.
tour_purpose_method
pydantic-field
tour_purpose_method: Literal['score', 'hierarchy'] = 'score'
How to pick a tour's purpose from its trips: 'score' (duration-weighted, highest score wins) or 'hierarchy' (priority rank, duration as tie-breaker only).
purpose_score_weights
pydantic-field
purpose_score_weights: dict[str, dict[PurposeCategory, float]] = {
PersonCategory.WORKER: {
PurposeCategory.WORK: 10.0,
PurposeCategory.WORK_RELATED: 10.0,
PurposeCategory.SCHOOL: 8.0,
PurposeCategory.SCHOOL_RELATED: 8.0,
PurposeCategory.ESCORT: 2.0,
PurposeCategory.SHOP: 4.0,
PurposeCategory.MEAL: 4.0,
PurposeCategory.SOCIALREC: 4.0,
PurposeCategory.ERRAND: 4.0,
PurposeCategory.CHANGE_MODE: 2.0,
PurposeCategory.OTHER: 2.0,
PurposeCategory.MISSING: 2.0,
PurposeCategory.PNTA: 2.0,
PurposeCategory.NOT_IMPUTABLE: 2.0,
PurposeCategory.OVERNIGHT: 0.0,
},
PersonCategory.STUDENT: {
PurposeCategory.SCHOOL: 10.0,
PurposeCategory.SCHOOL_RELATED: 10.0,
PurposeCategory.WORK: 8.0,
PurposeCategory.WORK_RELATED: 8.0,
PurposeCategory.ESCORT: 2.0,
PurposeCategory.SHOP: 4.0,
PurposeCategory.MEAL: 4.0,
PurposeCategory.SOCIALREC: 4.0,
PurposeCategory.ERRAND: 4.0,
PurposeCategory.CHANGE_MODE: 2.0,
PurposeCategory.OTHER: 2.0,
PurposeCategory.MISSING: 2.0,
PurposeCategory.PNTA: 2.0,
PurposeCategory.NOT_IMPUTABLE: 2.0,
PurposeCategory.OVERNIGHT: 0.0,
},
PersonCategory.OTHER: {
PurposeCategory.WORK: 10.0,
PurposeCategory.WORK_RELATED: 10.0,
PurposeCategory.SCHOOL: 8.0,
PurposeCategory.SCHOOL_RELATED: 8.0,
PurposeCategory.ESCORT: 2.0,
PurposeCategory.SHOP: 4.0,
PurposeCategory.MEAL: 4.0,
PurposeCategory.SOCIALREC: 4.0,
PurposeCategory.ERRAND: 4.0,
PurposeCategory.CHANGE_MODE: 2.0,
PurposeCategory.OTHER: 2.0,
PurposeCategory.MISSING: 2.0,
PurposeCategory.PNTA: 2.0,
PurposeCategory.NOT_IMPUTABLE: 2.0,
PurposeCategory.OVERNIGHT: 0.0,
},
}
Purpose ceiling weight W by person category for the scoring method (higher = more important as a tour anchor). Sets the purpose ranking; the one hand-tuned knob.
purpose_score_halfmax
pydantic-field
purpose_score_halfmax: dict[PurposeCategory, float] = {
PurposeCategory.WORK: 20.0,
PurposeCategory.WORK_RELATED: 20.0,
PurposeCategory.SCHOOL: 20.0,
PurposeCategory.SCHOOL_RELATED: 20.0,
PurposeCategory.ESCORT: 200.0,
PurposeCategory.SHOP: 18.0,
PurposeCategory.MEAL: 26.0,
PurposeCategory.SOCIALREC: 56.0,
PurposeCategory.ERRAND: 29.0,
PurposeCategory.OVERNIGHT: 46.0,
PurposeCategory.OTHER: 59.0,
PurposeCategory.CHANGE_MODE: 60.0,
PurposeCategory.MISSING: 60.0,
PurposeCategory.PNTA: 60.0,
PurposeCategory.NOT_IMPUTABLE: 60.0,
}
Half-saturation duration h in minutes by purpose for the scoring method: the duration at which a purpose reaches half its ceiling W. Discretionary purposes use their BATS-2023 typical activity duration; mandatory purposes use a low establishment threshold; escort is set high so it only wins a pure escort tour.
check_multiday_gaps
pydantic-field
check_multiday_gaps: bool = False
If True, reset tour boundaries when day_gap > 1 between trips.
detect_usual_workplace
pydantic-field
detect_usual_workplace: bool = False
If True, only detect work-based subtours from 'usual workplace' (coordinate match to work_lat/work_lon).
spatial_gap_threshold_meters
pydantic-field
spatial_gap_threshold_meters: float
A tour is flagged SPATIAL_GAP when any internal junction has a gap greater than this (meters) between one trip's destination and the next trip's origin - i.e. a missing leg the tour extractor would otherwise silently weld into one 'survey_complete' tour. BATS uses 1 km, which catches genuine teleports while ignoring same-place geocoding jitter.
default_activity_duration_minutes
pydantic-field
default_activity_duration_minutes: float = 240.0
Default activity duration in minutes for last trip of day when calculating duration tie-breaker (legacy: 4 hours).
ConfigDict
Pydantic model configuration.
arbitrary_types_allowed
class-attribute
instance-attribute
arbitrary_types_allowed = True
person_category_expression
person_category_expression(
age_col: str = "age",
employment_col: str = "employment",
student_col: str = "student",
school_type_col: str = "school_type",
) -> pl.Expr
Create expression to derive person category from person attributes.
This replicates the pptyp logic from the old pipeline's 02a-reformat step, converting employment/student/age data into person type categories.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
age_col
|
str
|
Name of age column (categorical AgeCategory) |
'age'
|
employment_col
|
str
|
Name of employment column |
'employment'
|
student_col
|
str
|
Name of student column |
'student'
|
school_type_col
|
str
|
Name of school_type column |
'school_type'
|
Returns:
| Type | Description |
|---|---|
pl.Expr
|
Polars expression that evaluates to PersonCategory enum value |
Note
Age is a categorical variable (see AgeCategory enum): 1=under 5, 2=5-15, 3=16-17, 4=18-24, 5=25-34, etc.
processing.tours.validation_helpers
Tour validation helper functions.
This module contains functions for:
- Grading each tour's structure into a tour_data_quality code
- Detecting spatial gaps (a missing leg) between consecutive trips
- Asserting that tour boundary detection assigned every trip to a tour
validate_and_correct_tours
validate_and_correct_tours(
tours: pl.DataFrame,
linked_trips: pl.DataFrame,
spatial_gap_threshold_meters: float = 1000.0,
) -> pl.DataFrame
Stamp tour_data_quality: why each tour is not a valid round trip.
Assigns :class:TourDataQuality by first match. The order is deliberate --
a tour can fail several ways at once and the code should name the one that
decides what to do with it:
NO_DESTINATION-- a closed tour with nothing to anchor on. Subsumes the old loop-trip code and the change-mode-only case alike. Restricted to closed tours on purpose: a partial tour lacks an activity because half of it went unobserved, which its open-end code already says. The one exception is a move between two homes (next).SPATIAL_GAP-- a leg is missing, inside the tour or at its boundary.OTHER_HOME-- a closed home-based tour that leaves from or returns to a home of the person's other than their primary one. Only closed tours: one also open at its other end is reported by that open end instead. Subtours are graded on their own anchor, whatever home sits beside it. A trip straight from one home to another has no destination, but the move is the reason, so it is this code rather thanNO_DESTINATION.PARTIAL_DAY_SPLIT-- the journey continues from the same place on the next diary day. Recoverable by stitching.PARTIAL_DIARY_EDGE-- the diary begins or ends at that open end. Nothing to recover; the survey simply stopped watching.
tour_category is left exactly as aggregation found it. It reports which
ends are open; this reports why, and the two are read together.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tours
|
pl.DataFrame
|
Aggregated tours with tour_category and tour_purpose. |
required |
linked_trips
|
pl.DataFrame
|
Linked trips with tour_id, coordinates and times. |
required |
spatial_gap_threshold_meters
|
float
|
Distance above which a junction between consecutive trips counts as a missing leg. |
1000.0
|
Returns:
| Type | Description |
|---|---|
pl.DataFrame
|
Tours with a |