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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/book/usage/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,30 @@ benefit_cap_reduction = sim.calculate("benefit_cap_reduction", 2026).mean()
print(f"Benefit cap reduction after abolition: £{benefit_cap_reduction:.0f}")
```

### Removing economic assumptions

If you want to isolate policy effects from forecast-driven uprating, use the
`no_economic_assumptions` scenario. It applies before data load, so it freezes
both parameter uprating and any dataset extension that depends on forecast
growth rates.

```python
from policyengine_uk import Simulation
from policyengine_uk.scenarios import no_economic_assumptions

situation = {
"people": {"person": {"age": {2025: 40}, "employment_income": {2025: 30_000}}},
"benunits": {"benunit": {"members": ["person"]}},
"households": {"household": {"members": ["person"]}},
}

baseline = Simulation(situation=situation)
static = Simulation(situation=situation, scenario=no_economic_assumptions)

print(float(baseline.calculate("employment_income", 2026)[0]))
print(float(static.calculate("employment_income", 2026)[0]))
```

## Advanced scenario techniques

### Time-varying parameters
Expand Down
28 changes: 28 additions & 0 deletions policyengine_uk/data/economic_assumptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,3 +357,31 @@ def reset_uprating(
dataset.datasets[year].time_period = str(year)

return dataset


def reset_growthfactor_uprating(
dataset: UKMultiYearDataset,
):
with open(Path(__file__).parent / "uprating_indices.yaml", "r") as f:
uprating = yaml.safe_load(f)

growthfactor_uprated_variables = {
variable for variables in uprating.values() for variable in variables
}
growthfactor_uprated_variables.update({"council_tax", "rent"})

first_year = min(dataset.datasets.keys())
base_year_dataset = dataset.datasets[first_year]

for year in dataset.datasets:
if year == first_year:
continue
current_year_dataset = dataset.datasets[year]
for table_name in current_year_dataset.table_names:
base_table = getattr(base_year_dataset, table_name)
current_table = getattr(current_year_dataset, table_name)
for variable in growthfactor_uprated_variables:
if variable in current_table.columns and variable in base_table.columns:
current_table[variable] = base_table[variable].values

return dataset
1 change: 1 addition & 0 deletions policyengine_uk/scenarios/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .abolish_benefit_cap import abolish_benefit_cap
from .no_economic_assumptions import no_economic_assumptions
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
Expand Down
29 changes: 29 additions & 0 deletions policyengine_uk/scenarios/no_economic_assumptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from policyengine_core.parameters import Parameter

from policyengine_uk import Simulation
from policyengine_uk.model_api import Scenario


def remove_economic_assumptions(simulation: Simulation):
simulation.disable_economic_assumptions = True
simulation.tax_benefit_system.reset_parameters()

cutoff = f"{simulation.default_input_period}-01-01"
yoy_growth = (
simulation.tax_benefit_system.parameters.gov.economic_assumptions.yoy_growth
)

for parameter in yoy_growth.get_descendants():
if not isinstance(parameter, Parameter):
continue
for value_at_instant in parameter.values_list:
if value_at_instant.instant_str >= cutoff:
value_at_instant.value = 0

simulation.tax_benefit_system.process_parameters()


no_economic_assumptions = Scenario(
simulation_modifier=remove_economic_assumptions,
applied_before_data_load=True,
)
6 changes: 6 additions & 0 deletions policyengine_uk/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from policyengine_uk.utils.scenario import Scenario
from policyengine_uk.data.economic_assumptions import (
extend_single_year_dataset,
reset_growthfactor_uprating,
)
from policyengine_uk.utils.dependencies import get_variable_dependencies
from policyengine_uk.reforms import create_structural_reforms_from_parameters
Expand Down Expand Up @@ -131,6 +132,7 @@ def __init__(
self.max_spiral_loops: int = 10
self.memory_config = None
self._data_storage_dir: Optional[str] = None
self.disable_economic_assumptions: bool = False

self.branches: Dict[str, Simulation] = {}

Expand Down Expand Up @@ -424,6 +426,10 @@ def build_from_multi_year_dataset(self, dataset: UKMultiYearDataset) -> None:
Args:
dataset: UKMultiYearDataset containing multiple years of data
"""
if self.disable_economic_assumptions:
dataset = dataset.copy()
reset_growthfactor_uprating(dataset)

# Ensure enum columns are encoded and _enum_columns is populated so
# that .person/.benunit/.household properties can decode back to strings.
if not any(dataset[y]._enum_columns for y in dataset.years):
Expand Down
98 changes: 98 additions & 0 deletions policyengine_uk/tests/test_no_economic_assumptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import pandas as pd
import pytest

from policyengine_uk import Simulation
from policyengine_uk.data.dataset_schema import (
UKMultiYearDataset,
UKSingleYearDataset,
)
from policyengine_uk.scenarios import no_economic_assumptions


def test_no_economic_assumptions_freezes_growthfactor_uprating():
situation = {
"people": {
"person": {
"age": {2025: 40},
"employment_income": {2025: 30_000},
}
},
"benunits": {"benunit": {"members": ["person"]}},
"households": {"household": {"members": ["person"]}},
}

baseline = Simulation(situation=situation)
reformed = Simulation(situation=situation, scenario=no_economic_assumptions)

baseline_income_2025 = float(baseline.calculate("employment_income", 2025)[0])
baseline_income_2026 = float(baseline.calculate("employment_income", 2026)[0])
reformed_income_2025 = float(reformed.calculate("employment_income", 2025)[0])
reformed_income_2026 = float(reformed.calculate("employment_income", 2026)[0])

assert baseline_income_2026 > baseline_income_2025
assert reformed_income_2026 == pytest.approx(reformed_income_2025)

baseline_fees = baseline.tax_benefit_system.parameters.gov.simulation.private_school_vat.private_school_fees
reformed_fees = reformed.tax_benefit_system.parameters.gov.simulation.private_school_vat.private_school_fees
assert float(baseline_fees("2026")) > float(baseline_fees("2025"))
assert float(reformed_fees("2026")) == pytest.approx(float(reformed_fees("2025")))

baseline_benefit_index = (
baseline.tax_benefit_system.parameters.gov.benefit_uprating_cpi
)
reformed_benefit_index = (
reformed.tax_benefit_system.parameters.gov.benefit_uprating_cpi
)
assert float(baseline_benefit_index("2026")) > float(baseline_benefit_index("2025"))
assert float(reformed_benefit_index("2026")) == pytest.approx(
float(reformed_benefit_index("2025"))
)


def test_no_economic_assumptions_resets_multi_year_dataset_without_mutating_input():
person_2025 = pd.DataFrame(
{
"person_id": [1],
"person_benunit_id": [1],
"person_household_id": [1],
"employment_income": [30_000.0],
"age": [40],
}
)
person_2026 = person_2025.copy()
person_2026["employment_income"] = [31_019.96875]
person_2026["age"] = [41]

benunit = pd.DataFrame({"benunit_id": [1]})
household = pd.DataFrame({"household_id": [1]})

dataset = UKMultiYearDataset(
datasets=[
UKSingleYearDataset(
person=person_2025,
benunit=benunit.copy(),
household=household.copy(),
fiscal_year=2025,
),
UKSingleYearDataset(
person=person_2026,
benunit=benunit.copy(),
household=household.copy(),
fiscal_year=2026,
),
]
)

baseline = Simulation(dataset=dataset)
reformed = Simulation(dataset=dataset, scenario=no_economic_assumptions)

assert float(baseline.calculate("employment_income", 2026)[0]) == pytest.approx(
31_019.96875
)
assert float(reformed.calculate("employment_income", 2026)[0]) == pytest.approx(
30_000.0
)
assert int(reformed.calculate("age", 2026)[0]) == 41
assert dataset[2026].person["employment_income"].iloc[0] == pytest.approx(
31_019.96875
)
Loading