Skip to content

feat: structural reform hooks in Python wrapper#23

Merged
nikhilwoodruff merged 2 commits intomainfrom
feat/structural-reforms
Apr 8, 2026
Merged

feat: structural reform hooks in Python wrapper#23
nikhilwoodruff merged 2 commits intomainfrom
feat/structural-reforms

Conversation

@nikhilwoodruff
Copy link
Copy Markdown
Contributor

Adds StructuralReform(pre=..., post=...) to the Python wrapper, enabling reforms that can't be expressed as parameter overlays.

Both hooks share the same signature so multi-year reforms work cleanly:

def hook(year: int, persons: pd.DataFrame, benunits: pd.DataFrame, households: pd.DataFrame) -> tuple[...]:
    ...
    return persons, benunits, households

Pre-hook — runs before the Rust binary sees the data. Mutate input columns: add a new income source, change household composition, set benefit eligibility flags, etc.

Post-hook — runs after the binary produces microdata output. All baseline_*/reform_* columns are populated at this point. Mutate result columns to apply a new tax, offset a benefit, impose a cap, etc. Aggregation into SimulationResult then happens in Python via the new aggregate_microdata() function.

from policyengine_uk_compiled import Simulation, StructuralReform

# Pre-hook: cap wages before simulation
def cap_wages(year, persons, benunits, households):
    persons["employment_income"] = persons["employment_income"].clip(upper=100_000)
    return persons, benunits, households

# Post-hook: add a flat per-adult UBI to reform net income
def add_ubi(year, persons, benunits, households):
    ubi = 50 * 52
    adult_counts = persons[persons["age"] >= 18].groupby("household_id").size()
    households["reform_net_income"] += households["household_id"].map(adult_counts).fillna(0) * ubi
    return persons, benunits, households

sim = Simulation(year=2025, dataset="frs")
result = sim.run(structural=StructuralReform(pre=cap_wages, post=add_ubi))

Both hooks work for DataFrame-based (single household) and file-based (FRS/dataset) sources. For file-based sources with a pre-hook, the CSVs are loaded into DataFrames, the hook is applied, and the result is piped to the binary via stdin.

Adds StructuralReform(pre=..., post=...) with the signature:

    hook(year, persons, benunits, households) -> (persons, benunits, households)

The year argument enables multi-year reforms. Pre-hooks mutate input
DataFrames before the Rust binary runs; post-hooks mutate microdata output
after the binary produces results, with aggregation then done in Python.

Also extracts a standalone aggregate_microdata() function used both by
post-hook runs and (replacing the old _aggregate_persons_only) for
persons-only datasets.
@nikhilwoodruff nikhilwoodruff merged commit 662aaa6 into main Apr 8, 2026
1 check passed
@nikhilwoodruff nikhilwoodruff deleted the feat/structural-reforms branch April 8, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant