Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions runs/noemp_band_evidence_v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"artifact": "noemp_band_evidence",
"version": 1,
"issue": "192",
"finding": "The 2019+ ASEC data dictionaries relabel NOEMP codes 2/3 from 10-49/50-99 to 10-24/25-99, but the weighted code shares are continuous across the supposed 2018/2019 re-binning and match SUSB only under the 10-49/50-99 reading: the relabeling is documentary, not an instrument change. IPUMS FIRMSIZE inherits the mislabel for 2019+ samples.",
"dictionary_labels_by_year": {
"2017": {
"2": "10-49",
"3": "50-99"
},
"2019": {
"2": "10-24",
"3": "25-99"
},
"2024": {
"2": "10-24",
"3": "25-99"
}
},
"share_pct_by_year": {
"2017": {
"workers_unweighted": 92818,
"workers_weighted_millions": 164.8,
"share_pct_by_code": {
"1": 19.23,
"2": 14.87,
"3": 6.97,
"4": 12.13,
"5": 4.91,
"6": 41.89
}
},
"2019": {
"workers_unweighted": 90476,
"workers_weighted_millions": 167.7,
"share_pct_by_code": {
"1": 18.45,
"2": 14.44,
"3": 6.94,
"4": 12.34,
"5": 5.13,
"6": 42.7
}
},
"2024": {
"workers_unweighted": 73471,
"workers_weighted_millions": 173.2,
"share_pct_by_code": {
"1": 18.53,
"2": 14.14,
"3": 6.71,
"4": 11.72,
"5": 5.22,
"6": 43.68
}
}
},
"max_adjacent_delta_pct_codes_2_3": 0.43,
"expected_delta_if_rebinned_pct": "code 2 roughly halves (~7 pp drop) and code 3 roughly doubles (~7 pp rise)",
"susb_reference": {
"50_99_share_pct": 7.5,
"25_99_share_pct": 15.0,
"note": "SUSB annual tables, 2021 employment by enterprise employment size; 50-99 near 7.5% of employment, 25-99 near 15%. NOEMP code 3's observed ~7% share matches the 50-99 reading and excludes the 25-99 reading."
},
"sources": {
"asec_files": "www2.census.gov/programs-surveys/cps/datasets/{year}/march (2017 converted from asec2017_pubuse.dat at the dictionary positions; 2019/2024 from the published person CSVs)",
"first_reported": "PolicyEngine/populace-dynamics#192, 2026-07-15"
}
}
166 changes: 166 additions & 0 deletions scripts/build_noemp_band_evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Build the NOEMP band-label evidence artifact (issue #192).

REPORTED ANCHOR, NOT A GATE RUN. Like the mortality/claiming/
disability floors, this reads no gate and decides nothing on its
own; it is committed evidence pinned by a reproduction test. It
records the empirical basis for the C2 banding decision's treatment
of CPS ASEC firm size: **the 2019+ data dictionaries' relabeling of
NOEMP codes 2/3 (from 10-49 / 50-99 to 10-24 / 25-99) never
happened in the instrument.**

Two products:

(a) **The discontinuity test.** Weighted NOEMP code shares among
``WKSWORK > 0`` workers from the published public-use files on
both sides of the supposed 2018/2019 re-binning (2017 converted
from the fixed-width ``asec2017_pubuse.dat`` at the dictionary
positions; 2019/2024 from the published CSVs). A genuine re-bin
of a 39-integer band (10-49) into a 15-integer band (10-24)
would roughly halve code 2's share and double code 3's; the
artifact records the observed deltas (under one percentage
point across seven years).

(b) **The SUSB cross-check.** The administrative enterprise-size
employment distribution puts 50-99 employment near 7.5% and
25-99 near 15%; NOEMP code 3's observed ~7% share matches the
former and excludes the latter. Reference values are recorded
with their source note, not calibrated against.

Inputs are the staged ASEC person files under
``~/PolicyEngine/asec-data`` (``POPULACE_DYNAMICS_ASEC_DIR``), as
documented in the ASEC reader (#204). Sources: Census ASEC
public-use files and data dictionaries at
www2.census.gov/programs-surveys/cps/datasets; SUSB annual data
tables (2021 employment by enterprise size). Method and finding
first posted to issue #192 (2026-07-15).

Usage::

python scripts/build_noemp_band_evidence.py

writes ``runs/noemp_band_evidence_v1.json``.
"""

from __future__ import annotations

import json
import os
from pathlib import Path

import pandas as pd

YEARS = (2017, 2019, 2024)

#: Dictionary labels for codes 2/3 as published per year — the
#: conflicting documentation this artifact adjudicates.
DICTIONARY_LABELS = {
2017: {"2": "10-49", "3": "50-99"},
2019: {"2": "10-24", "3": "25-99"},
2024: {"2": "10-24", "3": "25-99"},
}

#: SUSB 2021 employment shares by enterprise size (annual data
#: tables; reference points only, never calibrated against).
SUSB_REFERENCE = {
"50_99_share_pct": 7.5,
"25_99_share_pct": 15.0,
"note": (
"SUSB annual tables, 2021 employment by enterprise "
"employment size; 50-99 near 7.5% of employment, 25-99 "
"near 15%. NOEMP code 3's observed ~7% share matches the "
"50-99 reading and excludes the 25-99 reading."
),
}

ARTIFACT = Path(__file__).resolve().parents[1] / (
"runs/noemp_band_evidence_v1.json"
)


def _data_dir() -> Path:
env = os.environ.get("POPULACE_DYNAMICS_ASEC_DIR")
if env:
return Path(env).expanduser()
return Path("~/PolicyEngine/asec-data").expanduser()


def code_shares(year: int, data_dir: Path) -> dict:
"""Weighted NOEMP code shares among WKSWORK > 0 workers."""
path = data_dir / f"pppub{year % 100:02d}.csv"
raw = pd.read_csv(
path, usecols=lambda c: c in {"NOEMP", "WKSWORK", "MARSUPWT"}
)
workers = raw[raw["WKSWORK"] > 0]
weights = workers.groupby("NOEMP")["MARSUPWT"].sum() / 100.0
weights = weights[weights.index > 0]
shares = weights / weights.sum() * 100.0
return {
"workers_unweighted": int(len(workers)),
"workers_weighted_millions": round(weights.sum() / 1e6, 1),
"share_pct_by_code": {
str(int(code)): round(float(share), 2)
for code, share in shares.items()
},
}


def build() -> dict:
data_dir = _data_dir()
by_year = {str(year): code_shares(year, data_dir) for year in YEARS}

def share(year: int, code: int) -> float:
return by_year[str(year)]["share_pct_by_code"][str(code)]

max_delta = max(
abs(share(a, code) - share(b, code))
for code in (2, 3)
for a, b in ((2017, 2019), (2019, 2024))
)
return {
"artifact": "noemp_band_evidence",
"version": 1,
"issue": "192",
"finding": (
"The 2019+ ASEC data dictionaries relabel NOEMP codes "
"2/3 from 10-49/50-99 to 10-24/25-99, but the weighted "
"code shares are continuous across the supposed "
"2018/2019 re-binning and match SUSB only under the "
"10-49/50-99 reading: the relabeling is documentary, "
"not an instrument change. IPUMS FIRMSIZE inherits the "
"mislabel for 2019+ samples."
),
"dictionary_labels_by_year": DICTIONARY_LABELS,
"share_pct_by_year": by_year,
"max_adjacent_delta_pct_codes_2_3": round(max_delta, 2),
"expected_delta_if_rebinned_pct": (
"code 2 roughly halves (~7 pp drop) and code 3 roughly "
"doubles (~7 pp rise)"
),
"susb_reference": SUSB_REFERENCE,
"sources": {
"asec_files": (
"www2.census.gov/programs-surveys/cps/datasets/"
"{year}/march (2017 converted from "
"asec2017_pubuse.dat at the dictionary positions; "
"2019/2024 from the published person CSVs)"
),
"first_reported": (
"PolicyEngine/populace-dynamics#192, 2026-07-15"
),
},
}


def main() -> None:
artifact = build()
ARTIFACT.write_text(json.dumps(artifact, indent=2) + "\n")
print(f"wrote {ARTIFACT}")
print(
"max adjacent delta on codes 2/3:",
artifact["max_adjacent_delta_pct_codes_2_3"],
"pp",
)


if __name__ == "__main__":
main()
76 changes: 76 additions & 0 deletions tests/test_noemp_band_evidence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Pin the NOEMP band-label evidence artifact (issue #192).

The committed ``runs/noemp_band_evidence_v1.json`` records the
discontinuity test behind the C2 decision to read ASEC NOEMP codes
2/3 as 10-49 / 50-99 in every year. These tests pin the artifact's
internal consistency, and — when the ASEC files are staged —
reproduce it from the raw data.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

ARTIFACT = Path(__file__).resolve().parents[1] / (
"runs/noemp_band_evidence_v1.json"
)

REAL_DATA = Path("~/PolicyEngine/asec-data").expanduser()
needs_real_asec = pytest.mark.skipif(
not (REAL_DATA / "pppub24.csv").exists(),
reason="ASEC person files not staged",
)


@pytest.fixture(scope="module")
def artifact() -> dict:
return json.loads(ARTIFACT.read_text())


def test_artifact_shape(artifact):
assert artifact["artifact"] == "noemp_band_evidence"
assert artifact["version"] == 1
assert set(artifact["share_pct_by_year"]) == {"2017", "2019", "2024"}
for year_block in artifact["share_pct_by_year"].values():
shares = year_block["share_pct_by_code"]
assert set(shares) == {"1", "2", "3", "4", "5", "6"}
assert sum(shares.values()) == pytest.approx(100.0, abs=0.1)
assert year_block["workers_unweighted"] > 50_000


def test_continuity_is_the_finding(artifact):
# The discriminating fact: a genuine 10-49 -> 10-24 re-bin
# would move codes 2/3 by ~7 pp; the committed shares move by
# under one point across seven years.
assert artifact["max_adjacent_delta_pct_codes_2_3"] < 1.0
shares = artifact["share_pct_by_year"]
for year in ("2017", "2019", "2024"):
code3 = shares[year]["share_pct_by_code"]["3"]
# Code 3 tracks SUSB's 50-99 employment share (~7.5%), not
# a 25-99 band (~15%).
assert 5.0 < code3 < 9.0


def test_dictionary_conflict_is_recorded(artifact):
labels = artifact["dictionary_labels_by_year"]
assert labels["2017"]["2"] == "10-49"
assert labels["2019"]["2"] == "10-24"
assert "documentary" in artifact["finding"]


@needs_real_asec
def test_reproduces_from_staged_files(artifact):
import sys

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
from build_noemp_band_evidence import build

rebuilt = build()
assert rebuilt["share_pct_by_year"] == artifact["share_pct_by_year"]
assert (
rebuilt["max_adjacent_delta_pct_codes_2_3"]
== artifact["max_adjacent_delta_pct_codes_2_3"]
)
2 changes: 1 addition & 1 deletion tests/tier_counts.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schema_version": 1,
"counts": {
"unit": 437,
"artifact": 1014,
"artifact": 1018,
"integration_psid": 802,
"reproduction_legacy": 520,
"oracle_policyengine": 159
Expand Down