Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/populace-build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ wraps the Populace calibrator's log-weight optimizer for stacked local weights
and records per-area/per-metric diagnostics before the solved weights are
exported with `stacked_weights_to_long`.

`populace.build.uk.local_runner` is the Populace-owned candidate build path. It
loads explicit area and target tables, aligns a sorted household frame with
source-year/source-household/clone lineage, optionally computes household
metrics once per UK country by setting the PolicyEngine-UK `region` input, then
solves and writes `local_geography_weights.csv.gz`,
`solve_diagnostics.csv`, `area_support_summary.csv`, and `solve_summary.json`.
It accepts already-pooled or already-cloned household pools, so the compact UK
artifact can remain the fast national default while a separate `local` variant
scales up with pooled FRS years, cloned records, and L0 budget control.

## US plan status

`populace.build.us` declares the US build: stage order, donor graph with
Expand Down
4 changes: 4 additions & 0 deletions packages/populace-build/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ dependencies = [
# content is declared in packaged manifests and interpreted by shared Populace
# runtimes; country source loaders must not depend on incumbent data packages.
us = ["policyengine-us>=1.729,<2", "h5py>=3"]
# The UK extra adds the rules engine for local metric generation from a
# Populace UK H5. Target tables remain explicit inputs, and the base package
# still does not import policyengine-uk at import time.
uk = ["policyengine-uk>=2.88", "h5py>=3"]

[project.urls]
Homepage = "https://populace.dev"
Expand Down
26 changes: 26 additions & 0 deletions packages/populace-build/src/populace/build/uk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@
stacked_weights_to_long,
write_long_geography_weights,
)
from populace.build.uk.local_runner import (
UKLocalCandidateResult,
build_local_candidate,
build_local_candidate_from_dataset,
build_metric_tables_from_dataset,
load_metric_tables,
load_uk_dataset,
prepare_area_frame,
prepare_household_frame,
read_local_table,
set_simulation_area_group,
summarize_local_candidate,
write_local_candidate_outputs,
)
from populace.build.uk.local_solver import (
StackedLocalSolveResult,
solve_stacked_local_weights,
Expand All @@ -36,16 +50,28 @@
"LONG_GEOGRAPHY_COLUMNS",
"StackedLocalMatrix",
"StackedLocalSolveResult",
"UKLocalCandidateResult",
"align_area_targets",
"area_support_summary",
"area_groups_from_codes",
"build_local_candidate",
"build_local_candidate_from_dataset",
"build_metric_tables_from_dataset",
"build_stacked_local_matrix",
"compute_household_metrics",
"load_metric_tables",
"load_uk_dataset",
"metric_names",
"metric_tables_by_area_group",
"prepare_area_frame",
"prepare_household_frame",
"read_local_table",
"set_simulation_area_group",
"solve_stacked_local_weights",
"sort_households_by_id",
"stacked_design_weights",
"stacked_weights_to_long",
"summarize_local_candidate",
"write_local_candidate_outputs",
"write_long_geography_weights",
]
56 changes: 53 additions & 3 deletions packages/populace-build/src/populace/build/uk/local_geography.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,22 +362,64 @@ def stacked_weights_to_long(
return out.loc[:, LONG_GEOGRAPHY_COLUMNS]


def area_support_summary(long_weights: pd.DataFrame) -> pd.DataFrame:
"""Summarize non-zero household support by local area."""
def area_support_summary(
long_weights: pd.DataFrame,
*,
area_codes: Sequence[str] | None = None,
area_type: str | None = None,
) -> pd.DataFrame:
"""Summarize non-zero household support by local area.

Passing ``area_codes`` includes requested areas with no positive assigned
households, which is important for sparse/L0 local solves.
"""

missing = sorted(set(LONG_GEOGRAPHY_COLUMNS) - set(long_weights.columns))
if missing:
raise ValueError(f"long weight frame is missing column(s): {missing}.")
positive = long_weights[long_weights["weight"] > 0]
return (
summary = (
positive.groupby(["area_type", "area_code"], sort=True)
.agg(
nonzero_households=("household_id", "nunique"),
nonzero_source_households=("source_household_key", "nunique"),
weight_sum=("weight", "sum"),
max_weight=("weight", "max"),
effective_sample_size=("weight", _effective_sample_size),
)
.reset_index()
)
if area_codes is None:
return summary

codes = _area_code_tuple(area_codes)
if area_type is None:
area_types = long_weights["area_type"].dropna().unique()
if len(area_types) != 1:
raise ValueError(
"area_type must be supplied when area_codes are supplied and "
"long_weights does not contain exactly one area_type."
)
area_type = str(area_types[0])
full = pd.DataFrame(
{
"area_type": area_type,
"area_code": list(codes),
}
)
completed = full.merge(summary, on=["area_type", "area_code"], how="left")
completed["nonzero_households"] = (
completed["nonzero_households"].fillna(0).astype(int)
)
completed["nonzero_source_households"] = (
completed["nonzero_source_households"].fillna(0).astype(int)
)
completed["weight_sum"] = completed["weight_sum"].fillna(0.0).astype(float)
completed["max_weight"] = completed["max_weight"].fillna(0.0).astype(float)
completed["effective_sample_size"] = (
completed["effective_sample_size"].fillna(0.0).astype(float)
)
return completed


def write_long_geography_weights(
Expand Down Expand Up @@ -551,3 +593,11 @@ def _source_keys(
else:
keys.append(f"{year}:{household_id}")
return np.asarray(keys, dtype=object)


def _effective_sample_size(weights: pd.Series) -> float:
values = weights.to_numpy(dtype=np.float64)
square_sum = float(np.square(values).sum())
if square_sum == 0:
return 0.0
return float(values.sum() ** 2 / square_sum)
Loading
Loading