From 9f22b0ec3fe6da417b967789a0eea547fc014456 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 14 Mar 2026 17:14:19 -0600 Subject: [PATCH 1/3] Align SSI takeup and disability flags to CPS-reported receipt People who report receiving SSI in the CPS were getting $0 SSI in PE because (a) random takeup excluded them, and (b) some lacked CPS disability flags needed for eligibility. This adds two fixes in add_takeup(): 1. Sets takes_up_ssi_if_eligible=True for SSI reporters, with an adjusted random draw for non-reporters to hit the 50% target rate 2. Sets is_disabled=True for SSI reporters under 65 (SSI requires aged OR disabled/blind, so under-65 recipients must be disabled) Together these reduce the simulated poverty rate by ~0.6pp. Co-Authored-By: Claude Opus 4.6 (1M context) --- policyengine_us_data/datasets/cps/cps.py | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 418d7396..11bc83cc 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -340,6 +340,38 @@ def add_takeup(self): rng.random(n_tax_units) < voluntary_filing_rate ) + # --- SSI: align takeup and disability to CPS-reported receipt --- + # Without this, PE computes SSI eligibility from rules and then + # randomly assigns takeup, so many CPS respondents who actually + # report receiving SSI get $0 in the simulation. This inflates + # PE's poverty rate by ~0.6pp. + n_persons = len(data["person_id"]) + reported_ssi = data["ssi_reported"] > 0 + + # 1) Takeup: guarantee SSI for reporters; adjusted random draw + # for non-reporters so overall rate matches ~50% (Urban + # Institute estimate for adults 65+). + SSI_TAKEUP_RATE = 0.50 + n_reporters = reported_ssi.sum() + n_non_reporters = (~reported_ssi).sum() + target_takeup = int(SSI_TAKEUP_RATE * n_persons) + remaining_needed = max(0, target_takeup - n_reporters) + non_reporter_rate = ( + remaining_needed / n_non_reporters if n_non_reporters > 0 else 0 + ) + + ssi_rng = np.random.default_rng(seed=200) + data["takes_up_ssi_if_eligible"] = reported_ssi | ( + (~reported_ssi) & (ssi_rng.random(n_persons) < non_reporter_rate) + ) + + # 2) Disability: if someone reports SSI and is under 65 they + # must be disabled (SSI requires aged OR disabled/blind). + # CPS disability flags (PEDIS* columns) miss some of these. + ages = data["age"] + under_65 = ages < 65 + data["is_disabled"] = data["is_disabled"] | (reported_ssi & under_65) + self.save_dataset(data) From 95e85b21f8c26485ccf9989c0289fba5e702a1db Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 16 Mar 2026 21:16:18 -0400 Subject: [PATCH 2/3] Add changelog fragment for SSI takeup alignment Co-Authored-By: Claude Opus 4.6 (1M context) --- changelog.d/align-ssi-takeup-to-reported.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/align-ssi-takeup-to-reported.changed.md diff --git a/changelog.d/align-ssi-takeup-to-reported.changed.md b/changelog.d/align-ssi-takeup-to-reported.changed.md new file mode 100644 index 00000000..3f3e1126 --- /dev/null +++ b/changelog.d/align-ssi-takeup-to-reported.changed.md @@ -0,0 +1 @@ +Align SSI takeup and disability flags to CPS-reported receipt. From ab9de8c526d3df3eeb9d7daa6ee6b9ad8ed8cf17 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 16 Mar 2026 21:28:39 -0400 Subject: [PATCH 3/3] Format cps.py Co-Authored-By: Claude Opus 4.6 (1M context) --- policyengine_us_data/datasets/cps/cps.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/policyengine_us_data/datasets/cps/cps.py b/policyengine_us_data/datasets/cps/cps.py index 11bc83cc..f7f20fce 100644 --- a/policyengine_us_data/datasets/cps/cps.py +++ b/policyengine_us_data/datasets/cps/cps.py @@ -356,9 +356,7 @@ def add_takeup(self): n_non_reporters = (~reported_ssi).sum() target_takeup = int(SSI_TAKEUP_RATE * n_persons) remaining_needed = max(0, target_takeup - n_reporters) - non_reporter_rate = ( - remaining_needed / n_non_reporters if n_non_reporters > 0 else 0 - ) + non_reporter_rate = remaining_needed / n_non_reporters if n_non_reporters > 0 else 0 ssi_rng = np.random.default_rng(seed=200) data["takes_up_ssi_if_eligible"] = reported_ssi | (