Files
stack/notebooks/skin_sub_budget_neutrality.py
kert 71861d9d32
All checks were successful
CI / lint (push) Successful in 29s
CI / notebooks-smoke (push) Successful in 1m25s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 56s
Infra CI / zotero (push) Successful in 13s
Infra CI / docs (push) Successful in 1m14s
Infra CI / api (push) Successful in 50s
Infra CI / mc (push) Successful in 19s
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 14m27s
Harden / build-scan-report (push) Successful in 26m15s
Renovate / renovate (push) Successful in 15s
Notebooks Integration / notebooks-integration (push) Successful in 7m16s
Zotero Sync / zotero-sync (push) Successful in 53s
Package Supply Chain / pkg-supply-chain (push) Successful in 58s
fix(lake): M5 covered OPPS only — build out PFS (refs #514)
The M5 close-out missed half the issue's scope: #514 says 'OPPS/PFS
reference data' and I cut over only OPPS, leaving PFS — the largest
reference domain, 23.5M rows across 8 tables — entirely on the
monolith, including pfs.* queries in the very notebook whose OPPS
query was migrated. This completes PFS the same way:

- publish_opps_to_lake.py → publish_reference_to_lake.py with a
  schema registry (opps: 3 tables, pfs: 8); host-side docker-exec
  wrapper extracted to dev/scripts/_lake.py, shared by the ingests.
- PFS published to the lake and read-back verified: carrier_locality
  21,863,770 rows in 10.1s, plus rvu/gpci/clinical_labor/medical_
  equipment/medical_supply/physician_work_time/zip_carrier_locality.
- New dev/scripts/ingest_pfs.py wraps pfs.pipe.load_all (previously
  ad-hoc, no entrypoint) with the standard plumbing: duckdb_batch
  preflight, replica refresh, lake publish.
- 5 notebooks migrated: pfs_calcs, pfs_reconciliation,
  skin_sub_budget_neutrality read the lake as their primary
  connection; skin_sub_pricing and skin_sub_cost_sharing switch their
  pure-pfs cells to the lake. The one cross-source join
  (pfs × skin_subs) stays on the monolith mirror, annotated.
  All 5 headless-verified in prod: zero cell errors.
- pfs_calcs leaves the pre-commit host-run safe list (the lake catalog
  is compose-internal); the nightly integration covers it in-container.
2026-07-10 23:47:51 -04:00

609 lines
20 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.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 1527115278 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
# PFS reference data lives in the DuckLake lakehouse (M5, #514);
# queries are unchanged — the lake is the default database.
con = connect.ducklake()
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 (1527115278) 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
(20252026). 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 1527115278 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 1527115278.
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()