feat(notebooks): CY2027 proposed-rule financial changes + APM (refs #616)

This commit is contained in:
kert
2026-08-14 00:07:57 -04:00
parent 5063a77587
commit cb235a2036

View File

@@ -0,0 +1,779 @@
import marimo
__generated_with = "0.23.1"
app = marimo.App(width="medium")
@app.cell(hide_code=True)
def _():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def _(mo):
mo.md("""
# CY2027 PFS Proposed Rule — Financial Changes & Advanced APM
CMS's CY2027 Physician Fee Schedule NPRM (CMS-1848-P, 91 FR 43842,
published July 16, 2026; docket CMS-2026-2377) is a **proposed rule
only** — there is no CY2027 Final Rule (CMS-1848-F) yet, and every
"CY2027" figure below is a proposal, not a finalized payment
parameter. The comment period, thresholds, and Advanced-APM
schedule captured here can all still change before the Final Rule
ships.
**What this means:** every number below is read live from
`pfs.rules.RULES` / `pfs.rules.PROPOSED` (via `proposed_for(2027)`)
and `qpp.QPP` (the repo's rule registries) and the `pfs`/`cms`
DuckLake schemas — nothing here is a hard-coded figure. Columns are
explicitly labeled **"final rule pending"** rather than fabricating
a CY2027-final column that does not exist yet. All dollar figures
use **national, GPCI-unadjusted RVUs (GPCI = 1.0)** — a deliberate
scope simplification, disclosed once here rather than on every
figure; real payment varies by locality via `pfs.gpci`, which this
notebook does not join.
""")
return
@app.cell(hide_code=True)
def _():
import altair as alt
import polars as pl
from conf import connect
from pfs.rules import RULES, proposed_for
from qpp import QPP, for_payment_year
connect.theme()
# connect.theme() puts assets/ on sys.path — reuse the HTI-5 design
# tokens directly instead of re-typing hex literals for the gain/loss
# diverging pair used in Section 2.
from fhirworx import AMBER, TEAL
# PFS reference data lives in the DuckLake lakehouse (M5, #514) —
# notebooks connect read-only.
con = connect.ducklake()
def q(sql):
return con.execute(sql).pl()
return AMBER, QPP, RULES, TEAL, alt, con, for_payment_year, pl, proposed_for, q
# ── 1. The conversion-factor walk ─────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 1. The Conversion Factor Walk — CY2026 Final → CY2027 Proposed
CY2026 was the first year the PFS published a split conversion
factor — a qualifying-APM (QP) track and a nonqualifying-APM
(non-QP) track, each with a standard and an anesthesia CF. The
CY2027 NPRM proposes new values for all four; the chart below
places the CY2026 **Final Rule** values (the only finalized
baseline that exists) alongside the CY2027 **NPRM proposal**
(final rule pending).
""")
return
@app.cell(hide_code=True)
def _(RULES, pl, proposed_for):
_cy26 = RULES[2026]
_prop = proposed_for(2027)
_rows = [
{"vintage": "CY2026 Final", "family": "Standard", "track": "Non-QP", "cf": _cy26.conversion_factor},
{"vintage": "CY2027 Proposed (final rule pending)", "family": "Standard", "track": "Non-QP", "cf": _prop.conversion_factor},
{"vintage": "CY2026 Final", "family": "Standard", "track": "QP", "cf": _cy26.cf_qp},
{"vintage": "CY2027 Proposed (final rule pending)", "family": "Standard", "track": "QP", "cf": _prop.cf_qp},
{"vintage": "CY2026 Final", "family": "Anesthesia", "track": "Non-QP", "cf": _cy26.anesthesia_cf},
{"vintage": "CY2027 Proposed (final rule pending)", "family": "Anesthesia", "track": "Non-QP", "cf": _prop.anesthesia_cf},
{"vintage": "CY2027 Proposed (final rule pending)", "family": "Anesthesia", "track": "QP", "cf": _prop.anesthesia_cf_qp},
# NOTE: RULES[2026].anesthesia_cf models the non-QP anesthesia CF
# only — the CY2026 Final Rule anesthesia QP CF is not captured
# in the registry (see pfs.rules module docstring), so that bar
# is deliberately omitted rather than guessed. No CY2027-final
# bars exist anywhere in this table — there is no Final Rule yet.
]
cf_walk = pl.DataFrame(_rows)
cf_walk
return (cf_walk,)
@app.cell(hide_code=True)
def _(alt, cf_walk, mo):
_vintage_order = ["CY2026 Final", "CY2027 Proposed (final rule pending)"]
_track_domain = ["Non-QP", "QP"]
cf_chart = (
alt.Chart(cf_walk.to_pandas())
.mark_bar()
.encode(
x=alt.X("vintage:N", title=None, sort=_vintage_order),
xOffset=alt.XOffset("track:N", sort=_track_domain),
y=alt.Y("cf:Q", title="Conversion factor ($/RVU)", scale=alt.Scale(zero=False)),
color=alt.Color("track:N", title="Track", scale=alt.Scale(domain=_track_domain)),
tooltip=["vintage", "family", "track", alt.Tooltip("cf:Q", format="$.4f")],
)
.properties(width=300, height=280)
.facet(column=alt.Column("family:N", title=None))
.resolve_scale(y="independent")
.properties(title="CY2026 Final vs. CY2027 Proposed PFS Conversion Factors")
)
mo.vstack(
[
cf_chart,
mo.md("""
*CY2026 Final anesthesia QP is not shown — that CF is not
captured in the `pfs.rules` registry (see module docstring).
No CY2027-final bars appear anywhere in this notebook — the
Final Rule (CMS-1848-F) has not been published.*
**Sources:** CY2026 Final Rule — 90 FR 49266. CY2027 NPRM —
91 FR 43842 (CMS-1848-P).
"""),
]
)
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
### Budget-neutrality adjustor decomposition
CMS's own NPRM narrative (91 FR 44242) describes the CY2027 CF
derivation explicitly: start from the **CY2026 conversion factors
with the one-time 2.50% statutory increase backed out**, multiply
by the 0.53% budget-neutrality adjustment, then multiply by the
section 1848(d)(20) qualifying/nonqualifying-APM annual update
(+0.75% / +0.25%). Reproducing that arithmetic against the
registry's actual CY2026-final and CY2027-proposed CFs is a useful
cross-check that the registry's transcription is internally
consistent with CMS's narrative — for the **standard** CF only;
the anesthesia CFs reflect "the same overall PFS adjustments with
the addition of anesthesia-specific PE and MP adjustments" (91 FR
44242) that are not modeled as separate registry fields, so they
are excluded from this reconstruction rather than approximated.
""")
return
@app.cell(hide_code=True)
def _(RULES, mo, pl, proposed_for):
_cy26 = RULES[2026]
_prop = proposed_for(2027)
# CMS's own stated CF-derivation components for CY2027 (91 FR 44242,
# file lines ~39088-39106): back the CY2026 CFs out of the one-time
# 2.50% statutory increase, then reapply BN + the annual update.
# NONE of these bare percentages is a field anywhere in `pfs.rules`
# or `qpp` — there is nothing to attribute-access for them. They are
# kept as literals ONLY because CMS's narrative states them as bare
# numbers, not derived from any other registry value; this is a
# cited transcription of that narrative, not a second source of
# truth for `budget_neutrality_adjustor` or the CFs themselves
# (both of which — `bn_adjustor` / `registry_cf` below — ARE read
# live from the registry).
_ONE_TIME_BACKOUT = 1.0250 # CY2026's one-time +2.50% statutory increase, backed out per 91 FR 44242
_ANNUAL_UPDATE_MULT = {
"Non-QP": 1.0025, # +0.25%/yr nonqualifying-APM update, sec. 1848(d)(20), 91 FR 44242
"QP": 1.0075, # +0.75%/yr qualifying-APM update, sec. 1848(d)(20), 91 FR 44242
}
def _reconstruct(cy2026_final_cf, track, bn_adjustor):
return (cy2026_final_cf / _ONE_TIME_BACKOUT) * bn_adjustor * _ANNUAL_UPDATE_MULT[track]
_bn_rows = [
{
"track": "Non-QP",
"cy2026_final_cf": _cy26.conversion_factor,
"one_time_backout": _ONE_TIME_BACKOUT,
"bn_adjustor": _prop.budget_neutrality_adjustor,
"annual_update_mult": _ANNUAL_UPDATE_MULT["Non-QP"],
"reconstructed_cy2027_proposed_cf": round(
_reconstruct(_cy26.conversion_factor, "Non-QP", _prop.budget_neutrality_adjustor), 4
),
"registry_cy2027_proposed_cf": _prop.conversion_factor,
},
{
"track": "QP",
"cy2026_final_cf": _cy26.cf_qp,
"one_time_backout": _ONE_TIME_BACKOUT,
"bn_adjustor": _prop.budget_neutrality_adjustor,
"annual_update_mult": _ANNUAL_UPDATE_MULT["QP"],
"reconstructed_cy2027_proposed_cf": round(
_reconstruct(_cy26.cf_qp, "QP", _prop.budget_neutrality_adjustor), 4
),
"registry_cy2027_proposed_cf": _prop.cf_qp,
},
]
bn_decomp = pl.DataFrame(_bn_rows).with_columns(
(pl.col("reconstructed_cy2027_proposed_cf") - pl.col("registry_cy2027_proposed_cf"))
.abs()
.round(4)
.alias("abs_diff")
)
mo.vstack(
[
bn_decomp,
mo.md(f"""
`reconstructed_cf = (CY2026_final_cf / one_time_backout) ×
bn_adjustor × annual_update_mult` — `bn_adjustor` and
`registry_cy2027_proposed_cf` are read live from
`RULES[2026]` / `proposed_for(2027)`. `one_time_backout`
({_ONE_TIME_BACKOUT}) and `annual_update_mult`
({_ANNUAL_UPDATE_MULT["Non-QP"]} non-QP /
{_ANNUAL_UPDATE_MULT["QP"]} QP) are **cited transcriptions
of CMS's NPRM narrative (91 FR 44242,
`data/fr_downloads/2026-14327.txt:39088-39106`)**, not
registry fields — no field in `pfs.rules` or `qpp` models
the 2.50%/0.53%/0.25%/0.75% components individually, only
their combined effect on `conversion_factor` / `cf_qp`.
"""),
]
)
return
@app.cell(hide_code=True)
def _(mo, proposed_for):
_prop = proposed_for(2027)
mo.vstack(
[
mo.md("### Caveat — a drafting error in the NPRM's own summary section"),
mo.callout(
mo.md(f"""
`pfs.rules.PROPOSED[2027].notes` documents a transcription
caveat that is worth surfacing here rather than only in
source comments — quoted live below, not retyped:
> {_prop.notes}
"""),
kind="warn",
),
]
)
return
# ── 2. RVU-level deltas ────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 2. RVU-Level Deltas — Which HCPCS Codes Move the Most
`pfs.rvu_proposed` filtered to `cms_rule_id = 'CMS-1848-P'` (14,518
rows as of this ingest) is compared against `pfs.rvu` for CY2026
(the current final baseline — there is no CY2027 final table to
compare against). Both tables are deduplicated to one row per
HCPCS base code (no modifier) via `QUALIFY row_number() ... = 1`
per the P36 convention, `status_code = 'A'` (actively priced), and
— for the proposed table specifically — a non-null non-facility PE
RVU, since a meaningful share of `pfs.rvu_proposed` rows carry a
null non-facility *or* facility PE RVU (CMS's Addendum B only
populates the setting a code is actually priced in).
""")
return
@app.cell(hide_code=True)
def _(q):
rvu_delta_raw = q("""
WITH proposed AS (
SELECT hcpcs, description, work_rvu, non_fac_pe_rvu, mp_rvu,
work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P'
AND (mod IS NULL OR mod = '')
AND status_code = 'A'
AND non_fac_pe_rvu IS NOT NULL
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
),
final_2026 AS (
SELECT hcpcs, work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
FROM pfs.rvu
WHERE year = 2026 AND (mod IS NULL OR mod = '') AND status_code = 'A'
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
)
SELECT
p.hcpcs,
p.description,
p.total_nf_rvu AS total_nf_rvu_proposed,
f26.total_nf_rvu AS total_nf_rvu_2026final
FROM proposed p
JOIN final_2026 f26 USING (hcpcs)
""")
rvu_delta_raw
return (rvu_delta_raw,)
@app.cell(hide_code=True)
def _(RULES, pl, proposed_for, rvu_delta_raw):
_cf_2026_final = RULES[2026].conversion_factor
_cf_2027_proposed = proposed_for(2027).conversion_factor
rvu_delta = rvu_delta_raw.with_columns(
(pl.col("total_nf_rvu_proposed") * _cf_2027_proposed).round(2).alias("dollar_2027proposed"),
(pl.col("total_nf_rvu_2026final") * _cf_2026_final).round(2).alias("dollar_2026final"),
).with_columns(
(pl.col("dollar_2027proposed") - pl.col("dollar_2026final")).round(2).alias("delta_vs_2026final"),
)
rvu_delta
return (rvu_delta,)
@app.cell(hide_code=True)
def _(AMBER, RULES, TEAL, alt, mo, proposed_for, rvu_delta):
_n = 20
_winners = rvu_delta.sort("delta_vs_2026final", descending=True).head(_n)
_losers = rvu_delta.sort("delta_vs_2026final", descending=False).head(_n)
_top40 = _winners.vstack(_losers)
_plot_df = _top40.to_pandas()
_plot_df["label"] = _plot_df["hcpcs"] + " — " + _plot_df["description"].str.slice(0, 40)
_plot_df["sign"] = _plot_df["delta_vs_2026final"].apply(lambda v: "Gain" if v >= 0 else "Loss")
winners_losers_chart = (
alt.Chart(_plot_df)
.mark_bar()
.encode(
x=alt.X("delta_vs_2026final:Q", title="$ change, non-QP CF (national unadjusted, GPCI=1.0)"),
y=alt.Y("label:N", title=None, sort=alt.SortField(field="delta_vs_2026final", 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", "description", alt.Tooltip("delta_vs_2026final:Q", format="$.2f")],
)
.properties(
title="Top 20 Winners / Top 20 Losers — CY2027 Proposed vs. CY2026 Final",
width=700,
height=600,
)
)
mo.vstack(
[
winners_losers_chart,
mo.md(f"""
**Sources:** `pfs.rvu_proposed` (`CMS-1848-P`) — CY2027 NPRM,
{proposed_for(2027).federal_register_citation}. Baseline —
`pfs.rvu` CY2026 Final ({RULES[2026].federal_register_citation}).
Both sides priced with each vintage's own non-QP standard CF
— this delta blends RVU-table changes with the proposed CF
change, it is not an RVU-only comparison.
"""),
]
)
return
@app.cell(hide_code=True)
def _(mo, proposed_for, rvu_delta):
mo.vstack(
[
mo.md(f"""
**Full detail — {rvu_delta.height:,} codes** matched across
the `CMS-1848-P` proposed RVU table and CY2026-final RVU
table (search the table below by HCPCS or description).
"""),
mo.ui.table(
rvu_delta.sort("delta_vs_2026final").to_pandas(),
page_size=25,
label="RVU / Payment Deltas by HCPCS",
),
]
)
return
@app.cell(hide_code=True)
def _(con, mo, proposed_for):
_new = con.execute("""
SELECT count(*) FROM pfs.rvu_proposed p
WHERE p.cms_rule_id = 'CMS-1848-P' AND (p.mod IS NULL OR p.mod='') AND p.status_code='A' AND p.non_fac_pe_rvu IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM pfs.rvu f WHERE f.year=2026 AND f.hcpcs=p.hcpcs AND (f.mod IS NULL OR f.mod=''))
""").fetchone()[0]
_dropped = con.execute("""
SELECT count(*) FROM pfs.rvu f
WHERE f.year=2026 AND (f.mod IS NULL OR f.mod='') AND f.status_code='A'
AND NOT EXISTS (SELECT 1 FROM pfs.rvu_proposed p WHERE p.cms_rule_id = 'CMS-1848-P' AND p.hcpcs=f.hcpcs AND (p.mod IS NULL OR p.mod=''))
""").fetchone()[0]
mo.md(f"""
> **Coverage caveat:** {_new} HCPCS codes appear in the `CMS-1848-P`
> proposed RVU table with no CY2026-final counterpart (new/renumbered
> codes), and {_dropped} CY2026-final codes have no match in the
> proposed table (dropped, bundled, or excluded from the Addendum B
> extract used to build the lake table). Both groups are excluded
> from the delta analysis above rather than shown with a fabricated
> baseline.
**Source:** `pfs.rvu_proposed` — CMS-1848-P Addendum B,
{proposed_for(2027).federal_register_citation}. `pfs.rvu` — CY2026
PFS Final Rule Addendum B.
""")
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
### Cross-NPRM check — CY2026 NPRM vs. CY2027 NPRM (optional)
`pfs.rvu_proposed` holds **two** NPRM partitions in the same table
— `CMS-1832-P` (CY2026 NPRM, 14,169 rows) and `CMS-1848-P` (CY2027
NPRM, 14,518 rows). For codes proposed in both rulemakings, this
compares total non-facility RVUs NPRM-to-NPRM — a coarse signal of
which codes CMS has been proposing to move for two consecutive
rulemaking cycles, independent of which CF applies.
""")
return
@app.cell(hide_code=True)
def _(q):
cross_nprm = q("""
WITH cy2026_nprm AS (
SELECT hcpcs, description, work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1832-P' AND (mod IS NULL OR mod = '')
AND status_code = 'A' AND non_fac_pe_rvu IS NOT NULL
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
),
cy2027_nprm AS (
SELECT hcpcs, work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
FROM pfs.rvu_proposed
WHERE cms_rule_id = 'CMS-1848-P' AND (mod IS NULL OR mod = '')
AND status_code = 'A' AND non_fac_pe_rvu IS NOT NULL
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
)
SELECT
a.hcpcs, a.description,
a.total_nf_rvu AS total_nf_rvu_cy2026_nprm,
b.total_nf_rvu AS total_nf_rvu_cy2027_nprm,
round(b.total_nf_rvu - a.total_nf_rvu, 4) AS rvu_delta_nprm_to_nprm
FROM cy2026_nprm a
JOIN cy2027_nprm b USING (hcpcs)
ORDER BY abs(b.total_nf_rvu - a.total_nf_rvu) DESC
LIMIT 10
""")
return (cross_nprm,)
@app.cell(hide_code=True)
def _(cross_nprm, mo):
mo.vstack(
[
cross_nprm,
mo.md("""
**Sources:** `pfs.rvu_proposed` — `CMS-1832-P` (90 FR 32352)
and `CMS-1848-P` (91 FR 43842) partitions of the same table.
Total non-facility RVUs only — no dollar conversion, since
the two NPRMs' CFs are not directly comparable across a
one-year gap without also re-basing for the RVU-neutral
budget-neutrality adjustment each rulemaking applies.
"""),
]
)
return
# ── 3. Advanced APM ─────────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 3. Advanced APM (QP) Requirements
Under MACRA 2015, clinicians who participate heavily enough in an
Advanced Alternative Payment Model (Advanced APM) during a "QP
Performance Period" become Qualifying APM Participants (QPs) for
the payment year two years later (`payment_year = performance_year
+ 2`). The CY2027 NPRM's own regulatory-impact-analysis recital
covers **QP Performance Period 2027 / payment year 2029** —
`qpp.QPP[2027]`, reachable as `qpp.for_payment_year(2029)`.
`qpp.QPP`'s payment-year keying was corrected under task #620
shortly before this notebook was written — thresholds are keyed to
**payment year**, not performance year, and every table below
labels both explicitly so the correction is visible rather than
silently assumed.
""")
return
@app.cell(hide_code=True)
def _(for_payment_year, mo):
_governs_2029 = for_payment_year(2029)
mo.md(f"""
`qpp.for_payment_year(2029)` resolves to QP Performance Period
**{_governs_2029.performance_year}** ({_governs_2029.citation}) —
the performance period whose determinations govern the payment
year that the CY2027 NPRM's proposed conversion factors apply to.
`qp_cf_applies` is **{_governs_2029.qp_cf_applies}** for this
performance period.
""")
return
@app.cell(hide_code=True)
def _(QPP, pl):
_rows = []
for _perf_year, _qy in sorted(QPP.items()):
_prop = _qy.proposed
_rows.append(
{
"qp_performance_period": _perf_year,
"payment_year": _qy.payment_year,
"qp_payment_pct": _qy.qp_thresholds.payment_amount_pct,
"qp_patient_pct": _qy.qp_thresholds.patient_count_pct,
"partial_qp_payment_pct": _qy.partial_qp_thresholds.payment_amount_pct,
"partial_qp_patient_pct": _qy.partial_qp_thresholds.patient_count_pct,
"revenue_nominal_pct": _qy.risk_standards.revenue_nominal_pct,
"benchmark_nominal_pct": _qy.risk_standards.benchmark_nominal_pct,
"apm_incentive_pct": _qy.apm_incentive_pct,
"qp_cf_applies": _qy.qp_cf_applies,
"cehrt_required": _qy.cehrt_required,
"has_nprm_proposal": _prop is not None,
"citation": _qy.citation,
}
)
apm_thresholds = pl.DataFrame(_rows)
apm_thresholds
return (apm_thresholds,)
@app.cell(hide_code=True)
def _(QPP, mo):
mo.md(f"""
Columns are labeled `qp_performance_period` (the year Advanced-APM
participation is measured) and `payment_year` (the year the
resulting QP status is applied) explicitly and separately — every
threshold in this table governs by **`payment_year`**, per 42 CFR
414.1430(a) and section 1833(z)(2) of the Act. `QPP[2027]`
(payment year {QPP[2027].payment_year}) is the entry this
notebook's CF walk (Section 1) corresponds to.
""")
return
@app.cell(hide_code=True)
def _(alt, apm_thresholds, mo):
_df = apm_thresholds.to_pandas()
_df["apm_incentive_pct"] = _df["apm_incentive_pct"].fillna(0.0)
_df["status"] = _df["apm_incentive_pct"].apply(lambda v: "Paid" if v > 0 else "None under current law")
incentive_chart = (
alt.Chart(_df)
.mark_bar()
.encode(
x=alt.X("payment_year:O", title="Payment year"),
y=alt.Y("apm_incentive_pct:Q", title="APM incentive payment (% of base-year Part B paid claims)"),
color=alt.Color(
"status:N",
title=None,
scale=alt.Scale(domain=["Paid", "None under current law"]),
),
tooltip=["qp_performance_period", "payment_year", "apm_incentive_pct", "status"],
)
.properties(title="APM Incentive Payment — Not a Clean Sunset", width=600, height=300)
)
_citations = ", ".join(sorted(set(_df["citation"])))
mo.vstack(
[
incentive_chart,
mo.md(f"""
The lump-sum APM Incentive Payment does **not** sunset
cleanly after payment year 2026. Payment year 2027 (this
notebook's prior year) has no incentive under current law.
The Consolidated Appropriations Act, 2026 (CAA 2026, Pub.
L. 119-75) then **revives a 3.1% incentive for payment
year 2028 only** — CMS-1848-P proposes to codify that
revival into 42 CFR 414.1450(b)(1) (91 FR 44218-44219,
44286). Payment year 2029 (`QPP[2027]`, the year this
notebook's own CF walk applies to) reverts to **no**
incentive — the amendatory text lists no applicable
percentage for payment year 2029 at all. So the pattern
across 2025-2029 is paid / paid / gap / revived / gap, not
a single cutoff.
**Sources:** `qpp.QPP` performance years 2023-2027
({_citations}).
"""),
]
)
return
@app.cell(hide_code=True)
def _(QPP, mo):
_prop = QPP[2027].proposed
mo.vstack(
[
mo.md(f"""
### CMS-1848-P proposed QPP changes — proposals only, no Final Rule disposition yet
`qpp.QPP[2027].proposed.changes` ({_prop.federal_register_citation},
{_prop.cms_rule_id}) is the single source of truth for the
narrative below — quoted live, not retyped. Unlike the
CY2026 notebook's disposition table, there is **no**
finalized/not-finalized column here: CMS-1848-F has not
been published.
"""),
mo.callout(mo.md(_prop.changes), kind="info"),
]
)
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
### What QP status is worth in dollars (CY2027 proposed) — 3 example HCPCS
National, unadjusted (GPCI = 1.0) payment for a single unit of
service, computed as `(work_rvu + non_fac_pe_rvu + mp_rvu) × CF`,
using **CY2026-final RVUs** (the current baseline; CY2027 has no
finalized RVU table yet) priced at the **CY2027 NPRM's proposed**
QP and non-QP conversion factors:
- **99213** — established-patient office visit, low complexity (E/M)
- **27447** — total knee arthroplasty (major procedure)
- **70553** — MRI brain, without and with contrast (imaging)
""")
return
@app.cell(hide_code=True)
def _(pl, proposed_for, q):
_example_rvu = q("""
SELECT hcpcs, description, work_rvu, non_fac_pe_rvu, mp_rvu,
work_rvu + non_fac_pe_rvu + mp_rvu AS total_rvu
FROM pfs.rvu
WHERE year = 2026 AND (mod IS NULL OR mod = '')
AND hcpcs IN ('99213', '27447', '70553')
""")
_prop = proposed_for(2027)
_rows = []
for _r in _example_rvu.iter_rows(named=True):
_pay_nonqp = round(_r["total_rvu"] * _prop.conversion_factor, 2)
_pay_qp = round(_r["total_rvu"] * _prop.cf_qp, 2)
_rows.append(
{
"hcpcs": _r["hcpcs"],
"description": _r["description"],
"total_rvu_cy2026final": _r["total_rvu"],
"non_qp_payment_cy2027proposed": _pay_nonqp,
"qp_payment_cy2027proposed": _pay_qp,
"qp_differential": round(_pay_qp - _pay_nonqp, 2),
}
)
qp_differential = pl.DataFrame(_rows)
qp_differential
return (qp_differential,)
@app.cell(hide_code=True)
def _(alt, mo, proposed_for, qp_differential):
differential_chart = (
alt.Chart(qp_differential.to_pandas())
.mark_bar()
.encode(
x=alt.X("hcpcs:N", title="HCPCS"),
y=alt.Y("qp_differential:Q", title="QP minus non-QP payment ($, national unadjusted)"),
tooltip=["hcpcs", "description", alt.Tooltip("qp_differential:Q", format="$.2f")],
)
.properties(title="Dollar Value of QP Status — CY2027 Proposed (final rule pending)", width=500, height=320)
)
mo.vstack(
[
differential_chart,
mo.md(f"""
**Sources:** RVUs — `pfs.rvu`, CY2026 Final (90 FR 49266).
CFs — `proposed_for(2027)`, CY2027 NPRM
({proposed_for(2027).federal_register_citation},
{proposed_for(2027).cms_rule_id}) — final rule pending.
National unadjusted means GPCI = 1.0 — actual payment
varies by locality.
"""),
]
)
return
# ── 4. Provenance ───────────────────────────────────────────────────────
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 4. Provenance & Comment-Period Status
Ingest-log entries (`cms.ingest_log`) for every table this notebook
reads from — `pfs.rvu` and `pfs.rvu_proposed` — so every figure
above can be traced back to a specific ingest run, source file, and
Federal Register citation. (`pfs.gpci` is not queried anywhere in
this notebook — see the intro's GPCI = 1.0 scope note — so it is
deliberately excluded here rather than claimed as a source.)
""")
return
@app.cell(hide_code=True)
def _(mo, proposed_for):
from datetime import date
_prop = proposed_for(2027)
_close = _prop.comment_close
_today = date.today()
_open = _today <= _close
_status = "OPEN" if _open else "CLOSED"
mo.callout(
mo.md(f"""
**Comment period: {_status}** — {_prop.cms_rule_id} ({_prop.federal_register_citation})
published {_prop.published.isoformat()}, comment period closes
**{_close.isoformat()}** (`proposed_for(2027).comment_close`,
read live from the registry; today is {_today.isoformat()}).
"""),
kind="success" if _open else "neutral",
)
return
@app.cell(hide_code=True)
def _(mo, q):
provenance = q("""
SELECT run_id, ingested_at, module, table_name, rule_id,
source_file, sha256, rows, fr_citation, pincite_key
FROM cms.ingest_log
WHERE table_name IN ('pfs.rvu', 'pfs.rvu_proposed')
ORDER BY ingested_at DESC
""")
mo.ui.table(provenance.to_pandas(), label="Ingest Log — Tables Used in This Notebook")
return
if __name__ == "__main__":
app.run()