Skip to content

Commit 772d13f

Browse files
Add dataset filter and improve labour supply response logic (#1348)
* Add ability to zoom in on one household * Fix potential bug in LSRs!! * Improvements to LSR logic * Add dataset filter and improve labour supply response logic This PR adds the ability to filter datasets to single households for detailed analysis and fixes several issues in the labour supply response calculations. Closes #1349 Closes #1350 * Run formatter
1 parent 259692d commit 772d13f

8 files changed

Lines changed: 100 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.54.0] - 2025-09-30 14:40:31
9+
10+
### Added
11+
12+
- Dataset filter function to extract single households for analysis.
13+
14+
### Changed
15+
16+
- Default target variable for labour supply calculations to HBAI household net income.
17+
- Default adult count for labour supply calculations to 2.
18+
19+
### Fixed
20+
21+
- Labour supply response calculation issues by removing inappropriate clipping of marginal rates.
22+
- Potential bug in labour supply response variable initialization order.
23+
824
## [2.53.1] - 2025-09-29 12:26:51
925

1026
### Fixed
@@ -2248,6 +2264,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22482264

22492265

22502266

2267+
[2.54.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.53.1...2.54.0
22512268
[2.53.1]: https://github.com/PolicyEngine/openfisca-uk/compare/2.53.0...2.53.1
22522269
[2.53.0]: https://github.com/PolicyEngine/openfisca-uk/compare/2.52.1...2.53.0
22532270
[2.52.1]: https://github.com/PolicyEngine/openfisca-uk/compare/2.52.0...2.52.1

changelog.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,3 +1904,16 @@
19041904
fixed:
19051905
- Temporarily disabled OBR participation responses.
19061906
date: 2025-09-29 12:26:51
1907+
- bump: minor
1908+
changes:
1909+
added:
1910+
- Dataset filter function to extract single households for analysis.
1911+
changed:
1912+
- Default target variable for labour supply calculations to HBAI household net
1913+
income.
1914+
- Default adult count for labour supply calculations to 2.
1915+
fixed:
1916+
- Labour supply response calculation issues by removing inappropriate clipping
1917+
of marginal rates.
1918+
- Potential bug in labour supply response variable initialization order.
1919+
date: 2025-09-30 14:40:31

policyengine_uk/data/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
UKMultiYearDataset,
33
UKSingleYearDataset,
44
)
5+
from .filter_dataset import filter_dataset
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import typing
2+
3+
if typing.TYPE_CHECKING:
4+
from policyengine_uk import Microsimulation
5+
from policyengine_uk.data import UKSingleYearDataset
6+
7+
8+
def filter_dataset(
9+
sim: "Microsimulation", household_id: int, year: int = 2026
10+
) -> "UKSingleYearDataset":
11+
from policyengine_uk import Microsimulation
12+
from policyengine_uk.data import UKSingleYearDataset
13+
14+
"""
15+
Extract a single household from a simulation dataset.
16+
17+
This function creates a new dataset containing only the specified household
18+
and the associated benefit units and people within that household.
19+
20+
Parameters
21+
----------
22+
sim : Microsimulation
23+
The microsimulation object containing the dataset.
24+
household_id : int
25+
The ID of the household to extract.
26+
year : int, default 2026
27+
The dataset year to filter.
28+
29+
Returns
30+
-------
31+
UKSingleYearDataset
32+
A new dataset containing only data for the specified household.
33+
"""
34+
dataset: UKSingleYearDataset = sim.dataset[year]
35+
new_dataset = dataset.copy()
36+
new_dataset.person = new_dataset.person[
37+
new_dataset.person.person_household_id == household_id
38+
]
39+
new_dataset.household = new_dataset.household[
40+
new_dataset.household.household_id == household_id
41+
]
42+
benunits = new_dataset.person.person_benunit_id.unique()
43+
new_dataset.benunit = new_dataset.benunit[
44+
new_dataset.benunit.benunit_id.isin(benunits)
45+
]
46+
47+
return UKSingleYearDataset(
48+
person=new_dataset.person,
49+
household=new_dataset.household,
50+
benunit=new_dataset.benunit,
51+
fiscal_year=year,
52+
)

policyengine_uk/dynamics/labour_supply.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929

3030
def calculate_excluded_from_labour_supply_responses(
31-
sim: Simulation, count_adults: int = 1
31+
sim: Simulation, count_adults: int = 2
3232
):
3333
"""Calculate which individuals are excluded from labour supply responses.
3434
@@ -97,10 +97,10 @@ class LabourSupplyResponseData(BaseModel):
9797

9898
def apply_labour_supply_responses(
9999
sim: Simulation,
100-
target_variable: str = "household_net_income",
100+
target_variable: str = "hbai_household_net_income",
101101
input_variable: str = "employment_income",
102102
year: int = 2025,
103-
count_adults: int = 1,
103+
count_adults: int = 2,
104104
delta: float = 1_000,
105105
) -> pd.DataFrame:
106106
"""Apply labour supply responses to simulation and return the response vector.
@@ -197,10 +197,10 @@ def apply_labour_supply_responses(
197197

198198
def apply_progression_responses(
199199
sim: Simulation,
200-
target_variable: str = "household_net_income",
200+
target_variable: str = "hbai_household_net_income",
201201
input_variable: str = "employment_income",
202202
year: int = 2025,
203-
count_adults: int = 1,
203+
count_adults: int = 2,
204204
delta: float = 1_000,
205205
pre_calculated_income_rel_change: np.ndarray = None,
206206
) -> pd.DataFrame:
@@ -233,6 +233,7 @@ def apply_progression_responses(
233233
derivative_changes = derivative_changes.rename(
234234
columns={col: f"deriv_{col}" for col in derivative_changes.columns}
235235
)
236+
derivative_changes["person_id"] = sim.calculate("person_id", year).values
236237

237238
# Add in actual implied wages
238239
gross_wage = sim.calculate("employment_income", year) / sim.calculate(
@@ -262,20 +263,13 @@ def apply_progression_responses(
262263

263264
# Calculate changes in income levels (drives income effects)
264265
if pre_calculated_income_rel_change is not None:
265-
# Use pre-calculated values
266266
n_people = len(sim.calculate("person_id", year))
267267
income_changes = pd.DataFrame(
268268
{
269-
"baseline": np.zeros(
270-
n_people
271-
), # Not needed for behavioral response
272-
"scenario": np.zeros(
273-
n_people
274-
), # Not needed for behavioral response
269+
"baseline": np.zeros(n_people),
270+
"scenario": np.zeros(n_people),
275271
"rel_change": pre_calculated_income_rel_change,
276-
"abs_change": np.zeros(
277-
n_people
278-
), # Not needed for behavioral response
272+
"abs_change": np.zeros(n_people),
279273
}
280274
)
281275
else:

policyengine_uk/dynamics/progression.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
def calculate_derivative(
1616
sim: Simulation,
17-
target_variable: str = "household_net_income",
17+
target_variable: str = "hbai_household_net_income",
1818
input_variable: str = "employment_income",
1919
year: int = 2025,
2020
count_adults: int = 2,
@@ -72,12 +72,12 @@ def calculate_derivative(
7272
sim.set_input(input_variable, year, input_variable_values)
7373

7474
# Clip to ensure rates are between 0 and 1 (0% to 100% retention)
75-
return rel_marginal_wages.clip(0, 1)
75+
return rel_marginal_wages.round(4)
7676

7777

7878
def calculate_relative_income_change(
7979
sim: Simulation,
80-
target_variable: str = "household_net_income",
80+
target_variable: str = "hbai_household_net_income",
8181
year: int = 2025,
8282
) -> pd.DataFrame:
8383
"""Calculate relative change in income between baseline and scenario.
@@ -125,10 +125,10 @@ def calculate_relative_income_change(
125125

126126
def calculate_derivative_change(
127127
sim: Simulation,
128-
target_variable: str = "household_net_income",
128+
target_variable: str = "hbai_household_net_income",
129129
input_variable: str = "employment_income",
130130
year: int = 2025,
131-
count_adults: int = 1,
131+
count_adults: int = 2,
132132
delta: float = 1_000,
133133
) -> pd.DataFrame:
134134
"""Calculate change in marginal rates between baseline and scenario.

policyengine_uk/simulation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ def __init__(
104104
else:
105105
raise ValueError(f"Unsupported dataset type: {dataset.__class__}")
106106

107-
self.input_variables = self.get_known_variables()
108-
109107
# Universal Credit reform (July 2025). Needs closer integration in the baseline,
110108
# but adding here for ease of toggling on/off via the 'active' parameter.
111109
from policyengine_uk.scenarios import universal_credit_july_2025_reform
@@ -119,6 +117,8 @@ def __init__(
119117
self.move_values("capital_gains", "capital_gains_before_response")
120118
self.move_values("employment_income", "employment_income_before_lsr")
121119

120+
self.input_variables = self.get_known_variables()
121+
122122
if scenario is not None:
123123
self.baseline = Simulation(
124124
scenario=None,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "policyengine-uk"
3-
version = "2.53.1"
3+
version = "2.54.0"
44
description = "PolicyEngine tax and benefit system for the UK."
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)