Skip to content

Commit 3f1dc88

Browse files
authored
Merge pull request #630 from PolicyEngine/codex/cps-current-health-coverage
Align CPS health coverage anchors with at-interview rule inputs
2 parents a84b882 + 4102a8d commit 3f1dc88

11 files changed

Lines changed: 632 additions & 21 deletions

File tree

changelog.d/630.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Anchor ACA take-up to subsidized Marketplace coverage reports so unsubsidized exchange enrollment does not force premium tax credit take-up.

policyengine_us_data/calibration/publish_local_area.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from policyengine_us_data.utils.takeup import (
3939
SIMPLE_TAKEUP_VARS,
4040
apply_block_takeup_to_arrays,
41+
reported_subsidized_marketplace_by_tax_unit,
4142
)
4243

4344
CHECKPOINT_FILE = Path("completed_states.txt")
@@ -157,6 +158,34 @@ def record_completed_city(city_name: str):
157158
f.write(f"{city_name}\n")
158159

159160

161+
def _build_reported_takeup_anchors(
162+
data: dict, time_period: int
163+
) -> dict[str, np.ndarray]:
164+
reported_anchors = {}
165+
if (
166+
"reported_has_subsidized_marketplace_health_coverage_at_interview" in data
167+
and time_period
168+
in data["reported_has_subsidized_marketplace_health_coverage_at_interview"]
169+
):
170+
reported_anchors["takes_up_aca_if_eligible"] = (
171+
reported_subsidized_marketplace_by_tax_unit(
172+
data["person_tax_unit_id"][time_period],
173+
data["tax_unit_id"][time_period],
174+
data[
175+
"reported_has_subsidized_marketplace_health_coverage_at_interview"
176+
][time_period],
177+
)
178+
)
179+
if (
180+
"has_medicaid_health_coverage_at_interview" in data
181+
and time_period in data["has_medicaid_health_coverage_at_interview"]
182+
):
183+
reported_anchors["takes_up_medicaid_if_eligible"] = data[
184+
"has_medicaid_health_coverage_at_interview"
185+
][time_period].astype(bool)
186+
return reported_anchors
187+
188+
160189
def build_h5(
161190
weights: np.ndarray,
162191
geography,
@@ -551,6 +580,7 @@ def build_h5(
551580
}
552581
hh_state_fips = clone_geo["state_fips"].astype(np.int32)
553582
original_hh_ids = household_ids[active_hh].astype(np.int64)
583+
reported_anchors = _build_reported_takeup_anchors(data, time_period)
554584

555585
takeup_results = apply_block_takeup_to_arrays(
556586
hh_blocks=active_blocks,
@@ -561,6 +591,7 @@ def build_h5(
561591
entity_counts=entity_counts,
562592
time_period=time_period,
563593
takeup_filter=takeup_filter,
594+
reported_anchors=reported_anchors,
564595
)
565596
for var_name, bools in takeup_results.items():
566597
data[var_name] = {time_period: bools}

policyengine_us_data/calibration/unified_matrix_builder.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from collections import defaultdict
1515
from typing import Dict, List, Optional, Tuple
1616

17+
import h5py
1718
import numpy as np
1819
import pandas as pd
1920
from scipy import sparse
@@ -668,6 +669,7 @@ def _process_single_clone(
668669
entity_hh_idx_map = sd.get("entity_hh_idx_map", {})
669670
entity_to_person_idx = sd.get("entity_to_person_idx", {})
670671
precomputed_rates = sd.get("precomputed_rates", {})
672+
reported_takeup_anchors = sd.get("reported_takeup_anchors", {})
671673

672674
# Slice geography for this clone
673675
clone_states = geo_states[col_start:col_end]
@@ -789,6 +791,7 @@ def _process_single_clone(
789791
ent_blocks,
790792
ent_hh_ids,
791793
ent_ci,
794+
reported_mask=reported_takeup_anchors.get(takeup_var),
792795
)
793796

794797
ent_values = (ent_eligible * ent_takeup).astype(np.float32)
@@ -2132,6 +2135,7 @@ def build_matrix(
21322135
from policyengine_us_data.utils.takeup import (
21332136
TAKEUP_AFFECTED_TARGETS,
21342137
compute_block_takeup_for_entities,
2138+
reported_subsidized_marketplace_by_tax_unit,
21352139
)
21362140
from policyengine_us_data.parameters import (
21372141
load_take_up_rate,
@@ -2160,6 +2164,37 @@ def build_matrix(
21602164
"person": person_hh_indices,
21612165
}
21622166

2167+
reported_takeup_anchors = {}
2168+
with h5py.File(self.dataset_path, "r") as f:
2169+
period_key = str(self.time_period)
2170+
if (
2171+
"reported_has_subsidized_marketplace_health_coverage_at_interview"
2172+
in f
2173+
and period_key
2174+
in f[
2175+
"reported_has_subsidized_marketplace_health_coverage_at_interview"
2176+
]
2177+
):
2178+
person_marketplace = f[
2179+
"reported_has_subsidized_marketplace_health_coverage_at_interview"
2180+
][period_key][...].astype(bool)
2181+
person_tax_unit_ids = f["person_tax_unit_id"][period_key][...]
2182+
tax_unit_ids = f["tax_unit_id"][period_key][...]
2183+
reported_takeup_anchors["takes_up_aca_if_eligible"] = (
2184+
reported_subsidized_marketplace_by_tax_unit(
2185+
person_tax_unit_ids,
2186+
tax_unit_ids,
2187+
person_marketplace,
2188+
)
2189+
)
2190+
if (
2191+
"has_medicaid_health_coverage_at_interview" in f
2192+
and period_key in f["has_medicaid_health_coverage_at_interview"]
2193+
):
2194+
reported_takeup_anchors["takes_up_medicaid_if_eligible"] = f[
2195+
"has_medicaid_health_coverage_at_interview"
2196+
][period_key][...].astype(bool)
2197+
21632198
entity_to_person_idx = {}
21642199
for entity_level in ("spm_unit", "tax_unit"):
21652200
ent_ids = sim.calculate(
@@ -2200,6 +2235,7 @@ def build_matrix(
22002235
self.household_ids = household_ids
22012236
self.precomputed_rates = precomputed_rates
22022237
self.affected_target_info = affected_target_info
2238+
self.reported_takeup_anchors = reported_takeup_anchors
22032239

22042240
# 5d. Clone loop
22052241
from pathlib import Path
@@ -2249,6 +2285,7 @@ def build_matrix(
22492285
shared_data["entity_hh_idx_map"] = entity_hh_idx_map
22502286
shared_data["entity_to_person_idx"] = entity_to_person_idx
22512287
shared_data["precomputed_rates"] = precomputed_rates
2288+
shared_data["reported_takeup_anchors"] = reported_takeup_anchors
22522289

22532290
logger.info(
22542291
"Starting parallel clone processing: %d clones, %d workers",
@@ -2452,6 +2489,7 @@ def build_matrix(
24522489
ent_blocks,
24532490
ent_hh_ids,
24542491
ent_ci,
2492+
reported_mask=reported_takeup_anchors.get(takeup_var),
24552493
)
24562494

24572495
ent_values = (ent_eligible * ent_takeup).astype(np.float32)

policyengine_us_data/datasets/cps/census_cps.py

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,52 @@
77
from policyengine_us_data.storage import STORAGE_FOLDER
88

99

10+
OPTIONAL_PERSON_COLUMNS = {
11+
"NOW_COV",
12+
"NOW_DIR",
13+
"NOW_MRK",
14+
"NOW_MRKS",
15+
"NOW_MRKUN",
16+
"NOW_NONM",
17+
"NOW_PRIV",
18+
"NOW_PUB",
19+
"NOW_GRP",
20+
"NOW_CAID",
21+
"NOW_MCAID",
22+
"NOW_PCHIP",
23+
"NOW_OTHMT",
24+
"NOW_MCARE",
25+
"NOW_MIL",
26+
"NOW_CHAMPVA",
27+
"NOW_VACARE",
28+
"NOW_IHSFLG",
29+
}
30+
31+
32+
def _resolve_person_usecols(
33+
available_columns, spm_unit_columns: list[str]
34+
) -> list[str]:
35+
requested_columns = PERSON_COLUMNS + spm_unit_columns + TAX_UNIT_COLUMNS
36+
available_columns = set(available_columns)
37+
missing_required = sorted(
38+
column
39+
for column in requested_columns
40+
if column not in available_columns and column not in OPTIONAL_PERSON_COLUMNS
41+
)
42+
if missing_required:
43+
raise KeyError(
44+
"Missing required CPS person columns: " + ", ".join(missing_required[:10])
45+
)
46+
return [column for column in requested_columns if column in available_columns]
47+
48+
49+
def _fill_missing_optional_person_columns(person: pd.DataFrame) -> pd.DataFrame:
50+
for column in OPTIONAL_PERSON_COLUMNS:
51+
if column not in person.columns:
52+
person[column] = 0
53+
return person
54+
55+
1056
class CensusCPS(Dataset):
1157
"""Dataset containing CPS ASEC tables in the Census format."""
1258

@@ -59,12 +105,19 @@ def generate(self):
59105
file_prefix = "cpspb/asec/prod/data/2019/"
60106
else:
61107
file_prefix = ""
62-
with zipfile.open(f"{file_prefix}pppub{file_year_code}.csv") as f:
63-
storage["person"] = pd.read_csv(
108+
person_path = f"{file_prefix}pppub{file_year_code}.csv"
109+
with zipfile.open(person_path) as f:
110+
person_columns = pd.read_csv(f, nrows=0).columns
111+
person_usecols = _resolve_person_usecols(
112+
person_columns, spm_unit_columns
113+
)
114+
with zipfile.open(person_path) as f:
115+
person = pd.read_csv(
64116
f,
65-
usecols=PERSON_COLUMNS + spm_unit_columns + TAX_UNIT_COLUMNS,
117+
usecols=person_usecols,
66118
).fillna(0)
67-
person = storage["person"]
119+
person = _fill_missing_optional_person_columns(person)
120+
storage["person"] = person
68121
with zipfile.open(f"{file_prefix}ffpub{file_year_code}.csv") as f:
69122
person_family_id = person.PH_SEQ * 10 + person.PF_SEQ
70123
family = pd.read_csv(f).fillna(0)
@@ -236,7 +289,24 @@ class CensusCPS_2018(CensusCPS):
236289
"A_AGE",
237290
"A_SEX",
238291
"PEDISEYE",
292+
"NOW_COV",
293+
"NOW_DIR",
239294
"NOW_MRK",
295+
"NOW_MRKS",
296+
"NOW_MRKUN",
297+
"NOW_NONM",
298+
"NOW_PRIV",
299+
"NOW_PUB",
300+
"NOW_GRP",
301+
"NOW_CAID",
302+
"NOW_MCAID",
303+
"NOW_PCHIP",
304+
"NOW_OTHMT",
305+
"NOW_MCARE",
306+
"NOW_MIL",
307+
"NOW_CHAMPVA",
308+
"NOW_VACARE",
309+
"NOW_IHSFLG",
240310
"WSAL_VAL",
241311
"INT_VAL",
242312
"SEMP_VAL",
@@ -294,7 +364,6 @@ class CensusCPS_2018(CensusCPS):
294364
"PMED_VAL",
295365
"PEMCPREM",
296366
"PRCITSHP",
297-
"NOW_GRP",
298367
"POCCU2",
299368
"PEINUSYR",
300369
"MCARE",

0 commit comments

Comments
 (0)