feat: skin sub budget neutrality notebook — PFS cross-service impact
Some checks failed
CI / skinny-install (aco) (push) Successful in 48s
CI / lint-test (push) Successful in 1m13s
CI / skinny-install (api) (push) Successful in 28s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 29s
CI / skinny-install (cms) (push) Successful in 29s
CI / skinny-install (opps) (push) Successful in 29s
CI / skinny-install (perf) (push) Successful in 30s
CI / skinny-install (pfs) (push) Successful in 34s
Infra CI / notebooks (push) Successful in 6s
CI / skinny-install (rex) (push) Successful in 26s
Infra CI / docs (push) Failing after 6s
Infra CI / api (push) Successful in 5s
Infra CI / mc (push) Successful in 5s
CI / skinny-install (aco) (pull_request) Successful in 50s
CI / lint-test (pull_request) Successful in 1m17s
CI / skinny-install (api) (pull_request) Successful in 26s
CI / skinny-install (bib) (pull_request) Successful in 33s
CI / skinny-install (bcda) (pull_request) Successful in 36s
CI / skinny-install (cms) (pull_request) Successful in 30s
CI / skinny-install (pfs) (pull_request) Successful in 30s
Infra CI / notebooks (pull_request) Successful in 6s
Infra CI / docs (pull_request) Failing after 8s
Infra CI / api (pull_request) Successful in 6s
CI / skinny-install (bib) (push) Successful in 31s
CI / skinny-install (bls) (push) Successful in 29s
CI / skinny-install (conf) (push) Successful in 29s
Infra CI / zotero (push) Successful in 6s
CI / skinny-install (bls) (pull_request) Successful in 24s
CI / skinny-install (ccw) (pull_request) Successful in 35s
CI / skinny-install (cli) (pull_request) Successful in 36s
CI / skinny-install (conf) (pull_request) Successful in 29s
CI / skinny-install (opps) (pull_request) Successful in 35s
CI / skinny-install (perf) (pull_request) Successful in 31s
Infra CI / zotero (pull_request) Successful in 9s
CI / skinny-install (rex) (pull_request) Successful in 30s
Infra CI / mc (pull_request) Successful in 6s
Some checks failed
CI / skinny-install (aco) (push) Successful in 48s
CI / lint-test (push) Successful in 1m13s
CI / skinny-install (api) (push) Successful in 28s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 29s
CI / skinny-install (cms) (push) Successful in 29s
CI / skinny-install (opps) (push) Successful in 29s
CI / skinny-install (perf) (push) Successful in 30s
CI / skinny-install (pfs) (push) Successful in 34s
Infra CI / notebooks (push) Successful in 6s
CI / skinny-install (rex) (push) Successful in 26s
Infra CI / docs (push) Failing after 6s
Infra CI / api (push) Successful in 5s
Infra CI / mc (push) Successful in 5s
CI / skinny-install (aco) (pull_request) Successful in 50s
CI / lint-test (pull_request) Successful in 1m17s
CI / skinny-install (api) (pull_request) Successful in 26s
CI / skinny-install (bib) (pull_request) Successful in 33s
CI / skinny-install (bcda) (pull_request) Successful in 36s
CI / skinny-install (cms) (pull_request) Successful in 30s
CI / skinny-install (pfs) (pull_request) Successful in 30s
Infra CI / notebooks (pull_request) Successful in 6s
Infra CI / docs (pull_request) Failing after 8s
Infra CI / api (pull_request) Successful in 6s
CI / skinny-install (bib) (push) Successful in 31s
CI / skinny-install (bls) (push) Successful in 29s
CI / skinny-install (conf) (push) Successful in 29s
Infra CI / zotero (push) Successful in 6s
CI / skinny-install (bls) (pull_request) Successful in 24s
CI / skinny-install (ccw) (pull_request) Successful in 35s
CI / skinny-install (cli) (pull_request) Successful in 36s
CI / skinny-install (conf) (pull_request) Successful in 29s
CI / skinny-install (opps) (pull_request) Successful in 35s
CI / skinny-install (perf) (pull_request) Successful in 31s
Infra CI / zotero (pull_request) Successful in 9s
CI / skinny-install (rex) (pull_request) Successful in 30s
Infra CI / mc (pull_request) Successful in 6s
Analyses how skin substitute application code (15271-15278) RVU growth redistributes payment away from other services under PFS budget neutrality. 8 sections: 1. Skin sub share of unweighted RVU pool over time 2. PE RVU revaluation trajectory (PE grew 34% while Work stayed flat) 3. Implied budget neutrality tax rate per year 4. Service category burden (which specialties absorb the compression) 5. Conversion factor erosion vs RVU pool growth 6. Per-code dollar impact on common services (99213, 27447, etc.) 7. Cumulative PE RVU structural tax since 2015 8. Key findings (volume amplifier, PE battleground, CY2026 shift) refs #246
This commit is contained in:
564
notebooks/skin_sub_budget_neutrality.py
Normal file
564
notebooks/skin_sub_budget_neutrality.py
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
import marimo
|
||||||
|
|
||||||
|
__generated_with = "0.21.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("""
|
||||||
|
# PFS Budget Neutrality Impact of Skin Substitute Codes
|
||||||
|
|
||||||
|
The Physician Fee Schedule is **budget neutral** — when CMS raises
|
||||||
|
RVUs for any service, it must lower them elsewhere (or reduce the
|
||||||
|
conversion factor) so aggregate spending stays flat. This notebook
|
||||||
|
quantifies how skin substitute application code revaluations
|
||||||
|
redistribute payment away from other services.
|
||||||
|
|
||||||
|
**Mechanism:** CMS publishes ~11,000 HCPCS codes with Work, PE, and
|
||||||
|
MP RVUs. When the sum of (RVU × frequency) grows, the conversion
|
||||||
|
factor or a budget neutrality adjustor (BNA) scales down to
|
||||||
|
compensate. Every code shares the compression proportionally.
|
||||||
|
|
||||||
|
**What this means:** A PE RVU increase on 15271–15278 is not free
|
||||||
|
money — it is a tax on every other service in the fee schedule.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _():
|
||||||
|
import altair as alt
|
||||||
|
import polars as pl
|
||||||
|
from conf import connect
|
||||||
|
from pfs.rules import RULES
|
||||||
|
|
||||||
|
con = connect.duckdb()
|
||||||
|
|
||||||
|
def q(sql):
|
||||||
|
return con.execute(sql).pl()
|
||||||
|
|
||||||
|
SKIN_CODES = "('15271','15272','15273','15274','15275','15276','15277','15278')"
|
||||||
|
|
||||||
|
return RULES, SKIN_CODES, alt, con, pl, q
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1. Skin sub RVU growth vs. total pool ────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 1. Skin Substitute Share of the RVU Pool
|
||||||
|
|
||||||
|
The unweighted RVU pool (sum of all base-mod RVUs across ~11k codes)
|
||||||
|
grows over time as CMS adds codes and revalues services. The skin
|
||||||
|
sub application codes (15271–15278) are a small but growing share.
|
||||||
|
|
||||||
|
> **Note:** Budget neutrality operates on *frequency-weighted* RVUs
|
||||||
|
> (RVU × utilization), not unweighted sums. Without CMS utilization
|
||||||
|
> data, this analysis uses unweighted RVUs as a structural proxy.
|
||||||
|
> The actual impact is amplified by the explosive volume growth in
|
||||||
|
> skin substitute claims documented by OIG.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(SKIN_CODES, alt, mo, q):
|
||||||
|
pool_share = q(f"""
|
||||||
|
WITH pool AS (
|
||||||
|
SELECT year,
|
||||||
|
sum(work_rvu + non_fac_pe_rvu + mp_rvu) as total_rvu,
|
||||||
|
sum(CASE WHEN hcpcs IN {SKIN_CODES}
|
||||||
|
THEN work_rvu + non_fac_pe_rvu + mp_rvu ELSE 0 END) as skin_rvu,
|
||||||
|
sum(CASE WHEN hcpcs NOT IN {SKIN_CODES}
|
||||||
|
THEN work_rvu + non_fac_pe_rvu + mp_rvu ELSE 0 END) as other_rvu
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE mod IS NULL OR mod = ''
|
||||||
|
GROUP BY year
|
||||||
|
)
|
||||||
|
SELECT year,
|
||||||
|
round(skin_rvu, 2) as skin_rvu,
|
||||||
|
round(total_rvu, 1) as total_rvu,
|
||||||
|
round(100.0 * skin_rvu / total_rvu, 4) as skin_pct,
|
||||||
|
round(skin_rvu - LAG(skin_rvu) OVER (ORDER BY year), 2) as skin_delta,
|
||||||
|
round(total_rvu - LAG(total_rvu) OVER (ORDER BY year), 1) as pool_delta
|
||||||
|
FROM pool ORDER BY year
|
||||||
|
""")
|
||||||
|
|
||||||
|
share_chart = (
|
||||||
|
alt.Chart(pool_share.to_pandas())
|
||||||
|
.mark_bar()
|
||||||
|
.encode(
|
||||||
|
x=alt.X("year:O", title="Year"),
|
||||||
|
y=alt.Y("skin_rvu:Q", title="Skin Sub Total NF RVUs (8 codes)"),
|
||||||
|
tooltip=["year", "skin_rvu", "total_rvu", "skin_pct", "skin_delta"],
|
||||||
|
)
|
||||||
|
.properties(title="Skin Sub Application Codes — Total NF RVUs by Year", width=700, height=300)
|
||||||
|
)
|
||||||
|
|
||||||
|
mo.vstack([share_chart, pool_share])
|
||||||
|
return (pool_share,)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 2. PE RVU revaluation trajectory ─────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 2. Practice Expense RVU Revaluation
|
||||||
|
|
||||||
|
PE is the largest RVU component for skin sub application codes and
|
||||||
|
is where the budget neutrality tax bites hardest. When CMS increases
|
||||||
|
PE RVUs for these codes (e.g., to reflect updated clinical labor
|
||||||
|
rates or supply costs), **all other codes' PE RVUs must absorb a
|
||||||
|
compensating reduction** via the BNA.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(alt, q):
|
||||||
|
pe_trajectory = q("""
|
||||||
|
SELECT year, hcpcs,
|
||||||
|
CASE hcpcs
|
||||||
|
WHEN '15271' THEN '15271 trunk <100cm²'
|
||||||
|
WHEN '15272' THEN '15272 trunk add-on'
|
||||||
|
WHEN '15275' THEN '15275 face <100cm²'
|
||||||
|
WHEN '15276' THEN '15276 face add-on'
|
||||||
|
END as label,
|
||||||
|
non_fac_pe_rvu,
|
||||||
|
work_rvu,
|
||||||
|
mp_rvu,
|
||||||
|
non_fac_pe_rvu + work_rvu + mp_rvu as total_nf_rvu
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE hcpcs IN ('15271','15272','15275','15276')
|
||||||
|
AND (mod IS NULL OR mod = '')
|
||||||
|
ORDER BY year, hcpcs
|
||||||
|
""")
|
||||||
|
|
||||||
|
pe_chart = (
|
||||||
|
alt.Chart(pe_trajectory.to_pandas())
|
||||||
|
.mark_line(point=True)
|
||||||
|
.encode(
|
||||||
|
x=alt.X("year:O", title="Year"),
|
||||||
|
y=alt.Y("non_fac_pe_rvu:Q", title="Non-Facility PE RVU"),
|
||||||
|
color=alt.Color("label:N", title="Code"),
|
||||||
|
tooltip=["year", "hcpcs", "label", "non_fac_pe_rvu", "work_rvu", "total_nf_rvu"],
|
||||||
|
)
|
||||||
|
.properties(title="Practice Expense RVU Trajectory", width=700, height=350)
|
||||||
|
)
|
||||||
|
pe_chart
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── 3. Implied budget neutrality tax ─────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 3. Implied Budget Neutrality Tax
|
||||||
|
|
||||||
|
When skin sub PE RVUs increase by Δ, **every other code's effective
|
||||||
|
payment decreases** proportionally. The "tax rate" is:
|
||||||
|
|
||||||
|
```
|
||||||
|
tax_rate = skin_sub_PE_delta / total_pool_PE
|
||||||
|
```
|
||||||
|
|
||||||
|
This table shows the year-over-year PE RVU increase for the 8 skin
|
||||||
|
sub codes, the total PE pool, and the implied compression on all
|
||||||
|
other codes.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(SKIN_CODES, q):
|
||||||
|
bn_tax = q(f"""
|
||||||
|
WITH yearly AS (
|
||||||
|
SELECT year,
|
||||||
|
sum(CASE WHEN hcpcs IN {SKIN_CODES} THEN non_fac_pe_rvu ELSE 0 END) as skin_pe,
|
||||||
|
sum(non_fac_pe_rvu) as total_pe,
|
||||||
|
sum(CASE WHEN hcpcs NOT IN {SKIN_CODES} THEN non_fac_pe_rvu ELSE 0 END) as other_pe
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE mod IS NULL OR mod = ''
|
||||||
|
GROUP BY year
|
||||||
|
)
|
||||||
|
SELECT year,
|
||||||
|
round(skin_pe, 2) as skin_pe_rvu,
|
||||||
|
round(total_pe, 1) as total_pe_pool,
|
||||||
|
round(skin_pe - LAG(skin_pe) OVER (ORDER BY year), 2) as skin_pe_delta,
|
||||||
|
round(total_pe - LAG(total_pe) OVER (ORDER BY year), 1) as pool_pe_delta,
|
||||||
|
-- If skin PE grew and pool grew less, the difference is absorbed by others
|
||||||
|
round(CASE WHEN LAG(skin_pe) OVER (ORDER BY year) IS NOT NULL
|
||||||
|
THEN (skin_pe - LAG(skin_pe) OVER (ORDER BY year))
|
||||||
|
/ NULLIF(LAG(total_pe) OVER (ORDER BY year), 0) * 100
|
||||||
|
END, 4) as implied_tax_pct,
|
||||||
|
-- Dollar impact: tax_pct × average CF
|
||||||
|
round(CASE WHEN LAG(skin_pe) OVER (ORDER BY year) IS NOT NULL
|
||||||
|
THEN (skin_pe - LAG(skin_pe) OVER (ORDER BY year))
|
||||||
|
/ NULLIF(LAG(total_pe) OVER (ORDER BY year), 0) * 100
|
||||||
|
END, 4) as pct_compression
|
||||||
|
FROM yearly ORDER BY year
|
||||||
|
""")
|
||||||
|
bn_tax
|
||||||
|
return (bn_tax,)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 4. Which services bear the burden? ────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 4. Which Services Bear the Burden?
|
||||||
|
|
||||||
|
Budget neutrality compression is proportional to each service
|
||||||
|
category's share of the PE pool. Categories with large PE shares
|
||||||
|
(surgery, cardiology) absorb more dollars even though the per-code
|
||||||
|
reduction is tiny.
|
||||||
|
|
||||||
|
The table shows: if the skin sub PE increase in the most recent year
|
||||||
|
were fully offset by compressing other categories, how much does
|
||||||
|
each category lose?
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(SKIN_CODES, alt, mo, q):
|
||||||
|
# Get the latest year's skin sub PE delta
|
||||||
|
skin_pe_delta = q(f"""
|
||||||
|
WITH yearly AS (
|
||||||
|
SELECT year, sum(non_fac_pe_rvu) as skin_pe
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE hcpcs IN {SKIN_CODES} AND (mod IS NULL OR mod = '')
|
||||||
|
GROUP BY year
|
||||||
|
)
|
||||||
|
SELECT year, skin_pe,
|
||||||
|
skin_pe - LAG(skin_pe) OVER (ORDER BY year) as delta
|
||||||
|
FROM yearly ORDER BY year DESC LIMIT 1
|
||||||
|
""")
|
||||||
|
|
||||||
|
delta_val = skin_pe_delta.select("delta").item()
|
||||||
|
delta_year = skin_pe_delta.select("year").item()
|
||||||
|
|
||||||
|
category_impact = q(f"""
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN hcpcs BETWEEN '99201' AND '99499' THEN 'E/M'
|
||||||
|
WHEN hcpcs BETWEEN '10000' AND '19999' THEN 'Integumentary'
|
||||||
|
WHEN hcpcs BETWEEN '20000' AND '29999' THEN 'Musculoskeletal'
|
||||||
|
WHEN hcpcs BETWEEN '30000' AND '39999' THEN 'Resp/Cardiovascular'
|
||||||
|
WHEN hcpcs BETWEEN '40000' AND '49999' THEN 'Digestive'
|
||||||
|
WHEN hcpcs BETWEEN '50000' AND '59999' THEN 'Urinary/Genital'
|
||||||
|
WHEN hcpcs BETWEEN '60000' AND '69999' THEN 'Endocrine/Nervous'
|
||||||
|
WHEN hcpcs BETWEEN '70000' AND '79999' THEN 'Radiology'
|
||||||
|
WHEN hcpcs BETWEEN '80000' AND '89999' THEN 'Path/Lab'
|
||||||
|
WHEN hcpcs BETWEEN '90000' AND '99199' THEN 'Medicine'
|
||||||
|
WHEN hcpcs LIKE 'G%' THEN 'G-codes'
|
||||||
|
ELSE 'Other'
|
||||||
|
END as category,
|
||||||
|
count(*) as codes,
|
||||||
|
round(sum(non_fac_pe_rvu), 1) as category_pe,
|
||||||
|
round(100.0 * sum(non_fac_pe_rvu) /
|
||||||
|
NULLIF((SELECT sum(non_fac_pe_rvu) FROM pfs.rvu
|
||||||
|
WHERE year={delta_year} AND (mod IS NULL OR mod = '')
|
||||||
|
AND hcpcs NOT IN {SKIN_CODES}), 0), 2) as pe_share_pct,
|
||||||
|
-- Implied PE reduction absorbed by this category
|
||||||
|
round({delta_val} * sum(non_fac_pe_rvu) /
|
||||||
|
NULLIF((SELECT sum(non_fac_pe_rvu) FROM pfs.rvu
|
||||||
|
WHERE year={delta_year} AND (mod IS NULL OR mod = '')
|
||||||
|
AND hcpcs NOT IN {SKIN_CODES}), 0), 4) as implied_pe_loss
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE year = {delta_year}
|
||||||
|
AND (mod IS NULL OR mod = '')
|
||||||
|
AND hcpcs NOT IN {SKIN_CODES}
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY category_pe DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
impact_chart = (
|
||||||
|
alt.Chart(category_impact.to_pandas())
|
||||||
|
.mark_bar()
|
||||||
|
.encode(
|
||||||
|
x=alt.X("implied_pe_loss:Q", title=f"Implied PE RVU Loss (from {delta_val:+.2f} skin sub PE delta)"),
|
||||||
|
y=alt.Y("category:N", title="", sort="-x"),
|
||||||
|
color=alt.Color("pe_share_pct:Q", title="PE Pool Share %",
|
||||||
|
scale=alt.Scale(scheme="reds")),
|
||||||
|
tooltip=["category", "codes", "category_pe", "pe_share_pct", "implied_pe_loss"],
|
||||||
|
)
|
||||||
|
.properties(
|
||||||
|
title=f"Budget Neutrality Burden by Service Category (CY{delta_year})",
|
||||||
|
width=700, height=350,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
mo.vstack([
|
||||||
|
mo.md(f"""
|
||||||
|
**CY{delta_year}:** Skin sub application codes gained **{delta_val:+.2f} PE RVUs**.
|
||||||
|
Under budget neutrality, this is redistributed across ~{category_impact.select('codes').sum().item():,} other codes
|
||||||
|
proportional to their PE share.
|
||||||
|
"""),
|
||||||
|
impact_chart,
|
||||||
|
category_impact,
|
||||||
|
])
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── 5. Conversion factor erosion ─────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 5. Conversion Factor Erosion
|
||||||
|
|
||||||
|
The conversion factor has declined from $36.09 (2020) to $32.35
|
||||||
|
(2025–2026). While this is driven primarily by MACRA spending
|
||||||
|
targets, **RVU pool growth contributes to the pressure.** When total
|
||||||
|
unweighted RVUs grow faster than allowed spending, the CF must
|
||||||
|
decline to maintain budget neutrality.
|
||||||
|
|
||||||
|
This chart overlays the CF trajectory with the total RVU pool
|
||||||
|
growth to show the inverse relationship.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(RULES, alt, pl, q):
|
||||||
|
cf_data = pl.DataFrame({
|
||||||
|
"year": list(RULES.keys()),
|
||||||
|
"conversion_factor": [r.conversion_factor for r in RULES.values()],
|
||||||
|
})
|
||||||
|
|
||||||
|
pool_growth = q("""
|
||||||
|
SELECT year, round(sum(work_rvu + non_fac_pe_rvu + mp_rvu), 0) as total_rvu
|
||||||
|
FROM pfs.rvu WHERE mod IS NULL OR mod = ''
|
||||||
|
GROUP BY year ORDER BY year
|
||||||
|
""")
|
||||||
|
|
||||||
|
combined = cf_data.join(pool_growth, on="year", how="inner")
|
||||||
|
|
||||||
|
cf_line = (
|
||||||
|
alt.Chart(combined.to_pandas())
|
||||||
|
.mark_line(point=True, color="#1f77b4")
|
||||||
|
.encode(
|
||||||
|
x=alt.X("year:O", title="Year"),
|
||||||
|
y=alt.Y("conversion_factor:Q", title="Conversion Factor ($)",
|
||||||
|
scale=alt.Scale(zero=False)),
|
||||||
|
tooltip=["year", "conversion_factor", "total_rvu"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
rvu_line = (
|
||||||
|
alt.Chart(combined.to_pandas())
|
||||||
|
.mark_line(point=True, color="#d62728", strokeDash=[4, 4])
|
||||||
|
.encode(
|
||||||
|
x=alt.X("year:O"),
|
||||||
|
y=alt.Y("total_rvu:Q", title="Total Unweighted RVU Pool",
|
||||||
|
scale=alt.Scale(zero=False)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cf_chart = (
|
||||||
|
alt.layer(cf_line, rvu_line)
|
||||||
|
.resolve_scale(y="independent")
|
||||||
|
.properties(title="Conversion Factor vs. RVU Pool Growth", width=700, height=350)
|
||||||
|
)
|
||||||
|
cf_chart
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── 6. Per-code dollar impact ─────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 6. Dollar Impact on Common Services
|
||||||
|
|
||||||
|
How much does a typical office visit, imaging study, or surgical
|
||||||
|
procedure lose when skin sub PE RVUs increase? This table shows
|
||||||
|
the implied payment reduction for commonly billed codes.
|
||||||
|
|
||||||
|
The calculation: if skin sub PE grows by Δ and the total PE pool
|
||||||
|
is P, then each other code's PE is effectively reduced by
|
||||||
|
`code_PE × (Δ / P)`, and payment drops by that × CF.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(RULES, SKIN_CODES, mo, q):
|
||||||
|
|
||||||
|
latest_year = max(RULES.keys())
|
||||||
|
cf = RULES[latest_year].conversion_factor
|
||||||
|
|
||||||
|
# Latest skin PE delta
|
||||||
|
skin_delta = q(f"""
|
||||||
|
WITH yearly AS (
|
||||||
|
SELECT year, sum(non_fac_pe_rvu) as skin_pe
|
||||||
|
FROM pfs.rvu WHERE hcpcs IN {SKIN_CODES} AND (mod IS NULL OR mod = '')
|
||||||
|
GROUP BY year
|
||||||
|
)
|
||||||
|
SELECT skin_pe - LAG(skin_pe) OVER (ORDER BY year) as delta
|
||||||
|
FROM yearly ORDER BY year DESC LIMIT 1
|
||||||
|
""").item()
|
||||||
|
|
||||||
|
total_pe = q(f"""
|
||||||
|
SELECT sum(non_fac_pe_rvu) FROM pfs.rvu
|
||||||
|
WHERE year = {latest_year} AND (mod IS NULL OR mod = '')
|
||||||
|
AND hcpcs NOT IN {SKIN_CODES}
|
||||||
|
""").item()
|
||||||
|
|
||||||
|
tax_rate = skin_delta / total_pe if total_pe else 0
|
||||||
|
|
||||||
|
common_codes = q(f"""
|
||||||
|
SELECT hcpcs, description,
|
||||||
|
non_fac_pe_rvu,
|
||||||
|
work_rvu,
|
||||||
|
mp_rvu,
|
||||||
|
non_fac_pe_rvu + work_rvu + mp_rvu as total_rvu,
|
||||||
|
round(non_fac_pe_rvu * {tax_rate}, 6) as pe_rvu_loss,
|
||||||
|
round(non_fac_pe_rvu * {tax_rate} * {cf}, 4) as dollar_loss
|
||||||
|
FROM pfs.rvu
|
||||||
|
WHERE year = {latest_year}
|
||||||
|
AND (mod IS NULL OR mod = '')
|
||||||
|
AND hcpcs IN ('99213','99214','99215',
|
||||||
|
'99203','99204','99205',
|
||||||
|
'27447','27130',
|
||||||
|
'43239','45380',
|
||||||
|
'93000','93306',
|
||||||
|
'77067','74177',
|
||||||
|
'36415','85025',
|
||||||
|
'90834','90837',
|
||||||
|
'17000','11102')
|
||||||
|
ORDER BY dollar_loss
|
||||||
|
""")
|
||||||
|
|
||||||
|
mo.vstack([
|
||||||
|
mo.md(f"""
|
||||||
|
**CY{latest_year}** parameters:
|
||||||
|
- Skin sub PE delta: **{skin_delta:+.2f} RVUs**
|
||||||
|
- Other-code PE pool: **{total_pe:,.1f} RVUs**
|
||||||
|
- Implied tax rate: **{tax_rate*100:.4f}%** of each code's PE
|
||||||
|
- CF: **${cf}**
|
||||||
|
"""),
|
||||||
|
common_codes,
|
||||||
|
])
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── 7. Cumulative tax since 2015 ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 7. Cumulative Structural Tax Since 2015
|
||||||
|
|
||||||
|
The skin sub application codes' total PE RVUs have grown from
|
||||||
|
16.45 (2015) to 22.05 (2026). Under budget neutrality, this
|
||||||
|
5.60 RVU increase must come from somewhere.
|
||||||
|
|
||||||
|
This chart shows the cumulative PE RVU "withdrawn" from the
|
||||||
|
rest of the fee schedule by the growth in skin sub PE.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(SKIN_CODES, alt, q):
|
||||||
|
cumulative = q(f"""
|
||||||
|
WITH yearly AS (
|
||||||
|
SELECT year,
|
||||||
|
sum(CASE WHEN hcpcs IN {SKIN_CODES} THEN non_fac_pe_rvu ELSE 0 END) as skin_pe,
|
||||||
|
sum(non_fac_pe_rvu) as total_pe
|
||||||
|
FROM pfs.rvu WHERE mod IS NULL OR mod = ''
|
||||||
|
GROUP BY year
|
||||||
|
)
|
||||||
|
SELECT year,
|
||||||
|
round(skin_pe, 2) as skin_pe,
|
||||||
|
round(skin_pe - FIRST_VALUE(skin_pe) OVER (ORDER BY year), 2) as cumulative_pe_growth,
|
||||||
|
round(total_pe, 1) as total_pe
|
||||||
|
FROM yearly ORDER BY year
|
||||||
|
""")
|
||||||
|
|
||||||
|
cum_chart = (
|
||||||
|
alt.Chart(cumulative.to_pandas())
|
||||||
|
.mark_area(opacity=0.3, color="#d62728")
|
||||||
|
.encode(
|
||||||
|
x=alt.X("year:O", title="Year"),
|
||||||
|
y=alt.Y("cumulative_pe_growth:Q",
|
||||||
|
title="Cumulative Skin Sub PE Growth (RVUs above 2015 baseline)"),
|
||||||
|
tooltip=["year", "skin_pe", "cumulative_pe_growth", "total_pe"],
|
||||||
|
)
|
||||||
|
) + (
|
||||||
|
alt.Chart(cumulative.to_pandas())
|
||||||
|
.mark_line(point=True, color="#d62728")
|
||||||
|
.encode(
|
||||||
|
x="year:O",
|
||||||
|
y="cumulative_pe_growth:Q",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
cum_chart_final = cum_chart.properties(
|
||||||
|
title="Cumulative PE RVU Growth — Skin Sub Application Codes vs. 2015 Baseline",
|
||||||
|
width=700, height=300,
|
||||||
|
)
|
||||||
|
cum_chart_final
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# ── 8. Key findings ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell(hide_code=True)
|
||||||
|
def _(mo):
|
||||||
|
mo.md("""
|
||||||
|
## 8. Key Findings
|
||||||
|
|
||||||
|
1. **Structural tax is real but small per-code.** The unweighted RVU
|
||||||
|
share of skin sub application codes is ~0.1% of the pool.
|
||||||
|
Per-code compression is fractions of a cent.
|
||||||
|
|
||||||
|
2. **Volume is the amplifier.** The structural RVU analysis
|
||||||
|
understates the true impact because it ignores utilization.
|
||||||
|
OIG documented explosive volume growth in skin sub claims — when
|
||||||
|
frequency-weighted, these 8 codes consume a much larger share of
|
||||||
|
aggregate spending than their unweighted RVUs suggest.
|
||||||
|
|
||||||
|
3. **PE is the battleground.** Work RVUs for 15271–15278 have been
|
||||||
|
stable (13.46 total for 10 years). PE RVUs grew from 16.45 to
|
||||||
|
22.05 (+34%). The CY2022 clinical labor rate update was a major
|
||||||
|
driver.
|
||||||
|
|
||||||
|
4. **CY2026 reclassification shifts the tax.** Moving skin subs
|
||||||
|
from ASP + 6% (OPPS) to flat $127.28/cm² doesn't directly
|
||||||
|
affect the PFS budget neutrality pool — but it does change
|
||||||
|
the volume incentives that drive utilization of application
|
||||||
|
codes 15271–15278.
|
||||||
|
|
||||||
|
5. **The real cost is in the product, not the application.** For a
|
||||||
|
25cm² wound, the application fee (~$140) is dwarfed by the
|
||||||
|
product cost (often >$5,000). Budget neutrality only governs
|
||||||
|
the application fee; the product cost is outside the PFS.
|
||||||
|
""")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
Reference in New Issue
Block a user