Skip to content

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

  1. 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.
  2. Crosswalk Map — geographic crosswalk visualization, with every profile's seed count in each zone's tooltip. Boundaries are simplified to a few metres.
  3. 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.
  4. 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

  1. 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 imputation pipeline step.
  2. Balancer Performance — per-zone convergence status, target fit (MAPE, P90, Max), CV and ESS%.
  3. Weight Quality — per-zone weight and expansion-factor statistics, with violin plots of final_weight / base_weight.
  4. Expansion Factor Calibration — MAPE vs CV across a grid of max_expansion_factor values. Enabled by setting expansion_factor_grid in the weighting config.
  5. Target Fit (% Error) — diverging bar charts per control category per zone, with PUMS replicate-weight whiskers.
  6. 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 max_expansion_factor — shown as a vertical dashed marker line on every panel.

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 (ln A, ln B) for the scatter.

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

deciles_ab instance-attribute

deciles_ab: list[Decile]

deciles_ba instance-attribute

deciles_ba: list[Decile]

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.

sets instance-attribute

sets: list[WeightSet]

pairs instance-attribute

pairs: list[PairStats]

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 AGREEMENT_TOL.

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

{name: {"label": str, "columns": {table: column}}}, as read from the diagnostics.compare_weights block. label is optional and defaults to the name.

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 None when either column is missing or

PairStats | None

fewer than MIN_OVERLAP records carry both weights.

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

{"sets": [...], "levels": [...], "pairs": [...]}, empty lists when

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 puma_id column).

required
target_gdf gpd.GeoDataFrame

Target zone polygons (must have study_geoid column).

required
crosswalk_df pl.DataFrame

Crosswalk table with puma_id, study_geoid, population, allocation_weight.

required
seeds Mapping[str | None, pl.DataFrame] | None

Each profile's seed, keyed by profile name (must contain study_geoid). Per-zone counts appear in the tooltip.

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 = (&Sigma;w)<sup>2</sup> / &Sigma;w<sup>2</sup>; &nbsp;ESS % = 100 &times; 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 &times; (Y&#770; &minus; T) / T, where Y&#770; 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 = &radic;( 4/80 &middot; &Sigma;<sub>r</sub> (T<sub>r</sub> &minus; T)<sup>2</sup> ), over the 80 PUMS successive-difference replicate weights.",
        "How uncertain the target itself is. A miss within &plusmn;1 SE is inside the target's own sampling noise.",
        FIT,
    ),
    Term(
        "logloss",
        "log_loss",
        "Log loss",
        "&minus;(1/N) &Sigma;<sub>i</sub> ln p&#770;<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> &minus; 1| &le; 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&times; means the weights are equal.",
        COMPARER,
    ),
    Term(
        "rho",
        "Rank correlation",
        "Spearman rank correlation",
        "&rho; = 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&sup2;",
        "Coefficient of determination",
        "R&sup2; = 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) &middot; 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> &minus; 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, g-<key> in the report.

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 None key, and the toggle collapses to one pane.

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 group_row is used.

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 (label, colspan). Columns with colspan == 1 span both header rows (rowspan="2"). Columns with colspan > 1 become group headers spanning multiple sub-columns (labels consumed from headers list).

None
css_class str

Optional CSS class name applied to the <table> element.

''

Returns:

Type Description
str

Complete HTML <table> string.

Example

Simple table:

html_table(["Zone", "Count"], [["A", "100"], ["B", "200"]]) '

\n\n...'

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:

  1. 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.

  2. Weight Quality Table — Per-zone weight distribution statistics (mean, median, std, min, max) and expansion factor stats (min/max/mean/median EF ratio).

  3. Unweighted Cell Counts — Data sparsity matrix showing unweighted sample counts per control category per zone, with optional PUMS-weighted percentages for context.

  4. Crosswalk Summary Table — Zone → HH samples mapping with optional zone group aggregation.

  5. 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 in profile_tables.

imputation_summary_table

imputation_summary_table(summaries: list[ImputationSummary]) -> str

Generate 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 imputation step, which writes discrete values to canonical data.

balancer_performance_table

balancer_performance_table(
    statuses: list[ZoneStatus],
    weighted: pl.DataFrame,
    zone_fit: pl.DataFrame,
) -> str

Generate 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:

ZoneCount
Name Type Description Default
statuses list[ZoneStatus]

Per-zone convergence results from the balancer.

required
weighted pl.DataFrame

Household seed joined with final weights (must include ctrl_geoid, hh_weight, base_weight columns).

required
zone_fit pl.DataFrame

Zone-level target fit summary (output of zone_fit_summary()).

required

Returns:

Type Description
str

HTML table with 13 columns: Zone, N, Conv?, Iter, Household (Target, % Error),

str

Person (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) -> str

Generate 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
weighted pl.DataFrame

Household seed joined with final weights (must include ctrl_geoid, hh_weight, base_weight columns).

required

Returns:

Type Description
str

HTML table with 11 columns: Zone, N, Mean, Median, Std, Min, Max, Min EF,

str

Max 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,
) -> str

Single 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
) -> str

Compact Zone -> HH Samples table with optional Zone Group column.