processing.weighting.diagnostics
Diagnostics sub-package — HTML report generation for weighting results.
Produces one self-contained interactive HTML report per run (Plotly + Jinja2, no external dependencies), covering every profile the run fitted. The crosswalk geometry, control totals and PUMS incidence are built once however many profiles are fitted -- and on a regional run that geometry is the great majority of the file -- so one document per run embeds them once rather than once per profile.
A sidebar outlines the document and holds the profile toggle. Every statistic is
defined, with its formula, in a Definitions section at the top (see
glossary), and abbreviated headers link to their entry. Each section shows a
one-line summary, with the longer guidance collapsed beneath it.
Run-level sections, always visible
- Profile Comparison — one row per fit: seed size, weight sums by level, ESS%, CV, max/median, MAPE and convergence. Two profiles matching exactly means two balancing runs produced one answer.
- Crosswalk Map — geographic crosswalk visualization, with every profile's seed count in each zone's tooltip. Boundaries are simplified to a few metres.
- Weight Cascade — per level, what each profile admitted and what carries weight, all profiles side by side; plus the redistribution each survivor absorbs, whether each weight set's lower levels were copied, redistributed or adjusted per record, the day-split identity, and control-geography coverage.
- Weight Set Comparer — any two weight sets compared at any level: this
run's fits against each other, or against weights supplied with the survey
and named in
diagnostics.compare_weights. Run-level because a comparison names two profiles. Shows a scatter with a reduced-major-axis fit, the median, p90 and maximum fold difference, rank correlation and R-squared, and the spread of ln(A/B) by decile. No verdict is drawn: a comparison is not a validation, and the section says so above the plot.
Per-profile sections, behind the profile toggle
- Fractional Seed Imputation — per-control null rate in the seed incidence
matrix and the quality of the PUMS-trained model that filled it. Shown only
when something was filled. This describes the seed handed to the balancer,
not the survey
imputationpipeline step. - Balancer Performance — per-zone convergence status, target fit (MAPE, P90, Max), CV and ESS%.
- Weight Quality — per-zone weight and expansion-factor statistics, with
violin plots of
final_weight / base_weight. - Expansion Factor Calibration — MAPE vs CV across a grid of
max_expansion_factorvalues. Enabled by settingexpansion_factor_gridin the weighting config. - Target Fit (% Error) — diverging bar charts per control category per zone, with PUMS replicate-weight whiskers.
- Unweighted Cell Counts (Data Sparsity) — seed counts per control category per zone.
Each per-profile heading names its profile, so a screenshot cropped out of the document still says which fit it describes.
Section numbers are assigned to the sections a run actually renders, so they run consecutively from 1 whether or not the optional ones are present -- the numbers above are a full run. A per-profile section is numbered once for the whole document rather than per pane: the number is an address into the report, so a profile that omits a section leaves a gap in its own pane rather than shifting every heading below it when the reader presses the toggle.
Configuration (YAML)
diagnostics:
output_path: "{{ output_dir }}/weighting_diagnostics.html"
compare_weights: # optional; weight sets supplied with the survey
vendor:
label: vendor
columns: {households: hh_weight, unlinked_trips: trip_weight}
When output_path is omitted the report is written to
<cache_dir>/diagnostics.html. One file per run, with no profile suffix.
processing.weighting.diagnostics.charts
Plotly chart builders for the diagnostics report.
fit_diverging_figure
fit_diverging_figure(fit: pl.DataFrame) -> go.Figure
Grid of horizontal diverging bar charts (% error, one panel per zone + overall).
Expects fit to contain a label column (added by
apply_fit_merges).
Null placeholder rows are rendered as invisible bars
so the y-axis remains consistent across panels.
When moe_pct is present (from PUMS replicate weights), horizontal
error bars show the sampling margin of error on each target.
violins_figure
violins_figure(weighted: pl.DataFrame) -> go.Figure
Violin plot of hh_weight by zone (log scale).
imputation_distribution_figure
imputation_distribution_figure(
seed_incidence: pl.DataFrame,
pums_incidence: pl.DataFrame,
target_names: list[str],
imputed_controls: list[str],
*,
pre_imputation: pl.DataFrame | None = None
) -> go.Figure
Stacked bar chart: observed + imputed contribution vs PUMS shares.
For each filled control the survey bar is split into two stacked segments — Observed (pre-imputation, unweighted) and Imputed (the RF-predicted fractional contribution), so the second segment shows how much of a category's mass is prediction rather than response. A separate PUMS bar provides the reference distribution.
When pre_imputation is None the chart falls back to a single
post-imputation bar (no stacking).
ef_tradeoff_figure
ef_tradeoff_figure(
grid_results: list, selected_ef: float
) -> go.Figure
Small-multiples chart: four stacked subplots sharing the x-axis.
Hovering at an EF value on any subplot shows aligned tooltips on
all four panels via hovermode="x unified" and spike lines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid_results
|
list
|
One entry per expansion-factor value with aggregate metrics. |
required |
selected_ef
|
float
|
The user's chosen |
required |
processing.weighting.diagnostics.comparison
Pairwise comparison of weight sets, at every level of the hierarchy.
A weight set is one complete assignment of weights across the hierarchy: a profile this run fitted, or a set that arrived with the survey and is being used as a reference. Any two of them can be compared at any level where both carry a column.
What a comparison can and cannot say
Weights from two methods are not expected to agree record by record. Balancing matches marginal totals, and many different weight vectors satisfy the same margins equally well, so two methods can both be correct and still disagree on nearly every household. Low correlation is not evidence that either is wrong, and high correlation is not evidence that either is right. Nothing here is a validation, and the report says so above the plot rather than in a footnote.
What the comparison can show is how far apart two sets are, whether they agree on which records are heavy, and whether the gap is even across the range of weights or widens at one end. The report shows those numbers and leaves the judgement to the analyst.
The fit line
Least squares is the wrong fit for two weight sets. It treats one set as known and the other as noisy, so ln(A) on ln(B) and ln(B) on ln(A) give slopes that are not reciprocals, and a weak correlation drags both toward zero. On bats_2023 the household fit of our weights on the vendor's gave 0.42, the reverse fit implied 1.74, and the spreads actually differ by a factor of 0.86. The reduced major axis is symmetric and does not dilute: its slope is the ratio of the two spreads, signed by the correlation.
MIN_OVERLAP
module-attribute
MIN_OVERLAP = 30
N_DECILES
module-attribute
N_DECILES = 10
MAX_POINTS
module-attribute
MAX_POINTS = 700
SAMPLE_SEED
module-attribute
SAMPLE_SEED = 20260915
AGREEMENT_TOL
module-attribute
AGREEMENT_TOL = 0.01
WeightSet
dataclass
One complete assignment of weights across the hierarchy.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Stable key, used in markup and to address the set in the picker. |
label |
str
|
How the set is named to a reader. |
columns |
Mapping[str, str]
|
Weight column per table. A set need not reach every level. |
fitted |
bool
|
Whether this run produced it, as against it arriving with the survey. A comparison between two fitted sets holds the method constant and varies the universe; a comparison against a supplied set varies everything, and the two read differently. |
name
instance-attribute
name: str
label
instance-attribute
label: str
columns
instance-attribute
columns: Mapping[str, str]
fitted
instance-attribute
fitted: bool
Decile
dataclass
One decile of B, describing the ratio within it.
Attributes:
| Name | Type | Description |
|---|---|---|
x_mid |
float
|
Mean of log(B) over the decile, where the marker is drawn. |
median |
float
|
Median of log(A/B) -- where the band sits. |
iqr |
float
|
Interquartile range of log(A/B) -- how wide the band is at this weight level. |
x_mid
instance-attribute
x_mid: float
median
instance-attribute
median: float
iqr
instance-attribute
iqr: float
PairStats
dataclass
Two weight sets compared at one level.
Every summary is symmetric in the two sets, so it reads the same whichever is called A. Only the decile strip depends on direction, since it bins on one set, so it is held both ways.
Attributes:
| Name | Type | Description |
|---|---|---|
level |
str
|
Table the comparison was made on. |
a |
str
|
Name of the first set. |
b |
str
|
Name of the second set. |
n |
int
|
Records carrying a non-zero weight in both. |
median_fold |
float
|
Median over records of max(A/B, B/A). |
p90_fold |
float
|
90th percentile of the same. |
max_fold |
float
|
Its maximum. |
rank_corr |
float
|
Spearman correlation of the two weights. |
r2 |
float
|
Squared Pearson correlation of their logs. |
slope |
float
|
Reduced-major-axis slope of ln(A) against ln(B). The reverse fit is exactly its reciprocal. |
intercept |
float
|
Where that line crosses ln(B) = 0. |
deciles_ab |
list[Decile]
|
ln(A/B) within each decile of B. |
deciles_ba |
list[Decile]
|
ln(B/A) within each decile of A. |
points |
list[tuple[float, float]]
|
Sample of |
level
instance-attribute
level: str
a
instance-attribute
a: str
b
instance-attribute
b: str
n
instance-attribute
n: int
median_fold
instance-attribute
median_fold: float
p90_fold
instance-attribute
p90_fold: float
max_fold
instance-attribute
max_fold: float
rank_corr
instance-attribute
rank_corr: float
r2
instance-attribute
r2: float
slope
instance-attribute
slope: float
intercept
instance-attribute
intercept: float
points
class-attribute
instance-attribute
points: list[tuple[float, float]] = field(default_factory=list)
Comparison
dataclass
Every weight set a run could compare, and every pair that came of it.
Held together because the pairs name sets by key and are unreadable without them: the sets carry the labels, the ordering the picker offers, and which side of a pair was fitted here as against supplied.
Attributes:
| Name | Type | Description |
|---|---|---|
sets |
list[WeightSet]
|
The weight sets considered, in the order to offer them. |
pairs |
list[PairStats]
|
One entry per (level, pair) that had enough overlap to report. |
Inheritance
dataclass
How a level's weight descends from its parent, for one weight set.
Attributes:
| Name | Type | Description |
|---|---|---|
level |
str
|
The child table. |
parent |
str
|
The table its weight descends from. |
set_name |
str
|
Which weight set this describes. |
n |
int
|
Records carrying a weight at both levels. |
n_close |
int
|
Records whose weight matches their parent's to within
|
n_scopes |
int
|
Parent records with at least one weighted child. |
n_scopes_varying |
int
|
Parent records whose children do not all stand in the same ratio to it. Zero under any rule that scales a whole scope at once; non-zero only if something touched children individually. |
max_scope_spread |
float
|
Largest ratio between the highest and lowest factor within a single parent. 1.0 when every scope is internally uniform. |
level
instance-attribute
level: str
parent
instance-attribute
parent: str
set_name
instance-attribute
set_name: str
n
instance-attribute
n: int
n_close
instance-attribute
n_close: int
n_scopes
instance-attribute
n_scopes: int
n_scopes_varying
instance-attribute
n_scopes_varying: int
max_scope_spread
instance-attribute
max_scope_spread: float
share
property
share: float
Percentage of records inheriting the parent weight unchanged.
mode
property
mode: str
Which of the three mechanisms produced this level's weights.
copied -- every child carries its parent's weight unchanged, so the level adds
nothing of its own and comparing two sets here restates the level above.
redistributed -- children depart from the parent, but every child in
a scope departs by the same factor. That is conservation: the admitted
records absorb the share of any sibling the gate removed, and the level
stays a faithful expansion of its parent.
adjusted -- children in the same scope carry different factors, so
something rescaled records individually after the weights descended. The
level no longer nests inside its parent: two children of one parent now
claim to represent different amounts of it.
fitted_weight_sets
fitted_weight_sets(
profiles: Sequence[str | None],
) -> list[WeightSet]
One weight set per profile this run fitted.
Columns come from the hierarchy rather than from a pattern over the table's names: the suffix a profile's column carries is spelled in exactly one place and this is not it.
reference_weight_sets
reference_weight_sets(
spec: Mapping[str, Mapping] | None,
) -> list[WeightSet]
Weight sets that arrived with the survey, as named in the config.
Named rather than discovered, because neither half of a supplied column's
name is reliable. A vendor suffix is indistinguishable from a profile suffix
-- hh_weight_rmove_only parses as a profile called rmove_only that
was never fitted -- and a supplied column may not share the level's base name
at all, as the vendor's trip_weight does not match the hierarchy's
unlinked_trip_weight. An un-suffixed column is worse still: it may be the
vendor's, or it may be what a previous run of this pipeline left behind,
which is the ambiguity drop_unsuffixed_weights exists to remove. Only the
person configuring the run knows which, so they say.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
Mapping[str, Mapping] | None
|
|
required |
Returns:
| Type | Description |
|---|---|
list[WeightSet]
|
One set per entry, in the order given. |
compare_pair
compare_pair(
df: pl.DataFrame,
level: Level,
set_a: WeightSet,
set_b: WeightSet,
) -> PairStats | None
Compare two weight sets on one table, or None if there is too little to say.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
pl.DataFrame
|
The table both sets carry a column on. |
required |
level
|
Level
|
The hierarchy level that table belongs to. |
required |
set_a
|
WeightSet
|
First weight set. |
required |
set_b
|
WeightSet
|
Second weight set. |
required |
Returns:
| Type | Description |
|---|---|
PairStats | None
|
The pair's statistics, or |
PairStats | None
|
fewer than |
all_pairs
all_pairs(
tables: Mapping[str, pl.DataFrame | None],
sets: Sequence[WeightSet],
) -> list[PairStats]
Every comparable pair of weight sets, at every level both reach.
Computed exhaustively rather than on demand. A pair is a few numbers over a column already in memory, so precomputing all of them costs little and buys a picker where choosing a new set on one side leaves the other side alone -- which a lazily-computed subset cannot offer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
Mapping[str, pl.DataFrame | None]
|
Canonical tables by name, after propagation. |
required |
sets
|
Sequence[WeightSet]
|
The weight sets to compare, in the order they should be offered. |
required |
Returns:
| Type | Description |
|---|---|
list[PairStats]
|
One entry per (level, pair), in hierarchy order then pair order. Empty |
list[PairStats]
|
when fewer than two sets reach any level in common. |
inheritance
inheritance(
tables: Mapping[str, pl.DataFrame | None],
sets: Sequence[WeightSet],
) -> list[Inheritance]
Per set and copy-down level, how the weight descends from the parent.
Distinguishes conservation from post-hoc adjustment, which look alike in any summary that only counts departures from the parent weight. Both leave most children unequal to their parent; only one leaves the level coherent.
The discriminator is whether the child-to-parent ratio is constant within each parent. Conservation rescales a whole scope by one factor -- every usable sibling absorbs the same share of what the gate removed -- so the ratio cannot vary between siblings. A factor applied to individual records can, and does: measured on bats_2023, the vendor's trip weights vary within 1,102 of 25,312 days, by up to exactly 2x, while every fitted profile and the vendor's own rMove weights vary within none.
Split levels are excluded: days divide their person's weight rather than copying it, so sibling ratios differ there by design.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tables
|
Mapping[str, pl.DataFrame | None]
|
Canonical tables by name, after propagation. |
required |
sets
|
Sequence[WeightSet]
|
The weight sets to describe. |
required |
Returns:
| Type | Description |
|---|---|
list[Inheritance]
|
One entry per (level, set) that could be measured, in hierarchy order. |
payload
payload(comparison: Comparison) -> dict
Everything the comparer needs, as one JSON-serialisable object.
Embedded whole and rendered in the browser rather than baked into one figure per pair: forty static plots would multiply the document for a reader who looks at two or three of them, and a picker that can only offer what was pre-rendered cannot let one side of the comparison move on its own.
Only the sets that actually appear in a pair are listed, so the picker never offers a choice that resolves to nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
comparison
|
Comparison
|
The run's weight sets and the pairs computed from them. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
dict
|
nothing was comparable. |
processing.weighting.diagnostics.crosswalk_map
The crosswalk map: how PUMAs overlap the target zones, for the weighting report.
crosswalk_figure
crosswalk_figure(
puma_gdf: gpd.GeoDataFrame,
target_gdf: gpd.GeoDataFrame,
crosswalk_df: pl.DataFrame,
seeds: Mapping[str | None, pl.DataFrame] | None = None,
zone_groups: dict[str, list[str]] | None = None,
simplify_tolerance_m: float = _MAP_SIMPLIFY_M,
) -> go.Figure
Build an interactive Plotly map of the crosswalk.
Layers: - PUMA boundaries (dashed grey) — full extent - Study area outline (bold black) - Target zones (solid border, transparent fill) with tooltip showing PUMA allocation weights from the crosswalk.
The geometry is run-level: it comes from the crosswalk, which is built once however many profiles are fitted, so the map is drawn once and every profile's per-zone seed count is listed in the same tooltip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
puma_gdf
|
gpd.GeoDataFrame
|
PUMA boundary polygons (must have |
required |
target_gdf
|
gpd.GeoDataFrame
|
Target zone polygons (must have |
required |
crosswalk_df
|
pl.DataFrame
|
Crosswalk table with |
required |
seeds
|
Mapping[str | None, pl.DataFrame] | None
|
Each profile's seed, keyed by profile name (must contain
|
None
|
zone_groups
|
dict[str, list[str]] | None
|
Optional zone group mapping. When provided, grouped zones share a fill colour and labels include the group name. |
None
|
simplify_tolerance_m
|
float
|
Boundary simplification tolerance in metres. Zero keeps full TIGER precision. |
_MAP_SIMPLIFY_M
|
Returns:
| Type | Description |
|---|---|
go.Figure
|
go.Figure |
processing.weighting.diagnostics.data
Data transformations for the diagnostics report.
REDISTRIBUTION_TAIL
module-attribute
REDISTRIBUTION_TAIL = 1.5
ProfileSummary
dataclass
One fit, reduced to the figures that compare it against the others.
profile
instance-attribute
profile: str | None
seed_households
instance-attribute
seed_households: int
weight_sums
instance-attribute
weight_sums: dict[str, float]
ess_pct
instance-attribute
ess_pct: float
cv
instance-attribute
cv: float
max_over_median
instance-attribute
max_over_median: float
mape
instance-attribute
mape: float
zones_converged
instance-attribute
zones_converged: int
zones_total
instance-attribute
zones_total: int
CascadeRow
dataclass
One level's weight coverage under one profile.
rows splits three ways and the parts are exhaustive: a record is
rejected by the profile's own flag, or admitted and carrying weight, or
admitted and carrying none. Only the last needs explaining, and what it
means depends on the level -- for the anchor it is a household the control
geography could not place, and below it, a record whose parent was dropped.
table
instance-attribute
table: str
rows
instance-attribute
rows: int
usable
instance-attribute
usable: int
weighted
instance-attribute
weighted: int
gated
property
gated: int
Records the profile's own flag rejected.
unweighted
property
unweighted: int
Records the profile admits that still carry no weight.
RedistributionRow
dataclass
How far one level's weights were scaled to cover unusable siblings.
The ratio is a child's weight over its parent's. Under the copy-and-conserve rule every child in a scope is scaled by the same factor, so the ratio is that factor: 1.0 where every sibling was usable, and rising as the survivors take on the share of those that were not.
table
instance-attribute
table: str
parent
instance-attribute
parent: str
p50
instance-attribute
p50: float
p90
instance-attribute
p90: float
p99
instance-attribute
p99: float
maximum
instance-attribute
maximum: float
share_above
instance-attribute
share_above: float
SplitIdentityRow
dataclass
Whether a split level's children still sum to what their parent carries.
Arithmetic the pipeline controls, so a residual past floating-point noise is
a bug rather than a finding. stranded_parents is the exception it cannot
fix: a parent carrying weight with no child rows at all has nothing to
represent it below, and that weight is deliberately left unrepresented
rather than pooled onto other parents.
table
instance-attribute
table: str
parent
instance-attribute
parent: str
parents_checked
instance-attribute
parents_checked: int
max_residual
instance-attribute
max_residual: float
stranded_parents
instance-attribute
stranded_parents: int
stranded_weight
instance-attribute
stranded_weight: float
min_children
instance-attribute
min_children: int
median_children
instance-attribute
median_children: int
max_children
instance-attribute
max_children: int
category_label_map
category_label_map(
target_names: list[str], merges: list[MergeSpec] | None = None
) -> dict[tuple[str, str], str]
Map (control_name, category_str) to a human-readable label.
Categories are string member names (e.g. "size_1"). Merged
categories (e.g. "size_4_plus") get a title-cased label from
their merge spec.
merge_control_moe
merge_control_moe(
control_moe: pl.DataFrame, merges: list[MergeSpec] | None
) -> pl.DataFrame
Apply category merges to the MOE table so it matches post-merge fit categories.
For each merge spec, constituent category rows are combined: SE_merged = sqrt(Σ SE_i²), target_merged = Σ target_i, then moe_pct_merged = SE_merged / target_merged * 100.
Global merges (zones=None) replace originals for all zones.
Zone-specific merges replace originals only for the listed zones.
apply_fit_merges
apply_fit_merges(
fit: pl.DataFrame, merges: list | None, target_names: list[str]
) -> pl.DataFrame
Add human-readable label column to the fit table.
With category merges already applied at the data level (both incidence tables and control totals), the fit table already reflects the correct merged/unmerged categories per zone. This function only adds labels and pads missing rows.
zone_fit_summary
zone_fit_summary(
fit: pl.DataFrame, target_names: list[str]
) -> pl.DataFrame
Per-zone summary: HH/Person pop target & weighted, %Err, MAPE.
Population totals are derived by summing categories of one representative control at each level (any control's categories partition the population).
Returns columns: geo_id, hh_target, hh_weighted, hh_pct_err, per_target, per_weighted, per_pct_err, mape.
compute_weighted_totals
compute_weighted_totals(
seed: pl.DataFrame,
weights: pl.DataFrame,
target_names: list[str],
) -> pl.DataFrame
Weighted totals per (geo_id, control_name, category).
Uses uniform column handling for all controls:
- Structural controls: unpivoted column (e.g., h_total)
- Non-structural controls: pivoted columns (e.g., h_size__size_1)
fit_table
fit_table(
control_totals: ControlTotals, weighted_totals: pl.DataFrame
) -> pl.DataFrame
Join targets to weighted totals; add diff and diff_pct columns.
profile_summary
profile_summary(
fit: ProfileFit,
tables: dict[str, pl.DataFrame | None],
fit_table_df: pl.DataFrame,
) -> ProfileSummary
Reduce one completed fit to a comparison row.
Every figure is read off the fit's own rows -- the weight sums from each level's own column, never an ancestor's -- so a level that failed to receive a weight shows as a smaller sum rather than inheriting its parent's.
weight_cascade
weight_cascade(
tables: dict[str, pl.DataFrame | None],
*,
profile: str | None,
usability_flag_col: str
) -> list[CascadeRow]
Count, per level, what the profile admitted and what carries weight.
Walks HIERARCHY rather
than a list of table names, and reads each level's weight column through
weight_col_for -- the same declarations
propagate_weights
walks, so this cannot describe a rule the propagation does not follow. No
level's count is inferred from its parent's; each is read off its own rows.
A level whose table is absent, or which carries neither the flag nor this profile's weight column, is skipped rather than reported as empty.
redistribution
redistribution(
tables: dict[str, pl.DataFrame | None],
*,
profile: str | None,
usability_flag_col: str
) -> list[RedistributionRow]
Summarise the child/parent weight ratio on every copy-and-conserve edge.
Split edges (days) are excluded: their children divide the parent's weight
rather than copying it, so a ratio below 1 is the rule working, not a record
standing in for its siblings. Those are described by
split_identity.
split_identity
split_identity(
tables: dict[str, pl.DataFrame | None], *, profile: str | None
) -> list[SplitIdentityRow]
Verify sum(child weight) == parent weight on every split edge.
processing.weighting.diagnostics.glossary
Definitions of every statistic the diagnostics report uses.
One source for both places a definition appears: the Definitions section at the top of the report, and the hover text on a column header or tile that uses the statistic. A header links to its entry, so the hover is a shortcut to the reference rather than the only place the definition lives.
WEIGHTS
module-attribute
WEIGHTS = 'Weights'
FIT
module-attribute
FIT = 'Target fit'
IMPUTATION
module-attribute
IMPUTATION = 'Imputation model'
CASCADE
module-attribute
CASCADE = 'Weight cascade'
COMPARER
module-attribute
COMPARER = 'Weight set comparer'
GLOSSARY
module-attribute
GLOSSARY: tuple[Term, ...] = (
Term(
"ef",
"EF",
"Expansion factor",
"EF = w / w<sub>0</sub>, where w<sub>0</sub> is the base weight: the zone target divided by the number of responding households in the zone.",
"How far weighting moved a household from its starting weight. Values at the configured limits mean weighting would have moved them further if allowed.",
WEIGHTS,
),
Term(
"cv",
"CV",
"Coefficient of variation",
"CV = sd(w) / mean(w)",
"How unequal the weights are relative to their average. 0 means every record carries the same weight.",
WEIGHTS,
),
Term(
"ess",
"ESS %",
"Effective sample size (Kish)",
"ESS = (Σw)<sup>2</sup> / Σw<sup>2</sup>; ESS % = 100 × ESS / n. The matching design effect is DEFF = n / ESS.",
"The number of equally weighted records that would give the same precision. Unequal weights reduce precision, and this shows by how much.",
WEIGHTS,
),
Term(
"maxmed",
"max/med",
"Max over median weight",
"max(w) / median(w)",
"How much heavier the heaviest record is than a typical one. Large values mean a few records carry a large share of every estimate.",
WEIGHTS,
),
Term(
"pcterr",
"% Error",
"Percent error",
"100 × (Ŷ − T) / T, where Ŷ is the weighted total and T the control target.",
"How far a weighted total lands from its target, and in which direction.",
FIT,
),
Term(
"mape",
"MAPE",
"Mean absolute percent error",
"mean of |% error| over the control categories in a zone",
"The average miss across all of a zone's targets.",
FIT,
),
Term(
"p90",
"P90",
"90th percentile absolute percent error",
"90th percentile of |% error| over the control categories in a zone",
"How far the less accurate targets are from their values, ignoring the single worst.",
FIT,
),
Term(
"maxerr",
"Max",
"Maximum absolute percent error",
"max of |% error| over the control categories in a zone",
"The single worst-fitting target in the zone.",
FIT,
),
Term(
"se",
"SE",
"Standard error of the target",
"SE = √( 4/80 · Σ<sub>r</sub> (T<sub>r</sub> − T)<sup>2</sup> ), over the 80 PUMS successive-difference replicate weights.",
"How uncertain the target itself is. A miss within ±1 SE is inside the target's own sampling noise.",
FIT,
),
Term(
"logloss",
"log_loss",
"Log loss",
"−(1/N) Σ<sub>i</sub> ln p̂<sub>i</sub>(y<sub>i</sub>), stratified cross-validation on the PUMS training records.",
"How much probability the model puts on the true category. Lower is better; 0 is perfect.",
IMPUTATION,
),
Term(
"f1",
"F1",
"Macro F1 score",
"mean over categories of 2PR / (P + R), P precision and R recall, stratified cross-validation on the PUMS training records.",
"How often the model's best guess is the true category, counting every category equally however rare. 1 is perfect.",
IMPUTATION,
),
Term(
"ratio",
"Weight ratio to level above",
"Redistribution ratio",
"w / w<sub>above</sub>, where w<sub>above</sub> is the weight of the record it belongs to: a person's household, or a trip's day.",
"How much a record's weight was increased to cover dropped records that belong to the same higher-level record. 1 means no records were dropped.",
CASCADE,
),
Term(
"inherit",
"Same as level above",
"Share unchanged from level above",
"share of records with |w / w<sub>above</sub> − 1| ≤ 1%",
"The share of records with the same weight as the record they belong to.",
CASCADE,
),
Term(
"fold",
"Median / p90 / Max difference",
"Ratio between weight sets",
"max(A/B, B/A) for one record, over records weighted in both sets. The tiles give its median, 90th percentile and maximum.",
"How many times larger one set's weight is than the other's for the same record. 1× means the weights are equal.",
COMPARER,
),
Term(
"rho",
"Rank correlation",
"Spearman rank correlation",
"ρ = corr(rank(A), rank(B))",
"Whether the two sets rank records in the same order, regardless of the size of the weights. 1 means the same order.",
COMPARER,
),
Term(
"r2",
"R²",
"Coefficient of determination",
"R² = corr(ln A, ln B)<sup>2</sup>",
"The share of variation in one set's log weights explained by the other's.",
COMPARER,
),
Term(
"rma",
"RMA fit",
"Reduced major axis fit",
"slope = sign(r) · sd(ln A) / sd(ln B), through (mean ln B, mean ln A), where r = corr(ln A, ln B).",
"How widely A's weights vary compared with B's; below 1, A's vary less. Swapping A and B gives the reciprocal. Unlike an ordinary regression line, it is not flattened when the two sets are weakly related.",
COMPARER,
),
Term(
"iqr",
"IQR",
"Interquartile range of ln(A/B)",
"Q<sub>3</sub> − Q<sub>1</sub> of ln(A/B), within one decile of B",
"How far apart the two sets are for the middle half of records in that band.",
COMPARER,
),
)
Term
dataclass
One defined statistic.
Attributes:
| Name | Type | Description |
|---|---|---|
key |
str
|
Anchor id, |
label |
str
|
How the statistic is abbreviated in a header. |
name |
str
|
Its full name. |
formula |
str
|
The technical definition, as HTML. |
measures |
str
|
What it tells a reader, as short HTML prose. |
group |
str
|
The report area it belongs to, which decides whether it is listed. |
key
instance-attribute
key: str
label
instance-attribute
label: str
name
instance-attribute
name: str
formula
instance-attribute
formula: str
measures
instance-attribute
measures: str
group
instance-attribute
group: str
term
term(key: str, label: str | None = None) -> str
A header or tile label for a defined statistic.
Hovering shows the definition; clicking jumps to the Definitions section.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The statistic's glossary key. |
required |
label
|
str | None
|
Text to show, when it differs from the glossary's abbreviation. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The label as HTML. |
glossary_groups
glossary_groups(groups: set[str]) -> list[tuple[str, list[Term]]]
The definitions to list, grouped, for the report areas actually rendered.
processing.weighting.diagnostics.profile_tables
The report's per-profile tables: how each fit compares, and what carries weight.
One row or column per profile, so the fits can be read side by side: seed and weight sums, the cascade of usable records, redistribution, the day split, control-geography coverage and how lower-level weights descend from the level above.
profile_comparison_table
profile_comparison_table(rows: list[ProfileSummary]) -> str
Compare every fit in the run, one row each.
Each profile is balanced to the same controls over its own universe, so the totals should land on the same targets. Divergence is the usability gate changing what the survey can reach, not the balancer failing -- which is why this table only exists once all the fits are done.
cascade_table
cascade_table(cascades: dict[str | None, list[CascadeRow]]) -> str
Per-level weight coverage, one column group per profile.
Shown side by side rather than behind the profile toggle: the counts are small enough to compare directly, and a labelled column cannot be misread the way a toggled chart can.
redistribution_table
redistribution_table(
rows: dict[str | None, list[RedistributionRow]],
) -> str
Child/parent weight ratio per copy-and-conserve edge, per profile.
split_identity_table
split_identity_table(
rows: dict[str | None, list[SplitIdentityRow]],
) -> str
Whether each split level's children still sum to their parent's weight.
coverage_table
coverage_table(
coverages: dict[str | None, GeographyCoverage],
) -> str
Households each profile admits that the control geography cannot place.
A household outside every control polygon belongs to no balancing zone, so no fit can give it a weight. That is a bound on what the weighting answers, and it is stated rather than left to be inferred from a column of nulls.
inheritance_table
inheritance_table(
rows: list[Inheritance], labels: dict[str, str]
) -> str
How each set's weight descends from the level above, and by what mechanism.
The mechanism is the column that matters. Copied means the level adds nothing of its own, so a comparison there restates the level above. Adjusted means records were rescaled individually after the weight descended, which is the one case where the level no longer nests inside its parent -- and the one a reader is most likely to mistake for the harmless kind.
processing.weighting.diagnostics.report
Report orchestration: assemble sections and render the Jinja2 template.
Entry point is generate_report,
which collects every fit a run produced, builds Plotly figures and HTML tables via the sibling
modules (charts, crosswalk_map, data, tables, profile_tables,
comparison, glossary), then renders them
into a single .html file using a bundled Jinja2 template.
One document per run, not per fit
The crosswalk geometry, the control totals and the PUMS incidence are properties
of the run: they are built once in setup() however many profiles are
fitted. Only the balancer's output varies per profile. Writing one report per
fit therefore re-embedded the same polygons once per profile -- on a Bay Area
run that geometry is 95% of the file -- so the report is assembled once, with
the per-fit sections behind a profile toggle.
What sits where follows from what can be compared on screen:
- Run-level, always visible -- the definitions, the profile comparison, the map, the weight-cascade tables and the weight set comparer. These are small enough to show every profile at once, and a labelled column cannot be misread the way a toggled chart can.
- Per-profile, toggled -- the per-zone tables and every figure. Three profiles of a per-zone fit grid will not fit side by side, so they are switched rather than juxtaposed, and each carries its profile in the heading so a cropped screenshot still says what it shows.
generate_report
generate_report(
fits: dict[str | None, ProfileFit],
control_totals: ControlTotals,
target_names: list[str],
tables: dict[str, pl.DataFrame | None],
output_path: Path,
*,
run_meta: dict[str, str] | None = None,
puma_gdf: GeoDataFrame | None = None,
target_gdf: GeoDataFrame | None = None,
crosswalk_df: pl.DataFrame | None = None,
zone_groups: dict[str, list[str]] | None = None,
merge_specs: list | None = None,
control_moe: pl.DataFrame | None = None,
pums_incidence: pl.DataFrame | None = None,
max_expansion_factor: float | None = None,
comparison: Comparison | None = None
) -> Path
Write the run's self-contained HTML diagnostics report to output_path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fits
|
dict[str | None, ProfileFit]
|
Every completed fit in the run, keyed by profile. A run that
weights one un-profiled set has a single |
required |
control_totals
|
ControlTotals
|
The targets every fit was balanced to. |
required |
target_names
|
list[str]
|
Control registry names in the spec. |
required |
tables
|
dict[str, pl.DataFrame | None]
|
The propagated canonical tables, for the weight cascade. Read after propagation, so each level carries its own weight column. |
required |
output_path
|
Path
|
Destination for the HTML file. |
required |
run_meta
|
dict[str, str] | None
|
Label/value pairs identifying the run, shown in the header. |
None
|
puma_gdf
|
GeoDataFrame | None
|
PUMA boundaries for the crosswalk map. |
None
|
target_gdf
|
GeoDataFrame | None
|
Target-zone boundaries for the crosswalk map. |
None
|
crosswalk_df
|
pl.DataFrame | None
|
The PUMA-to-zone allocation table. |
None
|
zone_groups
|
dict[str, list[str]] | None
|
Zone groupings, when zones were merged for balancing. |
None
|
merge_specs
|
list | None
|
Category merges, for labelling the fit table. |
None
|
control_moe
|
pl.DataFrame | None
|
Per-cell PUMS standard errors from replicate weights. |
None
|
pums_incidence
|
pl.DataFrame | None
|
PUMS incidence, as the reference distribution for the fractional seed imputation chart. |
None
|
max_expansion_factor
|
float | None
|
The production EF, marked on the calibration chart when a grid was searched. |
None
|
comparison
|
Comparison | None
|
Weight sets to compare pairwise. Run-level by construction: a comparison names two profiles, so it cannot sit in a pane that shows one. Omitted, or holding no pairs, the section is left out. |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
The path written. |
processing.weighting.diagnostics.table_html
HTML building blocks shared by the report's table modules.
html_table gives every table one interface, from a simple grid to grouped,
spanned headers.
tag
tag(name: str, text: str, **attrs: str) -> str
Wrap text in an HTML element with optional attributes.
wbr
wbr(text: str) -> str
Insert <wbr> word-break opportunities after /, -, _, and spaces.
html_table
html_table(
headers: list[str],
rows: list[list[str]],
*,
group_row: list[tuple[str, int]] | None = None,
css_class: str = ""
) -> str
Build a <table> from headers and pre-formatted cell strings.
Provides a consistent interface for generating HTML tables with single or
multi-tier headers. Cells starting with <td are inserted as-is to support
pre-styled cells (e.g., CSS classes for coloring convergence status).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
list[str]
|
Column header labels for a single-row header, or sub-column labels
when |
required |
rows
|
list[list[str]]
|
Table body, one list per row. Each cell is a pre-formatted string. |
required |
group_row
|
list[tuple[str, int]] | None
|
Optional two-tier header specification. Each tuple is
|
None
|
css_class
|
str
|
Optional CSS class name applied to the |
''
|
Returns:
| Type | Description |
|---|---|
str
|
Complete HTML |
Example
Simple table:
html_table(["Zone", "Count"], [["A", "100"], ["B", "200"]]) '
\n
\n...' Zone Count Grouped header:
html_table( ... ["Target", "% Error"], # sub-headers ... [["A", "1000", "+2.5%"]], ... group_row=[("Zone", 1), ("Household", 2)] # Zone spans both rows ... )
processing.weighting.diagnostics.tables
HTML table builders for the diagnostics report.
This module generates five of the tables displayed in the diagnostics HTML report:
Balancer Performance Table — Per-zone convergence status, target-fit metrics (MAPE, P90, Max), and weight quality metrics (CV, ESS%). Combines all key performance indicators into a single comprehensive table.
Weight Quality Table — Per-zone weight distribution statistics (mean, median, std, min, max) and expansion factor stats (min/max/mean/median EF ratio).
Unweighted Cell Counts — Data sparsity matrix showing unweighted sample counts per control category per zone, with optional PUMS-weighted percentages for context.
Crosswalk Summary Table — Zone → HH samples mapping with optional zone group aggregation.
Imputation Summary Table — Per control, how many seed rows had an empty incidence block, and the cross-validated log-loss and F1 of the PUMS-trained model that filled them.
All table builders return raw HTML strings suitable for Jinja2 template insertion, built with the helpers in
table_html. The per-profile tables are inprofile_tables.imputation_summary_table
imputation_summary_table(summaries: list[ImputationSummary]) -> strGenerate the fractional seed imputation table (Section 0 of diagnostics report).
One row per control showing how many seed rows had an empty incidence block for it, plus the cross-validated log-loss and F1 of the PUMS-trained model that filled them. Describes the seed the balancer was handed -- not the survey
imputationstep, which writes discrete values to canonical data.balancer_performance_table
balancer_performance_table( statuses: list[ZoneStatus], weighted: pl.DataFrame, zone_fit: pl.DataFrame, ) -> strGenerate the main balancer performance table (Section 2 of diagnostics report).
Combines three categories of per-zone metrics into a single comprehensive table:
- Convergence: Did the balancer converge? How many iterations?
- Target Fit: How well do weighted totals match PUMS targets? (MAPE, P90, Max)
- Weight Quality: How stable/dispersed are the weights? (CV, ESS%)
Parameters:
Name Type Description Default statuseslist[ZoneStatus]Per-zone convergence results from the balancer.
required weightedpl.DataFrameHousehold seed joined with final weights (must include
ctrl_geoid,hh_weight,base_weightcolumns).required zone_fitpl.DataFrameZone-level target fit summary (output of
zone_fit_summary()).required Returns:
Type Description strHTML table with 13 columns: Zone, N, Conv?, Iter, Household (Target, % Error),
strPerson (Target, % Error), MAPE, P90, Max, CV, ESS%.
Note
Uses
html_table()with a two-tier grouped header. The "Household" and "Person" columns span their respective Target/% Error sub-columns.weight_quality_table
weight_quality_table(weighted: pl.DataFrame) -> strGenerate the weight quality table (Section 3 of diagnostics report).
Shows per-zone and total weight distribution statistics (mean, median, std, min, max) and expansion factor ratios (min/max/mean/median EF). This table complements the violin plot that follows it in the report.
Parameters:
Name Type Description Default weightedpl.DataFrameHousehold seed joined with final weights (must include
ctrl_geoid,hh_weight,base_weightcolumns).required Returns:
Type Description strHTML table with 11 columns: Zone, N, Mean, Median, Std, Min, Max, Min EF,
strMax EF, Mean EF, Median EF, plus a TOTAL row aggregating across zones.
Note
CV and ESS% were removed from this table in March 2026 and moved to the balancer performance table for a unified view of all key metrics.
unweighted_cell_counts
unweighted_cell_counts( seed: pl.DataFrame, target_names: list[str], control_totals: ControlTotals | None = None, merge_specs: list | None = None, ) -> strSingle matrix table: categories (rows) x zones (columns).
Row headers are grouped by control name using
<th rowspan>. A level separator row (Household / Person) divides the two groups.When control_totals is provided, each cell also shows the PUMS-weighted percentage in italic parentheses so the reader can compare survey representation against the PUMS universe.
Uses uniform column handling for all controls (structural unpivoted, non-structural pivoted).
crosswalk_summary_table
crosswalk_summary_table( crosswalk_df: pl.DataFrame, seed: pl.DataFrame ) -> strCompact Zone -> HH Samples table with optional Zone Group column.