From 57c4a849a4f747a921c4009ab65042f5ce0f30ed Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:47:46 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 19 + README.md | 3 + plugin.lock.json | 108 +++ skills/microdf-skill/SKILL.md | 329 +++++++ skills/policyengine-analysis-skill/SKILL.md | 569 +++++++++++ .../examples/reform_template.py | 178 ++++ skills/policyengine-design-skill/SKILL.md | 880 ++++++++++++++++++ .../policyengine-python-client-skill/SKILL.md | 356 +++++++ skills/policyengine-uk-skill/SKILL.md | 660 +++++++++++++ .../examples/couple.yaml | 29 + .../examples/family_with_children.yaml | 41 + .../examples/single_person.yaml | 21 + .../examples/universal_credit_sweep.yaml | 38 + .../scripts/situation_helpers.py | 339 +++++++ skills/policyengine-us-skill/SKILL.md | 524 +++++++++++ .../examples/donation_sweep.yaml | 71 ++ .../examples/single_filer.yaml | 38 + .../scripts/situation_helpers.py | 257 +++++ skills/policyengine-user-guide-skill/SKILL.md | 295 ++++++ skills/policyengine-writing-skill/SKILL.md | 526 +++++++++++ 20 files changed, 5281 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 plugin.lock.json create mode 100644 skills/microdf-skill/SKILL.md create mode 100644 skills/policyengine-analysis-skill/SKILL.md create mode 100644 skills/policyengine-analysis-skill/examples/reform_template.py create mode 100644 skills/policyengine-design-skill/SKILL.md create mode 100644 skills/policyengine-python-client-skill/SKILL.md create mode 100644 skills/policyengine-uk-skill/SKILL.md create mode 100644 skills/policyengine-uk-skill/examples/couple.yaml create mode 100644 skills/policyengine-uk-skill/examples/family_with_children.yaml create mode 100644 skills/policyengine-uk-skill/examples/single_person.yaml create mode 100644 skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml create mode 100644 skills/policyengine-uk-skill/scripts/situation_helpers.py create mode 100644 skills/policyengine-us-skill/SKILL.md create mode 100644 skills/policyengine-us-skill/examples/donation_sweep.yaml create mode 100644 skills/policyengine-us-skill/examples/single_filer.yaml create mode 100644 skills/policyengine-us-skill/scripts/situation_helpers.py create mode 100644 skills/policyengine-user-guide-skill/SKILL.md create mode 100644 skills/policyengine-writing-skill/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..556a44c --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "analysis-tools", + "description": "Policy analysis and research - impact studies, dashboards, notebooks, and visualizations", + "version": "0.0.0-2025.11.28", + "author": { + "name": "PolicyEngine", + "email": "hello@policyengine.org" + }, + "skills": [ + "./skills/policyengine-user-guide-skill", + "./skills/policyengine-python-client-skill", + "./skills/policyengine-us-skill", + "./skills/policyengine-uk-skill", + "./skills/policyengine-analysis-skill", + "./skills/microdf-skill", + "./skills/policyengine-design-skill", + "./skills/policyengine-writing-skill" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6f5623b --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# analysis-tools + +Policy analysis and research - impact studies, dashboards, notebooks, and visualizations diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..252336d --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,108 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:PolicyEngine/policyengine-claude:analysis-tools", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "106b0f5a260eaef291a2f568645409334b0b6e44", + "treeHash": "c6366e5027b8f3e27751e58cee7ecc403d882e27a4f21c4372e6e6150acdd835", + "generatedAt": "2025-11-28T10:12:37.466145Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "analysis-tools", + "description": "Policy analysis and research - impact studies, dashboards, notebooks, and visualizations" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "7adda3d92149bc1082824c5e0b436176dfbd2224dc3e9f3fd3efc7b7ed284047" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "f49feadc0b151cf3fc09c6d39a4ef268da1cfa9047ce2a8c6efc4958f895b82e" + }, + { + "path": "skills/policyengine-python-client-skill/SKILL.md", + "sha256": "dff10e9c433461089944a26e3cfac39e0a61754391df41748a97a65dd5c49d79" + }, + { + "path": "skills/microdf-skill/SKILL.md", + "sha256": "dd450afeb898e26a8936d2fde0cf28b4a5024e1e38cc27ad2674104cf72b43fd" + }, + { + "path": "skills/policyengine-analysis-skill/SKILL.md", + "sha256": "a1079e6448eb0e9cb31b6fcbd3e87f3ed04dd04d99dde95353b948b8c4c5dc06" + }, + { + "path": "skills/policyengine-analysis-skill/examples/reform_template.py", + "sha256": "6fd0bef010e9555326ac71eae67a4cbd2dec62eddb863f51676f01a925d58c1e" + }, + { + "path": "skills/policyengine-writing-skill/SKILL.md", + "sha256": "7b1b6dfecb7db0cfde2cc87548e5292c4851c1d9062b549907cbcd9959d7fb19" + }, + { + "path": "skills/policyengine-user-guide-skill/SKILL.md", + "sha256": "f5f30d5af0d986de0350b16ab8220d3536040ec7863f179e792bde03e6aabbf1" + }, + { + "path": "skills/policyengine-us-skill/SKILL.md", + "sha256": "434d93548e3c792320c2ac4e736ec3327218829df30e3b7f06336bac833b2833" + }, + { + "path": "skills/policyengine-us-skill/examples/donation_sweep.yaml", + "sha256": "2d9294d931daa667c66c8a8a011524d15adbafb068f764b3d261086e0774ff7e" + }, + { + "path": "skills/policyengine-us-skill/examples/single_filer.yaml", + "sha256": "d14503e102c796e2141d56d85db4405f1681756d58158469f97ebc0a6c0f022f" + }, + { + "path": "skills/policyengine-us-skill/scripts/situation_helpers.py", + "sha256": "0fa2858e702ff64bbece06e1b383d9a07a034abb72f5467323aca8c8db40a97d" + }, + { + "path": "skills/policyengine-uk-skill/SKILL.md", + "sha256": "02cfc0e284c7d57be4cc265a6b57145f5485022a9b4b29a300e43c575a476890" + }, + { + "path": "skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml", + "sha256": "6d8577eded1f047b7c064b4344de3e1cdb85e410f6d3240986fe44f2efef4669" + }, + { + "path": "skills/policyengine-uk-skill/examples/couple.yaml", + "sha256": "e1d115fae8f72cea29c00fb2aeb712bff8e2f6ed29ddd6515692695a1a79ffc1" + }, + { + "path": "skills/policyengine-uk-skill/examples/family_with_children.yaml", + "sha256": "b6f69b29ae9827b20a2d01c8fc5daf614d1fe875d4d680b7a21273f707671c16" + }, + { + "path": "skills/policyengine-uk-skill/examples/single_person.yaml", + "sha256": "5f96d30e99f1e0f834cdcaf575bc260d681314d614f27d975f60d05202606995" + }, + { + "path": "skills/policyengine-uk-skill/scripts/situation_helpers.py", + "sha256": "8281fad497f12c98b82a10a5474e502a2e48dd4ece0bdc2ff6da846b9fc12049" + }, + { + "path": "skills/policyengine-design-skill/SKILL.md", + "sha256": "ca0653af9eda135b5f98c64446b35474ba7bd641dae111c666e6f6995e39c70c" + } + ], + "dirSha256": "c6366e5027b8f3e27751e58cee7ecc403d882e27a4f21c4372e6e6150acdd835" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/microdf-skill/SKILL.md b/skills/microdf-skill/SKILL.md new file mode 100644 index 0000000..40a0d9c --- /dev/null +++ b/skills/microdf-skill/SKILL.md @@ -0,0 +1,329 @@ +--- +name: microdf +description: Weighted pandas DataFrames for survey microdata analysis - inequality, poverty, and distributional calculations +--- + +# MicroDF + +MicroDF provides weighted pandas DataFrames and Series for analyzing survey microdata, with built-in support for inequality and poverty calculations. + +## For Users 👥 + +### What is MicroDF? + +When you see poverty rates, Gini coefficients, or distributional charts in PolicyEngine, those are calculated using MicroDF. + +**MicroDF powers:** +- Poverty rate calculations (SPM) +- Inequality metrics (Gini coefficient) +- Income distribution analysis +- Weighted statistics from survey data + +### Understanding the Metrics + +**Gini coefficient:** +- Calculated using MicroDF from weighted income data +- Ranges from 0 (perfect equality) to 1 (perfect inequality) +- US typically around 0.48 + +**Poverty rates:** +- Calculated using MicroDF with weighted household data +- Compares income to poverty thresholds +- Accounts for household composition + +**Percentiles:** +- MicroDF calculates weighted percentiles +- Shows income distribution (10th, 50th, 90th percentile) + +## For Analysts 📊 + +### Installation + +```bash +pip install microdf-python +``` + +### Quick Start + +```python +import microdf as mdf +import pandas as pd + +# Create sample data +df = pd.DataFrame({ + 'income': [10000, 20000, 30000, 40000, 50000], + 'weights': [1, 2, 3, 2, 1] +}) + +# Create MicroDataFrame +mdf_df = mdf.MicroDataFrame(df, weights='weights') + +# All operations are weight-aware +print(f"Weighted mean: ${mdf_df.income.mean():,.0f}") +print(f"Gini coefficient: {mdf_df.income.gini():.3f}") +``` + +### Common Operations + +**Weighted statistics:** +```python +mdf_df.income.mean() # Weighted mean +mdf_df.income.median() # Weighted median +mdf_df.income.sum() # Weighted sum +mdf_df.income.std() # Weighted standard deviation +``` + +**Inequality metrics:** +```python +mdf_df.income.gini() # Gini coefficient +mdf_df.income.top_x_pct_share(10) # Top 10% share +mdf_df.income.top_x_pct_share(1) # Top 1% share +``` + +**Poverty analysis:** +```python +# Poverty rate (income < threshold) +poverty_rate = mdf_df.poverty_rate( + income_measure='income', + threshold=poverty_line +) + +# Poverty gap (how far below threshold) +poverty_gap = mdf_df.poverty_gap( + income_measure='income', + threshold=poverty_line +) + +# Deep poverty (income < 50% of threshold) +deep_poverty_rate = mdf_df.deep_poverty_rate( + income_measure='income', + threshold=poverty_line, + deep_poverty_line=0.5 +) +``` + +**Quantiles:** +```python +# Deciles +mdf_df.income.decile_values() + +# Quintiles +mdf_df.income.quintile_values() + +# Custom quantiles +mdf_df.income.quantile(0.25) # 25th percentile +``` + +### MicroSeries + +```python +# Extract a Series with weights +income_series = mdf_df.income # This is a MicroSeries + +# MicroSeries operations +income_series.mean() +income_series.gini() +income_series.percentile(50) +``` + +### Working with PolicyEngine Results + +```python +import microdf as mdf +from policyengine_us import Simulation + +# Run simulation with axes (multiple households) +situation_with_axes = {...} # See policyengine-us-skill +sim = Simulation(situation=situation_with_axes) + +# Get results as arrays +incomes = sim.calculate("household_net_income", 2024) +weights = sim.calculate("household_weight", 2024) + +# Create MicroDataFrame +df = pd.DataFrame({'income': incomes, 'weight': weights}) +mdf_df = mdf.MicroDataFrame(df, weights='weight') + +# Calculate metrics +gini = mdf_df.income.gini() +poverty_rate = mdf_df.poverty_rate('income', threshold=15000) + +print(f"Gini: {gini:.3f}") +print(f"Poverty rate: {poverty_rate:.1%}") +``` + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/microdf + +**Clone:** +```bash +git clone https://github.com/PolicyEngine/microdf +cd microdf +``` + +### Current Implementation + +**To see current API:** +```bash +# Main classes +cat microdf/microframe.py # MicroDataFrame +cat microdf/microseries.py # MicroSeries + +# Key modules +cat microdf/generic.py # Generic weighted operations +cat microdf/inequality.py # Gini, top shares +cat microdf/poverty.py # Poverty metrics +``` + +**To see all methods:** +```bash +# MicroDataFrame methods +grep "def " microdf/microframe.py + +# MicroSeries methods +grep "def " microdf/microseries.py +``` + +### Testing + +**To see test patterns:** +```bash +ls tests/ +cat tests/test_microframe.py +``` + +**Run tests:** +```bash +make test + +# Or +pytest tests/ -v +``` + +### Contributing + +**Before contributing:** +1. Check if method already exists +2. Ensure it's weighted correctly +3. Add tests +4. Follow policyengine-standards-skill + +**Common contributions:** +- New inequality metrics +- New poverty measures +- Performance optimizations +- Bug fixes + +## Advanced Patterns + +### Custom Aggregations + +```python +# Define custom weighted aggregation +def weighted_operation(series, weights): + return (series * weights).sum() / weights.sum() + +# Apply to MicroSeries +result = weighted_operation(mdf_df.income, mdf_df.weights) +``` + +### Groupby Operations + +```python +# Group by with weights +grouped = mdf_df.groupby('state') +state_means = grouped.income.mean() # Weighted means by state +``` + +### Inequality Decomposition + +**To see decomposition methods:** +```bash +grep -A 20 "def.*decomp" microdf/ +``` + +## Integration Examples + +### Example 1: PolicyEngine Blog Post Analysis + +```python +# Pattern from PolicyEngine blog posts +import microdf as mdf + +# Get simulation results +baseline_income = baseline_sim.calculate("household_net_income", 2024) +reform_income = reform_sim.calculate("household_net_income", 2024) +weights = baseline_sim.calculate("household_weight", 2024) + +# Create MicroDataFrame +df = pd.DataFrame({ + 'baseline_income': baseline_income, + 'reform_income': reform_income, + 'weight': weights +}) +mdf_df = mdf.MicroDataFrame(df, weights='weight') + +# Calculate impacts +baseline_gini = mdf_df.baseline_income.gini() +reform_gini = mdf_df.reform_income.gini() + +print(f"Gini change: {reform_gini - baseline_gini:+.4f}") +``` + +### Example 2: Poverty Analysis + +```python +# Calculate poverty under baseline and reform +from policyengine_us import Simulation + +baseline_sim = Simulation(situation=situation) +reform_sim = Simulation(situation=situation, reform=reform) + +# Get incomes +baseline_income = baseline_sim.calculate("spm_unit_net_income", 2024) +reform_income = reform_sim.calculate("spm_unit_net_income", 2024) +spm_threshold = baseline_sim.calculate("spm_unit_poverty_threshold", 2024) +weights = baseline_sim.calculate("spm_unit_weight", 2024) + +# Calculate poverty rates +df_baseline = mdf.MicroDataFrame( + pd.DataFrame({'income': baseline_income, 'threshold': spm_threshold, 'weight': weights}), + weights='weight' +) + +poverty_baseline = (df_baseline.income < df_baseline.threshold).mean() # Weighted + +# Similar for reform +print(f"Poverty reduction: {(poverty_baseline - poverty_reform):.1%}") +``` + +## Package Status + +**Maturity:** Stable, production-ready +**API stability:** Stable (rarely breaking changes) +**Performance:** Optimized for large datasets + +**To see version:** +```bash +pip show microdf-python +``` + +**To see changelog:** +```bash +cat CHANGELOG.md # In microdf repo +``` + +## Related Skills + +- **policyengine-us-skill** - Generating data for microdf analysis +- **policyengine-analysis-skill** - Using microdf in policy analysis +- **policyengine-us-data-skill** - Data sources for microdf + +## Resources + +**Repository:** https://github.com/PolicyEngine/microdf +**PyPI:** https://pypi.org/project/microdf-python/ +**Issues:** https://github.com/PolicyEngine/microdf/issues diff --git a/skills/policyengine-analysis-skill/SKILL.md b/skills/policyengine-analysis-skill/SKILL.md new file mode 100644 index 0000000..231072f --- /dev/null +++ b/skills/policyengine-analysis-skill/SKILL.md @@ -0,0 +1,569 @@ +--- +name: policyengine-analysis +description: Common analysis patterns for PolicyEngine research repositories (CRFB, newsletters, dashboards, impact studies) +--- + +# PolicyEngine Analysis + +Patterns for creating policy impact analyses, dashboards, and research using PolicyEngine. + +## For Users 👥 + +### What are Analysis Repositories? + +Analysis repositories produce the research you see on PolicyEngine: + +**Blog posts:** +- "How Montana's tax cuts affect poverty" +- "Harris EITC proposal costs and impacts" +- "UK Budget 2024 analysis" + +**Dashboards:** +- State tax comparisons +- Policy proposal scorecards +- Interactive calculators (GiveCalc, SALT calculator) + +**Research reports:** +- Distributional analyses for organizations +- Policy briefs for legislators +- Impact assessments + +### How Analysis Works + +1. **Define policy reform** using PolicyEngine parameters +2. **Create household examples** showing specific impacts +3. **Run population simulations** for aggregate effects +4. **Calculate distributional impacts** (who wins, who loses) +5. **Create visualizations** (charts, tables) +6. **Write report** following policyengine-writing-skill style +7. **Publish** to blog or share with stakeholders + +### Reading PolicyEngine Analysis + +**Key sections in typical analysis:** + +**The proposal:** +- What policy changes +- Specific parameter values + +**Household impacts:** +- 3-5 example households +- Dollar amounts for each +- Charts showing impact across income range + +**Statewide/national impacts:** +- Total cost or revenue +- Winners and losers by income decile +- Poverty and inequality effects + +**See policyengine-writing-skill for writing conventions.** + +## For Analysts 📊 + +### When to Use This Skill + +- Creating policy impact analyses +- Building interactive dashboards with Streamlit/Plotly +- Writing analysis notebooks +- Calculating distributional impacts +- Comparing policy proposals +- Creating visualizations for research +- Publishing policy research + +### Example Analysis Repositories + +- `crfb-tob-impacts` - Policy impact analyses +- `newsletters` - Data-driven newsletters +- `2024-election-dashboard` - Policy comparison dashboards +- `marginal-child` - Specialized policy analyses +- `givecalc` - Charitable giving calculator + +## Repository Structure + +Standard analysis repository structure: + +``` +analysis-repo/ +├── analysis.ipynb # Main Jupyter notebook +├── app.py # Streamlit app (if applicable) +├── requirements.txt # Python dependencies +├── README.md # Documentation +├── data/ # Data files (if needed) +├── outputs/ # Generated charts, tables +└── .streamlit/ # Streamlit config + └── config.toml +``` + +## Common Analysis Patterns + +### Pattern 1: Impact Analysis Across Income Distribution + +```python +import pandas as pd +import numpy as np +from policyengine_us import Simulation + +# Define reform +reform = { + "gov.irs.credits.ctc.amount.base_amount": { + "2024-01-01.2100-12-31": 5000 + } +} + +# Analyze across income distribution +incomes = np.linspace(0, 200000, 101) +results = [] + +for income in incomes: + # Baseline + situation = create_situation(income=income) + sim_baseline = Simulation(situation=situation) + tax_baseline = sim_baseline.calculate("income_tax", 2024)[0] + + # Reform + sim_reform = Simulation(situation=situation, reform=reform) + tax_reform = sim_reform.calculate("income_tax", 2024)[0] + + results.append({ + "income": income, + "tax_baseline": tax_baseline, + "tax_reform": tax_reform, + "tax_change": tax_reform - tax_baseline + }) + +df = pd.DataFrame(results) +``` + +### Pattern 2: Household-Level Case Studies + +```python +# Define representative households +households = { + "Single, No Children": { + "income": 40000, + "num_children": 0, + "married": False + }, + "Single Parent, 2 Children": { + "income": 50000, + "num_children": 2, + "married": False + }, + "Married, 2 Children": { + "income": 100000, + "num_children": 2, + "married": True + } +} + +# Calculate impacts for each +case_studies = {} +for name, params in households.items(): + situation = create_family(**params) + + sim_baseline = Simulation(situation=situation) + sim_reform = Simulation(situation=situation, reform=reform) + + case_studies[name] = { + "baseline_tax": sim_baseline.calculate("income_tax", 2024)[0], + "reform_tax": sim_reform.calculate("income_tax", 2024)[0], + "ctc_baseline": sim_baseline.calculate("ctc", 2024)[0], + "ctc_reform": sim_reform.calculate("ctc", 2024)[0] + } + +case_df = pd.DataFrame(case_studies).T +``` + +### Pattern 3: State-by-State Comparison + +```python +states = ["CA", "NY", "TX", "FL", "PA", "OH", "IL", "MI"] + +state_results = [] +for state in states: + situation = create_situation(income=75000, state=state) + + sim_baseline = Simulation(situation=situation) + sim_reform = Simulation(situation=situation, reform=reform) + + state_results.append({ + "state": state, + "baseline_net_income": sim_baseline.calculate("household_net_income", 2024)[0], + "reform_net_income": sim_reform.calculate("household_net_income", 2024)[0], + "change": (sim_reform.calculate("household_net_income", 2024)[0] - + sim_baseline.calculate("household_net_income", 2024)[0]) + }) + +state_df = pd.DataFrame(state_results) +``` + +### Pattern 4: Marginal Analysis (Winners/Losers) + +```python +import plotly.graph_objects as go + +# Calculate across income range +situation_with_axes = { + # ... setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 200000, + "period": 2024 + }]] +} + +sim_baseline = Simulation(situation=situation_with_axes) +sim_reform = Simulation(situation=situation_with_axes, reform=reform) + +incomes = sim_baseline.calculate("employment_income", 2024) +baseline_net = sim_baseline.calculate("household_net_income", 2024) +reform_net = sim_reform.calculate("household_net_income", 2024) + +gains = reform_net - baseline_net + +# Identify winners and losers +winners = gains > 0 +losers = gains < 0 +neutral = gains == 0 + +print(f"Winners: {winners.sum() / len(gains) * 100:.1f}%") +print(f"Losers: {losers.sum() / len(gains) * 100:.1f}%") +print(f"Neutral: {neutral.sum() / len(gains) * 100:.1f}%") +``` + +## Visualization Patterns + +### Standard Plotly Configuration + +```python +import plotly.graph_objects as go + +# PolicyEngine brand colors +TEAL = "#39C6C0" +BLUE = "#2C6496" +DARK_GRAY = "#616161" + +def create_pe_layout(title, xaxis_title, yaxis_title): + """Create standard PolicyEngine chart layout.""" + return go.Layout( + title=title, + xaxis_title=xaxis_title, + yaxis_title=yaxis_title, + font=dict(family="Roboto Serif", size=14), + plot_bgcolor="white", + hovermode="x unified", + xaxis=dict( + showgrid=True, + gridcolor="lightgray", + zeroline=True + ), + yaxis=dict( + showgrid=True, + gridcolor="lightgray", + zeroline=True + ) + ) + +# Use in charts +fig = go.Figure(layout=create_pe_layout( + "Tax Impact by Income", + "Income", + "Tax Change" +)) +fig.add_trace(go.Scatter(x=incomes, y=tax_change, line=dict(color=TEAL))) +``` + +### Common Chart Types + +**1. Line Chart (Impact by Income)** +```python +fig = go.Figure() +fig.add_trace(go.Scatter( + x=df.income, + y=df.tax_change, + mode='lines', + name='Tax Change', + line=dict(color=TEAL, width=3) +)) +fig.update_layout( + title="Tax Impact by Income Level", + xaxis_title="Income", + yaxis_title="Tax Change ($)", + xaxis_tickformat="$,.0f", + yaxis_tickformat="$,.0f" +) +``` + +**2. Bar Chart (State Comparison)** +```python +fig = go.Figure() +fig.add_trace(go.Bar( + x=state_df.state, + y=state_df.change, + marker_color=TEAL +)) +fig.update_layout( + title="Net Income Change by State", + xaxis_title="State", + yaxis_title="Change ($)", + yaxis_tickformat="$,.0f" +) +``` + +**3. Waterfall Chart (Budget Impact)** +```python +fig = go.Figure(go.Waterfall( + x=["Baseline", "Tax Credit", "Phase-out", "Reform"], + y=[baseline_revenue, credit_cost, phaseout_revenue, 0], + measure=["absolute", "relative", "relative", "total"], + connector={"line": {"color": "gray"}} +)) +``` + +## Streamlit Dashboard Patterns + +### Basic Streamlit Setup + +```python +import streamlit as st +from policyengine_us import Simulation + +st.set_page_config(page_title="Policy Analysis", layout="wide") + +st.title("Policy Impact Calculator") + +# User inputs +col1, col2, col3 = st.columns(3) +with col1: + income = st.number_input("Income", value=60000, step=5000) +with col2: + state = st.selectbox("State", ["CA", "NY", "TX", "FL"]) +with col3: + num_children = st.number_input("Children", value=0, min_value=0, max_value=10) + +# Calculate +if st.button("Calculate"): + situation = create_family( + parent_income=income, + num_children=num_children, + state=state + ) + + sim_baseline = Simulation(situation=situation) + sim_reform = Simulation(situation=situation, reform=reform) + + # Display results + col1, col2, col3 = st.columns(3) + with col1: + st.metric( + "Baseline Tax", + f"${sim_baseline.calculate('income_tax', 2024)[0]:,.0f}" + ) + with col2: + st.metric( + "Reform Tax", + f"${sim_reform.calculate('income_tax', 2024)[0]:,.0f}" + ) + with col3: + change = (sim_reform.calculate('income_tax', 2024)[0] - + sim_baseline.calculate('income_tax', 2024)[0]) + st.metric("Change", f"${change:,.0f}", delta=f"${-change:,.0f}") +``` + +### Interactive Chart with Streamlit + +```python +# Create chart based on user inputs +incomes = np.linspace(0, income_max, 1001) +results = [] + +for income in incomes: + situation = create_situation(income=income, state=selected_state) + sim = Simulation(situation=situation, reform=reform) + results.append(sim.calculate("household_net_income", 2024)[0]) + +fig = go.Figure() +fig.add_trace(go.Scatter(x=incomes, y=results, line=dict(color=TEAL))) +st.plotly_chart(fig, use_container_width=True) +``` + +## Jupyter Notebook Best Practices + +### Notebook Structure + +```python +# Cell 1: Title and Description +""" +# Policy Analysis: [Policy Name] + +**Date:** [Date] +**Author:** [Your Name] + +## Summary +Brief description of the analysis and key findings. +""" + +# Cell 2: Imports +import pandas as pd +import numpy as np +import plotly.graph_objects as go +from policyengine_us import Simulation + +# Cell 3: Configuration +YEAR = 2024 +STATES = ["CA", "NY", "TX", "FL"] + +# Cell 4+: Analysis sections with markdown headers +``` + +### Export Results + +```python +# Save DataFrame +df.to_csv("outputs/impact_analysis.csv", index=False) + +# Save Plotly chart +fig.write_html("outputs/chart.html") +fig.write_image("outputs/chart.png", width=1200, height=600) + +# Save summary statistics +summary = { + "total_winners": winners.sum(), + "total_losers": losers.sum(), + "avg_gain": gains[winners].mean(), + "avg_loss": gains[losers].mean() +} +pd.DataFrame([summary]).to_csv("outputs/summary.csv", index=False) +``` + +## Repository-Specific Examples + +This skill includes example templates in the `examples/` directory: + +- `impact_analysis_template.ipynb` - Standard impact analysis +- `dashboard_template.py` - Streamlit dashboard +- `state_comparison.py` - State-by-state analysis +- `case_studies.py` - Household case studies +- `reform_definitions.py` - Common reform patterns + +## Common Pitfalls + +### Pitfall 1: Not Using Consistent Year +**Problem:** Mixing 2024 and 2025 calculations + +**Solution:** Define year constant at top: +```python +CURRENT_YEAR = 2024 +# Use everywhere +simulation.calculate("income_tax", CURRENT_YEAR) +``` + +### Pitfall 2: Inefficient Simulations +**Problem:** Creating new simulation for each income level + +**Solution:** Use axes for efficiency: +```python +# SLOW +for income in incomes: + situation = create_situation(income=income) + sim = Simulation(situation=situation) + results.append(sim.calculate("income_tax", 2024)[0]) + +# FAST +situation_with_axes = create_situation_with_axes(incomes) +sim = Simulation(situation=situation_with_axes) +results = sim.calculate("income_tax", 2024) # Array of all results +``` + +### Pitfall 3: Forgetting to Compare Baseline and Reform +**Problem:** Only showing reform results + +**Solution:** Always show both: +```python +results = { + "baseline": sim_baseline.calculate("income_tax", 2024), + "reform": sim_reform.calculate("income_tax", 2024), + "change": reform - baseline +} +``` + +## PolicyEngine API Usage + +For larger-scale analyses, use the PolicyEngine API: + +```python +import requests + +def calculate_via_api(situation, reform=None): + """Calculate using PolicyEngine API.""" + url = "https://api.policyengine.org/us/calculate" + + payload = { + "household": situation, + "policy_id": reform_id if reform else baseline_policy_id + } + + response = requests.post(url, json=payload) + return response.json() +``` + +## Testing Analysis Code + +```python +import pytest + +def test_reform_increases_ctc(): + """Test that reform increases CTC as expected.""" + situation = create_family(income=50000, num_children=2) + + sim_baseline = Simulation(situation=situation) + sim_reform = Simulation(situation=situation, reform=reform) + + ctc_baseline = sim_baseline.calculate("ctc", 2024)[0] + ctc_reform = sim_reform.calculate("ctc", 2024)[0] + + assert ctc_reform > ctc_baseline, "Reform should increase CTC" + assert ctc_reform == 5000 * 2, "CTC should be $5000 per child" +``` + +## Documentation Standards + +### README Template + +```markdown +# [Analysis Name] + +## Overview +Brief description of the analysis. + +## Key Findings +- Finding 1 +- Finding 2 +- Finding 3 + +## Methodology +Explanation of approach and data sources. + +## How to Run + +\```bash +pip install -r requirements.txt +python app.py # or jupyter notebook analysis.ipynb +\``` + +## Outputs +- `outputs/chart1.png` - Description +- `outputs/results.csv` - Description + +## Contact +PolicyEngine Team - hello@policyengine.org +``` + +## Additional Resources + +- **PolicyEngine API Docs:** https://policyengine.org/us/api +- **Analysis Examples:** https://github.com/PolicyEngine/analysis-notebooks +- **Streamlit Docs:** https://docs.streamlit.io +- **Plotly Docs:** https://plotly.com/python/ diff --git a/skills/policyengine-analysis-skill/examples/reform_template.py b/skills/policyengine-analysis-skill/examples/reform_template.py new file mode 100644 index 0000000..5bd541f --- /dev/null +++ b/skills/policyengine-analysis-skill/examples/reform_template.py @@ -0,0 +1,178 @@ +""" +Template for PolicyEngine reform impact analysis. + +This template provides a starting point for analyzing the impact +of a policy reform across the income distribution. +""" + +import pandas as pd +import numpy as np +import plotly.graph_objects as go +from policyengine_us import Simulation + +# Configuration +CURRENT_YEAR = 2024 +INCOME_MIN = 0 +INCOME_MAX = 200000 +INCOME_STEPS = 101 + +# Define your reform here +REFORM = { + "gov.irs.credits.ctc.amount.base_amount": { + "2024-01-01.2100-12-31": 5000 # Example: Increase CTC to $5,000 + } +} + + +def create_situation(income, num_children=0, state="CA"): + """Create a basic household situation.""" + people = { + "parent": { + "age": {CURRENT_YEAR: 35}, + "employment_income": {CURRENT_YEAR: income} + } + } + + members = ["parent"] + + # Add children + for i in range(num_children): + child_id = f"child_{i+1}" + people[child_id] = {"age": {CURRENT_YEAR: 8}} + members.append(child_id) + + return { + "people": people, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "households": { + "household": { + "members": members, + "state_name": {CURRENT_YEAR: state} + } + } + } + + +def analyze_reform(num_children=2, state="CA"): + """Analyze reform impact across income distribution.""" + incomes = np.linspace(INCOME_MIN, INCOME_MAX, INCOME_STEPS) + results = [] + + for income in incomes: + situation = create_situation( + income=income, + num_children=num_children, + state=state + ) + + # Baseline + sim_baseline = Simulation(situation=situation) + income_tax_baseline = sim_baseline.calculate("income_tax", CURRENT_YEAR)[0] + ctc_baseline = sim_baseline.calculate("ctc", CURRENT_YEAR)[0] + net_income_baseline = sim_baseline.calculate("household_net_income", CURRENT_YEAR)[0] + + # Reform + sim_reform = Simulation(situation=situation, reform=REFORM) + income_tax_reform = sim_reform.calculate("income_tax", CURRENT_YEAR)[0] + ctc_reform = sim_reform.calculate("ctc", CURRENT_YEAR)[0] + net_income_reform = sim_reform.calculate("household_net_income", CURRENT_YEAR)[0] + + results.append({ + "income": income, + "income_tax_baseline": income_tax_baseline, + "income_tax_reform": income_tax_reform, + "ctc_baseline": ctc_baseline, + "ctc_reform": ctc_reform, + "net_income_baseline": net_income_baseline, + "net_income_reform": net_income_reform, + "tax_change": income_tax_reform - income_tax_baseline, + "ctc_change": ctc_reform - ctc_baseline, + "net_income_change": net_income_reform - net_income_baseline + }) + + return pd.DataFrame(results) + + +def create_chart(df, title="Reform Impact Analysis"): + """Create Plotly chart of reform impacts.""" + TEAL = "#39C6C0" + + fig = go.Figure() + + fig.add_trace(go.Scatter( + x=df.income, + y=df.net_income_change, + mode='lines', + name='Net Income Change', + line=dict(color=TEAL, width=3) + )) + + fig.update_layout( + title=title, + xaxis_title="Income", + yaxis_title="Net Income Change ($)", + font=dict(family="Roboto Serif", size=14), + plot_bgcolor="white", + hovermode="x unified", + xaxis=dict( + tickformat="$,.0f", + showgrid=True, + gridcolor="lightgray" + ), + yaxis=dict( + tickformat="$,.0f", + showgrid=True, + gridcolor="lightgray", + zeroline=True, + zerolinecolor="black", + zerolinewidth=1 + ) + ) + + return fig + + +def print_summary(df): + """Print summary statistics.""" + print("\n=== Reform Impact Summary ===\n") + + winners = df[df.net_income_change > 0] + losers = df[df.net_income_change < 0] + + print(f"Winners: {len(winners) / len(df) * 100:.1f}%") + print(f"Losers: {len(losers) / len(df) * 100:.1f}%") + + if len(winners) > 0: + print(f"\nAverage gain (winners): ${winners.net_income_change.mean():,.2f}") + print(f"Max gain: ${df.net_income_change.max():,.2f}") + + if len(losers) > 0: + print(f"\nAverage loss (losers): ${losers.net_income_change.mean():,.2f}") + print(f"Max loss: ${df.net_income_change.min():,.2f}") + + print(f"\nAverage CTC change: ${df.ctc_change.mean():,.2f}") + print(f"Average tax change: ${df.tax_change.mean():,.2f}") + + +if __name__ == "__main__": + # Run analysis + print("Running reform analysis...") + df = analyze_reform(num_children=2, state="CA") + + # Print summary + print_summary(df) + + # Save results + df.to_csv("reform_impact_results.csv", index=False) + print("\nResults saved to reform_impact_results.csv") + + # Create and save chart + fig = create_chart(df) + fig.write_html("reform_impact_chart.html") + print("Chart saved to reform_impact_chart.html") + + # Display chart (if running interactively) + fig.show() diff --git a/skills/policyengine-design-skill/SKILL.md b/skills/policyengine-design-skill/SKILL.md new file mode 100644 index 0000000..d5526ca --- /dev/null +++ b/skills/policyengine-design-skill/SKILL.md @@ -0,0 +1,880 @@ +--- +name: policyengine-design +description: PolicyEngine visual identity - colors, fonts, logos, and branding for web apps, calculators, charts, and research +--- + +# PolicyEngine Design System + +PolicyEngine's visual identity and branding guidelines for creating consistent user experiences across web apps, calculators, charts, and research outputs. + +## For Users 👥 + +### PolicyEngine Visual Identity + +**Brand colors:** +- **Teal** (#39C6C0) - Primary accent color (buttons, highlights, interactive elements) +- **Blue** (#2C6496) - Secondary color (links, charts, headers) + +**Typography:** +- **Charts:** Roboto Serif +- **Web app:** System fonts (sans-serif) +- **Streamlit apps:** Default sans-serif + +**Logo:** +- Used in charts (bottom right) +- Blue version for light backgrounds +- White version for dark backgrounds + +### Recognizing PolicyEngine Content + +**You can identify PolicyEngine content by:** +- Teal accent color (#39C6C0) on buttons and interactive elements +- Blue (#2C6496) in charts and links +- Roboto Serif font in charts +- PolicyEngine logo in chart footer +- Clean, minimal white backgrounds +- Data-focused, quantitative presentation + +## For Analysts 📊 + +### Chart Branding + +When creating charts for PolicyEngine analysis, follow these guidelines: + +#### Color Palette + +**Primary colors:** +```python +TEAL_ACCENT = "#39C6C0" # Primary color (teal) +BLUE_PRIMARY = "#2C6496" # Secondary color (blue) +DARK_GRAY = "#616161" # Text color +``` + +**Extended palette:** +```python +# Blues +BLUE = "#2C6496" +BLUE_LIGHT = "#D8E6F3" +BLUE_PRESSED = "#17354F" +BLUE_98 = "#F7FAFD" +DARK_BLUE_HOVER = "#1d3e5e" +DARKEST_BLUE = "#0C1A27" + +# Teals +TEAL_ACCENT = "#39C6C0" +TEAL_LIGHT = "#F7FDFC" +TEAL_PRESSED = "#227773" + +# Grays +DARK_GRAY = "#616161" +GRAY = "#808080" +MEDIUM_LIGHT_GRAY = "#BDBDBD" +MEDIUM_DARK_GRAY = "#D2D2D2" +LIGHT_GRAY = "#F2F2F2" + +# Accents +WHITE = "#FFFFFF" +BLACK = "#000000" +DARK_RED = "#b50d0d" # For negative values +``` + +**See current colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +#### Plotly Chart Formatting + +**Standard PolicyEngine chart:** + +```python +import plotly.graph_objects as go + +def format_fig(fig): + """Format chart with PolicyEngine branding.""" + fig.update_layout( + # Typography + font=dict( + family="Roboto Serif", + color="black" + ), + + # Background + plot_bgcolor="white", + template="plotly_white", + + # Margins (leave room for logo) + margin=dict( + l=50, + r=100, + t=50, + b=120, + pad=4 + ), + + # Chart size + height=600, + width=800, + ) + + # Add PolicyEngine logo (bottom right) + fig.add_layout_image( + dict( + source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", + xref="paper", + yref="paper", + x=1.0, + y=-0.10, + sizex=0.10, + sizey=0.10, + xanchor="right", + yanchor="bottom" + ) + ) + + # Clean modebar + fig.update_layout( + modebar=dict( + bgcolor="rgba(0,0,0,0)", + color="rgba(0,0,0,0)" + ) + ) + + return fig + +# Usage +fig = go.Figure() +fig.add_trace(go.Scatter(x=x_data, y=y_data, line=dict(color=TEAL_ACCENT))) +fig = format_fig(fig) +``` + +**Current implementation:** +```bash +# See format_fig in action +cat givecalc/ui/visualization.py +cat policyengine-app/src/pages/policy/output/... +``` + +#### Chart Colors + +**For line charts:** +- Primary line: Teal (#39C6C0) or Blue (#2C6496) +- Background lines: Light gray (rgb(180, 180, 180)) +- Markers: Teal with 70% opacity + +**For bar charts:** +- Positive values: Teal (#39C6C0) +- Negative values: Dark red (#b50d0d) +- Neutral: Gray + +**For multiple series:** +Use variations of blue and teal, or discrete color scale: +```python +colors = ["#2C6496", "#39C6C0", "#17354F", "#227773"] +``` + +#### Typography + +**Charts:** +```python +font=dict(family="Roboto Serif", size=14, color="black") +``` + +**Axis labels:** +```python +xaxis=dict( + title=dict(text="Label", font=dict(size=14)), + tickfont=dict(size=12) +) +``` + +**Load Roboto font:** +```python +# In Streamlit apps +st.markdown(""" + + + +""", unsafe_allow_html=True) +``` + +### Streamlit App Branding + +**Streamlit configuration (.streamlit/config.toml):** + +```toml +[theme] +base = "light" +primaryColor = "#39C6C0" # Teal accent +backgroundColor = "#FFFFFF" # White background +secondaryBackgroundColor = "#F7FDFC" # Teal light +textColor = "#616161" # Dark gray + +[client] +toolbarMode = "minimal" +``` + +**Current implementation:** +```bash +cat givecalc/.streamlit/config.toml +cat salt-amt-calculator/.streamlit/config.toml # Other calculators +``` + +### Logo Usage + +**Logo URLs:** + +```python +# Blue logo (for light backgrounds) +LOGO_BLUE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png" + +# White logo (for dark backgrounds) +LOGO_WHITE = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png" + +# SVG versions (scalable) +LOGO_BLUE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg" +LOGO_WHITE_SVG = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg" +``` + +**Logo placement in charts:** +- Bottom right corner +- 10% of chart width +- Slightly below bottom edge (y=-0.10) + +**Current logos:** +```bash +ls policyengine-app/src/images/logos/policyengine/ +``` + +### Complete Example: Branded Chart + +```python +import plotly.graph_objects as go + +# PolicyEngine colors +TEAL_ACCENT = "#39C6C0" +BLUE_PRIMARY = "#2C6496" + +# Create chart +fig = go.Figure() + +# Add data +fig.add_trace(go.Scatter( + x=incomes, + y=taxes, + mode='lines', + name='Tax liability', + line=dict(color=TEAL_ACCENT, width=3) +)) + +# Apply PolicyEngine branding +fig.update_layout( + # Typography + font=dict(family="Roboto Serif", size=14, color="black"), + + # Title and labels + title="Tax liability by income", + xaxis_title="Income", + yaxis_title="Tax ($)", + + # Formatting + xaxis_tickformat="$,.0f", + yaxis_tickformat="$,.0f", + + # Appearance + plot_bgcolor="white", + template="plotly_white", + + # Size and margins + height=600, + width=800, + margin=dict(l=50, r=100, t=50, b=120, pad=4) +) + +# Add logo +fig.add_layout_image( + dict( + source="https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png", + xref="paper", + yref="paper", + x=1.0, + y=-0.10, + sizex=0.10, + sizey=0.10, + xanchor="right", + yanchor="bottom" + ) +) + +# Show +fig.show() +``` + +## For Contributors 💻 + +### Brand Assets + +**Repository:** PolicyEngine/policyengine-app-v2 (current), PolicyEngine/policyengine-app (legacy) + +**Logo files:** +```bash +# Logos in app (both v1 and v2 use same logos) +ls policyengine-app/src/images/logos/policyengine/ +# - blue.png - For light backgrounds +# - white.png - For dark backgrounds +# - blue.svg - Scalable blue logo +# - white.svg - Scalable white logo +# - banners/ - Banner variations +# - profile/ - Profile/avatar versions +``` + +**Access logos:** +```bash +# View logo files (v1 repo has the assets) +cd policyengine-app/src/images/logos/policyengine/ +ls -la +``` + +### Color Definitions + +**⚠️ IMPORTANT: App V2 Transition** + +PolicyEngine is transitioning to policyengine-app-v2 with updated design tokens. Use app-v2 colors for new projects. + +**Current colors (policyengine-app-v2):** + +```typescript +// policyengine-app-v2/app/src/designTokens/colors.ts + +// Primary (teal) - 50 to 900 scale +primary[500]: "#319795" // Main teal +primary[400]: "#38B2AC" // Lighter teal +primary[600]: "#2C7A7B" // Darker teal + +// Blue scale +blue[700]: "#026AA2" // Primary blue +blue[500]: "#0EA5E9" // Lighter blue + +// Gray scale +gray[700]: "#344054" // Dark text +gray[100]: "#F2F4F7" // Light backgrounds + +// Semantic +success: "#22C55E" +warning: "#FEC601" +error: "#EF4444" + +// Background +background.primary: "#FFFFFF" +background.secondary: "#F5F9FF" + +// Text +text.primary: "#000000" +text.secondary: "#5A5A5A" +``` + +**To see current design tokens:** +```bash +cat policyengine-app-v2/app/src/designTokens/colors.ts +cat policyengine-app-v2/app/src/styles/colors.ts # Mantine integration +``` + +**Legacy colors (policyengine-app - still used in some projects):** + +```javascript +// policyengine-app/src/style/colors.js +TEAL_ACCENT = "#39C6C0" // Old teal (slightly different from v2) +BLUE = "#2C6496" // Old blue +DARK_GRAY = "#616161" // Old dark gray +``` + +**To see legacy colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +**Usage in React (app-v2):** +```typescript +import { colors } from 'designTokens'; + + + +// Links +Learn more + +// Text +

Description

+``` + +**Current colors:** +```bash +cat policyengine-app/src/style/colors.js +``` + +## Visual Guidelines + +### Chart Design Principles + +1. **Minimal decoration** - Let data speak +2. **White backgrounds** - Clean, print-friendly +3. **Clear axis labels** - Always include units +4. **Formatted numbers** - Currency ($), percentages (%), etc. +5. **Logo inclusion** - Bottom right, never intrusive +6. **Consistent sizing** - 800x600 standard +7. **Roboto Serif** - Professional, readable font + +### Color Usage Rules + +**Primary actions:** +- Use TEAL_ACCENT (#39C6C0) +- Buttons, highlights, current selection + +**Chart lines:** +- Primary data: TEAL_ACCENT or BLUE_PRIMARY +- Secondary data: BLUE_LIGHT or GRAY +- Negative values: DARK_RED (#b50d0d) + +**Backgrounds:** +- Main: WHITE (#FFFFFF) +- Secondary: TEAL_LIGHT (#F7FDFC) or BLUE_98 (#F7FAFD) +- Plot area: WHITE + +**Text:** +- Primary: BLACK (#000000) +- Secondary: DARK_GRAY (#616161) +- Muted: GRAY (#808080) + +### Accessibility + +**Color contrast requirements:** +- Text on background: 4.5:1 minimum (WCAG AA) +- DARK_GRAY on WHITE: ✅ Passes +- TEAL_ACCENT on WHITE: ✅ Passes for large text +- Use sufficient line weights for visibility + +**Don't rely on color alone:** +- Use patterns or labels for different data series +- Ensure charts work in grayscale + +## Common Branding Tasks + +### Task 1: Create Branded Plotly Chart + +1. **Define colors:** + ```python + TEAL_ACCENT = "#39C6C0" + BLUE_PRIMARY = "#2C6496" + ``` + +2. **Create chart:** + ```python + fig = go.Figure() + fig.add_trace(go.Scatter(x=x, y=y, line=dict(color=TEAL_ACCENT))) + ``` + +3. **Apply branding:** + ```python + fig = format_fig(fig) # See implementation above + ``` + +### Task 2: Setup Streamlit Branding + +1. **Create config directory:** + ```bash + mkdir .streamlit + ``` + +2. **Copy theme config:** + ```bash + cat givecalc/.streamlit/config.toml > .streamlit/config.toml + ``` + +3. **Verify in app:** + ```python + import streamlit as st + + st.button("Test", type="primary") # Should be teal + ``` + +### Task 3: Brand Consistency Check + +**Checklist:** +- [ ] Charts use Roboto Serif font +- [ ] Primary color is TEAL_ACCENT (#39C6C0) +- [ ] Secondary color is BLUE_PRIMARY (#2C6496) +- [ ] White backgrounds +- [ ] Logo in charts (bottom right) +- [ ] Currency formatted with $ and commas +- [ ] Percentages formatted with % +- [ ] Streamlit config.toml uses PolicyEngine theme + +## Reference Implementations + +### Excellent Examples + +**Streamlit calculators:** +```bash +# GiveCalc - Complete example +cat givecalc/ui/visualization.py +cat givecalc/.streamlit/config.toml + +# Other calculators +ls salt-amt-calculator/ +ls ctc-calculator/ +``` + +**Blog post charts:** +```bash +# Analysis with branded charts +cat policyengine-app/src/posts/articles/harris-eitc.md +cat policyengine-app/src/posts/articles/montana-tax-cuts-2026.md +``` + +**React app components:** +```bash +# Charts in app +cat policyengine-app/src/pages/policy/output/DistributionalImpact.jsx +``` + +### Don't Use These + +**❌ Wrong colors:** +```python +# Don't use random colors +color = "#FF5733" +color = "red" +color = "green" +``` + +**❌ Wrong fonts:** +```python +# Don't use other fonts for charts +font = dict(family="Arial") +font = dict(family="Times New Roman") +``` + +**❌ Missing logo:** +```python +# Don't skip the logo in charts for publication +# All published charts should include PolicyEngine logo +``` + +## Assets and Resources + +### Logo Files + +**In policyengine-app repository:** +```bash +policyengine-app/src/images/logos/policyengine/ +├── blue.png # Primary logo (light backgrounds) +├── white.png # Logo for dark backgrounds +├── blue.svg # Scalable blue logo +├── white.svg # Scalable white logo +├── banners/ # Banner variations +└── profile/ # Profile/avatar versions +``` + +**Raw URLs for direct use:** +```python +# Use these URLs in code +LOGO_URL = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png" +``` + +### Font Files + +**Roboto (charts):** +- Google Fonts: https://fonts.google.com/specimen/Roboto +- Family: Roboto Serif +- Weights: 300 (light), 400 (regular), 500 (medium), 700 (bold) + +**Loading:** +```html + +``` + +### Color Reference Files + +**JavaScript (React app):** +```bash +cat policyengine-app/src/style/colors.js +``` + +**Python (calculators, analysis):** +```python +# Define in constants.py or at top of file +TEAL_ACCENT = "#39C6C0" +BLUE_PRIMARY = "#2C6496" +DARK_GRAY = "#616161" +WHITE = "#FFFFFF" +``` + +## Brand Evolution + +**Current identity (2025):** +- Teal primary (#39C6C0) +- Blue secondary (#2C6496) +- Roboto Serif for charts +- Minimal, data-focused design + +**If brand evolves:** +- Colors defined in policyengine-app/src/style/colors.js are source of truth +- Update this skill to point to current definitions +- Never hardcode - always reference colors.js + +## Quick Reference + +### Color Codes + +| Color | Hex | Usage | +|-------|-----|-------| +| Teal Accent | #39C6C0 | Primary interactive elements | +| Blue Primary | #2C6496 | Secondary, links, charts | +| Dark Gray | #616161 | Body text | +| White | #FFFFFF | Backgrounds | +| Teal Light | #F7FDFC | Secondary backgrounds | +| Dark Red | #b50d0d | Negative values, errors | + +### Font Families + +| Context | Font | +|---------|------| +| Charts | Roboto Serif | +| Web app | System sans-serif | +| Streamlit | Default sans-serif | +| Code blocks | Monospace | + +### Logo URLs + +| Background | Format | URL | +|------------|--------|-----| +| Light | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.png | +| Light | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/blue.svg | +| Dark | PNG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.png | +| Dark | SVG | https://raw.githubusercontent.com/PolicyEngine/policyengine-app/master/src/images/logos/policyengine/white.svg | + +## Related Skills + +- **policyengine-app-skill** - React component styling +- **policyengine-analysis-skill** - Chart creation patterns +- **policyengine-writing-skill** - Content style (complements visual style) + +## Resources + +**Brand assets:** PolicyEngine/policyengine-app/src/images/ +**Color definitions:** PolicyEngine/policyengine-app/src/style/colors.js +**Examples:** givecalc, salt-amt-calculator, crfb-tob-impacts diff --git a/skills/policyengine-python-client-skill/SKILL.md b/skills/policyengine-python-client-skill/SKILL.md new file mode 100644 index 0000000..0ae7387 --- /dev/null +++ b/skills/policyengine-python-client-skill/SKILL.md @@ -0,0 +1,356 @@ +--- +name: policyengine-python-client +description: Using PolicyEngine programmatically via Python client or REST API +--- + +# PolicyEngine Python Client + +This skill covers programmatic access to PolicyEngine for analysts and researchers. + +## Installation + +```bash +# Install the Python client +pip install policyengine + +# Or for local development +pip install policyengine-us # Just the US model (offline) +``` + +## Quick Start: Python Client + +```python +from policyengine import Simulation + +# Create a household +household = { + "people": { + "you": { + "age": {"2024": 30}, + "employment_income": {"2024": 50000} + } + }, + "households": { + "your household": { + "members": ["you"], + "state_name": {"2024": "CA"} + } + } +} + +# Run simulation +sim = Simulation(situation=household, country_id="us") +income_tax = sim.calculate("income_tax", "2024") +``` + +## For Users: Why Use Python? + +**Web app limitations:** +- ✅ Great for exploring policies interactively +- ❌ Can't analyze many households at once +- ❌ Can't automate repetitive analyses +- ❌ Limited customization of charts + +**Python benefits:** +- ✅ Analyze thousands of households in batch +- ✅ Automate regular policy analysis +- ✅ Create custom visualizations +- ✅ Integrate with other data sources +- ✅ Reproducible research + +## For Analysts: Common Workflows + +### Workflow 1: Calculate Your Own Taxes + +```python +from policyengine import Simulation + +# Your household (more complex than web app) +household = { + "people": { + "you": { + "age": {"2024": 35}, + "employment_income": {"2024": 75000}, + "qualified_dividend_income": {"2024": 5000}, + "charitable_cash_donations": {"2024": 3000} + }, + "spouse": { + "age": {"2024": 33}, + "employment_income": {"2024": 60000} + }, + "child1": {"age": {"2024": 8}}, + "child2": {"age": {"2024": 5}} + }, + # ... entities setup (see policyengine-us-skill) +} + +sim = Simulation(situation=household, country_id="us") + +# Calculate specific values +federal_income_tax = sim.calculate("income_tax", "2024") +state_income_tax = sim.calculate("state_income_tax", "2024") +ctc = sim.calculate("ctc", "2024") +eitc = sim.calculate("eitc", "2024") + +print(f"Federal income tax: ${federal_income_tax:,.0f}") +print(f"State income tax: ${state_income_tax:,.0f}") +print(f"Child Tax Credit: ${ctc:,.0f}") +print(f"EITC: ${eitc:,.0f}") +``` + +### Workflow 2: Analyze a Policy Reform + +```python +from policyengine import Simulation + +# Define reform (increase CTC to $5,000) +reform = { + "gov.irs.credits.ctc.amount.base_amount": { + "2024-01-01.2100-12-31": 5000 + } +} + +# Compare baseline vs reform +household = create_household() # Your household definition + +sim_baseline = Simulation(situation=household, country_id="us") +sim_reform = Simulation(situation=household, country_id="us", reform=reform) + +ctc_baseline = sim_baseline.calculate("ctc", "2024") +ctc_reform = sim_reform.calculate("ctc", "2024") + +print(f"CTC baseline: ${ctc_baseline:,.0f}") +print(f"CTC reform: ${ctc_reform:,.0f}") +print(f"Increase: ${ctc_reform - ctc_baseline:,.0f}") +``` + +### Workflow 3: Batch Analysis + +```python +import pandas as pd +from policyengine import Simulation + +# Analyze multiple households +households = [ + {"income": 30000, "children": 0}, + {"income": 50000, "children": 2}, + {"income": 100000, "children": 3}, +] + +results = [] +for h in households: + situation = create_household(income=h["income"], num_children=h["children"]) + sim = Simulation(situation=situation, country_id="us") + + results.append({ + "income": h["income"], + "children": h["children"], + "income_tax": sim.calculate("income_tax", "2024"), + "ctc": sim.calculate("ctc", "2024"), + "eitc": sim.calculate("eitc", "2024") + }) + +df = pd.DataFrame(results) +print(df) +``` + +## Using the REST API Directly + +### Authentication + +**Public access:** +- 100 requests per minute (unauthenticated) +- No API key needed for basic use + +**Authenticated access:** +- 1,000 requests per minute +- Contact hello@policyengine.org for API key + +### Key Endpoints + +**Calculate household impact:** +```python +import requests + +url = "https://api.policyengine.org/us/calculate" +payload = { + "household": household_dict, + "policy_id": reform_id # or None for baseline +} + +response = requests.post(url, json=payload) +result = response.json() +``` + +**Get policy details:** +```python +# Get policy metadata +response = requests.get("https://api.policyengine.org/us/policy/12345") +policy = response.json() +``` + +**Get parameter values:** +```python +# Get current parameter value +response = requests.get( + "https://api.policyengine.org/us/parameter/gov.irs.credits.ctc.amount.base_amount" +) +parameter = response.json() +``` + +### For Full API Documentation + +**OpenAPI spec:** https://api.policyengine.org/docs + +**To explore:** +```bash +# View all endpoints +curl https://api.policyengine.org/docs + +# Test calculate endpoint +curl -X POST https://api.policyengine.org/us/calculate \ + -H "Content-Type: application/json" \ + -d '{"household": {...}}' +``` + +## Limitations and Considerations + +### Rate Limits + +**Unauthenticated:** +- 100 requests/minute +- Good for exploratory analysis + +**Authenticated:** +- 1,000 requests/minute +- Required for production use + +### Data Privacy + +- PolicyEngine does not store household data +- All calculations happen server-side and are not logged +- Reform URLs are public (don't include personal info in reforms) + +### Performance + +**API calls:** +- Simple household: ~200-500ms +- Population impact: ~5-30 seconds (varies by reform) +- Use caching for repeated calculations + +**Local simulation (policyengine-us):** +- Faster for batch analysis +- No rate limits +- No network dependency +- Limited to one country per package + +## Choosing Local vs API + +### Use Local (policyengine-us package) + +**When:** +- Batch analysis of many households +- Need offline capability +- Analyzing parameter sweeps (axes) +- Development/testing + +**Install:** +```bash +pip install policyengine-us # US only +pip install policyengine-uk # UK only +``` + +**Example:** +```python +from policyengine_us import Simulation + +# Works offline +sim = Simulation(situation=household) +``` + +### Use API (policyengine or requests) + +**When:** +- Multi-country analysis +- Using latest model version +- Don't want to manage dependencies +- Integration with web services + +**Example:** +```python +import requests + +# Requires internet +response = requests.post("https://api.policyengine.org/us/calculate", ...) +``` + +## For Contributors: Understanding the Client + +**Repository:** PolicyEngine/policyengine.py + +**To see implementation:** +```bash +# Clone the client +git clone https://github.com/PolicyEngine/policyengine.py + +# See the Simulation class +cat policyengine/simulation.py + +# See API integration +cat policyengine/api.py +``` + +**Architecture:** +- `Simulation` class wraps API calls +- `calculate()` method handles caching +- Transparent fallback between API and local + +## Advanced: Direct Country Package Usage + +For maximum control and performance, use country packages directly: + +```python +from policyengine_us import Simulation + +# Full control over situation structure +situation = { + # Complete situation dictionary + # See policyengine-us-skill for patterns +} + +sim = Simulation(situation=situation) +result = sim.calculate("variable_name", 2024) +``` + +**Benefits:** +- No API dependency +- Faster (no network) +- Full access to all variables +- Use axes for parameter sweeps + +**See policyengine-us-skill for detailed patterns.** + +## Examples and Tutorials + +**PolicyEngine documentation:** +- US: https://policyengine.org/us/docs +- UK: https://policyengine.org/uk/docs + +**Example notebooks:** +- Repository: PolicyEngine/analysis-notebooks +- See policyengine-analysis-skill for analysis patterns + +**Community examples:** +- Blog posts: policyengine.org/us/research +- GitHub discussions: github.com/PolicyEngine discussions + +## Getting Help + +**For usage questions:** +- GitHub Discussions: https://github.com/PolicyEngine/policyengine-us/discussions + +**For bugs:** +- File issues in appropriate repo (policyengine-us, policyengine.py, etc.) + +**For collaboration:** +- Email: hello@policyengine.org diff --git a/skills/policyengine-uk-skill/SKILL.md b/skills/policyengine-uk-skill/SKILL.md new file mode 100644 index 0000000..1ffd600 --- /dev/null +++ b/skills/policyengine-uk-skill/SKILL.md @@ -0,0 +1,660 @@ +--- +name: policyengine-uk +description: PolicyEngine-UK tax and benefit microsimulation patterns, situation creation, and common workflows +--- + +# PolicyEngine-UK + +PolicyEngine-UK models the UK tax and benefit system, including devolved variations for Scotland and Wales. + +## For Users 👥 + +### What is PolicyEngine-UK? + +PolicyEngine-UK is the "calculator" for UK taxes and benefits. When you use policyengine.org/uk, PolicyEngine-UK runs behind the scenes. + +**What it models:** + +**Direct taxes:** +- Income tax (UK-wide, Scottish, and Welsh variations) +- National Insurance (Classes 1, 2, 4) +- Capital gains tax +- Dividend tax + +**Property and transaction taxes:** +- Council Tax +- Stamp Duty Land Tax (England/NI) +- Land and Buildings Transaction Tax (Scotland) +- Land Transaction Tax (Wales) + +**Universal Credit:** +- Standard allowance +- Child elements +- Housing cost element +- Childcare costs element +- Carer element +- Work capability elements + +**Legacy benefits (being phased out):** +- Working Tax Credit +- Child Tax Credit +- Income Support +- Income-based JSA/ESA +- Housing Benefit + +**Other benefits:** +- Child Benefit +- Pension Credit +- Personal Independence Payment (PIP) +- Disability Living Allowance (DLA) +- Attendance Allowance +- State Pension + +**See full list:** https://policyengine.org/uk/parameters + +### Understanding Variables + +When you see results in PolicyEngine, these are variables: + +**Income variables:** +- `employment_income` - Gross employment earnings/salary +- `self_employment_income` - Self-employment profits +- `pension_income` - Private pension income +- `property_income` - Rental income +- `savings_interest_income` - Interest from savings +- `dividend_income` - Dividend income + +**Tax variables:** +- `income_tax` - Total income tax liability +- `national_insurance` - Total NI contributions +- `council_tax` - Council tax liability + +**Benefit variables:** +- `universal_credit` - Universal Credit amount +- `child_benefit` - Child Benefit amount +- `pension_credit` - Pension Credit amount +- `working_tax_credit` - Working Tax Credit (legacy) +- `child_tax_credit` - Child Tax Credit (legacy) + +**Summary variables:** +- `household_net_income` - Income after taxes and benefits +- `disposable_income` - Income after taxes +- `equivalised_household_net_income` - Adjusted for household size + +## For Analysts 📊 + +### Installation and Setup + +```bash +# Install PolicyEngine-UK +pip install policyengine-uk + +# Or with uv (recommended) +uv pip install policyengine-uk +``` + +### Quick Start + +```python +from policyengine_uk import Simulation + +# Create a household +situation = { + "people": { + "person": { + "age": {2025: 30}, + "employment_income": {2025: 30000} + } + }, + "benunits": { + "benunit": { + "members": ["person"] + } + }, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + } +} + +# Calculate taxes and benefits +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] + +print(f"Income tax: £{income_tax:,.0f}") +print(f"Universal Credit: £{universal_credit:,.0f}") +``` + +### Web App to Python + +**Web app URL:** +``` +policyengine.org/uk/household?household=12345 +``` + +**Equivalent Python (conceptually):** +The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation. + +### When to Use This Skill + +- Creating household situations for tax/benefit calculations +- Running microsimulations with PolicyEngine-UK +- Analyzing policy reforms and their impacts +- Building tools that use PolicyEngine-UK (calculators, analysis notebooks) +- Debugging PolicyEngine-UK calculations + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-uk + +**To see current implementation:** +```bash +git clone https://github.com/PolicyEngine/policyengine-uk +cd policyengine-uk + +# Explore structure +tree policyengine_uk/ +``` + +**Key directories:** +```bash +ls policyengine_uk/ +# - variables/ - Tax and benefit calculations +# - parameters/ - Policy rules (YAML) +# - reforms/ - Pre-defined reforms +# - tests/ - Test cases +``` + +## Core Concepts + +### 1. Situation Dictionary Structure + +PolicyEngine UK requires a nested dictionary defining household composition: + +```python +situation = { + "people": { + "person_id": { + "age": {2025: 35}, + "employment_income": {2025: 30000}, + # ... other person attributes + } + }, + "benunits": { + "benunit_id": { + "members": ["person_id", ...] + } + }, + "households": { + "household_id": { + "members": ["person_id", ...], + "region": {2025: "SOUTH_EAST"} + } + } +} +``` + +**Key Rules:** +- All entities must have consistent member lists +- Use year keys for all values: `{2025: value}` +- Region must be one of the ITL 1 regions (see below) +- All monetary values in pounds (not pence) +- UK tax year runs April 6 to April 5 (but use calendar year in code) + +**Important Entity Difference:** +- UK uses **benunits** (benefit units): a single adult OR couple + dependent children +- This is the assessment unit for most means-tested benefits +- Unlike US which uses families/marital_units/tax_units/spm_units + +### 2. Creating Simulations + +```python +from policyengine_uk import Simulation + +# Create simulation from situation +simulation = Simulation(situation=situation) + +# Calculate variables +income_tax = simulation.calculate("income_tax", 2025) +universal_credit = simulation.calculate("universal_credit", 2025) +household_net_income = simulation.calculate("household_net_income", 2025) +``` + +**Common Variables:** + +**Income:** +- `employment_income` - Gross employment earnings +- `self_employment_income` - Self-employment profits +- `pension_income` - Private pension income +- `property_income` - Rental income +- `savings_interest_income` - Interest income +- `dividend_income` - Dividend income +- `miscellaneous_income` - Other income sources + +**Tax Outputs:** +- `income_tax` - Total income tax liability +- `national_insurance` - Total NI contributions +- `council_tax` - Council tax liability +- `VAT` - Value Added Tax paid + +**Benefits:** +- `universal_credit` - Universal Credit +- `child_benefit` - Child Benefit +- `pension_credit` - Pension Credit +- `working_tax_credit` - Working Tax Credit (legacy) +- `child_tax_credit` - Child Tax Credit (legacy) +- `personal_independence_payment` - PIP +- `attendance_allowance` - Attendance Allowance +- `state_pension` - State Pension + +**Summary:** +- `household_net_income` - Income after taxes and benefits +- `disposable_income` - Income after taxes +- `equivalised_household_net_income` - Adjusted for household size + +### 3. Using Axes for Parameter Sweeps + +To vary a parameter across multiple values: + +```python +situation = { + # ... normal situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 100000, + "period": 2025 + }]] +} + +simulation = Simulation(situation=situation) +# Now calculate() returns arrays of 1001 values +incomes = simulation.calculate("employment_income", 2025) # Array of 1001 values +taxes = simulation.calculate("income_tax", 2025) # Array of 1001 values +``` + +**Important:** Remove axes before creating single-point simulations: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +simulation = Simulation(situation=situation_single) +``` + +### 4. Policy Reforms + +```python +from policyengine_uk import Simulation + +# Define a reform (modifies parameters) +reform = { + "gov.hmrc.income_tax.rates.uk.brackets[0].rate": { + "2025-01-01.2100-12-31": 0.25 # Increase basic rate to 25% + } +} + +# Create simulation with reform +simulation = Simulation(situation=situation, reform=reform) +``` + +## Common Patterns + +### Pattern 1: Single Person Household Calculation + +```python +from policyengine_uk import Simulation + +situation = { + "people": { + "person": { + "age": {2025: 30}, + "employment_income": {2025: 30000} + } + }, + "benunits": { + "benunit": { + "members": ["person"] + } + }, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + } +} + +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2025)[0] +national_insurance = sim.calculate("national_insurance", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] +``` + +### Pattern 2: Couple with Children + +```python +situation = { + "people": { + "parent_1": { + "age": {2025: 35}, + "employment_income": {2025: 35000} + }, + "parent_2": { + "age": {2025: 33}, + "employment_income": {2025: 25000} + }, + "child_1": { + "age": {2025: 8} + }, + "child_2": { + "age": {2025: 5} + } + }, + "benunits": { + "benunit": { + "members": ["parent_1", "parent_2", "child_1", "child_2"] + } + }, + "households": { + "household": { + "members": ["parent_1", "parent_2", "child_1", "child_2"], + "region": {2025: "NORTH_WEST"} + } + } +} + +sim = Simulation(situation=situation) +child_benefit = sim.calculate("child_benefit", 2025)[0] +universal_credit = sim.calculate("universal_credit", 2025)[0] +``` + +### Pattern 3: Marginal Tax Rate Analysis + +```python +# Create baseline with axes varying income +situation_with_axes = { + "people": { + "person": { + "age": {2025: 30} + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "region": {2025: "LONDON"} + } + }, + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 100000, + "period": 2025 + }]] +} + +sim = Simulation(situation=situation_with_axes) +incomes = sim.calculate("employment_income", 2025) +net_incomes = sim.calculate("household_net_income", 2025) + +# Calculate marginal tax rate +import numpy as np +mtr = 1 - (np.gradient(net_incomes) / np.gradient(incomes)) +``` + +### Pattern 4: Regional Comparison + +```python +regions = ["LONDON", "SCOTLAND", "WALES", "NORTH_EAST"] +results = {} + +for region in regions: + situation = create_situation(region=region, income=30000) + sim = Simulation(situation=situation) + results[region] = { + "income_tax": sim.calculate("income_tax", 2025)[0], + "national_insurance": sim.calculate("national_insurance", 2025)[0], + "total_tax": sim.calculate("income_tax", 2025)[0] + + sim.calculate("national_insurance", 2025)[0] + } +``` + +### Pattern 5: Policy Reform Impact + +```python +from policyengine_uk import Microsimulation, Reform + +# Define reform: Increase basic rate to 25% +class IncreaseBasicRate(Reform): + def apply(self): + def modify_parameters(parameters): + parameters.gov.hmrc.income_tax.rates.uk.brackets[0].rate.update( + period="year:2025:10", value=0.25 + ) + return parameters + self.modify_parameters(modify_parameters) + +# Run microsimulation +baseline = Microsimulation() +reformed = Microsimulation(reform=IncreaseBasicRate) + +# Calculate revenue impact +baseline_revenue = baseline.calc("income_tax", 2025).sum() +reformed_revenue = reformed.calc("income_tax", 2025).sum() +revenue_change = (reformed_revenue - baseline_revenue) / 1e9 # in billions + +# Calculate household impact +baseline_net_income = baseline.calc("household_net_income", 2025) +reformed_net_income = reformed.calc("household_net_income", 2025) +``` + +## Helper Scripts + +This skill includes helper scripts in the `scripts/` directory: + +```python +from policyengine_uk_skills.situation_helpers import ( + create_single_person, + create_couple, + create_family_with_children, + add_region +) + +# Quick situation creation +situation = create_single_person( + income=30000, + region="LONDON", + age=30 +) + +# Create couple +situation = create_couple( + income_1=35000, + income_2=25000, + region="SCOTLAND" +) +``` + +## Common Pitfalls and Solutions + +### Pitfall 1: Member Lists Out of Sync + +**Problem:** Different entities have different members +```python +# WRONG +"benunits": {"benunit": {"members": ["parent"]}}, +"households": {"household": {"members": ["parent", "child"]}} +``` + +**Solution:** Keep all entity member lists consistent: +```python +# CORRECT +all_members = ["parent", "child"] +"benunits": {"benunit": {"members": all_members}}, +"households": {"household": {"members": all_members}} +``` + +### Pitfall 2: Forgetting Year Keys + +**Problem:** `"age": 35` instead of `"age": {2025: 35}` + +**Solution:** Always use year dictionary: +```python +"age": {2025: 35}, +"employment_income": {2025: 30000} +``` + +### Pitfall 3: Wrong Region Format + +**Problem:** Using lowercase or incorrect region names + +**Solution:** Use uppercase ITL 1 region codes: +```python +# CORRECT regions: +"region": {2025: "LONDON"} +"region": {2025: "SCOTLAND"} +"region": {2025: "WALES"} +"region": {2025: "NORTH_EAST"} +"region": {2025: "SOUTH_EAST"} +``` + +### Pitfall 4: Axes Persistence + +**Problem:** Axes remain in situation when creating single-point simulation + +**Solution:** Remove axes before single-point simulation: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +``` + +### Pitfall 5: Missing Benunits + +**Problem:** Forgetting to include benunits (benefit units) + +**Solution:** Always include benunits in UK simulations: +```python +# UK requires benunits +situation = { + "people": {...}, + "benunits": {"benunit": {"members": [...]}}, # Required! + "households": {...} +} +``` + +## Regions in PolicyEngine UK + +UK uses ITL 1 (International Territorial Level 1, formerly NUTS 1) regions: + +**Regions:** +- `NORTH_EAST` - North East England +- `NORTH_WEST` - North West England +- `YORKSHIRE` - Yorkshire and the Humber +- `EAST_MIDLANDS` - East Midlands +- `WEST_MIDLANDS` - West Midlands +- `EAST_OF_ENGLAND` - East of England +- `LONDON` - London +- `SOUTH_EAST` - South East England +- `SOUTH_WEST` - South West England +- `WALES` - Wales +- `SCOTLAND` - Scotland +- `NORTHERN_IRELAND` - Northern Ireland + +**Regional Tax Variations:** + +**Scotland:** +- Has devolved income tax with 6 bands (starter 19%, basic 20%, intermediate 21%, higher 42%, advanced 45%, top 47%) +- Scottish residents automatically calculated with Scottish rates + +**Wales:** +- Has Welsh Rate of Income Tax (WRIT) +- Currently maintains parity with England/NI rates + +**England/Northern Ireland:** +- Standard UK rates: basic 20%, higher 40%, additional 45% + +## Key Parameters and Values (2025/26) + +### Income Tax +- **Personal Allowance:** £12,570 +- **Basic rate threshold:** £50,270 +- **Higher rate threshold:** £125,140 +- **Rates:** 20% (basic), 40% (higher), 45% (additional) +- **Personal allowance tapering:** £1 reduction for every £2 over £100,000 + +### National Insurance (Class 1) +- **Lower Earnings Limit:** £6,396/year +- **Primary Threshold:** £12,570/year +- **Upper Earnings Limit:** £50,270/year +- **Rates:** 12% (between primary and upper), 2% (above upper) + +### Universal Credit +- **Standard allowance:** Varies by single/couple and age +- **Taper rate:** 55% (rate at which UC reduced as income increases) +- **Work allowance:** Amount you can earn before UC reduced + +### Child Benefit +- **First child:** Higher rate +- **Subsequent children:** Lower rate +- **High Income Charge:** Tapered withdrawal starting at £60,000 + +## Version Compatibility + +- Use `policyengine-uk>=1.0.0` for 2025 calculations +- Check version: `import policyengine_uk; print(policyengine_uk.__version__)` +- Different years may require different package versions + +## Debugging Tips + +1. **Enable tracing:** + ```python + simulation.trace = True + result = simulation.calculate("variable_name", 2025) + ``` + +2. **Check intermediate calculations:** + ```python + gross_income = simulation.calculate("gross_income", 2025) + disposable_income = simulation.calculate("disposable_income", 2025) + ``` + +3. **Verify situation structure:** + ```python + import json + print(json.dumps(situation, indent=2)) + ``` + +4. **Test with PolicyEngine web app:** + - Go to policyengine.org/uk/household + - Enter same inputs + - Compare results + +## Additional Resources + +- **Documentation:** https://policyengine.org/uk/docs +- **API Reference:** https://github.com/PolicyEngine/policyengine-uk +- **Variable Explorer:** https://policyengine.org/uk/variables +- **Parameter Explorer:** https://policyengine.org/uk/parameters + +## Examples Directory + +See `examples/` for complete working examples: +- `single_person.yaml` - Single person household +- `couple.yaml` - Couple without children +- `family_with_children.yaml` - Family with dependents +- `universal_credit_sweep.yaml` - Analyzing UC with axes + +## Key Differences from US System + +1. **Benefit Units:** UK uses `benunits` (single/couple + children) instead of US multiple entity types +2. **Universal Credit:** Consolidated means-tested benefit (vs separate SNAP, TANF, etc. in US) +3. **National Insurance:** Separate from income tax with own thresholds (vs US Social Security tax) +4. **Devolved Taxes:** Scotland and Wales have different income tax rates +5. **Tax Year:** April 6 to April 5 (vs calendar year in US) +6. **No State Variation:** Council Tax is local, but most taxes/benefits are national (vs 50 US states) diff --git a/skills/policyengine-uk-skill/examples/couple.yaml b/skills/policyengine-uk-skill/examples/couple.yaml new file mode 100644 index 0000000..d2767d4 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/couple.yaml @@ -0,0 +1,29 @@ +# Example: Couple without children in Scotland +# Person 1: £35,000, Age: 35 +# Person 2: £25,000, Age: 33 + +people: + person_1: + age: + 2025: 35 + employment_income: + 2025: 35000 + person_2: + age: + 2025: 33 + employment_income: + 2025: 25000 + +benunits: + benunit: + members: + - person_1 + - person_2 + +households: + household: + members: + - person_1 + - person_2 + region: + 2025: SCOTLAND diff --git a/skills/policyengine-uk-skill/examples/family_with_children.yaml b/skills/policyengine-uk-skill/examples/family_with_children.yaml new file mode 100644 index 0000000..be02ac8 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/family_with_children.yaml @@ -0,0 +1,41 @@ +# Example: Family with children in Wales +# Parent 1: £35,000, Age: 35 +# Parent 2: £25,000, Age: 33 +# Child 1: Age 8 +# Child 2: Age 5 + +people: + parent_1: + age: + 2025: 35 + employment_income: + 2025: 35000 + parent_2: + age: + 2025: 33 + employment_income: + 2025: 25000 + child_1: + age: + 2025: 8 + child_2: + age: + 2025: 5 + +benunits: + benunit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +households: + household: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + region: + 2025: WALES diff --git a/skills/policyengine-uk-skill/examples/single_person.yaml b/skills/policyengine-uk-skill/examples/single_person.yaml new file mode 100644 index 0000000..33cd1e0 --- /dev/null +++ b/skills/policyengine-uk-skill/examples/single_person.yaml @@ -0,0 +1,21 @@ +# Example: Single person household in London +# Income: £30,000, Age: 30 + +people: + person: + age: + 2025: 30 + employment_income: + 2025: 30000 + +benunits: + benunit: + members: + - person + +households: + household: + members: + - person + region: + 2025: LONDON diff --git a/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml b/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml new file mode 100644 index 0000000..b57492d --- /dev/null +++ b/skills/policyengine-uk-skill/examples/universal_credit_sweep.yaml @@ -0,0 +1,38 @@ +# Example: Analyzing Universal Credit with income variation +# Single parent with 2 children in North West +# Sweeps employment income from £0 to £50,000 + +people: + parent: + age: + 2025: 30 + child_1: + age: + 2025: 8 + child_2: + age: + 2025: 5 + +benunits: + benunit: + members: + - parent + - child_1 + - child_2 + +households: + household: + members: + - parent + - child_1 + - child_2 + region: + 2025: NORTH_WEST + +# Axes: Vary employment income from £0 to £50,000 +axes: + - - name: employment_income + count: 1001 + min: 0 + max: 50000 + period: 2025 diff --git a/skills/policyengine-uk-skill/scripts/situation_helpers.py b/skills/policyengine-uk-skill/scripts/situation_helpers.py new file mode 100644 index 0000000..4c1aadc --- /dev/null +++ b/skills/policyengine-uk-skill/scripts/situation_helpers.py @@ -0,0 +1,339 @@ +""" +Helper functions for creating PolicyEngine-UK situations. + +These utilities simplify the creation of situation dictionaries +for common household configurations. +""" + +CURRENT_YEAR = 2025 + +# UK ITL 1 regions +VALID_REGIONS = [ + "NORTH_EAST", + "NORTH_WEST", + "YORKSHIRE", + "EAST_MIDLANDS", + "WEST_MIDLANDS", + "EAST_OF_ENGLAND", + "LONDON", + "SOUTH_EAST", + "SOUTH_WEST", + "WALES", + "SCOTLAND", + "NORTHERN_IRELAND" +] + + +def create_single_person(income, region="LONDON", age=30, **kwargs): + """ + Create a situation for a single person household. + + Args: + income (float): Employment income + region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND") + age (int): Person's age + **kwargs: Additional person attributes (e.g., self_employment_income) + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + person_attrs = { + "age": {CURRENT_YEAR: age}, + "employment_income": {CURRENT_YEAR: income}, + } + person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": {"person": person_attrs}, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "region": {CURRENT_YEAR: region} + } + } + } + + +def create_couple( + income_1, income_2=0, region="LONDON", age_1=35, age_2=35, **kwargs +): + """ + Create a situation for a couple without children. + + Args: + income_1 (float): First person's employment income + income_2 (float): Second person's employment income + region (str): ITL 1 region + age_1 (int): First person's age + age_2 (int): Second person's age + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + members = ["person_1", "person_2"] + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": { + "person_1": { + "age": {CURRENT_YEAR: age_1}, + "employment_income": {CURRENT_YEAR: income_1} + }, + "person_2": { + "age": {CURRENT_YEAR: age_2}, + "employment_income": {CURRENT_YEAR: income_2} + } + }, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def create_family_with_children( + parent_income, + num_children=1, + child_ages=None, + region="LONDON", + parent_age=35, + couple=False, + partner_income=0, + **kwargs +): + """ + Create a situation for a family with children. + + Args: + parent_income (float): Primary parent's employment income + num_children (int): Number of children + child_ages (list): List of child ages (defaults to [5, 8, 12, ...]) + region (str): ITL 1 region + parent_age (int): Parent's age + couple (bool): Whether this is a couple household + partner_income (float): Partner's income if couple + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + if child_ages is None: + child_ages = [5 + i * 3 for i in range(num_children)] + elif len(child_ages) != num_children: + raise ValueError("Length of child_ages must match num_children") + + people = { + "parent": { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: parent_income} + } + } + + members = ["parent"] + + if couple: + people["partner"] = { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: partner_income} + } + members.append("partner") + + for i, age in enumerate(child_ages): + child_id = f"child_{i+1}" + people[child_id] = {"age": {CURRENT_YEAR: age}} + members.append(child_id) + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def add_income_sources( + situation, + person_id=None, + self_employment_income=0, + pension_income=0, + property_income=0, + savings_interest_income=0, + dividend_income=0, + miscellaneous_income=0 +): + """ + Add additional income sources to a person in an existing situation. + + Args: + situation (dict): Existing PolicyEngine situation + person_id (str): Person ID to add income to (defaults to first person) + self_employment_income (float): Self-employment income + pension_income (float): Private pension income + property_income (float): Rental income + savings_interest_income (float): Interest income + dividend_income (float): Dividend income + miscellaneous_income (float): Other income + + Returns: + dict: Updated situation with additional income + """ + # Get person ID + if person_id is None: + person_id = list(situation["people"].keys())[0] + + # Add income sources + if self_employment_income > 0: + situation["people"][person_id]["self_employment_income"] = { + CURRENT_YEAR: self_employment_income + } + + if pension_income > 0: + situation["people"][person_id]["pension_income"] = { + CURRENT_YEAR: pension_income + } + + if property_income > 0: + situation["people"][person_id]["property_income"] = { + CURRENT_YEAR: property_income + } + + if savings_interest_income > 0: + situation["people"][person_id]["savings_interest_income"] = { + CURRENT_YEAR: savings_interest_income + } + + if dividend_income > 0: + situation["people"][person_id]["dividend_income"] = { + CURRENT_YEAR: dividend_income + } + + if miscellaneous_income > 0: + situation["people"][person_id]["miscellaneous_income"] = { + CURRENT_YEAR: miscellaneous_income + } + + return situation + + +def add_axes(situation, variable_name, min_val, max_val, count=1001): + """ + Add axes to a situation for parameter sweeps. + + Args: + situation (dict): Existing PolicyEngine situation + variable_name (str): Variable to vary (e.g., "employment_income") + min_val (float): Minimum value + max_val (float): Maximum value + count (int): Number of points (default: 1001) + + Returns: + dict: Updated situation with axes + """ + situation["axes"] = [[{ + "name": variable_name, + "count": count, + "min": min_val, + "max": max_val, + "period": CURRENT_YEAR + }]] + + return situation + + +def set_region(situation, region): + """ + Set or change the region for a household. + + Args: + situation (dict): Existing PolicyEngine situation + region (str): ITL 1 region (e.g., "LONDON", "SCOTLAND") + + Returns: + dict: Updated situation + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + household_id = list(situation["households"].keys())[0] + situation["households"][household_id]["region"] = {CURRENT_YEAR: region} + + return situation + + +def create_pensioner_household( + pension_income, + state_pension_income=0, + region="LONDON", + age=70, + couple=False, + partner_pension_income=0, + partner_age=68, + **kwargs +): + """ + Create a situation for a pensioner household. + + Args: + pension_income (float): Private pension income + state_pension_income (float): State pension income + region (str): ITL 1 region + age (int): Pensioner's age + couple (bool): Whether this is a couple household + partner_pension_income (float): Partner's pension income if couple + partner_age (int): Partner's age if couple + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if region not in VALID_REGIONS: + raise ValueError(f"Invalid region. Must be one of: {', '.join(VALID_REGIONS)}") + + people = { + "pensioner": { + "age": {CURRENT_YEAR: age}, + "pension_income": {CURRENT_YEAR: pension_income}, + "state_pension": {CURRENT_YEAR: state_pension_income} + } + } + + members = ["pensioner"] + + if couple: + people["partner"] = { + "age": {CURRENT_YEAR: partner_age}, + "pension_income": {CURRENT_YEAR: partner_pension_income}, + "state_pension": {CURRENT_YEAR: 0} + } + members.append("partner") + + household_attrs = { + "members": members, + "region": {CURRENT_YEAR: region} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "benunits": {"benunit": {"members": members}}, + "households": {"household": household_attrs} + } diff --git a/skills/policyengine-us-skill/SKILL.md b/skills/policyengine-us-skill/SKILL.md new file mode 100644 index 0000000..c8f9359 --- /dev/null +++ b/skills/policyengine-us-skill/SKILL.md @@ -0,0 +1,524 @@ +--- +name: policyengine-us +description: PolicyEngine-US tax and benefit microsimulation patterns, situation creation, and common workflows +--- + +# PolicyEngine-US + +PolicyEngine-US models the US federal and state tax and benefit system. + +## For Users 👥 + +### What is PolicyEngine-US? + +PolicyEngine-US is the "calculator" for US taxes and benefits. When you use policyengine.org/us, PolicyEngine-US runs behind the scenes. + +**What it models:** + +**Federal taxes:** +- Income tax (with standard/itemized deductions) +- Payroll tax (Social Security, Medicare) +- Capital gains tax + +**Federal benefits:** +- Earned Income Tax Credit (EITC) +- Child Tax Credit (CTC) +- SNAP (food stamps) +- WIC, ACA premium tax credits +- Social Security, SSI, TANF + +**State programs (varies by state):** +- State income tax (all 50 states + DC) +- State EITC, CTC +- State-specific benefits + +**See full list:** https://policyengine.org/us/parameters + +### Understanding Variables + +When you see results in PolicyEngine, these are variables: + +**Income variables:** +- `employment_income` - W-2 wages +- `self_employment_income` - 1099 income +- `qualified_dividend_income` - Dividends +- `capital_gains` - Capital gains + +**Tax variables:** +- `income_tax` - Federal income tax +- `state_income_tax` - State income tax +- `payroll_tax` - FICA taxes + +**Benefit variables:** +- `eitc` - Earned Income Tax Credit +- `ctc` - Child Tax Credit +- `snap` - SNAP benefits + +**Summary variables:** +- `household_net_income` - Income after taxes and benefits +- `household_tax` - Total taxes +- `household_benefits` - Total benefits + +## For Analysts 📊 + +### Installation and Setup + +```bash +# Install PolicyEngine-US +pip install policyengine-us + +# Or with uv (recommended) +uv pip install policyengine-us +``` + +### Quick Start + +```python +from policyengine_us import Simulation + +# Create a household +situation = { + "people": { + "you": { + "age": {2024: 30}, + "employment_income": {2024: 50000} + } + }, + "families": {"family": {"members": ["you"]}}, + "marital_units": {"marital_unit": {"members": ["you"]}}, + "tax_units": {"tax_unit": {"members": ["you"]}}, + "spm_units": {"spm_unit": {"members": ["you"]}}, + "households": { + "household": { + "members": ["you"], + "state_name": {2024: "CA"} + } + } +} + +# Calculate taxes and benefits +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2024)[0] +eitc = sim.calculate("eitc", 2024)[0] + +print(f"Income tax: ${income_tax:,.0f}") +print(f"EITC: ${eitc:,.0f}") +``` + +### Web App to Python + +**Web app URL:** +``` +policyengine.org/us/household?household=12345 +``` + +**Equivalent Python (conceptually):** +The household ID represents a situation dictionary. To replicate in Python, you'd create a similar situation. + +### When to Use This Skill + +- Creating household situations for tax/benefit calculations +- Running microsimulations with PolicyEngine-US +- Analyzing policy reforms and their impacts +- Building tools that use PolicyEngine-US (calculators, analysis notebooks) +- Debugging PolicyEngine-US calculations + +## For Contributors 💻 + +### Repository + +**Location:** PolicyEngine/policyengine-us + +**To see current implementation:** +```bash +git clone https://github.com/PolicyEngine/policyengine-us +cd policyengine-us + +# Explore structure +tree policyengine_us/ +``` + +**Key directories:** +```bash +ls policyengine_us/ +# - variables/ - Tax and benefit calculations +# - parameters/ - Policy rules (YAML) +# - reforms/ - Pre-defined reforms +# - tests/ - Test cases +``` + +## Core Concepts + +### 1. Situation Dictionary Structure + +PolicyEngine requires a nested dictionary defining household composition and characteristics: + +```python +situation = { + "people": { + "person_id": { + "age": {2024: 35}, + "employment_income": {2024: 50000}, + # ... other person attributes + } + }, + "families": { + "family_id": {"members": ["person_id", ...]} + }, + "marital_units": { + "marital_unit_id": {"members": ["person_id", ...]} + }, + "tax_units": { + "tax_unit_id": {"members": ["person_id", ...]} + }, + "spm_units": { + "spm_unit_id": {"members": ["person_id", ...]} + }, + "households": { + "household_id": { + "members": ["person_id", ...], + "state_name": {2024: "CA"} + } + } +} +``` + +**Key Rules:** +- All entities must have consistent member lists +- Use year keys for all values: `{2024: value}` +- State must be two-letter code (e.g., "CA", "NY", "TX") +- All monetary values in dollars (not cents) + +### 2. Creating Simulations + +```python +from policyengine_us import Simulation + +# Create simulation from situation +simulation = Simulation(situation=situation) + +# Calculate variables +income_tax = simulation.calculate("income_tax", 2024) +eitc = simulation.calculate("eitc", 2024) +household_net_income = simulation.calculate("household_net_income", 2024) +``` + +**Common Variables:** + +**Income:** +- `employment_income` - W-2 wages +- `self_employment_income` - 1099/business income +- `qualified_dividend_income` - Qualified dividends +- `capital_gains` - Capital gains +- `interest_income` - Interest income +- `social_security` - Social Security benefits +- `pension_income` - Pension/retirement income + +**Deductions:** +- `charitable_cash_donations` - Cash charitable giving +- `real_estate_taxes` - State and local property taxes +- `mortgage_interest` - Mortgage interest deduction +- `medical_expense` - Medical and dental expenses +- `casualty_loss` - Casualty and theft losses + +**Tax Outputs:** +- `income_tax` - Total federal income tax +- `payroll_tax` - FICA taxes +- `state_income_tax` - State income tax +- `household_tax` - Total taxes (federal + state + local) + +**Benefits:** +- `eitc` - Earned Income Tax Credit +- `ctc` - Child Tax Credit +- `snap` - SNAP benefits +- `household_benefits` - Total benefits + +**Summary:** +- `household_net_income` - Income minus taxes plus benefits + +### 3. Using Axes for Parameter Sweeps + +To vary a parameter across multiple values: + +```python +situation = { + # ... normal situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 200000, + "period": 2024 + }]] +} + +simulation = Simulation(situation=situation) +# Now calculate() returns arrays of 1001 values +incomes = simulation.calculate("employment_income", 2024) # Array of 1001 values +taxes = simulation.calculate("income_tax", 2024) # Array of 1001 values +``` + +**Important:** Remove axes before creating single-point simulations: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +simulation = Simulation(situation=situation_single) +``` + +### 4. Policy Reforms + +```python +from policyengine_us import Simulation + +# Define a reform (modifies parameters) +reform = { + "gov.irs.credits.ctc.amount.base_amount": { + "2024-01-01.2100-12-31": 5000 # Increase CTC to $5000 + } +} + +# Create simulation with reform +simulation = Simulation(situation=situation, reform=reform) +``` + +## Common Patterns + +### Pattern 1: Single Household Calculation + +```python +from policyengine_us import Simulation + +situation = { + "people": { + "parent": { + "age": {2024: 35}, + "employment_income": {2024: 60000} + }, + "child": { + "age": {2024: 5} + } + }, + "families": {"family": {"members": ["parent", "child"]}}, + "marital_units": {"marital_unit": {"members": ["parent"]}}, + "tax_units": {"tax_unit": {"members": ["parent", "child"]}}, + "spm_units": {"spm_unit": {"members": ["parent", "child"]}}, + "households": { + "household": { + "members": ["parent", "child"], + "state_name": {2024: "NY"} + } + } +} + +sim = Simulation(situation=situation) +income_tax = sim.calculate("income_tax", 2024)[0] +ctc = sim.calculate("ctc", 2024)[0] +``` + +### Pattern 2: Marginal Tax Rate Analysis + +```python +# Create baseline with axes varying income +situation_with_axes = { + # ... situation setup ... + "axes": [[{ + "name": "employment_income", + "count": 1001, + "min": 0, + "max": 200000, + "period": 2024 + }]] +} + +sim = Simulation(situation=situation_with_axes) +incomes = sim.calculate("employment_income", 2024) +taxes = sim.calculate("income_tax", 2024) + +# Calculate marginal tax rate +import numpy as np +mtr = np.gradient(taxes) / np.gradient(incomes) +``` + +### Pattern 3: Charitable Donation Impact + +```python +# Baseline (no donation) +situation_baseline = create_situation(income=100000, donation=0) +sim_baseline = Simulation(situation=situation_baseline) +tax_baseline = sim_baseline.calculate("income_tax", 2024)[0] + +# With donation +situation_donation = create_situation(income=100000, donation=5000) +sim_donation = Simulation(situation=situation_donation) +tax_donation = sim_donation.calculate("income_tax", 2024)[0] + +# Tax savings from donation +tax_savings = tax_baseline - tax_donation +effective_discount = tax_savings / 5000 # e.g., 0.24 = 24% discount +``` + +### Pattern 4: State Comparison + +```python +states = ["CA", "NY", "TX", "FL"] +results = {} + +for state in states: + situation = create_situation(state=state, income=75000) + sim = Simulation(situation=situation) + results[state] = { + "state_income_tax": sim.calculate("state_income_tax", 2024)[0], + "total_tax": sim.calculate("household_tax", 2024)[0] + } +``` + +## Helper Scripts + +This skill includes helper scripts in the `scripts/` directory: + +```python +from policyengine_skills.situation_helpers import ( + create_single_filer, + create_married_couple, + create_family_with_children, + add_itemized_deductions +) + +# Quick situation creation +situation = create_single_filer( + income=50000, + state="CA", + age=30 +) + +# Add deductions +situation = add_itemized_deductions( + situation, + charitable_donations=5000, + mortgage_interest=10000, + real_estate_taxes=8000 +) +``` + +## Common Pitfalls and Solutions + +### Pitfall 1: Member Lists Out of Sync +**Problem:** Different entities have different members +```python +# WRONG +"tax_units": {"tax_unit": {"members": ["parent"]}}, +"households": {"household": {"members": ["parent", "child"]}} +``` + +**Solution:** Keep all entity member lists consistent: +```python +# CORRECT +all_members = ["parent", "child"] +"families": {"family": {"members": all_members}}, +"tax_units": {"tax_unit": {"members": all_members}}, +"households": {"household": {"members": all_members}} +``` + +### Pitfall 2: Forgetting Year Keys +**Problem:** `"age": 35` instead of `"age": {2024: 35}` + +**Solution:** Always use year dictionary: +```python +"age": {2024: 35}, +"employment_income": {2024: 50000} +``` + +### Pitfall 3: Net Taxes vs Gross Taxes +**Problem:** Forgetting to subtract benefits from taxes + +**Solution:** Use proper calculation: +```python +# Net taxes (what household actually pays) +net_tax = sim.calculate("household_tax", 2024) - \ + sim.calculate("household_benefits", 2024) +``` + +### Pitfall 4: Axes Persistence +**Problem:** Axes remain in situation when creating single-point simulation + +**Solution:** Remove axes before single-point simulation: +```python +situation_single = situation.copy() +situation_single.pop("axes", None) +``` + +### Pitfall 5: State-Specific Variables +**Problem:** Using NYC-specific variables without `in_nyc: True` + +**Solution:** Set NYC flag for NY residents in NYC: +```python +"households": { + "household": { + "state_name": {2024: "NY"}, + "in_nyc": {2024: True} # Required for NYC taxes + } +} +``` + +## NYC Handling + +For New York City residents: +```python +situation = { + # ... people setup ... + "households": { + "household": { + "members": ["person"], + "state_name": {2024: "NY"}, + "in_nyc": {2024: True} # Enable NYC tax calculations + } + } +} +``` + +## Version Compatibility + +- Always use `policyengine-us>=1.155.0` for 2024 calculations +- Check version: `import policyengine_us; print(policyengine_us.__version__)` +- Different years may require different package versions + +## Debugging Tips + +1. **Enable tracing:** + ```python + simulation.trace = True + result = simulation.calculate("variable_name", 2024) + ``` + +2. **Check intermediate calculations:** + ```python + agi = simulation.calculate("adjusted_gross_income", 2024) + taxable_income = simulation.calculate("taxable_income", 2024) + ``` + +3. **Verify situation structure:** + ```python + import json + print(json.dumps(situation, indent=2)) + ``` + +4. **Test with PolicyEngine web app:** + - Go to policyengine.org/us/household + - Enter same inputs + - Compare results + +## Additional Resources + +- **Documentation:** https://policyengine.org/us/docs +- **API Reference:** https://github.com/PolicyEngine/policyengine-us +- **Example Notebooks:** https://github.com/PolicyEngine/analysis-notebooks +- **Variable Explorer:** https://policyengine.org/us/variables + +## Examples Directory + +See `examples/` for complete working examples: +- `single_filer.yaml` - Single person household +- `married_couple.yaml` - Married filing jointly +- `family_with_children.yaml` - Family with dependents +- `itemized_deductions.yaml` - Using itemized deductions +- `donation_sweep.yaml` - Analyzing donation impacts with axes diff --git a/skills/policyengine-us-skill/examples/donation_sweep.yaml b/skills/policyengine-us-skill/examples/donation_sweep.yaml new file mode 100644 index 0000000..6829690 --- /dev/null +++ b/skills/policyengine-us-skill/examples/donation_sweep.yaml @@ -0,0 +1,71 @@ +# Example: Analyzing charitable donation impacts using axes +# Married couple with 2 children in New York +# Sweeps charitable donations from $0 to $50,000 + +people: + parent_1: + age: + 2024: 35 + employment_income: + 2024: 100000 + parent_2: + age: + 2024: 35 + employment_income: + 2024: 50000 + child_1: + age: + 2024: 8 + child_2: + age: + 2024: 5 + +families: + family: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +marital_units: + marital_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +tax_units: + tax_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +spm_units: + spm_unit: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + +households: + household: + members: + - parent_1 + - parent_2 + - child_1 + - child_2 + state_name: + 2024: NY + +# Axes: Vary charitable donations from $0 to $50,000 +axes: + - - name: charitable_cash_donations + count: 1001 + min: 0 + max: 50000 + period: 2024 diff --git a/skills/policyengine-us-skill/examples/single_filer.yaml b/skills/policyengine-us-skill/examples/single_filer.yaml new file mode 100644 index 0000000..c51e04c --- /dev/null +++ b/skills/policyengine-us-skill/examples/single_filer.yaml @@ -0,0 +1,38 @@ +# Example: Single tax filer in California +# Income: $60,000, Age: 30, with charitable donations + +people: + person: + age: + 2024: 30 + employment_income: + 2024: 60000 + charitable_cash_donations: + 2024: 5000 + +families: + family: + members: + - person + +marital_units: + marital_unit: + members: + - person + +tax_units: + tax_unit: + members: + - person + +spm_units: + spm_unit: + members: + - person + +households: + household: + members: + - person + state_name: + 2024: CA diff --git a/skills/policyengine-us-skill/scripts/situation_helpers.py b/skills/policyengine-us-skill/scripts/situation_helpers.py new file mode 100644 index 0000000..1bac138 --- /dev/null +++ b/skills/policyengine-us-skill/scripts/situation_helpers.py @@ -0,0 +1,257 @@ +""" +Helper functions for creating PolicyEngine-US situations. + +These utilities simplify the creation of situation dictionaries +for common household configurations. +""" + +CURRENT_YEAR = 2024 + + +def create_single_filer(income, state="CA", age=35, **kwargs): + """ + Create a situation for a single tax filer. + + Args: + income (float): Employment income + state (str): Two-letter state code (e.g., "CA", "NY") + age (int): Person's age + **kwargs: Additional person attributes (e.g., self_employment_income) + + Returns: + dict: PolicyEngine situation dictionary + """ + person_attrs = { + "age": {CURRENT_YEAR: age}, + "employment_income": {CURRENT_YEAR: income}, + } + person_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": {"person": person_attrs}, + "families": {"family": {"members": ["person"]}}, + "marital_units": {"marital_unit": {"members": ["person"]}}, + "tax_units": {"tax_unit": {"members": ["person"]}}, + "spm_units": {"spm_unit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "state_name": {CURRENT_YEAR: state} + } + } + } + + +def create_married_couple( + income_1, income_2=0, state="CA", age_1=35, age_2=35, **kwargs +): + """ + Create a situation for a married couple filing jointly. + + Args: + income_1 (float): First spouse's employment income + income_2 (float): Second spouse's employment income + state (str): Two-letter state code + age_1 (int): First spouse's age + age_2 (int): Second spouse's age + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + members = ["spouse_1", "spouse_2"] + + household_attrs = { + "members": members, + "state_name": {CURRENT_YEAR: state} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": { + "spouse_1": { + "age": {CURRENT_YEAR: age_1}, + "employment_income": {CURRENT_YEAR: income_1} + }, + "spouse_2": { + "age": {CURRENT_YEAR: age_2}, + "employment_income": {CURRENT_YEAR: income_2} + } + }, + "families": {"family": {"members": members}}, + "marital_units": {"marital_unit": {"members": members}}, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def create_family_with_children( + parent_income, + num_children=1, + child_ages=None, + state="CA", + parent_age=35, + married=False, + spouse_income=0, + **kwargs +): + """ + Create a situation for a family with children. + + Args: + parent_income (float): Primary parent's employment income + num_children (int): Number of children + child_ages (list): List of child ages (defaults to [5, 8, 12, ...]) + state (str): Two-letter state code + parent_age (int): Parent's age + married (bool): Whether parents are married + spouse_income (float): Spouse's income if married + **kwargs: Additional household attributes + + Returns: + dict: PolicyEngine situation dictionary + """ + if child_ages is None: + child_ages = [5 + i * 3 for i in range(num_children)] + elif len(child_ages) != num_children: + raise ValueError("Length of child_ages must match num_children") + + people = { + "parent": { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: parent_income} + } + } + + members = ["parent"] + + if married: + people["spouse"] = { + "age": {CURRENT_YEAR: parent_age}, + "employment_income": {CURRENT_YEAR: spouse_income} + } + members.append("spouse") + + for i, age in enumerate(child_ages): + child_id = f"child_{i+1}" + people[child_id] = {"age": {CURRENT_YEAR: age}} + members.append(child_id) + + household_attrs = { + "members": members, + "state_name": {CURRENT_YEAR: state} + } + household_attrs.update({k: {CURRENT_YEAR: v} for k, v in kwargs.items()}) + + return { + "people": people, + "families": {"family": {"members": members}}, + "marital_units": { + "marital_unit": { + "members": members if married else ["parent"] + } + }, + "tax_units": {"tax_unit": {"members": members}}, + "spm_units": {"spm_unit": {"members": members}}, + "households": {"household": household_attrs} + } + + +def add_itemized_deductions( + situation, + charitable_donations=0, + mortgage_interest=0, + real_estate_taxes=0, + medical_expenses=0, + casualty_losses=0 +): + """ + Add itemized deductions to an existing situation. + + Adds deductions to the first person in the situation. + + Args: + situation (dict): Existing PolicyEngine situation + charitable_donations (float): Cash charitable contributions + mortgage_interest (float): Mortgage interest paid + real_estate_taxes (float): State and local property taxes + medical_expenses (float): Medical and dental expenses + casualty_losses (float): Casualty and theft losses + + Returns: + dict: Updated situation with deductions + """ + # Get first person ID + first_person = list(situation["people"].keys())[0] + + # Add deductions + if charitable_donations > 0: + situation["people"][first_person]["charitable_cash_donations"] = { + CURRENT_YEAR: charitable_donations + } + + if mortgage_interest > 0: + situation["people"][first_person]["mortgage_interest"] = { + CURRENT_YEAR: mortgage_interest + } + + if real_estate_taxes > 0: + situation["people"][first_person]["real_estate_taxes"] = { + CURRENT_YEAR: real_estate_taxes + } + + if medical_expenses > 0: + situation["people"][first_person]["medical_expense"] = { + CURRENT_YEAR: medical_expenses + } + + if casualty_losses > 0: + situation["people"][first_person]["casualty_loss"] = { + CURRENT_YEAR: casualty_losses + } + + return situation + + +def add_axes(situation, variable_name, min_val, max_val, count=1001): + """ + Add axes to a situation for parameter sweeps. + + Args: + situation (dict): Existing PolicyEngine situation + variable_name (str): Variable to vary (e.g., "employment_income") + min_val (float): Minimum value + max_val (float): Maximum value + count (int): Number of points (default: 1001) + + Returns: + dict: Updated situation with axes + """ + situation["axes"] = [[{ + "name": variable_name, + "count": count, + "min": min_val, + "max": max_val, + "period": CURRENT_YEAR + }]] + + return situation + + +def set_state_nyc(situation, in_nyc=True): + """ + Set state to NY and configure NYC residence. + + Args: + situation (dict): Existing PolicyEngine situation + in_nyc (bool): Whether household is in NYC + + Returns: + dict: Updated situation + """ + household_id = list(situation["households"].keys())[0] + situation["households"][household_id]["state_name"] = {CURRENT_YEAR: "NY"} + situation["households"][household_id]["in_nyc"] = {CURRENT_YEAR: in_nyc} + + return situation diff --git a/skills/policyengine-user-guide-skill/SKILL.md b/skills/policyengine-user-guide-skill/SKILL.md new file mode 100644 index 0000000..1e7354e --- /dev/null +++ b/skills/policyengine-user-guide-skill/SKILL.md @@ -0,0 +1,295 @@ +--- +name: policyengine-user-guide +description: Using PolicyEngine web apps to analyze tax and benefit policy impacts - for users of policyengine.org +--- + +# PolicyEngine User Guide + +This skill helps you use PolicyEngine to analyze how tax and benefit policies affect households and populations. + +## For Users: Getting Started + +### What is PolicyEngine? + +PolicyEngine computes the impact of public policy on households and society. You can: +- Calculate how policies affect your household +- Analyze population-wide impacts of reforms +- Create and share custom policy proposals +- Compare different policy options + +### Web App: policyengine.org + +**Main features:** +1. **Your household** - Calculate your taxes and benefits +2. **Policy** - Design custom reforms and see impacts +3. **Research** - Read policy analysis and blog posts + +### Available Countries + +- **United States** - policyengine.org/us +- **United Kingdom** - policyengine.org/uk +- **Canada** - policyengine.org/ca (beta) + +## Using the Household Calculator + +### Step 1: Navigate to Household Page + +**US:** https://policyengine.org/us/household +**UK:** https://policyengine.org/uk/household + +### Step 2: Enter Your Information + +**Income:** +- Employment income (W-2 wages) +- Self-employment income +- Capital gains and dividends +- Social Security, pensions, etc. + +**Household composition:** +- Adults and dependents +- Ages +- Marital status + +**Location:** +- State (US) or region (UK) +- NYC checkbox for New York City residents + +**Deductions (US):** +- Charitable donations +- Mortgage interest +- State and local taxes (SALT) +- Medical expenses + +### Step 3: View Results + +**Net income** - Your income after taxes and benefits + +**Breakdown:** +- Total taxes (federal + state + local) +- Total benefits (EITC, CTC, SNAP, etc.) +- Effective tax rate +- Marginal tax rate + +**Charts:** +- Net income by earnings +- Marginal tax rate by earnings + +## Creating a Policy Reform + +### Step 1: Navigate to Policy Page + +**US:** https://policyengine.org/us/policy +**UK:** https://policyengine.org/uk/policy + +### Step 2: Select Parameters to Change + +**Browse parameters by:** +- Government department (IRS, SSA, etc.) +- Program (EITC, CTC, SNAP) +- Type (tax rates, benefit amounts, thresholds) + +**Example: Increase Child Tax Credit** +1. Navigate to gov.irs.credits.ctc.amount.base_amount +2. Change from $2,000 to $5,000 +3. Click "Calculate economic impact" + +### Step 3: View Population Impacts + +**Budgetary impact:** +- Total cost or revenue raised +- Breakdown by program + +**Poverty impact:** +- Change in poverty rates +- By age group (children, adults, seniors) +- Deep poverty (income < 50% of threshold) + +**Distributional impact:** +- Average impact by income decile +- Winners and losers by decile +- Relative vs absolute changes + +**Inequality impact:** +- Gini index change +- Top 10% and top 1% income share + +### Step 4: Share Your Reform + +**Share URL:** +Every reform has a unique URL you can share: +``` +policyengine.org/us/policy?reform=12345®ion=enhanced_us&timePeriod=2025 +``` + +**Parameters in URL:** +- `reform=12345` - Your custom reform ID +- `region=enhanced_us` - Geography (US, state, or congressional district) +- `timePeriod=2025` - Year of analysis + +## Understanding Results + +### Metrics Explained + +**Supplemental Poverty Measure (SPM):** +- Accounts for taxes, benefits, and living costs +- US Census Bureau's official alternative poverty measure +- More comprehensive than Official Poverty Measure + +**Gini coefficient:** +- Measures income inequality (0 = perfect equality, 1 = perfect inequality) +- US Gini is typically around 0.48 +- Lower values = more equal income distribution + +**Income deciles:** +- Population divided into 10 equal groups by income +- Decile 1 = bottom 10% of earners +- Decile 10 = top 10% of earners + +**Winners and losers:** +- Winners: Net income increases by 5% or more +- Losers: Net income decreases by 5% or more +- Neutral: Net income change less than 5% + +### Reading Charts + +**Household impact charts:** +- X-axis: Usually income or earnings +- Y-axis: Net income, taxes, or benefits +- Hover to see exact values + +**Population impact charts:** +- Bar charts: Compare across groups (deciles, states) +- Line charts: Show relationships (income vs impact) +- Waterfall charts: Show components of budgetary impact + +## Common Use Cases + +### Use Case 1: How Does Policy X Affect My Household? + +1. Go to household calculator +2. Enter your information +3. Select "Reform" and choose the policy +4. Compare baseline vs reform results + +### Use Case 2: How Much Would Policy X Cost? + +1. Go to policy page +2. Create or select the reform +3. View "Budgetary impact" section +4. See total cost and breakdown + +### Use Case 3: Would Policy X Reduce Poverty? + +1. Go to policy page +2. Create or select the reform +3. View "Poverty impact" section +4. See change in poverty rate by age group + +### Use Case 4: Who Benefits from Policy X? + +1. Go to policy page +2. Create or select the reform +3. View "Distributional impact" section +4. See winners and losers by income decile + +### Use Case 5: Compare Two Policy Proposals + +1. Create Reform A (e.g., expand EITC) +2. Note the URL or reform ID +3. Create Reform B (e.g., expand CTC) +4. Compare budgetary, poverty, and distributional impacts + +## For Analysts: Moving Beyond the Web App + +Once you understand the web app, you can: + +**Use the Python client:** +- See `policyengine-python-client-skill` for programmatic access +- See `policyengine-us-skill` for detailed simulation patterns + +**Create custom analyses:** +- See `policyengine-analysis-skill` for analysis patterns +- See `microdf-skill` for data analysis utilities + +**Access the API directly:** +- See `policyengine-api-skill` for API documentation +- REST endpoints for integration + +## For Contributors: Building PolicyEngine + +To contribute to PolicyEngine development: + +**Understanding the stack:** +- See `policyengine-core-skill` for engine architecture +- See `policyengine-us-skill` for country model patterns +- See `policyengine-api-skill` for API development +- See `policyengine-app-skill` for app development + +**Development standards:** +- See `policyengine-standards-skill` for code quality requirements +- See `policyengine-writing-skill` for documentation style + +## Frequently Asked Questions + +### How accurate is PolicyEngine? + +PolicyEngine uses official tax and benefit rules from legislation and regulations. Calculations match official calculators (IRS, SSA, etc.) for individual households. + +Population-level estimates use microsimulation with survey data (Current Population Survey for US, Family Resources Survey for UK). + +### Can I use PolicyEngine for my taxes? + +PolicyEngine is for policy analysis, not tax filing. Results are estimates based on the information you provide. For filing taxes, use IRS.gov or professional tax software. + +### How is PolicyEngine funded? + +PolicyEngine is a nonprofit funded by grants and donations. The platform is free to use. + +### Can I export results? + +Yes! Charts can be downloaded as PNG or HTML. You can also share reform URLs with others. + +### What programs does PolicyEngine model? + +**US (federal):** +- Income tax, payroll tax, capital gains tax +- EITC, CTC, ACTC +- SNAP, WIC, ACA premium tax credits +- Social Security, SSI, TANF +- State income taxes (varies by state) + +**UK:** +- Income tax, National Insurance +- Universal Credit, Child Benefit +- State Pension, Pension Credit +- Council Tax, Council Tax Support + +For complete lists, see: +- US: https://policyengine.org/us/parameters +- UK: https://policyengine.org/uk/parameters + +### How do I report a bug? + +**If you find incorrect calculations:** +1. Go to the household calculator +2. Note your inputs and the incorrect result +3. File an issue: https://github.com/PolicyEngine/policyengine-us/issues (or appropriate country repo) +4. Include the household URL + +**If you find app bugs:** +1. Note what you were doing +2. File an issue: https://github.com/PolicyEngine/policyengine-app/issues + +## Resources + +- **Website:** https://policyengine.org +- **Documentation:** https://policyengine.org/us/docs +- **Blog:** https://policyengine.org/us/research +- **GitHub:** https://github.com/PolicyEngine +- **Contact:** hello@policyengine.org + +## Related Skills + +- **policyengine-python-client-skill** - Using PolicyEngine programmatically +- **policyengine-us-skill** - Understanding US tax/benefit calculations +- **policyengine-analysis-skill** - Creating custom policy analyses diff --git a/skills/policyengine-writing-skill/SKILL.md b/skills/policyengine-writing-skill/SKILL.md new file mode 100644 index 0000000..9f3ed06 --- /dev/null +++ b/skills/policyengine-writing-skill/SKILL.md @@ -0,0 +1,526 @@ +--- +name: policyengine-writing +description: PolicyEngine writing style for blog posts, documentation, PR descriptions, and research reports - emphasizing active voice, quantitative precision, and neutral tone +--- + +# PolicyEngine Writing Skill + +Use this skill when writing blog posts, documentation, PR descriptions, research reports, or any public-facing PolicyEngine content. + +## When to Use This Skill + +- Writing blog posts about policy analysis +- Creating PR descriptions +- Drafting documentation +- Writing research reports +- Composing social media posts +- Creating newsletters +- Writing README files + +## Core Principles + +PolicyEngine's writing emphasizes clarity, precision, and objectivity. + +1. **Active voice** - Prefer active constructions over passive +2. **Direct and quantitative** - Use specific numbers, avoid vague adjectives/adverbs +3. **Sentence case** - Use sentence case for headings, not title case +4. **Neutral tone** - Describe what policies do, not whether they're good or bad +5. **Precise language** - Choose exact verbs over vague modifiers + +## Active Voice + +Active voice makes writing clearer and more direct. + +**✅ Correct (Active):** +``` +Harris proposes expanding the Earned Income Tax Credit +The reform reduces poverty by 3.2% +PolicyEngine projects higher costs than other organizations +We estimate the ten-year costs +The bill lowers the state's top income tax rate +Montana raises the EITC from 10% to 20% +``` + +**❌ Wrong (Passive):** +``` +The Earned Income Tax Credit is proposed to be expanded by Harris +Poverty is reduced by 3.2% by the reform +Higher costs are projected by PolicyEngine +The ten-year costs are estimated +The state's top income tax rate is lowered by the bill +The EITC is raised from 10% to 20% by Montana +``` + +## Quantitative and Precise + +Replace vague modifiers with specific numbers and measurements. + +**✅ Correct (Quantitative):** +``` +Costs the state $245 million +Benefits 77% of Montana residents +Lowers the Supplemental Poverty Measure by 0.8% +Raises net income by $252 in 2026 +The reform affects 14.3 million households +Hours worked falls by 0.27%, or 411,000 full-time equivalent jobs +The top decile receives an average benefit of $1,033 +PolicyEngine projects costs 40% higher than the Tax Foundation +``` + +**❌ Wrong (Vague adjectives/adverbs):** +``` +Significantly costs the state +Benefits most Montana residents +Greatly lowers poverty +Substantially raises net income +The reform affects many households +Hours worked falls considerably +High earners receive large benefits +PolicyEngine projects much higher costs +``` + +## Sentence Case for Headings + +Use sentence case (capitalize only the first word and proper nouns) for all headings. + +**✅ Correct (Sentence case):** +``` +## The proposal +## Nationwide impacts +## Household impacts +## Statewide impacts 2026 +## Case study: the End Child Poverty Act +## Key findings +``` + +**❌ Wrong (Title case):** +``` +## The Proposal +## Nationwide Impacts +## Household Impacts +## Statewide Impacts 2026 +## Case Study: The End Child Poverty Act +## Key Findings +``` + +## Neutral, Objective Tone + +Describe what policies do without value judgments. Let readers draw their own conclusions from the data. + +**✅ Correct (Neutral):** +``` +The reform reduces poverty by 3.2% and raises inequality by 0.16% +Single filers with earnings between $8,000 and $37,000 see their net incomes increase +The tax changes raise the net income of 75.9% of residents +PolicyEngine projects higher costs than other organizations +The top income decile receives 42% of total benefits +``` + +**❌ Wrong (Value judgments):** +``` +The reform successfully reduces poverty by 3.2% but unfortunately raises inequality +Low-income workers finally see their net incomes increase +The tax changes benefit most residents +PolicyEngine provides more accurate cost estimates +The wealthiest households receive a disproportionate share of benefits +``` + +## Precise Verbs Over Adverbs + +Choose specific verbs instead of generic verbs modified by adverbs. + +**✅ Correct (Precise verbs):** +``` +The bill lowers the top rate from 5.9% to 5.4% +The policy raises the maximum credit from $632 to $1,774 +The reform increases the phase-in rate from 7.65% to 15.3% +This doubles Montana's EITC from 10% to 20% +The change eliminates the age cap +``` + +**❌ Wrong (Vague verbs + adverbs):** +``` +The bill significantly changes the top rate +The policy substantially increases the maximum credit +The reform greatly boosts the phase-in rate +This dramatically expands Montana's EITC +The change completely removes the age cap +``` + +## Concrete Examples + +Always include specific household examples with precise numbers. + +**✅ Correct:** +``` +For a single adult with no children and $10,000 of earnings, the tax provisions +increase their net income by $69 in 2026 and $68 in 2027, solely from the +doubled EITC match. + +A single parent of two kids with an annual income of $50,000 will see a $252 +increase to their net income: $179 from the expanded EITC, and $73 from the +lower bracket threshold. + +A married couple with no dependents and $200,000 of earnings will see their +liability drop by $1,306 in 2027. +``` + +**❌ Wrong:** +``` +Low-income workers see modest increases to their net income from the +expanded EITC. + +Families with children benefit substantially from the tax changes. + +High earners also see significant reductions in their tax liability. +``` + +## Tables and Data + +Use tables liberally to present data clearly. Always include units and context. + +**Example 1: Tax parameters over time** + +| Year | Phase-in rate | Max credit | Phase-out start | Phase-out rate | +| ---- | ------------- | ---------- | --------------- | -------------- | +| 2025 | 15.3% | $1,774 | $13,706 | 15.3% | +| 2026 | 15.3% | $1,815 | $14,022 | 15.3% | +| 2027 | 15.3% | $1,852 | $14,306 | 15.3% | + +**Example 2: Household impacts** + +| Household composition | 2026 net income change | 2027 net income change | +| ------------------------------ | ---------------------- | ---------------------- | +| Single, no children, $10,000 | $66 | $68 | +| Single, two children, $50,000 | $252 | $266 | +| Married, no children, $200,000 | $853 | $1,306 | + +**Example 3: Ten-year costs** + +| Year | Federal cost ($ billions) | +| ------- | ------------------------- | +| 2025 | 14.3 | +| 2026 | 14.4 | +| 2027 | 14.7 | +| 2025-34 | 143.7 | + +## Avoid Superlatives + +Replace superlative claims with specific comparisons. + +**✅ Correct:** +``` +PolicyEngine projects costs 40% higher than the Tax Foundation +The top decile receives an average benefit of $1,033 +The reform reduces child poverty by 3.2 percentage points +This represents Montana's largest income tax cut since 2021 +``` + +**❌ Wrong:** +``` +PolicyEngine provides the most accurate cost projections +The wealthiest households receive massive benefits +The reform dramatically slashes child poverty +This is Montana's largest income tax cut in history +``` + +## Structure and Organization + +Follow a clear hierarchical structure with key findings up front. + +**Standard blog post structure:** + +```markdown +# Title (H1) + +Opening paragraph states what happened and when, with a link to PolicyEngine. + +Key results in [year]: +- Cost: $245 million +- Benefits: 77% of residents +- Poverty impact: Reduces SPM by 0.8% +- Inequality impact: Raises Gini by 0.16% + +## The proposal (H2) + +Detailed description of the policy changes, often with a table showing +the specific parameter values. + +## Household impacts (H2) + +Specific examples for representative household types. + +### Example 1: Single filer (H3) +Detailed calculation... + +### Example 2: Family with children (H3) +Detailed calculation... + +## Statewide impacts (H2) + +Population-level analysis with charts and tables. + +### Budgetary impact (H3) +Cost/revenue estimates... + +### Distributional impact (H3) +Winners/losers by income decile... + +### Poverty and inequality (H3) +Impact on poverty rates and inequality measures... + +## Methodology (H2) + +Explanation of data sources, modeling approach, and caveats. +``` + +## Common Patterns + +### Opening Paragraphs + +State the facts directly with dates, actors, and actions: + +``` +[On April 28, 2025], Governor Greg Gianforte (R-MT) signed House Bill 337, +a bill that amends Montana's individual income tax code. + +Vice President Harris proposes expanding the Earned Income Tax Credit (EITC) +for filers without qualifying dependents. + +In her economic plan, Harris proposes to restore the expanded Earned Income +Tax Credit for workers without children to its level under the American +Rescue Plan Act in 2021. +``` + +### Key Findings Format + +Lead with bullet points of quantitative results: + +``` +Key results in 2027: +- Costs the state $245 million +- Benefits 77% of Montana residents +- Lowers the Supplemental Poverty Measure by 0.8% +- Raises the Gini index by 0.16% +``` + +### Methodological Transparency + +Always specify the model, version, and assumptions: + +``` +Based on static microsimulation modeling with PolicyEngine US (version 1.103.0), +we project the following economic impacts for 2025. + +Assuming no behavioral responses, we project that the EITC expansion will cost +the federal government $14.3 billion in 2025. + +Incorporating elasticities of labor supply used by the Congressional Budget Office +increases the reform's cost. + +Over the ten-year budget window, this amounts to $143.7 billion. +``` + +### Household Examples + +Always include the household composition, income, and specific dollar impacts: + +``` +For a single adult with no children and $10,000 of earnings, the tax provisions +increase their net income by $69 in 2026 and $68 in 2027. + +A single parent of two kids with an annual income of $50,000 will see a $252 +increase to their net income due to House Bill 337: $179 from the expanded EITC, +and $73 from the lower bracket threshold. +``` + +## Examples in Context + +### Blog Post Opening + +**✅ Correct:** +``` +On April 28, 2025, Governor Gianforte signed House Bill 337, which lowers +Montana's top income tax rate from 5.9% to 5.4% and doubles the state EITC +from 10% to 20% of the federal credit. + +Key results in 2027: +- Costs the state $245 million +- Benefits 77% of Montana residents +- Lowers the Supplemental Poverty Measure by 0.8% +- Raises the Gini index by 0.16% + +Use PolicyEngine to view the full results or calculate the effect on your +household. +``` + +**❌ Wrong:** +``` +On April 28, 2025, Governor Gianforte made history by signing an amazing new +tax cut bill that will dramatically help Montana families. House Bill 337 +significantly reduces tax rates and greatly expands the EITC. + +This groundbreaking reform will: +- Cost the state money +- Help most residents +- Reduce poverty substantially +- Impact inequality + +Check out PolicyEngine to see how much you could save! +``` + +### PR Description + +**✅ Correct:** +``` +## Summary + +This PR adds Claude Code plugin configuration to enable automated installation +of agents and skills for PolicyEngine development. + +## Changes + +- Add plugin auto-install configuration in .claude/settings.json +- Configure auto-install of country-models plugin from PolicyEngine/policyengine-claude + +## Benefits + +- Access to 15 specialized agents +- 3 slash commands (/encode-policy, /review-pr, /fix-pr) +- 2 skills (policyengine-us-skill, policyengine-standards-skill) + +## Testing + +After merging, team members trust the repo and the plugin auto-installs. +``` + +**❌ Wrong:** +``` +## Summary + +This amazing PR adds incredible new Claude Code plugin support that will +revolutionize PolicyEngine development! + +## Changes + +- Adds some configuration files +- Sets up plugins and stuff + +## Benefits + +- Gets you lots of cool new features +- Makes development much easier +- Provides great new tools + +## Testing + +Should work great once merged! +``` + +### Documentation + +**✅ Correct:** +``` +## Installation + +Install PolicyEngine-US from PyPI: + +```bash +pip install policyengine-us +``` + +This installs version 1.103.0 or later, which includes support for 2025 +tax parameters. +``` + +**❌ Wrong:** +``` +## Installation + +Simply install PolicyEngine-US: + +```bash +pip install policyengine-us +``` + +This will install the latest version with all the newest features! +``` + +## Special Cases + +### Comparisons to Other Organizations + +State facts neutrally without claiming superiority: + +**✅ Correct:** +``` +PolicyEngine projects higher costs than other organizations when considering +behavioral responses. + +| Organization | Cost, 2025-2034 ($ billions) | +| ------------------------- | ---------------------------- | +| PolicyEngine (static) | 144 | +| PolicyEngine (dynamic) | 201 | +| Tax Foundation | 157 | +| Penn Wharton Budget Model | 135 | +``` + +**❌ Wrong:** +``` +PolicyEngine provides more accurate estimates than other organizations. + +Unlike other models that underestimate costs, PolicyEngine correctly accounts +for behavioral responses to project a more realistic $201 billion cost. +``` + +### Discussing Limitations + +Acknowledge limitations directly without hedging: + +**✅ Correct:** +``` +## Caveats + +The Current Population Survey has several limitations for tax microsimulation: + +- Truncates high incomes for privacy, underestimating tax impacts on high earners +- Underestimates benefit receipt compared to administrative totals +- Reflects 2020 data with 2025 policy parameters +- Lacks detail for specific income types (assumes all capital gains are long-term) +``` + +**❌ Wrong:** +``` +## Caveats + +While our model is highly sophisticated, like all models it has some potential +limitations that users should be aware of: + +- The data might not perfectly capture high incomes +- Benefits may be slightly underestimated +- We do our best to extrapolate older data to current years +``` + +## Writing Checklist + +Before publishing, verify: + +- [ ] Use active voice throughout +- [ ] Include specific numbers for all claims +- [ ] Use sentence case for all headings +- [ ] Maintain neutral, objective tone +- [ ] Choose precise verbs over vague adverbs +- [ ] Include concrete household examples +- [ ] Present data in tables +- [ ] Avoid all superlatives +- [ ] Structure with clear hierarchy +- [ ] Open with key quantitative findings +- [ ] Specify model version and assumptions +- [ ] Link to PolicyEngine when relevant +- [ ] Acknowledge limitations directly + +## Resources + +- **Example posts**: See `policyengine-app/src/posts/articles/` for reference implementations +- **PolicyEngine app**: https://policyengine.org for linking to analyses +- **Microsimulation docs**: https://policyengine.org/us/docs for methodology details