From 13ca491bd80edeaa898a8aa33208360fd590863f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 15 Sep 2026 13:17:52 -0400 Subject: [PATCH 1/5] Add the 2025 SPM anchored-threshold recomputation from the 2026 ASEC public-use file Replicates P60-290's SPM and official toplines from pppub26.csv with person weights, then recomputes 2025 SPM poverty with national thresholds held at 2024 times CPI-U (and C-CPI-U): 12.3 percent for all people, 12.3 for children, 14.7 for people 65 and over, so threshold growth beyond prices accounts for +0.8, +1.1 and +0.7 points of the published rates. Co-Authored-By: Claude Fable 5.1 --- .../asec-2026-anchored-thresholds/README.md | 28 +++++++++++ .../anchored_results.json | 46 +++++++++++++++++++ .../anchored_results_chained.json | 46 +++++++++++++++++++ .../bls_ccpi.json | 5 ++ .../replicate_and_anchor.py | 28 +++++++++++ 5 files changed, 153 insertions(+) create mode 100644 analysis/asec-2026-anchored-thresholds/README.md create mode 100644 analysis/asec-2026-anchored-thresholds/anchored_results.json create mode 100644 analysis/asec-2026-anchored-thresholds/anchored_results_chained.json create mode 100644 analysis/asec-2026-anchored-thresholds/bls_ccpi.json create mode 100644 analysis/asec-2026-anchored-thresholds/replicate_and_anchor.py diff --git a/analysis/asec-2026-anchored-thresholds/README.md b/analysis/asec-2026-anchored-thresholds/README.md new file mode 100644 index 0000000..462994c --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/README.md @@ -0,0 +1,28 @@ +# 2025 SPM poverty with thresholds anchored to 2024 + +Computed September 15, 2026, the day Census released *Poverty in the United States: 2025* (P60-290), from the 2026 CPS ASEC public-use person file (`asecpub26csv.zip`, `pppub26.csv`, posted at www2.census.gov/programs-surveys/cps/datasets/2026/march/). + +## Replication + +Person weight `MARSUPWT`. SPM poverty (`SPM_POOR`): 13.11 percent of all people, 13.39 percent of children, 12.28 percent of people 18 to 64, 15.38 percent of people 65 and over; 44,390 thousand people and 9,653 thousand children below the threshold. Published (Table 8): 13.1 / 13.4 / 12.3 / 15.4; 44,390 and 9,657 thousand. Official poverty (`PERLIS` = 1 within `POV_UNIV` = 1): 10.20 / 13.41 / 9.20 / 9.77 against 10.2 / 13.4 / 9.2 / 9.8. + +## Anchoring + +Each unit's `SPM_POVTHRESHOLD` is scaled by tenure so that the national two-adult, two-child base equals its 2024 value (BLS corrected series: owners with a mortgage 39,231; owners without 32,879; renters 39,220) times the CPI-U ratio Census used for the 2025 official thresholds (32,649 / 31,812 = 1.02631). Tenure codes were verified from the implied 2025 bases (`SPM_POVTHRESHOLD / (SPM_EQUIVSCALE × SPM_GEOADJ)`): 1 = 41,323, 2 = 34,326, 3 = 41,701. The equivalence scale and geographic adjustment stay at their published 2025 values. A person is poor when `SPM_RESOURCES` is below the anchored threshold. + +| SPM poverty rate, percent | 2024 (re-based, P60-290) | 2025 published | 2025 at 2024 thresholds × CPI-U | 2025 at 2024 thresholds × C-CPI-U | +| --- | ---: | ---: | ---: | ---: | +| All people | 13.0 | 13.11 | 12.31 | 12.29 | +| Under 18 | 13.5 | 13.39 | 12.31 | 12.30 | +| 18 to 64 | 12.2 | 12.28 | 11.55 | 11.53 | +| 65 and over | 15.1 | 15.38 | 14.66 | 14.62 | + +Threshold growth beyond CPI-U adds 0.80 points for all people, 1.08 for children, 0.73 for people 18 to 64 and 0.72 for people 65 and over. By tenure: renters 23.96 published against 22.13 anchored, owners without a mortgage 11.89 against 11.49, owners with a mortgage 6.00 against 5.67. + +The chained variant uses C-CPI-U (`SUUR0000SA0`) annual means, 174.372 for 2024 and 178.697 for 2025 (eleven months; October 2025 was not published), a ratio of 1.02480; recent C-CPI-U values are interim. + +## Files + +- `replicate_and_anchor.py`: run in a directory containing `pppub26.csv`; optional argument is the price factor (default 32649/31812). +- `anchored_results.json`, `anchored_results_chained.json`: outputs. +- `bls_ccpi.json`: BLS API response for `SUUR0000SA0` and `CUUR0000SA0`, 2024 to 2025. diff --git a/analysis/asec-2026-anchored-thresholds/anchored_results.json b/analysis/asec-2026-anchored-thresholds/anchored_results.json new file mode 100644 index 0000000..e05202c --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/anchored_results.json @@ -0,0 +1,46 @@ +{ + "cpi_factor": 1.02631, + "anchor_factors": { + "1": 0.9744, + "2": 0.983, + "3": 0.9653 + }, + "spm_published_replication": { + "all": 13.11, + "children": 13.39, + "18-64": 12.28, + "65+": 15.38 + }, + "official_replication": { + "all": 10.2, + "children": 13.41, + "18-64": 9.2, + "65+": 9.77 + }, + "spm_anchored_2024_thresholds": { + "all": 12.31, + "children": 12.31, + "18-64": 11.55, + "65+": 14.66 + }, + "by_tenure": { + "owner_with_mortgage": { + "published": 6.0, + "anchored": 5.67 + }, + "owner_without_mortgage": { + "published": 11.89, + "anchored": 11.49 + }, + "renter": { + "published": 23.96, + "anchored": 22.13 + } + }, + "threshold_effect_pp": { + "all": 0.8, + "children": 1.08, + "18-64": 0.73, + "65+": 0.72 + } +} diff --git a/analysis/asec-2026-anchored-thresholds/anchored_results_chained.json b/analysis/asec-2026-anchored-thresholds/anchored_results_chained.json new file mode 100644 index 0000000..437664f --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/anchored_results_chained.json @@ -0,0 +1,46 @@ +{ + "cpi_factor": 1.0248, + "anchor_factors": { + "1": 0.9729, + "2": 0.9816, + "3": 0.9638 + }, + "spm_published_replication": { + "all": 13.11, + "children": 13.39, + "18-64": 12.28, + "65+": 15.38 + }, + "official_replication": { + "all": 10.2, + "children": 13.41, + "18-64": 9.2, + "65+": 9.77 + }, + "spm_anchored_2024_thresholds": { + "all": 12.29, + "children": 12.3, + "18-64": 11.53, + "65+": 14.62 + }, + "by_tenure": { + "owner_with_mortgage": { + "published": 6.0, + "anchored": 5.66 + }, + "owner_without_mortgage": { + "published": 11.89, + "anchored": 11.48 + }, + "renter": { + "published": 23.96, + "anchored": 22.09 + } + }, + "threshold_effect_pp": { + "all": 0.82, + "children": 1.09, + "18-64": 0.75, + "65+": 0.76 + } +} diff --git a/analysis/asec-2026-anchored-thresholds/bls_ccpi.json b/analysis/asec-2026-anchored-thresholds/bls_ccpi.json new file mode 100644 index 0000000..1a86eb0 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/bls_ccpi.json @@ -0,0 +1,5 @@ +{"status":"REQUEST_SUCCEEDED","responseTime":94,"message":[],"Results":{ +"series": +[{"seriesID":"SUUR0000SA0","data":[{"year":"2025","period":"M12","periodName":"December","value":"179.775","footnotes":[{"code":"U","text":"Interim"}]},{"year":"2025","period":"M11","periodName":"November","value":"179.889","footnotes":[{"code":"U","text":"Interim"}]},{"year":"2025","period":"M09","periodName":"September","value":"180.373","footnotes":[{}]},{"year":"2025","period":"M08","periodName":"August","value":"179.839","footnotes":[{}]},{"year":"2025","period":"M07","periodName":"July","value":"179.272","footnotes":[{}]},{"year":"2025","period":"M06","periodName":"June","value":"178.991","footnotes":[{}]},{"year":"2025","period":"M05","periodName":"May","value":"178.427","footnotes":[{}]},{"year":"2025","period":"M04","periodName":"April","value":"178.081","footnotes":[{}]},{"year":"2025","period":"M03","periodName":"March","value":"177.520","footnotes":[{}]},{"year":"2025","period":"M02","periodName":"February","value":"177.134","footnotes":[{}]},{"year":"2025","period":"M01","periodName":"January","value":"176.362","footnotes":[{}]},{"year":"2024","period":"M12","periodName":"December","value":"175.219","footnotes":[{}]},{"year":"2024","period":"M11","periodName":"November","value":"175.180","footnotes":[{}]},{"year":"2024","period":"M10","periodName":"October","value":"175.346","footnotes":[{}]},{"year":"2024","period":"M09","periodName":"September","value":"175.099","footnotes":[{}]},{"year":"2024","period":"M08","periodName":"August","value":"174.848","footnotes":[{}]},{"year":"2024","period":"M07","periodName":"July","value":"174.792","footnotes":[{}]},{"year":"2024","period":"M06","periodName":"June","value":"174.721","footnotes":[{}]},{"year":"2024","period":"M05","periodName":"May","value":"174.685","footnotes":[{}]},{"year":"2024","period":"M04","periodName":"April","value":"174.424","footnotes":[{}]},{"year":"2024","period":"M03","periodName":"March","value":"173.796","footnotes":[{}]},{"year":"2024","period":"M02","periodName":"February","value":"172.700","footnotes":[{}]},{"year":"2024","period":"M01","periodName":"January","value":"171.649","footnotes":[{}]}]}, +{"seriesID":"CUUR0000SA0","data":[{"year":"2025","period":"M13","periodName":"Annual","value":"321.943","footnotes":[{}]},{"year":"2025","period":"M12","periodName":"December","value":"324.054","footnotes":[{}]},{"year":"2025","period":"M11","periodName":"November","value":"324.122","footnotes":[{}]},{"year":"2025","period":"M10","periodName":"October","value":"-","footnotes":[{"code":"X","text":"Data unavailable due to the 2025 lapse in appropriations"}]},{"year":"2025","period":"M09","periodName":"September","value":"324.800","footnotes":[{}]},{"year":"2025","period":"M08","periodName":"August","value":"323.976","footnotes":[{}]},{"year":"2025","period":"M07","periodName":"July","value":"323.048","footnotes":[{}]},{"year":"2025","period":"M06","periodName":"June","value":"322.561","footnotes":[{}]},{"year":"2025","period":"M05","periodName":"May","value":"321.465","footnotes":[{}]},{"year":"2025","period":"M04","periodName":"April","value":"320.795","footnotes":[{}]},{"year":"2025","period":"M03","periodName":"March","value":"319.799","footnotes":[{}]},{"year":"2025","period":"M02","periodName":"February","value":"319.082","footnotes":[{}]},{"year":"2025","period":"M01","periodName":"January","value":"317.671","footnotes":[{}]},{"year":"2024","period":"M13","periodName":"Annual","value":"313.689","footnotes":[{}]},{"year":"2024","period":"M12","periodName":"December","value":"315.605","footnotes":[{}]},{"year":"2024","period":"M11","periodName":"November","value":"315.493","footnotes":[{}]},{"year":"2024","period":"M10","periodName":"October","value":"315.664","footnotes":[{}]},{"year":"2024","period":"M09","periodName":"September","value":"315.301","footnotes":[{}]},{"year":"2024","period":"M08","periodName":"August","value":"314.796","footnotes":[{}]},{"year":"2024","period":"M07","periodName":"July","value":"314.540","footnotes":[{}]},{"year":"2024","period":"M06","periodName":"June","value":"314.175","footnotes":[{}]},{"year":"2024","period":"M05","periodName":"May","value":"314.069","footnotes":[{}]},{"year":"2024","period":"M04","periodName":"April","value":"313.548","footnotes":[{}]},{"year":"2024","period":"M03","periodName":"March","value":"312.332","footnotes":[{}]},{"year":"2024","period":"M02","periodName":"February","value":"310.326","footnotes":[{}]},{"year":"2024","period":"M01","periodName":"January","value":"308.417","footnotes":[{}]}]}] +}} \ No newline at end of file diff --git a/analysis/asec-2026-anchored-thresholds/replicate_and_anchor.py b/analysis/asec-2026-anchored-thresholds/replicate_and_anchor.py new file mode 100644 index 0000000..0d60c6e --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/replicate_and_anchor.py @@ -0,0 +1,28 @@ +"""Replicate P60-290 SPM/official toplines from the 2026 CPS ASEC public-use person file +and recompute 2025 SPM poverty with national thresholds anchored to 2024 (x CPI-U). +Weights: SPM_WEIGHT/100 for SPM, MARSUPWT/100 for official. Tenure codes verified from +the implied base threshold: 1 owner w/ mortgage 41,323; 2 owner w/o 34,326; 3 renter 41,701. +Anchoring scales only the national base by tenure (equivalence scale and geo adjustment +stay at 2025 values). 2024 bases = BLS corrected series (39,231 / 32,879 / 39,220). +CPI-U factor = Census's own 2025/2024 two-adult-two-child OPM threshold ratio. +""" +import sys, json, pandas as pd, numpy as np +cpi = float(sys.argv[1]) if len(sys.argv) > 1 else 32649/31812 +d = pd.read_csv('pppub26.csv', usecols=['A_AGE','MARSUPWT','SPM_WEIGHT','SPM_POOR','SPM_RESOURCES','SPM_POVTHRESHOLD','SPM_TENMORTSTATUS','PERLIS','POV_UNIV']) +mw = d.MARSUPWT/100; w = mw # person weight for person-level poverty rates (SPM_WEIGHT variant checked: within 0.05pp) +grp = np.select([d.A_AGE<18, d.A_AGE>=65], ['children','65+'], '18-64') +t25 = {1:41322.707, 2:34325.998, 3:41700.556}; t24 = {1:39231.0, 2:32879.0, 3:39220.0} +fac = d.SPM_TENMORTSTATUS.map({k:(t24[k]*cpi)/t25[k] for k in t25}) +poor_anch = d.SPM_RESOURCES < d.SPM_POVTHRESHOLD*fac +def rates(flag, wt): + return {g: round(100*(wt[m]*flag[m]).sum()/wt[m].sum(), 2) for g,m in + [('all', np.ones(len(d),bool)), ('children', grp=='children'), ('18-64', grp=='18-64'), ('65+', grp=='65+')]} +out = {"cpi_factor": round(cpi,5), "anchor_factors": {k: round((t24[k]*cpi)/t25[k],4) for k in t25}, + "spm_published_replication": rates(d.SPM_POOR==1, w), + "official_replication": {g: round(100*(mw[m]*(d.PERLIS[m]==1)).sum()/mw[m].sum(),2) for g,m in + [('all', d.POV_UNIV==1), ('children', (grp=='children')&(d.POV_UNIV==1)), ('18-64', (grp=='18-64')&(d.POV_UNIV==1)), ('65+', (grp=='65+')&(d.POV_UNIV==1))]}, + "spm_anchored_2024_thresholds": rates(poor_anch, w), + "by_tenure": {name: {"published": round(100*(w[m]*(d.SPM_POOR[m]==1)).sum()/w[m].sum(),2), "anchored": round(100*(w[m]*poor_anch[m]).sum()/w[m].sum(),2)} + for name,m in [('owner_with_mortgage', d.SPM_TENMORTSTATUS==1), ('owner_without_mortgage', d.SPM_TENMORTSTATUS==2), ('renter', d.SPM_TENMORTSTATUS==3)]}} +out["threshold_effect_pp"] = {g: round(out["spm_published_replication"][g]-out["spm_anchored_2024_thresholds"][g],2) for g in out["spm_published_replication"]} +print(json.dumps(out, indent=1)) From 4dd83566c10220365be19558f1a2a9625fa755ae Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 16 Sep 2026 07:26:14 -0400 Subject: [PATCH 2/5] Extend the anchored recomputation to states and detailed ages with replicate-weight standard errors Add state_age_anchored.py, which joins the 2026 ASEC household file for state, computes published and anchored 2025 SPM poverty for the 51 states and for detailed age groups, and estimates standard errors from the 160 replicate weights. Outputs in state_age_results.json and by_state.csv. Co-Authored-By: Claude Fable 5.1 --- .../asec-2026-anchored-thresholds/README.md | 4 + .../by_state.csv | 52 ++ .../state_age_anchored.py | 40 ++ .../state_age_results.json | 607 ++++++++++++++++++ 4 files changed, 703 insertions(+) create mode 100644 analysis/asec-2026-anchored-thresholds/by_state.csv create mode 100644 analysis/asec-2026-anchored-thresholds/state_age_anchored.py create mode 100644 analysis/asec-2026-anchored-thresholds/state_age_results.json diff --git a/analysis/asec-2026-anchored-thresholds/README.md b/analysis/asec-2026-anchored-thresholds/README.md index 462994c..3513c5b 100644 --- a/analysis/asec-2026-anchored-thresholds/README.md +++ b/analysis/asec-2026-anchored-thresholds/README.md @@ -26,3 +26,7 @@ The chained variant uses C-CPI-U (`SUUR0000SA0`) annual means, 174.372 for 2024 - `replicate_and_anchor.py`: run in a directory containing `pppub26.csv`; optional argument is the price factor (default 32649/31812). - `anchored_results.json`, `anchored_results_chained.json`: outputs. - `bls_ccpi.json`: BLS API response for `SUUR0000SA0` and `CUUR0000SA0`, 2024 to 2025. + +## By state and by detailed age + +`state_age_anchored.py` extends the recomputation to the 51 states (from `hhpub26.csv` `GESTFIPS`, joined to the person file on `H_SEQ`) and to detailed age groups, with standard errors from the 160 ASEC replicate weights (`asec_csv_repwgt_2026.csv`; variance = 4/160 times the sum of squared replicate deviations; `PWWGT0` equals `MARSUPWT`/100). Outputs: `state_age_results.json`, `by_state.csv`. Run it with the scratch directory holding the three CSV files as the first argument and the output directory as the second. diff --git a/analysis/asec-2026-anchored-thresholds/by_state.csv b/analysis/asec-2026-anchored-thresholds/by_state.csv new file mode 100644 index 0000000..17105c2 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/by_state.csv @@ -0,0 +1,52 @@ +state,published,published_se,anchored,anchored_se,effect_pp,effect_se,n_persons,weighted_persons_thousands +AL,15.75,1.14,14.04,1.19,1.71,0.52,2334,5079.0 +AK,10.37,1.22,10.02,1.18,0.35,0.34,1648,708.0 +AZ,14.78,1.62,13.41,1.47,1.36,0.51,2279,7666.0 +AR,12.67,1.42,11.87,1.4,0.8,0.33,2196,3073.0 +CA,17.29,0.66,16.22,0.63,1.08,0.2,13025,38913.0 +CO,11.36,2.17,10.9,2.26,0.46,0.29,1665,5950.0 +CT,10.56,1.32,9.75,1.27,0.81,0.31,1376,3651.0 +DE,7.51,1.15,6.14,0.98,1.37,0.72,1471,1066.0 +DC,16.76,1.4,15.65,1.43,1.12,0.46,1919,691.0 +FL,17.56,0.91,16.55,0.89,1.01,0.17,6342,23261.0 +GA,13.85,1.11,13.11,1.04,0.74,0.27,2881,11164.0 +HI,12.5,1.29,11.36,1.2,1.14,0.43,2427,1388.0 +ID,9.47,0.9,8.89,0.85,0.58,0.3,2366,1995.0 +IL,11.38,1.03,10.78,1.01,0.61,0.25,3297,12607.0 +IN,11.78,1.29,10.4,1.28,1.38,0.67,2024,6893.0 +IA,9.55,1.11,8.48,1.09,1.07,0.47,1634,3240.0 +KS,7.53,1.12,7.45,1.11,0.08,0.08,1678,2909.0 +KY,10.63,1.29,10.0,1.26,0.63,0.32,1461,4550.0 +LA,17.85,1.01,16.72,0.92,1.13,0.37,2869,4545.0 +ME,5.76,1.12,5.69,1.13,0.07,0.07,1061,1408.0 +MD,10.51,1.46,9.96,1.38,0.55,0.32,1724,6188.0 +MA,13.26,1.08,12.43,1.04,0.83,0.27,2732,7032.0 +MI,10.86,1.04,10.42,1.03,0.44,0.22,2517,10006.0 +MN,7.78,1.4,7.63,1.42,0.15,0.1,1694,5772.0 +MS,18.26,1.77,17.56,1.73,0.7,0.27,2446,2914.0 +MO,9.08,1.12,8.11,1.08,0.97,0.3,2010,6308.0 +MT,9.68,1.69,8.21,1.3,1.47,0.59,2006,1123.0 +NE,5.47,0.96,5.47,0.96,0.0,0.0,1885,2011.0 +NV,15.7,1.52,14.49,1.51,1.21,0.43,2116,3335.0 +NH,7.09,1.17,6.09,0.99,1.0,0.53,1608,1407.0 +NJ,11.53,1.07,10.95,1.03,0.58,0.23,2868,9488.0 +NM,14.95,1.36,14.25,1.22,0.7,0.46,3251,2103.0 +NY,16.42,1.06,14.92,1.03,1.5,0.37,5036,19683.0 +NC,11.69,1.12,11.23,1.05,0.46,0.22,3035,11050.0 +ND,8.86,1.12,8.57,1.17,0.29,0.17,1980,780.0 +OH,10.05,1.24,9.56,1.24,0.48,0.33,2952,11974.0 +OK,11.96,1.35,11.36,1.32,0.6,0.33,2090,4070.0 +OR,9.93,1.02,9.52,0.98,0.41,0.19,2241,4234.0 +PA,9.83,0.9,9.53,0.89,0.31,0.15,3367,12893.0 +RI,11.34,1.45,10.47,1.38,0.87,0.47,1196,1110.0 +SC,11.07,1.29,10.27,1.22,0.8,0.34,2302,5592.0 +SD,8.78,1.25,8.39,1.15,0.39,0.33,1455,913.0 +TN,12.41,1.13,11.66,1.09,0.75,0.27,2368,7196.0 +TX,15.27,0.74,14.25,0.71,1.02,0.22,8575,31542.0 +UT,10.35,1.04,10.03,1.04,0.31,0.13,2469,3510.0 +VT,9.08,1.14,8.76,1.11,0.32,0.2,1544,633.0 +VA,10.85,1.19,10.6,1.17,0.24,0.12,2380,8726.0 +WA,9.85,0.96,9.71,0.94,0.14,0.13,2752,7975.0 +WV,12.23,1.35,11.15,1.19,1.08,0.4,2547,1737.0 +WI,7.81,1.07,7.6,1.12,0.21,0.17,1754,5923.0 +WY,9.5,1.08,9.07,1.07,0.43,0.27,1876,568.0 diff --git a/analysis/asec-2026-anchored-thresholds/state_age_anchored.py b/analysis/asec-2026-anchored-thresholds/state_age_anchored.py new file mode 100644 index 0000000..03d886e --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/state_age_anchored.py @@ -0,0 +1,40 @@ +"""2025 SPM poverty, published and at 2024 thresholds x CPI-U, by state and by detailed age, +with replicate-weight standard errors. Inputs: pppub26.csv + asec_csv_repwgt_2026.csv (2026 CPS ASEC PU). +Anchoring identical to spm-threshold-paper analysis/asec-2026-anchored-thresholds/replicate_and_anchor.py.""" +import sys, json, numpy as np, pandas as pd +S = sys.argv[1]; OUT = sys.argv[2] +cols = ["PH_SEQ","PPPOS","A_AGE","MARSUPWT","SPM_POOR","SPM_RESOURCES","SPM_POVTHRESHOLD","SPM_TENMORTSTATUS"] +d = pd.read_csv(f"{S}/asec26/pppub26.csv", usecols=cols) +hh = pd.read_csv(f"{S}/asec26/hhpub26.csv", usecols=["H_SEQ","GESTFIPS"]) +d = d.merge(hh, left_on="PH_SEQ", right_on="H_SEQ", how="left", validate="many_to_one"); assert d.GESTFIPS.notna().all() +rw = pd.read_csv(f"{S}/asec26/asec_csv_repwgt_2026.csv") +rw.columns = [c.upper() for c in rw.columns] +m = d.merge(rw, left_on=["PH_SEQ","PPPOS"], right_on=["H_SEQ","PPPOS"], how="left", validate="one_to_one") +assert m.PWWGT0.notna().all(), "unmatched replicate rows" +assert np.allclose(m.PWWGT0, m.MARSUPWT / 100, rtol=1e-4), "PWWGT0 != MARSUPWT/100" # MARSUPWT carries two implied decimals +cpi = 32649/31812; t25 = {1:41322.707, 2:34325.998, 3:41700.556}; t24 = {1:39231.0, 2:32879.0, 3:39220.0} +fac = m.SPM_TENMORTSTATUS.map({k:(t24[k]*cpi)/t25[k] for k in t25}) +pub = (m.SPM_POOR == 1).to_numpy(float); anc = (m.SPM_RESOURCES < m.SPM_POVTHRESHOLD*fac).to_numpy(float) +W = m[[f"PWWGT{i}" for i in range(161)]].to_numpy(float) # col 0 = full sample +def est(mask): + w = W[mask]; p, a = pub[mask], anc[mask] + rp = (w * p[:,None]).sum(0) / w.sum(0) * 100; ra = (w * a[:,None]).sum(0) / w.sum(0) * 100 + se = lambda r: float(np.sqrt(4/160 * ((r[1:] - r[0])**2).sum())) + return {"published": round(float(rp[0]),2), "published_se": round(se(rp),2), "anchored": round(float(ra[0]),2), "anchored_se": round(se(ra),2), + "effect_pp": round(float(rp[0]-ra[0]),2), "effect_se": round(se(rp-ra),2), "n_persons": int(mask.sum()), "weighted_persons_thousands": round(float(w[:,0].sum()/1e3),0)} +age = m.A_AGE.to_numpy() +ages = {"all": age>=0, "age_0_3": age<=3, "age_0_4": age<=4, "under_6": age<6, "under_18": age<18, "age_6_17": (age>=6)&(age<18), "age_18_64": (age>=18)&(age<65), "age_65_plus": age>=65, "age_75_plus": age>=75} +out = {"method": "2026 CPS ASEC public-use person file; person weight MARSUPWT/100 (= PWWGT0, the replicate base weight); anchoring scales SPM_POVTHRESHOLD by tenure so the national 2A2K base = corrected 2024 BLS value x (32,649/31,812); SEs from the 160 ASEC replicate weights, variance = 4/160 x sum of squared replicate deviations", + "by_age": {k: est(v) for k, v in ages.items()}} +fips = {1:"AL",2:"AK",4:"AZ",5:"AR",6:"CA",8:"CO",9:"CT",10:"DE",11:"DC",12:"FL",13:"GA",15:"HI",16:"ID",17:"IL",18:"IN",19:"IA",20:"KS",21:"KY",22:"LA",23:"ME",24:"MD",25:"MA",26:"MI",27:"MN",28:"MS",29:"MO",30:"MT",31:"NE",32:"NV",33:"NH",34:"NJ",35:"NM",36:"NY",37:"NC",38:"ND",39:"OH",40:"OK",41:"OR",42:"PA",44:"RI",45:"SC",46:"SD",47:"TN",48:"TX",49:"UT",50:"VT",51:"VA",53:"WA",54:"WV",55:"WI",56:"WY"} +st = m.GESTFIPS.to_numpy() +out["by_state"] = {fips[f]: est(st == f) for f in sorted(fips)} +json.dump(out, open(f"{OUT}/RESULTS.json", "w"), indent=1) +rows = [{"state": k, **v} for k, v in out["by_state"].items()] +pd.DataFrame(rows).to_csv(f"{OUT}/by_state.csv", index=False) +print(json.dumps(out["by_age"], indent=1)) +bs = pd.DataFrame(rows).sort_values("effect_pp", ascending=False) +print(bs[["state","published","published_se","anchored","effect_pp","effect_se","n_persons"]].head(12).to_string(index=False)) +print("...") +print(bs[["state","published","published_se","anchored","effect_pp","effect_se","n_persons"]].tail(6).to_string(index=False)) +print("effect range", bs.effect_pp.min(), bs.effect_pp.max(), "| states with effect > 2 SE:", int((bs.effect_pp > 2*bs.effect_se).sum()), "of", len(bs)) diff --git a/analysis/asec-2026-anchored-thresholds/state_age_results.json b/analysis/asec-2026-anchored-thresholds/state_age_results.json new file mode 100644 index 0000000..833f93a --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/state_age_results.json @@ -0,0 +1,607 @@ +{ + "method": "2026 CPS ASEC public-use person file; person weight MARSUPWT/100 (= PWWGT0, the replicate base weight); anchoring scales SPM_POVTHRESHOLD by tenure so the national 2A2K base = corrected 2024 BLS value x (32,649/31,812); SEs from the 160 ASEC replicate weights, variance = 4/160 x sum of squared replicate deviations", + "by_age": { + "all": { + "published": 13.11, + "published_se": 0.19, + "anchored": 12.31, + "anchored_se": 0.18, + "effect_pp": 0.8, + "effect_se": 0.05, + "n_persons": 134729, + "weighted_persons_thousands": 338555.0 + }, + "age_0_3": { + "published": 14.13, + "published_se": 0.57, + "anchored": 12.9, + "anchored_se": 0.56, + "effect_pp": 1.23, + "effect_se": 0.2, + "n_persons": 5611, + "weighted_persons_thousands": 14671.0 + }, + "age_0_4": { + "published": 13.94, + "published_se": 0.55, + "anchored": 12.66, + "anchored_se": 0.53, + "effect_pp": 1.28, + "effect_se": 0.19, + "n_persons": 7259, + "weighted_persons_thousands": 18482.0 + }, + "under_6": { + "published": 14.15, + "published_se": 0.52, + "anchored": 12.94, + "anchored_se": 0.51, + "effect_pp": 1.2, + "effect_se": 0.17, + "n_persons": 8881, + "weighted_persons_thousands": 22154.0 + }, + "under_18": { + "published": 13.39, + "published_se": 0.34, + "anchored": 12.31, + "anchored_se": 0.32, + "effect_pp": 1.09, + "effect_se": 0.11, + "n_persons": 31444, + "weighted_persons_thousands": 72083.0 + }, + "age_6_17": { + "published": 13.06, + "published_se": 0.39, + "anchored": 12.02, + "anchored_se": 0.36, + "effect_pp": 1.03, + "effect_se": 0.12, + "n_persons": 22563, + "weighted_persons_thousands": 49929.0 + }, + "age_18_64": { + "published": 12.28, + "published_se": 0.2, + "anchored": 11.55, + "anchored_se": 0.19, + "effect_pp": 0.73, + "effect_se": 0.05, + "n_persons": 77083, + "weighted_persons_thousands": 201265.0 + }, + "age_65_plus": { + "published": 15.38, + "published_se": 0.32, + "anchored": 14.66, + "anchored_se": 0.32, + "effect_pp": 0.72, + "effect_se": 0.06, + "n_persons": 26202, + "weighted_persons_thousands": 65208.0 + }, + "age_75_plus": { + "published": 16.57, + "published_se": 0.47, + "anchored": 15.82, + "anchored_se": 0.47, + "effect_pp": 0.75, + "effect_se": 0.1, + "n_persons": 11182, + "weighted_persons_thousands": 28184.0 + } + }, + "by_state": { + "AL": { + "published": 15.75, + "published_se": 1.14, + "anchored": 14.04, + "anchored_se": 1.19, + "effect_pp": 1.71, + "effect_se": 0.52, + "n_persons": 2334, + "weighted_persons_thousands": 5079.0 + }, + "AK": { + "published": 10.37, + "published_se": 1.22, + "anchored": 10.02, + "anchored_se": 1.18, + "effect_pp": 0.35, + "effect_se": 0.34, + "n_persons": 1648, + "weighted_persons_thousands": 708.0 + }, + "AZ": { + "published": 14.78, + "published_se": 1.62, + "anchored": 13.41, + "anchored_se": 1.47, + "effect_pp": 1.36, + "effect_se": 0.51, + "n_persons": 2279, + "weighted_persons_thousands": 7666.0 + }, + "AR": { + "published": 12.67, + "published_se": 1.42, + "anchored": 11.87, + "anchored_se": 1.4, + "effect_pp": 0.8, + "effect_se": 0.33, + "n_persons": 2196, + "weighted_persons_thousands": 3073.0 + }, + "CA": { + "published": 17.29, + "published_se": 0.66, + "anchored": 16.22, + "anchored_se": 0.63, + "effect_pp": 1.08, + "effect_se": 0.2, + "n_persons": 13025, + "weighted_persons_thousands": 38913.0 + }, + "CO": { + "published": 11.36, + "published_se": 2.17, + "anchored": 10.9, + "anchored_se": 2.26, + "effect_pp": 0.46, + "effect_se": 0.29, + "n_persons": 1665, + "weighted_persons_thousands": 5950.0 + }, + "CT": { + "published": 10.56, + "published_se": 1.32, + "anchored": 9.75, + "anchored_se": 1.27, + "effect_pp": 0.81, + "effect_se": 0.31, + "n_persons": 1376, + "weighted_persons_thousands": 3651.0 + }, + "DE": { + "published": 7.51, + "published_se": 1.15, + "anchored": 6.14, + "anchored_se": 0.98, + "effect_pp": 1.37, + "effect_se": 0.72, + "n_persons": 1471, + "weighted_persons_thousands": 1066.0 + }, + "DC": { + "published": 16.76, + "published_se": 1.4, + "anchored": 15.65, + "anchored_se": 1.43, + "effect_pp": 1.12, + "effect_se": 0.46, + "n_persons": 1919, + "weighted_persons_thousands": 691.0 + }, + "FL": { + "published": 17.56, + "published_se": 0.91, + "anchored": 16.55, + "anchored_se": 0.89, + "effect_pp": 1.01, + "effect_se": 0.17, + "n_persons": 6342, + "weighted_persons_thousands": 23261.0 + }, + "GA": { + "published": 13.85, + "published_se": 1.11, + "anchored": 13.11, + "anchored_se": 1.04, + "effect_pp": 0.74, + "effect_se": 0.27, + "n_persons": 2881, + "weighted_persons_thousands": 11164.0 + }, + "HI": { + "published": 12.5, + "published_se": 1.29, + "anchored": 11.36, + "anchored_se": 1.2, + "effect_pp": 1.14, + "effect_se": 0.43, + "n_persons": 2427, + "weighted_persons_thousands": 1388.0 + }, + "ID": { + "published": 9.47, + "published_se": 0.9, + "anchored": 8.89, + "anchored_se": 0.85, + "effect_pp": 0.58, + "effect_se": 0.3, + "n_persons": 2366, + "weighted_persons_thousands": 1995.0 + }, + "IL": { + "published": 11.38, + "published_se": 1.03, + "anchored": 10.78, + "anchored_se": 1.01, + "effect_pp": 0.61, + "effect_se": 0.25, + "n_persons": 3297, + "weighted_persons_thousands": 12607.0 + }, + "IN": { + "published": 11.78, + "published_se": 1.29, + "anchored": 10.4, + "anchored_se": 1.28, + "effect_pp": 1.38, + "effect_se": 0.67, + "n_persons": 2024, + "weighted_persons_thousands": 6893.0 + }, + "IA": { + "published": 9.55, + "published_se": 1.11, + "anchored": 8.48, + "anchored_se": 1.09, + "effect_pp": 1.07, + "effect_se": 0.47, + "n_persons": 1634, + "weighted_persons_thousands": 3240.0 + }, + "KS": { + "published": 7.53, + "published_se": 1.12, + "anchored": 7.45, + "anchored_se": 1.11, + "effect_pp": 0.08, + "effect_se": 0.08, + "n_persons": 1678, + "weighted_persons_thousands": 2909.0 + }, + "KY": { + "published": 10.63, + "published_se": 1.29, + "anchored": 10.0, + "anchored_se": 1.26, + "effect_pp": 0.63, + "effect_se": 0.32, + "n_persons": 1461, + "weighted_persons_thousands": 4550.0 + }, + "LA": { + "published": 17.85, + "published_se": 1.01, + "anchored": 16.72, + "anchored_se": 0.92, + "effect_pp": 1.13, + "effect_se": 0.37, + "n_persons": 2869, + "weighted_persons_thousands": 4545.0 + }, + "ME": { + "published": 5.76, + "published_se": 1.12, + "anchored": 5.69, + "anchored_se": 1.13, + "effect_pp": 0.07, + "effect_se": 0.07, + "n_persons": 1061, + "weighted_persons_thousands": 1408.0 + }, + "MD": { + "published": 10.51, + "published_se": 1.46, + "anchored": 9.96, + "anchored_se": 1.38, + "effect_pp": 0.55, + "effect_se": 0.32, + "n_persons": 1724, + "weighted_persons_thousands": 6188.0 + }, + "MA": { + "published": 13.26, + "published_se": 1.08, + "anchored": 12.43, + "anchored_se": 1.04, + "effect_pp": 0.83, + "effect_se": 0.27, + "n_persons": 2732, + "weighted_persons_thousands": 7032.0 + }, + "MI": { + "published": 10.86, + "published_se": 1.04, + "anchored": 10.42, + "anchored_se": 1.03, + "effect_pp": 0.44, + "effect_se": 0.22, + "n_persons": 2517, + "weighted_persons_thousands": 10006.0 + }, + "MN": { + "published": 7.78, + "published_se": 1.4, + "anchored": 7.63, + "anchored_se": 1.42, + "effect_pp": 0.15, + "effect_se": 0.1, + "n_persons": 1694, + "weighted_persons_thousands": 5772.0 + }, + "MS": { + "published": 18.26, + "published_se": 1.77, + "anchored": 17.56, + "anchored_se": 1.73, + "effect_pp": 0.7, + "effect_se": 0.27, + "n_persons": 2446, + "weighted_persons_thousands": 2914.0 + }, + "MO": { + "published": 9.08, + "published_se": 1.12, + "anchored": 8.11, + "anchored_se": 1.08, + "effect_pp": 0.97, + "effect_se": 0.3, + "n_persons": 2010, + "weighted_persons_thousands": 6308.0 + }, + "MT": { + "published": 9.68, + "published_se": 1.69, + "anchored": 8.21, + "anchored_se": 1.3, + "effect_pp": 1.47, + "effect_se": 0.59, + "n_persons": 2006, + "weighted_persons_thousands": 1123.0 + }, + "NE": { + "published": 5.47, + "published_se": 0.96, + "anchored": 5.47, + "anchored_se": 0.96, + "effect_pp": 0.0, + "effect_se": 0.0, + "n_persons": 1885, + "weighted_persons_thousands": 2011.0 + }, + "NV": { + "published": 15.7, + "published_se": 1.52, + "anchored": 14.49, + "anchored_se": 1.51, + "effect_pp": 1.21, + "effect_se": 0.43, + "n_persons": 2116, + "weighted_persons_thousands": 3335.0 + }, + "NH": { + "published": 7.09, + "published_se": 1.17, + "anchored": 6.09, + "anchored_se": 0.99, + "effect_pp": 1.0, + "effect_se": 0.53, + "n_persons": 1608, + "weighted_persons_thousands": 1407.0 + }, + "NJ": { + "published": 11.53, + "published_se": 1.07, + "anchored": 10.95, + "anchored_se": 1.03, + "effect_pp": 0.58, + "effect_se": 0.23, + "n_persons": 2868, + "weighted_persons_thousands": 9488.0 + }, + "NM": { + "published": 14.95, + "published_se": 1.36, + "anchored": 14.25, + "anchored_se": 1.22, + "effect_pp": 0.7, + "effect_se": 0.46, + "n_persons": 3251, + "weighted_persons_thousands": 2103.0 + }, + "NY": { + "published": 16.42, + "published_se": 1.06, + "anchored": 14.92, + "anchored_se": 1.03, + "effect_pp": 1.5, + "effect_se": 0.37, + "n_persons": 5036, + "weighted_persons_thousands": 19683.0 + }, + "NC": { + "published": 11.69, + "published_se": 1.12, + "anchored": 11.23, + "anchored_se": 1.05, + "effect_pp": 0.46, + "effect_se": 0.22, + "n_persons": 3035, + "weighted_persons_thousands": 11050.0 + }, + "ND": { + "published": 8.86, + "published_se": 1.12, + "anchored": 8.57, + "anchored_se": 1.17, + "effect_pp": 0.29, + "effect_se": 0.17, + "n_persons": 1980, + "weighted_persons_thousands": 780.0 + }, + "OH": { + "published": 10.05, + "published_se": 1.24, + "anchored": 9.56, + "anchored_se": 1.24, + "effect_pp": 0.48, + "effect_se": 0.33, + "n_persons": 2952, + "weighted_persons_thousands": 11974.0 + }, + "OK": { + "published": 11.96, + "published_se": 1.35, + "anchored": 11.36, + "anchored_se": 1.32, + "effect_pp": 0.6, + "effect_se": 0.33, + "n_persons": 2090, + "weighted_persons_thousands": 4070.0 + }, + "OR": { + "published": 9.93, + "published_se": 1.02, + "anchored": 9.52, + "anchored_se": 0.98, + "effect_pp": 0.41, + "effect_se": 0.19, + "n_persons": 2241, + "weighted_persons_thousands": 4234.0 + }, + "PA": { + "published": 9.83, + "published_se": 0.9, + "anchored": 9.53, + "anchored_se": 0.89, + "effect_pp": 0.31, + "effect_se": 0.15, + "n_persons": 3367, + "weighted_persons_thousands": 12893.0 + }, + "RI": { + "published": 11.34, + "published_se": 1.45, + "anchored": 10.47, + "anchored_se": 1.38, + "effect_pp": 0.87, + "effect_se": 0.47, + "n_persons": 1196, + "weighted_persons_thousands": 1110.0 + }, + "SC": { + "published": 11.07, + "published_se": 1.29, + "anchored": 10.27, + "anchored_se": 1.22, + "effect_pp": 0.8, + "effect_se": 0.34, + "n_persons": 2302, + "weighted_persons_thousands": 5592.0 + }, + "SD": { + "published": 8.78, + "published_se": 1.25, + "anchored": 8.39, + "anchored_se": 1.15, + "effect_pp": 0.39, + "effect_se": 0.33, + "n_persons": 1455, + "weighted_persons_thousands": 913.0 + }, + "TN": { + "published": 12.41, + "published_se": 1.13, + "anchored": 11.66, + "anchored_se": 1.09, + "effect_pp": 0.75, + "effect_se": 0.27, + "n_persons": 2368, + "weighted_persons_thousands": 7196.0 + }, + "TX": { + "published": 15.27, + "published_se": 0.74, + "anchored": 14.25, + "anchored_se": 0.71, + "effect_pp": 1.02, + "effect_se": 0.22, + "n_persons": 8575, + "weighted_persons_thousands": 31542.0 + }, + "UT": { + "published": 10.35, + "published_se": 1.04, + "anchored": 10.03, + "anchored_se": 1.04, + "effect_pp": 0.31, + "effect_se": 0.13, + "n_persons": 2469, + "weighted_persons_thousands": 3510.0 + }, + "VT": { + "published": 9.08, + "published_se": 1.14, + "anchored": 8.76, + "anchored_se": 1.11, + "effect_pp": 0.32, + "effect_se": 0.2, + "n_persons": 1544, + "weighted_persons_thousands": 633.0 + }, + "VA": { + "published": 10.85, + "published_se": 1.19, + "anchored": 10.6, + "anchored_se": 1.17, + "effect_pp": 0.24, + "effect_se": 0.12, + "n_persons": 2380, + "weighted_persons_thousands": 8726.0 + }, + "WA": { + "published": 9.85, + "published_se": 0.96, + "anchored": 9.71, + "anchored_se": 0.94, + "effect_pp": 0.14, + "effect_se": 0.13, + "n_persons": 2752, + "weighted_persons_thousands": 7975.0 + }, + "WV": { + "published": 12.23, + "published_se": 1.35, + "anchored": 11.15, + "anchored_se": 1.19, + "effect_pp": 1.08, + "effect_se": 0.4, + "n_persons": 2547, + "weighted_persons_thousands": 1737.0 + }, + "WI": { + "published": 7.81, + "published_se": 1.07, + "anchored": 7.6, + "anchored_se": 1.12, + "effect_pp": 0.21, + "effect_se": 0.17, + "n_persons": 1754, + "weighted_persons_thousands": 5923.0 + }, + "WY": { + "published": 9.5, + "published_se": 1.08, + "anchored": 9.07, + "anchored_se": 1.07, + "effect_pp": 0.43, + "effect_se": 0.27, + "n_persons": 1876, + "weighted_persons_thousands": 568.0 + } + } +} \ No newline at end of file From 8c9f6e3129d68f5a07e7fe17f9ad2e85f48334c5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 17 Sep 2026 14:58:04 -0400 Subject: [PATCH 3/5] Add the state tile map script and the Table 17 three-year state averages Co-Authored-By: Claude Fable 5.1 --- .../asec-2026-anchored-thresholds/README.md | 4 ++ .../census_table17_state_3yr.csv | 52 +++++++++++++++++++ .../state_tile_map.py | 43 +++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 analysis/asec-2026-anchored-thresholds/census_table17_state_3yr.csv create mode 100644 analysis/asec-2026-anchored-thresholds/state_tile_map.py diff --git a/analysis/asec-2026-anchored-thresholds/README.md b/analysis/asec-2026-anchored-thresholds/README.md index 3513c5b..8baf544 100644 --- a/analysis/asec-2026-anchored-thresholds/README.md +++ b/analysis/asec-2026-anchored-thresholds/README.md @@ -30,3 +30,7 @@ The chained variant uses C-CPI-U (`SUUR0000SA0`) annual means, 174.372 for 2024 ## By state and by detailed age `state_age_anchored.py` extends the recomputation to the 51 states (from `hhpub26.csv` `GESTFIPS`, joined to the person file on `H_SEQ`) and to detailed age groups, with standard errors from the 160 ASEC replicate weights (`asec_csv_repwgt_2026.csv`; variance = 4/160 times the sum of squared replicate deviations; `PWWGT0` equals `MARSUPWT`/100). Outputs: `state_age_results.json`, `by_state.csv`. Run it with the scratch directory holding the three CSV files as the first argument and the output directory as the second. + +## State tile maps + +`state_tile_map.py` renders a state tile map (11 by 8 grid, teal shading, the layout of the PolicyEngine slides' state map) from any CSV with a `state` column. The post uses it twice: `by_state.csv` for the 2025 threshold effect (default arguments) and `census_table17_state_3yr.csv` (P60-290 Table 17, SPM percent and margin of error, three-year average 2023 to 2025) with `--value spm_pct --se spm_moe` and the 9/12/15/18 percent bins. It needs Playwright with its Chromium (`uv run --with playwright --with pandas python state_tile_map.py ...` after `playwright install chromium`). diff --git a/analysis/asec-2026-anchored-thresholds/census_table17_state_3yr.csv b/analysis/asec-2026-anchored-thresholds/census_table17_state_3yr.csv new file mode 100644 index 0000000..ed8e918 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/census_table17_state_3yr.csv @@ -0,0 +1,52 @@ +state,name,official_plus_pct,official_plus_moe,spm_pct,spm_moe +AL,Alabama,15.2,1.6,14.4,1.3 +AK,Alaska,10,1.1,10.4,1.3 +AZ,Arizona,10.7,1.1,13.7,1.2 +AR,Arkansas,12.7,1.5,12.8,1.4 +CA,California,10.7,0.5,17.8,0.6 +CO,Colorado,8.6,1.5,10.8,1.5 +CT,Connecticut,8.6,1.4,10.4,1.4 +DE,Delaware,7.3,1.2,8.9,1.3 +DC,District of Columbia,12.1,1.4,15.7,1.5 +FL,Florida,11.3,0.7,16.4,0.9 +GA,Georgia,12.3,1.1,14,1.2 +HI,Hawaii,8.5,1.3,11.7,1.3 +ID,Idaho,8.4,0.9,9.4,1 +IL,Illinois,10.4,0.8,11.6,0.9 +IN,Indiana,9.3,1.1,10.3,1.2 +IA,Iowa,8.1,0.9,8.3,1.1 +KS,Kansas,8.1,1.2,8.1,1 +KY,Kentucky,13.7,1.6,13.2,1.6 +LA,Louisiana,19.6,1.5,19,1.4 +ME,Maine,7.5,1.1,6.4,1.1 +MD,Maryland,7.2,1.1,10.9,1.4 +MA,Massachusetts,9.1,1,12.5,1 +MI,Michigan,11.5,0.9,10.6,1.2 +MN,Minnesota,7.4,1,7.9,1 +MS,Mississippi,17.6,1.2,16.8,1.4 +MO,Missouri,9.3,1.4,9.4,1.3 +MT,Montana,8.6,1.3,9.5,1.6 +NE,Nebraska,7.3,1.2,7.5,1 +NV,Nevada,11.8,1.2,15.3,1.5 +NH,New Hampshire,5.4,0.9,8,1.3 +NJ,New Jersey,8.9,1,12.1,1 +NM,New Mexico,16,2.6,13.2,1.5 +NY,New York,10.9,0.8,15.1,0.9 +NC,North Carolina,13.1,1.3,13.5,1.2 +ND,North Dakota,8.6,1.1,8.9,1.1 +OH,Ohio,10.8,1,9.9,1 +OK,Oklahoma,12.6,1.3,11.1,1.3 +OR,Oregon,9.2,0.9,11,1 +PA,Pennsylvania,9.3,0.9,10.7,1 +RI,Rhode Island,9.4,1.3,10.6,1.4 +SC,South Carolina,11,1.1,11.1,1.2 +SD,South Dakota,8.6,1.3,8,1.1 +TN,Tennessee,10.7,1.2,11.1,1.1 +TX,Texas,11.7,0.6,14.4,0.6 +UT,Utah,6.9,1,9.9,1.1 +VT,Vermont,7.3,1,8.9,1.2 +VA,Virginia,8.7,1.2,10.6,1.2 +WA,Washington,8.8,1.2,10.5,1.3 +WV,West Virginia,13.5,1.5,12.1,1.6 +WI,Wisconsin,8.8,1.5,7.9,1.2 +WY,Wyoming,8.6,1.1,8.7,1.1 diff --git a/analysis/asec-2026-anchored-thresholds/state_tile_map.py b/analysis/asec-2026-anchored-thresholds/state_tile_map.py new file mode 100644 index 0000000..ae403bf --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/state_tile_map.py @@ -0,0 +1,43 @@ +"""Render a state tile map in the style of the PolicyEngine slides' state map (StateChildPovertyMapSlide): 11x8 tile grid, teal shading. +usage: python state_tile_map.py [--value COL] [--title T] [--subtitle S] [--us LABEL] [--bins "0.4:under 0.4,0.8:0.4 to 0.8,..."] [--note N] [--source SRC] [--decimals 1] +Defaults render the 2025 threshold effect from by_state.csv. Needs: uv run --with playwright --with pandas; playwright chromium installed.""" +import sys, argparse, pandas as pd +from playwright.sync_api import sync_playwright +POS = {"AK": (0, 0), "AL": (6, 6), "AR": (4, 5), "AZ": (1, 5), "CA": (0, 4), "CO": (2, 4), "CT": (9, 3), "DC": (8, 5), "DE": (9, 4), "FL": (7, 7), "GA": (7, 6), "HI": (0, 7), "IA": (4, 3), "ID": (1, 2), "IL": (5, 2), "IN": (5, 3), "KS": (3, 5), "KY": (5, 4), "LA": (4, 6), "MA": (9, 2), "MD": (8, 4), "ME": (10, 0), "MI": (6, 2), "MN": (4, 2), "MO": (4, 4), "MS": (5, 6), "MT": (2, 2), "NC": (6, 5), "ND": (3, 2), "NE": (3, 4), "NH": (10, 1), "NJ": (8, 3), "NM": (2, 5), "NV": (1, 3), "NY": (8, 2), "OH": (6, 3), "OK": (3, 6), "OR": (0, 3), "PA": (7, 3), "RI": (10, 3), "SC": (7, 5), "SD": (3, 3), "TN": (5, 5), "TX": (3, 7), "UT": (1, 4), "VA": (7, 4), "VT": (9, 1), "WA": (0, 2), "WI": (5, 1), "WV": (6, 4), "WY": (2, 3)} +SHADES = [("rgba(44,110,107,0.12)", "#1f2937"), ("rgba(44,110,107,0.30)", "#1f2937"), ("rgba(44,110,107,0.52)", "#ffffff"), ("rgba(44,110,107,0.74)", "#ffffff"), ("rgba(44,110,107,1)", "#ffffff")] +ap = argparse.ArgumentParser(); ap.add_argument("csv"); ap.add_argument("out") +ap.add_argument("--value", default="effect_pp"); ap.add_argument("--se", default="effect_se") +ap.add_argument("--title", default="Points added to 2025 SPM poverty by threshold growth beyond CPI-U, by state") +ap.add_argument("--subtitle", default="Published 2025 rate minus the rate at 2024 thresholds grown by CPI-U, in percentage points") +ap.add_argument("--us", default="United States: 0.8"); ap.add_argument("--decimals", type=int, default=1) +ap.add_argument("--bins", default="0.4:under 0.4,0.8:0.4 to 0.8,1.2:0.8 to 1.2,1.6:1.2 to 1.6,99:1.6 and up") +ap.add_argument("--note", default="Single-year state estimates; replicate-weight standard errors run 0.1 to 0.7 points. Tiles are equal in size, so the layout is about position, not area.") +ap.add_argument("--source", default="Source: PolicyEngine recomputation from the 2026 CPS ASEC public-use file; 2024 thresholds from BLS's corrected series, grown by CPI-U (32,649 / 31,812).") +args = ap.parse_args() +BINS = [(float(b.split(":")[0]), b.split(":")[1], SHADES[i][0], SHADES[i][1]) for i, b in enumerate(args.bins.split(","))] +def bin_for(v): return next(b for b in BINS if v < b[0]) +df = pd.read_csv(args.csv); out = args.out; VAL, SE, D = args.value, args.se, args.decimals +tiles = "".join(f'
{r["state"]}{r[VAL]:.{D}f}
' for _, r in df.iterrows()) +legend = "".join(f'
  • {b[1]}
  • ' for b in BINS) +html = f"""
    +

    {args.title}

    +

    {args.subtitle}

    +
    {tiles}
    +

    {args.us}

      {legend}

    {args.note}

    +

    {args.source}

    +
    """ +import pathlib, os +htmlp = pathlib.Path(out).with_suffix(".html"); htmlp.write_text(html) +with sync_playwright() as p: + b = p.chromium.launch(); pg = b.new_page(viewport={"width": 1440, "height": 720}, device_scale_factor=2) + pg.goto(f"file://{htmlp.resolve()}"); pg.wait_for_load_state("networkidle"); pg.wait_for_timeout(700) + pg.locator(".wrap").screenshot(path=out); b.close() +print("wrote", out) From 8b6989bfe63a21ee7f650a90198d92882e0c871e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 18 Sep 2026 18:11:47 -0400 Subject: [PATCH 4/5] Tile map: optional PolicyEngine wordmark watermark Adds --logo to state_tile_map.py, rendering the teal wordmark bottom-right opposite the source line, and notes it in the README. Co-Authored-By: Claude Fable 5.1 --- analysis/asec-2026-anchored-thresholds/README.md | 2 +- .../asec-2026-anchored-thresholds/state_tile_map.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/analysis/asec-2026-anchored-thresholds/README.md b/analysis/asec-2026-anchored-thresholds/README.md index 8baf544..f8deba8 100644 --- a/analysis/asec-2026-anchored-thresholds/README.md +++ b/analysis/asec-2026-anchored-thresholds/README.md @@ -33,4 +33,4 @@ The chained variant uses C-CPI-U (`SUUR0000SA0`) annual means, 174.372 for 2024 ## State tile maps -`state_tile_map.py` renders a state tile map (11 by 8 grid, teal shading, the layout of the PolicyEngine slides' state map) from any CSV with a `state` column. The post uses it twice: `by_state.csv` for the 2025 threshold effect (default arguments) and `census_table17_state_3yr.csv` (P60-290 Table 17, SPM percent and margin of error, three-year average 2023 to 2025) with `--value spm_pct --se spm_moe` and the 9/12/15/18 percent bins. It needs Playwright with its Chromium (`uv run --with playwright --with pandas python state_tile_map.py ...` after `playwright install chromium`). +`state_tile_map.py` renders a state tile map (pass `--logo` with the PolicyEngine teal wordmark PNG from policyengine-app-v2, `app/public/assets/logos/policyengine/teal.png`, to watermark it) (11 by 8 grid, teal shading, the layout of the PolicyEngine slides' state map) from any CSV with a `state` column. The post uses it twice: `by_state.csv` for the 2025 threshold effect (default arguments) and `census_table17_state_3yr.csv` (P60-290 Table 17, SPM percent and margin of error, three-year average 2023 to 2025) with `--value spm_pct --se spm_moe` and the 9/12/15/18 percent bins. It needs Playwright with its Chromium (`uv run --with playwright --with pandas python state_tile_map.py ...` after `playwright install chromium`). diff --git a/analysis/asec-2026-anchored-thresholds/state_tile_map.py b/analysis/asec-2026-anchored-thresholds/state_tile_map.py index ae403bf..0a910eb 100644 --- a/analysis/asec-2026-anchored-thresholds/state_tile_map.py +++ b/analysis/asec-2026-anchored-thresholds/state_tile_map.py @@ -1,7 +1,7 @@ """Render a state tile map in the style of the PolicyEngine slides' state map (StateChildPovertyMapSlide): 11x8 tile grid, teal shading. -usage: python state_tile_map.py [--value COL] [--title T] [--subtitle S] [--us LABEL] [--bins "0.4:under 0.4,0.8:0.4 to 0.8,..."] [--note N] [--source SRC] [--decimals 1] +usage: python state_tile_map.py [--value COL] [--title T] [--subtitle S] [--us LABEL] [--bins "0.4:under 0.4,0.8:0.4 to 0.8,..."] [--note N] [--source SRC] [--decimals 1] [--logo PNG] Defaults render the 2025 threshold effect from by_state.csv. Needs: uv run --with playwright --with pandas; playwright chromium installed.""" -import sys, argparse, pandas as pd +import sys, argparse, pathlib, pandas as pd from playwright.sync_api import sync_playwright POS = {"AK": (0, 0), "AL": (6, 6), "AR": (4, 5), "AZ": (1, 5), "CA": (0, 4), "CO": (2, 4), "CT": (9, 3), "DC": (8, 5), "DE": (9, 4), "FL": (7, 7), "GA": (7, 6), "HI": (0, 7), "IA": (4, 3), "ID": (1, 2), "IL": (5, 2), "IN": (5, 3), "KS": (3, 5), "KY": (5, 4), "LA": (4, 6), "MA": (9, 2), "MD": (8, 4), "ME": (10, 0), "MI": (6, 2), "MN": (4, 2), "MO": (4, 4), "MS": (5, 6), "MT": (2, 2), "NC": (6, 5), "ND": (3, 2), "NE": (3, 4), "NH": (10, 1), "NJ": (8, 3), "NM": (2, 5), "NV": (1, 3), "NY": (8, 2), "OH": (6, 3), "OK": (3, 6), "OR": (0, 3), "PA": (7, 3), "RI": (10, 3), "SC": (7, 5), "SD": (3, 3), "TN": (5, 5), "TX": (3, 7), "UT": (1, 4), "VA": (7, 4), "VT": (9, 1), "WA": (0, 2), "WI": (5, 1), "WV": (6, 4), "WY": (2, 3)} SHADES = [("rgba(44,110,107,0.12)", "#1f2937"), ("rgba(44,110,107,0.30)", "#1f2937"), ("rgba(44,110,107,0.52)", "#ffffff"), ("rgba(44,110,107,0.74)", "#ffffff"), ("rgba(44,110,107,1)", "#ffffff")] @@ -9,7 +9,7 @@ ap.add_argument("--value", default="effect_pp"); ap.add_argument("--se", default="effect_se") ap.add_argument("--title", default="Points added to 2025 SPM poverty by threshold growth beyond CPI-U, by state") ap.add_argument("--subtitle", default="Published 2025 rate minus the rate at 2024 thresholds grown by CPI-U, in percentage points") -ap.add_argument("--us", default="United States: 0.8"); ap.add_argument("--decimals", type=int, default=1) +ap.add_argument("--us", default="United States: 0.8"); ap.add_argument("--decimals", type=int, default=1); ap.add_argument("--logo", default=None, help="path to the PolicyEngine teal wordmark PNG; rendered bottom-right as a watermark") ap.add_argument("--bins", default="0.4:under 0.4,0.8:0.4 to 0.8,1.2:0.8 to 1.2,1.6:1.2 to 1.6,99:1.6 and up") ap.add_argument("--note", default="Single-year state estimates; replicate-weight standard errors run 0.1 to 0.7 points. Tiles are equal in size, so the layout is about position, not area.") ap.add_argument("--source", default="Source: PolicyEngine recomputation from the 2026 CPS ASEC public-use file; 2024 thresholds from BLS's corrected series, grown by CPI-U (32,649 / 31,812).") @@ -18,6 +18,7 @@ def bin_for(v): return next(b for b in BINS if v < b[0]) df = pd.read_csv(args.csv); out = args.out; VAL, SE, D = args.value, args.se, args.decimals tiles = "".join(f'
    {r["state"]}{r[VAL]:.{D}f}
    ' for _, r in df.iterrows()) +logo = f'PolicyEngine' if args.logo else "" legend = "".join(f'
  • {b[1]}
  • ' for b in BINS) html = f"""

    {args.title}

    {args.subtitle}

    {tiles}

    {args.us}

      {legend}

    {args.note}

    -

    {args.source}

    +

    {args.source}

    {logo}
    """ -import pathlib, os htmlp = pathlib.Path(out).with_suffix(".html"); htmlp.write_text(html) with sync_playwright() as p: b = p.chromium.launch(); pg = b.new_page(viewport={"width": 1440, "height": 720}, device_scale_factor=2) From 4b8df6615e2beca582835713d0b2ca3c1ab3bf9c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 18 Sep 2026 18:31:45 -0400 Subject: [PATCH 5/5] Add the 2024-to-2025 waterfall chart script and its data Co-Authored-By: Claude Fable 5.1 --- .../waterfall.py | 52 +++++++++++++++++++ .../waterfall_data.json | 30 +++++++++++ 2 files changed, 82 insertions(+) create mode 100644 analysis/asec-2026-anchored-thresholds/waterfall.py create mode 100644 analysis/asec-2026-anchored-thresholds/waterfall_data.json diff --git a/analysis/asec-2026-anchored-thresholds/waterfall.py b/analysis/asec-2026-anchored-thresholds/waterfall.py new file mode 100644 index 0000000..abe9867 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/waterfall.py @@ -0,0 +1,52 @@ +"""Waterfall: 2024 SPM rate -> resources step (to the 2025 rate at 2024 thresholds x CPI-U) -> threshold step -> published 2025 rate, four age groups. +usage: uv run --with playwright python waterfall.py data.json out.png --logo teal.png""" +import json, sys, pathlib, argparse +from playwright.sync_api import sync_playwright +ap=argparse.ArgumentParser(); ap.add_argument("data"); ap.add_argument("out"); ap.add_argument("--logo"); a=ap.parse_args() +D=json.load(open(a.data)); order=["All people","Under 18","18 to 64","65 and over"] +YMAX=16.0; H=380; ppp=H/YMAX # px per point +TEAL="rgba(44,110,107,1)"; TEAL_DK="#1D4044"; GRAY="#CBD5E1" +def bar(left,bottom_val,top_val,color,label,label_above=True,text_color="#1f2937"): + b=bottom_val*ppp; h=max((top_val-bottom_val)*ppp,2) + lab_y = (top_val*ppp+8) if label_above else (bottom_val*ppp-26) + return (f'
    ' + f'
    {label}
    ') +panels="" +for g in order: + d=D[g]; y0=d["y2024_census"]; y1=d["anchored_2025"]; y2=d["published_2025"]; rs=d["resources_step"]; ts=d["threshold_step"] + xs=[14,84,154,224]; w=56 + els =bar(xs[0],0,y0,TEAL,f"{y0:.1f}") + els+=bar(xs[1],y1,y0,GRAY,f"{rs:+.1f}".replace("-","−"),label_above=False,text_color="#475467") + els+=bar(xs[2],y1,y2,TEAL_DK,f"{ts:+.1f}") + els+=bar(xs[3],0,y2,TEAL,f"{y2:.1f}") + # connectors + con=f'
    ' + con+=f'
    ' + con+=f'
    ' + grid="".join(f'
    {v:g}
    ' for v in (0,4,8,12,16)) + cats="".join(f'
    {t}
    ' for x,t in zip(xs,["2024","Resources","Thresholds","2025"])) + panels+=f'

    {g}

    {grid}{con}{els}
    {cats}
    ' +logo=f'PolicyEngine' if a.logo else "" +html=f"""
    +

    SPM poverty, 2024 to 2025: what resources did, and what thresholds did

    +

    Percent of people below the SPM threshold. Resources: the change from 2024 to the 2025 rate at 2024 thresholds grown by CPI-U, so resources measured against prices. Thresholds: the further change to the published 2025 rate, from threshold growth beyond CPI-U.

    +
    {panels}
    +
    Published rateResources, at 2024 thresholds × CPI-UThreshold growth beyond CPI-U
    +

    Source: U.S. Census Bureau, P60-290 (2024 rates, restated on Vintage 2025 controls); PolicyEngine recomputation from the 2026 CPS ASEC public-use file for the 2025 rates. Labels rounded to one decimal; steps computed from unrounded rates.

    {logo}
    +
    """ +htmlp=pathlib.Path(a.out).with_suffix(".html"); htmlp.write_text(html) +with sync_playwright() as p: + b=p.chromium.launch(); pg=b.new_page(viewport={"width":1440,"height":760},device_scale_factor=2) + pg.goto(f"file://{htmlp.resolve()}"); pg.wait_for_load_state("networkidle"); pg.wait_for_timeout(700); pg.locator(".wrap").screenshot(path=a.out); b.close() +print("wrote",a.out) diff --git a/analysis/asec-2026-anchored-thresholds/waterfall_data.json b/analysis/asec-2026-anchored-thresholds/waterfall_data.json new file mode 100644 index 0000000..0272ca9 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/waterfall_data.json @@ -0,0 +1,30 @@ +{ + "All people": { + "y2024_census": 13.0, + "anchored_2025": 12.3089, + "published_2025": 13.1117, + "resources_step": -0.6911, + "threshold_step": 0.8028 + }, + "Under 18": { + "y2024_census": 13.5, + "anchored_2025": 12.306, + "published_2025": 13.3913, + "resources_step": -1.194, + "threshold_step": 1.0853 + }, + "18 to 64": { + "y2024_census": 12.2, + "anchored_2025": 11.5482, + "published_2025": 12.2773, + "resources_step": -0.6518, + "threshold_step": 0.7291 + }, + "65 and over": { + "y2024_census": 15.1, + "anchored_2025": 14.6601, + "published_2025": 15.3779, + "resources_step": -0.4399, + "threshold_step": 0.7178 + } +} \ No newline at end of file