fix(pfs): member-code new payment comes from the exposure calendar, not the payment window (R84c)

new_codes_in judges a family member code new in Y only when
pfs.code_exposure has a becomes-payable row for it in Y; link targets
stay new only at their link since (R84a). The round-3 row-count
heuristic and the payability scan over ValuationRows are gone, so a
truncated valuation(years=N) window can neither invent nor hide a new
code. money_by_specialty takes exposures=; read_becomes_payable reads
them for a family's member codes.
This commit is contained in:
kert
2026-11-12 19:31:15 -05:00
parent a006ae4a72
commit e330f90cdc
2 changed files with 154 additions and 75 deletions

View File

@@ -9,7 +9,10 @@ revalued, in two public series.
``pfs.specialty_impact``.
* ``money_by_specialty`` joins a family's payment change per code (from
``pfs.valuation``) with each specialty's billing of those codes (PSPS,
``pfs.psps``) into a dollar delta per specialty.
``pfs.psps``) into a dollar delta per specialty. Whether the family gained
new payment comes from the exposure calendar (``pfs.code_exposure``,
``read_becomes_payable``) and the curated links, never from the payment
rows (R84c).
Percents are stored as fractions (``-2%`` -> ``-0.02``). A bare integer in
the Federal Register tables is a percent point, so ``1`` -> ``0.01``.
@@ -24,13 +27,15 @@ from pathlib import Path
from typing import Any, Sequence
from pfs.codetables import (
ExposureRow,
ImpactRow,
PspsRow,
read_exposures,
read_specialty_impacts,
write_specialty_impacts,
)
from pfs.commenters import specialty_key
from pfs.valuation import PAID_STATUSES, ValuationRow
from pfs.valuation import ValuationRow
__all__ = [
"ImpactRow",
@@ -39,6 +44,7 @@ __all__ = [
"money_by_specialty",
"parse_impact_508",
"parse_impact_fr_text",
"read_becomes_payable",
"read_impacts",
"write_impacts",
]
@@ -316,50 +322,44 @@ def billing_share(psps: Sequence[PspsRow], *, year: int) -> dict[str, float]:
return {k: v / whole for k, v in totals.items()}
def read_becomes_payable(con: Any, codes: Sequence[str]) -> list[ExposureRow]:
"""The exposure calendar's ``becomes-payable`` rows (``pfs.code_exposure``,
#693) for *codes* — what ``money_by_specialty`` judges a member code's
newness on (R84c). Pass the family's member codes."""
return [
r
for code in sorted({c.upper() for c in codes})
for r in read_exposures(con, code=code)
if r.kind == "becomes-payable"
]
def _link_target(link: Any) -> str:
return getattr(link, "target", None) or link.code
def new_codes_in(
payment: Sequence[ValuationRow], links: Sequence[Any], *, year: int
exposures: Sequence[Any], links: Sequence[Any], *, year: int
) -> tuple[str, ...]:
"""The family's new payment events in *year* (R84): a code payable in
*year* (status A/R/T) that was not payable, or absent, in year-1 (judged
only when year-1 is in *payment*), and every curated link with
``since == year``. A link target is never judged by payability (R84a). A year-1 before the
first supplied year is unknown, and so is a code whose row count equals the
longest history (a ``valuation(years=N)`` window cut) while starting after
the family's first year: neither is flagged (R84b)."""
payable = {
(v.code, v.year)
for v in payment
if not v.proposed and v.status.strip().upper() in PAID_STATUSES
}
by_code: dict[str, set[int]] = {}
for v in payment:
if not v.proposed:
by_code.setdefault(v.code, set()).add(v.year)
first = min((min(ys) for ys in by_code.values()), default=year)
longest = max((len(ys) for ys in by_code.values()), default=0)
"""The family's new payment events in *year* (R84): every member code
with a ``becomes-payable`` exposure row in *year*, and every curated
link with ``since == year``.
def truncated(code: str) -> bool:
# ``valuation(years=N)`` keeps the last N vintages per code, so a code
# with N rows that starts after the family's first year may have lost
# its earlier rows: its year-1 is unknown, never "absent" (R84b)
ys = by_code.get(code, set())
return bool(ys) and min(ys) > first and len(ys) == longest
targets = {getattr(link, "target", None) or link.code for link in links}
# R84a: payability counts only for the family's own member codes; a link
# target is new only in its link ``since`` year (G2211 became payable for
# office visits in 2024 but joins the home-visit family in 2026)
Member-code newness is the reviewed exposure calendar's (R84c,
``pfs.exposure``: payable = status A/R/T, left-censored at the first
RVU file, a first-year row only with FR birth evidence), never the
supplied payment rows, so a truncated ``valuation(years=N)`` window can
neither invent nor hide a new code. A link target is new only in its
link ``since`` year, whatever the calendar says (R84a: G2211 became
payable for office visits in 2024 but joins the home-visit family in
2026)."""
targets = {_link_target(link) for link in links}
found = {
c
for c, y in payable
if y == year and year - 1 >= first and c not in targets and not truncated(c)
}
found -= {c for c, y in payable if y == year - 1}
found |= {
getattr(link, "target", None) or link.code
for link in links
if link.since == year
e.code
for e in exposures
if e.kind == "becomes-payable" and e.year == year and e.code not in targets
}
found |= {_link_target(link) for link in links if link.since == year}
return tuple(sorted(found))
@@ -369,6 +369,7 @@ def money_by_specialty(
*,
year: int,
links: Sequence[Any] = (),
exposures: Sequence[Any] = (),
) -> list[SpecialtyMoney]:
"""Per specialty (``specialty_key``), the family's payment change
in *year*, largest billers first. A code missing either year's final
@@ -376,12 +377,15 @@ def money_by_specialty(
``services`` and ``share``. Services are PSPS *submitted* counts, which
include denied services.
*links* (``pfs.curated.CuratedLink`` rows: ``target``/``since``; a
``pfs.code_family`` row's ``code`` works too) add the curated links that
begin in *year*. New payment is a family-level
event (R84): when a code or link begins in *year*, every specialty with
family services in year-1 gets ``new_payment=True``, the codes in
``new_codes`` and ``delta_note="new, amount n/a"``."""
*exposures* are the family's member codes' exposure-calendar rows
(``read_becomes_payable``; any ``ExposureRow``-shaped rows, other kinds
ignored) and *links* (``pfs.curated.CuratedLink`` rows: ``target``/
``since``; a ``pfs.code_family`` row's ``code`` works too) the curated
links. New payment is a family-level event (R84, ``new_codes_in``): when
a member code becomes payable or a link begins in *year*, every
specialty with family services in year-1 gets ``new_payment=True``,
the codes in ``new_codes`` and ``delta_note="new, amount n/a"``. The
payment rows only price the deltas."""
final: dict[tuple[str, int], float | None] = {}
proposed: dict[str, float | None] = {}
for v in payment:
@@ -391,7 +395,7 @@ def money_by_specialty(
else:
final[(v.code, v.year)] = v.pay_nf
services = _latest_services(psps, year)
new_codes = new_codes_in(payment, links, year=year)
new_codes = new_codes_in(exposures, links, year=year)
acc: dict[str, dict[str, Any]] = {}
for code, per in services.items():
now, before = final.get((code, year)), final.get((code, year - 1))

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import datetime as dt
from pathlib import Path
import duckdb
@@ -10,7 +11,13 @@ import pytest
from bib.item import Source
from bib.store import Store
from pfs import stakes
from pfs.codetables import ImpactRow, PspsRow, ensure_tables
from pfs.codetables import (
ExposureRow,
ImpactRow,
PspsRow,
ensure_tables,
write_exposures,
)
from pfs.stakes import (
billing_share,
discover_508,
@@ -287,6 +294,28 @@ def ps(year, code, spec, n):
return PspsRow(year, code, "00", spec, n, None, None, "d", "K")
def exp(code, year, kind="becomes-payable"):
"""An exposure-calendar row (pfs.code_exposure, #693)."""
return ExposureRow(
code=code,
family="",
effective_date=dt.date(year, 1, 1),
year=year,
kind=kind,
status="A",
prior_status="",
rvu_band=0,
non_fac_total=0.0,
delta_pct=0.0,
item_key="",
p_id=0,
page=0,
anchor_kind="",
correction=False,
note="",
)
def test_money_by_specialty_exact():
payment = [val("99350", 2022, 100.0), val("99350", 2023, 90.0)]
psps = [
@@ -321,13 +350,18 @@ def test_missing_year_gives_none_delta_but_counts_in_share():
ps(2022, "A", "Cardiology", None), # suppressed, skipped
ps(2024, "A", "Family Practice", 999.0), # after year-1, ignored
]
by = {m.specialty: m for m in money_by_specialty(payment, psps, year=2023)}
by = {
m.specialty: m
for m in money_by_specialty(
payment, psps, year=2023, exposures=[exp("B", 2023)]
)
}
fp, ca = by["FAMILY PRACTICE"], by["CARDIOLOGY"]
assert fp.services == 20.0 and fp.share == pytest.approx(20 / 45)
assert fp.delta_total == -500.0 # only code A carries a delta
assert fp.delta_per_service == -50.0 # per service of A, not of B
assert fp.delta_pct == pytest.approx(-0.5)
assert fp.new_payment is True # B is first payable in 2023
assert fp.new_payment is True # the calendar: B becomes payable in 2023
assert ca.services == 25.0
assert (
ca.delta_total is None and ca.delta_per_service is None and ca.delta_pct is None
@@ -422,7 +456,7 @@ def test_new_code_with_no_psps_rows_flips_new_payment_for_every_specialty():
ps(2022, "99350", "Family Practice", 10.0),
ps(2022, "99350", "Cardiology", 5.0),
]
out = money_by_specialty(payment, psps, year=2023)
out = money_by_specialty(payment, psps, year=2023, exposures=[exp("G0318", 2023)])
assert [m.new_payment for m in out] == [True, True]
assert all(m.new_codes == ("G0318",) for m in out)
assert all(m.delta_note == "new, amount n/a" for m in out)
@@ -441,56 +475,97 @@ def test_curated_link_since_year_is_a_new_payment_event():
assert not m.new_payment and m.new_codes == () and m.delta_note == ""
def test_not_new_when_prior_year_payable_or_outside_the_window():
# B was payable in 2022 (A->now still A): not new; D has no 2022 row at
# all but 2022 is present for other codes: new; E in a window without
# year-1 cannot be judged: not new
def test_payment_rows_alone_never_flag_a_member_code():
# R84c: D absent in 2022 and X non-payable in 2022 looked "new" in 2023
# from the payment rows; newness now comes only from the calendar
payment = [
val("B", 2022, 5.0),
val("B", 2023, 5.0),
val("D", 2023, 5.0),
val("X", 2022, 5.0, status="B"),
val("X", 2023, 5.0), # not payable in 2022 -> payable 2023: new
val("X", 2023, 5.0),
]
psps = [ps(2022, "B", "Family Practice", 1.0)]
(m,) = money_by_specialty(payment, psps, year=2023)
assert m.new_codes == ("D", "X")
only23 = [val("E", 2023, 5.0)]
(m,) = money_by_specialty(
only23, [ps(2022, "E", "Family Practice", 1.0)], year=2023
)
assert m.new_codes == () and not m.new_payment
assert m.new_codes == () and not m.new_payment and m.delta_note == ""
def test_member_code_is_new_in_its_becomes_payable_year_only():
payment = [val("B", 2022, 5.0), val("B", 2023, 5.0), val("B", 2024, 5.0)]
psps = [ps(2022, "B", "Family Practice", 1.0)]
exposures = [
exp("X", 2023),
exp("D", 2024), # another year
exp("B", 2023, "revalued"), # other calendar kinds never flag
exp("B", 2023, "status-change"),
exp("E", 2023, "ends"),
exp("T", 2023, "telehealth-listed"),
]
flags = {
y: money_by_specialty(payment, psps, year=y, exposures=exposures)[0]
for y in (2023, 2024, 2025)
}
assert {y: m.new_codes for y, m in flags.items()} == {
2023: ("X",),
2024: ("D",),
2025: (),
}
assert flags[2023].new_payment and flags[2023].delta_note == "new, amount n/a"
assert not flags[2025].new_payment
def test_link_target_flags_only_in_its_since_year_not_when_first_payable():
# G2211-shaped: payable from 2024, link since 2026
# G2211-shaped: the calendar has it becoming payable in 2024, link since 2026
payment = [val("99350", y, 100.0) for y in (2023, 2024, 2025, 2026)]
payment += [val("G2211", y, 15.0) for y in (2024, 2025, 2026)]
psps = [ps(2022, "99350", "Family Practice", 10.0)]
links = [Link("G2211", 2026)]
exposures = [exp("G2211", 2024)]
flags = {
y: money_by_specialty(payment, psps, year=y, links=links)[0].new_codes
y: money_by_specialty(payment, psps, year=y, links=links, exposures=exposures)[
0
].new_codes
for y in (2024, 2025, 2026)
}
assert flags == {2024: (), 2025: (), 2026: ("G2211",)}
# a member code (not a link target) still flags by payability
payment.append(val("G0318", 2024, 1.0))
(m,) = money_by_specialty(payment, psps, year=2024, links=links)
# a member code (not a link target) still flags by its calendar row
exposures.append(exp("G0318", 2024))
(m,) = money_by_specialty(
payment, psps, year=2024, links=links, exposures=exposures
)
assert m.new_codes == ("G0318",)
def test_truncated_window_flags_nothing_in_its_first_year():
def test_truncated_payment_window_neither_flags_nor_hides_a_code():
# valuation(years=7) shape: predecessors 2016-2022, current codes cut to
# 2021-2027 (their earlier rows fell out of the window), a real new code
# 2021-2027. NEWC is genuinely new in 2021 with the same row count as the
# longest history (the round-3 heuristic hid it); 99350 is not new
payment = [val("99324", y, 50.0) for y in range(2016, 2023)]
payment += [val("99350", y, 100.0) for y in range(2021, 2028)]
payment += [val("NEWC", y, 20.0) for y in range(2021, 2028)]
payment += [val("G0318", y, 15.0) for y in range(2023, 2028)]
psps = [ps(2015, "99350", "Family Practice", 10.0)]
exposures = [exp("NEWC", 2021), exp("G0318", 2023), exp("99324", 2023, "ends")]
new = {
y: money_by_specialty(payment, psps, year=y)[0].new_codes
y: money_by_specialty(payment, psps, year=y, exposures=exposures)[0].new_codes
for y in range(2016, 2028)
}
assert new[2021] == () # 99350's year-1 is outside its window
assert new[2016] == () # before the first supplied year
assert new[2023] == ("G0318",)
assert [y for y, c in new.items() if c] == [2023]
assert {y: c for y, c in new.items() if c} == {2021: ("NEWC",), 2023: ("G0318",)}
def test_read_becomes_payable(con):
write_exposures(
con,
[
exp("G0318", 2023),
exp("G0318", 2023, "telehealth-listed"),
exp("99350", 2027, "telehealth-listed"),
exp("99343", 2023, "ends"),
exp("OTHER", 2020),
],
)
got = stakes.read_becomes_payable(con, ["g0318", "99350", "99343"])
assert [(r.code, r.year, r.kind) for r in got] == [
("G0318", 2023, "becomes-payable")
]
assert stakes.read_becomes_payable(con, []) == []