processing.completeness
Canonical completeness and model-usability logic.
Two kinds of flag roll up from two atomic facts -- whether each day was
surveyed (days.survey_complete, which the project cleaner sets from the day's
surveyed trips or a declared no-travel day), and each tour's structural validity:
survey_complete-- was it reported? Rolls up from complete days. One answer per run, never narrowed by model criteria and never configurable: if it is wrong, the fix belongs upstream in the project cleaner.- one column per usability profile -- can the model consume it? Fuses each tour's structure with its household-day coherence, then gates the rest.
A profile states a standard on four axes -- which home has to close a tour, what the household-date has to show, which zone system has to be able to address the record, and what a joint tour has to be -- and a run may stamp several, so different consumers can hold different standards. A joint-tour model needs whole households and fully joint tours; a trip-level estimation does not, and needs no model geography at all.
Every profile is named in config and answers every axis, so a column's meaning
reads off the config without knowing a base rule, and no verdict appears that
nobody asked for. Consumers name the profiles they read (a formatter's
usability_profile, the weighting's weight_profiles), and survey_complete
is always available as the floor
beneath all of them.
Each flag comes from one place in the tree -- its direction -- via a counting
operation (the op column below). Naming the direction makes it clear that
household-day and the joint entities are the same kind of thing: cross-person
groupings, not parent/child.
| direction | value comes from | example |
|---|---|---|
| self | measured on the record | a trip never filled out |
| up | aggregate your own children | person with zero complete days |
| down | inherit your parent's verdict | good trip on a dropped tour |
| lateral | aggregate a cross-person group | one member skips a day, whole date fails |
The flow, and the rule on each line:
COMPLETE -- rolls up from complete days op rule
------------------------------------------------------------------------
person-day ........................... direct days.survey_complete, from the cleaner
(all trips surveyed, or no-travel)
├ trip / tour ....................... inherit its own value AND its day's
├ person ............................ >=1 has >=1 complete day
â”” household-day ..................... ALL all surveyable members complete that date
â”” household ...................... >=1 has >=1 complete household-day
USABLE, UP (_cascade_up) -- counts from the tour fuse op rule
------------------------------------------------------------------------
household ........................... >=1 has >=1 usable household-day
├ household-day .................... ALL all surveyable members' days usable that date
â”” person ........................... >=1 has >=1 usable day
â”” day ........................... >=1 >=1 usable tour (or a no-travel day)
â”” tour ....................... fuse complete AND admitted-quality AND
hh-day complete AND has a zone
(the last three are the axes)
USABLE, DOWN (_cascade_down) -- inherits from the household op rule
------------------------------------------------------------------------
household -> person -> day -> tour .. inherit unusable if the one it belongs to is
â”” linked trip / unlinked trip ...... inherit takes its tour's verdict
JOINT (flag_joint) -- judged beside the chain, after it op rule
------------------------------------------------------------------------
joint tour .......................... >=2 >=2 tours on it, and its
household usable
joint trip .......................... >=2 >=2 linked trips on it, likewise;
judged again after rejected joint
tours void their ids
JOINT IDS -- joint_tour_id_<profile> on tours and both trip tables, and
joint_trip_id_<profile> on linked trips: the bare id where the record is on it,
null where it is not. Decided first within flag_joint, read by the groupings
above, the weighting and the formatters; the bare id is the survey's own
answer and is never edited.
------------------------------------------------------------------------
partial ............................. direct usable and in a survey-detected group
fully_joint ......................... class usable, not work/school/escort, and
made the same joint trips as the
others (largest class of identical
legs); then the tour needs >=2 such
people and one purpose, or everyone
on it travels alone
declared no-travel: num_reasons_no_travel >= 1, OR proxy_complete (a proxy filled the day in --
this is how children, who file no trips themselves, still get a complete day).
surveyable: persons whose travel the survey could collect at all. Unsurveyable persons
(unrelated members, e.g. roommates) have no day rows in the vendor data; where a source
carries any, they neither veto the household-day ALL reductions nor inherit their verdict.
VALID feeds the fuse: trips -> linked trips -> tour, home-to-home, no missing legs.
op: ALL / >=1 / >=2 = quantity gate (count members vs threshold);
direct = measured; inherit = take a neighbour's verdict; fuse = AND of conditions
Because each level reads its neighbours, the derivation order is load-bearing; getting it wrong fails loudly (an unflagged member table raises), never silently.
This package is the one place the logic lives. The cascade_completeness
pipeline step runs :func:cascade_complete once and then :func:stamp_usable
per profile; every downstream consumer only reads the resulting flags.
Modules:
profiles: what a profile is, and how config states one.survey_complete: the reporting cascade, once per run.household_day: the household-date reduction, for both kinds of flag.zone_coverage: whether a record's locations have a zone.usable_tours: the tour fuse, where a verdict is decided.usable_cascade: that verdict, walked up and then down every table.joint_groupings: which records are on a joint tour or joint trip, per profile.kept_records: which records survive a verdict once references are restored.descriptions: what each stamped column means, in words.cascade_completeness: the pipeline step.
processing.completeness.cascade_completeness
The pipeline step: reporting completeness once, then one pass per profile.
cascade_completeness
cascade_completeness(
households: pl.DataFrame | None = None,
persons: pl.DataFrame | None = None,
days: pl.DataFrame | None = None,
unlinked_trips: pl.DataFrame | None = None,
linked_trips: pl.DataFrame | None = None,
joint_trips: pl.DataFrame | None = None,
tours: pl.DataFrame | None = None,
joint_tours: pl.DataFrame | None = None,
usability_profiles: dict[str, dict[str, str]] | None = None,
canonical_data: CanonicalData | None = None,
) -> dict[str, pl.DataFrame]
Cascade survey_complete through the hierarchy and stamp one column per profile.
Walks both kinds of flag across every table so they are internally consistent with whatever was set upstream (the vendor's day-level completeness, any manual adjustments in the project cleaner, and the structural verdicts from tour extraction):
survey_complete-- survey reporting completeness, cascaded hh <-> person <-> day <-> trip/tour. Never narrowed by model criteria, and never configurable: if it is wrong the fix belongs upstream in the cleaner.- one column per usability profile -- the subset of
survey_completethat profile admits.
A profile states its standard on four axes: which home has to close a tour, what the household-date has to show, which zone system must hold every location, and what a joint tour has to be. Several can be stamped in one run so different consumers can hold different standards -- a joint-tour model needs whole households and fully joint tours, a trip-level estimation does not:
usability_profiles:
ctramp:
tour_closes_at: primary_home
household_day_needs: all_members
zone_coverage: taz
joint_tours: fully_joint
analysis:
tour_closes_at: anywhere
household_day_needs: nothing
zone_coverage: none
joint_tours: partial
Nothing is implicit: every column stamped is named in config and every
profile answers every axis. Consumers then choose which to honour by name
(weight_profiles in the weighting, usability_profile in each
formatter), and none of
them re-derive completeness or tour validity -- the formatters read the
column and raise if it is absent. survey_complete is always available as the
floor without being declared.
See stamp_usable for the per-level
rules and parse_usability_profiles for the vocabulary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
households
|
pl.DataFrame | None
|
Canonical households. |
None
|
persons
|
pl.DataFrame | None
|
Canonical persons. |
None
|
days
|
pl.DataFrame | None
|
Canonical person-days. |
None
|
unlinked_trips
|
pl.DataFrame | None
|
Canonical unlinked trips. |
None
|
linked_trips
|
pl.DataFrame | None
|
Canonical linked trips. |
None
|
joint_trips
|
pl.DataFrame | None
|
Aggregated joint trips. |
None
|
tours
|
pl.DataFrame | None
|
Canonical tours (with |
None
|
joint_tours
|
pl.DataFrame | None
|
Aggregated joint tours. |
None
|
usability_profiles
|
dict[str, dict[str, str]] | None
|
Profile name -> |
None
|
canonical_data
|
CanonicalData | None
|
Injected by the pipeline. Profile columns are named from config and so cannot be model fields; they are registered here with a description of what each admits, which keeps them in the delivered output and documented rather than silently dropped. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, pl.DataFrame]
|
The provided tables, each with |
dict[str, pl.DataFrame]
|
per profile added. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
processing.completeness.descriptions
What each stamped column means, in words -- for the log and for the writer.
A profile's columns are named in config, so they cannot be model fields. They are described here instead: once into the run log, so an analyst can see what a gate admitted, and once onto the canonical data, so the delivered output carries the same sentence.
suggest_usability_columns
suggest_usability_columns(frame: pl.DataFrame) -> str
A "did you mean" line naming the boolean columns frame actually has.
For a consumer that was pointed at a usability column which is not there. The names come from config, so there is no pattern to match on: a rule like "ends in _usable" only holds until a project picks a name that does not fit it, and then the hint reports none rather than admitting it cannot tell. Every boolean column is offered instead. Some will be unrelated, which costs a reader one glance; a confident empty answer costs them a debugging session in the wrong step.
processing.completeness.household_day
The household-date reduction, for both kinds of flag.
A household-day is the set of person-days sharing one hh_id and
travel_date. Both reductions here are the same shape -- ALL surveyable
members' days pass, written back onto every day on that date -- and they sit
side by side so the usable one reads as the mirror of the complete one.
flag_household_day_complete
flag_household_day_complete(
tables: dict[str, pl.DataFrame | None],
) -> None
Stamp hh_day_survey_complete on the days table, in place (reverse cascade).
A household-day -- the set of person-days sharing one hh_id and
travel_date -- is complete only when every surveyable member's day is
complete (an ALL reduction over surveyable member-days). The result is
written back onto each day on that date, so hh_day_survey_complete marks
whether the day belongs to a coherently observed household-date.
Unsurveyable members (see :func:_join_surveyable) neither veto the date
nor borrow its verdict: their own day rows -- if a data source carries any
-- keep their own survey_complete, which is normally False since they file no
trips. A source that nonetheless marks one complete is believed rather than
overruled: the contradiction is upstream, and inventing a verdict here would
hide it.
This runs after :func:rollup_completeness, so survey_complete already
reflects ancestry; the reduction then flows the other way, up from members to
the shared date. Idempotent. Days without hh_id / travel_date (e.g.
schema-only fixtures) fall back to each day's own survey_complete.
flag_household_day_usable
flag_household_day_usable(
tables: dict[str, pl.DataFrame | None], cols: UsabilityProfile
) -> None
Stamp the household-day usable flag: ALL surveyable member-days usable, in place.
The usable-side mirror of :func:flag_household_day_complete: a household-day
is usable only when every surveyable member's day is model-usable, not
merely complete. household then needs at least one such date.
Unsurveyable members neither veto the date nor inherit its verdict.
Requires the per-record flag on days; days without hh_id /
travel_date fall back to each day's own verdict.
processing.completeness.joint_groupings
Which tours and trips are on a joint tour or joint trip, per profile.
joint_tour_id and joint_trip_id record the groups the survey detected,
once, for every profile. Whether a record is on its group is a profile's
call, so each profile gets its own copy of the id -- joint_tour_id_<profile>
on tours and both trip tables, joint_trip_id_<profile> on linked trips --
holding the same value where the record is on it and null where it is not.
Nothing here edits the bare id.
Runs after the up and down walks in
usable_cascade, since it reads
every table's verdict. The functions below are in the order they run.
MIN_JOINT_PARTICIPANTS
module-attribute
MIN_JOINT_PARTICIPANTS = 2
flag_joint
flag_joint(
tables: dict[str, pl.DataFrame | None], cols: UsabilityProfile
) -> None
Decide the joint tours and joint trips for cols, in place.
First which tour is on which joint tour, and which trip on which joint trip. Then the groupings are judged and the ids of any rejected grouping voided -- twice, in the only order the chain runs: rejecting a joint tour takes its tours' trips off their joint trips, which can empty a joint trip, so joint trips are judged once more. Nothing flows back up, which the final check states rather than assumes.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the second void changed anything, meaning a rule now flows from joint trips back to joint tours. |
processing.completeness.kept_records
Which records a usability profile admits, read off every table at once.
Nothing here removes a record from the canonical data. keep_records
returns filtered copies of the tables it is given -- the rows whose verdict
column is true -- and leaves the inputs as they were. Only a formatter ever
writes fewer records than the survey holds, and it writes exactly these.
Filtering each table on its own verdict is enough because the cascade already made the verdicts agree: it carries the household's verdict down to every record in it, voids the joint ids of any grouping it rejects, and dissolves any grouping left with one record. What this module adds is the proof. After filtering it follows every reference the schema declares and raises on the first one that no longer resolves, so a cascade that stops agreeing with itself fails here, loudly, rather than being quietly patched.
The formatters gate on this, and the weighting runs it as a check: weight on a record the filter excludes is weight the output would lose.
GATED_TABLES
module-attribute
GATED_TABLES = (
"households",
"persons",
"days",
"tours",
"linked_trips",
"unlinked_trips",
"joint_trips",
"joint_tours",
)
KeepLedger
dataclass
How many rows each table lost to the profile's verdict.
excluded
class-attribute
instance-attribute
excluded: dict[str, int] = field(default_factory=dict)
add
add(bucket: dict[str, int], key: str, n: int) -> None
Add n to key in bucket.
keep_records
keep_records(
tables: dict[str, pl.DataFrame], usability_flag_col: str
) -> tuple[dict[str, pl.DataFrame], KeepLedger]
Keep the records usability_flag_col admits, and prove nothing dangles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame]
|
Canonical tables, keyed by name, carrying the profile's joint ids under the bare names (the formatter resolves them first). |
required |
usability_flag_col
|
str
|
The stamped verdict to gate on. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, pl.DataFrame]
|
The kept tables, and a ledger of what each lost. Tables absent from the |
KeepLedger
|
input stay absent. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a gated table carries no verdict, which means
|
select_profile_columns
select_profile_columns(
tables: dict[str, pl.DataFrame],
usability_profile: str,
*,
only: tuple[str, ...] | None = None
) -> dict[str, pl.DataFrame]
Resolve this consumer's per-profile columns to their base names.
The weighting writes one weight column per profile, and the cascade one copy of each joint id per profile, each suffixed with the profile's name. A consumer already names the profile it reads, and the suffix is that name, so one setting answers every question and there is no second one to disagree with it.
Renaming here rather than at each use is what keeps every downstream read --
sampleRate, hhexpfac, which tours are joint -- correct without
knowing profiles exist at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame]
|
Canonical tables, keyed by name. |
required |
usability_profile
|
str
|
The profile this consumer reads. |
required |
only
|
tuple[str, ...] | None
|
Base columns to resolve, for a caller that wants the joint ids but not the weights; every column a profile writes, by default. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, pl.DataFrame]
|
The same mapping with this profile's columns under the base names. A |
dict[str, pl.DataFrame]
|
table with no suffixed column at all is returned untouched, which is the |
dict[str, pl.DataFrame]
|
single-weight-set case. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a table carries suffixed columns but none for this profile. The bare column would then be read instead -- whatever it happens to hold -- and a formatter would publish another universe's expansion factors or joint tours. |
check_kept_records_agree
check_kept_records_agree(
tables: dict[str, pl.DataFrame], usability_flag_col: str
) -> None
Raise if the kept records do not form a consistent set.
Two things, each once a repair this module made silently:
- Every reference the schema declares resolves. A required one that does not is a record whose household, person or day the profile rejected; an optional one is a joint id the profile should have voided.
- No joint id is carried by fewer than two records.
Each is something the cascade guarantees under every profile, so a failure
names a cascade bug. A rule that holds under some profiles only -- such as
every leg of a joint tour being a joint trip, which partial does not
promise -- belongs to the consumer that needs it, since this function
receives a column name and cannot know the profile's settings.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame]
|
The kept tables. |
required |
usability_flag_col
|
str
|
The verdict they were gated on, for the message. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
On the first check that fails. |
processing.completeness.profiles
What a usability profile is, and how config states one.
A profile answers four axes explicitly -- which home has to close a tour, what the household-date has to show, which zone system has to hold every one of the record's locations, and what a joint tour has to be. Nothing here touches a table; it is the vocabulary the rest of the package is written against.
NO_ZONE_COVERAGE
module-attribute
NO_ZONE_COVERAGE = 'none'
USABLE_FAMILY
module-attribute
USABLE_FAMILY = 'usable'
SURVEY_COMPLETE
module-attribute
SURVEY_COMPLETE = 'survey_complete'
TourClosesAt
Which home has to close a tour.
The quality codes divide into other-home anchors, open ends and missing data, and this axis walks down the first two. No setting admits missing data.
PRIMARY_HOME
class-attribute
instance-attribute
PRIMARY_HOME = 'primary_home'
ANY_HOME
class-attribute
instance-attribute
ANY_HOME = 'any_home'
ANYWHERE
class-attribute
instance-attribute
ANYWHERE = 'anywhere'
HouseholdDayNeeds
What the household-date has to show.
ALL_MEMBERS
class-attribute
instance-attribute
ALL_MEMBERS = 'all_members'
NOTHING
class-attribute
instance-attribute
NOTHING = 'nothing'
JointTours
What a joint tour has to be.
Activity-based models draw a joint tour's trips once for everyone on it, so a person who leaves for a leg is not on the same tour; the survey's own groups need no such thing.
FULLY_JOINT
class-attribute
instance-attribute
FULLY_JOINT = 'fully_joint'
PARTIAL
class-attribute
instance-attribute
PARTIAL = 'partial'
UsabilityProfile
dataclass
One usability standard, stated on every axis, and the columns it writes.
A profile says which home has to close a tour, what the household-date has to show, which zone system has to be able to address the record, and what a joint tour has to be. Config gives all four explicitly; none may be left implicit there.
Coverage is an axis rather than a universal because the zone systems differ: a trip outside one model's area may sit comfortably inside another's, so "has a zone" is a question only a named consumer can answer.
Two things hold at every setting and so are not axes. A profile is always a
subset of survey_complete -- a usability column admitting unreported records
would be a different flag wearing the name. And no profile admits a tour
with missing data (a missing leg, an activity that never happened), as
opposed to one that merely ends somewhere unexpected.
Two columns come out of a pass. flag is the per-record verdict consumers
read. household_day records, per date, whether all surveyable members'
days passed. It is computed for every profile even where that profile does
not gate on it, so the column means the same thing everywhere and can still
answer "which dates cost this household its usability".
name
instance-attribute
name: str
flag
property
flag: str
Per-record verdict column.
requires_zones
property
requires_zones: bool
Whether this profile asks that a record's locations have a zone.
household_day
property
household_day: str
All-surveyable-members-that-date column.
joint_tour_id
property
joint_tour_id: str
joint_tour_id as this profile sees it: the id where the tour is on it, else null.
joint_trip_id
property
joint_trip_id: str
joint_trip_id as this profile sees it: the id where the trip is on it, else null.
admitted_quality
property
admitted_quality: tuple[TourDataQuality, ...]
Tour quality codes this profile's closure setting admits.
needs_whole_household_day
property
needs_whole_household_day: bool
Whether a date must show every surveyable member's day complete.
requires_fully_joint
property
requires_fully_joint: bool
Whether everyone on a joint tour must share every leg of it.
usable_col_for
usable_col_for(profile: str) -> str
The column carrying profile's verdict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
str
|
A usability profile named in config, or
:data: |
required |
Returns:
| Type | Description |
|---|---|
str
|
|
str
|
is vendor data rather than a verdict this pipeline reached, so it is |
str
|
available under its own name but not a member of the family. |
profile_of
profile_of(usability_flag_col: str) -> str | None
The profile a verdict column belongs to, or None for one outside the family.
The inverse of :func:usable_col_for: usable_ctramp -> ctramp.
survey_complete, or a hand-written flag, names no profile, so a reader
looking for that profile's other columns (joint_tour_id_<profile>) gets
None and falls back to the bare column.
parse_usability_profiles
parse_usability_profiles(
spec: dict[str, dict[str, str]],
) -> list[UsabilityProfile]
Turn the configured profile block into profiles, or say why it cannot.
Every profile answers every axis. A missing axis is an error rather than a default, because a default is exactly the implicit meaning profiles exist to remove: the config should say what a column means without the reader knowing a base rule.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
dict[str, dict[str, str]]
|
Mapping of profile name to |
required |
Returns:
| Type | Description |
|---|---|
list[UsabilityProfile]
|
One profile per entry, in declaration order. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the block is empty, a name collides with a column the reporting cascade owns, or any axis is missing, unknown, or given a value outside its vocabulary. |
processing.completeness.survey_complete
The reporting cascade: what the survey collected, walked across the tables.
Survey reporting only. Nothing here consults a model criterion or reads a profile, and one run of the pipeline has exactly one answer, so this is independent of how many usability passes follow it.
rollup_completeness
rollup_completeness(tables: dict[str, pl.DataFrame | None]) -> None
Roll survey_complete UP from days, then broadcast each day's value down, in place.
Completeness is measured at the person-day (days.survey_complete is set
upstream by the project cleaner from surveyed trips / a declared no-travel
day). This derives the rest:
persons.survey_complete= it has at least one complete day (an ANY rollup).- every day-record (trip, tour) = its own reporting AND its day complete --
broadcast down, since a trip is no more complete than the day it sits in.
Joint trips and tours are not day-records; see
_DAY_RECORDS.
households.survey_complete is a household-day rollup and is set separately by
:func:rollup_household_complete (it needs hh_day_survey_complete first). Tables
lacking survey_complete or the join key are left unchanged. Idempotent.
rollup_household_complete
rollup_household_complete(
tables: dict[str, pl.DataFrame | None],
) -> None
Set households.survey_complete = has at least one complete household-day, in place.
A household is complete when at least one date was coherently observed (every
member complete). Requires hh_day_survey_complete on days (from
:func:flag_household_day_complete). Left unchanged if days or the flag is
absent.
cascade_complete
cascade_complete(tables: dict[str, pl.DataFrame | None]) -> None
Derive every survey_complete flag, in place.
Survey reporting only: what the vendor collected, cascaded through the hierarchy. Nothing here consults a model criterion, and one run of the pipeline has exactly one answer, so this is independent of how many usability passes follow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame | None]
|
Mutable dict of table_name -> DataFrame (or None). |
required |
processing.completeness.usable_cascade
One profile's verdict, walked across every table.
Reads the survey_complete flags the reporting cascade left behind and the
tour fuse's verdict, then walks the hierarchy twice -- up, counting from
the tour to the household, and down, inheriting from the household to the
trips -- and last decides the joint groupings beside it (see
joint_groupings). The functions
below are in the order they run.
stamp_usable
stamp_usable(
tables: dict[str, pl.DataFrame | None], cols: UsabilityProfile
) -> None
Derive one usability verdict for every table, in place.
Reads the survey_complete flags :func:cascade_complete left behind and writes
the columns cols names. It never writes survey_complete, so it can be
run more than once over the same tables to stamp several verdicts side by
side, each under its own name.
Three passes, in the order the package docstring draws them: count up from the tour fuse to the household, inherit down from the household to the trips, then decide the joint groupings beside them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame | None]
|
Mutable dict of table_name -> DataFrame (or None). |
required |
cols
|
UsabilityProfile
|
Column names this pass writes. |
required |
compute_usability
compute_usability(
tables: dict[str, pl.DataFrame | None],
profile: UsabilityProfile,
) -> None
Stamp survey_complete and one profile's verdict on every table, in place.
The two halves in order: reporting completeness once, then one usability
pass over it. A run stamping several profiles calls :func:cascade_complete
once and :func:stamp_usable per profile instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame | None]
|
Mutable dict of table_name -> DataFrame (or None). |
required |
profile
|
UsabilityProfile
|
The standard to apply, named on every axis. |
required |
processing.completeness.usable_tours
The tour fuse: the one place a usability verdict is decided rather than read.
Every other level counts or inherits. A tour is where reporting completeness, an admissible structure, household-date coherence and zone coverage are ANDed into a single verdict, and the rest of the cascade flows from it.
processing.completeness.zone_coverage
Whether a record's locations have a zone in the geography a profile names.
The zone step joins each location to a zone polygon and leaves it null when no polygon contains it. This asks the one question that follows: does every location this record needs have a zone in the named geography?
That is a fact about where the zone layer stops, not about how well the record was reported. A trip to a place outside the layer may be reported perfectly; a consumer that writes zone IDs simply has nothing to write for it.