fix(opps): CY2020 CF→80.784 (NFRM) + verify all 13 years vs Addendum B

With CY2014-2020 Addendum B now ingested, every year's conversion factor
is cross-checked against CMS's own published rates (weight × CF vs
payment_rate) — all 13 reconcile 100% exact, independently confirming the
Federal-Register CFs for 2014-2020.

One correction fell out: CY2020 = 80.784 (not 80.793). The archived
addenda are the NFRM (as-published) version, whose national rates were
computed with the as-published 80.784; CMS later cited a corrected
80.793, but our source data uses 80.784, so that is the value that
reproduces it to the cent. Update the rule, the golden pin, and the note.

Extend the CI fixture (tests/opps/fixtures/addendum_b_sample.csv) from
CY2021-2026 to all 13 years — 5 real rows per year spanning APC weights
~0.05 to ~700 — so the in-CI correctness proof now covers every year.
This commit is contained in:
kert
2026-07-08 14:52:17 -04:00
parent e9801f599e
commit a9e5d58601
3 changed files with 52 additions and 14 deletions

View File

@@ -77,7 +77,9 @@ class RuleYear(BaseModel):
# reporting (not the reduced/OQR-penalty CF). Verified against each # reporting (not the reduced/OQR-penalty CF). Verified against each
# year's Final Rule preamble and cross-checked against the empirical # year's Final Rule preamble and cross-checked against the empirical
# payment_rate / relative_weight ratio in opps.addendum_b (CY2021+). # payment_rate / relative_weight ratio in opps.addendum_b (CY2021+).
# CY2020 uses the corrected $80.793 (the raw final rule stated $80.784). # CY2020 = 80.784, the as-published NFRM value that reproduces the CY2020
# Addendum B national rates we ingest (verified to the cent). CMS later cited
# a corrected $80.793, but the archived NFRM addenda — our source — use 80.784.
RULES: dict[int, RuleYear] = { RULES: dict[int, RuleYear] = {
2014: RuleYear( 2014: RuleYear(
@@ -127,7 +129,7 @@ RULES: dict[int, RuleYear] = {
), ),
2020: RuleYear( 2020: RuleYear(
year=2020, year=2020,
conversion_factor=80.793, conversion_factor=80.784,
outlier_threshold=4625.0, outlier_threshold=4625.0,
cms_rule_id="CMS-1717-FC", cms_rule_id="CMS-1717-FC",
federal_register_citation="84 FR 61142", federal_register_citation="84 FR 61142",

View File

@@ -1,4 +1,39 @@
hcpcs,status_indicator,relative_weight,payment_rate,year hcpcs,status_indicator,relative_weight,payment_rate,year
57170,T,0.1421,10.33,2014
P9038,R,3.0182,219.34,2014
28635,T,17.2302,1252.15,2014
25151,T,35.4456,2575.9,2014
33249,Q3,442.3292,32144.95,2014
A9527,U,0.1787,13.25,2015
74160,Q3,3.2481,240.83,2015
23000,T,18.0849,1340.89,2015
75833,Q2,34.5196,2559.42,2015
33270,J1,415.4941,30806.39,2015
A9527,U,0.0968,7.14,2016
62252,S,3.2713,241.18,2016
24071,T,19.1832,1414.28,2016
27696,T,32.4936,2395.59,2016
33249,J1,413.561,30489.78,2016
C2645,U,0.0625,4.69,2017
74262,Q3,3.532,264.9,2017
17108,T,19.0286,1427.16,2017
54620,J1,33.8861,2541.49,2017
69930,J1,424.3083,31823.55,2017
P9043,R,0.1957,15.39,2018
67810,T,3.4036,267.65,2018
14000,T,19.944,1568.32,2018
93530,J1,35.7776,2813.41,2018
C9741,J1,414.903,32626.31,2018
C2645,U,0.059,4.69,2019
67938,T,3.5667,283.52,2019
15760,T,19.4862,1548.96,2019
52250,J1,36.8205,2926.86,2019
38240,J1,476.6985,37892.76,2019
C2645,U,0.0581,4.69,2020
59070,T,3.3508,270.69,2020
C5273,T,20.0852,1622.56,2020
38525,J1,37.4977,3029.21,2020
38240,J1,463.3555,37431.71,2020
C2645,U,0.0566,4.69,2021 C2645,U,0.0566,4.69,2021
37195,T,3.7532,310.75,2021 37195,T,3.7532,310.75,2021
51040,J1,21.6552,1792.99,2021 51040,J1,21.6552,1792.99,2021
1 hcpcs status_indicator relative_weight payment_rate year
2 57170 T 0.1421 10.33 2014
3 P9038 R 3.0182 219.34 2014
4 28635 T 17.2302 1252.15 2014
5 25151 T 35.4456 2575.9 2014
6 33249 Q3 442.3292 32144.95 2014
7 A9527 U 0.1787 13.25 2015
8 74160 Q3 3.2481 240.83 2015
9 23000 T 18.0849 1340.89 2015
10 75833 Q2 34.5196 2559.42 2015
11 33270 J1 415.4941 30806.39 2015
12 A9527 U 0.0968 7.14 2016
13 62252 S 3.2713 241.18 2016
14 24071 T 19.1832 1414.28 2016
15 27696 T 32.4936 2395.59 2016
16 33249 J1 413.561 30489.78 2016
17 C2645 U 0.0625 4.69 2017
18 74262 Q3 3.532 264.9 2017
19 17108 T 19.0286 1427.16 2017
20 54620 J1 33.8861 2541.49 2017
21 69930 J1 424.3083 31823.55 2017
22 P9043 R 0.1957 15.39 2018
23 67810 T 3.4036 267.65 2018
24 14000 T 19.944 1568.32 2018
25 93530 J1 35.7776 2813.41 2018
26 C9741 J1 414.903 32626.31 2018
27 C2645 U 0.059 4.69 2019
28 67938 T 3.5667 283.52 2019
29 15760 T 19.4862 1548.96 2019
30 52250 J1 36.8205 2926.86 2019
31 38240 J1 476.6985 37892.76 2019
32 C2645 U 0.0581 4.69 2020
33 59070 T 3.3508 270.69 2020
34 C5273 T 20.0852 1622.56 2020
35 38525 J1 37.4977 3029.21 2020
36 38240 J1 463.3555 37431.71 2020
37 C2645 U 0.0566 4.69 2021
38 37195 T 3.7532 310.75 2021
39 51040 J1 21.6552 1792.99 2021

View File

@@ -37,15 +37,16 @@ _FIXTURE = Path(__file__).parent / "fixtures" / "addendum_b_sample.csv"
# Full OPPS conversion factor — the rate for hospitals that MEET quality # Full OPPS conversion factor — the rate for hospitals that MEET quality
# reporting (not the reduced/OQR-penalty CF). # reporting (not the reduced/OQR-penalty CF).
# #
# Sources: # Sources (every year cross-checked BOTH ways):
# CY2014-CY2020 — each year's Federal Register OPPS/ASC Final Rule # Federal Register — each year's OPPS/ASC Final Rule preamble ("we are
# preamble ("we are using a conversion factor of $X"). # using a conversion factor of $X"), primary for CY2014-2020.
# CY2021-CY2026 — empirical payment_rate / relative_weight in # Empirical — payment_rate / relative_weight in opps.addendum_b (all 13
# opps.addendum_b (5,000+ APC-priced rows/year), which # years now ingested, 5,000+ APC-priced rows/year), which
# reproduces the published national rate to the cent. # reproduces the published national rate to the cent.
# CY2020 = 80.793 is the corrected value; the final rule as published # CY2020 = 80.784, the as-published NFRM value that reproduces the CY2020
# stated 80.784 and CMS later corrected it (the CY2021 rule # Addendum B we ingest (the archived NFRM addenda are our source).
# builds off 80.793). # CMS later cited a corrected 80.793; we track the value that
# matches our data.
# CY2016 is genuinely below CY2015 — a one-time -2.0pp packaged-lab # CY2016 is genuinely below CY2015 — a one-time -2.0pp packaged-lab
# ("two-times") adjustment more than offset the market basket. # ("two-times") adjustment more than offset the market basket.
_EXPECTED_CF: dict[int, float] = { _EXPECTED_CF: dict[int, float] = {
@@ -55,7 +56,7 @@ _EXPECTED_CF: dict[int, float] = {
2017: 75.001, 2017: 75.001,
2018: 78.636, 2018: 78.636,
2019: 79.490, 2019: 79.490,
2020: 80.793, 2020: 80.784,
2021: 82.797, 2021: 82.797,
2022: 84.177, 2022: 84.177,
2023: 85.585, 2023: 85.585,
@@ -98,8 +99,8 @@ class TestAddendumBFixtureConsistency:
Addendum B rows. Addendum B rows.
The fixture holds real published ``payment_rate`` values (never The fixture holds real published ``payment_rate`` values (never
computed), spanning APC weights from ~0.05 to ~700 across CY2021- computed), spanning APC weights from ~0.05 to ~700 across every year
CY2026. If any of those years' conversion factors is wrong, the CY2014-CY2026. If any of those years' conversion factors is wrong, the
calculated ``weight × CF`` diverges from the published rate and this calculated ``weight × CF`` diverges from the published rate and this
fails — no external dataset required, so the correctness check runs fails — no external dataset required, so the correctness check runs
in headless CI. in headless CI.
@@ -119,7 +120,7 @@ class TestAddendumBFixtureConsistency:
) )
pricer = OppsPricer() pricer = OppsPricer()
years = pricer.years_available(con) years = pricer.years_available(con)
assert years == [2021, 2022, 2023, 2024, 2025, 2026] assert years == list(range(2014, 2027))
for year in years: for year in years:
r = reconcile(pricer, con, year) r = reconcile(pricer, con, year)
assert r.matched_rows > 0, f"CY{year}: no fixture rows" assert r.matched_rows > 0, f"CY{year}: no fixture rows"