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:
@@ -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))
|
||||
|
||||
@@ -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, []) == []
|
||||
|
||||
Reference in New Issue
Block a user