Files
stack/notebooks/skin_sub_pricing.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

530 lines
15 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("""
# Skin Substitute Pricing Over Time
Tracks skin substitute application code RVUs and locality-based
payment amounts (PFS) alongside quarterly ASP drug pricing, layered
over the OPPS payment method timeline (pass-through → high/low →
flat rate).
**Data sources:**
- `pfs.rvu` — Work, PE, MP RVUs for application codes 1527115278
- `pfs.carrier_locality` — CMS-published locality fees (ground truth)
- `pfs.gpci` — Geographic practice cost indices by MAC/locality
- `skin_subs.asp_quarterly` — Quarterly ASP + 6% payment limits
- `opps.skin_sub_addendum_b` — OPPS status indicators and APC rates
""")
return
@app.cell(hide_code=True)
def _():
import altair as alt
import polars as pl
from conf import connect
con = connect.duckdb()
# OPPS reference data lives in the DuckLake lakehouse (M5, #514);
# the monolith's opps schema is a deprecated mirror.
lake = connect.ducklake()
def q(sql):
return con.execute(sql).pl()
def ql(sql):
"""Query the lake (OPPS reference tables)."""
return lake.execute(sql).pl()
return alt, con, lake, pl, q, ql
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 1. Application Code RVUs Over Time
CPT 1527115278 are the procedure codes for applying skin
substitutes. Their RVUs determine the surgeon's payment
independent of the product cost.
""")
return
@app.cell(hide_code=True)
def _(alt, ql):
rvu_ts = ql("""
SELECT year, hcpcs,
CASE hcpcs
WHEN '15271' THEN 'Trunk/limbs <100cm² (initial)'
WHEN '15272' THEN 'Trunk/limbs <100cm² (add-on)'
WHEN '15273' THEN 'Trunk/limbs ≥100cm² (initial)'
WHEN '15274' THEN 'Trunk/limbs ≥100cm² (add-on)'
WHEN '15275' THEN 'Face/hands/feet <100cm² (initial)'
WHEN '15276' THEN 'Face/hands/feet <100cm² (add-on)'
WHEN '15277' THEN 'Face/hands/feet ≥100cm² (initial)'
WHEN '15278' THEN 'Face/hands/feet ≥100cm² (add-on)'
END as description,
work_rvu, non_fac_pe_rvu, fac_pe_rvu, mp_rvu,
work_rvu + non_fac_pe_rvu + mp_rvu as non_fac_total,
work_rvu + fac_pe_rvu + mp_rvu as fac_total
FROM pfs.rvu
WHERE hcpcs IN ('15271','15272','15273','15274',
'15275','15276','15277','15278')
ORDER BY year, hcpcs
""")
rvu_chart = (
alt.Chart(rvu_ts.to_pandas())
.mark_line(point=True)
.encode(
x=alt.X("year:O", title="Year"),
y=alt.Y("non_fac_total:Q", title="Total Non-Facility RVUs"),
color=alt.Color("description:N", title="Code"),
tooltip=[
"year",
"hcpcs",
"description",
"work_rvu",
"non_fac_pe_rvu",
"mp_rvu",
"non_fac_total",
],
)
.properties(title="Application Code RVUs (Non-Facility)", width=700, height=400)
)
rvu_chart
return
@app.cell(hide_code=True)
def _(con, mo):
_regions = con.execute("""
SELECT DISTINCT locality_name
FROM pfs.gpci WHERE year = 2025
ORDER BY locality_name
""").fetchall()
region_picker = mo.ui.dropdown(
options={r[0]: r[0] for r in _regions},
value="MANHATTAN",
label="Region",
)
mo.md(f"""
## 2. Application Code Payment by Region
CMS-published carrier locality fees for skin sub application codes.
Select a region to see how payments vary geographically over time.
{region_picker}
""")
return (region_picker,)
@app.cell(hide_code=True)
def _(alt, q, region_picker):
_region = region_picker.value
carrier_ts = q(f"""
SELECT c.year, c.hcpcs,
CASE c.hcpcs
WHEN '15271' THEN 'Trunk <100cm²'
WHEN '15272' THEN 'Trunk <100cm² add-on'
WHEN '15273' THEN 'Trunk ≥100cm²'
WHEN '15274' THEN 'Trunk ≥100cm² add-on'
WHEN '15275' THEN 'Face/hands <100cm²'
WHEN '15276' THEN 'Face/hands <100cm² add-on'
WHEN '15277' THEN 'Face/hands ≥100cm²'
WHEN '15278' THEN 'Face/hands ≥100cm² add-on'
END as description,
c.non_fac_fee, c.fac_fee,
g.locality_name
FROM pfs.carrier_locality c
JOIN pfs.gpci g ON c.mac = g.mac AND c.locality = g.locality AND c.year = g.year
WHERE c.hcpcs IN ('15271','15272','15273','15274',
'15275','15276','15277','15278')
AND g.locality_name = '{_region}'
ORDER BY c.year, c.hcpcs
""")
carrier_chart = (
alt.Chart(carrier_ts.to_pandas())
.mark_line(point=True)
.encode(
x=alt.X("year:O", title="Year"),
y=alt.Y("non_fac_fee:Q", title="Non-Facility Fee ($)"),
color=alt.Color("description:N", title="Code"),
tooltip=[
"year",
"hcpcs",
"description",
"non_fac_fee",
"fac_fee",
"locality_name",
],
)
.properties(
title=f"Application Code Fees — {_region}",
width=700,
height=400,
)
)
carrier_chart
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 3. Regional Comparison — 15271 (Primary Application)
Non-facility fee for 15271 (trunk/limbs, <100cm², initial
application) across selected high-volume regions.
""")
return
@app.cell(hide_code=True)
def _(alt, q):
regions = [
"MANHATTAN",
"ALASKA*",
"REST OF FLORIDA",
"REST OF TEXAS",
"REST OF CALIFORNIA",
"CHICAGO",
"DETROIT",
"ATLANTA",
"HAWAII, GUAM",
"SOUTH CAROLINA",
]
region_list = ", ".join(f"'{r}'" for r in regions)
regional_15271 = q(f"""
SELECT c.year, g.locality_name,
c.non_fac_fee
FROM pfs.carrier_locality c
JOIN pfs.gpci g ON c.mac = g.mac AND c.locality = g.locality AND c.year = g.year
WHERE c.hcpcs = '15271'
AND g.locality_name IN ({region_list})
ORDER BY c.year, g.locality_name
""")
regional_chart = (
alt.Chart(regional_15271.to_pandas())
.mark_line(point=True)
.encode(
x=alt.X("year:O", title="Year"),
y=alt.Y("non_fac_fee:Q", title="Non-Facility Fee ($)"),
color=alt.Color("locality_name:N", title="Region"),
tooltip=["year", "locality_name", "non_fac_fee"],
)
.properties(
title="15271 Non-Facility Fee — Regional Comparison",
width=700,
height=400,
)
)
regional_chart
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 4. ASP Drug Pricing Over Time
Quarterly ASP + 6% payment limits for skin substitute products.
The flat horizontal line at **$127.28** marks the CY2026 flat rate —
products above it lose revenue, products below it gain.
""")
return
@app.cell(hide_code=True)
def _(alt, pl, q):
# Top products by volume (most quarters of data)
top_products = q("""
SELECT hcpcs_code, short_description,
count(*) as quarters
FROM skin_subs.asp_quarterly
GROUP BY hcpcs_code, short_description
HAVING count(*) >= 8
ORDER BY quarters DESC
LIMIT 15
""")
top_codes = top_products.select("hcpcs_code").to_series().to_list()
code_list = ", ".join(f"'{c}'" for c in top_codes)
asp_ts = q(f"""
SELECT quarter, hcpcs_code, short_description,
payment_limit, asp_per_unit
FROM skin_subs.asp_quarterly
WHERE hcpcs_code IN ({code_list})
ORDER BY quarter, hcpcs_code
""")
flat_rate_rule = (
alt.Chart(pl.DataFrame({"y": [127.28]}).to_pandas())
.mark_rule(color="red", strokeDash=[4, 4], strokeWidth=2)
.encode(y="y:Q")
)
asp_lines = (
alt.Chart(asp_ts.to_pandas())
.mark_line()
.encode(
x=alt.X(
"quarter:O",
title="Quarter",
axis=alt.Axis(labelAngle=-45, labelFontSize=8),
),
y=alt.Y(
"payment_limit:Q",
title="Payment Limit (ASP + 6%) $",
scale=alt.Scale(domainMax=800),
),
color=alt.Color("short_description:N", title="Product"),
tooltip=[
"quarter",
"hcpcs_code",
"short_description",
"payment_limit",
"asp_per_unit",
],
)
)
asp_chart = (asp_lines + flat_rate_rule).properties(
title="ASP Quarterly Payment Limits — Top 15 Products",
width=700,
height=450,
)
asp_chart
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 5. OPPS Payment Method Timeline
How CMS has paid for skin substitutes in the outpatient setting:
| Period | Method | Detail |
|--------|--------|--------|
| Pre-2023 | Pass-through | ASP + 6% per unit |
| 20232025 | High/low split | High-cost: pass-through; Low-cost: packaged into APC |
| 2026+ | Flat rate | $127.28/cm² for all products |
The table below shows OPPS status indicators and payment rates
for skin sub codes over time.
""")
return
@app.cell(hide_code=True)
def _(ql):
opps_ts = ql("""
SELECT year, hcpcs, short_description,
status_indicator, apc, payment_rate
FROM opps.skin_sub_addendum_b
WHERE payment_rate IS NOT NULL
AND payment_rate > 0
ORDER BY year, hcpcs
""")
opps_ts
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 6. Total Episode Cost: Product + Application
For a typical 25cm² wound, the total Medicare payment is:
- **Product cost**: ASP + 6% payment limit × 25 units
- **Application fee**: 15271 non-facility fee (PFS, locality-specific)
- **Post-2026**: $127.28 × 25 = $3,182 flat + application fee
This chart shows the combined cost trend for selected products.
""")
return
@app.cell(hide_code=True)
def _(alt, pl, q, region_picker):
_region = region_picker.value
# Get 15271 fee for the selected region, all years
app_fee = q(f"""
SELECT c.year, c.non_fac_fee as application_fee
FROM pfs.carrier_locality c
JOIN pfs.gpci g ON c.mac = g.mac AND c.locality = g.locality AND c.year = g.year
WHERE c.hcpcs = '15271'
AND g.locality_name = '{_region}'
""")
# Get annual ASP for top products (use Q1 of each year)
episode_data = q("""
SELECT
CAST(substr(quarter, 1, 4) AS INTEGER) as year,
hcpcs_code, short_description,
payment_limit,
payment_limit * 25 as product_cost_25cm2
FROM skin_subs.asp_quarterly
WHERE substr(quarter, 6, 2) = 'Q1'
AND hcpcs_code IN ('Q4101','Q4186','Q4132','Q4116','Q4100')
ORDER BY year, hcpcs_code
""")
# Join to get total episode cost
combined = episode_data.join(app_fee, on="year", how="left").with_columns(
(pl.col("product_cost_25cm2") + pl.col("application_fee").fill_null(0)).alias(
"total_episode_cost"
)
)
flat_line = (
alt.Chart(pl.DataFrame({"y": [127.28 * 25]}).to_pandas())
.mark_rule(color="red", strokeDash=[4, 4], strokeWidth=2)
.encode(y="y:Q")
)
episode_lines = (
alt.Chart(combined.to_pandas())
.mark_line(point=True)
.encode(
x=alt.X("year:O", title="Year"),
y=alt.Y("total_episode_cost:Q", title="Total Episode Cost ($)"),
color=alt.Color("short_description:N", title="Product"),
tooltip=[
"year",
"hcpcs_code",
"short_description",
"product_cost_25cm2",
"application_fee",
"total_episode_cost",
],
)
)
episode_chart = (episode_lines + flat_line).properties(
title=f"Total Episode Cost (25cm² wound) — {_region}",
width=700,
height=400,
)
episode_chart
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 7. Flat-Rate Impact: Winners and Losers
Products with ASP + 6% above $127.28 lose revenue under the 2026
flat rate; those below gain. This chart shows the latest quarter's
payment limit vs. the flat rate.
""")
return
@app.cell(hide_code=True)
def _(alt, q):
impact = q("""
SELECT hcpcs_code, short_description,
payment_limit,
127.28 as flat_rate,
payment_limit - 127.28 as delta,
CASE
WHEN payment_limit > 127.28 THEN 'loses'
WHEN payment_limit < 127.28 THEN 'gains'
ELSE 'neutral'
END as impact
FROM skin_subs.asp_quarterly
WHERE quarter = (SELECT max(quarter) FROM skin_subs.asp_quarterly)
ORDER BY delta DESC
""")
impact_chart = (
alt.Chart(impact.to_pandas())
.mark_bar()
.encode(
x=alt.X("delta:Q", title="Payment Limit $127.28 Flat Rate"),
y=alt.Y(
"short_description:N",
title="",
sort="-x",
axis=alt.Axis(labelLimit=300),
),
color=alt.Color(
"impact:N",
scale=alt.Scale(
domain=["loses", "gains", "neutral"],
range=["#d62728", "#2ca02c", "#7f7f7f"],
),
title="Impact",
),
tooltip=[
"hcpcs_code",
"short_description",
"payment_limit",
"flat_rate",
"delta",
],
)
.properties(title="CY2026 Flat-Rate Impact by Product", width=700)
)
impact_chart
return
@app.cell(hide_code=True)
def _(mo):
mo.md("""
## 8. Summary: Application Code Fee Range by Year
Min, median, and max non-facility fee across all MAC/locality
combinations for application code 15271.
""")
return
@app.cell(hide_code=True)
def _(ql):
fee_summary = ql("""
SELECT c.year,
count(*) as localities,
round(min(c.non_fac_fee), 2) as min_fee,
round(percentile_cont(0.5) WITHIN GROUP (ORDER BY c.non_fac_fee), 2) as median_fee,
round(max(c.non_fac_fee), 2) as max_fee,
round(max(c.non_fac_fee) - min(c.non_fac_fee), 2) as fee_spread
FROM pfs.carrier_locality c
WHERE c.hcpcs = '15271'
GROUP BY c.year
ORDER BY c.year
""")
fee_summary
return
if __name__ == "__main__":
app.run()