PolicyEngine / PolicyEngine/policyengine-uk

Pre-existing defects in dynamics/participation.py that surface when the participation model is enabled

Open
#1,836 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
50
Forks
33
Avg merge
20h 58m
Merged PRs (30d)
14

Description

Summary

Four defects in policyengine_uk/dynamics/participation.py that predate #1828/#1829 and are dormant only because apply_participation_responses is commented out of its coordinator (dynamics/labour_supply.py:157). They should be resolved before the extensive margin is switched on.

Found across three adversarial reviews of #1828 and #1829. None is caused by those PRs, and I have deliberately not fixed them there — bundling them would have made both harder to review.

1. Demographic reads have no period argument

calculate_participation_elasticities calls sim.calculate("gender"), "is_married", "benunit_count_children", "youngest_child_age", "employment_income", "adult_index" and others without a period, while the quintiles handed to it were computed for an explicit year. Same at the adult_index read in apply_participation_responses.

So at year=2027 the model combines 2027 quintiles with default-period demographics and earnings. Silent, and wrong in a way that grows with the gap between the default period and the requested year.

2. Children set the bin edges for the spillover calculation

Every child is assigned quintile 1 — the highest-elasticity row of OBR Table A1 — giving them a mean elasticity of 0.492 against 0.217 for adults. They are correctly excluded from responses, but they are not excluded from pd.cut(elasticities, bins=20), which defines the elasticity groups used for the surplus/spillover loop. On enhanced_frs_2024_25 that is 23,265 children shifting how adults are grouped.

3. np.random.seed mutates global NumPy state

apply_participation_responses calls np.random.seed(random_seed), which reseeds the process-wide generator rather than a local Generator. Anything else drawing random numbers later in the same process is silently affected. policyengine-uk-data has already moved its own stochastic assignments to seeded generators for this reason.

4. Cost

Per call, impute_wages_for_nonworkers is recomputed four times, and calculate_gain_to_work triggers 2 x count_adults full household_net_income recomputes — eight complete model runs at the default count_adults=2. The two for i in np.where(...) loops iterate about 90,000 times in Python. A single invocation takes about 6.5 seconds on the Enhanced FRS; the stochastic loops are the obvious thing to vectorise.

Also worth knowing

The exit path (left_work, and the hours writeback for people leaving employment) has never been exercised: a tax cut produces entrants and no exits, and no test constructs a reform that pushes anyone out. It may be fine; it is simply unverified.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in policyengine_uk/dynamics/participation.py and inspect its calls from dynamics/labour_supply.py:157, including calculate_participation_elasticities and apply_participation_responses. Review existing participation tests and the untested exit path, then verify the four reported defects and the Enhanced FRS runtime behavior are addressed before enabling the coordinator.

Written by the indexing model from the issue text.

Assessment

Tech stack
numpy, python
Domain
backend, performance
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.