Skip to content

Detect Habitual Locations

Habitual locations: find each person's anchors, and say which one a trip end is at.

A habitual location is an anchor: a place a person has a standing relationship with — their home, their workplace, their school — as opposed to somewhere they merely happened to stop. Two tables come out of here, because identity and presence have different existence conditions. A habitual location exists whether or not the person went there during the diary period — a reported workplace belonging to someone who teleworked all week is still their workplace — so it cannot live on a table keyed by day::

habitual_locations      one row per (person, kind, number)
                        who / where / how we know

habitual_location_days  one row per (location, day) the person was present
                        how long / how many visits / how the day related

For example::

habitual_locations
  person  type  num  is_primary  source
  1       WORK  1    True        reported
  1       WORK  2    False       observed   <- a Tue/Thu office
  2       HOME  1    True        reported
  2       HOME  2    False       reported   <- a second home

The table is delivered by survey cleaning holding the reported locations — the survey's home, work and school coordinates, plus any further ones such as a vendor's second home — built by :func:reported_habitual_locations and taken as given. The detect_habitual_locations step, after trip linking, appends the observed ones and writes the day table; it never changes a delivered row. Tour extraction and everything after only read it.

Observed locations:

  • Observed homes, only when include_observed_homes is on (it has no default): days the respondent said began or ended at home or at their other home, placed where that day's travel began or ended. Away from every reported home, such a place is another home of theirs. Off, the only homes are the reported ones.
  • Observed workplaces and schools: places the person went for that purpose and stayed long enough. Work-related stops count towards a workplace at their own longer cutoff: their median is under an hour, a meeting or a delivery, but a stay the length of a working day says the person works there.

Homes are never inferred from travel alone. A stay at "another residence" may be a second home or a friend's flat, and only the respondent can say which.

A single buffer (MatchConfig.buffer_meters) is the whole spatial vocabulary. Stops within it of one another are one place; a place within it of a reported location is that location, so it is not added again; and a trip end within it of a location is at that location when its purpose agrees with the location's kind (:func:matching.match_points). Only an agreeing purpose counts: an unknown one matches nothing. The exception is a person's own front door: within at_address_meters of their primary home, any purpose is at that home, because there the purpose describes what they were doing rather than a different place. Tour extraction asks the same question through :func:match_trip_ends, with the same settings.

Modules:

  • detect_habitual_locations: the pipeline step.
  • habitual_location_configs: the rules.
  • reported: the delivered table, built from the survey's coordinates.
  • episodes: trips as stays.
  • observed: clustering, and the observed locations.
  • numbering: primacy, numbering and identifiers.
  • matching: the one "at a location" test.
  • location_days: the per-day table.

The step

Find observed homes, workplaces and schools, and record presence per day.

Runs after trip linking. The table arrives from survey cleaning holding the reported locations (see reported_habitual_locations); this appends what the travel shows, and never changes a delivered row. See the package docstring for the rules.

Example

Example
- name: detect_habitual_locations
  params:
    buffer_meters: 300
    include_observed_homes: true

Settings

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.
include_observed_homes true / false required Whether to add homes found from where days were said to begin or end, beside the reported ones. An analyst's choice with no default: a 'home' answer away from the reported home becomes another home, and tours touching it are graded OTHER_HOME.
min_dwell_minutes number, ≥ 0 90.0 Minimum length of one stop for it to count as evidence of a workplace or school, unless its purpose has its own entry in min_dwell_minutes_by_purpose.
min_dwell_minutes_by_purpose mapping built in Per-purpose overrides of min_dwell_minutes. College is lower because a student may attend one class and leave. Work-related purposes are higher: their median stop is under an hour, a meeting or a delivery, and only a stay the length of a working day says the person works there.
min_distinct_days whole number, ≥ 1 1 Optional minimum number of distinct travel days an observed location was visited. Defaults to 1 (no filter).

Delivering the reported locations

Survey cleaning builds the delivered table with this helper.

Reported locations: the survey's own coordinates, as the delivered table.

habitual_locations is delivered by survey cleaning holding only the reported locations, and :func:reported_habitual_locations is how every project builds it, so the rows and their numbering are the same whoever delivers them.

reported_habitual_locations

reported_habitual_locations(
    households: pl.DataFrame,
    persons: pl.DataFrame,
    extra: pl.DataFrame | None = None,
    buffer_meters: float = DEFAULT_BUFFER_METERS,
) -> pl.DataFrame

Build the delivered habitual_locations table: every reported location.

The survey's home (household), work and school (person) coordinates are each person's primaries. Any further reported locations — a vendor's second home — come in extra and follow them, in the order given.

Parameters:

Name Type Description Default
households pl.DataFrame

Households with hh_id, home_lat and home_lon.

required
persons pl.DataFrame

Persons with person_id, hh_id, work_lat/lon and school_lat/lon.

required
extra pl.DataFrame | None

Optional further reported locations with person_id, location_type, lat and lon. They are never primary.

None
buffer_meters float

An extra location this close to its kind's primary is the same place and is dropped.

DEFAULT_BUFFER_METERS

Returns:

Type Description
pl.DataFrame

Table conforming to HabitualLocationModel, all REPORTED.

Code: processing.habitual_locations