feat(notebooks): palliative_care_rfi — PRISMA flow + RFI payment context (closes #647, refs #646, P43)
The P42/P43 deliverable for the CY2027 NPRM Community-Based Palliative Care RFI (91 FR 43949-43950): PRISMA 2020 flow diagram and screening crosstabs rendered live from Zotero project tags (prisma.flow + mo.mermaid, skin-subs pattern), stage-2 included-studies table, and payment context for the code families the RFI names — ACP, CCM, PCM, TCM, APCM priced CY2026-final vs CY2027-proposed from pfs.rvu / pfs.rvu_proposed at each vintage's own CF, MSSP attribution overlap flagged, RVU YoY heatmap 2015-2027p. Citations route through bib.frlink/cfrlink (#642 pattern); year displays through conf.display.plain_years (#643); relevance-capped identification and single-reviewer local-LLM screening are disclosed inline as methods limitations.
This commit is contained in:
682
notebooks/palliative_care_rfi.py
Normal file
682
notebooks/palliative_care_rfi.py
Normal file
@@ -0,0 +1,682 @@
|
||||
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 _(fr_md, mo):
|
||||
mo.md(f"""
|
||||
# Community-Based Palliative Care RFI — Evidence Base & Payment Context
|
||||
|
||||
The CY2027 PFS NPRM (CMS-1848-P, {fr_md("91 FR 43842")}, docket
|
||||
CMS-2026-2377) carries a Request for Information on
|
||||
**Community-Based Palliative Care** at {fr_md("91 FR 43949")}-43950.
|
||||
This notebook is the P42/P43 deliverable behind the comment we will
|
||||
file to that docket: a PRISMA 2020 systematic evidence base for the
|
||||
RFI's five questions, plus the payment context for the code
|
||||
families the RFI itself names — read live from the repo's lake and
|
||||
registries, nothing hard-coded.
|
||||
|
||||
The RFI asks five things:
|
||||
|
||||
- **(a)** where CMS should focus **fraud, waste, and abuse**
|
||||
scrutiny in community-based palliative care;
|
||||
- **(b)** whether eligibility for a serious-illness service should
|
||||
be gated on **certified life expectancy**, and what interval the
|
||||
evidence supports;
|
||||
- **(c)** whether eligibility is better defined by **impact on
|
||||
daily function (ADLs)** and/or **caregiver strain**;
|
||||
- **(d)** which **care-management service elements**
|
||||
(interdisciplinary teams, home visits, timely follow-up) should
|
||||
be required — explicitly tied to section II.E of the same rule;
|
||||
- **(e)** how **quality** of palliative care management should be
|
||||
measured (MIPS Value Pathway reporting, CBE 3665 "feeling heard
|
||||
and understood").
|
||||
|
||||
CMS asks that every statement be supported by peer-reviewed or
|
||||
institutional evidence — so the evidence base *is* the response.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _():
|
||||
import altair as alt
|
||||
import polars as pl
|
||||
|
||||
from conf import connect
|
||||
from conf.display import plain_years
|
||||
from pfs.rules import RULES, proposed_for
|
||||
|
||||
connect.theme()
|
||||
|
||||
# connect.theme() puts assets/ on sys.path — reuse the HTI-5 design
|
||||
# tokens: AMBER/TEAL are the gain/loss diverging pair, INDIGO_MED
|
||||
# the default series, PLUM/GRAY categorical follow-ons.
|
||||
from fhirworx import AMBER, GRAY, INDIGO_MED, PLUM, 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()
|
||||
|
||||
def cfr_md(ref, text=""):
|
||||
"""eCFR jump link as markdown (P41, #641) — degrades to plain
|
||||
text on a malformed cite so the notebook never breaks."""
|
||||
try:
|
||||
from bib import cfrlink
|
||||
|
||||
return cfrlink.md_link(ref, text=text)
|
||||
except Exception:
|
||||
return text or ref
|
||||
|
||||
def fr_md(ref, text=""):
|
||||
"""FR web jump link as markdown (P40, #638) — degrades to plain
|
||||
text when bib.sqlite or the anchor map is unavailable so the
|
||||
notebook never breaks offline."""
|
||||
try:
|
||||
from bib import frlink
|
||||
|
||||
return frlink.md_link(ref, store=connect.bib(), text=text or ref)
|
||||
except Exception:
|
||||
return text or ref
|
||||
|
||||
return (
|
||||
AMBER,
|
||||
GRAY,
|
||||
INDIGO_MED,
|
||||
PLUM,
|
||||
RULES,
|
||||
TEAL,
|
||||
alt,
|
||||
cfr_md,
|
||||
con,
|
||||
connect,
|
||||
fr_md,
|
||||
pl,
|
||||
plain_years,
|
||||
proposed_for,
|
||||
q,
|
||||
)
|
||||
|
||||
|
||||
# ── 1. PRISMA evidence base ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 1. PRISMA 2020 Evidence Base — `project:palliative-rfi`
|
||||
|
||||
**Identification** (2026-08-18): five PubMed E-utilities searches,
|
||||
one per RFI part, via
|
||||
`dev/scripts/search_pubmed_palliative_rfi.py` — the same pipeline
|
||||
the skin-substitutes review (P29) uses. The palliative literature
|
||||
dwarfs the skin-subs corpus, so each domain search was
|
||||
**relevance-sorted and capped**; capped identification is a stated
|
||||
limitation of this review, not silent truncation:
|
||||
|
||||
| RFI part | Domain search | Matching | Retrieved (cap) |
|
||||
|---|---|---:|---:|
|
||||
| (a) | fraud/waste/abuse in hospice & palliative care | 177 | 177 (250) |
|
||||
| (b) | eligibility via prognosis / life expectancy | 655 | 250 |
|
||||
| (c) | eligibility via function / caregiver strain | 665 | 250 |
|
||||
| (d) | community-based delivery & care management | 1,446 | 300 |
|
||||
| (e) | quality measures | 1,671 | 250 |
|
||||
|
||||
1,227 retrieved records dedupe to **1,189 unique articles**, stored
|
||||
in bib.sqlite and pushed to Zotero tagged `module:palliative-rfi` +
|
||||
`rfi:{a-e}`.
|
||||
|
||||
**Screening** (stage 2, title/abstract) runs through the repo's
|
||||
PRISMA pipeline (`stack prisma screen palliative-rfi`) against the
|
||||
project's criteria/reasons anchors in Zotero, using a **local
|
||||
self-hosted model (qwen2.5:14b via Ollama on the server GPU)** —
|
||||
decisions, reason codes, and rationales land back in Zotero as
|
||||
tags and notes. The flow below is computed **live from those
|
||||
tags**, so it always reflects current screening progress.
|
||||
|
||||
> **Methods disclosures:** single-reviewer LLM screening (no human
|
||||
> dual-screening yet); stage-3 full-text eligibility and data
|
||||
> extraction have not run; identification is relevance-capped as
|
||||
> tabulated above; grey literature (OIG/GAO/MedPAC/CMMI) enters
|
||||
> via the bib evidence base rather than PubMed and bypasses
|
||||
> clinical screening per the project criteria.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(connect, mo):
|
||||
from prisma import flow as _flow
|
||||
|
||||
class _RoDb:
|
||||
"""Duck-typed shim: prisma.flow only touches ``.con``."""
|
||||
|
||||
def __init__(self, con):
|
||||
self.con = con
|
||||
|
||||
_zcon = connect.zotero()
|
||||
flow_counts = _flow.count(_RoDb(_zcon), "palliative-rfi")
|
||||
flow_mermaid = _flow.mermaid(flow_counts, project="palliative-rfi")
|
||||
|
||||
_screened_pct = (
|
||||
100 * flow_counts.screened / flow_counts.identified
|
||||
if flow_counts.identified
|
||||
else 0.0
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
mo.mermaid(flow_mermaid),
|
||||
mo.md(f"""
|
||||
**{flow_counts.identified:,} identified** →
|
||||
**{flow_counts.screened:,} screened** ({_screened_pct:.0f}%
|
||||
of identified so far) → **{flow_counts.included}
|
||||
included** at the current pass. Diagram and counts are
|
||||
regenerated from Zotero tags on every notebook run —
|
||||
re-run after more screening completes and the flow
|
||||
updates itself.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return (flow_counts,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(AMBER, GRAY, TEAL, alt, connect, mo, pl):
|
||||
# Screening status per RFI domain, live from Zotero tags.
|
||||
_zcon = connect.zotero()
|
||||
_rows = _zcon.execute("""
|
||||
WITH proj AS (
|
||||
SELECT it.itemID
|
||||
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE t.name = 'project:palliative-rfi'
|
||||
AND it.itemID NOT IN (
|
||||
SELECT it2.itemID FROM itemTags it2
|
||||
JOIN tags t2 ON it2.tagID = t2.tagID
|
||||
WHERE t2.name LIKE 'prisma:%')
|
||||
),
|
||||
dom AS (
|
||||
SELECT it.itemID, t.name AS rfi
|
||||
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE t.name IN ('rfi:a','rfi:b','rfi:c','rfi:d','rfi:e')
|
||||
AND it.itemID IN (SELECT itemID FROM proj)
|
||||
),
|
||||
decision AS (
|
||||
SELECT it.itemID, t.name AS decision
|
||||
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE t.name IN ('screen:include','screen:exclude','screen:uncertain')
|
||||
)
|
||||
SELECT dom.rfi,
|
||||
coalesce(decision.decision, 'unscreened') AS decision,
|
||||
count(*) AS n
|
||||
FROM dom LEFT JOIN decision ON dom.itemID = decision.itemID
|
||||
GROUP BY dom.rfi, decision.decision
|
||||
ORDER BY dom.rfi
|
||||
""").fetchall()
|
||||
|
||||
_labels = {
|
||||
"rfi:a": "(a) fraud/waste/abuse",
|
||||
"rfi:b": "(b) prognosis eligibility",
|
||||
"rfi:c": "(c) function/caregiver",
|
||||
"rfi:d": "(d) delivery & mgmt",
|
||||
"rfi:e": "(e) quality measures",
|
||||
}
|
||||
rfi_status = pl.DataFrame(
|
||||
[
|
||||
{"rfi_part": _labels[r], "decision": d.removeprefix("screen:"), "n": n}
|
||||
for r, d, n in _rows
|
||||
]
|
||||
)
|
||||
|
||||
_order = ["include", "uncertain", "exclude", "unscreened"]
|
||||
rfi_status_chart = (
|
||||
alt.Chart(rfi_status.to_pandas())
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("n:Q", title="Records"),
|
||||
y=alt.Y("rfi_part:N", title=None),
|
||||
color=alt.Color(
|
||||
"decision:N",
|
||||
title="Stage-2 decision",
|
||||
sort=_order,
|
||||
scale=alt.Scale(
|
||||
domain=_order,
|
||||
range=[TEAL, "#C9A227", AMBER, GRAY],
|
||||
),
|
||||
),
|
||||
order=alt.Order("color_decision_sort_index:Q"),
|
||||
tooltip=["rfi_part", "decision", "n"],
|
||||
)
|
||||
.properties(
|
||||
title="Screening Status by RFI Domain (live)", width=620, height=220
|
||||
)
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
rfi_status_chart,
|
||||
mo.md("""
|
||||
Records retrieved by more than one domain search carry
|
||||
multiple `rfi:*` tags and count once per domain here —
|
||||
the PRISMA flow above deduplicates; this chart
|
||||
deliberately does not, since a paper can inform two RFI
|
||||
answers.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(connect, mo, pl, plain_years):
|
||||
# Included studies, live: one row per include, with domain tags.
|
||||
_zcon = connect.zotero()
|
||||
_rows = _zcon.execute("""
|
||||
WITH inc AS (
|
||||
SELECT it.itemID
|
||||
FROM itemTags it JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE t.name = 'screen:include'
|
||||
AND it.itemID IN (
|
||||
SELECT it2.itemID FROM itemTags it2
|
||||
JOIN tags t2 ON it2.tagID = t2.tagID
|
||||
WHERE t2.name = 'project:palliative-rfi')
|
||||
)
|
||||
SELECT
|
||||
(SELECT idv.value FROM itemData id
|
||||
JOIN itemDataValues idv ON idv.valueID = id.valueID
|
||||
WHERE id.itemID = inc.itemID
|
||||
AND id.fieldID = (SELECT fieldID FROM fields
|
||||
WHERE fieldName = 'title')) AS title,
|
||||
(SELECT idv.value FROM itemData id
|
||||
JOIN itemDataValues idv ON idv.valueID = id.valueID
|
||||
WHERE id.itemID = inc.itemID
|
||||
AND id.fieldID = (SELECT fieldID FROM fields
|
||||
WHERE fieldName = 'publicationTitle')) AS journal,
|
||||
(SELECT group_concat(t.name, ', ') FROM itemTags it
|
||||
JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE it.itemID = inc.itemID
|
||||
AND t.name LIKE 'rfi:%') AS rfi_parts,
|
||||
(SELECT group_concat(t.name, ', ') FROM itemTags it
|
||||
JOIN tags t ON it.tagID = t.tagID
|
||||
WHERE it.itemID = inc.itemID
|
||||
AND t.name LIKE 'year:%') AS year
|
||||
FROM inc
|
||||
ORDER BY year DESC
|
||||
""").fetchall()
|
||||
|
||||
included = pl.DataFrame(
|
||||
[
|
||||
{
|
||||
"year": (y or "").removeprefix("year:"),
|
||||
"title": ti or "",
|
||||
"journal": j or "",
|
||||
"rfi_parts": r or "",
|
||||
}
|
||||
for ti, j, r, y in _rows
|
||||
]
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
mo.md(f"""
|
||||
### Included at title/abstract stage — {included.height} records (live)
|
||||
|
||||
Stage-2 includes plus their RFI-domain tags. Every include
|
||||
still awaits stage-3 full-text eligibility, so this list
|
||||
can only shrink; the LLM's per-record rationale sits on
|
||||
each Zotero item as a child note for reviewer audit.
|
||||
"""),
|
||||
mo.ui.table(
|
||||
plain_years(included).to_pandas(),
|
||||
page_size=25,
|
||||
label="Stage-2 included records",
|
||||
),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# ── 2. Payment context ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(cfr_md, fr_md, mo):
|
||||
mo.md(f"""
|
||||
## 2. Payment Context — the Code Families the RFI Names
|
||||
|
||||
The RFI's background ({fr_md("91 FR 43949")}) frames the question
|
||||
around whether current **E/M, care management, and ACP** billing
|
||||
reflects palliative practice, and part (e) names **APCM (HCPCS
|
||||
G0556-G0558)** explicitly. This section prices those families —
|
||||
CY2026 Final (the current baseline) against the CY2027 NPRM
|
||||
proposal — from `pfs.rvu` / `pfs.rvu_proposed`, at each vintage's
|
||||
own non-QP standard CF from `pfs.rules`, national unadjusted
|
||||
(GPCI = 1.0). Codes on the MSSP primary-care attribution list
|
||||
({cfr_md("42 CFR 425.400(c)")}, `cms.primary_care_service_code`)
|
||||
are flagged: for those, valuation moves clinician revenue **and**
|
||||
ACO attribution denominators at once.
|
||||
|
||||
Families and code sets are declared in this cell's source —
|
||||
membership in each PFS table is queried, never assumed, and codes
|
||||
a table doesn't price are reported as such.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(RULES, pl, proposed_for, q):
|
||||
_families = {
|
||||
"ACP": ["99497", "99498"],
|
||||
"CCM": ["99437", "99439", "99487", "99489", "99490", "99491"],
|
||||
"PCM": ["99424", "99425", "99426", "99427"],
|
||||
"TCM": ["99495", "99496"],
|
||||
"APCM": ["G0556", "G0557", "G0558"],
|
||||
}
|
||||
_fam_rows = [
|
||||
{"hcpcs": c, "family": fam} for fam, codes in _families.items() for c in codes
|
||||
]
|
||||
fam_df = pl.DataFrame(_fam_rows)
|
||||
_codes_sql = ",".join(f"'{c}'" for c in fam_df["hcpcs"])
|
||||
|
||||
_raw = q(f"""
|
||||
WITH fin AS (
|
||||
SELECT hcpcs, description,
|
||||
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 = '')
|
||||
AND hcpcs IN ({_codes_sql})
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
),
|
||||
prop AS (
|
||||
SELECT hcpcs, description AS description_p,
|
||||
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 = '')
|
||||
AND hcpcs IN ({_codes_sql})
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
),
|
||||
pcs AS (
|
||||
SELECT hcpcs_code AS hcpcs, 1 AS attribution_code
|
||||
FROM cms.primary_care_service_code
|
||||
)
|
||||
SELECT coalesce(fin.hcpcs, prop.hcpcs) AS hcpcs,
|
||||
coalesce(fin.description, prop.description_p) AS description,
|
||||
fin.total_2026f, prop.total_2027p,
|
||||
coalesce(pcs.attribution_code, 0) = 1 AS mssp_attribution
|
||||
FROM fin
|
||||
FULL JOIN prop ON fin.hcpcs = prop.hcpcs
|
||||
LEFT JOIN pcs ON coalesce(fin.hcpcs, prop.hcpcs) = pcs.hcpcs
|
||||
""")
|
||||
|
||||
_cf26 = RULES[2026].conversion_factor
|
||||
_cf27p = proposed_for(2027).conversion_factor
|
||||
family_pay = (
|
||||
fam_df.join(_raw, on="hcpcs", how="left")
|
||||
.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"),
|
||||
)
|
||||
.sort("family", "hcpcs")
|
||||
)
|
||||
family_pay
|
||||
return (family_pay,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(AMBER, RULES, TEAL, alt, family_pay, fr_md, mo, pl, proposed_for):
|
||||
_priced = family_pay.filter(
|
||||
pl.col("dollar_2026final").is_not_null()
|
||||
& pl.col("dollar_2027proposed").is_not_null()
|
||||
).with_columns(
|
||||
pl.when(pl.col("pct_change") >= 0)
|
||||
.then(pl.lit("Gain"))
|
||||
.otherwise(pl.lit("Loss"))
|
||||
.alias("sign"),
|
||||
(pl.col("hcpcs") + pl.when(pl.col("mssp_attribution")).then(pl.lit(" ●")).otherwise(pl.lit(""))).alias(
|
||||
"code_label"
|
||||
),
|
||||
)
|
||||
_unpriced = family_pay.filter(
|
||||
pl.col("dollar_2026final").is_null() | pl.col("dollar_2027proposed").is_null()
|
||||
)
|
||||
|
||||
family_chart = (
|
||||
alt.Chart(_priced.to_pandas())
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("pct_change:Q", title="% change, CY2026 final → CY2027 proposed (national, GPCI=1.0)"),
|
||||
y=alt.Y("code_label: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", "family", "description", "mssp_attribution",
|
||||
alt.Tooltip("dollar_2026final:Q", format="$.2f"),
|
||||
alt.Tooltip("dollar_2027proposed:Q", format="$.2f"),
|
||||
alt.Tooltip("pct_change:Q", format="+.2f"),
|
||||
],
|
||||
)
|
||||
.properties(
|
||||
title="ACP / Care-Management / APCM Codes — Proposed Payment Change",
|
||||
width=620,
|
||||
height=max(220, 16 * _priced.height),
|
||||
)
|
||||
.facet(row=alt.Row("family:N", title=None))
|
||||
.resolve_scale(y="independent")
|
||||
)
|
||||
|
||||
_missing_lines = (
|
||||
"\n".join(
|
||||
f" - `{r['hcpcs']}` ({r['family']}) — priced in "
|
||||
+ (
|
||||
"CY2026 final only"
|
||||
if r["dollar_2026final"] is not None
|
||||
else "CY2027 proposal only"
|
||||
if r["dollar_2027proposed"] is not None
|
||||
else "neither table"
|
||||
)
|
||||
for r in _unpriced.iter_rows(named=True)
|
||||
)
|
||||
or " - (none — every declared code prices in both tables)"
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
family_chart,
|
||||
mo.md(f"""
|
||||
● = MSSP designated primary care service (attribution
|
||||
code). One-sided or unpriced codes, excluded from the
|
||||
chart rather than shown with a fabricated baseline:
|
||||
|
||||
{_missing_lines}
|
||||
|
||||
**Sources:** `pfs.rvu` CY2026 Final
|
||||
({fr_md(RULES[2026].federal_register_citation)});
|
||||
`pfs.rvu_proposed` `CMS-1848-P`
|
||||
({fr_md(proposed_for(2027).federal_register_citation)}) —
|
||||
**final rule pending**. Dollars blend RVU and CF changes
|
||||
deliberately: this is what the proposal does to realized
|
||||
national payment for each service.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(AMBER, TEAL, alt, family_pay, mo, pl, q):
|
||||
# Long-term RVU trajectory for the declared codes: YoY % heatmap,
|
||||
# final rules 2015-2026 + CY2027 proposal.
|
||||
_codes_sql = ",".join(f"'{c}'" for c in family_pay["hcpcs"])
|
||||
_hist = q(f"""
|
||||
WITH 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 = '') AND year >= 2015
|
||||
AND hcpcs IN ({_codes_sql})
|
||||
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 = '')
|
||||
AND hcpcs IN ({_codes_sql})
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
)
|
||||
SELECT * FROM fin UNION ALL SELECT * FROM prop
|
||||
ORDER BY hcpcs, year
|
||||
""")
|
||||
|
||||
_long = (
|
||||
_hist.filter(pl.col("total_rvu").is_not_null())
|
||||
.sort("hcpcs", "year")
|
||||
.with_columns(
|
||||
(
|
||||
100
|
||||
* (pl.col("total_rvu") / pl.col("total_rvu").shift(1).over("hcpcs") - 1)
|
||||
)
|
||||
.round(2)
|
||||
.alias("rvu_yoy_pct")
|
||||
)
|
||||
.join(family_pay.select("hcpcs", "family"), on="hcpcs")
|
||||
)
|
||||
|
||||
family_heatmap = (
|
||||
alt.Chart(_long.to_pandas())
|
||||
.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", "family", "year:O",
|
||||
alt.Tooltip("total_rvu:Q", format=".2f"),
|
||||
alt.Tooltip("rvu_yoy_pct:Q", format="+.2f"),
|
||||
],
|
||||
)
|
||||
.properties(
|
||||
title="Total-RVU Year-over-Year % — RFI Code Families, 2015-2027p",
|
||||
width=560,
|
||||
height=16 * _long["hcpcs"].n_unique(),
|
||||
)
|
||||
)
|
||||
mo.vstack(
|
||||
[
|
||||
family_heatmap,
|
||||
mo.md("""
|
||||
First priced year per code is blank (no prior year). Late
|
||||
first-appearances are themselves informative: PCM
|
||||
(99424-7) enters in 2022, APCM (G0556-8) in 2025 — the
|
||||
between-visit care infrastructure the RFI's part (d)
|
||||
builds on is recent, which is part of why CMS is asking
|
||||
what the *next* care-management construct should require.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# ── 3. Provenance ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(fr_md, 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}
|
||||
({fr_md(_prop.federal_register_citation)}) published
|
||||
{_prop.published.isoformat()}, comments due
|
||||
**{_close.isoformat()}** (`proposed_for(2027).comment_close`,
|
||||
read live; today is {_today.isoformat()}). This notebook's
|
||||
exhibits and the PRISMA evidence base feed the P44 comment to
|
||||
docket CMS-2026-2377.
|
||||
"""),
|
||||
kind="success" if _open else "danger",
|
||||
)
|
||||
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.vstack(
|
||||
[
|
||||
mo.md("""
|
||||
## 3. Provenance
|
||||
|
||||
Ingest-log rows for the lake tables Section 2 reads, plus
|
||||
the evidence-base pipeline: PubMed identification —
|
||||
`dev/scripts/search_pubmed_palliative_rfi.py` (E-utilities,
|
||||
relevance-capped, run 2026-08-18); screening — `stack
|
||||
prisma screen palliative-rfi` with the project's Zotero
|
||||
criteria anchors; flow — `prisma.flow` over live Zotero
|
||||
tags. `cms.primary_care_service_code` is the aco module's
|
||||
attribution reference list (P39), not an FR ingest.
|
||||
"""),
|
||||
mo.ui.table(
|
||||
provenance.to_pandas(),
|
||||
label="Ingest Log — Lake Tables Used in Section 2",
|
||||
),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
Reference in New Issue
Block a user