Files
stack/src/pfs/pipe.py
kert 16f3b43974
Some checks failed
CI / skinny-install (aco) (push) Successful in 1m12s
CI / skinny-install (api) (push) Successful in 30s
CI / skinny-install (bcda) (push) Successful in 36s
CI / skinny-install (bib) (push) Successful in 35s
CI / skinny-install (bls) (push) Successful in 27s
CI / skinny-install (ccw) (push) Successful in 32s
CI / skinny-install (cli) (push) Successful in 41s
CI / skinny-install (cms) (push) Successful in 37s
CI / skinny-install (conf) (push) Successful in 38s
CI / skinny-install (opps) (push) Successful in 33s
CI / skinny-install (perf) (push) Successful in 38s
CI / skinny-install (pfs) (push) Successful in 38s
CI / skinny-install (rex) (push) Successful in 34s
Deploy / build-scan-report (push) Failing after 46s
Infra CI / notebooks (push) Failing after 25s
Infra CI / zotero (push) Successful in 12s
Infra CI / docs (push) Failing after 16s
CI / lint-test (push) Failing after 11m2s
Infra CI / mc (push) Successful in 21s
Infra CI / api (push) Successful in 29s
Package Supply Chain / pkg-supply-chain (push) Failing after 41s
feat: full session — mail servers, comment pipeline, PRISMA fetch, email ingest
Mail: Maddy on DO (corwins.media+Resend, fhirworx.io+Postmark),
touchless/stateless/idempotent. Gitea SMTP via env_file. CMS inbox
at cmsupdates@mail.fhirworx.io with IMAP→bib poller.

Bib: regulations.gov v4 client, Federal Register discovery, 164K
comment backfill (running), IMAP email ingest, Zotero sync routing.

PRISMA: altcha PoW solver, CrossRef DOI resolution, 83/129 PDFs.
Zotero: schema parity, ops module, CLI, fail-fast guard.
CI: docs.Dockerfile COPY glob fix (tracks #341).
Infra: Gitea+marimo fhirworx themes, IOM/OIG modules.
2026-04-16 09:04:38 -04:00

1912 lines
66 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.
"""PFS data pipeline — Zotero library → pfs tables → DuckDB.
Discovers CMS Physician Fee Schedule data files stored in Zotero
by tag, reads Excel/CSV with polars, normalizes columns using
``pfs.files`` column maps, and loads into DuckDB (or any lake context).
File discovery uses three Zotero tags:
- ``file:rvu`` — Addendum B (RVU files), PPRRVU quarterly releases
- ``file:gpci`` — Addendum D/E (GAFs/GPCIs), GPCI quarterly files
- ``file:pe`` — PUF files (Labor, Supply, Equipment), Work Time, PEHR
Each file type has a reader that handles CMS header quirks
(multi-row titles, merged cells, varying column names across years).
Usage::
from pfs.pipe import load_all
import duckdb
con = duckdb.connect(path("db.aco")) # path from conf
stats = load_all(con)
for table, info in stats.items():
print(f"{table}: {info['rows']} rows from {info['files']} files")
"""
from __future__ import annotations
import logging
import re
import sqlite3
from pathlib import Path
from typing import Any
import duckdb
import polars as pl
from pfs.calcs.payment import LIMITING_CHARGE_FACTOR
log = logging.getLogger(__name__)
# ── Column maps from pfs.files (import at use time to avoid cycles) ──
_RVU_COLUMNS = {
"HCPCS": "hcpcs",
"CPT1/\nHCPCS": "hcpcs",
"CPT1/HCPCS": "hcpcs",
"MOD": "mod",
"Mod": "mod",
"DESCRIPTION": "description",
"STATUS CODE": "status_code",
"STATUS\nCODE": "status_code",
"STATUS": "status_code",
"NOT USED FOR MEDICARE PAYMENT": "not_used_for_medicare",
"NOT USED\nFOR\nMEDICARE \nPAYMENT": "not_used_for_medicare",
"WORK RVU": "work_rvu",
"WORK\nRVU": "work_rvu",
"\nWork\nRVUs2": "work_rvu",
"WORK\nRVUs2": "work_rvu",
"NON-FACILITY PE RVU": "non_fac_pe_rvu",
"NON-FAC PE RVU": "non_fac_pe_rvu",
"NON-FAC\nPE RVU": "non_fac_pe_rvu",
"\nNon-\nFacility\nPE\nRVUs2": "non_fac_pe_rvu",
"FACILITY PE RVU": "fac_pe_rvu",
"FAC PE RVU": "fac_pe_rvu",
"FACILITY\nPE RVU": "fac_pe_rvu",
"\nFacility\nPE\nRVUs2": "fac_pe_rvu",
"MALPRACTICE RVU": "mp_rvu",
"MP RVU": "mp_rvu",
"MP\nRVU": "mp_rvu",
"Mal-\nPractice\nRVUs2": "mp_rvu",
"NON-FACILITY TOTAL": "non_fac_total",
"NON-FACILITY\nTOTAL": "non_fac_total",
"\nTotal Non-Facility RVUs2": "non_fac_total",
"FACILITY TOTAL": "fac_total",
"FACILITY\nTOTAL": "fac_total",
"\nTotal Facility RVUs2": "fac_total",
"PCTC IND": "pctc_ind",
"PCTC\nIND": "pctc_ind",
"GLOB DAYS": "glob_days",
"GLOB\nDAYS": "glob_days",
"Global": "glob_days",
"GLOBAL": "glob_days",
"PRE OP": "pre_op",
"PRE\nOP": "pre_op",
"INTRA OP": "intra_op",
"INTRA\nOP": "intra_op",
"POST OP": "post_op",
"POST\nOP": "post_op",
"MULT PROC": "mult_proc",
"MULT\nPROC": "mult_proc",
"BILAT SURG": "bilat_surg",
"BILAT\nSURG": "bilat_surg",
"ASST SURG": "asst_surg",
"ASST\nSURG": "asst_surg",
"CO SURG": "co_surg",
"CO-\nSURG": "co_surg",
"TEAM SURG": "team_surg",
"TEAM\nSURG": "team_surg",
"ENDO BASE": "endo_base",
"ENDO\nBASE": "endo_base",
"CONV FACTOR": "conv_factor",
"CONV\nFACTOR": "conv_factor",
"PHYSICIAN SUPERVISION OF DIAGNOSTIC PROCEDURES": "physician_supervision",
"PHYSICIAN\nSUPERVISION OF\nDIAGNOSTIC\nPROCEDURES": "physician_supervision",
"CALCULATION FLAG": "calc_flag",
"CALCULATION\nFLAG": "calc_flag",
"DIAGNOSTIC IMAGING FAMILY INDICATOR": "diag_imaging_family",
"DIAGNOSTIC\nIMAGING\nFAMILY\nINDICATOR": "diag_imaging_family",
"NON-FACILITY PE USED FOR OPPS PAYMENT AMOUNT": "non_fac_pe_used_for_opps",
"FACILITY PE USED FOR OPPS PAYMENT AMOUNT": "fac_pe_used_for_opps",
"MP USED FOR OPPS PAYMENT AMOUNT": "mp_used_for_opps",
"PRIC IND": "pctc_ind",
"PRIC\nIND": "pctc_ind",
}
_GPCI_COLUMNS = {
"MAC": "mac",
"Medicare Administrative Contractor (MAC)": "mac",
"CARRIER": "mac",
"Carrier": "mac",
"LOCALITY": "locality",
"Locality Number": "locality",
"LOCALITY NAME": "locality_name",
"Locality Name": "locality_name",
"STATE": "state",
"State": "state",
}
_LABOR_COLUMNS = {
"hcpcs": "hcpcs",
"HCPCS": "hcpcs",
"labor_code": "activity_code",
"source": "_source",
"description": "activity_description",
"rate_per_minute": "rate_per_minute",
"nf_pre_svc": "_nf_pre_svc",
"nf_svc": "_nf_svc",
"nf_post_svc": "_nf_post_svc",
"f_pre_svc": "_f_pre_svc",
"f_svc": "_f_svc",
"f_post_svc": "_f_post_svc",
"global_period": "_global_period",
}
_SUPPLY_COLUMNS = {
"hcpcs": "hcpcs",
"HCPCS": "hcpcs",
"cms_code": "supply_code",
"description": "supply_description",
"unit": "_unit",
"price": "unit_price",
"nf_quantity": "nf_quantity",
"f_quantity": "f_quantity",
"source": "_source",
"category": "_category",
"global_period": "_global_period",
}
_EQUIPMENT_COLUMNS = {
"hcpcs": "hcpcs",
"HCPCS": "hcpcs",
"cms_code": "equipment_code",
"description": "equipment_description",
"useful_life": "useful_life",
"price": "unit_price",
"utilization_rate": "_utilization_rate",
"minutes_per_year": "minutes_per_year",
"nf_time": "nf_minutes",
"f_time": "f_minutes",
"source": "_source",
"category": "_category",
"global_period": "_global_period",
}
_WORK_TIME_COLUMNS = {
"cpt_code": "hcpcs",
"CPT_CODE": "hcpcs",
"Cpt_code": "hcpcs",
"HCPCS": "hcpcs",
"Pre_Evaluation_Time": "pre_eval_time",
"Pre_Positioning_time": "pre_positioning_time",
"Pre_Service_Scrub_Dress_Wait_tim": "pre_scrub_dress_wait",
"Median_Intra_Service_Time": "intra_service_time",
"Immediate_post_Service_time": "post_immediate_time",
"RUC_Recommended_Adjusted_Time": "total_time",
}
# ── Zotero discovery ─────────────────────────────────────────
def _zotero_pfs_files(
zotero_db: str | Path,
zotero_storage: str | Path,
) -> list[dict]:
"""Query Zotero for all module:pfs attachments (xlsx/xls/csv).
Returns list of dicts with keys: item_key, year, title, path,
filename, ext. Files are classified by filename pattern later.
"""
db = sqlite3.connect(str(zotero_db))
# All items tagged module:pfs
rows = db.execute(
"""
SELECT DISTINCT i.itemID, i.key
FROM items i
JOIN itemTags it ON i.itemID = it.itemID
JOIN tags t ON it.tagID = t.tagID
WHERE t.name = 'module:pfs'
""",
).fetchall()
results = []
for item_id, item_key in rows:
year_rows = db.execute(
"""
SELECT t.name FROM itemTags it
JOIN tags t ON it.tagID = t.tagID
WHERE it.itemID = ? AND t.name LIKE 'year:%'
""",
(item_id,),
).fetchall()
year = int(year_rows[0][0].split(":")[1]) if year_rows else 0
# Release quarter tag (q1..q4) — used by _load_rvu_files to
# prefer the latest quarter when multiple releases exist for
# the same calendar year.
release_rows = db.execute(
"""
SELECT t.name FROM itemTags it
JOIN tags t ON it.tagID = t.tagID
WHERE it.itemID = ? AND t.name LIKE 'release:%'
""",
(item_id,),
).fetchall()
release = release_rows[0][0].split(":", 1)[1] if release_rows else ""
title_row = db.execute(
"""
SELECT value FROM itemDataValues idv
JOIN itemData id ON idv.valueID = id.valueID
JOIN fields f ON id.fieldID = f.fieldID
WHERE id.itemID = ? AND f.fieldName = 'title'
""",
(item_id,),
).fetchone()
title = title_row[0] if title_row else ""
attachments = db.execute(
"""
SELECT ia.itemID, ia.path FROM itemAttachments ia
WHERE ia.parentItemID = ?
AND (ia.path LIKE '%.xlsx' OR ia.path LIKE '%.xls'
OR ia.path LIKE '%.csv' OR ia.path LIKE '%.txt')
""",
(item_id,),
).fetchall()
for att_id, att_path in attachments:
filename = att_path.replace("storage:", "")
# Skip macOS resource fork files
if filename.startswith("._"):
continue
storage_key = db.execute(
"SELECT key FROM items WHERE itemID = ?", (att_id,)
).fetchone()
if not storage_key:
continue
full_path = Path(zotero_storage) / storage_key[0] / filename
if not full_path.exists():
continue
ext = full_path.suffix.lower()
results.append(
{
"item_key": item_key,
"year": year,
"release": release, # "q1"/"q2"/"q3"/"q4" or ""
"title": title,
"path": str(full_path),
"filename": filename,
"ext": ext,
}
)
db.close()
return results
# ── Excel/CSV readers with CMS header detection ─────────────
def _find_header_row(path: str, *, min_cols: int = 5) -> int:
"""Find the row index containing column headers in an Excel file.
Scans for the first row where >= min_cols cells are non-null
strings that look like column names (contain alpha chars, not
just copyright/title boilerplate).
"""
import openpyxl
wb = openpyxl.load_workbook(path, read_only=True)
ws = wb.active
for i, row in enumerate(ws.iter_rows(values_only=True)):
non_null = [c for c in row if c is not None]
str_cells = [c for c in non_null if isinstance(c, str) and len(c.strip()) < 60]
if len(str_cells) >= min_cols:
wb.close()
return i
wb.close()
return 0
def _read_excel(path: str, header_row: int | None = None) -> pl.DataFrame:
"""Read an Excel file with auto-detected or explicit header row."""
if header_row is None:
header_row = _find_header_row(path)
df = pl.read_excel(path, read_options={"header_row": header_row})
return df
def _read_pprrvu(path: str) -> pl.DataFrame:
"""Read a PPRRVU quarterly file with its multi-row stacked header.
PPRRVU files have ~5 title rows, then a 4-row stacked header
(rows 5-9 typically). The last header row contains the actual
short column names (``HCPCS, MOD, DESCRIPTION, CODE, PAYMENT,
RVU, PE RVU, INDICATOR, PE RVU, INDICATOR, RVU, TOTAL, TOTAL,
IND, DAYS, OP, OP, OP, PROC, SURG, SURG, SURG, SURG, BASE,
FACTOR, PROCEDURES, FLAG, INDICATOR, AMOUNT, AMOUNT, AMOUNT``).
Several short names repeat across unrelated columns (``RVU``,
``PE RVU``, ``INDICATOR``, ``TOTAL``, ``SURG``, ``OP``, ``AMOUNT``)
so a name-based rename can't disambiguate them — we map
**positionally** to our schema.
Two historical gotchas this function now guards against:
1. ``pl.read_excel`` silently ignores ``read_options['header_row']``
unless the ``calamine`` engine is active — with the default
xlsx2csv engine it returns a 1-column DataFrame of the title
row. We pin ``engine='calamine'``.
2. The positional rename previously required exactly 32 columns
(``_pric_ind`` was a phantom entry between ``team_surg`` and
``endo_base`` that does not exist in any real PPRRVU file).
The list now has the correct 31 entries and the rename gate
uses ``==`` on the true count.
"""
import openpyxl
wb = openpyxl.load_workbook(path, read_only=True)
ws = wb.active
hcpcs_row = 0
for i, row in enumerate(ws.iter_rows(values_only=True)):
cells = [str(c).strip() if c is not None else "" for c in row]
if cells and cells[0] == "HCPCS":
hcpcs_row = i
break
wb.close()
# calamine respects read_options['header_row']; xlsx2csv does not.
df = pl.read_excel(
path,
engine="calamine",
read_options={"header_row": hcpcs_row},
)
# CMS PPRRVU schema changed in CY2026 — a "PRIC IND" (pricing
# indicator) column was inserted at position 23 between TEAM SURG
# and ENDO BASE, pushing the later columns (ENDO BASE, CONV FACTOR,
# PHYSICIAN SUPERVISION, ...) out by one. Detect the layout by
# column count and pick the matching positional map. Columns
# starting with "_" are stripped after renaming.
_EXPECTED_31 = [
"hcpcs", # 0 HCPCS
"mod", # 1 MOD
"description", # 2 DESCRIPTION
"status_code", # 3 STATUS CODE
"not_used_for_medicare", # 4 NOT USED FOR MEDICARE PAYMENT
"work_rvu", # 5 WORK RVU
"non_fac_pe_rvu", # 6 NON-FAC PE RVU
"_nf_na_indicator", # 7 NON-FAC NA INDICATOR
"fac_pe_rvu", # 8 FACILITY PE RVU
"_f_na_indicator", # 9 FACILITY NA INDICATOR
"mp_rvu", # 10 MP RVU
"non_fac_total", # 11 NON-FACILITY TOTAL
"fac_total", # 12 FACILITY TOTAL
"pctc_ind", # 13 PCTC IND
"glob_days", # 14 GLOB DAYS
"pre_op", # 15 PRE OP
"intra_op", # 16 INTRA OP
"post_op", # 17 POST OP
"mult_proc", # 18 MULT PROC
"bilat_surg", # 19 BILAT SURG
"asst_surg", # 20 ASST SURG
"co_surg", # 21 CO-SURG
"team_surg", # 22 TEAM SURG
"endo_base", # 23 ENDO BASE
"conv_factor", # 24 CONV FACTOR
"physician_supervision", # 25 PHYSICIAN SUPERVISION
"calc_flag", # 26 CALCULATION FLAG
"diag_imaging_family", # 27 DIAGNOSTIC IMAGING FAMILY
"non_fac_pe_used_for_opps", # 28 NON-FACILITY PE USED FOR OPPS
"fac_pe_used_for_opps", # 29 FACILITY PE USED FOR OPPS
"mp_used_for_opps", # 30 MP USED FOR OPPS
]
# CY2026+ layout: "PRIC IND" slot at position 23, shifts everything
# downstream by one. Mark internal ("_pric_ind") because nothing
# downstream of ingest uses it yet.
_EXPECTED_32 = _EXPECTED_31[:23] + ["_pric_ind"] + _EXPECTED_31[23:]
if len(df.columns) >= len(_EXPECTED_32):
expected_order = _EXPECTED_32
elif len(df.columns) >= len(_EXPECTED_31):
expected_order = _EXPECTED_31
else:
log.warning(
"PPRRVU %s has %d columns, expected 31 or 32 — positional rename skipped",
path,
len(df.columns),
)
return df
rename = {
old: new for old, new in zip(df.columns[: len(expected_order)], expected_order)
}
df = df.rename(rename)
# Keep only the mapped columns — trailing extras are noise.
df = df.select([c for c in expected_order if not c.startswith("_")])
return df
def _read_csv_with_header(path: str) -> pl.DataFrame:
"""Read a CSV file, skipping title rows before the actual header."""
# Read raw to find header
with open(path, encoding="utf-8-sig") as f:
import csv
reader = csv.reader(f)
for i, row in enumerate(reader):
non_empty = [c for c in row if c.strip()]
if len(non_empty) >= 3:
# Check if this looks like a header (has alpha chars)
alpha_cells = [c for c in non_empty if any(ch.isalpha() for ch in c)]
if len(alpha_cells) >= 2:
break
else:
i = 0
return pl.read_csv(path, skip_rows=i, encoding="utf8-lossy", infer_schema_length=0)
def _detect_encoding(path: str) -> str:
"""Detect if a file is UTF-16 (LE/BE) or UTF-8."""
with open(path, "rb") as f:
bom = f.read(4)
if bom[:2] in (b"\xff\xfe", b"\xfe\xff"):
return "utf-16"
return "utf-8-sig"
def _read_tsv(path: str) -> pl.DataFrame:
"""Read a tab-separated .txt file, skipping leading title rows."""
enc = _detect_encoding(path)
with open(path, encoding=enc, errors="replace") as f:
lines = f.readlines()
# Strip null bytes that can appear in UTF-16 decoded text
lines = [line.replace("\x00", "") for line in lines]
# Find header row — first row with multiple tab-separated alpha tokens
header_idx = 0
for i, line in enumerate(lines):
parts = line.strip().split("\t")
alpha = [p for p in parts if p.strip() and any(c.isalpha() for c in p)]
if len(alpha) >= 3:
header_idx = i
break
# Re-join lines from header onward and read with polars
text = "".join(lines[header_idx:])
return pl.read_csv(
text.encode("utf-8"),
separator="\t",
encoding="utf8-lossy",
infer_schema_length=0,
truncate_ragged_lines=True,
)
def _normalize_columns(
df: pl.DataFrame,
column_map: dict[str, str],
) -> pl.DataFrame:
"""Rename columns using a case-insensitive map, drop unmapped."""
# Build a normalized lookup: strip whitespace, collapse newlines
lookup: dict[str, str] = {}
for raw, target in column_map.items():
# Normalize the key
norm = raw.replace("\n", " ").strip()
norm = re.sub(r"\s+", " ", norm)
lookup[norm.upper()] = target
# Also add the raw key
lookup[raw.upper()] = target
rename = {}
keep = []
for col in df.columns:
norm = col.replace("\n", " ").strip()
norm = re.sub(r"\s+", " ", norm)
target = lookup.get(norm.upper()) or lookup.get(col.upper())
if target and not target.startswith("_"):
rename[col] = target
keep.append(col)
elif target and target.startswith("_"):
# Internal columns — keep for now, rename
rename[col] = target
keep.append(col)
if not keep:
return df
df = df.select(keep).rename(rename)
# Drop internal columns
internal = [c for c in df.columns if c.startswith("_")]
if internal:
df = df.drop(internal)
return df
# ── File type readers ────────────────────────────────────────
def _is_addendum_b(filename: str) -> bool:
fn = filename.upper()
return "ADDENDUM" in fn and (
"B " in fn or "B_" in fn or fn.startswith("ADDENDUM_B")
)
def _is_pprrvu(filename: str) -> bool:
return "PPRRVU" in filename.upper()
def _is_gpci_file(filename: str) -> bool:
fn = filename.upper()
if "GPCI" in fn and "ADDENDUM B" not in fn:
return True
if re.search(r"ADDENDUM[_ ]E\b", fn):
return True
return False
def _is_puf_labor(filename: str) -> bool:
fn = filename.upper()
return "PUF_LABOR" in fn and "TASK" not in fn and "OLD" not in fn
def _is_puf_supply(filename: str) -> bool:
return "PUF_SUPPLY" in filename.upper()
def _is_puf_equip(filename: str) -> bool:
fn = filename.upper()
return "PUF_EQUIP" in fn or "PUF_EQUIPMENT" in fn
def _is_work_time(filename: str) -> bool:
fn = filename.upper()
return "WORK_TIME" in fn or "WORK TIME" in fn or "PHYSTIME" in fn
def _is_carrier_file(filename: str) -> bool:
"""Detect carrier fee schedule files (PFALL*.txt or PF{ST}{YY}*.TXT)."""
fn = filename.upper()
if fn.startswith("PFALL") and fn.endswith(".TXT"):
return True
# Per-state: PF{2-3 letters}{2-digit}{suffix}.TXT
# e.g. PFAL26A.TXT, PFNY26R.TXT, PFPRV26.TXT, PFCA262.TXT
if re.match(r"^PF[A-Z]{2,3}\d{2}", fn) and fn.endswith(".TXT"):
return True
return False
def _is_zip_carrier_file(filename: str) -> bool:
"""Detect ZIP code carrier locality files (ZIP5_*.txt)."""
fn = filename.upper()
return fn.startswith("ZIP5") and fn.endswith(".TXT") and "LYOUT" not in fn
def _extract_year_from_title(title: str) -> int | None:
"""Extract CY year from Zotero title like 'CY 2026 PFS Final — ...'"""
m = re.search(r"CY\s*(\d{4})", title)
return int(m.group(1)) if m else None
def _is_final_rule(title: str) -> bool:
return "Final" in title or "final" in title
# ── Table loaders ────────────────────────────────────────────
def _load_rvu_files(
files: list[dict],
con: duckdb.DuckDBPyConnection,
) -> dict[str, Any]:
"""Load Addendum B and PPRRVU files into pfs.rvu.
PPRRVU files are preferred over Addendum B because they include
~19 additional columns that Addendum B omits — notably
``conv_factor`` (the CMS-published conversion factor for the
year), pre/intra/post-op indicators, surgical modifiers, and
physician supervision codes. See :pincite:`VVBEVYLC` for the
CF history the PPRRVU column captures. Addendum B is used as
a fallback only when no PPRRVU file exists for a given year.
"""
rvu_files = []
for f in files:
fn = f["filename"]
if _is_addendum_b(fn) or _is_pprrvu(fn):
# Prefer xlsx over csv/txt
if f["ext"] in (".xlsx", ".xls"):
rvu_files.append(f)
elif f["ext"] == ".csv" and not any(
g["year"] == f["year"]
and g["ext"] in (".xlsx", ".xls")
and (
(_is_addendum_b(g["filename"]) and _is_addendum_b(fn))
or (_is_pprrvu(g["filename"]) and _is_pprrvu(fn))
)
for g in files
):
rvu_files.append(f)
# Deduplicate: per year, pick the PPRRVU release whose embedded
# conv_factor matches pfs.rules.RULES[year].conversion_factor —
# the dominant CF per the AMA history table :pincite:`VVBEVYLC`.
# For years with a mid-year CAA update (2015, 2024), RULES stores
# the post-update CF as dominant, so this selects the post-update
# quarterly release. For years without a split, every quarter has
# the same CF and we pick the earliest matching release.
# Addendum B is used only as a last-resort fallback when no
# PPRRVU file exists for the year.
from pfs.rules import RULES
_RELEASE_RANK = {"q1": 1, "q2": 2, "q3": 3, "q4": 4}
def _release_sort_key(f: dict) -> tuple[int, int, str]:
# Sort PPRRVU files so the earliest quarter is scanned first.
# Addendum B files get release_rank = 99 so they're last;
# within that tier, Final rule beats Proposed rule.
is_pprrvu_file = _is_pprrvu(f["filename"])
if not is_pprrvu_file:
final_priority = 0 if _is_final_rule(f.get("title", "")) else 1
return (99, final_priority, f["filename"])
rank = _RELEASE_RANK.get(f.get("release", ""), 50)
return (rank, 0, f["filename"])
def _scan_cf(f: dict) -> float | None:
"""Read the embedded conv_factor from a PPRRVU file."""
try:
if _is_pprrvu(f["filename"]) and f["ext"] in (".xlsx", ".xls"):
df = _read_pprrvu(f["path"])
if "conv_factor" in df.columns and df.height > 0:
val = (
df.filter(pl.col("conv_factor").is_not_null())
.get_column("conv_factor")
.cast(pl.Float64, strict=False)
.head(1)
)
if len(val) > 0:
return float(val[0])
except Exception as exc:
log.debug("CF scan failed for %s: %s", f["filename"], exc)
return None
# Group files by year then pick the best release per year.
files_by_year: dict[int, list[dict]] = {}
for f in rvu_files:
files_by_year.setdefault(f["year"], []).append(f)
by_year: dict[int, dict] = {}
for year, candidates in files_by_year.items():
target_cf = RULES[year].conversion_factor if year in RULES else None
ordered = sorted(candidates, key=_release_sort_key)
pprrvu_candidates = [f for f in ordered if _is_pprrvu(f["filename"])]
chosen = None
# When multiple PPRRVU releases exist, pick the earliest whose
# embedded CF matches RULES to within 1¢. This catches the
# mid-year split years (2015, 2024) cleanly. Skip the scan
# entirely when there is only one PPRRVU candidate — it will
# be selected by the fallback below, avoiding a redundant read.
if len(pprrvu_candidates) > 1 and target_cf is not None:
for f in pprrvu_candidates:
cf = _scan_cf(f)
if cf is None:
continue
if abs(cf - target_cf) < 0.005:
chosen = f
log.info(
"RVU %d: matched release=%s (%s) CF=%s to RULES dominant CF=%s",
year,
f.get("release", "?"),
f["filename"],
cf,
target_cf,
)
break
if chosen is None:
log.warning(
"RVU %d: no PPRRVU matched RULES CF %s; "
"falling back to earliest PPRRVU",
year,
target_cf,
)
# Fallback: earliest PPRRVU by rank, then Addendum B (Final
# preferred over Proposed via _release_sort_key).
if chosen is None and pprrvu_candidates:
chosen = pprrvu_candidates[0]
if chosen is None:
chosen = ordered[0]
log.warning(
"RVU %d: no PPRRVU available, using %s (Addendum B)",
year,
chosen["filename"],
)
by_year[year] = chosen
total_rows = 0
file_count = 0
for year in sorted(by_year):
f = by_year[year]
path = f["path"]
log.info("Loading RVU year=%d from %s", year, f["filename"])
try:
is_pprrvu = _is_pprrvu(f["filename"])
if is_pprrvu and f["ext"] in (".xlsx", ".xls"):
df = _read_pprrvu(path)
elif f["ext"] == ".csv":
df = _read_csv_with_header(path)
else:
df = _read_excel(path)
if "hcpcs" not in df.columns:
df = _normalize_columns(df, _RVU_COLUMNS)
if "hcpcs" not in df.columns:
log.warning("Skipping %s — no hcpcs column", f["filename"])
continue
# Cast numeric columns
for col in [
"work_rvu",
"non_fac_pe_rvu",
"fac_pe_rvu",
"mp_rvu",
"non_fac_total",
"fac_total",
"pre_op",
"intra_op",
"post_op",
"conv_factor",
]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
# Cast string columns
for col in df.columns:
if col not in [
"work_rvu",
"non_fac_pe_rvu",
"fac_pe_rvu",
"mp_rvu",
"non_fac_total",
"fac_total",
"pre_op",
"intra_op",
"post_op",
"conv_factor",
"year",
]:
df = df.with_columns(pl.col(col).cast(pl.Utf8, strict=False))
df = df.with_columns(pl.lit(year).alias("year"))
# Filter out empty/null hcpcs rows
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
_insert_into(con, "pfs", "rvu", df)
total_rows += len(df)
file_count += 1
except Exception as exc:
log.error("Failed to load RVU %s: %s", f["filename"], exc)
return {"rows": total_rows, "files": file_count}
def _is_stacked_header_gpci(path: str, ext: str) -> bool:
"""Detect if a GPCI file has a 2-row stacked header (2017 format).
2017 files have years in one row and GPCI sub-headers in the next:
Row 1: Carrier | Locality Number | Locality Name | 2016 | | | 2017 | ...
Row 2: | | | PW GPCI | PE GPCI | MP GPCI | PW GPCI | PE GPCI | ...
Later files (2020+) have year-prefixed columns in ONE row:
Row 2: MAC | State | Locality | Name | 2019 PW GPCI | 2019 PE GPCI | ...
This function only returns True for the stacked (2-row) format.
"""
if ext == ".txt":
enc = _detect_encoding(path)
with open(path, encoding=enc, errors="replace") as f:
lines = [l.replace("\x00", "") for l in f.readlines()]
for i, line in enumerate(lines):
parts = [p.strip() for p in line.strip().split("\t")]
# A year row has bare year numbers as values
bare_years = [p for p in parts if re.match(r"^20\d{2}$", p)]
if len(bare_years) >= 1:
# Check next row has GPCI sub-headers
if i + 1 < len(lines):
next_parts = lines[i + 1].strip().split("\t")
if any("GPCI" in p for p in next_parts):
return True
return False
elif ext == ".xlsx":
import openpyxl
wb = openpyxl.load_workbook(path, read_only=True)
ws = wb.active
rows = []
for i, row in enumerate(ws.iter_rows(values_only=True)):
if i > 5:
break
rows.append([str(c).strip() if c else "" for c in row])
wb.close()
for i, cells in enumerate(rows):
bare_years = [c for c in cells if re.match(r"^20\d{2}$", c)]
if len(bare_years) >= 1 and i + 1 < len(rows):
if any("GPCI" in c for c in rows[i + 1]):
return True
return False
def _parse_stacked_gpci(
year_parts: list[str],
sub_parts: list[str],
data_lines: list[list[str]],
target_year: int,
) -> pl.DataFrame | None:
"""Core parser for stacked-header GPCI data.
Handles both multi-year (2017: 3 years in one file) and single-year
(2018/2019: sub-header omits identifier columns) formats.
"""
# Find where years start in year_parts
first_year_idx = None
for idx, val in enumerate(year_parts):
if re.match(r"^20\d{2}$", val):
first_year_idx = idx
break
if first_year_idx is None:
return None
n_id_cols = first_year_idx
# Map data column index → year.
# Extend beyond year_parts length to cover data columns —
# single-year files have fewer header cells than data cells.
max_data_cols = max((len(row) for row in data_lines), default=len(year_parts))
col_year: dict[int, int] = {}
current_year = 0
for idx in range(max(len(year_parts), max_data_cols)):
if idx < len(year_parts) and re.match(r"^20\d{2}$", year_parts[idx]):
current_year = int(year_parts[idx])
if current_year and idx >= n_id_cols:
col_year[idx] = current_year
# Align sub_parts with data columns.
# Multi-year: sub_parts has leading blanks matching id cols
# Single-year: sub_parts omits id cols entirely
sub_offset = 0
if len(sub_parts) < len(year_parts):
sub_offset = n_id_cols
def _sub_label(data_idx: int) -> str:
si = data_idx - sub_offset
if 0 <= si < len(sub_parts):
return sub_parts[si].upper()
return ""
rows = []
for parts in data_lines:
if len(parts) < n_id_cols + 3:
continue
carrier = parts[0].strip().strip('"')
locality = parts[1].strip().strip('"') if n_id_cols > 1 else ""
loc_name = parts[2].strip().strip('"') if n_id_cols > 2 else ""
if not locality:
continue
work = pe = mp = None
for idx, yr in sorted(col_year.items()):
if yr == target_year and idx < len(parts):
label = _sub_label(idx)
val = parts[idx].strip().strip('"')
if "PW GPCI" in label or "WORK" in label:
work = val
elif "PE GPCI" in label:
pe = val
elif "MP GPCI" in label:
mp = val
if work is not None:
rows.append(
{
"mac": carrier,
"locality": locality,
"locality_name": loc_name,
"work_gpci": work,
"pe_gpci": pe,
"mp_gpci": mp,
}
)
return pl.DataFrame(rows) if rows else None
def _find_stacked_header(
lines_or_rows: list,
is_text: bool = True,
) -> tuple[int | None, int | None, list[str], list[str]]:
"""Find year-header and sub-header rows in stacked GPCI data."""
year_row_idx = None
sub_header_idx = None
for i, item in enumerate(lines_or_rows):
if is_text:
parts = [p.strip() for p in item.strip().split("\t")]
else:
parts = [str(c).strip() if c else "" for c in item]
years_found = [p for p in parts if re.match(r"^20\d{2}$", p)]
if len(years_found) >= 1 and year_row_idx is None:
year_row_idx = i
continue
if year_row_idx is not None and any("GPCI" in p for p in parts):
sub_header_idx = i
break
if year_row_idx is None or sub_header_idx is None:
return None, None, [], []
if is_text:
yp = [p.strip() for p in lines_or_rows[year_row_idx].strip().split("\t")]
sp = [p.strip() for p in lines_or_rows[sub_header_idx].strip().split("\t")]
else:
yp = [str(c).strip() if c else "" for c in lines_or_rows[year_row_idx]]
sp = [str(c).strip() if c else "" for c in lines_or_rows[sub_header_idx]]
return year_row_idx, sub_header_idx, yp, sp
def _read_multi_year_gpci(
path: str,
ext: str,
target_year: int,
) -> pl.DataFrame | None:
"""Read a stacked-header GPCI file and extract columns for target_year."""
if ext == ".txt":
enc = _detect_encoding(path)
with open(path, encoding=enc, errors="replace") as f:
lines = [l.replace("\x00", "") for l in f.readlines()]
_, sub_idx, year_parts, sub_parts = _find_stacked_header(lines, is_text=True)
if sub_idx is None:
return None
data_lines = [
line.strip().split("\t")
for line in lines[sub_idx + 1 :]
if len(line.strip().split("\t")) >= 4
]
return _parse_stacked_gpci(year_parts, sub_parts, data_lines, target_year)
elif ext == ".xlsx":
import openpyxl
wb = openpyxl.load_workbook(path, read_only=True)
ws = wb.active
all_rows = list(ws.iter_rows(values_only=True))
wb.close()
_, sub_idx, year_parts, sub_parts = _find_stacked_header(
all_rows, is_text=False
)
if sub_idx is None:
return None
data_lines = [
[str(c).strip() if c else "" for c in row]
for row in all_rows[sub_idx + 1 :]
if len(row) >= 4
]
return _parse_stacked_gpci(year_parts, sub_parts, data_lines, target_year)
return None
def _load_gpci_files(
files: list[dict],
con: duckdb.DuckDBPyConnection,
) -> dict[str, Any]:
"""Load GPCI/Addendum E files into pfs.gpci."""
gpci_files = [
f
for f in files
if _is_gpci_file(f["filename"])
and f["ext"] in (".xlsx", ".xls", ".csv", ".txt")
]
# Deduplicate by year — prefer Final xlsx > Final txt > Proposed
by_year: dict[int, dict] = {}
for f in gpci_files:
year = f["year"]
is_final = _is_final_rule(f["title"])
fn = f["filename"].upper()
is_xlsx = f["ext"] == ".xlsx"
is_gpci_quarterly = fn.startswith("GPCI")
priority = (is_gpci_quarterly, is_final, is_xlsx)
if year not in by_year or priority > by_year[year]["_priority"]:
f["_priority"] = priority
by_year[year] = f
# Check for stacked-header multi-year files that can fill gaps
multi_year_files = []
for f in gpci_files:
if _is_stacked_header_gpci(f["path"], f["ext"]):
multi_year_files.append(f)
total_rows = 0
file_count = 0
loaded_years: set[int] = set()
for year in sorted(by_year):
f = by_year[year]
path = f["path"]
log.info("Loading GPCI year=%d from %s", year, f["filename"])
try:
# Check if this is a stacked-header multi-year file (2017)
if _is_stacked_header_gpci(path, f["ext"]):
df = _read_multi_year_gpci(path, f["ext"], year)
if df is None or len(df) == 0:
log.warning(
"Stacked GPCI %s — no data for year %d",
f["filename"],
year,
)
continue
elif f["ext"] == ".txt":
df = _read_tsv(path)
elif f["ext"] == ".csv":
df = _read_csv_with_header(path)
else:
df = _read_excel(path)
# Normalize column names for non-stacked files
if "work_gpci" not in df.columns:
rename_map: dict[str, str] = {}
yr_str = str(year)
for col in df.columns:
# Collapse all whitespace (handles "Work GPCI ")
norm = col.replace("\n", " ").strip()
norm = re.sub(r"\s+", " ", norm).upper()
if any(k in norm for k in ["MAC", "CONTRACTOR"]):
rename_map[col] = "mac"
elif norm in ("CARRIER",):
rename_map[col] = "mac"
elif norm in ("STATE", "ST"):
rename_map[col] = "state"
elif "LOCALITY" in norm and "NAME" in norm:
rename_map[col] = "locality_name"
elif "LOCALITY" in norm:
rename_map[col] = "locality"
elif "WITHOUT" in norm:
continue # skip without-floor variants
# Year-prefixed: "2020 PW GPCI ..." — only match target year
elif "PW GPCI" in norm or "WORK GPCI" in norm:
yr_match = re.match(r"(\d{4})\s", norm)
if yr_match and yr_match.group(1) != yr_str:
continue
if "work_gpci" not in rename_map.values():
rename_map[col] = "work_gpci"
elif "PE GPCI" in norm:
yr_match = re.match(r"(\d{4})\s", norm)
if yr_match and yr_match.group(1) != yr_str:
continue
if "pe_gpci" not in rename_map.values():
rename_map[col] = "pe_gpci"
elif "MP GPCI" in norm:
yr_match = re.match(r"(\d{4})\s", norm)
if yr_match and yr_match.group(1) != yr_str:
continue
if "mp_gpci" not in rename_map.values():
rename_map[col] = "mp_gpci"
if "locality" not in rename_map.values():
log.warning(
"Skipping GPCI %s — no locality column",
f["filename"],
)
continue
keep = list(rename_map.keys())
df = df.select(keep).rename(rename_map)
for col in ["work_gpci", "pe_gpci", "mp_gpci"]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
for col in ["mac", "state", "locality", "locality_name"]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Utf8, strict=False))
df = df.with_columns(pl.lit(year).alias("year"))
df = df.filter(
pl.col("locality").is_not_null() & (pl.col("locality") != "")
)
_insert_into(con, "pfs", "gpci", df)
total_rows += len(df)
file_count += 1
loaded_years.add(year)
except Exception as exc:
log.error("Failed to load GPCI %s: %s", f["filename"], exc)
# Fill gaps from multi-year files
for mf in multi_year_files:
if mf["ext"] not in (".xlsx", ".txt"):
continue
# Detect what years this file contains
if mf["ext"] == ".txt":
enc = _detect_encoding(mf["path"])
with open(mf["path"], encoding=enc, errors="replace") as fh:
for line in (l.replace("\x00", "") for l in fh):
parts = [p.strip() for p in line.strip().split("\t")]
avail_years = [int(p) for p in parts if re.match(r"^20\d{2}$", p)]
if len(avail_years) >= 1:
break
else:
avail_years = []
else:
import openpyxl
wb = openpyxl.load_workbook(mf["path"], read_only=True)
ws = wb.active
avail_years = []
for i, row in enumerate(ws.iter_rows(values_only=True)):
if i > 5:
break
cells = [str(c).strip() if c else "" for c in row]
found = [int(c) for c in cells if re.match(r"^20\d{2}$", c)]
if len(found) >= 1:
avail_years = found
break
wb.close()
for gap_year in avail_years:
if gap_year in loaded_years:
continue
log.info(
"Filling GPCI gap year=%d from multi-year %s",
gap_year,
mf["filename"],
)
try:
df = _read_multi_year_gpci(mf["path"], mf["ext"], gap_year)
if df is None or len(df) == 0:
continue
for col in ["work_gpci", "pe_gpci", "mp_gpci"]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
for col in ["mac", "state", "locality", "locality_name"]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Utf8, strict=False))
df = df.with_columns(pl.lit(gap_year).alias("year"))
df = df.filter(
pl.col("locality").is_not_null() & (pl.col("locality") != "")
)
_insert_into(con, "pfs", "gpci", df)
total_rows += len(df)
file_count += 1
loaded_years.add(gap_year)
except Exception as exc:
log.error("Failed to fill GPCI gap year=%d: %s", gap_year, exc)
return {"rows": total_rows, "files": file_count}
def _read_tabular(path: str, ext: str) -> pl.DataFrame:
"""Read a tabular file by extension: xlsx/xls → Excel, csv → CSV, txt → TSV."""
if ext == ".txt":
return _read_tsv(path)
elif ext == ".csv":
return _read_csv_with_header(path)
else:
return _read_excel(path)
def _load_pe_files(
files: list[dict],
con: duckdb.DuckDBPyConnection,
) -> dict[str, dict[str, Any]]:
"""Load PUF Labor/Supply/Equipment and Work Time files."""
stats: dict[str, dict[str, Any]] = {
"clinical_labor": {"rows": 0, "files": 0},
"medical_supply": {"rows": 0, "files": 0},
"medical_equipment": {"rows": 0, "files": 0},
"physician_work_time": {"rows": 0, "files": 0},
}
# Deduplicate per (type, year) — prefer Final over Proposed
typed_files: dict[str, dict[int, dict]] = {
"labor": {},
"supply": {},
"equipment": {},
"work_time": {},
}
for f in files:
fn = f["filename"]
year = f["year"]
is_final = _is_final_rule(f["title"])
ext_ok = f["ext"] in (".xlsx", ".xls", ".csv", ".txt")
if not ext_ok:
continue
for ftype, checker in [
("labor", _is_puf_labor),
("supply", _is_puf_supply),
("equipment", _is_puf_equip),
("work_time", _is_work_time),
]:
if checker(fn):
# Priority: Final xlsx > Final txt > Proposed xlsx > ...
is_xlsx = f["ext"] == ".xlsx"
priority = (is_final, is_xlsx)
existing = typed_files[ftype].get(year)
if existing is None or priority > existing.get(
"_priority", (False, False)
):
f["_final"] = is_final
f["_priority"] = priority
typed_files[ftype][year] = f
break
# Load labor
for year in sorted(typed_files["labor"]):
f = typed_files["labor"][year]
log.info("Loading Labor year=%d from %s", year, f["filename"])
try:
df = _read_tabular(f["path"], f["ext"])
df = _normalize_columns(df, _LABOR_COLUMNS)
if "hcpcs" not in df.columns:
log.warning("Skipping Labor %s — no hcpcs", f["filename"])
continue
# Compute nf_minutes and f_minutes from service period columns
nf_min_cols = [
c for c in df.columns if c.startswith("nf_") and c != "nf_minutes"
]
f_min_cols = [
c for c in df.columns if c.startswith("f_") and c != "f_minutes"
]
# Cast numeric
for col in ["rate_per_minute"] + nf_min_cols + f_min_cols:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
if nf_min_cols and "nf_minutes" not in df.columns:
df = df.with_columns(
pl.sum_horizontal(*[pl.col(c) for c in nf_min_cols]).alias(
"nf_minutes"
)
)
if f_min_cols and "f_minutes" not in df.columns:
df = df.with_columns(
pl.sum_horizontal(*[pl.col(c) for c in f_min_cols]).alias(
"f_minutes"
)
)
df = df.with_columns(pl.lit(year).alias("year"))
# Keep only columns matching the table model
keep = [
c
for c in [
"hcpcs",
"activity_code",
"activity_description",
"nf_minutes",
"f_minutes",
"rate_per_minute",
"staff_type",
"year",
]
if c in df.columns
]
df = df.select(keep)
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
_insert_into(con, "pfs", "clinical_labor", df)
stats["clinical_labor"]["rows"] += len(df)
stats["clinical_labor"]["files"] += 1
except Exception as exc:
log.error("Failed to load Labor %s: %s", f["filename"], exc)
# Load supply
for year in sorted(typed_files["supply"]):
f = typed_files["supply"][year]
log.info("Loading Supply year=%d from %s", year, f["filename"])
try:
df = _read_tabular(f["path"], f["ext"])
df = _normalize_columns(df, _SUPPLY_COLUMNS)
if "hcpcs" not in df.columns:
log.warning("Skipping Supply %s — no hcpcs", f["filename"])
continue
for col in ["unit_price", "nf_quantity", "f_quantity"]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
df = df.with_columns(pl.lit(year).alias("year"))
keep = [
c
for c in [
"hcpcs",
"supply_code",
"supply_description",
"nf_quantity",
"f_quantity",
"unit_price",
"year",
]
if c in df.columns
]
df = df.select(keep)
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
_insert_into(con, "pfs", "medical_supply", df)
stats["medical_supply"]["rows"] += len(df)
stats["medical_supply"]["files"] += 1
except Exception as exc:
log.error("Failed to load Supply %s: %s", f["filename"], exc)
# Load equipment
for year in sorted(typed_files["equipment"]):
f = typed_files["equipment"][year]
log.info("Loading Equipment year=%d from %s", year, f["filename"])
try:
df = _read_tabular(f["path"], f["ext"])
df = _normalize_columns(df, _EQUIPMENT_COLUMNS)
if "hcpcs" not in df.columns:
log.warning("Skipping Equipment %s — no hcpcs", f["filename"])
continue
for col in [
"unit_price",
"useful_life",
"minutes_per_year",
"nf_minutes",
"f_minutes",
"maintenance_factor",
]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
df = df.with_columns(pl.lit(year).alias("year"))
keep = [
c
for c in [
"hcpcs",
"equipment_code",
"equipment_description",
"nf_minutes",
"f_minutes",
"unit_price",
"useful_life",
"minutes_per_year",
"maintenance_factor",
"year",
]
if c in df.columns
]
df = df.select(keep)
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
_insert_into(con, "pfs", "medical_equipment", df)
stats["medical_equipment"]["rows"] += len(df)
stats["medical_equipment"]["files"] += 1
except Exception as exc:
log.error("Failed to load Equipment %s: %s", f["filename"], exc)
# Load work time
for year in sorted(typed_files["work_time"]):
f = typed_files["work_time"][year]
log.info("Loading WorkTime year=%d from %s", year, f["filename"])
try:
df = _read_tabular(f["path"], f["ext"])
df = _normalize_columns(df, _WORK_TIME_COLUMNS)
if "hcpcs" not in df.columns:
log.warning("Skipping WorkTime %s — no hcpcs", f["filename"])
continue
# hcpcs might be int (like 100 instead of "00100") — pad
if df["hcpcs"].dtype in (pl.Int64, pl.Float64, pl.Int32):
df = df.with_columns(
pl.col("hcpcs")
.cast(pl.Int64, strict=False)
.cast(pl.Utf8)
.str.zfill(5)
.alias("hcpcs")
)
for col in [
"pre_eval_time",
"pre_positioning_time",
"pre_scrub_dress_wait",
"intra_service_time",
"post_immediate_time",
"total_time",
]:
if col in df.columns:
df = df.with_columns(pl.col(col).cast(pl.Float64, strict=False))
# Compute total_time if not present
time_cols = [
c
for c in [
"pre_eval_time",
"pre_positioning_time",
"pre_scrub_dress_wait",
"intra_service_time",
"post_immediate_time",
]
if c in df.columns
]
if time_cols and "total_time" not in df.columns:
df = df.with_columns(
pl.sum_horizontal(*[pl.col(c) for c in time_cols]).alias(
"total_time"
)
)
df = df.with_columns(pl.lit(year).alias("year"))
keep = [
c
for c in [
"hcpcs",
"pre_eval_time",
"pre_positioning_time",
"pre_scrub_dress_wait",
"intra_service_time",
"post_immediate_time",
"total_time",
"year",
]
if c in df.columns
]
df = df.select(keep)
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
_insert_into(con, "pfs", "physician_work_time", df)
stats["physician_work_time"]["rows"] += len(df)
stats["physician_work_time"]["files"] += 1
except Exception as exc:
log.error("Failed to load WorkTime %s: %s", f["filename"], exc)
return stats
# ── ZIP code carrier locality loader ─────────────────────────
# Fixed-width column spec for ZIP5 files (from ZIP5lyout.txt)
_ZIP5_COLUMNS = [
("state", 0, 2),
("zip_code", 2, 7),
("carrier", 7, 12),
("locality", 12, 14),
("rural_indicator", 14, 15),
# 15-17: lab CB locality (skip)
# 17: rural indicator 2 (skip)
# 18-19: filler
("plus_four_flag", 20, 21),
# 21: filler
("part_b_indicator", 22, 23),
# 23-74: filler
("year_quarter", 75, 80),
]
def _load_zip_carrier_files(
files: list[dict],
con: duckdb.DuckDBPyConnection,
) -> dict[str, Any]:
"""Load ZIP5 carrier locality files into pfs.zip_carrier_locality."""
zip_files = [
f for f in files if _is_zip_carrier_file(f["filename"]) and f["ext"] == ".txt"
]
# Deduplicate by year — prefer latest version
by_year: dict[int, dict] = {}
for f in zip_files:
year = f["year"]
existing = by_year.get(year)
if existing is None or f["filename"] > existing["filename"]:
by_year[year] = f
total_rows = 0
file_count = 0
for year in sorted(by_year):
f = by_year[year]
path = f["path"]
log.info("Loading ZipCarrier year=%d from %s", year, f["filename"])
try:
with open(path, encoding="utf-8-sig") as fh:
lines = fh.readlines()
rows = []
for line in lines:
if len(line.rstrip()) < 14:
continue
row = {}
for col_name, start, end in _ZIP5_COLUMNS:
row[col_name] = line[start:end].strip()
rows.append(row)
if not rows:
log.warning("Skipping %s — no data rows", f["filename"])
continue
df = pl.DataFrame(rows)
# Parse year/quarter from the YYYYQ field
if "year_quarter" in df.columns:
df = df.with_columns(
pl.col("year_quarter")
.str.slice(0, 4)
.cast(pl.Int64, strict=False)
.alias("year"),
pl.col("year_quarter")
.str.slice(4, 1)
.cast(pl.Int64, strict=False)
.alias("quarter"),
)
df = df.drop("year_quarter")
else:
df = df.with_columns(
pl.lit(year).alias("year"),
pl.lit(None, dtype=pl.Int64).alias("quarter"),
)
# Filter empty zip codes
df = df.filter(
pl.col("zip_code").is_not_null() & (pl.col("zip_code") != "")
)
_insert_into(con, "pfs", "zip_carrier_locality", df)
total_rows += len(df)
file_count += 1
log.info(" ZipCarrier year=%d: %d rows", year, len(df))
except Exception as exc:
log.error("Failed to load ZipCarrier %s: %s", f["filename"], exc)
return {"rows": total_rows, "files": file_count}
# ── Carrier file loader ──────────────────────────────────────
# Positional columns for the 16-column format (2011+)
_CARRIER_POS_16 = [
"year",
"mac",
"locality",
"hcpcs",
"mod",
"non_fac_fee",
"fac_fee",
"_filler",
"_pctc",
"_status",
"_mult_surg",
"_therapy_nf",
"_therapy_f",
"_opps_ind",
"_opps_nf",
"_opps_f",
]
# Positional columns for the 8-column format (2002-2009)
_CARRIER_POS_8 = [
"year",
"mac",
"locality",
"hcpcs",
"mod",
"non_fac_fee",
"fac_fee",
"_opps_ind",
]
# Limiting-charge factor (0.95 × 1.15 = 1.0925) is defined in
# pfs.calcs.payment.LIMITING_CHARGE_FACTOR — single source of truth.
def _load_carrier_files(
files: list[dict],
con: duckdb.DuckDBPyConnection,
) -> dict[str, Any]:
"""Load carrier fee schedule files into pfs.carrier_locality."""
carrier_files = [
f for f in files if _is_carrier_file(f["filename"]) and f["ext"] == ".txt"
]
# Group by year
by_year: dict[int, list[dict]] = {}
for f in carrier_files:
by_year.setdefault(f["year"], []).append(f)
# For 2026, prefer NonQP over QP (standard payment amounts)
for year, year_files in by_year.items():
titles = {f["title"] for f in year_files}
if any("Nonqp" in t or "NONQP" in t.upper() for t in titles):
# Keep only files from the NonQP item
by_year[year] = [
f
for f in year_files
if "Nonqp" in f["title"] or "NONQP" in f["title"].upper()
]
total_rows = 0
file_count = 0
for year in sorted(by_year):
year_files = by_year[year]
log.info("Loading Carrier year=%d from %d file(s)", year, len(year_files))
year_frames: list[pl.DataFrame] = []
for f in year_files:
try:
df = pl.read_csv(
f["path"],
has_header=False,
infer_schema_length=0,
truncate_ragged_lines=True,
)
except Exception as exc:
log.warning("Skipping carrier file %s: %s", f["filename"], exc)
continue
n_cols = len(df.columns)
if n_cols >= 16:
pos = _CARRIER_POS_16
elif n_cols >= 8:
pos = _CARRIER_POS_8
else:
log.warning("Skipping %s — only %d columns", f["filename"], n_cols)
continue
rename = {old: new for old, new in zip(df.columns[: len(pos)], pos)}
df = df.select(list(rename.keys())).rename(rename)
# Drop internal columns
internal = [c for c in df.columns if c.startswith("_")]
if internal:
df = df.drop(internal)
# Strip quotes and whitespace from string fields
for col in ["mac", "locality", "hcpcs", "mod"]:
if col in df.columns:
df = df.with_columns(pl.col(col).str.strip_chars('" ').alias(col))
# Cast fee columns to float
for col in ["non_fac_fee", "fac_fee"]:
if col in df.columns:
df = df.with_columns(
pl.col(col)
.str.strip_chars('" ')
.cast(pl.Float64, strict=False)
.alias(col)
)
# Compute limiting charges (PAR fee × 0.95 × 1.15 = 1.0925).
# Constant lives in pfs.calcs.payment for a single source of truth.
if "non_fac_fee" in df.columns:
df = df.with_columns(
(pl.col("non_fac_fee") * LIMITING_CHARGE_FACTOR)
.round(2)
.alias("non_fac_limiting_charge")
)
if "fac_fee" in df.columns:
df = df.with_columns(
(pl.col("fac_fee") * LIMITING_CHARGE_FACTOR)
.round(2)
.alias("fac_limiting_charge")
)
# Override year from metadata (more reliable than file content)
df = df.with_columns(pl.lit(year).alias("year"))
# Filter empty HCPCS rows
df = df.filter(pl.col("hcpcs").is_not_null() & (pl.col("hcpcs") != ""))
year_frames.append(df)
if not year_frames:
continue
combined = pl.concat(year_frames)
_insert_into(con, "pfs", "carrier_locality", combined)
total_rows += len(combined)
file_count += 1
log.info(
" Carrier year=%d: %d rows from %d file(s)",
year,
len(combined),
len(year_frames),
)
return {"rows": total_rows, "files": file_count}
# ── DuckDB insertion ─────────────────────────────────────────
def _insert_into(
con: duckdb.DuckDBPyConnection,
schema: str,
table: str,
df: pl.DataFrame,
) -> None:
"""INSERT INTO schema.table from a polars DataFrame."""
qualified = f'"{schema}"."{table}"'
# Ensure schema exists
con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
# Create table if not exists from the DataFrame
try:
con.execute(f"SELECT 1 FROM {qualified} LIMIT 0")
except duckdb.CatalogException:
con.execute(f"CREATE TABLE {qualified} AS SELECT * FROM df WHERE 1=0")
# Insert — match columns by name
cols = df.columns
# Only insert columns that exist in target
target_cols = [
r[0]
for r in con.execute(
f"SELECT column_name FROM information_schema.columns "
f"WHERE table_schema = '{schema}' AND table_name = '{table}'"
).fetchall()
]
shared = [c for c in cols if c in target_cols]
if not shared:
# Table doesn't have overlapping cols — create fresh
con.execute(f"DROP TABLE IF EXISTS {qualified}")
con.execute(f"CREATE TABLE {qualified} AS SELECT * FROM df")
return
missing_in_target = [c for c in cols if c not in target_cols]
if missing_in_target:
for mc in missing_in_target:
dtype = df[mc].dtype
sql_type = "VARCHAR"
if dtype in (pl.Float64, pl.Float32):
sql_type = "DOUBLE"
elif dtype in (pl.Int64, pl.Int32, pl.Int16, pl.Int8):
sql_type = "BIGINT"
try:
con.execute(f'ALTER TABLE {qualified} ADD COLUMN "{mc}" {sql_type}')
except Exception: # pragma: no cover
pass # Column may already exist
col_list = ", ".join(f'"{c}"' for c in cols)
con.execute(f"INSERT INTO {qualified} ({col_list}) SELECT {col_list} FROM df")
# ── Main entry point ─────────────────────────────────────────
def load_all(
con: duckdb.DuckDBPyConnection,
*,
zotero_db: str | Path | None = None,
zotero_storage: str | Path | None = None,
years: tuple[int, int] | None = None,
) -> dict[str, dict[str, Any]]:
"""Load all PFS data from Zotero into DuckDB tables.
Parameters
----------
con : duckdb.DuckDBPyConnection
DuckDB connection to load into.
zotero_db : str | Path, optional
Path to Zotero SQLite database. Defaults to ``db.zotero``
from ``stack.toml``.
zotero_storage : str | Path, optional
Path to Zotero storage directory. Defaults to
``storage.zotero`` from ``stack.toml``.
years : tuple[int, int] | None
Optional (start, end) year range filter (inclusive).
Returns
-------
dict[str, dict]
Per-table stats: ``{"pfs.rvu": {"rows": N, "files": N}, ...}``
"""
if zotero_db is None or zotero_storage is None:
from conf import path
if zotero_db is None:
zotero_db = path("db.zotero")
if zotero_storage is None:
zotero_storage = path("storage.zotero")
log.info("Discovering PFS files from Zotero...")
all_files = _zotero_pfs_files(zotero_db, zotero_storage)
# Filter by year range
if years:
lo, hi = years
all_files = [f for f in all_files if lo <= f["year"] <= hi]
# Classify files by filename pattern
rvu_files = [
f
for f in all_files
if _is_addendum_b(f["filename"]) or _is_pprrvu(f["filename"])
]
gpci_files = [f for f in all_files if _is_gpci_file(f["filename"])]
pe_files = [
f
for f in all_files
if (
_is_puf_labor(f["filename"])
or _is_puf_supply(f["filename"])
or _is_puf_equip(f["filename"])
or _is_work_time(f["filename"])
)
]
carrier_files = [f for f in all_files if _is_carrier_file(f["filename"])]
zip_carrier_files = [f for f in all_files if _is_zip_carrier_file(f["filename"])]
log.info(
"Found %d RVU, %d GPCI, %d PE, %d Carrier, %d ZipCarrier "
"attachment(s) from %d total",
len(rvu_files),
len(gpci_files),
len(pe_files),
len(carrier_files),
len(zip_carrier_files),
len(all_files),
)
con.execute('CREATE SCHEMA IF NOT EXISTS "pfs"')
# Drop existing tables for clean load
for tbl in [
"rvu",
"gpci",
"clinical_labor",
"medical_supply",
"medical_equipment",
"physician_work_time",
"carrier_locality",
"zip_carrier_locality",
]:
con.execute(f'DROP TABLE IF EXISTS "pfs"."{tbl}"')
stats: dict[str, dict[str, Any]] = {}
# Load RVU files
stats["pfs.rvu"] = _load_rvu_files(rvu_files, con)
# Load GPCI files
stats["pfs.gpci"] = _load_gpci_files(gpci_files, con)
# Load PE files (labor, supply, equipment, work time)
pe_stats = _load_pe_files(pe_files, con)
for table_name, table_stats in pe_stats.items():
stats[f"pfs.{table_name}"] = table_stats
# Load Carrier files
stats["pfs.carrier_locality"] = _load_carrier_files(carrier_files, con)
# Load ZIP code carrier locality files
stats["pfs.zip_carrier_locality"] = _load_zip_carrier_files(zip_carrier_files, con)
return stats