Initial commit
This commit is contained in:
660
skills/domain-knowledge/policyengine-uk-skill/SKILL.md
Normal file
660
skills/domain-knowledge/policyengine-uk-skill/SKILL.md
Normal file
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}
|
||||
}
|
||||
524
skills/domain-knowledge/policyengine-us-skill/SKILL.md
Normal file
524
skills/domain-knowledge/policyengine-us-skill/SKILL.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user