diff --git a/analysis/asec-2026-anchored-thresholds/README.md b/analysis/asec-2026-anchored-thresholds/README.md new file mode 100644 index 0000000..f8deba8 --- /dev/null +++ b/analysis/asec-2026-anchored-thresholds/README.md @@ -0,0 +1,36 @@ +# 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. + +## 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 (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/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/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/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/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)) 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 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..0a910eb --- /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] [--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, 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")] +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("--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).") +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()) +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}

    {logo}
    +
    """ +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) 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