processing.weighting.existing
processing.weighting.existing.config
Settings for add_existing_weights: each supplied weight file, and the step's own.
ExistingWeightConfig
pydantic-model
Configuration for a single weight file.
Most fields use sensible defaults - typically you only need to provide weight_path. Override weight_id_col only if your weight file uses different ID column names. Override weight_col only if your weight file uses different weight column names.
Examples:
Basic usage with defaults:
>>> ExistingWeightConfig(config_key="hh_weight", weight_path="hh_weights.csv")
# Uses: weight_id_col="hh_id", weight_col="hh_weight"
Override weight column name:
>>> ExistingWeightConfig(
... config_key="person_weight",
... weight_path="person_weights.csv",
... weight_col="final_weight" # Weight file uses "final_weight" not "person_weight"
... )
Override ID column name (external weight file with non-canonical IDs):
>>> ExistingWeightConfig(
... config_key="unlinked_trip_weight",
... weight_path="trip_weights.csv",
... weight_id_col="trip_id", # Weight file uses "trip_id" not "unlinked_trip_id"
... weight_col="trip_weight" # Weight file uses "trip_weight" not "unlinked_trip_weight"
... )
Attributes:
| Name | Type | Description |
|---|---|---|
config_key |
str
|
Key identifying the weight type (e.g., 'hh_weight') |
weight_path |
Path
|
Path to the CSV file containing weights |
weight_id_col |
str | None
|
ID column name in the weight file. Defaults to canonical table ID. |
weight_col |
str | None
|
Weight column name in the weight file. Defaults to canonical weight column. |
Fields:
-
config_key(str) -
weight_path(Path) -
weight_id_col(str | None) -
weight_col(str | None)
Validators:
config_key
pydantic-field
config_key: str
weight_path
pydantic-field
weight_path: Path
weight_id_col
pydantic-field
weight_id_col: str | None = None
Defaults to canonical table ID if not provided
weight_col
pydantic-field
weight_col: str | None = None
Defaults to canonical weight column if not provided
table_name
property
table_name: str
Get the table name for this weight config.
table_id_col
property
table_id_col: str
Get the canonical table ID column.
canonical_weight_col
property
canonical_weight_col: str
Get the canonical weight column name for this table.
validate_config_key
pydantic-validator
validate_config_key(v: str) -> str
Validate that config_key is one of the allowed values.
validate_path_exists
pydantic-validator
validate_path_exists(v: Path) -> Path
Validate that the weight file exists.
set_defaults_from_mapping
pydantic-validator
set_defaults_from_mapping() -> ExistingWeightConfig
Set default values for weight_id_col and weight_col from the mapping.
joined_weight_col
joined_weight_col(profile: str) -> str
The column the supplied weight lands in under profile.
validate_columns
validate_columns(
weight_df: pl.DataFrame, table_df: pl.DataFrame
) -> None
Validate that required columns exist in weight file and table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weight_df
|
pl.DataFrame
|
The weight DataFrame loaded from weight_path |
required |
table_df
|
pl.DataFrame
|
The survey data table to join weights to |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required columns are missing |
ExistingWeightsConfig
pydantic-model
Settings for add_existing_weights: how supplied weights move onto usable records.
Config:
extra:forbid
Fields:
-
preserve_travel_totals(bool) -
household_size_top_group(int) -
weight_profiles(list[str]) -
derive_missing_weights(bool) -
max_weight_factor(dict[str, float] | None)
model_config
class-attribute
instance-attribute
model_config = ConfigDict(extra='forbid')
preserve_travel_totals
pydantic-field
preserve_travel_totals: bool
Households are always held to their total and persons to their household, as known populations, and a person's days carry that person's weight. Every unlinked trip moves with its day, by the day's final weight over its supplied weight. Trips and tours are not targets, so whether a dropped trip's weight moves is a choice. true: a kept day's dropped trips hand their weight to its kept trips, in proportion, so the day's trips keep their total. false: every kept trip keeps its own weight times its day's factor, and a dropped one's weight is gone. Either way, linked trips and tours are the mean of their final trips, and joint trips and joint tours the sum of their kept members.
household_size_top_group
pydantic-field
household_size_top_group: int
Households are scaled by one factor per household size (number of persons), so dropping more large households than small ones does not move the person total. Sizes from this one up share a factor, which keeps a size with few households from carrying an extreme one.
weight_profiles
pydantic-field
weight_profiles: list[str]
Usability profiles to weight, each writing its own column set suffixed with the profile name; a single profile is a list of one. Each is stamped upstream by cascade_completeness; name survey_complete to weight the whole valid survey including partial and overnight tours. The supplied files are read once and moved onto each profile's usable records rather than simply zeroed, so every set holds the same household and person totals -- the vendor's anchor cannot be re-balanced from here.
derive_missing_weights
pydantic-field
derive_missing_weights: bool
Whether to derive weights for tables without supplied weight files.
max_weight_factor
pydantic-field
max_weight_factor: dict[str, float] | None = None
Optional limits, table -> the most a record's final weight may be, as a multiple of its own supplied weight. A table left out has no limit. The step raises naming the records past it. Each table named must have a supplied weight.
attach_supplied
attach_supplied(
tables: dict[str, pl.DataFrame | None],
supplied: dict[str, pl.DataFrame],
configs: dict[str, ExistingWeightConfig],
profile: str | None,
) -> dict[str, str]
Join each supplied weight under its column for profile, in place.
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
table -> the weight column that is actually there, which is what every |
dict[str, str]
|
later step reads the weight by. |
processing.weighting.existing.population
Supplied weights onto usable households, persons and days.
Households scale by one factor per household size; persons copy their household, a dropped person's share staying inside it; a person's days carry exactly that person's weight.
SCALE_TOL
module-attribute
SCALE_TOL = 1e-09
POPULATION_TABLES
module-attribute
POPULATION_TABLES = frozenset({'households', 'persons', 'days'})
CARRIES
module-attribute
CARRIES = '_can_carry_weight'
apply_usability
apply_usability(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
*,
usability_flag_col: str
) -> dict[str, float]
Redistribute supplied weights onto usable records, preserving each total.
Unlike the computed path, the vendor's anchor cannot be re-balanced: their
weights already sum to their population estimate, so dropping records must
not shrink that total. Each supplied weight is first conserved within its
declared scope (distribute_within_scope);
whatever that leaves stranded — scopes with no usable record at all — is
then closed with one table-wide factor. Modifies tables in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
dict[str, pl.DataFrame | None]
|
Mutable dict of table_name → DataFrame (or None). |
required |
has_weight
|
dict[str, str]
|
Dict of table_name → weight column name. |
required |
usability_flag_col
|
str
|
Boolean column deciding which records may carry weight
-- a usability profile stamped upstream, or |
required |
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
table -> the supplied total, for the placement check to hold each |
dict[str, float]
|
table to. |
mark_persons_who_can_carry
mark_persons_who_can_carry(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
supplied: Collection[str],
*,
usability_flag_col: str
) -> None
Add the CARRIES verdict to persons, in place.
Days take weight in proportion to the supplier's, so a day weighted 0 stays 0. A usable person whose supplier-weighted days were all dropped has no day to put their weight on, so they count as a dropped person: their weight goes to the household's other persons. A household with no person who can carry weight counts as a dropped household. Their own verdicts are untouched -- they stay usable, with weight 0.
Raises:
| Type | Description |
|---|---|
ValueError
|
If there is no persons table, or it carries no verdict. Household size is the number of persons, so there is no size without them. |
gate_households_by_size
gate_households_by_size(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
*,
usability_flag_col: str,
top_group: int
) -> float
Scale kept households by one factor per household size, in place.
Size is the number of persons; sizes from top_group up share one group. Within each group::
household = supplied x (group's supplied total / group's kept supplied total)
Because every household in a group has the same size, this holds the group's person total as well as its household total -- one factor for all sizes would not, when the dropped households are larger or smaller than the kept ones. A household is kept when it is usable and has a person who can carry weight.
Returns:
| Type | Description |
|---|---|
float
|
The supplied household total, for the placement check to hold. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a size group carries supplied weight but has no kept household to put it on. |
persons_follow_household
persons_follow_household(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
) -> None
Give each household's weight to its persons who can carry it, in place.
A household stands for all its persons, so its persons together carry its weight times its number of persons. Those who can carry share it in proportion to their supplied weights::
person = household x n_persons x supplied / sum(supplied of those who can carry)
so a dropped person's share stays in the household, and household weight x number of persons = the sum of its persons' weights.
follow_parent
follow_parent(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
table: str,
*,
usability_flag_col: str
) -> None
Share each parent's final weight among table's usable records, in place.
Each record takes its parent's weight in proportion to its own claim::
w = claim x parent_final / sum(usable claims under the parent)
so a parent's usable records sum to the parent, and a record the supplier weighted 0 stays 0. A parent whose final weight is positive but that has no usable claim under it is stranded: its weight reaches no record here, and is logged for the totals check to report.
processing.weighting.existing.travel
Supplied weights onto usable trips and tours, and the checks after redistribution.
A trip moves with its day; linked trips and tours are the mean of their final trips, and joint trips and joint tours the sum of their kept members.
TRAVEL_TABLES
module-attribute
TRAVEL_TABLES = frozenset(
{"unlinked_trips", "linked_trips", "tours"}
)
JOINT_TABLES
module-attribute
JOINT_TABLES = frozenset({'joint_trips', 'joint_tours'})
gate_travel
gate_travel(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
*,
usability_flag_col: str,
preserve_travel_totals: bool
) -> dict[str, float]
Gate the travel tables, each record starting from its pre-gate claim, in place.
Used only where a travel weight cannot follow its day: a linked trip or
tour weight supplied as a file, or unlinked trips supplied without day
weights. When days and unlinked trips are both supplied, the trips follow
their day ([trips_follow_their_day]) and this does not see them.
A claim is the weight a record would carry if every trip and tour were usable: the supplied weight, or the weight derived from its usable day.
With preserve_travel_totals, a dropped record's claim moves to the kept records on its day and anything left stranded closes region-wide, so each table keeps its claim total. Without it, a kept record keeps exactly its claim and a dropped one carries nothing.
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
table -> the claim total the placement check holds it to; empty when |
dict[str, float]
|
the totals are not preserved. |
check_weight_factors
check_weight_factors(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
supplied: dict[str, pl.DataFrame],
limits: dict[str, float],
) -> None
Raise if any record's weight grew past its limit times its supplied weight.
The factor is a record's final weight over its own supplied weight, so it counts everything that moved weight onto it: a household's size factor, a person taking a dropped member's share, a day taking a dropped day's.
log_usable_supplied_zero
log_usable_supplied_zero(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
*,
usability_flag_col: str
) -> None
Count the usable records the supplier gave no weight.
Supplied weights are used as given, so such a record stays at 0. The count is how far this profile's universe is wider than the supplier's.
trips_follow_their_day
trips_follow_their_day(
tables: dict[str, pl.DataFrame | None],
has_weight: dict[str, str],
supplied_days: pl.DataFrame,
*,
usability_flag_col: str,
preserve_travel_totals: bool
) -> None
Scale each unlinked trip by its day's factor, in place.
A day's factor is its final weight over its supplied weight, so a trip moves exactly as its day did -- a dropped day's weight went to the person's other days, and the trips on those days carry it::
w = claim x day_final / day_supplied
With preserve_travel_totals, a kept day's dropped trips also hand their claim to its kept trips, in proportion::
w x sum(claims on the day) / sum(usable claims on the day)
Raises:
| Type | Description |
|---|---|
ValueError
|
If a trip carries a supplied weight but its day was supplied 0 or not at all -- there is no factor to apply, and guessing one would invent the supplier's intent. |
warn_null_weights
warn_null_weights(
tables: dict[str, pl.DataFrame | None], profile: str | None
) -> None
Report weights that came out null -- a record no supplied total covered.