From ca68f53dc22c7bbfe2911d3e9fabe108dd49fd51 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Feb 2026 15:40:48 -0800 Subject: [PATCH 1/3] Add TANF take-up to microdata construction Add state-level TANF takeup rates derived from ACF FY2023 caseload data and Census ACS poverty estimates, following the same pattern as Medicaid (state-specific rates) and SNAP (SPM unit level assignment). Priors are TANF-to-poverty ratios scaled ~2x to approximate takeup among eligible families. These will be adjusted during calibration against the existing $9B national cash assistance target. Closes #535 Co-Authored-By: Claude Opus 4.6 --- policyengine_us_data/datasets/cps/cps.py | 29 ++++++-- .../parameters/take_up/tanf.yaml | 71 +++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 policyengine_us_data/parameters/take_up/tanf.yaml diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 4a5fa36d..161aaf7e 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -207,6 +207,7 @@ def add_takeup(self): snap_rate = load_take_up_rate("snap", self.time_period) aca_rate = load_take_up_rate("aca", self.time_period) medicaid_rates_by_state = load_take_up_rate("medicaid", self.time_period) + tanf_rates_by_state = load_take_up_rate("tanf", self.time_period) head_start_rate = load_take_up_rate("head_start", self.time_period) early_head_start_rate = load_take_up_rate( "early_head_start", self.time_period @@ -232,15 +233,35 @@ def add_takeup(self): rng = seeded_rng("takes_up_snap_if_eligible") data["takes_up_snap_if_eligible"] = rng.random(n_spm_units) < snap_rate + # TANF: state-specific rates at SPM unit level + state_codes = baseline.calculate("state_code_str").values + hh_ids = data["household_id"] + person_hh_ids = data["person_household_id"] + hh_to_state = dict(zip(hh_ids, state_codes)) + spm_unit_ids = data["spm_unit_id"] + person_spm_unit_ids = data["person_spm_unit_id"] + # Map each SPM unit to its state via its first member's household + spm_to_state = {} + for person_idx, spm_id in enumerate(person_spm_unit_ids): + if spm_id not in spm_to_state: + hh_id = person_hh_ids[person_idx] + spm_to_state[spm_id] = hh_to_state.get(hh_id, "CA") + tanf_rate_by_spm = np.array( + [ + tanf_rates_by_state.get(spm_to_state.get(spm_id, "CA"), 0.21) + for spm_id in spm_unit_ids + ] + ) + rng = seeded_rng("takes_up_tanf_if_eligible") + data["takes_up_tanf_if_eligible"] = ( + rng.random(n_spm_units) < tanf_rate_by_spm + ) + # ACA rng = seeded_rng("takes_up_aca_if_eligible") data["takes_up_aca_if_eligible"] = rng.random(n_tax_units) < aca_rate # Medicaid: state-specific rates - state_codes = baseline.calculate("state_code_str").values - hh_ids = data["household_id"] - person_hh_ids = data["person_household_id"] - hh_to_state = dict(zip(hh_ids, state_codes)) person_states = np.array( [hh_to_state.get(hh_id, "CA") for hh_id in person_hh_ids] ) diff --git a/policyengine_us_data/parameters/take_up/tanf.yaml b/policyengine_us_data/parameters/take_up/tanf.yaml new file mode 100644 index 00000000..bfa37a2d --- /dev/null +++ b/policyengine_us_data/parameters/take_up/tanf.yaml @@ -0,0 +1,71 @@ +description: >- + Percentage of eligible TANF families who receive TANF cash assistance. + Priors derived from ACF FY2023 TANF caseload data divided by Census + ACS 2023 families in poverty, scaled up ~2x to approximate takeup + among eligible families (since eligible families are roughly half of + families in poverty due to asset tests, work requirements, and time + limits). These priors will be adjusted during calibration against the + $9B national cash assistance target. +metadata: + label: TANF takeup rate + unit: /1 + period: year + breakdown: + - state_code + reference: + - title: ACF FY2023 TANF Caseload Data + href: https://acf.gov/ofa/data/tanf-caseload-data-2023 + - title: Census Bureau 2023 ACS 1-Year Estimates (Table S1702) + href: https://data.census.gov/table/ACSST1Y2023.S1702 +rates_by_state: + AK: 0.18 + AL: 0.10 + AR: 0.03 + AZ: 0.08 + CA: 0.91 + CO: 0.33 + CT: 0.21 + DC: 0.54 + DE: 0.37 + FL: 0.21 + GA: 0.05 + HI: 0.36 + IA: 0.21 + ID: 0.12 + IL: 0.11 + IN: 0.08 + KS: 0.14 + KY: 0.22 + LA: 0.08 + MA: 0.83 + MD: 0.45 + ME: 0.38 + MI: 0.11 + MN: 0.46 + MO: 0.10 + MS: 0.04 + MT: 0.23 + NC: 0.10 + ND: 0.14 + NE: 0.24 + NH: 0.45 + NJ: 0.19 + NM: 0.32 + NV: 0.24 + NY: 0.52 + OH: 0.39 + OK: 0.09 + OR: 0.62 + PA: 0.29 + RI: 0.42 + SC: 0.12 + SD: 0.40 + TN: 0.22 + TX: 0.04 + UT: 0.13 + VA: 0.36 + VT: 0.43 + WA: 0.66 + WI: 0.37 + WV: 0.26 + WY: 0.15 From 11802318a275120f78259f5c80ec3aa052209ce1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 13 Feb 2026 16:29:53 -0800 Subject: [PATCH 2/3] Simplify TANF takeup code - Separate shared state lookup from TANF-specific logic - Remove unnecessary intermediate variables - Replace redundant .get() fallback with direct dict access - Update docstring to reflect TANF as rates_by_state consumer Co-Authored-By: Claude Opus 4.6 --- policyengine_us_data/datasets/cps/cps.py | 14 +++++++------- policyengine_us_data/parameters/__init__.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 161aaf7e..6ffa2808 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -233,23 +233,23 @@ def add_takeup(self): rng = seeded_rng("takes_up_snap_if_eligible") data["takes_up_snap_if_eligible"] = rng.random(n_spm_units) < snap_rate - # TANF: state-specific rates at SPM unit level + # Shared state lookup used by TANF and Medicaid state_codes = baseline.calculate("state_code_str").values hh_ids = data["household_id"] person_hh_ids = data["person_household_id"] hh_to_state = dict(zip(hh_ids, state_codes)) - spm_unit_ids = data["spm_unit_id"] - person_spm_unit_ids = data["person_spm_unit_id"] - # Map each SPM unit to its state via its first member's household + + # TANF: state-specific rates at SPM unit level + # Map each SPM unit to a state via its first member's household spm_to_state = {} - for person_idx, spm_id in enumerate(person_spm_unit_ids): + for person_idx, spm_id in enumerate(data["person_spm_unit_id"]): if spm_id not in spm_to_state: hh_id = person_hh_ids[person_idx] spm_to_state[spm_id] = hh_to_state.get(hh_id, "CA") tanf_rate_by_spm = np.array( [ - tanf_rates_by_state.get(spm_to_state.get(spm_id, "CA"), 0.21) - for spm_id in spm_unit_ids + tanf_rates_by_state.get(spm_to_state[spm_id], 0.21) + for spm_id in data["spm_unit_id"] ] ) rng = seeded_rng("takes_up_tanf_if_eligible") diff --git a/policyengine_us_data/parameters/__init__.py b/policyengine_us_data/parameters/__init__.py index 2fcddb5a..f49a5ba7 100644 --- a/policyengine_us_data/parameters/__init__.py +++ b/policyengine_us_data/parameters/__init__.py @@ -19,7 +19,7 @@ def load_take_up_rate(variable_name: str, year: int = 2018): year: Year for which to get the rate Returns: - float, dict (EITC rates_by_children), or dict (Medicaid + float, dict (EITC rates_by_children), or dict (Medicaid/TANF rates_by_state) """ yaml_path = PARAMETERS_DIR / "take_up" / f"{variable_name}.yaml" @@ -31,7 +31,7 @@ def load_take_up_rate(variable_name: str, year: int = 2018): if "rates_by_children" in data: return data["rates_by_children"] - # Medicaid: state-specific rates + # State-specific rates (Medicaid, TANF, etc.) if "rates_by_state" in data: return data["rates_by_state"] From fbf903e17facd8130db5cf360d3dd0fe36f590d9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 14 Feb 2026 14:25:34 -0800 Subject: [PATCH 3/3] Add changelog entry for TANF takeup Co-Authored-By: Claude Opus 4.6 --- changelog_entry.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29b..74643c86 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + added: + - Add state-level TANF takeup rates and assign takeup during CPS microdata construction.