PolicyEngine / PolicyEngine/microcosm-dynamics
Employer–firm relationships: US-first plan, evaluation gates, and two-workstream split
@daphnehanse11 is already working on this.
Since Jul 14, 2026.
- Dominant language
- Python
- Stars
- 1
- Forks
- 1
- Avg merge
- 1h 46m
- Merged PRs (30d)
- 28
Description
Employer–firm relationships in Populace (US-first)
📊 Visual plan (live): https://employer-firm-plan.vercel.app
Per Max's ask, this is the plan for attaching job spells with employer attributes to the Populace dynamic person panel — US first, following the ECPS recipe (impute from panel data → calibrate to administrative aggregates) and this repo's evaluation discipline (pre-registered gates, noise floors, one-shot candidate runs). The plan has been through two independent adversarial reviews (a data-claims audit with every source web-verified, and a modeling/precedents review against DYNASIM, MINT, PENSIM, Census SSB/SynLBD, AKM/BLM); rev 1 of the page incorporates both.
Architecture
flowchart LR
subgraph A["Workstream A — Daphne (person side)"]
SIPP["SIPP job-level panel<br/>job IDs, tenure, industry,<br/>establishment size (not firm)"]
CPS["CPS ASEC + tenure supp.<br/>NOEMP = true firm-size label"]
PSID["PSID / NLSY<br/>long-horizon tenure"]
IMP["microimpute QRF<br/>spells onto CPS persons +<br/>transition hazards (phase 1)"]
SIPP --> IMP
CPS --> IMP
PSID --> IMP
end
subgraph B["Workstream B — Vahid (firm side)"]
MARG["SUSB · BDS · CBP · SynLBD<br/>firm-type margins & dynamics"]
FLOW["QWI · J2J · JOLTS · BED<br/>flow & earnings aggregates"]
REG["BLM firm-type register<br/>(types, not firm IDs)"]
MARG --> REG
end
MATCH["match layer (interface)<br/>C1 spell schema is the contract;<br/>calibrated to QWI/J2J,<br/>disjoint from gate cells"]
IMP --> MATCH
REG --> MATCH
FLOW --> MATCH
HARNESS["dynamics harness<br/>E1–E12 battery, noise floors,<br/>pre-registered gates"]
PE["PolicyEngine-US<br/>employer-keyed rules (ESI, mandates)"]
MATCH --> HARNESS
MATCH --> PE
Phasing
- Phase 0 (static): employer attributes (industry, firm-size class, tenure) imputed onto CPS persons; calibrated to SUSB. Enough for firm-size-threshold policy cross-sectionally.
- Phase 1 (dynamic): job-spell transition hazards (separation, hire, job-to-job, destination attributes) trained on SIPP, scored against SIPP holdouts, calibrated to QWI/J2J.
- Phase 2 (two-sided, gated): BLM-style firm-type register — firm types (industry × size × state), not firm identities, since rosters are unidentified from public margins. Goes ahead only if phase-1 gates pass and the E12 identification story holds; otherwise claims scope down to firm-size-keyed policy only.
Evaluation criteria (to be pre-registered, gates.yaml style)
E1–E12 battery (full table on the live page), highlights:
| Gate | Moment | Reference |
|---|---|---|
| E1 | employment share by firm-size × industry | SUSB |
| E2 | separation/hire/J2J rates by age × sex | LEHD J2J |
| E4/E5 | employer-retention pairs + multi-window attachment runs (catches chained-model persistence understatement) | SIPP holdout |
| E9 | earnings-change distribution conditional on transition type (layering-coherence gate) | SIPP holdout |
| E10 | regression gate: existing locked PSID earnings gates must still pass after the spell layer attaches | existing gates.yaml |
| E11 | J2J flows by origin × destination firm size (the size ladder) | LEHD J2JOD |
| E12 | within/between-firm earnings variance + coworker correlation — the moments that would be wrong under random worker-to-firm assignment | bias-corrected AKM (Song et al.; KSS), BDS |
Protocol rules from review: calibration cells pre-registered disjoint from gate cells; a SIPP-vs-J2J seam-reconciliation run before thresholds lock; noise floors before any threshold number.
Key review findings already folded in
- SIPP 2014+ measures establishment size, not firm size (
EJB1_EMPSIZEis location size) → CPS ASECNOEMPis the firm-size training label. - QWI publishes mean earnings, never medians → E7 restated on means.
- QWI/J2J dimensions are pairwise-restricted (firm size XOR firm age; one demographic pair; sub-sector industry × size at state level only).
- No one has published a credible synthetic matched US employer–employee file — Census released synthetic persons (SSB) and synthetic establishments (SynLBD) but never the link. Phase 2 would be first; the paper must say so.
Division of labor
Two independent workstreams meeting at three frozen interface contracts (details on the live page):
- @daphnehanse11 — Workstream A (person side): SIPP job-level reader (label-verified,
family.pypattern), CPS NOEMP/tenure loaders, harmonized PSID/NLSY tenure series, QRF spell imputation, phase-1 transition hazards; owns SIPP noise floors and gates E3–E5, E8–E10 plus the seam-vs-J2J reconciliation run. Deliverable to B: the person panel emitting spells in the frozen C1 schema. - @vahid-ahmadi — Workstream B (firm side): external-target pipeline (SUSB/BDS/CBP/QWI/J2J/JOLTS/BED extracts committed with provenance notes), canonical firm-size banding decision (C2), calibration with the target/gate partition, BLM firm-type register; owns gates E1, E2, E6, E7, E11, E12 and the aggregate-side floor studies.
Interface contracts: C1 spell schema and C2 canonical firm-size banding freeze in week 1 (changed only by joint PR); C3, the jointly-authored employer gate block, locks after a referee round in week ~4 — no one-shot candidate runs before C3 locks. Sync points are weeks 1, 4, and ~11 (phase-2 go/no-go); everything else is asynchronous — A can develop against fixture spells, B against a synthetic C1-conforming file.
cc @MaxGhenis — process update: plan built, adversarially reviewed twice (data claims + modeling/precedents), revised, split into the two workstreams above, and deployed for browsing at the Vercel link on top. Next concrete step is the week-1 items: readers + ADR (A), target pipeline + banding proposal (B).
🤖 Generated with Claude Code
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.
Assessment
This issue has not been assessed yet.