Skip to content
Open
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/hud-utility-allowance-county.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add county-level HUD utility allowance schedules for Texas (TDHCA) and Kansas, move the schedules into parameters, and update Los Angeles County to the FY2025 schedule.
46 changes: 46 additions & 0 deletions policyengine_us/parameters/gov/hud/utility_allowance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# HUD utility allowances

County-level Housing Choice Voucher utility allowance schedules, used by
`hud_utility_allowance`.

Utility allowances are set per public housing agency (PHA) under
[24 CFR 982.517](https://www.law.cornell.edu/cfr/text/24/982.517), broken out by
dwelling-unit size (bedroom count) and utility/fuel type. HUD does not publish a
national dataset of these schedules, so `county_utility_allowances.csv` encodes
the schedules for the counties PolicyEngine currently models:

- **Los Angeles County, CA** (LACDA) — schedules effective 2023-07-01 (from
LACDA's prior schedule PDF, no longer online; values match the table
previously hardcoded in the model) and 2025-07-01.
- **Texas** — the 28 counties in the Texas Department of Housing and Community
Affairs (TDHCA) Housing Choice Voucher service area, effective 2026-01-01.
- **Kansas** — Sedgwick (Wichita HA), Shawnee (Topeka HA), Wyandotte (Kansas
City KS HA) and Johnson (Johnson County HA) counties.

## Convention

Each schedule is collapsed into a single monthly dollar amount per bedroom size
using one consistent convention, matching how LA County was originally modeled:

> **Multi-Family (apartment) unit type**, **all-electric** heating / cooking /
> water heating, and the **sum of every tenant-paid line item** — other electric,
> air conditioning, water, sewer, trash, the electric service charge, and the
> range and refrigerator appliance allowances.

Gas rows, gas service charges, and electric heat-pump rows are excluded. This is
an approximation: the model has a single `tenant_pays_utilities` flag and does
not know a household's actual fuel type or which specific utilities it pays.

## File format

`county_utility_allowances.csv` columns:

- `county_fips` — five-digit county FIPS code.
- `year` — the schedule's effective year. `hud_utility_allowance` uses the latest
schedule at or before the simulated year (and the earliest schedule for years
before it begins).
- `bedrooms` — bedroom count, or `-1` for single-room occupancy (SRO). Households
larger than a schedule's top bedroom size reuse that top value. Counties whose
PHA publishes no SRO row (all except LA County) receive 75% of their
zero-bedroom value, per [24 CFR 982.604(b)](https://www.law.cornell.edu/cfr/text/24/982.604).
- `monthly_value` — monthly utility allowance in dollars.
88 changes: 88 additions & 0 deletions policyengine_us/parameters/gov/hud/utility_allowance/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""County-level HUD utility allowance schedules.

Utility allowances are set per public housing agency (PHA) under 24 CFR 982.517,
by dwelling-unit size (bedroom count) and utility/fuel type. There is no national
dataset of these schedules, so this file encodes the schedules for the specific
counties PolicyEngine currently models (LA County, the TDHCA service area in
Texas, and four Kansas PHAs).

Each schedule is collapsed into a single monthly dollar amount per bedroom size
using one consistent convention, matching how LA County was originally modeled:

Multi-Family (apartment) unit type
+ all-electric heating / cooking / water heating
+ sum of every tenant-paid line item
(other electric, air conditioning, water, sewer, trash, the electric
service charge, and the range and refrigerator appliance allowances)

Gas rows, gas service charges, and electric heat-pump rows are excluded.

`county_utility_allowances.csv` stores the raw per-county schedules keyed by
`county_fips`, `year` (the schedule's effective year), and `bedrooms` (with
`-1` denoting single-room occupancy). `utility_allowance_schedule(year)` returns
the effective schedule for a given year, expanded to bedrooms 0-8. Counties
whose PHA publishes no SRO row receive 75% of their zero-bedroom value per
24 CFR 982.604(b).
"""

from functools import lru_cache
from pathlib import Path

import pandas as pd

FOLDER = Path(__file__).parent

# Household bedroom counts are clipped to this range before lookup; schedules
# that stop at a smaller size reuse their largest published bedroom value.
MAX_BEDROOMS = 8
# Sentinel bedroom key for single-room-occupancy (SRO) units.
SRO_BEDROOMS = -1
# 24 CFR 982.604(b): the utility allowance for SRO housing is 75% of the
# zero-bedroom allowance. Applied when a PHA publishes no SRO row; a published
# SRO row (LA County) takes precedence.
SRO_ZERO_BEDROOM_RATE = 0.75


def _load_county_utility_allowances() -> pd.DataFrame:
df = pd.read_csv(
FOLDER / "county_utility_allowances.csv",
dtype={"county_fips": str},
)
df["county_fips"] = df["county_fips"].str.zfill(5)
return df


county_utility_allowances = _load_county_utility_allowances()


def _effective_year_by_county(target_year: int) -> pd.Series:
"""Latest schedule year at or before `target_year` for each county, falling
back to the county's earliest schedule year for years before it begins."""
df = county_utility_allowances
prior = df[df["year"] <= target_year].groupby("county_fips")["year"].max()
earliest = df.groupby("county_fips")["year"].min()
return prior.reindex(earliest.index).fillna(earliest).astype(int)


@lru_cache(maxsize=None)
def utility_allowance_schedule(target_year: int) -> pd.DataFrame:
"""Effective monthly utility allowance per county and bedroom size for a
year, expanded to bedrooms 0-`MAX_BEDROOMS` (plus SRO where published)."""
df = county_utility_allowances
effective = _effective_year_by_county(target_year)
selected = df[
df.apply(lambda row: row["year"] == effective[row["county_fips"]], axis=1)
]

rows = []
for county_fips, group in selected.groupby("county_fips"):
by_bedroom = dict(zip(group["bedrooms"], group["monthly_value"]))
largest = max(bedroom for bedroom in by_bedroom if bedroom >= 0)
for bedrooms in range(MAX_BEDROOMS + 1):
rows.append(
(county_fips, bedrooms, by_bedroom.get(bedrooms, by_bedroom[largest]))
)
sro_value = by_bedroom.get(SRO_BEDROOMS, by_bedroom[0] * SRO_ZERO_BEDROOM_RATE)
rows.append((county_fips, SRO_BEDROOMS, sro_value))

return pd.DataFrame(rows, columns=["county_fips", "bedrooms", "monthly_value"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
county_fips,year,bedrooms,monthly_value
06037,2023,-1,179
06037,2023,0,227
06037,2023,1,271
06037,2023,2,318
06037,2023,3,375
06037,2023,4,452
06037,2023,5,517
06037,2023,6,580
06037,2023,7,647
06037,2023,8,719
06037,2025,-1,194
06037,2025,0,249
06037,2025,1,290
06037,2025,2,333
06037,2025,3,389
06037,2025,4,456
06037,2025,5,517
06037,2025,6,572
06037,2025,7,635
06037,2025,8,696
20091,2026,0,208
20091,2026,1,223
20091,2026,2,285
20091,2026,3,367
20091,2026,4,452
20091,2026,5,536
20173,2025,0,210
20173,2025,1,224
20173,2025,2,266
20173,2025,3,307
20173,2025,4,349
20173,2025,5,390
20173,2025,6,415
20177,2025,0,189
20177,2025,1,199
20177,2025,2,256
20177,2025,3,315
20177,2025,4,372
20177,2025,5,430
20177,2025,6,486
20177,2025,7,543
20209,2026,0,252
20209,2026,1,265
20209,2026,2,319
20209,2026,3,369
20209,2026,4,419
20209,2026,5,471
48013,2026,0,206
48013,2026,1,222
48013,2026,2,265
48013,2026,3,308
48013,2026,4,350
48013,2026,5,394
48015,2026,0,242
48015,2026,1,260
48015,2026,2,312
48015,2026,3,363
48015,2026,4,415
48015,2026,5,469
48019,2026,0,214
48019,2026,1,225
48019,2026,2,265
48019,2026,3,308
48019,2026,4,352
48019,2026,5,394
48055,2026,0,201
48055,2026,1,212
48055,2026,2,254
48055,2026,3,295
48055,2026,4,339
48055,2026,5,384
48071,2026,0,239
48071,2026,1,252
48071,2026,2,301
48071,2026,3,352
48071,2026,4,403
48071,2026,5,457
48089,2026,0,218
48089,2026,1,237
48089,2026,2,290
48089,2026,3,345
48089,2026,4,397
48089,2026,5,452
48091,2026,0,197
48091,2026,1,204
48091,2026,2,243
48091,2026,3,282
48091,2026,4,328
48091,2026,5,372
48093,2026,0,221
48093,2026,1,237
48093,2026,2,292
48093,2026,3,347
48093,2026,4,399
48093,2026,5,449
48121,2026,0,238
48121,2026,1,250
48121,2026,2,298
48121,2026,3,345
48121,2026,4,392
48121,2026,5,439
48139,2026,0,208
48139,2026,1,225
48139,2026,2,276
48139,2026,3,325
48139,2026,4,375
48139,2026,5,424
48145,2026,0,233
48145,2026,1,248
48145,2026,2,295
48145,2026,3,342
48145,2026,4,391
48145,2026,5,438
48157,2026,0,195
48157,2026,1,213
48157,2026,2,262
48157,2026,3,314
48157,2026,4,364
48157,2026,5,416
48161,2026,0,218
48161,2026,1,234
48161,2026,2,283
48161,2026,3,329
48161,2026,4,378
48161,2026,5,424
48163,2026,0,197
48163,2026,1,213
48163,2026,2,251
48163,2026,3,291
48163,2026,4,330
48163,2026,5,369
48167,2026,0,236
48167,2026,1,255
48167,2026,2,312
48167,2026,3,371
48167,2026,4,429
48167,2026,5,487
48171,2026,0,146
48171,2026,1,155
48171,2026,2,183
48171,2026,3,213
48171,2026,4,244
48171,2026,5,275
48185,2026,0,240
48185,2026,1,253
48185,2026,2,298
48185,2026,3,343
48185,2026,4,388
48185,2026,5,433
48187,2026,0,269
48187,2026,1,287
48187,2026,2,341
48187,2026,3,392
48187,2026,4,446
48187,2026,5,498
48251,2026,0,252
48251,2026,1,269
48251,2026,2,322
48251,2026,3,374
48251,2026,4,427
48251,2026,5,480
48255,2026,0,218
48255,2026,1,235
48255,2026,2,283
48255,2026,3,331
48255,2026,4,380
48255,2026,5,432
48259,2026,0,188
48259,2026,1,198
48259,2026,2,227
48259,2026,3,256
48259,2026,4,287
48259,2026,5,315
48265,2026,0,209
48265,2026,1,221
48265,2026,2,263
48265,2026,3,307
48265,2026,4,350
48265,2026,5,395
48287,2026,0,218
48287,2026,1,235
48287,2026,2,283
48287,2026,3,331
48287,2026,4,380
48287,2026,5,432
48299,2026,0,240
48299,2026,1,251
48299,2026,2,284
48299,2026,3,325
48299,2026,4,362
48299,2026,5,400
48325,2026,0,197
48325,2026,1,210
48325,2026,2,243
48325,2026,3,275
48325,2026,4,309
48325,2026,5,343
48473,2026,0,201
48473,2026,1,212
48473,2026,2,239
48473,2026,3,267
48473,2026,4,293
48473,2026,5,322
48481,2026,0,204
48481,2026,1,222
48481,2026,2,272
48481,2026,3,322
48481,2026,4,371
48481,2026,5,423
48493,2026,0,193
48493,2026,1,202
48493,2026,2,232
48493,2026,3,260
48493,2026,4,288
48493,2026,5,316
2 changes: 1 addition & 1 deletion policyengine_us/programs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ programs:
status: in_progress
variable: hud_hap
parameter_prefix: gov.hud
notes: "National rules; only have AMI for Los Angeles County and Colorado. Illinois, Massachusetts in progress"
notes: "National rules with county-level income limits and fair market rents; utility allowance schedules encoded for Los Angeles County, the Texas TDHCA service area (28 counties), and four Kansas PHAs (Sedgwick, Shawnee, Wyandotte, and Johnson counties)"

# --- FCC programs ---
- id: lifeline
Expand Down
Loading
Loading