Files
stack/notebooks/cy2027_pfs_proposed_rule.py

780 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()