Skip to content

Add labor supply response macro output#360

Open
anth-volk wants to merge 6 commits into
mainfrom
add-labor-supply-response-output
Open

Add labor supply response macro output#360
anth-volk wants to merge 6 commits into
mainfrom
add-labor-supply-response-output

Conversation

@anth-volk
Copy link
Copy Markdown
Contributor

Fixes #355

Summary

  • Add a legacy-compatible LaborSupplyResponse output model and calculator for US and UK macro analyses.
  • Wire labor_supply_response into US and UK PolicyReformAnalysis outputs.
  • Materialize active LSR-only variables only when policy or dynamic inputs indicate an LSR run.
  • Preserve inactive/zero-output behavior without requiring optional LSR columns.
  • Harden parameter activation and household-weighted decile calculations against false-positive parameter prefixes and non-default household indexes.

Tests

  • ruff check src/policyengine/outputs/labor_supply_response.py tests/test_labor_supply_response.py
  • uv run --extra dev pytest -q tests/test_labor_supply_response.py
  • git diff --check

@anth-volk anth-volk requested a review from vahid-ahmadi May 14, 2026 19:16
@anth-volk anth-volk marked this pull request as ready for review May 14, 2026 19:16
@anth-volk
Copy link
Copy Markdown
Contributor Author

@vahid-ahmadi, when you review this PR, could you be particularly concerned with how labor_supply_response impacts integrate into the broader macro output set?

The key risk is that LSRs depend on a conditionally present set of variables. When a reform/dynamic activates labor-supply responses, all of those variables need to be present in the output data for LSRs to calculate correctly; when LSRs are inactive, we intentionally avoid requiring those optional columns and return the legacy zero-shaped result.

I did not add a full integration test for this wiring because the full simulation runtime is long, so the current coverage is focused unit coverage around activation detection, missing-column behavior, output shape, and household-weighted decile calculations.

@anth-volk anth-volk requested review from PavelMakarchuk and removed request for vahid-ahmadi May 15, 2026 13:43
@anth-volk anth-volk force-pushed the add-labor-supply-response-output branch from 0b34481 to a37079f Compare May 18, 2026 13:31
Copy link
Copy Markdown
Collaborator

@PavelMakarchuk PavelMakarchuk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on methodology. Verified field-by-field against legacy policyengine-api/policyengine_api/endpoints/economy/compare.py:26-98:

Field Match
substitution_lsr / income_lsr (scalar diffs)
relative_lsr = lsr_hh.sum() / original_earnings.sum() ✓ (PR uses _safe_ratio → 0 instead of NaN/inf on zero denominator — minor robustness improvement)
decile.average.{income,substitution}.groupby(decile).mean(), all deciles
decile.relative.{income,substitution} — sum/sum per decile, filtered > 0
original_earnings = baseline_earnings - total_lsr_hh (where total_lsr_hh = reform_lsr - baseline_lsr) ✓ (preserves the quirky legacy formula)
hours.{change, income_effect, substitution_effect}
revenue_change = 0.0 ✓ (preserves legacy bug — budgetary_impact_lsr was never populated)

The test_calculate_us_labor_supply_response_uses_legacy_shape test pins hand-computed expected values (e.g. relative_lsr.income = 40.0/285.0, decile.relative.income[1] = 20.0/165.0), which locks in the legacy numerator/denominator structure.

The activeness-gating piece is new — it skips materializing 6 (US) or 2 (UK) expensive person-level variables when neither policy nor dynamic looks LSR-related. Detection uses parameter-prefix matching (both labor/labour spellings for UK), an explicit affects_labor_supply_response field, and simulation_modifier presence as a conservative fallback. Sensible heuristic.

Non-blocking follow-ups:

  1. revenue_change legacy bug — please file an issue to actually compute the LSR revenue effect. Parity with the legacy zero is fine today, but it's a real user-visible gap.
  2. Code volume — 633 lines for ~50 lines of legacy logic. The active/inactive bifurcation duplicates much of the calc; could be unified by constructing zero-filled series upfront and running one code path. Tests pin behavior so refactoring later is safe.
  3. _calculate_hours control flow — the dual-path column-probing plus active flag is convoluted. Likely simplifies to "if active, compute; else zeros."
  4. affects_labor_supply_response: Optional[bool] on Policy/Dynamic — new public API surface for a single use case, with a non-trivial three-way truth table in __add__. Consider whether this belongs in a metadata dict instead of a top-level field.
  5. _safe_ratio vs legacy NaN/inf behavior — deliberate improvement; worth a code comment noting this as a deliberate divergence from "legacy-compatible."

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.

Add 4.x output support for legacy labor_supply_response macro result

2 participants