PolicyEngine / PolicyEngine/firm-microsim-paper

Catalogue of every change made from #52 to #58, with method, for selective re-application after the revert (#59)

Open
#60 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
0
Forks
0
Avg merge
5h 5m
Merged PRs (30d)
10

Description

PR #59 returns the repository to its pre-#52 state (merge commit of #49). This issue lists what #52–#58 changed and how, so each item can be re-applied on its own and judged on its own. Items are grouped by what they touch. Status column: K = probably worth keeping, D = the item Vahid rejected, ? = a judgement call.

A. Population generator and calibration (src/firm_microsim/)

# Change Method PR Status
A1 DBT unregistered stratum New scripts/etl_bpe_tables.py reads BPE Table 6 (businesses registered for neither VAT nor PAYE, by SIC division; 2.86m, mean turnover £43k). generate_unregistered_firms draws them exponential (or lognormal, --unregistered-shape) around the division mean; weights frozen in calibration (frozen_mask); registrable below the threshold, exempt above; new flags in_frame, unregistered. #52, #55 D
A2 OBR levels on the stratum With the stratum present, the OBR Chart C £1k-bin counts enter as level targets (stratum below the threshold, frame above) instead of frame-scaled shape targets. #57 refined this to the residual: chart count minus the frame's own base mass in the bin, floored at 5%, so the frame is left undistorted and the stratum carries the rise and the cliff. #52, #57 D
A3 Log-log within-band fill Closed ONS bands above the first are drawn from a truncated power law whose exponent is the log-log slope between neighbouring band densities (_band_alphas, _draw_power_law); the open 5000+ band is log-uniform to £50m. Removes the £100k and £250k steps; max calibration weight fell from ~2,400 to ~300 while the stratum was present. #52 K
A4 Registration propensity per band retained registration_propensity() = HMRC count / frame mass per band (~0.88 below, 0.54–0.94 above) survives from #46; #52 only added the stratum on top. #46 already in pre-#52
A5 Weight diagnostics in the calibration report ESS, weight quantiles and coefficient of variation printed per vintage. #52 K

B. Static model (static/model.py, analysis/static_results_dump.py)

# Change Method PR Status
B1 Data-year band membership _registered(df, T, gap, retain_voluntary) evaluates membership on data-year turnover; liabilities are aged to the fiscal year (FISCAL_YEARS, VINTAGE_BASE_GROWTH); the whole vacated band is released; DEREGISTRATION_GAP=0 with the statutory £2k gap as a sensitivity. Same rule for anchor and sweep. This is what moved the anchor from ~3–4× HMRC to −193/−197/−120/−41/+88. #52 K
B2 Anchor sensitivities Aged-membership variant, gap-protected variant, LLAT 43% retention variant, released-firm counts, per-firm-remittance (£2,152) variant for the cut rows; all in results/static_sweep.txt. #53 K
B3 Revenue-matched constant-m taper taper_band_top(m) = mT/(m−τ), make_schedule_taper_flat(m); brentq for the m* whose cost equals the raise to £100k (m* ≈ 71%, top ≈ £118k). Reform menu gains taper_flat50. #55 K

C. Dominated region and behavioural model (analysis/dominated_region_mass.py, dynamic/model.py)

# Change Method PR Status
C1 a(δ) under unreclaimed input VAT a(δ) = T τ(1−2δ)/((1−τ)(1−δ)), zero for δ ≥ ½; firm-specific widths on the synthetic population; £21,250 presented as the zero-input upper bound. #53 K
C2 Standard-rated input share s* δ_s = s·δ; the share of below-threshold in-scope firms that prefer registration (a(δ_s) ≤ 0) is solved by brentq to equal LLAT's observed 43% voluntary share → s* ≈ 0.78. Reported: share with positive width, mean widths, firms inside their own region. #55 K
C3 δ-invariant secondary notch Corrects a false claim: the reduced-rate band top notch a′ = T₁(τ−r)/(1−τ) does not depend on δ, so a reduced-rate band creates a dominated interval for firms whose primary width is zero. #54 K (a correctness fix)
C4 Input-VAT marginal buncher marginal_buncher_iso(..., formulation="input_vat"); n_H falls in δ and vanishes for δ ≥ ½; reform costs unchanged because the intensive ratio is formulation-free. Incidence convention stated (fixed consumer price). #55 K

D. Bunching estimator (bunching/, scripts/bunching_inference.py)

# Change Method PR Status
D1 Gross/net/mass-conservation outputs locate_marginal_buncher_full returns gross E, signed E_net, Δ_R, y_R with a censoring flag when the search window binds. #48/#53 already in pre-#52 (gross/net split from #53)
D2 Universe switch `BunchingEstimator(vintage, universe="all" "chart" "scope")`; the inference script prints all three. Only meaningful with the stratum; on the frame alone "chart" = "all".
D3 Weight-driven spikes Not a change but a finding: with the frame alone, single retail rows with weights 850–5,600 create spurious spikes at £90k, £92k, £99k, £104k and £150–165k. A weight cap (~50) or entropy penalty in optimize_weights would remove them. #58 (diagnosis) to do
D4 Cross-threshold cliff Finding: the OBR profile is applied as a shape target per side scaled to the frame's own mass on that side, so the 2:1 cliff at £85k is discarded and the frame shows no bunching. Scaling the profile to the frame's mass over the whole £65k–£105k window would reproduce the cliff without the stratum. #58 (diagnosis) to do

E. Manuscript and tooling

# Change Method PR Status
E1 Sections 5 and 7 compressed Behavioural section 1,960 → 880 words; bunching 890 → 620; formulation-A optima figure and ability-recovery detail dropped; ~13,000 words total. #56 K
E2 Abstract ≤150 words 138 words. #57 K
E3 Claims manifest extended 47 → 78 claims; compares values, not just presence; anchor series, sweep rows, menu, dynamic, E/b_LLAT, a(δ) block, taper match, seeds. #53–#57 K (drop stratum claims)
E4 Referee trims and fixes Correction narrative and earlier-build sentences removed; placebo/recovery moved to appendix; literature comparison folded into one paragraph; benedek2015, belloncopestake2022 cited; δ replaces ρ; "within 30%" anchor statement (now "within a third" on the frame alone). #53, #54, #55 K
E5 Figure 3 stacked by stratum; £150k line labelled "HMRC band edge"; legend placement plot_turnover_distribution stacks frame + stratum below the threshold when stratum rows exist, otherwise draws the frame alone. #57, #58 ?
E6 Stratum-shape sensitivity appendix, three-universe wording, "5.5m businesses" framing Text and analysis/stratum_shape_sensitivity.py. #55, #57 D

Suggested re-application order after #59 merges

  1. A3 + A5 (generator, no result change beyond smoothing) → regenerate.
  2. B1 + B2 (anchor convention) → regenerate; this is the anchor result.
  3. C1–C4, B3 (analysis-only; no regeneration).
  4. E1–E4 (text and claims).
  5. D3 + D4 (weight cap, whole-window OBR scaling) as a new PR to fix the frame-only Figure 3 and the missing cliff.

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 by reading PR #59 and the suggested re-application order. Review the named entry points in src/firm_microsim/, static/model.py, analysis/dominated_region_mass.py, dynamic/model.py, bunching/, and scripts/bunching_inference.py, plus the manuscript and claims manifest. Done means the selected K items are independently re-applied, regenerated or re-run where required, and their listed results and claims are updated.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
analytics, data, documentation
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.