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
1 change: 1 addition & 0 deletions changelog.d/uprating-per-capita.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Uprate dollar inputs by national totals per capita, so weighted totals track each national total instead of also growing with the population-uprated weights (a 0.9 percent overshoot in 2025 rising to 5.1 percent by 2035).
2 changes: 1 addition & 1 deletion docs-quarto/methodology/spm-poverty.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ The [MOOP decomposition page](moop-decomposition.qmd) records the design and the

Population results use the Microcosm US dataset. The current default is Build P: 57,240 Current Population Survey Annual Social and Economic Supplement households with 166,321 people, whose weights are fitted to 5,659 administrative and demographic targets (IRS Statistics of Income by state and by income, Census population estimates by age, sex and state, CMS Medicaid and Marketplace enrollment, USDA SNAP participation and benefits, SSA SSI, TANF, state tax collections and BEA national accounts). Poverty rates are never a target. That is a rule of the calibration, not an accident: a model whose weights were fitted to survey-based poverty rates could not be checked against them.

2025 values are the 2024 survey values carried forward by the model's uprating parameters: each uprated input grows by the ratio of its uprating parameter between the two years. The main income sources (employment, self-employment and partnership income, Social Security, pensions and retirement account distributions, capital gains, dividends, interest, rental and farm income, and unemployment compensation) follow [IRS Statistics of Income series](https://github.com/PolicyEngine/policyengine-us/tree/main/policyengine_us/parameters/calibration/gov/irs/soi). Each series holds IRS totals through its last recorded year and a CBO projection extends it from there, so the growth applied from 2024 to 2025 is CBO's. Non-premium medical spending and the survey-reported premiums that enter SPM resources follow [CMS's projection of per-capita out-of-pocket health spending](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml). Child support paid and received, the energy subsidy, real estate taxes and household assets follow [CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml). Every other dollar input, including rent and child care expenses, follows [CBO's projection of total adjusted gross income](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml), the default that [`default_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/default_uprating.py) assigns. Household weights follow the model's [total population series](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/census/populations/total.yaml), which takes its values from CBO's demographic outlook; person and SPM unit weights read the household weight. Inputs without an uprating parameter (ages, take-up indicators, tenure, geography) carry over unchanged, so household composition is held at its 2024 state. The population is projected, not resampled.
2025 values are the 2024 survey values carried forward by the model's uprating parameters: each uprated input grows by the ratio of its uprating series between the two years. Household weights grow with total population, so an input that follows a national total grows by that total divided by population ([`per_capita_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/per_capita_uprating.py)); its weighted total then tracks the national total. The main income sources (employment, self-employment and partnership income, Social Security, pensions and retirement account distributions, capital gains, dividends, interest, rental and farm income, and unemployment compensation) follow [IRS Statistics of Income series](https://github.com/PolicyEngine/policyengine-us/tree/main/policyengine_us/parameters/calibration/gov/irs/soi). Each series holds IRS totals through its last recorded year and a CBO projection extends it from there, so the growth applied from 2024 to 2025 is CBO's, per capita. Non-premium medical spending and the survey-reported premiums that enter SPM resources follow [CMS's projection of per-capita out-of-pocket health spending](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/hhs/cms/moop_per_capita.yaml), which is already a per-person series. Child support paid and received, the energy subsidy, real estate taxes and household assets follow [CPI-U](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/gov/bls/cpi/cpi_u.yaml). Every other dollar input, including rent and child care expenses, follows [CBO's projection of total adjusted gross income](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/cbo/income_by_source.yaml), the default that [`default_uprating.py`](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/tools/default_uprating.py) assigns, per capita like every national total. Household weights follow the model's [total population series](https://github.com/PolicyEngine/policyengine-us/blob/main/policyengine_us/parameters/calibration/gov/census/populations/total.yaml), which takes its values from CBO's demographic outlook; person and SPM unit weights read the household weight. Inputs without an uprating parameter (ages, take-up indicators, tenure, geography) carry over unchanged, so household composition is held at its 2024 state. The population is projected, not resampled.

## What the model does not do

Expand Down
38 changes: 37 additions & 1 deletion policyengine_us/data/economic_assumptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
USSingleYearDataset,
USMultiYearDataset,
)
from policyengine_us.tools.per_capita_uprating import (
DERIVED_FROM,
check_per_capita_series_is_current,
is_national_total_path,
per_capita_path,
)

# The default end year for dataset extension is derived at runtime from
# the CPI-U parameter YAML (gov.bls.cpi.cpi_u). When the CPI-U YAML
Expand Down Expand Up @@ -174,10 +180,16 @@ def _apply_single_year_uprating(current, previous, system):
if uprating_path is None:
continue

param = _resolve_parameter(system.parameters, uprating_path)
param = _resolve_uprating_parameter(system.parameters, uprating_path)
if param is None:
continue

derived_from = getattr(param, "metadata", {}).get(DERIVED_FROM)
if derived_from:
for period in (previous_period, current_period):
check_per_capita_series_is_current(
system.parameters, derived_from, period
)
prev_val = param(previous_period)
curr_val = param(current_period)
if prev_val == 0:
Expand All @@ -191,6 +203,30 @@ def _apply_single_year_uprating(current, previous, system):
current_df[col] = prev_df[col] * factor


def _resolve_uprating_parameter(parameters, path):
"""Resolve an uprating path, using the per-capita series for a national
total.

Weights grow with population, so a column uprated by a national total
uses the total's per-capita sibling (see ``per_capita_uprating``). The
overrides above name national totals and resolve through here. A tree
that holds the total but not its sibling raises: falling back to the
total would quietly count population growth twice.
"""
if is_national_total_path(path):
if _resolve_parameter(parameters, path) is None:
return None
per_capita = _resolve_parameter(parameters, per_capita_path(path))
if per_capita is None:
raise ValueError(
f"{path} is a national total but {per_capita_path(path)} is "
"missing from the parameter tree. Build the system with "
"CountryTaxBenefitSystem, or call add_per_capita_uprating."
)
return per_capita
return _resolve_parameter(parameters, path)


def _resolve_parameter(parameters, path):
"""Resolve a dotted parameter path like 'gov.bls.cpi.cpi_u'."""
node = parameters
Expand Down
3 changes: 2 additions & 1 deletion policyengine_us/parameters/gov/hhs/smi/amount.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,8 @@ metadata:
unit: currency-USD
period: year
uprating:
parameter: calibration.gov.cbo.income_by_source.adjusted_gross_income
# Per capita, like the incomes this threshold is compared against.
parameter: calibration.gov.cbo.income_by_source.adjusted_gross_income_per_capita
start_instant: 2027-01-01
reference:
- title: 60 percent of estimated State Median Income adjusted for family size, by state, FFY 2018 (Attachment 1, LIHEAP-IM-2017-03)
Expand Down
11 changes: 10 additions & 1 deletion policyengine_us/reforms/ssa/trustees_2025.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,16 @@ def apply_trustees_2025_soi_income_projection(
benefits use these calibration totals as uprating factors. If they flatten
after the CBO projection window, nominal incomes freeze in long-run
microsimulations even when NAWI and tax parameters keep growing.

Inputs uprate by each total divided by population (see
``policyengine_us.tools.per_capita_uprating``), so each total grows by
average-wage growth times population growth. Each record's income then
follows the average wage and the weights carry population growth.
"""

nawi = parameters.gov.ssa.nawi
soi = parameters.calibration.gov.irs.soi
population = parameters.calibration.gov.census.populations.total

for parameter_name in TRUSTEES_2025_SOI_INCOME_UPRATING_PARAMETERS:
parameter = getattr(soi, parameter_name)
Expand All @@ -170,9 +176,12 @@ def apply_trustees_2025_soi_income_projection(
wage_growth = float(nawi(f"{year}-01-01")) / float(
nawi(f"{year - 1}-01-01")
)
population_growth = float(population(f"{year}-01-01")) / float(
population(f"{year - 1}-01-01")
)
parameter.update(
period=f"year:{year}-01-01:1",
value=previous_value * wage_growth,
value=previous_value * wage_growth * population_growth,
)

final_value = float(parameter(f"{end_year}-01-01"))
Expand Down
18 changes: 18 additions & 0 deletions policyengine_us/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
uprate_parameters,
)
from .tools.default_uprating import add_default_uprating
from .tools.per_capita_uprating import (
add_per_capita_parameters_for_parameter_uprating,
add_per_capita_uprating,
)
from policyengine_us.data.dataset_schema import (
US_ENTITIES,
USSingleYearDataset,
Expand Down Expand Up @@ -113,6 +117,7 @@ def __init__(
)
self.parameters = propagate_parameter_metadata(self.parameters)
self.parameters = interpolate_parameters(self.parameters)
add_per_capita_parameters_for_parameter_uprating(self.parameters)
self.parameters = uprate_parameters(self.parameters)
self.parameters = propagate_parameter_metadata(self.parameters)
add_default_uprating(self)
Expand Down Expand Up @@ -146,6 +151,19 @@ def __init__(

self.add_variables(*create_50_state_variables())

# Last, so reforms to a national total or to the population series
# reach the per-capita series the variables uprate by.
add_per_capita_uprating(self)
self._per_capita_uprating_built = True

def modify_parameters(self, modifier_function):
result = super().modify_parameters(modifier_function)
# Reforms applied after init (core's Simulation applies the reform
# once more) must reach the derived per-capita series too.
if getattr(self, "_per_capita_uprating_built", False):
add_per_capita_uprating(self)
return result

def clone(self):
return clone_spm_system(self)

Expand Down
Loading
Loading