From 169f14772fb73af0d379b669d6c2a87c01f18bf4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 25 Jul 2026 09:38:49 -0400 Subject: [PATCH] Format the docs code blocks with current ruff CI runs `uvx ruff format --check .`, which resolves the current ruff release while contributors format against the pinned dev version. Four docs pages have drifted out of format, so Lint fails on main and on every branch cut from it. No prose or logic changes; only the Python blocks inside these pages. Co-Authored-By: Claude Opus 5 --- docs/book/policy/uc-rebalancing.md | 34 ++-- docs/book/usage/dynamics.md | 66 ++++--- docs/book/usage/scenarios.md | 308 ++++++++++++++++------------- docs/book/usage/simulations.md | 136 ++++++------- 4 files changed, 290 insertions(+), 254 deletions(-) diff --git a/docs/book/policy/uc-rebalancing.md b/docs/book/policy/uc-rebalancing.md index 0101818aab..5082996119 100644 --- a/docs/book/policy/uc-rebalancing.md +++ b/docs/book/policy/uc-rebalancing.md @@ -65,9 +65,11 @@ You can use these reforms in your own analysis by creating a `Simulation` with p from policyengine_uk import Simulation, Scenario # Disable the reforms from 2026 onwards -scenario = Scenario(parameter_changes={ - "gov.dwp.universal_credit.rebalancing.active": False, -}) +scenario = Scenario( + parameter_changes={ + "gov.dwp.universal_credit.rebalancing.active": False, + } +) sim = Simulation(scenario=scenario) ``` @@ -78,14 +80,16 @@ sim = Simulation(scenario=scenario) 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 +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) ``` @@ -96,11 +100,13 @@ sim = Simulation(scenario=scenario) 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 +scenario = Scenario( + parameter_changes={ + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "2026-01-01": 250.00 + } } -}) +) sim = Simulation(scenario=scenario) ``` diff --git a/docs/book/usage/dynamics.md b/docs/book/usage/dynamics.md index dd509e1c15..21b9693448 100644 --- a/docs/book/usage/dynamics.md +++ b/docs/book/usage/dynamics.md @@ -21,13 +21,17 @@ from policyengine_uk import Microsimulation from policyengine_uk.model_api import Scenario # Define baseline and reform scenarios -baseline_scenario = Scenario(parameter_changes={ - "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.12, -}) +baseline_scenario = Scenario( + parameter_changes={ + "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.12, + } +) -reform_scenario = Scenario(parameter_changes={ - "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.10, -}) +reform_scenario = Scenario( + parameter_changes={ + "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.10, + } +) # Create microsimulations baseline = Microsimulation(scenario=baseline_scenario) @@ -38,7 +42,9 @@ reformed.baseline.apply_parameter_changes(baseline_scenario.parameter_changes) dynamics = reformed.apply_dynamics(2025) # View FTE impacts -print(f"Substitution effect FTEs: {dynamics.fte_impacts.substitution_response_ftes:,.0f}") +print( + f"Substitution effect FTEs: {dynamics.fte_impacts.substitution_response_ftes:,.0f}" +) print(f"Income effect FTEs: {dynamics.fte_impacts.income_response_ftes:,.0f}") print(f"Participation FTEs: {dynamics.fte_impacts.participation_response_ftes:,.0f}") print(f"Total FTE change: {dynamics.fte_impacts.ftes:,.0f}") @@ -76,19 +82,23 @@ Combine dynamics with standard microsimulation analysis to understand how behavi progression = dynamics.progression # Add demographic information -progression['decile'] = reformed.calculate("household_income_decile", 2025, map_to="person") -progression['age_group'] = pd.cut( - reformed.calculate("age", 2025), +progression["decile"] = reformed.calculate( + "household_income_decile", 2025, map_to="person" +) +progression["age_group"] = pd.cut( + reformed.calculate("age", 2025), bins=[0, 30, 50, 65, 100], - labels=["Under 30", "30-49", "50-64", "65+"] + labels=["Under 30", "30-49", "50-64", "65+"], ) # Analyse by income decile -by_decile = progression.groupby('decile').agg({ - 'total_response_ftes': 'sum', - 'substitution_response_ftes': 'sum', - 'income_response_ftes': 'sum' -}) +by_decile = progression.groupby("decile").agg( + { + "total_response_ftes": "sum", + "substitution_response_ftes": "sum", + "income_response_ftes": "sum", + } +) print("FTE changes by income decile:") print(by_decile.round(0)) @@ -100,14 +110,14 @@ The model uses different elasticities for different groups based on empirical ev ```python # Examine elasticities applied -elasticities = progression[['substitution_elasticity', 'income_elasticity']].describe() +elasticities = progression[["substitution_elasticity", "income_elasticity"]].describe() print("Elasticity distribution:") print(elasticities) # See which groups have different elasticities -progression['has_children'] = reformed.calculate("is_parent", 2025) -by_parent = progression.groupby('has_children')[ - ['substitution_elasticity', 'income_elasticity'] +progression["has_children"] = reformed.calculate("is_parent", 2025) +by_parent = progression.groupby("has_children")[ + ["substitution_elasticity", "income_elasticity"] ].mean() print("\nElasticities by parental status:") print(by_parent) @@ -123,13 +133,17 @@ For policies analysed by the OBR, compare your results with their published esti # Example: National Insurance cut from Autumn Statement 2023 # OBR estimated ~94,000 FTE increase -baseline_scenario = Scenario(parameter_changes={ - "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.12, -}) +baseline_scenario = Scenario( + parameter_changes={ + "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.12, + } +) -reform_scenario = Scenario(parameter_changes={ - "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.10, -}) +reform_scenario = Scenario( + parameter_changes={ + "gov.hmrc.national_insurance.class_1.rates.employee.main": 0.10, + } +) baseline = Microsimulation(scenario=baseline_scenario) reformed = Microsimulation(scenario=reform_scenario) diff --git a/docs/book/usage/scenarios.md b/docs/book/usage/scenarios.md index b22544b672..adf3cea981 100644 --- a/docs/book/usage/scenarios.md +++ b/docs/book/usage/scenarios.md @@ -10,20 +10,13 @@ Let's start by creating a scenario that reduces the basic rate of income tax fro from policyengine_uk import Scenario, Simulation # Create a scenario with one policy change -scenario = Scenario(parameter_changes={ - "gov.hmrc.income_tax.rates.uk[0].rate": 0.15 -}) +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} - } - }, + "people": {"person": {"age": {2025: 35}, "employment_income": {2025: 40_000}}}, "benunits": {"benunit": {"members": ["person"]}}, - "households": {"household": {"members": ["person"]}} + "households": {"household": {"members": ["person"]}}, } sim = Simulation(situation=situation, scenario=scenario) @@ -43,19 +36,20 @@ PolicyEngine UK organises policy parameters in a hierarchical structure. Each pa ```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 +"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 +"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 +"gov.hmrc.child_benefit.rates.first_child" # First child rate +"gov.hmrc.child_benefit.rates.additional_child" # Additional children rate ``` ```{tip} @@ -72,35 +66,34 @@ This represents the kind of coordinated policy change you might see in a budget ```python # A comprehensive package that increases Universal Credit support while adjusting income tax -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 -}) +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}} + "child": {"age": {2025: 5}}, }, "benunits": {"family": {"members": ["parent", "child"]}}, "households": { "home": { "members": ["parent", "child"], - "housing_costs": {2025: 7200} # Annual housing costs + "housing_costs": {2025: 7200}, # Annual housing costs } - } + }, } reformed_sim = Simulation(situation=family_situation, scenario=welfare_reform) @@ -112,7 +105,7 @@ 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}") +print(f"Monthly net income: £{net_income / 12:.2f}") ``` ## Combining scenarios @@ -123,19 +116,25 @@ You can combine different scenarios using the `+` operator, which is useful for ```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 -}) +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 -}) +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 -}) +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 @@ -143,11 +142,15 @@ 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)]: +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") + print(f"{name}: £{net_income / 12:.2f} per month") ``` ## Defining structural scenarios @@ -168,11 +171,13 @@ from policyengine_uk import Scenario # 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 +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 = { @@ -180,15 +185,15 @@ large_family = { "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 + "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 + "housing_costs": {2026: 10200}, # Annual housing costs } - } + }, } # Compare the impact @@ -214,30 +219,33 @@ Here's how to create a scenario that automatically indexes it to CPI: 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() - + 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" + 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() + # Create the scenario using a simulation modifier reindex_benefit_cap = Scenario(simulation_modifier=reindex_benefit_cap_to_cpi) @@ -247,15 +255,15 @@ benefit_cap_family = { "parent1": {"age": {2026: 30}, "employment_income": {2026: 0}}, "parent2": {"age": {2026: 28}, "employment_income": {2026: 0}}, "child1": {"age": {2026: 6}}, - "child2": {"age": {2026: 3}} + "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 + "housing_costs": {2026: 14400}, # High annual housing costs } - } + }, } baseline_sim = Simulation(situation=benefit_cap_family) @@ -265,7 +273,9 @@ reformed_sim = Simulation(situation=benefit_cap_family, scenario=reindex_benefit 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") +print( + f"Benefit cap - frozen: £{baseline_cap:.0f}/year, indexed: £{reformed_cap:.0f}/year" +) ``` If you want to remove the cap entirely for a poverty-analysis package, PolicyEngine @@ -317,20 +327,22 @@ A scenario that gradually increases the personal allowance over four years - thi ```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 +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"]}} + "households": {"household": {"members": ["person"]}}, } sim = Simulation(situation=situation, scenario=phased_scenario) @@ -354,38 +366,40 @@ Here's how to build a scenario that phases out PIP payments for some claimants o import numpy as np from policyengine_uk import Scenario, Simulation + 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) @@ -395,11 +409,11 @@ pip_recipient = { "person": { "age": {2025: 45}, "pip": {2025: 150}, # Weekly PIP payment - "employment_income": {2025: 8_000} + "employment_income": {2025: 8_000}, } }, "benunits": {"benunit": {"members": ["person"]}}, - "households": {"household": {"members": ["person"]}} + "households": {"household": {"members": ["person"]}}, } # Compare baseline and phase-out scenarios @@ -410,7 +424,9 @@ reformed_sim = Simulation(situation=pip_recipient, scenario=pip_phase_out) 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") + print( + f"{year}: Baseline £{baseline_pip:.0f}/week, with phase-out £{reformed_pip:.0f}/week" + ) ``` ### Building Universal Credit scenarios with dynamic changes @@ -421,54 +437,59 @@ Some scenarios need to make changes that depend on the simulation's own data. He from policyengine_uk import Scenario, Microsimulation import numpy as np + def modify_uc_for_new_claimants(sim: Microsimulation): """Reduce health elements for new UC claimants while preserving claimant protections""" # Access the parameter system to check if reforms are active - rebalancing_params = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing - + 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 + 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 - + current_health_element[(current_health_element > 0) & is_new_claimant] = ( + new_health_amount + ) + sim.set_input("uc_LCWRA_element", year, current_health_element) - + # General standard allowance uplifts are already handled in the # uc_standard_allowance formula. Scenario modifiers only need to add # claimant-specific overrides such as protected health elements. + # 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 +uc_rebalancing_active = ( + Scenario(parameter_changes={"gov.dwp.universal_credit.rebalancing.active": True}) + + uc_rebalancing +) # Test on a typical UC claimant uc_claimant = { @@ -476,16 +497,11 @@ uc_claimant = { "person": { "age": {2026: 35}, "employment_income": {2026: 8_000}, - "uc_LCWRA_element": {2026: 390} # Monthly health element + "uc_LCWRA_element": {2026: 390}, # Monthly health element } }, "benunits": {"benunit": {"members": ["person"]}}, - "households": { - "home": { - "members": ["person"], - "housing_costs": {2026: 7200} - } - } + "households": {"home": {"members": ["person"], "housing_costs": {2026: 7200}}}, } baseline_sim = Simulation(situation=uc_claimant) @@ -495,7 +511,9 @@ reformed_sim = Simulation(situation=uc_claimant, scenario=uc_rebalancing_active) 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") + print( + f"{year}: Baseline UC £{baseline_uc:.0f}/month, reformed £{reformed_uc:.0f}/month" + ) ``` ## Analysing scenarios at population level @@ -507,12 +525,14 @@ 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% -}) +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( @@ -520,7 +540,7 @@ baseline = Microsimulation( ) reformed = Microsimulation( dataset="hf://policyengine/policyengine-uk-data/enhanced_frs_2022_23.h5", - scenario=major_scenario + scenario=major_scenario, ) # Calculate the fiscal impact of this major intervention @@ -557,41 +577,53 @@ When building complex scenarios, test each component separately: 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 -}) +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 -}) +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 -}) +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}} + "child": {"age": {2025: 6}}, }, "benunits": {"family": {"members": ["parent", "child"]}}, - "households": {"home": {"members": ["parent", "child"], "housing_costs": {2025: 8400}}} # Annual housing costs + "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)] +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") + print(f"{name}: £{net_income / 12:.2f} per month") ``` ## Practical tips for scenario design diff --git a/docs/book/usage/simulations.md b/docs/book/usage/simulations.md index 38332414d7..2ff8dc3f9c 100644 --- a/docs/book/usage/simulations.md +++ b/docs/book/usage/simulations.md @@ -10,22 +10,9 @@ Every simulation starts with a situation that describes the people, benefit unit from policyengine_uk import Simulation situation = { - "people": { - "person_1": { - "age": {2025: 30}, - "employment_income": {2025: 30_000} - } - }, - "benunits": { - "benunit_1": { - "members": ["person_1"] - } - }, - "households": { - "household_1": { - "members": ["person_1"] - } - } + "people": {"person_1": {"age": {2025: 30}, "employment_income": {2025: 30_000}}}, + "benunits": {"benunit_1": {"members": ["person_1"]}}, + "households": {"household_1": {"members": ["person_1"]}}, } sim = Simulation(situation=situation) @@ -115,9 +102,11 @@ Scenarios create reformed simulations without modifying your original baseline s 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 -}) +scenario = Scenario( + parameter_changes={ + "gov.dwp.universal_credit.standard_allowance.single.OVER_25": 500 + } +) # Apply the scenario to create a new simulation reformed_sim = Simulation(situation=situation, scenario=scenario) @@ -142,33 +131,19 @@ Each person must belong to exactly one benefit unit and one household. Children ```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"] - } + "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 + "childcare_costs": {2025: 7200}, # Annual childcare costs } - } + }, } family_sim = Simulation(situation=family_with_children) @@ -191,12 +166,14 @@ If you have data in a DataFrame, convert it to the right format: import pandas as pd # Your data needs specific column naming -df = pd.DataFrame({ - "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 -}) +df = pd.DataFrame( + { + "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 + } +) # Create simulation from the DataFrame sim_from_df = Simulation(dataset=df) @@ -220,23 +197,29 @@ import pandas as pd from policyengine_uk import Simulation from policyengine_uk.data import UKMultiYearDataset, UKSingleYearDataset -person_2025 = pd.DataFrame({ - "person_id": [1, 2], - "person_benunit_id": [1, 1], - "person_household_id": [1, 1], - "age": [35, 7], - "employment_income": [32_000, 0], -}) +person_2025 = pd.DataFrame( + { + "person_id": [1, 2], + "person_benunit_id": [1, 1], + "person_household_id": [1, 1], + "age": [35, 7], + "employment_income": [32_000, 0], + } +) -benunit_2025 = pd.DataFrame({ - "benunit_id": [1], -}) +benunit_2025 = pd.DataFrame( + { + "benunit_id": [1], + } +) -household_2025 = pd.DataFrame({ - "household_id": [1], - "region": ["LONDON"], - "council_tax": [1_800], -}) +household_2025 = pd.DataFrame( + { + "household_id": [1], + "region": ["LONDON"], + "council_tax": [1_800], + } +) dataset_2025 = UKSingleYearDataset( person=person_2025, @@ -326,12 +309,10 @@ baseline = Microsimulation( ) # 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 -}) +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 + scenario=scenario, ) # Calculate revenue impact by comparing household net incomes @@ -374,15 +355,15 @@ income_tax = sim.calculate("income_tax", 2025) # Cached after first calculation # To insert new values, use set_input with arrays current_income = sim.calculate("employment_income", 2025) new_income = current_income * 0 + 35_000 # Create array with new values -sim.set_input("employment_income", 2025, new_income) # This will invalidate related caches +sim.set_input( + "employment_income", 2025, new_income +) # This will invalidate related caches # Parameters are also cached when requested parameters = sim.tax_benefit_system.parameters(2025) # Cached for 2025 # If you have a reform that changes parameters after requesting them: -scenario = Scenario(parameter_changes={ - "gov.hmrc.income_tax.rates.uk[0].rate": 0.25 -}) +scenario = Scenario(parameter_changes={"gov.hmrc.income_tax.rates.uk[0].rate": 0.25}) # The parameter change won't take effect unless you reset the cache: sim.tax_benefit_system.reset_parameter_caches() ``` @@ -393,7 +374,12 @@ Instead of calculating variables one by one, you can get multiple results at onc ```python # Get multiple variables in a single DataFrame -variables = ["employment_income", "income_tax", "universal_credit", "household_net_income"] +variables = [ + "employment_income", + "income_tax", + "universal_credit", + "household_net_income", +] results_df = sim.calculate_dataframe(variables, 2025) # This gives you a DataFrame with columns for each variable @@ -414,7 +400,7 @@ for year in years: # 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") + print(f"{year}: £{avg_income / 12:.2f} per month") ``` ### Mapping variables to different entities @@ -448,7 +434,7 @@ modified_weights = weights * 1.1 # Increase all weights by 10% # 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") +print(f"Total with 10% higher weights: £{weighted_total / 1e9:.1f}bn") ``` ## Debugging simulations @@ -513,14 +499,12 @@ reformed = Microsimulation() # Apply your reforms here # Compare specific variables between simulations comparison_df = baseline.compare( - reformed, - baseline.get_variable_dependencies("income_tax", depth=1), - 2029 + reformed, baseline.get_variable_dependencies("income_tax", depth=1), 2029 ) # The DataFrame has columns for each variable: # - variable_self: baseline value -# - variable_other: reformed value +# - variable_other: reformed value # - variable_change: difference between reformed and baseline ```