feat(notebooks): CY2027 NPRM — GPCI & locality section + primary-care (ACO attribution) per-service analysis (closes #631, closes #632)

This commit is contained in:
kert
2026-08-17 12:24:01 -04:00
parent fc26d5fc54
commit 8c4e13c854

View File

@@ -29,11 +29,12 @@ def _(mo):
and `qpp.QPP` (the repo's rule registries) and the `pfs`/`cms` and `qpp.QPP` (the repo's rule registries) and the `pfs`/`cms`
DuckLake schemas — nothing here is a hard-coded figure. Columns are DuckLake schemas — nothing here is a hard-coded figure. Columns are
explicitly labeled **"final rule pending"** rather than fabricating explicitly labeled **"final rule pending"** rather than fabricating
a CY2027-final column that does not exist yet. All dollar figures a CY2027-final column that does not exist yet. Dollar figures in
use **national, GPCI-unadjusted RVUs (GPCI = 1.0)** — a deliberate Sections 1-2 and 4 are **national, GPCI-unadjusted (GPCI = 1.0)**
scope simplification, disclosed once here rather than on every unless a figure says otherwise; **Section 3** carries the locality
figure; real payment varies by locality via `pfs.gpci`, which this story — the CY2027 NPRM's proposed GPCIs (`pfs.gpci_proposed`,
notebook does not join. Addendum E) against the final-rule history in `pfs.gpci`, with
locality-adjusted payment examples.
""") """)
return return
@@ -50,9 +51,13 @@ def _():
connect.theme() connect.theme()
# connect.theme() puts assets/ on sys.path — reuse the HTI-5 design # connect.theme() puts assets/ on sys.path — reuse the HTI-5 design
# tokens directly instead of re-typing hex literals for the gain/loss # tokens directly instead of re-typing hex literals: AMBER/TEAL are
# diverging pair used in Section 2. # the gain/loss diverging pair (Sections 2-4), INDIGO_MED the
from fhirworx import AMBER, TEAL # default series, PLUM/GRAY the fixed categorical follow-ons for
# the GPCI locality lines (Section 3).
from fhirworx import AMBER, GRAY, INDIGO_MED, PLUM, TEAL
from pfs.calcs.payment import payment as pfs_payment
# PFS reference data lives in the DuckLake lakehouse (M5, #514) — # PFS reference data lives in the DuckLake lakehouse (M5, #514) —
# notebooks connect read-only. # notebooks connect read-only.
@@ -61,7 +66,22 @@ def _():
def q(sql): def q(sql):
return con.execute(sql).pl() return con.execute(sql).pl()
return AMBER, QPP, RULES, TEAL, alt, con, for_payment_year, pl, proposed_for, q return (
AMBER,
GRAY,
INDIGO_MED,
PLUM,
QPP,
RULES,
TEAL,
alt,
con,
for_payment_year,
pfs_payment,
pl,
proposed_for,
q,
)
# ── 1. The conversion-factor walk ───────────────────────────────────── # ── 1. The conversion-factor walk ─────────────────────────────────────
@@ -493,13 +513,777 @@ def _(cross_nprm, mo):
return return
# ── 3. Advanced APM ───────────────────────────────────────────────────── # ── 3. GPCI & Locality ──────────────────────────────────────────────────
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(mo): def _(mo):
mo.md(""" mo.md("""
## 3. Advanced APM (QP) Requirements ## 3. GPCI & Locality — Proposed CY2027 Geographic Indices
Geographic Practice Cost Indices scale each RVU component to local
input costs: `payment = (work_rvu x work_gpci + pe_rvu x pe_gpci +
mp_rvu x mp_gpci) x CF`. Every dollar figure in Sections 1-2 is
national/unadjusted (all GPCIs = 1.0); this section is where
locality enters, from `pfs.gpci_proposed` — the NPRM **Addendum E**
workbooks, ingested per rule partition exactly like
`pfs.rvu_proposed`.
GPCIs update on a **biennial** cycle, so the table holds three
partition-years: the CY2026 NPRM (`CMS-1832-P`) proposed CY2026
values *and* CY2027 phase-in values, and the CY2027 NPRM
(`CMS-1848-P`) re-proposes CY2027. That makes "did CMS change its
mind about 2027?" a plain self-join, done below.
> **Work-floor caveat:** the proposed work GPCIs are published
> **without** the statutory 1.0 work floor — that is the Addendum E
> column heading's own wording, while the CY2025 baseline column in
> the CY2026-NPRM workbook was published *with* the floor. Values
> here are shown exactly as published; whether a floor applies to
> actual CY2027 payment depends on legislation not adjudicated in
> this notebook.
""")
return
@app.cell(hide_code=True)
def _(q):
gpci_2027p = q("""
WITH prop AS (
SELECT mac, state, locality, locality_name,
work_gpci, pe_gpci, mp_gpci
FROM pfs.gpci_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND gpci_year = 2027
),
fin AS (
SELECT mac, locality,
work_gpci AS work_gpci_2026f,
pe_gpci AS pe_gpci_2026f,
mp_gpci AS mp_gpci_2026f
FROM pfs.gpci WHERE year = 2026
)
SELECT p.*,
f.work_gpci_2026f, f.pe_gpci_2026f, f.mp_gpci_2026f,
round(p.work_gpci - f.work_gpci_2026f, 4) AS d_work,
round(p.pe_gpci - f.pe_gpci_2026f, 4) AS d_pe,
round(p.mp_gpci - f.mp_gpci_2026f, 4) AS d_mp,
round((p.work_gpci + p.pe_gpci + p.mp_gpci) / 3
- (f.work_gpci_2026f + f.pe_gpci_2026f + f.mp_gpci_2026f) / 3,
4) AS d_mean
FROM prop p
JOIN fin f USING (mac, locality)
""")
gpci_2027p
return (gpci_2027p,)
@app.cell(hide_code=True)
def _(AMBER, TEAL, alt, gpci_2027p, mo, pl, proposed_for):
_n = 15
_up = (gpci_2027p["d_mean"] > 0).sum()
_down = (gpci_2027p["d_mean"] < 0).sum()
_flat = (gpci_2027p["d_mean"] == 0).sum()
_movers = (
gpci_2027p.sort("d_mean", descending=True)
.head(_n)
.vstack(gpci_2027p.sort("d_mean").head(_n).sort("d_mean", descending=True))
.with_columns(
(pl.col("locality_name") + " (" + pl.col("state") + ")").alias("label"),
pl.when(pl.col("d_mean") >= 0)
.then(pl.lit("Gain"))
.otherwise(pl.lit("Loss"))
.alias("sign"),
)
)
gpci_movers_chart = (
alt.Chart(_movers.to_pandas())
.mark_bar()
.encode(
x=alt.X("d_mean:Q", title="Mean GPCI change (unweighted mean of work/PE/MP)"),
y=alt.Y("label:N", title=None, sort=alt.SortField(field="d_mean", order="descending")),
color=alt.Color(
"sign:N",
title=None,
scale=alt.Scale(domain=["Gain", "Loss"], range=[TEAL, AMBER]),
legend=alt.Legend(orient="top"),
),
tooltip=[
"locality_name", "state", "mac", "locality",
alt.Tooltip("d_work:Q", format="+.4f"),
alt.Tooltip("d_pe:Q", format="+.4f"),
alt.Tooltip("d_mp:Q", format="+.4f"),
],
)
.properties(
title="GPCI Movers — CY2027 Proposed vs. CY2026 Final, Top ±15 Localities",
width=640,
height=520,
)
)
mo.vstack(
[
gpci_movers_chart,
mo.md(f"""
Across the {gpci_2027p.height} payment localities matched to
the CY2026-final GPCI table: **{_up} rise, {_down} fall,
{_flat} unchanged** on the unweighted mean of the three
components (a GAF proxy — CMS's official GAFs in Addendum D
are utilization-weighted and not loaded here).
**Sources:** `pfs.gpci_proposed` (`CMS-1848-P`, gpci_year
2027) — CY2027 NPRM Addendum E,
{proposed_for(2027).federal_register_citation}. Baseline —
`pfs.gpci` year 2026 (CY2026 Final).
"""),
]
)
return
@app.cell(hide_code=True)
def _(mo, q):
_drift = q("""
WITH projected AS (
SELECT mac, locality, locality_name, state,
work_gpci AS w_1832, pe_gpci AS p_1832, mp_gpci AS m_1832
FROM pfs.gpci_proposed
WHERE cms_rule_id = 'CMS-1832-P' AND gpci_year = 2027
),
reproposed AS (
SELECT mac, locality,
work_gpci AS w_1848, pe_gpci AS p_1848, mp_gpci AS m_1848
FROM pfs.gpci_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND gpci_year = 2027
)
SELECT a.locality_name, a.state,
a.w_1832, b.w_1848, a.p_1832, b.p_1848, a.m_1832, b.m_1848,
round(abs(b.w_1848 - a.w_1832) + abs(b.p_1848 - a.p_1832)
+ abs(b.m_1848 - a.m_1832), 4) AS total_abs_drift
FROM projected a
JOIN reproposed b USING (mac, locality)
ORDER BY total_abs_drift DESC
""")
_changed = _drift.filter(_drift["total_abs_drift"] > 0)
if _changed.height == 0:
_msg = (
f"**All {_drift.height} localities carry identical CY2027 GPCIs in "
"both rulemakings** — the CY2027 NPRM re-proposes the CY2026 "
"NPRM's phase-in values unchanged."
)
else:
_msg = (
f"**{_changed.height} of {_drift.height} localities moved** between "
"the CY2026 NPRM's projected 2027 values and the CY2027 NPRM's "
"re-proposed values (largest movers below)."
)
_parts = [
mo.md(f"""
### Did CMS change its 2027 mind? — CMS-1832-P projection vs. CMS-1848-P proposal
{_msg}
""")
]
if _changed.height > 0:
_parts.append(
mo.ui.table(
_changed.head(15).to_pandas(),
label="Largest 1832-P → 1848-P CY2027 GPCI revisions",
)
)
mo.vstack(_parts)
return
@app.cell(hide_code=True)
def _(GRAY, INDIGO_MED, PLUM, alt, mo, pl, q):
# Long-term: highest- and lowest-cost localities (by CY2026-final mean
# GPCI) against the all-locality median, 2016-2026 final + 2027
# proposed (flagged).
_extremes = q("""
SELECT mac, locality, locality_name, state,
(work_gpci + pe_gpci + mp_gpci) / 3 AS mean_gpci
FROM pfs.gpci WHERE year = 2026
ORDER BY mean_gpci DESC
""")
_hi = _extremes.row(0, named=True)
_lo = _extremes.row(-1, named=True)
_hist = q(f"""
WITH sel AS (
SELECT year, locality_name, work_gpci, pe_gpci, mp_gpci
FROM pfs.gpci
WHERE (mac = '{_hi["mac"]}' AND locality = '{_hi["locality"]}')
OR (mac = '{_lo["mac"]}' AND locality = '{_lo["locality"]}')
),
med AS (
SELECT year, 'All-locality median' AS locality_name,
median(work_gpci) AS work_gpci,
median(pe_gpci) AS pe_gpci,
median(mp_gpci) AS mp_gpci
FROM pfs.gpci GROUP BY year
),
prop AS (
SELECT gpci_year AS year, locality_name, work_gpci, pe_gpci, mp_gpci
FROM pfs.gpci_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND gpci_year = 2027
AND ((mac = '{_hi["mac"]}' AND locality = '{_hi["locality"]}')
OR (mac = '{_lo["mac"]}' AND locality = '{_lo["locality"]}'))
),
propmed AS (
SELECT gpci_year AS year, 'All-locality median' AS locality_name,
median(work_gpci) AS work_gpci,
median(pe_gpci) AS pe_gpci,
median(mp_gpci) AS mp_gpci
FROM pfs.gpci_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND gpci_year = 2027
GROUP BY gpci_year
)
SELECT *, false AS proposed FROM sel
UNION ALL SELECT *, false FROM med
UNION ALL SELECT *, true FROM prop
UNION ALL SELECT *, true FROM propmed
ORDER BY year, locality_name
""")
_long = _hist.unpivot(
index=["year", "locality_name", "proposed"],
on=["work_gpci", "pe_gpci", "mp_gpci"],
variable_name="component",
value_name="gpci",
).with_columns(
pl.col("component").replace(
{"work_gpci": "Work", "pe_gpci": "PE", "mp_gpci": "MP"}
)
)
_domain = [_hi["locality_name"], _lo["locality_name"], "All-locality median"]
_range = [INDIGO_MED, PLUM, GRAY]
_charts = []
for _comp in ("Work", "PE", "MP"):
_cdf = _long.filter(pl.col("component") == _comp).to_pandas()
_line = (
alt.Chart(_cdf[~_cdf["proposed"]])
.mark_line(point=True)
.encode(
x=alt.X("year:O", title="Year"),
y=alt.Y("gpci:Q", title="GPCI", scale=alt.Scale(zero=False)),
color=alt.Color(
"locality_name:N",
title=None,
scale=alt.Scale(domain=_domain, range=_range),
legend=alt.Legend(orient="bottom") if _comp == "PE" else None,
),
tooltip=["locality_name", "year:O", alt.Tooltip("gpci:Q", format=".4f")],
)
)
_prop_pts = (
alt.Chart(_cdf[_cdf["proposed"]])
.mark_point(shape="diamond", size=90, filled=True)
.encode(
x="year:O",
y="gpci:Q",
color=alt.Color(
"locality_name:N",
scale=alt.Scale(domain=_domain, range=_range),
legend=None,
),
tooltip=["locality_name", "year:O", alt.Tooltip("gpci:Q", format=".4f")],
)
)
_charts.append(
(_line + _prop_pts).properties(title=f"{_comp} GPCI", width=240, height=220)
)
gpci_longterm_chart = alt.hconcat(*_charts)
mo.vstack(
[
gpci_longterm_chart,
mo.md(f"""
Final-rule GPCIs 2016-2026 (`pfs.gpci`) for the
highest-cost locality (**{_hi["locality_name"]}**,
{_hi["state"]}) and lowest-cost locality
(**{_lo["locality_name"]}**, {_lo["state"]}) by CY2026 mean
GPCI, against the all-locality median. Diamond markers at
2027 are the **CY2027 NPRM proposal — final rule pending**,
with the work-floor caveat above applying to the Work
panel.
"""),
]
)
return
@app.cell(hide_code=True)
def _(gpci_2027p, mo, pfs_payment, pl, proposed_for, q):
# Locality-adjusted payment for the Section-1 example trio at the
# best/worst CY2027-proposed localities vs the national (GPCI=1.0)
# rate, all priced at the CY2027 proposed non-QP CF.
_best = gpci_2027p.sort("d_mean", descending=True).row(0, named=True)
_worst = gpci_2027p.sort("d_mean").row(0, named=True)
_rvu = q("""
SELECT hcpcs, description, work_rvu, non_fac_pe_rvu, mp_rvu
FROM pfs.rvu
WHERE year = 2026 AND (mod IS NULL OR mod = '')
AND hcpcs IN ('99213', '27447', '70553')
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
""")
_localities = pl.DataFrame(
[
{
"mac": r["mac"],
"locality": r["locality"],
"where": f"{r['locality_name']} ({r['state']})",
"work_gpci": r["work_gpci"],
"pe_gpci": r["pe_gpci"],
"mp_gpci": r["mp_gpci"],
}
for r in (_best, _worst)
]
+ [
{
"mac": "NATL",
"locality": "00",
"where": "National (GPCI = 1.0)",
"work_gpci": 1.0,
"pe_gpci": 1.0,
"mp_gpci": 1.0,
}
]
)
_rvu_x_loc = _rvu.join(_localities.select("mac", "locality"), how="cross")
_priced = pfs_payment(
_rvu_x_loc,
_localities.select("mac", "locality", "work_gpci", "pe_gpci", "mp_gpci"),
cf=proposed_for(2027).conversion_factor,
)
gpci_payment_table = (
_priced.join(_localities.select("mac", "locality", "where"), on=["mac", "locality"])
.with_columns(pl.col("payment_amount").round(2))
.pivot(on="where", index=["hcpcs", "description"], values="payment_amount")
)
mo.vstack(
[
mo.ui.table(gpci_payment_table.to_pandas(), label="Locality-adjusted payment — CY2027 proposed non-QP CF"),
mo.md(f"""
`pfs.calcs.payment.payment` (the module's locality formula)
applied to CY2026-final RVUs at the CY2027 **proposed**
non-QP CF ({proposed_for(2027).conversion_factor}) — final
rule pending. Localities shown are the CY2027 proposal's
biggest mean-GPCI gainer (**{_best["locality_name"]}**) and
decliner (**{_worst["locality_name"]}**), with the
national GPCI = 1.0 rate for reference.
"""),
]
)
return
# ── 4. Primary Care Services (ACO attribution set) ──────────────────────
@app.cell(hide_code=True)
def _(mo, q):
pcs_codes = q("""
SELECT hcpcs_code AS hcpcs, trim(description) AS pcs_description
FROM cms.primary_care_service_code
ORDER BY hcpcs
""")
assert pcs_codes.height == 109, (
f"expected 109 designated primary care service codes, got {pcs_codes.height}"
)
mo.md(f"""
## 4. Primary Care Services — the ACO Attribution Set
The **{pcs_codes.height} designated primary care service codes**
used for Medicare Shared Savings Program beneficiary assignment (42
CFR § 425.400(c)), read live from
`cms.primary_care_service_code` — the attribution reference list
published to the lake from the aco module. These are the services
whose billing patterns decide which ACO a beneficiary is attributed
to, so their PFS valuation moves both clinician revenue **and** the
attribution denominators every MSSP financial calculation stands
on.
Two disclosures, once: (1) this is the **current-law list applied
retrospectively** — the designation set itself changes across
rulemakings and is not versioned by year in this repo; (2) all
aggregate statements are **unweighted by utilization** (the lake
carries no claims-volume reference), so each code counts equally.
""")
return (pcs_codes,)
@app.cell(hide_code=True)
def _(RULES, pl, proposed_for, q):
_pqm_raw = q("""
WITH pcs AS (
SELECT hcpcs_code AS hcpcs, trim(description) AS pcs_description
FROM cms.primary_care_service_code
),
prop AS (
SELECT hcpcs, status_code AS status_2027p,
work_rvu AS work_2027p,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_2027p
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND (mod IS NULL OR mod = '')
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
),
fin AS (
SELECT hcpcs, status_code AS status_2026f,
work_rvu AS work_2026f,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_2026f
FROM pfs.rvu
WHERE year = 2026 AND (mod IS NULL OR mod = '')
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
)
SELECT pcs.hcpcs, pcs.pcs_description,
fin.status_2026f, fin.work_2026f, fin.total_2026f,
prop.status_2027p, prop.work_2027p, prop.total_2027p
FROM pcs
LEFT JOIN fin USING (hcpcs)
LEFT JOIN prop USING (hcpcs)
ORDER BY pcs.hcpcs
""")
_cf26 = RULES[2026].conversion_factor
_cf27p = proposed_for(2027).conversion_factor
pqm_near = _pqm_raw.with_columns(
(pl.col("total_2026f") * _cf26).round(2).alias("dollar_2026final"),
(pl.col("total_2027p") * _cf27p).round(2).alias("dollar_2027proposed"),
).with_columns(
(pl.col("dollar_2027proposed") - pl.col("dollar_2026final"))
.round(2)
.alias("dollar_delta"),
pl.when(pl.col("total_2026f").is_not_null() & pl.col("total_2027p").is_not_null())
.then(
(100 * (pl.col("dollar_2027proposed") - pl.col("dollar_2026final"))
/ pl.col("dollar_2026final")).round(2)
)
.alias("pct_change"),
pl.when(pl.col("total_2026f").is_not_null() & pl.col("total_2027p").is_not_null())
.then(pl.lit("both"))
.when(pl.col("total_2027p").is_not_null())
.then(pl.lit("new_in_2027"))
.when(pl.col("total_2026f").is_not_null())
.then(pl.lit("missing_from_2027"))
.otherwise(pl.lit("absent_both"))
.alias("coverage"),
)
assert pqm_near.height == 109, pqm_near.height
return (pqm_near,)
@app.cell(hide_code=True)
def _(RULES, mo, pqm_near, proposed_for):
mo.vstack(
[
mo.md(f"""
### Near-term — every designated code, CY2026 final vs. CY2027 proposed
One row per designated code: CY2026-final and
CY2027-proposed work/total RVUs (non-facility PE where
priced, facility otherwise), dollars at each vintage's own
standard CF (CY2026 final {RULES[2026].conversion_factor};
CY2027 proposed non-QP
{proposed_for(2027).conversion_factor} — **final rule
pending**), national unadjusted; Section 3 carries the
locality adjustment.
"""),
mo.ui.table(
pqm_near.sort("pct_change", nulls_last=True).to_pandas(),
page_size=25,
label="All 109 designated primary care services — near-term comparison",
),
]
)
return
@app.cell(hide_code=True)
def _(AMBER, TEAL, alt, mo, pl, pqm_near):
_both = pqm_near.filter(pl.col("coverage") == "both").with_columns(
pl.when(pl.col("pct_change") >= 0)
.then(pl.lit("Gain"))
.otherwise(pl.lit("Loss"))
.alias("sign")
)
_one_sided = pqm_near.filter(pl.col("coverage") != "both")
pqm_near_chart = (
alt.Chart(_both.to_pandas())
.mark_bar()
.encode(
x=alt.X("pct_change:Q", title="% change in national payment, CY2026 final → CY2027 proposed"),
y=alt.Y("hcpcs:N", title=None, sort=alt.SortField(field="pct_change", order="descending")),
color=alt.Color(
"sign:N",
title=None,
scale=alt.Scale(domain=["Gain", "Loss"], range=[TEAL, AMBER]),
legend=alt.Legend(orient="top"),
),
tooltip=[
"hcpcs", "pcs_description",
alt.Tooltip("dollar_2026final:Q", format="$.2f"),
alt.Tooltip("dollar_2027proposed:Q", format="$.2f"),
alt.Tooltip("pct_change:Q", format="+.2f"),
],
)
.properties(
title="Designated Primary Care Services — % Payment Change (every matched code)",
width=640,
height=max(300, 11 * _both.height),
)
)
_sided_lines = "\n".join(
f" - `{r['hcpcs']}` — {r['pcs_description']} (*{r['coverage'].replace('_', ' ')}*)"
for r in _one_sided.iter_rows(named=True)
)
mo.vstack(
[
pqm_near_chart,
mo.md(f"""
**{_both.height} of 109** designated codes price in both
tables and appear above. The remaining
**{_one_sided.height}** are one-sided or unpriced and are
listed explicitly rather than plotted with a fabricated
baseline:
{_sided_lines}
The one-sided groups are computed live, so their
composition tracks the data: as of this ingest every
designated code that prices anywhere prices in **both**
tables (the APCM G-codes already carry CY2026-final RVUs),
and the unmatched remainder is entirely `absent_both` —
deleted or replaced legacy codes (telephone E/M 99441-3,
prolonged 99354-5, consult-era visits, pre-APCM
check-in/CCM G-codes) that stay on the attribution
designation list without a current PFS price.
"""),
]
)
return
@app.cell(hide_code=True)
def _(RULES, pl, proposed_for, q):
_hist = q("""
WITH pcs AS (
SELECT hcpcs_code AS hcpcs, trim(description) AS pcs_description
FROM cms.primary_care_service_code
),
fin AS (
SELECT year, hcpcs,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_rvu
FROM pfs.rvu
WHERE (mod IS NULL OR mod = '')
QUALIFY row_number() OVER (PARTITION BY hcpcs, year ORDER BY hcpcs) = 1
),
prop AS (
SELECT 2027 AS year, hcpcs,
work_rvu + coalesce(non_fac_pe_rvu, fac_pe_rvu) + mp_rvu
AS total_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND (mod IS NULL OR mod = '')
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
)
SELECT pcs.hcpcs, pcs.pcs_description, u.year, u.total_rvu,
u.year = 2027 AS proposed
FROM pcs
JOIN (SELECT * FROM fin UNION ALL SELECT * FROM prop) u USING (hcpcs)
WHERE u.total_rvu IS NOT NULL
ORDER BY pcs.hcpcs, u.year
""")
_cf = pl.DataFrame(
{
"year": [y for y in sorted(RULES) if y >= 2015] + [2027],
"cf": [RULES[y].conversion_factor for y in sorted(RULES) if y >= 2015]
+ [proposed_for(2027).conversion_factor],
}
)
pqm_long = (
_hist.join(_cf, on="year")
.with_columns((pl.col("total_rvu") * pl.col("cf")).round(2).alias("dollar"))
.with_columns(
(
100
* (pl.col("total_rvu") / pl.col("total_rvu").shift(1).over("hcpcs") - 1)
)
.round(2)
.alias("rvu_yoy_pct")
)
)
return (pqm_long,)
@app.cell(hide_code=True)
def _(AMBER, TEAL, alt, mo, pqm_long):
_hm = pqm_long.to_pandas()
pqm_heatmap = (
alt.Chart(_hm)
.mark_rect(stroke="#F7F5F0", strokeWidth=1)
.encode(
x=alt.X("year:O", title="Year (2027 = proposed)"),
y=alt.Y("hcpcs:N", title=None),
color=alt.Color(
"rvu_yoy_pct:Q",
title="RVU YoY %",
scale=alt.Scale(domainMid=0, range=[AMBER, "#EDEBE6", TEAL]),
),
tooltip=[
"hcpcs", "pcs_description", "year:O",
alt.Tooltip("total_rvu:Q", format=".2f"),
alt.Tooltip("dollar:Q", format="$.2f"),
alt.Tooltip("rvu_yoy_pct:Q", format="+.2f"),
],
)
.properties(
title="Total-RVU Year-over-Year % Change — Every Designated Code, 2015-2027p",
width=560,
height=11 * _hm["hcpcs"].nunique(),
)
)
mo.vstack(
[
pqm_heatmap,
mo.md("""
Long-term valuation of **each individual designated code**:
cells are year-over-year % change in total RVUs (first
available year blank). Sustained white rows are codes CMS
has left untouched for a decade; the visible vertical band
at 2021 is the office/outpatient E/M revaluation. Hover any
cell for that code-year's RVUs and dollars.
"""),
]
)
return
@app.cell(hide_code=True)
def _(INDIGO_MED, alt, mo, pl, pqm_long):
_span = (
pqm_long.sort("hcpcs", "year")
.group_by("hcpcs", "pcs_description")
.agg(
pl.col("dollar").first().alias("dollar_first"),
pl.col("dollar").last().alias("dollar_last"),
pl.len().alias("n_years"),
)
.filter(pl.col("n_years") >= 5)
.with_columns((pl.col("dollar_last") - pl.col("dollar_first")).abs().alias("abs_move"))
.sort("abs_move", descending=True)
.head(12)
)
_sel = pqm_long.filter(pl.col("hcpcs").is_in(_span["hcpcs"]))
_rows = []
_codes = _span["hcpcs"].to_list()
for _i in range(0, len(_codes), 4):
_row_charts = []
for _code in _codes[_i : _i + 4]:
_cdf = _sel.filter(pl.col("hcpcs") == _code).to_pandas()
_solid = (
alt.Chart(_cdf[~_cdf["proposed"]])
.mark_line(point=True, color=INDIGO_MED)
.encode(
x=alt.X("year:O", title=None, axis=alt.Axis(values=[2015, 2020, 2026])),
y=alt.Y("dollar:Q", title=None, scale=alt.Scale(zero=False)),
tooltip=["hcpcs", "year:O", alt.Tooltip("dollar:Q", format="$.2f")],
)
)
_prop = (
alt.Chart(_cdf[_cdf["year"] >= 2026])
.mark_line(point=alt.OverlayMarkDef(shape="diamond", size=70), color=INDIGO_MED, strokeDash=[4, 3])
.encode(
x="year:O",
y="dollar:Q",
tooltip=["hcpcs", "year:O", alt.Tooltip("dollar:Q", format="$.2f")],
)
)
_row_charts.append(
(_solid + _prop).properties(title=str(_code), width=150, height=120)
)
_rows.append(alt.hconcat(*_row_charts))
pqm_multiples = alt.vconcat(*_rows)
mo.vstack(
[
pqm_multiples,
mo.md("""
The 12 designated codes with the largest absolute national
dollar move across their observed span (min. 5 priced
years). Solid line = final rules at each year's own final
CF; dashed segment to the diamond = **CY2027 proposed, at
the proposed non-QP CF — final rule pending**. Dollar
trajectories mix RVU changes with CF changes deliberately —
this is realized-price history, not resource-weight
history (the heatmap above isolates RVUs).
"""),
]
)
return
@app.cell(hide_code=True)
def _(gpci_2027p, mo, pl, pqm_near, proposed_for):
# Cross-cut: what the GPCI proposal does to one bread-and-butter
# attribution code (99213) at the Section-3 extreme localities.
_r = pqm_near.filter(pl.col("hcpcs") == "99213").row(0, named=True)
_best = gpci_2027p.sort("d_mean", descending=True).row(0, named=True)
_worst = gpci_2027p.sort("d_mean").row(0, named=True)
_natl = _r["dollar_2027proposed"]
_rows = pl.DataFrame(
[
{
"where": f"{loc['locality_name']} ({loc['state']})",
"work_gpci": loc["work_gpci"],
"pe_gpci": loc["pe_gpci"],
"mp_gpci": loc["mp_gpci"],
"mean_gpci_delta_vs_2026f": loc["d_mean"],
}
for loc in (_best, _worst)
]
)
mo.vstack(
[
mo.md(f"""
### GPCI × attribution cross-cut — 99213 under the CY2027 proposal
`99213` (established-patient office visit — the single most
attribution-relevant E/M code) prices at
**${_natl:.2f}** nationally under the CY2027 proposed
non-QP CF. At the proposal's extreme localities that
becomes a spread of locality-adjusted rates (Section 3's
payment table carries the exact figures for the example
trio); the GPCI movement below is what drives it.
"""),
mo.ui.table(_rows.to_pandas(), label="Extreme localities — proposed CY2027 GPCIs (vs CY2026-final mean)"),
]
)
return
# ── 5. Advanced APM ─────────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 5. Advanced APM (QP) Requirements
Under MACRA 2015, clinicians who participate heavily enough in an Under MACRA 2015, clinicians who participate heavily enough in an
Advanced Alternative Payment Model (Advanced APM) during a "QP Advanced Alternative Payment Model (Advanced APM) during a "QP
@@ -722,20 +1506,23 @@ def _(alt, mo, proposed_for, qp_differential):
return return
# ── 4. Provenance ─────────────────────────────────────────────────────── # ── 6. Provenance ───────────────────────────────────────────────────────
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(mo): def _(mo):
mo.md(""" mo.md("""
## 4. Provenance & Comment-Period Status ## 6. Provenance & Comment-Period Status
Ingest-log entries (`cms.ingest_log`) for every table this notebook Ingest-log entries (`cms.ingest_log`) for every FR-ingested table
reads from — `pfs.rvu` and `pfs.rvu_proposed` — so every figure this notebook reads — `pfs.rvu`, `pfs.rvu_proposed`, `pfs.gpci`,
above can be traced back to a specific ingest run, source file, and and `pfs.gpci_proposed` — so every figure above can be traced back
Federal Register citation. (`pfs.gpci` is not queried anywhere in to a specific ingest run, source file, and Federal Register
this notebook — see the intro's GPCI = 1.0 scope note — so it is citation. `cms.primary_care_service_code` (Section 4's 109-code
deliberately excluded here rather than claimed as a source.) attribution list) is a reference table published from the aco
module's CCLF/attribution data, not an FR ingest, so it carries no
ingest-log row; its provenance is the MSSP assignment
specification (42 CFR § 425.400(c)).
""") """)
return return
@@ -768,7 +1555,8 @@ def _(mo, q):
SELECT run_id, ingested_at, module, table_name, rule_id, SELECT run_id, ingested_at, module, table_name, rule_id,
source_file, sha256, rows, fr_citation, pincite_key source_file, sha256, rows, fr_citation, pincite_key
FROM cms.ingest_log FROM cms.ingest_log
WHERE table_name IN ('pfs.rvu', 'pfs.rvu_proposed') WHERE table_name IN ('pfs.rvu', 'pfs.rvu_proposed',
'pfs.gpci', 'pfs.gpci_proposed')
ORDER BY ingested_at DESC ORDER BY ingested_at DESC
""") """)
mo.ui.table(provenance.to_pandas(), label="Ingest Log — Tables Used in This Notebook") mo.ui.table(provenance.to_pandas(), label="Ingest Log — Tables Used in This Notebook")