Skip to content

Weight Hierarchy

The canonical weight hierarchy and the code that walks it.

hierarchy

The canonical weight hierarchy, declared once.

Every table that carries a weight is declared here exactly once -- its weight column, where the weight comes from, and the group within which that weight is conserved. Everything else is a projection of that list, so the shape of the hierarchy cannot drift between the propagation (processing.weighting.core.propagation), the supplied-weight path and the checksum.

Weights disaggregate down from households and aggregate back up into groupings; both directions are edges of one tree.

Table Weight Derivation
households hh_weight Anchor -- from the balancer, or supplied
persons person_weight Down from hh_weight via hh_id
days day_weight Split: person_weight / n_usable_days
unlinked unlinked_trip_weight Down from day_weight via day_id
linked linked_trip_weight Up: mean of member unlinked_trip_weight
tours tour_weight Up: mean of member linked_trip_weight
joint trips joint_trip_weight Up: sum of member linked_trip_weight
joint tours joint_tour_weight Up: sum of member tour_weight

Two units

Most weights count one row of their own table. The joint levels instead sum their members, so they count person-trips -- the travel of the whole party, not the joint occasion -- and are therefore an overlay on the member table, never a partition of it: sum(linked) + sum(joint) double counts.

To count occasions instead, divide by the number of members that carried weight -- counted from the member table, not read off the grouping::

events = joint_trip_weight / n_member_trips_with_weight

A party-size column is not that divisor: it counts travellers, including any the weighting excluded.

One column set per profile

A run may weight several usability profiles, each fitted to its own universe. The columns above are then suffixed with the profile's name, verbatim and unabridged (hh_weight_ctramp), so a column names the universe it expands and no rule is needed to read one back. Only [weight_col_for][processing.weighting.core .hierarchy.weight_col_for] and its siblings spell that suffix; Level.weight_col stays the base name, which is what keeps id_col derivable.

propagation

Walk the weight hierarchy, moving weights between canonical tables.

Used by both the compute_weights (computed) and add_existing_weights (supplied) steps. The shape being walked is declared in processing.weighting.core.hierarchy; this module only moves weights along it.

The distribution rule

A record is admitted by a usability flag -- one of the usability profiles stamped by the cascade_completeness step (see processing.completeness), so the weighted sample matches the records that profile's consumer actually keeps. Pass survey_complete instead to weight the whole valid survey, including partial and overnight tours.

Unusable records carry no weight, but their population does not vanish: it is carried by the usable records in the same scope. Give each record a claim equal to its parent's weight -- what it would carry if usable -- and the usable claims share the scope's whole claim:

w = claim x sum(claims in scope) / sum(claims of usable in scope)

Because every record in a scope is scaled by the same factor, the relative weights inside a household are untouched, and the conservation identity holds by construction:

sum(weight in scope) == sum(claim in scope)

Days: the average-day split

Days use a different DOWN rule (split=True): a person's usable days jointly represent that person once, not once each --

day_weight = person_weight / n_usable_days        (usable days; 0 otherwise)
sum(day_weight over a person's days) == person_weight

the vendor's own convention (their weighted days are exactly person_weight / n), with our usability flag deciding which days count. Summing day weights therefore yields persons on an average day, not person-days.

Conservation is strictly within the person -- never pooled across a household: pooling would siphon day-weight from members with fewer usable days toward members with more, silently distorting person-day totals while still passing any household-level checksum. A person with no usable day keeps their person weight (person weights stay calibrated to the person controls) but contributes nothing at the day level; that shortfall is reported by the checksum, never rescaled away. Unsurveyable persons (e.g. unrelated household members) carry no day rows at all, matching the vendor's day table.

Where a whole scope is unusable there is no denominator anywhere and the weight is genuinely stranded. That is reported as a shortfall, never silently rescaled.

Code: processing.weighting.core