PolicyEngine / PolicyEngine/firm-microsim-paper
Catalogue of every change made from #52 to #58, with method, for selective re-application after the revert (#59)
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
- A3 + A5 (generator, no result change beyond smoothing) → regenerate.
- B1 + B2 (anchor convention) → regenerate; this is the anchor result.
- C1–C4, B3 (analysis-only; no regeneration).
- E1–E4 (text and claims).
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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