Skip to content

Add eligibility across income measures to the analysis and dashboard - #3

Merged
juaristi22 merged 16 commits into
mainfrom
eligibility-income-measures
Oct 2, 2026
Merged

juaristi22 merged 16 commits into
mainfrom
eligibility-income-measures

Conversation

@juaristi22

@juaristi22 juaristi22 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds an analysis of how the targeted energy discount's eligibility rules line up with household income. It shows which households the rules leave out lower down the income distribution and which they reach higher up.

The income test looks at one person's taxable income. This PR ranks households by five wider measures of household income instead:

  • equivalised household net income, before and after housing costs;
  • the same net income without the size adjustment;
  • household taxable income (every member's taxable income summed).

It then breaks down how households qualify across each distribution. On the dashboard this appears as a collapsible section at the end of the Economic impact tab, titled "Does the discount reach lower-income households?". The cross-tabulations and the three divergence groups stay in the analysis results and receipts, to be shared as files on request. They are not on the dashboard.

The branch also contains the dashboard redesign from #4 and #5, which were stacked on it and merged into it. The dashboard now has four tabs: Economic impact, Your household, Baseline and Methodology.

How PolicyEngine equivalises income

  • policyengine-uk uses the modified OECD scale from DWP's HBAI:
    • before housing costs, 0.67 for the first adult, 0.33 for each other adult and each child aged 14 to 17, and 0.2 for each younger child;
    • after housing costs, 0.58, 0.42 and 0.2.
  • Adults and children are told apart by age alone.
  • Only net income is equivalised: equiv_hbai_household_net_income and its _ahc twin. Gross and taxable income never are.
  • household_income_decile ranks people by equivalised income before housing costs.

Changes

Analysis

  • simulate.py: the household frame gains household taxable income, the number of members with taxable income, the second-highest member's income and the AHC equivalisation factor.

  • analysis.income_distributions. It uses household-weighted deciles, so each holds a tenth of GB households, and tied incomes share a decile. For each of the five measures it reports:

    • how households qualify (passported, income test alone, not at all), their share of spending and their average gain;
    • 5 × 5 quintile cross-tabulations of equivalised against unequivalised income;
    • three groups: households in deciles 1–3 that do not qualify, and households in deciles 6–10 that qualify through the income test alone or through passporting, with size, children, poverty, incomes and household make-up;
    • the first group again on deciles of people, as HBAI counts them (person_deciles).

    Cells and groups with fewer than 10 survey records are blanked (MIN_RECORDS), keeping their position and record count.

  • Which runs get it. It is computed for every run: each option, payment basis (RF's amounts, bill share, £2bn budget), year and dataset, 60 combinations in all. Eligibility is the same across payment bases. Spending and gains follow each run's own payments.

  • coverage() adds absolute and relative poverty and the four lowest deciles before housing costs. The after-housing-costs rows are unchanged.

  • Unchanged figures. Apart from the key renames below, every result field stored on main is unchanged.

Receipts and export

  • report.py adds, to each receipt:
    • a decile table;
    • the three groups;
    • the person-decile check.
  • dashboard_data.py exports a rounded distributions block with the deciles only.
  • The dashboard data file grows from 1.68 MB to 2.08 MB.
  • .gitattributes marks the generated JSON as generated, so it stays collapsed in review.

Dashboard

  • Economic impact:

    • Headline: poverty defaults to absolute poverty before housing costs, and the headline card uses the same measure.
    • Poverty: separate controls for the poverty line (absolute or relative) and housing costs (before or after).
    • Reach: the bars have the same housing-costs control, also defaulting to before housing costs.
    • Inequality: a bar chart with a housing-costs control and the top 1% share.
    • Region and household-type bars: ordered by the passported share.
    • Income-measures section: collapsible, with its controls and two charts in one card (who qualifies by decile, and where the support goes).
  • Your household:

    • The income cut-offs section is gone. Instead the tab charts the discount as one adult's taxable income changes, marks the current income and describes each step in words. The helpers are supportCurve and supportSteps in lib/calculator.js, with tests.
    • The result, the heading and the option comparison sit in one card.
  • Baseline:

    • fixed to 2026-27;
    • bills by decile shown as vertical bars;
    • the official-statistics table collapsible;
    • the RF comparison in one card.
  • Methodology:

    • each step in its own card;
    • how PolicyEngine equivalises and how household deciles differ from the HBAI ones;
    • data coverage and the figures not reproduced, moved here from the Baseline tab.
  • Across tabs:

    • the option, payment basis and year chosen are shared across tabs and kept in the URL;
    • household inputs are kept when switching tabs;
    • dropdown controls, collapsed number tables, and phone and keyboard layouts.
  • Neutral terms:

    • "dead zone" becomes the offset range, the band above a line where extra income after tax is smaller than the support lost;
    • "struggling" becomes "in poverty or with high energy costs";
    • "poor", "missed" and "misses" become "in poverty" and "not reached";
    • "gets nothing" becomes "does not qualify".

    Result keys are renamed to match.

  • Export: the dashboard export drops the cut-off data it no longer shows. The population counts at the lines and the offset range stay in the receipts and the README.

Write-up

  • Analysis README:
    • a new section, "Eligibility across income measures";
    • absolute poverty reach before housing costs;
    • a note that the cut-off counts are in the receipts only.
  • Root README: layout and receipts updated.

Findings (flat option, 2026-27, Microcosm / Enhanced FRS, equivalised income before housing costs)

  • Eligibility by decile: falls from 96% / 97% of households in the lowest decile to 6% / 3% in the highest. The lowest three deciles receive 59% / 62% of spending.
  • Not eligible, deciles 1–3: 1.16m / 1.01m households, or 13% / 11% of those deciles.
    • They include 1.12m / 0.96m children.
    • Couples with children are the largest group (44% on both datasets). Their highest earner averages £32,700 / £36,300, above the line.
    • With deciles of people, as HBAI counts them, the count is 1.61m / 1.28m households, holding 22% / 19% of the people in those deciles.
  • Income test alone, deciles 6–10: 0.17m / 0.21m households, receiving 1–2% of spending. These rest on effective samples of 29 / 5, so the Enhanced FRS figure is indicative only.
  • Passported, deciles 6–10: 2.22m / 1.74m households, or 28% / 22% of passported households, receiving 18% / 13% of spending.
    • On Microcosm, 46% of them are multi-family households, where one benefit unit's means-tested benefit passports the whole household.
    • This composition differs between the datasets: multi-family households are 21% / 9% of all households. The Enhanced FRS figure rests on an effective sample of 27.
  • Reach before housing costs: the flat option reaches 95% of households in absolute poverty, leaving 0.17m / 0.21m unreached, against 0.56m after housing costs.

Testing

  • uv run pytest: 35 passed. New tests cover:
    • household incomes and grouping with tied incomes;
    • decile and cross-tab shares, and the three groups;
    • the person-decile check and the blanking of small cells;
    • the rounding.
  • bun run test: 228 passed. New tests cover:
    • the range helpers;
    • the support-by-income curve;
    • the shared dashboard state;
    • the household tab;
    • that every option, payment basis, year and dataset has a distributions block whose shares add up.
  • bun run lint, ruff check, ruff format --check and bun run build are clean.
  • CI passes: python, dashboard and the Vercel preview. The duplicate Vercel project behind the earlier failing uk-energy-reforms check has been deleted.
  • Merging to main redeploys the production dashboard.

🤖 Generated with Claude Code

juaristi22 and others added 2 commits September 25, 2026 16:07
Ranks GB households by five baseline incomes: equivalised HBAI net income
before and after housing costs (policyengine-uk's modified OECD scale), the
same net income without the size adjustment, and household taxable income
(members' total_income summed). For each it reports, in household-weighted
deciles with ties kept together:
- how households qualify (passported, income test alone, not at all), their
  share of spending and average gain;
- 5x5 quintile cross-tabulations of equivalised against unequivalised
  income;
- three groups: households in deciles 1-3 that do not qualify, and
  households in deciles 6-10 that qualify through the income test alone or
  through passporting, with their size, poverty, incomes and make-up.

The frame gains household taxable income, members with income, the
second-highest income and the AHC equivalisation factor. Computed for each
preset's published run (eligibility does not depend on the amounts);
existing results are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the main tab's reach and targeting area:
- who qualifies by household decile on equivalised, household net or
  household taxable income (before or after housing costs), with spending
  and average gain on the standard decile chart;
- a cross-tab heatmap of equivalised against unequivalised income;
- the three groups where eligibility and income diverge, their make-up, and
  a comparison with the other rules.

The Methodology tab explains how policyengine-uk equivalises income and
defines the measures and groups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
uk-energy-reforms Error Error Oct 2, 2026 1:32pm UTC
uk-targeted-energy-discount Ready Ready Preview Oct 2, 2026 1:32pm UTC

Request Review

… terms

- Poverty: separate toggles for the poverty line (absolute, relative) and
  housing costs (before, after), defaulting to absolute poverty before
  housing costs; the headline card uses the same measure.
- Region and household-type rows are ordered by the passported share, the
  first segment of each bar.
- The income cut-offs section is removed. Instead, "Your household" charts
  the discount as one adult's taxable income changes, with the current
  income marked and the steps described in words (supportCurve and
  supportSteps in lib/calculator.js, with tests).
- Neutral terms throughout: "dead zone" becomes the offset range (the band
  above a line where extra income after tax is below the support lost),
  "struggling" becomes poverty or high energy costs, "poor"/"missed" become
  in poverty/not reached, and "gets nothing" becomes "does not qualify".
  Result keys are renamed to match; stored results were rewritten with the
  renames and receipts regenerated, and a fresh computation matches them.
- The dashboard export drops the cut-off data it no longer shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort) — round 1 at 1857936a

Verdict: nothing blocking in the analysis code. The new distributions are computed correctly, every pre-existing figure is unchanged apart from the renames, and the tests, lint and build pass. One figure in the write-up can't be traced to the committed results. One question is about content: the PR removes the population cut-off figures from the dashboard, and those were part of what NEF asked for. The rest are wording and small-cell nits.

What I checked

  • Base. The branch sits on current main (6b7bba3). Main hasn't moved since Add the targeted energy discount reform and analysis #1 and Add a dashboard for the targeted energy discount results #2 merged, so there are no conflicts.
  • Equivalisation, against the engine source (policyengine-uk b340cf53).
    • BHC uses 0.67 / 0.33 / 0.33 / 0.2 and AHC uses 0.58 / 0.42 / 0.42 / 0.2 (parameters/household/demographic/equiv).
    • is_adult is age ≥ 18, is_older_child is 14 to 17 and is_young_child is under 14. So adults and children are told apart by age alone, as the body says. HBAI itself counts a 16–19-year-old dependant in non-advanced education as a child, so the engine's scale differs from HBAI for that group.
    • Only hbai_household_net_income and its AHC twin are equivalised.
  • income_distributions.
    • _quantile_groups puts ties in one group. Decile sizes on Microcosm are 2.84–2.87m in every measure.
    • On the Enhanced FRS the taxable measure's D1 holds 3.34m against about 3.07m elsewhere. 6.9% of households have no taxable income, and the weights are coarse (ESS 1,120). That's expected granularity, not a bug.
    • The three categories sum to 1 in every decile, and spending shares sum to 1.
    • The five measures are distinct columns (eq_bhc_base, eq_ahc_base, net_bhc_base, net_ahc_base, taxable_income).
  • Headline figures, reproduced from results-2026/results.json. All of these match:
    • eligibility in the lowest and highest deciles: 96% / 97% and 6% / 3%;
    • spending: 59% / 62% to the lowest three deciles and 19% / 15% to the top half;
    • not eligible in deciles 1–3: 1.16m / 1.01m, 13% / 11%, with 1.12m / 0.96m children;
    • the same group after housing costs: 1.59m / 1.38m;
    • income test alone in the top half: 0.17m / 0.21m;
    • passported in the top half: 2.22m / 1.74m, 28% / 22%;
    • the household-test and passport-only comparisons: 1.53m / 1.20m and 4.99m / 4.91m.
  • "Every pre-existing figure matches main." I mapped the renames back and diffed every leaf of results-{2024,2026,2027}/results.json against main: 6,552 / 32,670 / 32,670 leaves, none missing, none changed. No old key or term (dead_zone, struggling, _poor_, "gets nothing") is left in the source, dashboard, data or write-ups.
  • The support-by-income curve. supportCurve puts points at ⌈line⌉ − 1 and ⌈line⌉. So under the strict < £24,000 test the step falls between £23,999 and £24,000. Under the household test the line becomes t × factor − others. Benefit receipt is held fixed, as documented.
  • Tests and builds. uv run pytest: 34 passed. ruff check and ruff format --check are clean. bun run test: 185 passed. bun run lint and bun run build are clean.

1. Should-fix: the person-weighted 1.61m isn't in the committed results. The README says "Counted the HBAI way, with deciles of people rather than households, 1.61m households in the lowest three deciles do not qualify: 22% of the people in them" (analyses/rf-billing-me-softly/README.md:199). The PR body says the same. No value near 1.61 in results-2026/results.json is a person-weighted decile count; the only nearby values are unrelated. Either export it (for example as low_not_eligible_person_deciles beside the household-weighted block) or drop it from the write-up.

2. Question: the dashboard no longer shows any population figures at the £24,000 line. The PR removes the "Income cut-offs" section and drops cliffs from the export (dashboard_data.py). The new "Your household" curve shows the step for one household, but no longer how many households sit just above the line or in the offset range. Those counts (about 410k within £1,000 above, and the offset range) were among the answers given to NEF. They are still in the README and the receipts. If NEF uses the dashboard, I'd keep one compact line or table for them. Otherwise, say in the README that they are receipts-only.

3. Nit: the headline poverty card changes basis. It moves from people in relative poverty after housing costs to absolute poverty before housing costs (ReformTab.jsx), and the poverty section defaults to the same. The reach section on the same tab still measures absolute and relative poverty after housing costs ("absolute AHC poverty", coverage()). Either default is defensible. But the tab now leads with BHC while its reach bars are AHC, so I'd either align them or label the reach bars "after housing costs" in their titles.

4. Nit: "Most are couples with children." In the lowest three deciles, the households that don't qualify are 44% couples with children on both datasets: the largest group, not most of them. Please reword in the README ("the largest group") and in the PR body ("Most are couples with children whose highest earner is above £24,000").

5. Nit: the taxable-income explanation is incomplete. The README says almost no household in the lowest three taxable-income deciles is left out (0.10m / none), and that "that follows from the rule". On Microcosm, though, the third decile's upper cut is £24,500. So the 0.10m are households with combined taxable income between £24,000 and £24,500, which includes single earners just over the line. That group rests on an effective sample of 13. One clause would explain the difference: on the Enhanced FRS the cut is £23,000.

6. Nit: two cross-tab cells rest on one or two records. In eq_bhc|taxable, cell (1, 5) holds 1 record on Microcosm and 2 records (0.014m households, ESS 1.0) on the Enhanced FRS. Both are exported with their eligibility share and are only hatched. Given that Microcosm is built from licensed FRS records, I'd blank cells with fewer than 10 records rather than hatch them, as the microcosm evidence files now do.

7. Nit: the eFRS figure for top-half households qualifying through the income test alone has an effective sample of 5. The README states it ("effective samples of 29 / 5"), but the PR body quotes 0.17m / 0.21m without the caveat. Same for passported on the Enhanced FRS (ESS 27). The dashboard handles this through thinNote.

Not findings: the data file grows from 1.7 MB to 2.2 MB, which is fine for a static fetch. The Methodology text on the scale ("expressed for a childless couple") is correct, since 0.67 + 0.33 = 1.

- Export the lowest-three-decile group on deciles of people (person_deciles), so the
  README's person-weighted figure traces to the results.
- coverage() adds absolute and relative poverty and the four lowest deciles before
  housing costs; the reach bars get a housing-costs toggle defaulting to before.
- Cells and groups with 1-9 survey records are blanked in the analysis (MIN_RECORDS),
  keeping position and record count; the heatmap shows them as not shown.
- README: the largest group, the £24,000-£24,500 taxable band, cut-off counts are
  receipts-only, both datasets for the person-weighted count, "does not reach".
- Results for 2024, 2026 and 2027 regenerated; every pre-existing figure unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Thanks, Vahid. All seven are addressed in fe63c26.

  1. The person-weighted count is now exported. income_distributions gains person_deciles, which repeats the lowest-three-decile group using prepare()'s person-weighted deciles, before and after housing costs. The receipts print it. In 2026-27 under the flat option, the figures are 1.61m / 1.28m households (Microcosm / Enhanced FRS), with 22% / 19% of the people in those deciles. The README and the PR body now quote both datasets from the results.
  2. Cut-off counts stay off the dashboard. María asked for the section to be removed. The README now says the counts at each line and the offset range are in the receipts only (under "Cliff edges"). The household tab shows where support changes for one household.
  3. Housing-cost basis now matches. The reach bars get the same "Housing costs" toggle as the poverty section, and it defaults to before housing costs. coverage() adds absolute and relative poverty and the four lowest person-weighted deciles before housing costs. The after-housing-costs rows are unchanged. Before housing costs, the flat option reaches 95% of households in absolute poverty and leaves 0.17m / 0.21m unreached, against 0.56m after housing costs. The poverty columns in the region and household-type table stay after housing costs, and their description says so.
  4. Reworded. "The largest group (44% on both datasets)" in the README and the PR body.
  5. Clause added. On Microcosm the third taxable decile reaches £24,500. So the 0.10m are households with combined taxable income between £24,000 and £24,500, including single earners just over the line (effective sample 13). On the Enhanced FRS the cut is £23,000.
  6. Small cells are blanked in the analysis itself.
    • Any cell or group with 1 to 9 records keeps its position and record count. Its figures are set to null, with suppressed: true (MIN_RECORDS = 10, _suppress).
    • This blanks cell (1, 5) of eq_bhc|taxable on both datasets in every year, and the same cell of eq_ahc|taxable in the 2024 Enhanced FRS runs (8 records).
    • Every non-empty group profile has at least 10 records, so none is blanked.
    • The heatmap shows blanked cells as "not shown" and the table shows dashes. The Methodology tab and the README state the rule.
  7. ESS caveats added to the PR body. Income test alone in the top half rests on 29 / 5; passported in the top half on the Enhanced FRS rests on 27.

I also replaced the three remaining "misses" in the README with "does not reach".

Checks

  • I reran every year and diffed against the previous head. The only differences are the 3 new coverage rows, the person_deciles block, the suppressed flags and the blanked cells. No existing figure changed.
  • Python: 35 tests pass, and ruff is clean.
  • Dashboard: 185 tests pass, and lint and build are clean.

The failing "Vercel – uk-energy-reforms" check doesn't come from this PR. It is a second Vercel project imported at the repo root, which Vercel detected as Python. It will stop reporting once that project is removed or disconnected.

The dashboard keeps who qualifies by decile on each income measure. The
cross-tabulations and the three divergence groups stay in the analysis results and
receipts, to be shared as files on request, and leave the dashboard export, so the
data file falls from 2.25 MB to 1.71 MB. The heatmap component and its helpers go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi and others added 10 commits October 2, 2026 14:19
- Short sticky title bar and full-width intro with links
- Dropdowns replace pill toggles; one card width throughout
- Poverty and inequality side by side; inequality as a bar chart
- Chart footers merged; legends centred below each chart
- Lower-income reach section collapsible, controls grouped with its charts
- Household tab: result card merged, styled inputs and benefit chips
- Baseline tab fixed to 2026-27; RF comparison redesigned with gaps
- Data coverage and unreproduced figures moved to the methodology
- Footer centred

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RF diamond drawn hollow above the PE dot so overlapping markers stay visible
- Official statistics comparison moved last and shown as labelled rows
- Methodology detail text uses the full card width

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tidy dashboard layout, controls and explanations
Make dashboard results and methodology easier to explore
@juaristi22
juaristi22 marked this pull request as ready for review October 2, 2026 13:37
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Review — round 2 at b60a0e0

Verdict: ready to merge once it leaves draft. All seven round-1 points are fixed, and the checks pass at the merged head (#4 and #5 are now in this branch).

Round-1 points, checked in the code and results

  1. The person-weighted count is exported. income_distributions has a person_deciles block, report.py prints it, and results-2026/results.json contains it. The README now quotes 1.61m / 1.28m from the results.
  2. Cut-off counts: the README now says they are in the receipts only. That's fine.
  3. Housing-cost basis: reach now has the same toggle and default as poverty.
  4. "The largest group (44% on both datasets)" is in the README.
  5. The £24,500 clause is in the README.
  6. Small cells: MIN_RECORDS = 10 and _suppress blank them in the analysis, with 30 suppressed: true entries in the 2026 results.
  7. ESS caveats: added to the PR body.

No old term is left (dead_zone, "struggling", "gets nothing").

Checks at b60a0e0: bun run test passes 228 tests, and lint is clean. uv run pytest passes 35, and ruff is clean.

Before merging

  1. The PR body is out of date after Make dashboard results and methodology easier to explore #4 and Tidy dashboard layout, controls and explanations #5:
    • The section it calls "Eligibility across income measures" is now a collapsible card at the end of the Economic impact tab, titled "Does the discount reach lower-income households?".
    • The tabs are now Economic impact, Your household, Baseline and Methodology.
    • The Baseline tab shows 2026-27 only.
    • Data coverage and the figures not reproduced moved to the Methodology tab.
    • The dashboard test count is now 228, not 184.
  2. The failing uk-energy-reforms Vercel check: I agree it isn't from this PR. Disconnecting that second Vercel project before merging would keep main's checks green.
  3. The draft flag: mark the PR ready when you're happy. Merging to main redeploys the production dashboard.

@juaristi22
juaristi22 merged commit f970e3b into main Oct 2, 2026
4 checks passed

This branch had an error being deployed

1 failed (outdated) and 2 active (1 outdated) deployments
Preview — b60a0e06 Deployed Oct 2, 2026 by vercel[bot]
Preview – uk-targeted-energy-discount — fe63c262 Deployed Oct 2, 2026 by vercel[bot]
Preview – uk-energy-reforms — fe63c262 Deployed Oct 2, 2026 by vercel[bot]
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.

2 participants