Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: white; }
#root { width: 100%; height: 100vh; padding: 20px; }
.chart-container { width: 100%; height: 100%; position: relative; }
.logo { position: absolute; bottom: 10px; right: 10px; height: 24px; opacity: 0.8; }
.title { text-align: center; font-size: 18px; font-weight: 600; color: #344054; margin-bottom: 20px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import ReactDOM from 'https://esm.sh/react-dom@18/client';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ReferenceLine, ResponsiveContainer, LabelList } from 'https://esm.sh/recharts@2.12.7';

const data = [
{
name: 'Total Benefits',
'Raw CPS': 64,
'PolicyEngine': 98,
rawAbs: '$25.6B',
peAbs: '$39.1B',
adminAbs: '$40.0B'
},
{
name: 'Recipients',
'Raw CPS': 59,
'PolicyEngine': 104,
rawAbs: '3.2M',
peAbs: '5.6M',
adminAbs: '5.4M'
},
];

const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const dataPoint = data.find(d => d.name === label);
return React.createElement('div', {
style: {
background: 'white',
border: '1px solid #319795',
borderRadius: '8px',
padding: '12px',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
}
}, [
React.createElement('p', { key: 'label', style: { fontWeight: 600, marginBottom: '8px', color: '#344054' } }, label),
...payload.map((entry, index) =>
React.createElement('p', { key: index, style: { color: entry.color, margin: '4px 0' } }, [
React.createElement('span', { key: 'name', style: { fontWeight: 500 } }, `${entry.name}: `),
`${entry.value}%`,
React.createElement('span', { key: 'abs', style: { color: '#666', marginLeft: '8px' } },
`(${entry.name === 'Raw CPS' ? dataPoint.rawAbs : dataPoint.peAbs})`
)
])
),
React.createElement('p', { key: 'admin', style: { color: '#344054', marginTop: '8px', fontSize: '13px' } },
`Admin total: ${dataPoint.adminAbs}`
)
]);
}
return null;
};

const Chart = () => React.createElement('div', { className: 'chart-container' }, [
React.createElement('div', { key: 'title', className: 'title' }, 'UI Estimates as Percentage of Administrative Data (2024)'),
React.createElement(ResponsiveContainer, { key: 'chart', width: '100%', height: '85%' },
React.createElement(BarChart, { data, margin: { top: 30, right: 30, left: 20, bottom: 20 } }, [
React.createElement(CartesianGrid, { key: 'grid', strokeDasharray: '3 3', stroke: '#e5e7eb', vertical: false }),
React.createElement(XAxis, {
key: 'x',
dataKey: 'name',
tick: { fill: '#344054', fontSize: 14 },
axisLine: { stroke: '#e5e7eb' },
tickLine: false
}),
React.createElement(YAxis, {
key: 'y',
domain: [0, 125],
ticks: [25, 50, 75, 100, 125],
tick: { fill: '#344054', fontSize: 12 },
axisLine: { stroke: '#e5e7eb' },
tickLine: false,
label: { value: '% of Administrative Total', angle: -90, position: 'insideLeft', style: { fill: '#344054', fontSize: 13 } }
}),
React.createElement(Tooltip, { key: 'tooltip', content: React.createElement(CustomTooltip) }),
React.createElement(Legend, {
key: 'legend',
wrapperStyle: { paddingTop: '10px' },
formatter: (value) => React.createElement('span', { style: { color: '#344054' } }, value)
}),
React.createElement(ReferenceLine, {
key: 'ref',
y: 100,
stroke: '#344054',
strokeDasharray: '5 5',
strokeWidth: 1.5
}),
React.createElement(Bar, { key: 'cps', dataKey: 'Raw CPS', fill: '#94a3b8', radius: [4, 4, 0, 0] },
React.createElement(LabelList, { dataKey: 'Raw CPS', position: 'top', formatter: (v) => `${v}%`, style: { fill: '#344054', fontSize: 14, fontWeight: 500 } })
),
React.createElement(Bar, { key: 'pe', dataKey: 'PolicyEngine', fill: '#319795', radius: [4, 4, 0, 0] },
React.createElement(LabelList, { dataKey: 'PolicyEngine', position: 'top', formatter: (v) => `${v}%`, style: { fill: '#344054', fontSize: 14, fontWeight: 500 } })
)
])
),
React.createElement('img', {
key: 'logo',
className: 'logo',
src: '../../assets/logos/policyengine/teal.png',
alt: 'PolicyEngine'
})
]);

ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(Chart));
</script>
</body>
</html>
13 changes: 13 additions & 0 deletions app/src/data/apps/apps.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
[
{
"type": "iframe",
"slug": "missouri-transitional-benefits",
"title": "How Missouri's Transitional Benefits Program reshapes cliffs",
"description": "Interactive analysis of how Missouri SB 82's transitional benefits program redistributes SNAP cliffs across the income distribution, with multi-year capitalization effects computed using PolicyEngine-US",
"source": "https://missouri-transitional-benefits.vercel.app",
"tags": ["us", "us-mo", "policy", "interactives"],
"countryId": "us",
"displayWithResearch": true,
"image": "missouri-transitional-benefits.png",
"date": "2026-02-12 12:00:00",
"authors": ["max-ghenis"]
},
{
"type": "iframe",
"slug": "state-legislative-tracker",
Expand Down
86 changes: 86 additions & 0 deletions app/src/data/posts/notebooks/validating-ui-estimates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# %% [markdown]
# # Validating PolicyEngine unemployment insurance estimates
#
# This notebook compares PolicyEngine's UI estimates against administrative data.

# %%
from policyengine_us import Microsimulation
import pandas as pd

# %% [markdown]
# ## PolicyEngine estimates (2025)

# %%
sim = Microsimulation(dataset="enhanced_cps_2024")
sim.default_calculation_period = 2025

# Total UI benefits
pe_total_benefits = sim.calculate("unemployment_compensation").sum() / 1e9
print(f"PolicyEngine total UI benefits: ${pe_total_benefits:.1f}B")

# Recipients (people with UI > 0)
ui = sim.calculate("unemployment_compensation")
weights = sim.calculate("person_weight")
pe_recipients = (weights * (ui > 0)).sum() / 1e6
print(f"PolicyEngine UI recipients: {pe_recipients:.1f}M")

# %% [markdown]
# ## Administrative totals
#
# Sources:
# - CBO Budget Projections (March 2024): https://www.cbo.gov/data/budget-economic-data
# - DOL UI Data Summary: https://oui.doleta.gov/unemploy/claimssum.asp

# %%
# CBO projects $40B in UI benefits for 2025
cbo_total = 40.0 # billions

# DOL historical data suggests ~5.4M recipients annually in non-recession years
dol_recipients = 5.4 # millions

print(f"CBO projected UI benefits: ${cbo_total:.1f}B")
print(f"DOL estimated recipients: {dol_recipients:.1f}M")

# %% [markdown]
# ## Raw CPS comparison

# %%
# Raw CPS without enhancement
sim_raw = Microsimulation(dataset="cps_2024")
sim_raw.default_calculation_period = 2025

raw_total = sim_raw.calculate("unemployment_compensation").sum() / 1e9
ui_raw = sim_raw.calculate("unemployment_compensation")
weights_raw = sim_raw.calculate("person_weight")
raw_recipients = (weights_raw * (ui_raw > 0)).sum() / 1e6

print(f"Raw CPS total UI benefits: ${raw_total:.1f}B")
print(f"Raw CPS UI recipients: {raw_recipients:.1f}M")

# %% [markdown]
# ## Summary comparison

# %%
comparison = pd.DataFrame({
"Source": ["Raw CPS", "External (CBO/DOL)", "PolicyEngine"],
"Total Benefits ($B)": [raw_total, cbo_total, pe_total_benefits],
"Recipients (M)": [raw_recipients, dol_recipients, pe_recipients]
})
comparison = comparison.round(1)
print(comparison.to_markdown(index=False))

# %% [markdown]
# ## Deviation from admin totals

# %%
pe_benefits_pct = (pe_total_benefits - cbo_total) / cbo_total * 100
pe_recipients_pct = (pe_recipients - dol_recipients) / dol_recipients * 100
raw_benefits_pct = (raw_total - cbo_total) / cbo_total * 100
raw_recipients_pct = (raw_recipients - dol_recipients) / dol_recipients * 100

print(f"PolicyEngine vs admin:")
print(f" Benefits: {pe_benefits_pct:+.1f}%")
print(f" Recipients: {pe_recipients_pct:+.1f}%")
print(f"\nRaw CPS vs admin:")
print(f" Benefits: {raw_benefits_pct:+.1f}%")
print(f" Recipients: {raw_recipients_pct:+.1f}%")