diff --git a/docs/book/_toc.yml b/docs/book/_toc.yml
index ccb1cc77a..53801e973 100644
--- a/docs/book/_toc.yml
+++ b/docs/book/_toc.yml
@@ -13,4 +13,6 @@ parts:
- caption: Validation
chapters:
- file: validation/hbai
-
+ - caption: Policy
+ chapters:
+ - file: policy/uc-rebalancing
diff --git a/docs/book/index.ipynb b/docs/book/index.ipynb
index a48f3d4a0..77627d9e3 100644
--- a/docs/book/index.ipynb
+++ b/docs/book/index.ipynb
@@ -121,7 +121,7 @@
" reformed.calculate(\"gov_balance\", 2025).sum()\n",
" - baseline.calc(\"gov_balance\", 2025).sum()\n",
")\n",
- "f\"Revenue: £{round(revenue / 1e+9, 1)}bn\""
+ "f\"Revenue: £{round(revenue / 1e9, 1)}bn\""
]
}
],
diff --git a/docs/book/policy/uc-rebalancing.md b/docs/book/policy/uc-rebalancing.md
new file mode 100644
index 000000000..cd6ab2c2c
--- /dev/null
+++ b/docs/book/policy/uc-rebalancing.md
@@ -0,0 +1,95 @@
+# Universal Credit rebalancing reforms
+
+The Universal Credit rebalancing reforms represent changes to Universal Credit provisions introduced through the Universal Credit Bill. These reforms take effect from April 2026 and are designed to adjust benefit levels and eligibility criteria.
+
+## Overview
+
+The reforms consist of two main components:
+
+1. **Health element changes for new claimants**: New Universal Credit claimants from April 2026 onwards receive a fixed health element amount, while existing claimants continue to receive inflation-linked increases.
+
+2. **Standard allowance uplifts**: The standard allowance receives additional uplifts beyond the annual inflationary increase from 2026-2029.
+
+## Health element changes
+
+From April 2026, new Universal Credit claimants who qualify for the Limited Capacity for Work-Related Activity (LCWRA) element receive a fixed monthly amount of £217.26, rather than the inflation-adjusted amount that pre-2026 claimants continue to receive.
+
+The implementation uses transition probabilities based on WPI Economics analysis for the Trussell Trust, derived from administrative Personal Independence Payment data. The probability of being a new claimant varies by year:
+
+- 2026: 11%
+- 2027: 13%
+- 2028: 16%
+- 2029: 22%
+
+## Standard allowance uplifts
+
+The standard allowance receives additional percentage uplifts beyond the normal inflationary increase:
+
+- 2026: 2.3% additional uplift
+- 2027: 3.1% additional uplift (cumulative)
+- 2028: 4.0% additional uplift (cumulative)
+- 2029: 4.8% additional uplift (cumulative)
+
+These uplifts are applied to the previous year's standard allowance amount and compound over time.
+
+## Implementation
+
+The reforms are implemented through:
+
+- **Parameters**: Three YAML files define the reform's activation status, health element amount for new claimants, and standard allowance uplift rates.
+- **Scenario modifier**: The `add_universal_credit_reform` function applies the changes to Universal Credit calculations during microsimulation.
+- **Scenario**: The `universal_credit_july_2025_reform` scenario enables the reforms in policy analysis.
+
+## Examples
+
+You can use these reforms in your own analysis by creating a `Simulation` with parametric changes to modify the reform parameters.
+
+### Disabling the rebalancing reforms entirely
+
+```python
+from policyengine_uk import Simulation, Scenario
+
+# Disable the reforms from 2026 onwards
+scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.rebalancing.active": False,
+})
+
+sim = Simulation(scenario=scenario)
+```
+
+### Changing the standard allowance uplift parameters
+
+```python
+from policyengine_uk import Simulation, Scenario
+
+# Set different uplift rates - e.g. 5% in 2026, 7% in 2027
+scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.rebalancing.standard_allowance_uplift": {
+ "2026-01-01": 0.05,
+ "2027-01-01": 0.07,
+ "2028-01-01": 0.07,
+ "2029-01-01": 0.07
+ }
+})
+
+sim = Simulation(scenario=scenario)
+```
+
+### Changing the health element amount for new claimants
+
+```python
+from policyengine_uk import Simulation, Scenario
+
+# Set the new claimant health element to £250 per month
+scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": {
+ "2026-01-01": 250.00
+ }
+})
+
+sim = Simulation(scenario=scenario)
+```
+
+## Legislative reference
+
+The reforms are based on provisions in the Universal Credit Bill, available at: https://bills.parliament.uk/publications/62123/documents/6889.
\ No newline at end of file
diff --git a/docs/book/programs/gov/dcms/bbc/tv-licence.ipynb b/docs/book/programs/gov/dcms/bbc/tv-licence.ipynb
index 4eed2eaad..9e7123015 100644
--- a/docs/book/programs/gov/dcms/bbc/tv-licence.ipynb
+++ b/docs/book/programs/gov/dcms/bbc/tv-licence.ipynb
@@ -128,12 +128,10 @@
"\n",
"df = pd.DataFrame()\n",
"df[\"Date\"] = [\n",
- " parameter.instant_str\n",
- " for parameter in dcms.bbc.tv_licence.colour.values_list\n",
+ " parameter.instant_str for parameter in dcms.bbc.tv_licence.colour.values_list\n",
"]\n",
"df[\"Full TV Licence Fee\"] = [\n",
- " f\"£{parameter.value:.2f}\"\n",
- " for parameter in dcms.bbc.tv_licence.colour.values_list\n",
+ " f\"£{parameter.value:.2f}\" for parameter in dcms.bbc.tv_licence.colour.values_list\n",
"]\n",
"df[\"Blind TV Licence Fee\"] = [\n",
" f\"£{0.5 * parameter.value:.2f}\"\n",
@@ -332,13 +330,11 @@
"for i in range(len(aged_discount[\"Reformed value\"])):\n",
" aged_discount[\"Current value\"] += [\"100%\"]\n",
" aged_discount[\"Change against current\"] += [\n",
- " f\"{aged_discount['Reformed value'][i]-1:.0%}\"\n",
+ " f\"{aged_discount['Reformed value'][i] - 1:.0%}\"\n",
" ]\n",
- " aged_discount[\"Reformed value\"][\n",
- " i\n",
- " ] = f\"{aged_discount['Reformed value'][i]:.0%}\"\n",
+ " aged_discount[\"Reformed value\"][i] = f\"{aged_discount['Reformed value'][i]:.0%}\"\n",
" aged_discount[\"Reference\"] += [\n",
- " f\"Budgetary impact of changing aged discount to {aged_discount['Reformed value'][i]}\"\n",
+ " f'Budgetary impact of changing aged discount to {aged_discount[\"Reformed value\"][i]}'\n",
" ]\n",
"\n",
"df_aged_discount = pd.DataFrame(aged_discount)\n",
@@ -406,11 +402,11 @@
"for i in range(len(licence_fee[\"Reformed value\"])):\n",
" licence_fee[\"Current value\"] += [\"£159\"]\n",
" licence_fee[\"Change against current\"] += [\n",
- " f\"£{licence_fee['Reformed value'][i]-159}\"\n",
+ " f\"£{licence_fee['Reformed value'][i] - 159}\"\n",
" ]\n",
" licence_fee[\"Reformed value\"][i] = f\"£{licence_fee['Reformed value'][i]}\"\n",
" licence_fee[\"Reference\"] += [\n",
- " f\"Budgetary impact of changing licence fee to {licence_fee['Reformed value'][i]}\"\n",
+ " f'Budgetary impact of changing licence fee to {licence_fee[\"Reformed value\"][i]}'\n",
" ]\n",
"\n",
"df_licence_fee = pd.DataFrame(licence_fee)\n",
diff --git a/docs/book/programs/gov/dwp/pension-credit.ipynb b/docs/book/programs/gov/dwp/pension-credit.ipynb
index d88ca5aa4..1601aa923 100644
--- a/docs/book/programs/gov/dwp/pension-credit.ipynb
+++ b/docs/book/programs/gov/dwp/pension-credit.ipynb
@@ -167,12 +167,8 @@
"\n",
"parameters = system.parameters\n",
"\n",
- "carer_addition = (\n",
- " parameters.gov.dwp.pension_credit.guarantee_credit.carer.addition\n",
- ")\n",
- "child_addition = (\n",
- " parameters.gov.dwp.pension_credit.guarantee_credit.child.addition\n",
- ")\n",
+ "carer_addition = parameters.gov.dwp.pension_credit.guarantee_credit.carer.addition\n",
+ "child_addition = parameters.gov.dwp.pension_credit.guarantee_credit.child.addition\n",
"disabled_child = (\n",
" parameters.gov.dwp.pension_credit.guarantee_credit.child.disability.addition\n",
")\n",
@@ -1506,12 +1502,8 @@
"\n",
"parameters = system.parameters\n",
"\n",
- "threshold_single = (\n",
- " parameters.gov.dwp.pension_credit.savings_credit.threshold.SINGLE\n",
- ")\n",
- "threshold_couple = (\n",
- " parameters.gov.dwp.pension_credit.savings_credit.threshold.COUPLE\n",
- ")\n",
+ "threshold_single = parameters.gov.dwp.pension_credit.savings_credit.threshold.SINGLE\n",
+ "threshold_couple = parameters.gov.dwp.pension_credit.savings_credit.threshold.COUPLE\n",
"\n",
"elements = [threshold_single, threshold_couple] # [...]\n",
"\n",
diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb
index 843b7d45b..08e1146d7 100644
--- a/docs/book/programs/gov/dwp/universal-credit.ipynb
+++ b/docs/book/programs/gov/dwp/universal-credit.ipynb
@@ -194,9 +194,7 @@
"disabled_child_amount = (\n",
" parameters.gov.dwp.universal_credit.elements.child.disabled.amount\n",
")\n",
- "higher_amount = (\n",
- " parameters.gov.dwp.universal_credit.elements.child.first.higher_amount\n",
- ")\n",
+ "higher_amount = parameters.gov.dwp.universal_credit.elements.child.first.higher_amount\n",
"\n",
"elements = [\n",
" carer_amount,\n",
diff --git a/docs/book/programs/gov/hmrc/child-benefit.ipynb b/docs/book/programs/gov/hmrc/child-benefit.ipynb
index a6cb1c291..02b915df5 100644
--- a/docs/book/programs/gov/hmrc/child-benefit.ipynb
+++ b/docs/book/programs/gov/hmrc/child-benefit.ipynb
@@ -169,29 +169,27 @@
" data[\"Value\"] += [f\"£{parameter.value:.2f}\"]\n",
" try:\n",
" data[\"Reference\"] += [\n",
- " f\"{parameter.metadata['reference'][0]['title']}\"\n",
+ " f'{parameter.metadata[\"reference\"][0][\"title\"]}'\n",
" ]\n",
" except:\n",
" data[\"Reference\"] += [\"\"]\n",
"\n",
"\n",
- "for (\n",
- " parameter\n",
- ") in parameters.gov.hmrc.child_benefit.amount.additional.values_list:\n",
+ "for parameter in parameters.gov.hmrc.child_benefit.amount.additional.values_list:\n",
" data[\"Date\"] += [parameter.instant_str]\n",
" data[\"Name\"] += [\"Additional\"]\n",
" data[\"Value\"] += [f\"£{parameter.value:.2f}\"]\n",
" try:\n",
" data[\"Reference\"] += [\n",
- " f\"{parameter.metadata['reference'][0]['title']}\"\n",
+ " f'{parameter.metadata[\"reference\"][0][\"title\"]}'\n",
" ]\n",
" except:\n",
" data[\"Reference\"] += [\"\"]\n",
"\n",
"\n",
- "pd.DataFrame(data).sort_values(\"Date\").set_index(\n",
- " [\"Date\", \"Name\"]\n",
- ").style.format(lambda x: x)"
+ "pd.DataFrame(data).sort_values(\"Date\").set_index([\"Date\", \"Name\"]).style.format(\n",
+ " lambda x: x\n",
+ ")"
]
},
{
@@ -296,15 +294,13 @@
" for i in range(n):\n",
" sim.add_person(age=10, name=str(i))\n",
" sim.add_benunit(members=[str(i) for i in range(n)])\n",
- " return f'£{sim.calc(\"child_benefit\").sum():.2f}'\n",
+ " return f\"£{sim.calc('child_benefit').sum():.2f}\"\n",
"\n",
"\n",
"pd.DataFrame(\n",
" {\n",
" \"Number of children\": list(range(1, 7)),\n",
- " \"Child Benefit (Annual)\": list(\n",
- " map(get_cb_for_n_children, range(1, 7))\n",
- " ),\n",
+ " \"Child Benefit (Annual)\": list(map(get_cb_for_n_children, range(1, 7))),\n",
" }\n",
").set_index(\"Number of children\")"
]
diff --git a/docs/book/programs/gov/hmrc/fuel-duty.ipynb b/docs/book/programs/gov/hmrc/fuel-duty.ipynb
index a4ba22fff..51d676866 100644
--- a/docs/book/programs/gov/hmrc/fuel-duty.ipynb
+++ b/docs/book/programs/gov/hmrc/fuel-duty.ipynb
@@ -126,12 +126,10 @@
"df = pd.DataFrame()\n",
"\n",
"df[\"Date of change\"] = [\n",
- " parameter.instant_str\n",
- " for parameter in hmrc.fuel_duty.petrol_and_diesel.values_list\n",
+ " parameter.instant_str for parameter in hmrc.fuel_duty.petrol_and_diesel.values_list\n",
"]\n",
"df[\"Fuel duty rate (£/litre)\"] = [\n",
- " parameter.value\n",
- " for parameter in hmrc.fuel_duty.petrol_and_diesel.values_list\n",
+ " parameter.value for parameter in hmrc.fuel_duty.petrol_and_diesel.values_list\n",
"]\n",
"df.sort_values(\"Date of change\", inplace=True)\n",
"df.set_index(\"Date of change\")"
@@ -1072,11 +1070,7 @@
" height=600,\n",
" width=800,\n",
" template=\"plotly_white\",\n",
- ").update_xaxes(\n",
- " tickangle=45, tickfont={\"size\": 10}\n",
- ").update_traces(\n",
- " line_shape=\"hv\"\n",
- ")"
+ ").update_xaxes(tickangle=45, tickfont={\"size\": 10}).update_traces(line_shape=\"hv\")"
]
}
],
diff --git a/docs/book/programs/gov/hmrc/income-tax.ipynb b/docs/book/programs/gov/hmrc/income-tax.ipynb
index ff2491bfe..77f1fc99a 100644
--- a/docs/book/programs/gov/hmrc/income-tax.ipynb
+++ b/docs/book/programs/gov/hmrc/income-tax.ipynb
@@ -207,9 +207,7 @@
"from policyengine_uk import Simulation\n",
"\n",
"\n",
- "def calculate_taxes(\n",
- " employment_income, dividend_income, savings_interest_income\n",
- "):\n",
+ "def calculate_taxes(employment_income, dividend_income, savings_interest_income):\n",
" simulation = Simulation(\n",
" situation={\n",
" \"people\": {\n",
@@ -222,9 +220,7 @@
" }\n",
" )\n",
" income_tax = simulation.calculate(\"income_tax\")[0]\n",
- " dividend_income_tax = simulation.calculate(\"dividend_income_tax\")[\n",
- " 0\n",
- " ] # Added\n",
+ " dividend_income_tax = simulation.calculate(\"dividend_income_tax\")[0] # Added\n",
" savings_income_tax = simulation.calculate(\"savings_income_tax\")[0] # Added\n",
"\n",
" return income_tax, dividend_income_tax, savings_income_tax\n",
@@ -245,9 +241,7 @@
" data[\"Dividend Income (£)\"],\n",
" data[\"Savings Interest Income (£)\"],\n",
"):\n",
- " income_tax, div_tax, sav_tax = calculate_taxes(\n",
- " emp_income, div_income, sav_income\n",
- " )\n",
+ " income_tax, div_tax, sav_tax = calculate_taxes(emp_income, div_income, sav_income)\n",
" data[\"Income Tax (£)\"].append(income_tax)\n",
" data[\"Dividend Income Tax (£)\"].append(div_tax)\n",
" data[\"Savings Income Tax (£)\"].append(sav_tax)\n",
@@ -524,12 +518,8 @@
"from policyengine_uk.system import system\n",
"import pandas as pd\n",
"\n",
- "default = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.annual_allowance.default\n",
- ")\n",
- "minimum = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.annual_allowance.minimum\n",
- ")\n",
+ "default = system.parameters.gov.hmrc.income_tax.allowances.annual_allowance.default\n",
+ "minimum = system.parameters.gov.hmrc.income_tax.allowances.annual_allowance.minimum\n",
"reduction_rate = (\n",
" system.parameters.gov.hmrc.income_tax.allowances.annual_allowance.reduction_rate\n",
")\n",
@@ -559,9 +549,7 @@
"\n",
"\n",
"max = system.parameters.gov.hmrc.income_tax.allowances.marriage_allowance.max\n",
- "rounding_increment = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.marriage_allowance.rounding_increment\n",
- ")\n",
+ "rounding_increment = system.parameters.gov.hmrc.income_tax.allowances.marriage_allowance.rounding_increment\n",
"takeup_rate = (\n",
" system.parameters.gov.hmrc.income_tax.allowances.marriage_allowance.takeup_rate\n",
")\n",
@@ -586,9 +574,7 @@
"}\n",
"\n",
"\n",
- "deduction_rate = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.married_couples_allowance.deduction_rate\n",
- ")\n",
+ "deduction_rate = system.parameters.gov.hmrc.income_tax.allowances.married_couples_allowance.deduction_rate\n",
"married_couples_allowance_data = {\n",
" \"Attribute\": [\"Married Couples Allowance\"],\n",
" \"Type\": [\"Deduction Rate\"],\n",
@@ -598,9 +584,7 @@
"}\n",
"\n",
"\n",
- "amount = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.personal_allowance.amount\n",
- ")\n",
+ "amount = system.parameters.gov.hmrc.income_tax.allowances.personal_allowance.amount\n",
"values = [item.value for item in amount.values_list]\n",
"dates = [item.instant_str for item in amount.values_list]\n",
"personal_allowance_data = {\n",
@@ -612,9 +596,7 @@
"}\n",
"\n",
"\n",
- "addtional_threshold = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.personal_savings_allowance.additional\n",
- ")\n",
+ "addtional_threshold = system.parameters.gov.hmrc.income_tax.allowances.personal_savings_allowance.additional\n",
"basic_threshold = (\n",
" system.parameters.gov.hmrc.income_tax.allowances.personal_savings_allowance.basic\n",
")\n",
@@ -642,9 +624,7 @@
"}\n",
"\n",
"\n",
- "dividend_allowance = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.dividend_allowance\n",
- ")\n",
+ "dividend_allowance = system.parameters.gov.hmrc.income_tax.allowances.dividend_allowance\n",
"dividend_allowance_data = {\n",
" \"Attribute\": [\"Dividend Allowance\"],\n",
" \"Type\": [\"Dividend Allowance\"],\n",
@@ -654,9 +634,7 @@
"}\n",
"\n",
"\n",
- "property_allowance = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.property_allowance\n",
- ")\n",
+ "property_allowance = system.parameters.gov.hmrc.income_tax.allowances.property_allowance\n",
"property_allowance_data = {\n",
" \"Attribute\": [\"Property Allowance\"],\n",
" \"Type\": [\"Property Allowance\"],\n",
@@ -666,9 +644,7 @@
"}\n",
"\n",
"\n",
- "trading_allowance = (\n",
- " system.parameters.gov.hmrc.income_tax.allowances.trading_allowance\n",
- ")\n",
+ "trading_allowance = system.parameters.gov.hmrc.income_tax.allowances.trading_allowance\n",
"trading_allowance_data = {\n",
" \"Attribute\": [\"Trading Allowance\"],\n",
" \"Type\": [\"Trading Allowance\"],\n",
diff --git a/docs/book/programs/gov/hmrc/stamp-duty.ipynb b/docs/book/programs/gov/hmrc/stamp-duty.ipynb
index e4bdd751f..b7975450b 100644
--- a/docs/book/programs/gov/hmrc/stamp-duty.ipynb
+++ b/docs/book/programs/gov/hmrc/stamp-duty.ipynb
@@ -2509,7 +2509,7 @@
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.10000031441450119,
+ 0.1000003144145012,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
@@ -2603,7 +2603,7 @@
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.09999906271696091,
+ 0.09999906271696092,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
@@ -2681,222 +2681,222 @@
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.11999862641096115,
- 0.12000187486410141,
+ 0.11999862641096117,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.119998998939991,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.119998998939991,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.119998998939991,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.119998998939991,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
0.1199987530708313,
@@ -3165,7 +3165,7 @@
0.1199987530708313,
0.1199987530708313,
0.1199987530708313,
- 0.11999925225973129,
+ 0.11999925225973128,
0.1199987530708313,
0.12000499665737152,
0.1199987530708313,
@@ -3265,7 +3265,7 @@
0.12000499665737152,
0.1199987530708313,
0.1199987530708313,
- 0.11999925225973129,
+ 0.11999925225973128,
0.1199987530708313,
0.12000499665737152,
0.1199987530708313,
@@ -6287,9 +6287,7 @@
"stamp_duty = sim.calculate(\"stamp_duty_land_tax\")\n",
"home_price = sim.calculate(\"main_residential_property_purchased\")\n",
"\n",
- "marginal_rate = (stamp_duty[1:] - stamp_duty[:-1]) / (\n",
- " home_price[1:] - home_price[:-1]\n",
- ")\n",
+ "marginal_rate = (stamp_duty[1:] - stamp_duty[:-1]) / (home_price[1:] - home_price[:-1])\n",
"home_price = home_price[:-1]\n",
"\n",
"df = pd.DataFrame(\n",
@@ -6305,14 +6303,10 @@
"\n",
"# left shows marginal rate, right shows total SDLT\n",
"\n",
- "fig = make_subplots(\n",
- " rows=1, cols=2, subplot_titles=(\"Marginal SDLT rate\", \"Total SDLT\")\n",
- ")\n",
+ "fig = make_subplots(rows=1, cols=2, subplot_titles=(\"Marginal SDLT rate\", \"Total SDLT\"))\n",
"\n",
"fig.add_trace(\n",
- " px.line(\n",
- " df, x=\"Home price\", y=\"Marginal SDLT rate\", title=\"Marginal SDLT rate\"\n",
- " )\n",
+ " px.line(df, x=\"Home price\", y=\"Marginal SDLT rate\", title=\"Marginal SDLT rate\")\n",
" .update_traces(line_shape=\"hv\")\n",
" .data[0],\n",
" row=1,\n",
diff --git a/docs/book/programs/gov/ofgem/energy-price-guarantee.ipynb b/docs/book/programs/gov/ofgem/energy-price-guarantee.ipynb
index 9df670e58..e23a13d53 100644
--- a/docs/book/programs/gov/ofgem/energy-price-guarantee.ipynb
+++ b/docs/book/programs/gov/ofgem/energy-price-guarantee.ipynb
@@ -1011,9 +1011,7 @@
"\n",
"df = pd.DataFrame()\n",
"\n",
- "df[\"Date\"] = [\n",
- " parameter.instant_str for parameter in ofgem.energy_price_cap.values_list\n",
- "]\n",
+ "df[\"Date\"] = [parameter.instant_str for parameter in ofgem.energy_price_cap.values_list]\n",
"df[\"Energy price cap\"] = [\n",
" parameter.value for parameter in ofgem.energy_price_cap.values_list\n",
"]\n",
@@ -1107,11 +1105,11 @@
"xaxis": "x",
"y": [
8.373929949593731e-10,
- 1.6747859899187461e-9,
- 2.5121789848781193e-9,
+ 1.674785989918746e-9,
+ 2.512178984878119e-9,
3.3495719798374923e-9,
1.2009600411861108,
- 2.4019200790226494,
+ 2.40192007902265,
3.602880116859188,
9.034827286738466,
14.466774456617744,
@@ -1998,9 +1996,7 @@
" *[f\"2023-{month:02d}\" for month in range(1, 13)],\n",
"]\n",
"\n",
- "epg_costs = [\n",
- " sim.calc(\"monthly_epg_subsidy\", period).sum() for period in time_periods\n",
- "]\n",
+ "epg_costs = [sim.calc(\"monthly_epg_subsidy\", period).sum() for period in time_periods]\n",
"\n",
"df = pd.DataFrame(\n",
" {\n",
diff --git a/docs/book/programs/gov/revenue_scotland/land-and-buildings-transaction-tax.ipynb b/docs/book/programs/gov/revenue_scotland/land-and-buildings-transaction-tax.ipynb
index 437eac879..6e6400b9c 100644
--- a/docs/book/programs/gov/revenue_scotland/land-and-buildings-transaction-tax.ipynb
+++ b/docs/book/programs/gov/revenue_scotland/land-and-buildings-transaction-tax.ipynb
@@ -1217,9 +1217,7 @@
" color=\"Label\",\n",
" title=\"Land and Buildings Transaction Tax (LBTT) rates over property price thresholds\",\n",
" )\n",
- " .update_layout(\n",
- " yaxis_tickformat=\",.0%\", xaxis_tickprefix=\"£\", legend_title=\"\"\n",
- " )\n",
+ " .update_layout(yaxis_tickformat=\",.0%\", xaxis_tickprefix=\"£\", legend_title=\"\")\n",
" .update_traces(line_shape=\"hv\")\n",
")\n",
"fig = format_fig(fig)\n",
@@ -2334,38 +2332,38 @@
0.05000000074505806,
0.05000000074505806,
0.05324987694621086,
- 0.09999996423721313,
+ 0.09999996423721312,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.09999996423721313,
+ 0.09999996423721312,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.10000015795230865,
+ 0.10000015795230864,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.09999996423721313,
+ 0.09999996423721312,
0.10000000149011612,
0.10000000149011612,
0.0999998077750206,
0.10000000149011612,
0.10000000149011612,
- 0.10000015795230865,
+ 0.10000015795230864,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.10000015795230865,
+ 0.10000015795230864,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
@@ -2400,7 +2398,7 @@
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
- 0.10000031441450119,
+ 0.1000003144145012,
0.10000000149011612,
0.10000000149011612,
0.10000000149011612,
@@ -2425,7 +2423,7 @@
0.11999953538179398,
0.12000031024217606,
0.12000031024217606,
- 0.11999959498643875,
+ 0.11999959498643877,
0.12000031024217606,
0.11999953538179398,
0.12000031024217606,
@@ -2437,7 +2435,7 @@
0.12000031024217606,
0.11999953538179398,
0.12000031024217606,
- 0.11999959498643875,
+ 0.11999959498643877,
0.12000031024217606,
0.11999953538179398,
0.12000031024217606,
@@ -2450,7 +2448,7 @@
0.11999953538179398,
0.11999953538179398,
0.12000031024217606,
- 0.12000037729740143,
+ 0.12000037729740144,
0.12000031024217606,
0.12000031024217606,
0.1199987530708313,
@@ -2462,7 +2460,7 @@
0.12000031024217606,
0.12000031024217606,
0.12000031024217606,
- 0.12000037729740143,
+ 0.12000037729740144,
0.1199987530708313,
0.12000031024217606,
0.12000031024217606,
@@ -2475,7 +2473,7 @@
0.12000031024217606,
0.1199987530708313,
0.12000031024217606,
- 0.12000037729740143,
+ 0.12000037729740144,
0.12000031024217606,
0.12000031024217606,
0.11999868601560593,
@@ -2558,223 +2556,223 @@
0.12000031024217606,
0.11999887973070145,
0.12000031024217606,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.12000200152397156,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.11999862641096115,
- 0.12000187486410141,
+ 0.11999862641096117,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.119998998939991,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.119998998939991,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
- 0.12000187486410141,
+ 0.1200018748641014,
0.1199987530708313,
0.1199987530708313,
0.1199987530708313,
@@ -3118,7 +3116,7 @@
0.1199987530708313,
0.1199987530708313,
0.1199987530708313,
- 0.11999925225973129,
+ 0.11999925225973128,
0.1199987530708313,
0.12000499665737152,
0.1199987530708313,
@@ -4326,7 +4324,7 @@
0,
0,
3.5034375190734863,
- 103.60375213623047,
+ 103.60375213623048,
203.70375061035156,
303.8037414550781,
403.90374755859375,
@@ -6241,9 +6239,7 @@
"lbtt_sim = sim.calculate(\"land_and_buildings_transaction_tax\")\n",
"home_price = sim.calculate(\"main_residential_property_purchased\")\n",
"\n",
- "marginal_rate = (lbtt_sim[1:] - lbtt_sim[:-1]) / (\n",
- " home_price[1:] - home_price[:-1]\n",
- ")\n",
+ "marginal_rate = (lbtt_sim[1:] - lbtt_sim[:-1]) / (home_price[1:] - home_price[:-1])\n",
"home_price = home_price[:-1]\n",
"\n",
"df = pd.DataFrame(\n",
@@ -6259,14 +6255,10 @@
"\n",
"# left shows marginal rate, right shows total LBTT\n",
"\n",
- "fig = make_subplots(\n",
- " rows=1, cols=2, subplot_titles=(\"Marginal LBTT rate\", \"Total LBTT\")\n",
- ")\n",
+ "fig = make_subplots(rows=1, cols=2, subplot_titles=(\"Marginal LBTT rate\", \"Total LBTT\"))\n",
"\n",
"fig.add_trace(\n",
- " px.line(\n",
- " df, x=\"Home price\", y=\"Marginal LBTT rate\", title=\"Marginal LBTT rate\"\n",
- " )\n",
+ " px.line(df, x=\"Home price\", y=\"Marginal LBTT rate\", title=\"Marginal LBTT rate\")\n",
" .update_traces(line_shape=\"hv\")\n",
" .data[0],\n",
" row=1,\n",
diff --git a/docs/book/programs/gov/wra/land-transaction-tax.ipynb b/docs/book/programs/gov/wra/land-transaction-tax.ipynb
index 12a05d5c3..80d4549ce 100644
--- a/docs/book/programs/gov/wra/land-transaction-tax.ipynb
+++ b/docs/book/programs/gov/wra/land-transaction-tax.ipynb
@@ -1153,9 +1153,7 @@
" color=\"Label\",\n",
" title=\"Land Transaction Tax (LTT) rates over property price thresholds\",\n",
" )\n",
- " .update_layout(\n",
- " yaxis_tickformat=\",.0%\", xaxis_tickprefix=\"£\", legend_title=\"\"\n",
- " )\n",
+ " .update_layout(yaxis_tickformat=\",.0%\", xaxis_tickprefix=\"£\", legend_title=\"\")\n",
" .update_traces(line_shape=\"hv\")\n",
")\n",
"fig = format_fig(fig)\n",
diff --git a/docs/book/usage/getting-started.ipynb b/docs/book/usage/getting-started.ipynb
deleted file mode 100644
index 5a4b74188..000000000
--- a/docs/book/usage/getting-started.ipynb
+++ /dev/null
@@ -1,265 +0,0 @@
-{
- "cells": [
- {
- "attachments": {},
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Getting started\n",
- "\n",
- "PolicyEngine has two main use-cases:\n",
- "\n",
- "* I want to simulate policy over specific households.\n",
- "* I want to run microsimulation analyses over large datasets.\n",
- "\n",
- "Anyone can do the former in a few minutes of setup, but the UK's large household surveys are only available to academics, researchers and nonprofits, so getting set up takes a bit longer (and you should [get in touch](https://policyengine.org/uk/contact) so we can make it as fast as possible).\n",
- "\n",
- "If you can, please use Google Colab. It's free, and more importantly, it enables everyone to use the same computing environment, saving all the trouble of fiddling around with bad Python installations, etc. It means that the below snippet is all you need to use the full microsimulation model:\n",
- "\n",
- "```python\n",
- "!export HUGGING_FACE_TOKEN=\n",
- "!pip install policyengine-uk\n",
- "```\n",
- "\n",
- "## Household-level analysis\n",
- "\n",
- "The PolicyEngine UK Python package can be installed just like any other. Here's an example below.\n",
- "\n",
- "First, we need to install the package (please make sure you're using Python >=3.7):\n",
- "\n",
- "```bash\n",
- "pip install policyengine-uk\n",
- "```\n",
- "\n",
- "### Simulating current law\n",
- "\n",
- "This example shows how to define a situation (you need to define the people, benefit units and households, and their variables in specific time periods) and simulate the current tax-benefit system."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "/Users/nikhilwoodruff/policyengine/policyengine-uk/.venv/lib/python3.11/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
- " from .autonotebook import tqdm as notebook_tqdm\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "array([3486.], dtype=float32)"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "# The `Simulation` class is the most important class in PolicyEngine- it runs the actual simulation.\n",
- "\n",
- "from policyengine_uk import Simulation\n",
- "\n",
- "situation = {\n",
- " \"people\": {\n",
- " \"person\": {\n",
- " \"age\": {2025: 30},\n",
- " \"employment_income\": {2025: 30_000},\n",
- " },\n",
- " },\n",
- " \"benunits\": {\n",
- " \"benunit\": {\n",
- " \"members\": [\"person\"],\n",
- " },\n",
- " },\n",
- " \"households\": {\n",
- " \"household\": {\n",
- " \"members\": [\"person\"],\n",
- " }\n",
- " },\n",
- "}\n",
- "\n",
- "simulation = Simulation(situation=situation)\n",
- "\n",
- "simulation.calculate(\"income_tax\", 2025)"
- ]
- },
- {
- "attachments": {},
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Simulating a policy reform\n",
- "\n",
- "Now, let's simulate a policy reform that changes a policy parameter. The [OpenFisca documentation](https://openfisca.org/doc) has some excellent documentation on the syntax here."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Raising the basic rate to 25% would increase this person's income tax by £871.50\n"
- ]
- }
- ],
- "source": [
- "from policyengine_core.model_api import *\n",
- "\n",
- "\n",
- "increase_basic_rate = {\"gov.hmrc.income_tax.rates.uk[0].rate\": 0.25}\n",
- "\n",
- "\n",
- "baseline = Simulation(situation=situation)\n",
- "reformed = Simulation(situation=situation, reform=increase_basic_rate)\n",
- "\n",
- "baseline_income_tax = baseline.calculate(\"income_tax\", 2025)[0]\n",
- "reformed_income_tax = reformed.calculate(\"income_tax\", 2025)[0]\n",
- "\n",
- "print(\n",
- " f\"Raising the basic rate to 25% would increase this person's income tax by £{reformed_income_tax - baseline_income_tax:.2f}\"\n",
- ")"
- ]
- },
- {
- "attachments": {},
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Microsimulation analysis\n",
- "\n",
- "PolicyEngine UK has all the code needed to actually generate the microsimulation datasets from the raw dataset files if you have them, but it's easiest to download our final datasets. To do this, you'll need to do the following:\n",
- "\n",
- "* Make sure you've got a HuggingFace account.\n",
- "* Get a personal access token via the settings.\n",
- "* Set HUGGING_FACE_TOKEN as an environment variable.\n",
- "\n",
- "For example, in a bash shell:\n",
- "\n",
- "```bash\n",
- "export HUGGING_FACE_TOKEN=your_token_here\n",
- "```\n",
- "\n",
- "If you don't want to set it, it'll also just prompt you when you try to access the data.\n",
- "\n",
- "### Getting set up\n",
- "\n",
- "Now the datasets will be downloaded automatically as you select them. Here's an example:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "79.42656804925272"
- ]
- },
- "execution_count": 3,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from policyengine_uk import Microsimulation\n",
- "\n",
- "sim = Microsimulation(\n",
- " dataset=\"hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5\"\n",
- ")\n",
- "\n",
- "# The hf:// points to the private data-\n",
- "# hf:// <- go get the data from huggingface\n",
- "# policyengine/policyengine-uk-data <- the repo\n",
- "# enhanced_frs_2022_23.h5 <- the file in the repo\n",
- "\n",
- "sim.calculate(\"universal_credit\", 2025).sum() / 1e9"
- ]
- },
- {
- "attachments": {},
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Running reform analyses\n",
- "\n",
- "Reforms work in exactly the same way as in the household-level analysis above. Here's the same example reform:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Raising the basic rate to 25% would raise the UK £35.54bn per year\n"
- ]
- }
- ],
- "source": [
- "ENHANCED_FRS = \"hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5\"\n",
- "\n",
- "baseline = Microsimulation(\n",
- " dataset=ENHANCED_FRS\n",
- ") # Enhanced FRS 2022 by default\n",
- "reformed = Microsimulation(dataset=ENHANCED_FRS, reform=increase_basic_rate)\n",
- "\n",
- "revenue = (\n",
- " -(\n",
- " reformed.calculate(\"household_net_income\", 2025)\n",
- " - baseline.calculate(\"household_net_income\", 2025)\n",
- " ).sum()\n",
- " / 1e9\n",
- ")\n",
- "\n",
- "print(\n",
- " f\"Raising the basic rate to 25% would raise the UK £{revenue:.2f}bn per year\"\n",
- ")"
- ]
- },
- {
- "attachments": {},
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The [PolicyEngine web app](https://policyengine.org) actually has a helpful tool for this: if you can generate a household or reform on the app, scroll down in the bottom left to the `Reproduce in Python` section, and you'll see an automatically-generated code snippet to reproduce the same analysis in Python."
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.11"
- },
- "orig_nbformat": 4
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/docs/book/usage/getting-started.md b/docs/book/usage/getting-started.md
new file mode 100644
index 000000000..5f5047b02
--- /dev/null
+++ b/docs/book/usage/getting-started.md
@@ -0,0 +1,181 @@
+# Getting started
+
+PolicyEngine UK helps you analyse the UK tax and benefit system in two main ways: simulating policies for specific households, or running large-scale microsimulation analyses. The first requires just a few minutes of setup, while the second needs access to survey data and takes a bit longer to configure.
+
+If possible, use Google Colab - it's free and ensures everyone uses the same computing environment. The full microsimulation model needs just this snippet:
+
+```python
+!export HUGGING_FACE_TOKEN=
+!pip install policyengine-uk
+```
+
+## Installing PolicyEngine UK
+
+Install the package like any other Python library (requires Python ≥3.7):
+
+```bash
+pip install policyengine-uk
+```
+
+## Your first household simulation
+
+Let's start with a simple example: calculating how much income tax a 30-year-old earning £30,000 would pay.
+
+```python
+from policyengine_uk import Simulation
+
+# Define the household situation
+situation = {
+ "people": {
+ "person": {
+ "age": {2025: 30},
+ "employment_income": {2025: 30_000},
+ },
+ },
+ "benunits": {
+ "benunit": {
+ "members": ["person"],
+ },
+ },
+ "households": {
+ "household": {
+ "members": ["person"],
+ }
+ },
+}
+
+# Create and run the simulation
+simulation = Simulation(situation=situation)
+income_tax = simulation.calculate("income_tax", 2025)
+print(f"Income tax: £{income_tax[0]:.2f}")
+```
+
+This gives us £3,486 in income tax. The structure might look complex at first, but it reflects how the UK tax system works - people belong to benefit units (families for benefit purposes) and households (for housing costs and council tax).
+
+## Analysing a policy reform
+
+Now let's see what happens if we increase the basic rate of income tax from 20% to 25%:
+
+```python
+# Define the policy reform
+increase_basic_rate = {"gov.hmrc.income_tax.rates.uk[0].rate": 0.25}
+
+# Compare baseline and reformed systems
+baseline = Simulation(situation=situation)
+reformed = Simulation(situation=situation, reform=increase_basic_rate)
+
+baseline_tax = baseline.calculate("income_tax", 2025)[0]
+reformed_tax = reformed.calculate("income_tax", 2025)[0]
+
+increase = reformed_tax - baseline_tax
+print(f"Tax increase: £{increase:.2f}")
+```
+
+The reform increases this person's tax by £871.50 per year. Policy parameters use a hierarchical structure - here we're changing the first UK income tax rate (`uk[0].rate`), which is the basic rate.
+
+## Running microsimulation analyses
+
+To analyse policies across the entire population, you'll need survey data. PolicyEngine provides processed datasets via HuggingFace, but you'll need an account and access token:
+
+1. Create a [HuggingFace account](https://huggingface.co)
+2. Generate a personal access token in your settings
+3. Set it as an environment variable:
+
+```bash
+export HUGGING_FACE_TOKEN=your_token_here
+```
+
+### Your first population analysis
+
+Here's how to calculate total Universal Credit spending across the UK:
+
+```python
+from policyengine_uk import Microsimulation
+
+# Load the enhanced Family Resources Survey data
+sim = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
+)
+
+# Calculate total UC spending (in billions)
+total_uc = sim.calculate("universal_credit", 2025).sum() / 1e9
+print(f"Total Universal Credit spending: £{total_uc:.1f}bn")
+```
+
+This shows approximately £79.4bn in Universal Credit spending.
+
+### Estimating revenue from reforms
+
+Let's apply the same basic rate increase to the entire population:
+
+```python
+# Set up baseline and reformed simulations
+DATASET = "hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
+
+baseline = Microsimulation(dataset=DATASET)
+reformed = Microsimulation(dataset=DATASET, reform=increase_basic_rate)
+
+# Calculate the revenue impact
+baseline_income = baseline.calculate("household_net_income", 2025)
+reformed_income = reformed.calculate("household_net_income", 2025)
+
+revenue = -(reformed_income - baseline_income).sum() / 1e9
+print(f"Additional revenue: £{revenue:.1f}bn per year")
+```
+
+The reform would raise approximately £35.5bn annually.
+
+## Working with complex households
+
+Real households often have multiple people, children, and various income sources. Here's a family with two adults and two children:
+
+```python
+family_situation = {
+ "people": {
+ "parent_1": {
+ "age": {2025: 35},
+ "employment_income": {2025: 25_000},
+ },
+ "parent_2": {
+ "age": {2025: 33},
+ "employment_income": {2025: 15_000},
+ },
+ "child_1": {
+ "age": {2025: 8},
+ },
+ "child_2": {
+ "age": {2025: 5},
+ },
+ },
+ "benunits": {
+ "family": {
+ "members": ["parent_1", "parent_2", "child_1", "child_2"],
+ },
+ },
+ "households": {
+ "home": {
+ "members": ["parent_1", "parent_2", "child_1", "child_2"],
+ "housing_costs": {2025: 9600}, # Annual rent
+ }
+ },
+}
+
+family_sim = Simulation(situation=family_situation)
+
+# Calculate various benefits and taxes
+child_benefit = family_sim.calculate("child_benefit", 2025)[0]
+universal_credit = family_sim.calculate("universal_credit", 2025)[0]
+total_tax = family_sim.calculate("income_tax", 2025).sum()
+
+print(f"Child benefit: £{child_benefit:.2f}")
+print(f"Universal Credit: £{universal_credit:.2f}")
+print(f"Total income tax: £{total_tax:.2f}")
+```
+
+## Next steps
+
+You're now ready to explore PolicyEngine UK further. The [PolicyEngine web app](https://policyengine.org) includes a 'Reproduce in Python' section at the bottom of each page, which generates code snippets for any household or reform you create there.
+
+For more detailed guidance, see:
+- [Simulations](simulations.md) - comprehensive guide to running simulations
+- [Scenarios](scenarios.md) - creating and combining policy reforms
\ No newline at end of file
diff --git a/docs/book/usage/scenarios.md b/docs/book/usage/scenarios.md
index cb9fade76..c9a0aa7fd 100644
--- a/docs/book/usage/scenarios.md
+++ b/docs/book/usage/scenarios.md
@@ -1,93 +1,545 @@
-## Scenario
+# Creating policy scenarios
-The `Scenario` class represents a configuration for policy simulations, allowing you to define parameter changes and custom modifications to apply to simulations.
+The `Scenario` class lets you define policy reforms and apply them to simulations. This guide shows you how to create everything from simple parameter changes to complex reform packages, and how to combine different policy ideas together.
-### Basic usage
+## Your first policy scenario
+
+Let's start by creating a scenario that reduces the basic rate of income tax from 20% to 15%:
```python
from policyengine_uk import Scenario, Simulation
-# Create a scenario with parameter changes
-scenario = Scenario(
- parameter_changes={
- "gov.hmrc.income_tax.rates.uk[0].rate": 0.15, # Change basic rate to 15%
- "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 400
+# Create a scenario with one policy change
+scenario = Scenario(parameter_changes={
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.15
+})
+
+# Apply it to a simulation
+situation = {
+ "people": {
+ "person": {
+ "age": {2025: 35},
+ "employment_income": {2025: 40_000}
+ }
+ },
+ "benunits": {"benunit": {"members": ["person"]}},
+ "households": {"household": {"members": ["person"]}}
+}
+
+sim = Simulation(situation=situation, scenario=scenario)
+income_tax = sim.calculate("income_tax", 2025).mean()
+print(f"Income tax under 15% basic rate: £{income_tax:.2f}")
+```
+
+Under this scenario, this person would pay £2,486 instead of the current £4,486 - a reduction of £2,000 per year due to the lower tax rate.
+
+## Understanding parameter paths
+
+PolicyEngine UK organises policy parameters in a hierarchical structure. Each parameter has a path that describes where it sits in the system:
+
+```python
+# Income tax parameters
+"gov.hmrc.income_tax.rates.uk[0].rate" # Basic rate (20%)
+"gov.hmrc.income_tax.rates.uk[1].rate" # Higher rate (40%)
+"gov.hmrc.income_tax.personal_allowance" # Personal allowance
+"gov.hmrc.income_tax.rates.uk[0].threshold" # Basic rate threshold
+
+# Universal Credit parameters
+"gov.dwp.universal_credit.standard_allowance.single.OVER_25" # Single person allowance
+"gov.dwp.universal_credit.elements.housing.max_monthly_cap" # Housing cost cap
+"gov.dwp.universal_credit.means_test.income_disregard" # Work allowance
+
+# Child benefits
+"gov.hmrc.child_benefit.rates.first_child" # First child rate
+"gov.hmrc.child_benefit.rates.additional_child" # Additional children rate
+```
+
+You can find parameter paths by exploring the PolicyEngine web app or looking in the `parameters/` directory of the codebase.
+
+## Creating comprehensive reform packages
+
+Real policy proposals often involve multiple changes. Here's how to create a comprehensive reform:
+
+```python
+# A comprehensive package that increases Universal Credit support while adjusting income tax
+# This represents the kind of coordinated policy change you might see in a budget
+welfare_reform = Scenario(parameter_changes={
+ # Increase UC standard allowances
+ "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 500,
+ "gov.dwp.universal_credit.standard_allowance.single.UNDER_25": 400,
+ "gov.dwp.universal_credit.standard_allowance.couple": 700,
+
+ # Reduce UC taper rate (less benefit withdrawn per pound earned)
+ "gov.dwp.universal_credit.means_test.reduction_rate": 0.55,
+
+ # Increase income tax personal allowance
+ "gov.hmrc.income_tax.personal_allowance": 15_000,
+
+ # Reduce basic rate slightly to help fund UC increases
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.22
+})
+
+# Test on a working family
+family_situation = {
+ "people": {
+ "parent": {"age": {2025: 30}, "employment_income": {2025: 20_000}},
+ "child": {"age": {2025: 5}}
+ },
+ "benunits": {"family": {"members": ["parent", "child"]}},
+ "households": {
+ "home": {
+ "members": ["parent", "child"],
+ "housing_costs": {2025: 7200} # Annual housing costs
+ }
}
-)
+}
+
+reformed_sim = Simulation(situation=family_situation, scenario=welfare_reform)
+
+# Calculate the impact of this comprehensive package on the family
+uc_amount = reformed_sim.calculate("universal_credit", 2025).mean()
+income_tax = reformed_sim.calculate("income_tax", 2025).mean()
+net_income = reformed_sim.calculate("household_net_income", 2025).mean()
+
+print(f"Universal Credit: £{uc_amount:.2f} per month")
+print(f"Income tax: £{income_tax:.2f} per year")
+print(f"Monthly net income: £{net_income/12:.2f}")
+```
+
+## Combining scenarios
-# Apply to a simulation
-sim = Simulation(scenario=scenario)
+You can combine different scenarios using the `+` operator, which is useful for testing different combinations of policies:
+
+```python
+# Create separate scenarios for different policy areas
+tax_scenario = Scenario(parameter_changes={
+ "gov.hmrc.income_tax.personal_allowance": 15_000,
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.18
+})
+
+benefits_scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 450,
+ "gov.dwp.universal_credit.means_test.reduction_rate": 0.50
+})
+
+childcare_scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.elements.childcare.max_proportion": 0.90
+})
+
+# Combine them in different ways
+tax_and_benefits = tax_scenario + benefits_scenario
+full_package = tax_scenario + benefits_scenario + childcare_scenario
+
+# Test each combination to see how different policy areas interact
+# This shows how you can build up complex policies piece by piece
+for name, scenario in [("Tax only", tax_scenario), ("Benefits only", benefits_scenario),
+ ("Tax + benefits", tax_and_benefits), ("Full package", full_package)]:
+ sim = Simulation(situation=family_situation, scenario=scenario)
+ net_income = sim.calculate("household_net_income", 2025).mean()
+ print(f"{name}: £{net_income/12:.2f} per month")
```
-### Creating scenarios from reforms
+## Defining structural scenarios
+
+Beyond simple parameter changes, you sometimes need scenarios that make structural modifications to how the tax-benefit system works. Let's walk through how to create these, using examples similar to those in PolicyEngine UK's scenarios module.
+
+### Creating a two-child limit repeal scenario
+
+The two-child limit restricts Universal Credit child elements to the first two children. Here's how to build a scenario that removes this restriction:
```python
+import numpy as np
from policyengine_uk import Scenario
-from policyengine_uk.reforms import BasicIncomeReform
-# From a Reform class
-scenario = Scenario.from_reform(BasicIncomeReform)
+# The two-child limit is controlled by this parameter
+# Setting it to infinity effectively removes the limit
+repeal_two_child_limit = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.elements.child.limit.child_count": {
+ "year:2026:10": np.inf # From October 2026 onwards, no limit
+ }
+})
+
+# Test this on a family with three children
+large_family = {
+ "people": {
+ "parent": {"age": {2026: 35}, "employment_income": {2026: 18_000}},
+ "child1": {"age": {2026: 12}},
+ "child2": {"age": {2026: 8}},
+ "child3": {"age": {2026: 4}} # Third child - currently gets no UC support
+ },
+ "benunits": {"family": {"members": ["parent", "child1", "child2", "child3"]}},
+ "households": {
+ "home": {
+ "members": ["parent", "child1", "child2", "child3"],
+ "housing_costs": {2026: 10200} # Annual housing costs
+ }
+ }
+}
+
+# Compare the impact
+baseline_sim = Simulation(situation=large_family)
+reformed_sim = Simulation(situation=large_family, scenario=repeal_two_child_limit)
+
+baseline_uc = baseline_sim.calculate("universal_credit", 2026).mean()
+reformed_uc = reformed_sim.calculate("universal_credit", 2026).mean()
+
+increase = reformed_uc - baseline_uc
+print(f"UC increase from third child: £{increase:.2f} per month")
+```
+
+### Building a benefit cap reindexing scenario
+
+The benefit cap hasn't risen with inflation since 2016. Here's how to create a scenario that automatically indexes it to CPI:
+
+```python
+from policyengine_uk import Scenario, Simulation
+from policyengine_core.parameters import Parameter
+
+def reindex_benefit_cap_to_cpi(simulation: Simulation):
+ """Modify simulation to index benefit cap parameters to CPI inflation"""
+ # Reset the parameter system to make changes
+ simulation.tax_benefit_system.reset_parameters()
+
+ # Find all benefit cap parameters in the system
+ params = simulation.tax_benefit_system.parameters.gov.dwp.benefit_cap.get_descendants()
+
+ # Filter to only the actual parameter values (leaf nodes)
+ cap_parameters = [param for param in params if isinstance(param, Parameter)]
+
+ for parameter in cap_parameters:
+ # Remove any values after 2025 (current frozen values)
+ parameter.values_list = [
+ entry for entry in parameter.values_list
+ if entry.instant_str < "2026-01-01"
+ ]
+
+ # Set the parameter to follow CPI uprating from 2026
+ parameter.metadata.update(uprating="gov.benefit_uprating_cpi")
+
+ # Reprocess the parameters to apply changes
+ simulation.tax_benefit_system.process_parameters()
-# From a dictionary of parameter changes
-scenario = Scenario.from_reform({
- "gov.hmrc.income_tax.rates.uk[0].rate": 0.25
+# Create the scenario using a simulation modifier
+reindex_benefit_cap = Scenario(simulation_modifier=reindex_benefit_cap_to_cpi)
+
+# Test on a family that might hit the benefit cap
+benefit_cap_family = {
+ "people": {
+ "parent1": {"age": {2026: 30}, "employment_income": {2026: 0}},
+ "parent2": {"age": {2026: 28}, "employment_income": {2026: 0}},
+ "child1": {"age": {2026: 6}},
+ "child2": {"age": {2026: 3}}
+ },
+ "benunits": {"family": {"members": ["parent1", "parent2", "child1", "child2"]}},
+ "households": {
+ "home": {
+ "members": ["parent1", "parent2", "child1", "child2"],
+ "housing_costs": {2026: 14400} # High annual housing costs
+ }
+ }
+}
+
+baseline_sim = Simulation(situation=benefit_cap_family)
+reformed_sim = Simulation(situation=benefit_cap_family, scenario=reindex_benefit_cap)
+
+# Compare the benefit cap levels
+baseline_cap = baseline_sim.calculate("benefit_cap", 2026).mean()
+reformed_cap = reformed_sim.calculate("benefit_cap", 2026).mean()
+
+print(f"Benefit cap - frozen: £{baseline_cap:.0f}/year, indexed: £{reformed_cap:.0f}/year")
+```
+
+## Advanced scenario techniques
+
+### Time-varying parameters
+
+Some reforms phase in over time:
+
+```python
+# A scenario that gradually increases the personal allowance over four years
+# This kind of phasing helps manage fiscal costs and economic adjustment
+phased_scenario = Scenario(parameter_changes={
+ "gov.hmrc.income_tax.personal_allowance": {
+ "2025": 13_000,
+ "2026": 14_000,
+ "2027": 15_000,
+ "2028": 16_000
+ }
})
+
+# Test this on a middle-income earner to see the progressive impact
+situation = {
+ "people": {"person": {"age": {2025: 30}, "employment_income": {2025: 35_000}}},
+ "benunits": {"benunit": {"members": ["person"]}},
+ "households": {"household": {"members": ["person"]}}
+}
+
+sim = Simulation(situation=situation, scenario=phased_scenario)
+
+# Show how the tax burden decreases year by year
+for year in [2025, 2026, 2027, 2028]:
+ allowance = sim.calculate("income_tax_personal_allowance", year).mean()
+ tax = sim.calculate("income_tax", year).mean()
+ print(f"{year}: Personal allowance £{allowance:.0f}, annual tax £{tax:.2f}")
```
-### Combining scenarios
+### Creating complex scenarios with simulation modifiers
-Scenarios can be combined using the `+` operator:
+Some policy changes require modifying individual records rather than just changing parameters. Here's how to build a scenario that phases out PIP payments for some claimants over time:
```python
-# Create two scenarios
-tax_scenario = Scenario(
- parameter_changes={"gov.hmrc.income_tax.rates.uk[0].rate": 0.15}
-)
+import numpy as np
+from policyengine_uk import Scenario, Simulation
-benefit_scenario = Scenario(
- parameter_changes={"gov.dwp.universal_credit.standard_allowance.single.OVER_25": 400}
-)
+def phase_out_pip_gradually(sim: Simulation):
+ """Gradually phase out PIP for a proportion of claimants between 2025-2029"""
+ # Set random seed for reproducible results
+ np.random.seed(42)
+
+ # Create random assignment for each person
+ pip_phase_out_seed = np.random.random(len(sim.calculate("person_id")))
+
+ # Define the phase-out period
+ start_year = 2025
+ end_year = 2029
+
+ # Apply phase-out year by year
+ for year in range(start_year, end_year + 1):
+ # Get current PIP payments
+ current_pip = sim.calculate("pip", year)
+
+ # Calculate how far through the phase-out we are
+ phase_progress = (year - start_year) / (end_year - start_year)
+
+ # 25% of claimants lose PIP, gradually over the period
+ # In year 1, nobody loses it; by final year, 25% have lost it completely
+ affected_threshold = 0.25 * phase_progress
+
+ # Set PIP to zero for affected claimants
+ current_pip[pip_phase_out_seed < affected_threshold] = 0
+
+ # Apply the modified values back to the simulation
+ sim.set_input("pip", year, current_pip)
+
+ return sim
+
+# Create the scenario
+pip_phase_out = Scenario(simulation_modifier=phase_out_pip_gradually)
+
+# Test on someone who receives PIP
+pip_recipient = {
+ "people": {
+ "person": {
+ "age": {2025: 45},
+ "pip": {2025: 150}, # Weekly PIP payment
+ "employment_income": {2025: 8_000}
+ }
+ },
+ "benunits": {"benunit": {"members": ["person"]}},
+ "households": {"household": {"members": ["person"]}}
+}
-# Combine them
-combined_scenario = tax_scenario + benefit_scenario
+# Compare baseline and phase-out scenarios
+baseline_sim = Simulation(situation=pip_recipient)
+reformed_sim = Simulation(situation=pip_recipient, scenario=pip_phase_out)
+
+# Show how PIP changes over the phase-out period
+for year in [2025, 2027, 2029]:
+ baseline_pip = baseline_sim.calculate("pip", year).mean()
+ reformed_pip = reformed_sim.calculate("pip", year).mean()
+ print(f"{year}: Baseline £{baseline_pip:.0f}/week, with phase-out £{reformed_pip:.0f}/week")
```
-### Custom simulation modifiers
+### Building Universal Credit scenarios with dynamic changes
-For modifications beyond parameter changes:
+Some scenarios need to make changes that depend on the simulation's own data. Here's how to create a UC scenario that adjusts payments based on claimant characteristics:
```python
-def add_new_benefit(simulation):
- # Custom logic to add a new benefit to the simulation
- simulation.add_variable(MyNewBenefit)
+from policyengine_uk import Scenario, Microsimulation
+import numpy as np
-scenario = Scenario(
- simulation_modifier=add_new_benefit
-)
+def modify_uc_for_new_claimants(sim: Microsimulation):
+ """Reduce health elements for new UC claimants while increasing standard allowances"""
+ # Access the parameter system to check if reforms are active
+ rebalancing_params = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing
+
+ # Create random assignment to simulate new vs existing claimants
+ np.random.seed(42)
+ uc_seed = np.random.random(len(sim.calculate("benunit_id")))
+
+ # Proportion of claimants who are "new" each year (based on real data)
+ new_claimant_rates = {
+ 2025: 0.00, # No change in 2025
+ 2026: 0.11, # 11% are new claimants
+ 2027: 0.13,
+ 2028: 0.16,
+ 2029: 0.22
+ }
+
+ # Apply changes year by year
+ for year in range(2026, 2030):
+ if not rebalancing_params.active(year):
+ continue # Skip if reforms aren't active this year
+
+ # Identify new claimants
+ is_new_claimant = uc_seed < new_claimant_rates[year]
+
+ # Modify health element for new claimants
+ current_health_element = sim.calculate("uc_LCWRA_element", year)
+ new_health_amount = rebalancing_params.new_claimant_health_element(year) * 12
+
+ # Set new amount for new claimants who get health element
+ current_health_element[
+ (current_health_element > 0) & is_new_claimant
+ ] = new_health_amount
+
+ sim.set_input("uc_LCWRA_element", year, current_health_element)
+
+ # Increase standard allowances for everyone
+ uplift_rate = rebalancing_params.standard_allowance_uplift(year)
+ previous_allowance = sim.calculate("uc_standard_allowance", year - 1)
+ new_allowance = previous_allowance * (1 + uplift_rate)
+ sim.set_input("uc_standard_allowance", year, new_allowance)
+
+# Create the UC rebalancing scenario
+uc_rebalancing = Scenario(simulation_modifier=modify_uc_for_new_claimants)
+
+# This scenario needs the rebalancing parameters to be active
+uc_rebalancing_active = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.rebalancing.active": True
+}) + uc_rebalancing
+
+# Test on a typical UC claimant
+uc_claimant = {
+ "people": {
+ "person": {
+ "age": {2026: 35},
+ "employment_income": {2026: 8_000},
+ "uc_LCWRA_element": {2026: 390} # Monthly health element
+ }
+ },
+ "benunits": {"benunit": {"members": ["person"]}},
+ "households": {
+ "home": {
+ "members": ["person"],
+ "housing_costs": {2026: 7200}
+ }
+ }
+}
+
+baseline_sim = Simulation(situation=uc_claimant)
+reformed_sim = Simulation(situation=uc_claimant, scenario=uc_rebalancing_active)
+
+# Compare UC components
+for year in [2026, 2028]:
+ baseline_uc = baseline_sim.calculate("universal_credit", year).mean()
+ reformed_uc = reformed_sim.calculate("universal_credit", year).mean()
+ print(f"{year}: Baseline UC £{baseline_uc:.0f}/month, reformed £{reformed_uc:.0f}/month")
```
-### API reference
+## Analysing scenarios at population level
-#### Constructor
+Use microsimulation to understand the broader impact of your scenarios:
```python
-Scenario(
- parameter_changes: Optional[Dict[str, Union[int, float, bool, Dict]]] = None,
- simulation_modifier: Optional[Callable[[Simulation], None]] = None
+from policyengine_uk import Microsimulation
+
+# Create a major scenario package combining tax cuts and benefit increases
+# This represents a significant fiscal intervention
+major_scenario = Scenario(parameter_changes={
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.15, # Cut basic rate from 20% to 15%
+ "gov.hmrc.income_tax.personal_allowance": 16_000, # Raise allowance significantly
+ "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 600, # Increase UC by £100/month
+ "gov.dwp.universal_credit.means_test.reduction_rate": 0.45 # Reduce taper from 55% to 45%
+})
+
+# Compare baseline and reform
+baseline = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
+)
+reformed = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5",
+ scenario=major_scenario
)
+
+# Calculate the fiscal impact of this major intervention
+# Tax cuts reduce revenue, benefit increases raise spending
+baseline_revenue = baseline.calculate("income_tax", 2025).sum() / 1e9
+reformed_revenue = reformed.calculate("income_tax", 2025).sum() / 1e9
+tax_cost = baseline_revenue - reformed_revenue
+
+baseline_uc = baseline.calculate("universal_credit", 2025).sum() / 1e9
+reformed_uc = reformed.calculate("universal_credit", 2025).sum() / 1e9
+uc_cost = reformed_uc - baseline_uc
+
+total_cost = tax_cost + uc_cost
+
+print(f"Income tax revenue loss: £{tax_cost:.1f}bn")
+print(f"UC spending increase: £{uc_cost:.1f}bn")
+print(f"Total fiscal cost: £{total_cost:.1f}bn")
+
+# Calculate distributional impact
+baseline_poverty = baseline.calculate("in_poverty", 2025).mean()
+reformed_poverty = reformed.calculate("in_poverty", 2025).mean()
+poverty_reduction = baseline_poverty - reformed_poverty
+
+print(f"Poverty rate reduction: {poverty_reduction:.1%}")
```
-**Parameters:**
-- `parameter_changes`: Dictionary mapping parameter paths to new values
-- `simulation_modifier`: Function that takes a Simulation and modifies it
+## Testing scenarios step by step
-#### Methods
+When building complex scenarios, test each component separately:
+
+```python
+# Build up a complex scenario incrementally to understand each component's impact
+# Starting with an empty scenario as our baseline
+base_scenario = Scenario(parameter_changes={})
+
+# Add income tax changes
+with_tax_changes = base_scenario + Scenario(parameter_changes={
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.18,
+ "gov.hmrc.income_tax.personal_allowance": 14_000
+})
+
+# Add UC changes
+with_uc_changes = with_tax_changes + Scenario(parameter_changes={
+ "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 500,
+ "gov.dwp.universal_credit.means_test.reduction_rate": 0.50
+})
+
+# Add child benefit changes
+full_scenario = with_uc_changes + Scenario(parameter_changes={
+ "gov.hmrc.child_benefit.rates.first_child": 25,
+ "gov.hmrc.child_benefit.rates.additional_child": 18
+})
+
+# Test the impact of each addition
+test_situation = {
+ "people": {
+ "parent": {"age": {2025: 30}, "employment_income": {2025: 25_000}},
+ "child": {"age": {2025: 6}}
+ },
+ "benunits": {"family": {"members": ["parent", "child"]}},
+ "households": {"home": {"members": ["parent", "child"], "housing_costs": {2025: 8400}}} # Annual housing costs
+}
+
+# Test each incremental addition to see cumulative effects
+scenarios = [("Baseline", base_scenario), ("With tax changes", with_tax_changes),
+ ("With UC changes", with_uc_changes), ("Full package", full_scenario)]
+
+for name, scenario in scenarios:
+ sim = Simulation(situation=test_situation, scenario=scenario)
+ net_income = sim.calculate("household_net_income", 2025).mean()
+ print(f"{name}: £{net_income/12:.2f} per month")
+```
-##### `from_reform(reform: Union[tuple, dict, Type[Reform]]) -> Scenario`
-Create a Scenario from various reform representations.
+## Practical tips for scenario design
-##### `apply(simulation: Simulation) -> None`
-Apply the scenario to a simulation instance.
+- Start simple and build complexity gradually
+- Test scenarios on multiple household types to understand different impacts
+- Use meaningful parameter names and document your choices
+- Combine related changes logically (tax reforms with tax reforms, benefit changes with benefit changes)
+- Always check the fiscal impact of major reforms
+- Consider unintended interactions between different policy areas
-##### `__add__(other: Scenario) -> Scenario`
-Combine two scenarios. Parameter changes are merged (with the second scenario taking precedence in conflicts), and simulation modifiers are chained.
+These techniques let you model complex policy changes that go beyond simple parameter adjustments. Simulation modifiers give you complete control over how the tax-benefit system works, allowing you to implement everything from gradual phase-outs to dynamic eligibility changes. The key is to understand the underlying data structures and use them thoughtfully to represent real policy proposals.
\ No newline at end of file
diff --git a/docs/book/usage/simulations.md b/docs/book/usage/simulations.md
index e937ac786..2f1916e38 100644
--- a/docs/book/usage/simulations.md
+++ b/docs/book/usage/simulations.md
@@ -1,18 +1,24 @@
-## Simulation
+# Running simulations
-The `Simulation` class calculates tax and benefit outcomes for individuals and households in the UK tax-benefit system.
+The `Simulation` class is the heart of PolicyEngine UK - it calculates tax and benefit outcomes for people and households. This guide walks you through everything from basic calculations to advanced microsimulation analysis.
-### Basic usage
+## Creating your first simulation
+
+Every simulation starts with a situation that describes the people, benefit units, and households you want to analyse:
```python
from policyengine_uk import Simulation
-# Create from a situation dictionary
-sim = Simulation(situation={
+situation = {
"people": {
"person_1": {
- "age": {"2023": 30},
- "employment_income": {"2023": 30000}
+ "age": {2025: 30},
+ "employment_income": {2025: 30_000}
+ }
+ },
+ "benunits": {
+ "benunit_1": {
+ "members": ["person_1"]
}
},
"households": {
@@ -20,89 +26,325 @@ sim = Simulation(situation={
"members": ["person_1"]
}
}
+}
+
+sim = Simulation(situation=situation)
+income_tax = sim.calculate("income_tax", 2025)
+print(f"Income tax: £{income_tax[0]:.2f}")
+```
+
+This creates a simulation for one person and calculates their annual income tax liability. The result shows they would pay £3,486 per year in income tax.
+
+## Understanding the situation structure
+
+The situation dictionary has three main sections:
+
+**People** define individuals with their characteristics:
+```python
+"people": {
+ "sarah": {
+ "age": {2025: 28},
+ "employment_income": {2025: 35_000},
+ "pension_contributions": {2025: 2_800}
+ },
+ "tom": {
+ "age": {2025: 32},
+ "self_employment_income": {2025: 18_000}
+ }
+}
+```
+
+**Benefit units** group people for benefit calculations (typically families):
+```python
+"benunits": {
+ "family": {
+ "members": ["sarah", "tom"]
+ }
+}
+```
+
+**Households** group people who live together:
+```python
+"households": {
+ "home": {
+ "members": ["sarah", "tom"],
+ "housing_costs": {2025: 11400}, # Annual rent
+ "council_tax": {2025: 1_800} # Annual
+ }
+}
+```
+
+## Calculating different variables
+
+PolicyEngine UK can calculate hundreds of variables. Here are some common ones:
+
+```python
+# Taxes
+income_tax = sim.calculate("income_tax", 2025)
+national_insurance = sim.calculate("national_insurance", 2025)
+council_tax = sim.calculate("council_tax", 2025)
+
+# Benefits
+universal_credit = sim.calculate("universal_credit", 2025)
+child_benefit = sim.calculate("child_benefit", 2025)
+state_pension = sim.calculate("state_pension", 2025)
+
+# Summary measures
+net_income = sim.calculate("household_net_income", 2025)
+in_poverty = sim.calculate("in_poverty", 2025)
+marginal_tax_rate = sim.calculate("marginal_tax_rate", 2025)
+```
+
+These calculations return arrays because simulations can handle multiple people, benefit units or households simultaneously. Each variable gives you results at the appropriate level - income tax is calculated per person, Universal Credit per benefit unit, and council tax per household.
+
+## Simulating policy scenarios
+
+To test policy changes, create a scenario and apply it to your simulation. This example shows what happens when we increase Universal Credit's standard allowance for single people over 25 from the current rate to £500 per month:
+
+```python
+from policyengine_uk import Scenario
+
+# Create a scenario that increases UC standard allowance
+scenario = Scenario(parameter_changes={
+ "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 500
})
-# Calculate a variable
-income_tax = sim.calculate("income_tax", "2023")
+# Apply the scenario to create a new simulation
+reformed_sim = Simulation(situation=situation, scenario=scenario)
+
+# Compare the average UC payment between baseline and scenario
+baseline_uc = sim.calculate("universal_credit", 2025).mean()
+reformed_uc = reformed_sim.calculate("universal_credit", 2025).mean()
+
+# Calculate the difference to see the policy impact
+difference = reformed_uc - baseline_uc
+print(f"Average UC increase: £{difference:.2f} per month")
+```
+
+## Working with families and children
+
+Family situations require more detailed setup:
+
+```python
+family_with_children = {
+ "people": {
+ "parent": {
+ "age": {2025: 35},
+ "employment_income": {2025: 28_000}
+ },
+ "partner": {
+ "age": {2025: 33},
+ "employment_income": {2025: 12_000}
+ },
+ "child_1": {
+ "age": {2025: 7}
+ },
+ "child_2": {
+ "age": {2025: 4}
+ }
+ },
+ "benunits": {
+ "family": {
+ "members": ["parent", "partner", "child_1", "child_2"]
+ }
+ },
+ "households": {
+ "home": {
+ "members": ["parent", "partner", "child_1", "child_2"],
+ "housing_costs": {2025: 9600}, # Annual housing costs
+ "childcare_costs": {2025: 7200} # Annual childcare costs
+ }
+ }
+}
+
+family_sim = Simulation(situation=family_with_children)
+
+# Calculate family-specific benefits - these are per benefit unit (family)
+child_benefit = family_sim.calculate("child_benefit", 2025).mean()
+childcare_support = family_sim.calculate("universal_credit_childcare", 2025).mean()
+
+print(f"Child benefit: £{child_benefit:.2f} per month")
+print(f"Childcare support: £{childcare_support:.2f} per month")
```
-### Data sources
+## Using different data sources
-Simulations can be created from various data sources:
+### From pandas DataFrames
-#### DataFrames
+If you have data in a DataFrame, convert it to the right format:
```python
import pandas as pd
+# Your data needs specific column naming
df = pd.DataFrame({
- "person_id__2023": [1, 2, 3],
- "age__2023": [25, 30, 35],
- "employment_income__2023": [20000, 30000, 40000],
- # ... other columns
+ "person_id__2025": [1, 2, 3],
+ "age__2025": [25, 45, 65],
+ "employment_income__2025": [20000, 40000, 0],
+ "state_pension__2025": [0, 0, 180] # Weekly pension
})
-sim = Simulation(dataset=df)
+# Create simulation from the DataFrame
+sim_from_df = Simulation(dataset=df)
+# Calculate total income tax across all people in the dataset
+total_tax = sim_from_df.calculate("income_tax", 2025).sum()
+print(f"Total income tax from all people: £{total_tax:.2f}")
+```
+
+The column naming follows the pattern `variable_name__year` for time-varying variables.
+
+### From survey datasets
+
+For population-level analysis, use survey data:
+
+```python
+from policyengine_uk import Microsimulation
+
+# Load the Enhanced Family Resources Survey
+sim = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
+)
+
+# Calculate population totals using survey weights
+# This gives us an estimate for the entire UK population
+total_income_tax = sim.calculate("income_tax", 2025).sum() / 1e9
+print(f"Total UK income tax revenue: £{total_income_tax:.1f}bn")
```
-#### HuggingFace locations
+## Microsimulation analysis
+
+The `Microsimulation` class extends `Simulation` with survey weights for population estimates:
```python
-# Load from HuggingFace
-sim = Simulation(dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2023_24.h5")
+from policyengine_uk import Microsimulation
+
+# Compare baseline and reform
+baseline = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5"
+)
+
+# Create a scenario that increases the basic rate of income tax to 25%
+scenario = Scenario(parameter_changes={
+ "gov.hmrc.income_tax.rates.uk[0].rate": 0.25
+})
+reformed = Microsimulation(
+ dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5",
+ scenario=scenario
+)
+
+# Calculate revenue impact by comparing household net incomes
+# When net income falls, government revenue increases
+baseline_net = baseline.calculate("household_net_income", 2025)
+reformed_net = reformed.calculate("household_net_income", 2025)
+
+# The negative of the income change gives us the revenue change
+revenue_change = -(reformed_net - baseline_net).sum() / 1e9
+print(f"Additional government revenue: £{revenue_change:.1f}bn")
+
+# Analyse distributional impact by looking at poverty rates
+# in_poverty is a boolean variable, so mean() gives us the poverty rate
+poverty_baseline = baseline.calculate("in_poverty", 2025).mean()
+poverty_reformed = reformed.calculate("in_poverty", 2025).mean()
+
+poverty_change = poverty_reformed - poverty_baseline
+print(f"Poverty rate change: {poverty_change:.1%}")
```
-#### From PolicyEngine-Core Dataset objects
+## Advanced techniques
+
+### Calculating multiple variables efficiently
+
+Instead of calculating variables one by one, you can get multiple results at once using the built-in `calculate_dataframe` method:
```python
-from policyengine_uk.data import EnhancedFRS
+# Get multiple variables in a single DataFrame
+variables = ["employment_income", "income_tax", "universal_credit", "household_net_income"]
+results_df = sim.calculate_dataframe(variables, 2025)
-# Using the Enhanced FRS dataset
-dataset = EnhancedFRS()
-sim = Simulation(dataset=dataset)
+# This gives you a DataFrame with columns for each variable
+print(results_df.head())
+print(f"Average net income: £{results_df['household_net_income'].mean():.2f}")
```
-### Calculating variables
+### Working with different time periods
```python
-# Calculate for all entities
-income_tax = sim.calculate("income_tax", "2023")
+# Multi-year analysis to see how income changes over time
+years = [2023, 2024, 2025]
+income_by_year = {}
+
+for year in years:
+ income_by_year[year] = sim.calculate("household_net_income", year)
-# Calculate with mapping to different entity
-household_income = sim.calculate("employment_income", "2023", map_to="household")
+# Compare average household income across years
+for year in years:
+ avg_income = income_by_year[year].mean()
+ print(f"{year}: £{avg_income/12:.2f} per month")
```
-### Microsimulation
+### Mapping variables to different entities
-The `Microsimulation` class extends `Simulation` with survey weights for population-level analysis.
+Some variables can be calculated at different levels:
```python
-from policyengine_uk import Microsimulation
+# Calculate employment income at different entity levels
+# Individual level - one value per person
+person_income = sim.calculate("employment_income", 2025)
+print(f"Average personal employment income: £{person_income.mean():.2f}")
+
+# Household level - sums all employment income within each household
+household_income = sim.calculate("employment_income", 2025, map_to="household")
+print(f"Average household employment income: £{household_income.mean():.2f}")
+
+# Benefit unit level - sums employment income within each benefit unit (family)
+benunit_income = sim.calculate("employment_income", 2025, map_to="benunit")
+print(f"Average benefit unit employment income: £{benunit_income.mean():.2f}")
+```
+
+### Using custom weights
-# Create from weighted dataset
-sim = Microsimulation(dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2023_24.h5")
+For microsimulation with custom weights:
-# Calculate with weights
-poverty_rate = sim.calculate("in_poverty", "2023").mean()
-print(f"Poverty rate: {poverty_rate:.1%}")
+```python
+# Access and modify survey weights for sensitivity analysis
+weights = sim.get_weights("income_tax", 2025)
+modified_weights = weights * 1.1 # Increase all weights by 10%
-# Get total government revenue
-income_tax_revenue = sim.calculate("income_tax", "2023").sum()
-print(f"Total income tax: £{income_tax_revenue/1e9:.1f}bn")
+# Calculate population total with modified weights
+unweighted_values = sim.calculate("income_tax", 2025, use_weights=False)
+weighted_total = (unweighted_values * modified_weights).sum()
+print(f"Total with 10% higher weights: £{weighted_total/1e9:.1f}bn")
```
-### Working with weights
+## Debugging simulations
+
+When simulations don't behave as expected:
```python
-# Calculate without weights for debugging
-values = sim.calculate("income_tax", "2023", use_weights=False)
+# Check individual components to understand the tax calculation
+# These help you trace through how income tax is calculated step by step
+employment_income = sim.calculate("employment_income", 2025).mean()
+personal_allowance = sim.calculate("personal_allowance", 2025).mean()
+taxable_income = sim.calculate("adjusted_net_income", 2025).mean()
+income_tax = sim.calculate("income_tax", 2025).mean()
-# Access weights directly
-weights = sim.get_weights("income_tax", "2023")
+print(f"Employment income: £{employment_income:.2f}")
+print(f"Personal allowance: £{personal_allowance:.2f}")
+print(f"Taxable income: £{taxable_income:.2f}")
+print(f"Income tax: £{income_tax:.2f}")
-# Calculate multiple variables as weighted DataFrame
-df = sim.calculate_dataframe(
- ["employment_income", "income_tax", "universal_credit"],
- "2023"
-)
+# Verify the household composition was interpreted correctly
+avg_age = sim.calculate("age", 2025).mean()
+children_per_household = sim.calculate("household_count_children", 2025).mean()
+print(f"Average age: {avg_age:.1f} years")
+print(f"Average children per household: {children_per_household:.1f}")
```
+
+## Performance tips
+
+- Use `Microsimulation` for population analysis, `Simulation` for household-level work
+- Calculate multiple variables in batches rather than individually
+- For large datasets, consider calculating subsets first to verify your logic
+- Cache simulation results when running the same calculation multiple times
+
+The simulation system is designed to be flexible and powerful. Start with simple examples and gradually build up to more complex analyses as you become familiar with the structure and capabilities.
\ No newline at end of file
diff --git a/docs/book/validation/validation.ipynb b/docs/book/validation/validation.ipynb
index 2c7e9223e..2e3dc159b 100644
--- a/docs/book/validation/validation.ipynb
+++ b/docs/book/validation/validation.ipynb
@@ -192,7 +192,7 @@
12.168675106094156,
14.564548934461316,
48.475609170684386,
- 164.69818933192443,
+ 164.69818933192445,
211.1923993499099
],
"xaxis": "x",
@@ -376,7 +376,7 @@
12.168675106094156,
14.564548934461316,
48.475609170684386,
- 164.69818933192443,
+ 164.69818933192445,
211.1923993499099
],
"xaxis": "x",
@@ -745,8 +745,8 @@
4.583001415580091,
10.765335944529406,
17.151028316398943,
- 59.902550849244356,
- 181.01423598843414,
+ 59.90255084924435,
+ 181.01423598843417,
255.79033352598896
],
"xaxis": "x",
@@ -1901,9 +1901,7 @@
" return (\n",
" np.array(\n",
" [\n",
- " get_parameter(calibration_parameters, parameter)(\n",
- " f\"{year}-01-01\"\n",
- " )\n",
+ " get_parameter(calibration_parameters, parameter)(f\"{year}-01-01\")\n",
" for year in range(2023, 2026)\n",
" ]\n",
" )\n",
diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml
new file mode 100644
index 000000000..eb5ee6148
--- /dev/null
+++ b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/active.yaml
@@ -0,0 +1,9 @@
+description: Whether the Universal Credit 'rebalancing' reforms are active.
+values:
+ 2025-01-01: true
+metadata:
+ unit: bool
+ label: Universal Credit rebalancing reforms active
+ reference:
+ - title: Universal Credit Bill
+ href: https://bills.parliament.uk/publications/62123/documents/6889
\ No newline at end of file
diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml
new file mode 100644
index 000000000..cf59221af
--- /dev/null
+++ b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/new_claimant_health_element.yaml
@@ -0,0 +1,9 @@
+description: Monthly value of the Universal Credit health element for new claimants from April 2026.
+values:
+ 2026-01-01: 217.26
+metadata:
+ unit: currency-GBP
+ label: Universal Credit new claimant health element amount
+ reference:
+ - title: Universal Credit Bill
+ href: https://bills.parliament.uk/publications/62123/documents/6889#page=16
\ No newline at end of file
diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml
new file mode 100644
index 000000000..1d10817db
--- /dev/null
+++ b/policyengine_uk/parameters/gov/dwp/universal_credit/rebalancing/standard_allowance_uplift.yaml
@@ -0,0 +1,13 @@
+description: Uplift in the Standard Allowance for Universal Credit, in addition to the annual inflationary increase, compared to 2025-26 levels.
+values:
+ 2025-01-01: 0.000
+ 2026-01-01: 0.023
+ 2027-01-01: 0.031
+ 2028-01-01: 0.040
+ 2029-01-01: 0.048
+metadata:
+ unit: /1
+ label: Universal Credit rebalancing standard allowance uplift
+ reference:
+ - title: Universal Credit Bill
+ href: https://bills.parliament.uk/publications/62123/documents/6889
diff --git a/policyengine_uk/reforms/policyengine/adjust_budgets.py b/policyengine_uk/reforms/policyengine/adjust_budgets.py
index 534b5134a..56354ce15 100644
--- a/policyengine_uk/reforms/policyengine/adjust_budgets.py
+++ b/policyengine_uk/reforms/policyengine/adjust_budgets.py
@@ -34,7 +34,6 @@ def adjust_budgets(parameters, period):
class adjust_budgets_reform(Reform):
def apply(self):
-
for reform in reforms:
reform.apply(self)
diff --git a/policyengine_uk/scenarios/__init__.py b/policyengine_uk/scenarios/__init__.py
index 924fbb82b..f3c0a1a6e 100644
--- a/policyengine_uk/scenarios/__init__.py
+++ b/policyengine_uk/scenarios/__init__.py
@@ -1,3 +1,4 @@
from .pip_reform import reform_pip_phase_in
from .reindex_benefit_cap import reindex_benefit_cap
from .repeal_two_child_limit import repeal_two_child_limit
+from .uc_reform import universal_credit_july_2025_reform
diff --git a/policyengine_uk/scenarios/uc_reform.py b/policyengine_uk/scenarios/uc_reform.py
new file mode 100644
index 000000000..363bafc99
--- /dev/null
+++ b/policyengine_uk/scenarios/uc_reform.py
@@ -0,0 +1,48 @@
+from policyengine_uk.model_api import Scenario
+from policyengine_uk import Microsimulation
+import numpy as np
+
+
+def add_universal_credit_reform(sim: Microsimulation):
+ rebalancing = (
+ sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing
+ )
+
+ uc_seed = np.random.random(len(sim.calculate("benunit_id")))
+ p_uc_post_2026_status = {
+ 2025: 0,
+ 2026: 0.11,
+ 2027: 0.13,
+ 2028: 0.16,
+ 2029: 0.22,
+ } # WPI Economics for Trussell Trust based on admin PIP data, 2025
+ new_claimant_health_element = rebalancing.new_claimant_health_element
+ for year in range(2026, 2030):
+ if not rebalancing.active(year):
+ continue
+ is_post_25_claimant = uc_seed < p_uc_post_2026_status[year]
+ current_health_element = sim.calculate("uc_LCWRA_element", year)
+ # Set new claimants to £217.26/month from April 2026 (pre-2026 claimaints keep inflation-linked increases)
+ # https://bills.parliament.uk/publications/62123/documents/6889#page=16
+ current_health_element[
+ (current_health_element > 0) & is_post_25_claimant
+ ] = (
+ new_claimant_health_element(year) * 12
+ ) # Monthly amount * 12
+ sim.set_input("uc_LCWRA_element", year, current_health_element)
+
+ # https://bills.parliament.uk/publications/62123/documents/6889#page=14
+
+ uc_uplift = rebalancing.standard_allowance_uplift
+
+ for year in range(2026, 2030):
+ if not rebalancing.active(year):
+ continue
+ previous_value = sim.calculate("uc_standard_allowance", year - 1)
+ new_value = previous_value * (1 + uc_uplift(year))
+ sim.set_input("uc_standard_allowance", year, new_value)
+
+
+universal_credit_july_2025_reform = Scenario(
+ simulation_modifier=add_universal_credit_reform,
+)
diff --git a/policyengine_uk/simulation.py b/policyengine_uk/simulation.py
index c66aad5d9..37f4a512d 100644
--- a/policyengine_uk/simulation.py
+++ b/policyengine_uk/simulation.py
@@ -109,6 +109,12 @@ def __init__(
self.input_variables = self.get_known_variables()
+ # Universal Credit reform (July 2025). Needs closer integration in the baseline,
+ # but adding here for ease of toggling on/off via the 'active' parameter.
+ from policyengine_uk.scenarios import universal_credit_july_2025_reform
+
+ universal_credit_july_2025_reform.simulation_modifier(self)
+
# Apply structural modifiers
if scenario is not None:
diff --git a/policyengine_uk/tests/microsimulation/reforms_config.yaml b/policyengine_uk/tests/microsimulation/reforms_config.yaml
index c9d1cbc86..c7e0fbd66 100644
--- a/policyengine_uk/tests/microsimulation/reforms_config.yaml
+++ b/policyengine_uk/tests/microsimulation/reforms_config.yaml
@@ -16,7 +16,7 @@ reforms:
parameters:
gov.hmrc.child_benefit.amount.additional: 25
- name: Reduce Universal Credit taper rate to 20%
- expected_impact: -38.4
+ expected_impact: -39.7
parameters:
gov.dwp.universal_credit.means_test.reduction_rate: 0.2
- name: Raise Class 1 main employee NICs rate to 10%
diff --git a/policyengine_uk/utils/create_ahc_deflator.py b/policyengine_uk/utils/create_ahc_deflator.py
index 0d5f65b22..336f7cd8c 100644
--- a/policyengine_uk/utils/create_ahc_deflator.py
+++ b/policyengine_uk/utils/create_ahc_deflator.py
@@ -47,7 +47,6 @@ def create_modified_cpi_forecast(
forecast_start_year=2025,
forecast_end_year=2029,
):
-
cpi_values = get_parameter_values(
cpi_parameter, forecast_start_year, forecast_end_year
)
diff --git a/policyengine_uk/utils/create_triple_lock.py b/policyengine_uk/utils/create_triple_lock.py
index 13ec8d9e9..4bb61f75b 100644
--- a/policyengine_uk/utils/create_triple_lock.py
+++ b/policyengine_uk/utils/create_triple_lock.py
@@ -20,5 +20,5 @@
triple_lock_increase = max(earnings_increase, cpi_increase, 1.025)
lock_value *= triple_lock_increase
print(
- f" {year}-01-01: {lock_value:.3f} # Earnings increase FY{year - 1}/{year - 2} = {earnings_increase-1:.1%}, CPI increase FY{year - 1}/{year - 2} = {cpi_increase-1:.1%}"
+ f" {year}-01-01: {lock_value:.3f} # Earnings increase FY{year - 1}/{year - 2} = {earnings_increase - 1:.1%}, CPI increase FY{year - 1}/{year - 2} = {cpi_increase - 1:.1%}"
)
diff --git a/policyengine_uk/utils/dependencies.py b/policyengine_uk/utils/dependencies.py
index 840995209..ed3a5e022 100644
--- a/policyengine_uk/utils/dependencies.py
+++ b/policyengine_uk/utils/dependencies.py
@@ -194,7 +194,6 @@ def add_fonts():
def format_fig(fig):
-
# PolicyEngine style (roboto mono for numbers, roboto serif for text), dark grey for negative, blue for positive, spacing, etc.
# Set layout properties for a clean, professional look
diff --git a/policyengine_uk/utils/scenario.py b/policyengine_uk/utils/scenario.py
index 944a6a7aa..64f4d3787 100644
--- a/policyengine_uk/utils/scenario.py
+++ b/policyengine_uk/utils/scenario.py
@@ -165,7 +165,9 @@ def apply(self, simulation: Simulation) -> None:
else:
# Simple parameter change
simulation.tax_benefit_system.parameters.update(
- path, period=None, value=value # Apply to all periods
+ path,
+ period=None,
+ value=value, # Apply to all periods
)
# Then apply simulation modifier
diff --git a/policyengine_uk/variables/contrib/policyengine/education_budget_change.py b/policyengine_uk/variables/contrib/policyengine/education_budget_change.py
index d1e3c28a1..40dc89305 100644
--- a/policyengine_uk/variables/contrib/policyengine/education_budget_change.py
+++ b/policyengine_uk/variables/contrib/policyengine/education_budget_change.py
@@ -8,7 +8,6 @@ class education_budget_change(Variable):
value_type = float
def formula(household, period, parameters):
-
budget_increase = (
parameters(period).gov.contrib.policyengine.budget.education * 1e9
)
diff --git a/policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py b/policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py
index b6f6649c8..20758220c 100644
--- a/policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py
+++ b/policyengine_uk/variables/contrib/policyengine/other_public_spending_budget_change.py
@@ -8,7 +8,6 @@ class other_public_spending_budget_change(Variable):
value_type = float
def formula(household, period, parameters):
-
budget_increase = (
parameters(
period
diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py
index 47be60f5e..6e7fed0fe 100644
--- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py
+++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/targeted_childcare_entitlement_eligible.py
@@ -9,7 +9,6 @@ class targeted_childcare_entitlement_eligible(Variable):
defined_for = "would_claim_targeted_childcare"
def formula(benunit, period, parameters):
-
# Check if household is in England
country = benunit.household("country", period)
in_england = country == country.possible_values.ENGLAND