Extract Tours
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.
Example
- name: extract_tours
params:
spatial_gap_threshold_meters: 1000.0
buffer_meters: 300 # how near a trip end must be to a habitual location to match it
Settings
extraction
| Key | Type | Default | Meaning |
|---|---|---|---|
mode_hierarchy |
list | built in | Ordered list of mode types by priority - later in list = higher priority |
purpose_priority_by_personcat |
mapping | built in | 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 |
score or 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 |
mapping | built in | 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 |
mapping | built in | 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 |
true / false | false |
If True, reset tour boundaries when day_gap > 1 between trips. |
detect_usual_workplace |
true / false | false |
If True, only detect work-based subtours from 'usual workplace' (coordinate match to work_lat/work_lon). |
spatial_gap_threshold_meters |
number | required | 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 |
number | 240.0 |
Default activity duration in minutes for last trip of day when calculating duration tie-breaker (legacy: 4 hours). |
habitual_match
| Key | Type | Default | Meaning |
|---|---|---|---|
at_address_meters |
number, > 0 | 100.0 |
Distance in metres within which a trip end is at the person's own primary home whatever its purpose. At their doorstep the purpose describes what they are doing -- exercising, working from home, dropping someone off -- not a different place, and refusing it welds two tours into one. Further out the purpose has to agree, because that is where the corner shop is. Homes only: a lunch place can be metres from the office door. |
buffer_meters |
number, > 0 | required | Distance in metres within which two points are the same place. It groups stops into one location, decides a found place is a reported one, decides a trip end is at a location, and decides a tour came back to its primary destination. Reported locations are geocoded from what the respondent typed while trip ends are GPS, and people walk about a site, so it is a few hundred metres rather than a building's width. Set it to suit the survey's location noise. |
A block's keys may also sit flat under params:; see Configuring a step.
Determining tour purpose
A tour visits several places; the extractor picks which one is the tour
(home → work → lunch → work → home is a work tour). The purpose is chosen from
the non-final destinations, using their purpose and activity duration (time
spent before the next trip). Two methods, set by tour_purpose_method.
Hierarchy ("hierarchy")
Each purpose has a fixed priority rank by person category
(purpose_priority_by_personcat); the highest-ranked purpose present wins,
duration only breaks ties within a rank. Predictable, but priority is absolute:
a 2-minute work stop outranks a 4-hour shop.
Scoring ("score", default)
Duration and purpose trade off, so a long activity can outweigh a brief higher-priority one. Each candidate scores
score = W · x / (x + h)
x = activity duration (min); W = ceiling weight; h = half-saturation
duration (score = W/2 at x = h, rising toward W). Highest score wins.

W(purpose_score_weights) = the priority order, with escort demoted to the bottom. Only ratios matter, and it stays per person category (a work+school day resolves to work for a worker, school for a student).h(purpose_score_halfmax) = the median dwell for discretionary purposes; a fixed threshold for the rest.
| purpose | W |
h (min) |
h source |
|---|---|---|---|
| Work, School | 10 / 8 | 20 | fixed low threshold — a real visit is sticky; below it is a drive-by. Not the ~5 h median, which would demote short real visits. |
| Shop, Meal, Social, Errand | 4 | 18, 26, 56, 29 | BATS-2023 median dwell — duration picks the primary discretionary activity |
| Escort | 2 | 200 | fixed high — suppressed; only wins a pure drop-off-then-home tour |
| Overnight | 0 | — | never wins |
h must differ by purpose: with a shared h, scores become constant multiples
and duration cancels, collapsing back to the hierarchy.
The scoring-function figure is regenerated from the config defaults by
scripts/plot_purpose_scoring.py.
Effect vs the hierarchy
Switching BATS-2023 from hierarchy to scoring leaves work and school almost
unchanged (the sticky mandatory h), collapses escort (drop-off-then-activity
tours move to the real activity), and re-sorts discretionary tours by duration.

This figure is a BATS-2023 snapshot, regenerated from the committed counts in
docs/assets/data/ by scripts/plot_purpose_reclassification.py.
Code: processing.tours