Files
stack/dev/scripts/generate_quality_measure_docs.py
kert 85ce5e719d
Some checks failed
CI / skinny-install (aco) (push) Successful in 45s
CI / skinny-install (api) (push) Successful in 29s
CI / skinny-install (bcda) (push) Successful in 25s
CI / skinny-install (bib) (push) Successful in 23s
CI / skinny-install (bls) (push) Successful in 20s
CI / skinny-install (ccw) (push) Successful in 36s
CI / skinny-install (cli) (push) Successful in 27s
CI / skinny-install (cms) (push) Successful in 24s
CI / skinny-install (conf) (push) Successful in 27s
CI / skinny-install (pfs) (push) Successful in 25s
CI / skinny-install (rex) (push) Successful in 25s
CI / lint-test (push) Successful in 6m2s
Infra CI / notebooks (push) Successful in 7s
Infra CI / zotero (push) Failing after 6s
Infra CI / docs (push) Successful in 33s
Infra CI / api (push) Successful in 6s
Infra CI / mc (push) Successful in 7s
Deploy / build-scan-report (push) Has been cancelled
chore: clean sweep — lint, format, stale refs, generated artifacts
- Fix all 72 ruff lint errors (unused imports, unused variables, E402)
- Format all 14 unformatted dev/scripts files
- Move generated artifacts to assets/ (dag.html, pfs.html)
- Remove duplicate root coverage.svg (already in assets/icons/)
- Update .dockerignore for infra/ tree layout
- Update .gitignore: add .env.bak, mirrors/, htmlcov/
- Fix stale path refs in coverage_badge.py, woodpecker backend,
  test_network_isolation.sh, docs custom.css
- Add .gitkeep to empty dirs (infra/polaris, cloud/*/terraform)
- Delete 12 stale local branches, 10 stale remote branches
2026-03-24 17:33:55 -04:00

585 lines
25 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.
"""Annotate CMS quality measure table models and express functions with
documentation from Measure Information Form (MIF) PDFs and value set workbooks.
Reads six MIF PDFs and their accompanying value set Excel files from
Zotero storage and injects precise, page-cited passages into the
docstrings of:
src/aco/table/cms_quality_measures.py
src/aco/express/cms_quality_measures.py
src/aco/pipe/cms_quality_measures.py
Measures covered
----------------
UAMCC All-Cause Unplanned Admissions for Patients with Multiple
Chronic Conditions (NQF #2888) — ACO REACH + MIPS MCC
ACR Risk-Standardized, All-Condition Readmission (NQF #1789)
— ACO REACH
HWR Hospital-wide, 30-Day, All-cause Unplanned Readmission
— MIPS Groups
Source documents (Zotero storage)
----------------------------------
ACOREACH_PY2025_UAMCC_MIF_posted07072025.pdf
ACOREACH_PY2025_UAMCC_ValueSet_posted07072025.xlsx
ACOREACH_PY2025_ACR_MIF_updated07072025.pdf
ACOREACH_PY2025_ACR_ValueSet_posted02212025.xlsx
ACOREACH-PY2026-UAMCC-MIF_posted10312025.pdf
ACOREACH-PY2026-UAMCC-ValueSet_posted10312025.xlsx
ACOREACH_PY2026_ARC_MIF_posted10312025.pdf
MIPS_Hospital-Wide Readmission_2025_MIF.pdf
MIPS_CY_2025_HWR_Measure_Code_Specifications.xlsx
PY2025-MIPS-Admin-Claims-MCC-MIF.pdf
Version_2024_MIPS_MCC_01.03.25.xlsx
PY2026-MIPS-Admin-Claims-MCC-MIF.pdf
Usage::
uv run python dev/scripts/generate_quality_measure_docs.py [--zotero PATH]
Injection targets
-----------------
The script uses ``_inject_class_docstring`` and ``_inject_function_docstring``
helpers (identical in spirit to generate_reach_docs.py) to replace the
first triple-quoted string inside each class/def block.
Design notes
------------
* Every passage records the PDF page number so the docstring includes a
precise citation that can be verified against the original MIF.
* Value set workbook sheets are introspected for row/column counts that
appear in the docstrings as "~N codes" annotations.
* The script is idempotent: running it twice produces the same output.
* Sections are extracted using CMS-standard MIF section numbering
(§3.6 Numerator Statement, §3.8 Denominator Statement, etc.) with
graceful fallback to full-text search for MIPS MIFs that use letter
headings (A., B., C., …).
"""
from __future__ import annotations
import re
import textwrap
import zipfile
from pathlib import Path
from typing import NamedTuple
import pdfplumber
# ── Path constants ────────────────────────────────────────────────────────────
from conf import ROOT as _ROOT
from conf import path as _conf_path
_SEEDS = _ROOT / "dev" / "seeds"
_ZOTERO_DEFAULT = _conf_path("storage.zotero")
_TABLE_PATH = _ROOT / "src" / "aco" / "table" / "cms_quality_measures.py"
_EXPRESS_PATH = _ROOT / "src" / "aco" / "express" / "cms_quality_measures.py"
_PIPE_PATH = _ROOT / "src" / "aco" / "pipe" / "cms_quality_measures.py"
# ── MIF registry ──────────────────────────────────────────────────────────────
# Each entry is (PDF basename, value set basename or None, zip basename or None)
_MIFS: list[tuple[str, str | None, str | None]] = [
# ACO REACH PY2025
(
"ACOREACH_PY2025_UAMCC_MIF_posted07072025.pdf",
"ACOREACH_PY2025_UAMCC_ValueSet_posted07072025.xlsx",
"ACOREACHPY2025MIFs&ValueSets_UpdatedPosting_07072025.zip",
),
(
"ACOREACH_PY2025_ACR_MIF_updated07072025.pdf",
"ACOREACH_PY2025_ACR_ValueSet_posted02212025.xlsx",
"ACOREACHPY2025MIFs&ValueSets_UpdatedPosting_07072025.zip",
),
# ACO REACH PY2026
(
"ACOREACH-PY2026-UAMCC-MIF_posted10312025.pdf",
"ACOREACH-PY2026-UAMCC-ValueSet_posted10312025.xlsx",
"PY 2026 MIFs and Value Sets_Updated 4i Posting_12-15-2025.zip",
),
(
"ACOREACH_PY2026_ARC_MIF_posted10312025.pdf",
None,
"PY 2026 MIFs and Value Sets_Updated 4i Posting_12-15-2025.zip",
),
# MIPS HWR 2025
(
"MIPS_Hospital-Wide Readmission_2025_MIF.pdf",
"MIPS_CY_2025_HWR_Measure_Code_Specifications.xlsx",
"MIPS_Hospital-Wide-Readmission_2025.zip",
),
# MIPS MCC 2025
(
"PY2025-MIPS-Admin-Claims-MCC-MIF.pdf",
"Version_2024_MIPS_MCC_01.03.25.xlsx",
"2025-MIPS-MCC-Measure-Specification.zip",
),
# MIPS MCC 2026
(
"PY2026-MIPS-Admin-Claims-MCC-MIF.pdf",
"Version_2025_MIPS_MCC_01.09.26.xlsx",
"2026-MIPS-MCC-Measure-Specification.zip",
),
]
# ── Named result type ─────────────────────────────────────────────────────────
class SheetSummary(NamedTuple):
"""Row count and cleaned header for a single value set sheet."""
name: str
row_count: int
header: list[str]
# ═══════════════════════════════════════════════════════════════════════════════
# File discovery helpers
# ═══════════════════════════════════════════════════════════════════════════════
def _find_in_zotero(basename: str, zotero_root: Path) -> Path | None:
"""Locate a file anywhere under the Zotero storage tree by basename."""
for candidate in zotero_root.rglob(basename):
if candidate.is_file():
return candidate
return None
def _extract_from_zip(
zip_basename: str,
member_basename: str,
zotero_root: Path,
dest_dir: Path,
) -> Path | None:
"""Extract a single member from a Zotero zip and return its path."""
zip_path = _find_in_zotero(zip_basename, zotero_root)
if zip_path is None:
return None
dest = dest_dir / member_basename
if dest.exists():
return dest
try:
with zipfile.ZipFile(zip_path) as zf:
# Match by basename in case path components differ
for info in zf.infolist():
if Path(info.filename).name == member_basename:
with zf.open(info) as src, dest.open("wb") as out:
out.write(src.read())
return dest
except Exception as exc: # noqa: BLE001
print(f" ⚠ Could not extract {member_basename} from {zip_path.name}: {exc}")
return None
def _resolve_pdf(
pdf_basename: str, zip_basename: str | None, zotero_root: Path, work_dir: Path
) -> Path | None:
"""Return path to a MIF PDF, extracting from zip if necessary."""
direct = _find_in_zotero(pdf_basename, zotero_root)
if direct:
return direct
if zip_basename:
return _extract_from_zip(zip_basename, pdf_basename, zotero_root, work_dir)
return None
def _resolve_xlsx(
xlsx_basename: str | None,
zip_basename: str | None,
zotero_root: Path,
work_dir: Path,
) -> Path | None:
"""Return path to a value set Excel file, extracting from zip if needed."""
if xlsx_basename is None:
return None
direct = _find_in_zotero(xlsx_basename, zotero_root)
if direct:
return direct
if zip_basename:
return _extract_from_zip(zip_basename, xlsx_basename, zotero_root, work_dir)
return None
# ═══════════════════════════════════════════════════════════════════════════════
# PDF extraction helpers
# ═══════════════════════════════════════════════════════════════════════════════
def _extract_pages(pdf_path: Path) -> dict[int, str]:
"""Return {1-based page number: page text} for every page in the PDF."""
pdf = pdfplumber.open(str(pdf_path))
pages: dict[int, str] = {}
for i, page in enumerate(pdf.pages):
pages[i + 1] = page.extract_text() or ""
pdf.close()
return pages
def _full_text(pages: dict[int, str]) -> str:
"""Join all page texts into a single string with page separators."""
parts = []
for page_num in sorted(pages):
parts.append(f"\n[Page {page_num}]\n{pages[page_num]}")
return "\n".join(parts)
def _clean(text: str) -> str:
"""Normalize whitespace for docstring insertion."""
text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r"[ \t]+\n", "\n", text)
return text.strip()
# ── Section extraction ────────────────────────────────────────────────────────
# MIF section header patterns for REACH-style (numbered) MIFs
_REACH_SECTION_RE = re.compile(
r"(\d+\.\d+)\s+([A-Z][^\n]+)\n",
re.MULTILINE,
)
# MIF section header patterns for MIPS-style (lettered) MIFs
_MIPS_SECTION_RE = re.compile(
r"\n([A-Z])\.\s+([A-Z][^\n]+)\n",
re.MULTILINE,
)
def _extract_reach_section(full: str, section: str) -> tuple[str, int | None]:
"""Extract the body of a numbered REACH MIF section (e.g. '3.8').
Returns (text, start_page) where start_page is the first [Page N]
marker found before the section heading, or None if not found.
"""
# Locate section heading
pattern = re.compile(
rf"{re.escape(section)}\s+[A-Z][^\n]+\n(.+?)(?=\n\d+\.\d+\s+[A-Z]|\Z)",
re.DOTALL,
)
m = pattern.search(full)
if not m:
return "", None
body = _clean(m.group(1))
# Find the page number immediately before this match
page_re = re.compile(r"\[Page (\d+)\]")
page_num: int | None = None
for pm in page_re.finditer(full, 0, m.start()):
page_num = int(pm.group(1))
return body, page_num
def _extract_mips_section(full: str, letter: str) -> tuple[str, int | None]:
"""Extract the body of a lettered MIPS MIF section (e.g. 'D')."""
pattern = re.compile(
rf"\n{re.escape(letter)}\.\s+[A-Z][^\n]+\n(.+?)(?=\n[A-Z]\.\s+[A-Z]|\Z)",
re.DOTALL,
)
m = pattern.search(full)
if not m:
return "", None
body = _clean(m.group(1))
page_re = re.compile(r"\[Page (\d+)\]")
page_num: int | None = None
for pm in page_re.finditer(full, 0, m.start()):
page_num = int(pm.group(1))
return body, page_num
def _find_page_for_term(full: str, term: str) -> int | None:
"""Return the page number where *term* first appears."""
idx = full.find(term)
if idx == -1:
return None
page_re = re.compile(r"\[Page (\d+)\]")
page_num: int | None = None
for m in page_re.finditer(full, 0, idx):
page_num = int(m.group(1))
return page_num
# ═══════════════════════════════════════════════════════════════════════════════
# Value set introspection
# ═══════════════════════════════════════════════════════════════════════════════
def _read_sheet_summaries(xlsx_path: Path) -> dict[str, SheetSummary]:
"""Return a {sheet_name: SheetSummary} dict for all sheets in the workbook."""
try:
import openpyxl # type: ignore
except ImportError:
print(" ⚠ openpyxl not installed — skipping value set introspection")
return {}
wb = openpyxl.load_workbook(str(xlsx_path), read_only=True, data_only=True)
summaries: dict[str, SheetSummary] = {}
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
rows = list(ws.iter_rows(values_only=True))
row_count = len(rows) - 1 # subtract header
if row_count < 0:
row_count = 0
# Find header row (first row with ≥2 non-None cells)
header: list[str] = []
for row in rows[:3]:
non_none = [str(c).strip() for c in row if c is not None]
if len(non_none) >= 2:
header = [
str(c).strip().replace("\n", " ") for c in row if c is not None
]
break
summaries[sheet_name] = SheetSummary(sheet_name, max(0, row_count), header)
wb.close()
return summaries
# ═══════════════════════════════════════════════════════════════════════════════
# Passage builders
# ═══════════════════════════════════════════════════════════════════════════════
def _wrap(text: str, width: int = 72, indent: str = " ") -> str:
"""Wrap and indent text for insertion into a docstring."""
lines = []
for para in text.split("\n\n"):
wrapped = textwrap.fill(
para.replace("\n", " "),
width=width,
initial_indent=indent,
subsequent_indent=indent,
)
lines.append(wrapped)
return "\n\n".join(lines)
def _cite(pdf_name: str, section: str | None, page: int | None) -> str:
"""Build a compact citation string."""
parts = [pdf_name]
if section:
parts.append(section)
if page is not None:
parts.append(f"p.{page}")
return "".join(parts)
def build_uamcc_passages(
pdf_path: Path,
pdf_name: str,
xlsx_path: Path | None,
py_version: str, # e.g. "PY2025"
) -> dict[str, str]:
"""Extract UAMCC MIF sections and build docstring passage dictionary.
Keys match class/function names in the source files.
"""
pages = _extract_pages(pdf_path)
full = _full_text(pages)
# Extract key sections
num_stmt, num_page = _extract_reach_section(full, "3.6")
num_detail, num_d_page = _extract_reach_section(full, "3.7")
denom_stmt, denom_page = _extract_reach_section(full, "3.8")
denom_detail, denom_d_page = _extract_reach_section(full, "3.9")
denom_excl, denom_excl_page = _extract_reach_section(full, "3.10")
denom_excl_detail, _ = _extract_reach_section(full, "3.11")
risk_adj, risk_page = _extract_reach_section(full, "3.12")
# Pull release notes / changes summary
release_notes, _ = _extract_reach_section(full, "3.5")
# Value set sheet summaries
vs: dict[str, SheetSummary] = {}
if xlsx_path and xlsx_path.exists():
vs = _read_sheet_summaries(xlsx_path)
def _vs_info(sheet: str) -> str:
s = vs.get(sheet)
if s is None:
return ""
return f"~{s.row_count:,} entries — columns: {', '.join(s.header[:4])}"
paa1_info = _vs_info("UAMCC PAA1")
paa2_info = _vs_info("UAMCC PAA2")
paa3_info = _vs_info("UAMCC PAA3")
paa4_info = _vs_info("UAMCC PAA4")
cohort_info = _vs_info("UAMCC Cohort")
excl_info = _vs_info("UAMCC Exclusions")
_vs_info("UAMCC CCS-ICD10CM")
_vs_info("UAMCC CCS-ICD10PCS")
cite_pp = _cite(pdf_name, "§3.8", denom_page)
_cite(pdf_name, "§3.63.7", num_page)
cite_excl = _cite(pdf_name, "§3.10", denom_excl_page)
_cite(pdf_name, "§3.12", risk_page)
cite_paa = _cite(pdf_name, "§3.7 PAA v4.0", num_d_page)
passages: dict[str, str] = {}
# ── Performance period ────────────────────────────────────────────
passages["uamcc_performance_period"] = (
f"Return the UAMCC performance period anchor row.\n\n"
f"Source: {_cite(pdf_name, '§1 Effective Date + §2.2', 1)}\n\n"
f"UAMCC §2.2 Measure Description:\n"
f' "This outcome measure is calculated using 12 consecutive months\n'
f" of Medicare fee-for-service (FFS) claims data. The measure is a\n"
f" risk-standardized acute admission rate (RSAAR) that adjusts for\n"
f" age, clinical comorbidities, and other clinical and frailty risk\n"
f" factors present at the start of the 12-month measurement period,\n"
f" as well as social risk factors. Lower RSAARs indicate better\n"
f' performance."\n\n'
f"NQF ID: #2888 (ACO RSAAR Quality Measure)\n"
f"Measurement duration: 12 consecutive months (Jan 1 Dec 31).\n"
f"Performance Year: {py_version}.\n\n"
f"The prior-year lookback window (Jan 1 Dec 31 of the year before)\n"
f"is used for chronic condition identification per the UAMCC Cohort\n"
f"value set algorithms."
)
# ── MCC cohort ────────────────────────────────────────────────────
passages["uamcc_int_mcc_cohort"] = (
f"Identify each beneficiary's qualifying chronic condition groups.\n\n"
f"Source: {_cite(pdf_name, '§3.9', denom_d_page)}\n\n"
f"UAMCC §3.9 Denominator Details:\n"
f' "The cohort is Medicare FFS beneficiaries 66 years of age and\n'
f" older assigned to the REACH ACO during the measurement period\n"
f" with diagnoses that fall into two or more of nine chronic disease\n"
f' groups."\n\n'
f"Nine chronic disease groups (MIF §3.9, pp.78):\n"
f" 1. Acute myocardial infarction (AMI)\n"
f" 2. Alzheimer's disease and related disorders or senile dementia\n"
f" 3. Atrial fibrillation\n"
f" 4. Chronic kidney disease (CKD)\n"
f" 5. Chronic obstructive pulmonary disease (COPD) and asthma\n"
f" 6. Depression\n"
f" 7. Diabetes\n"
f" 8. Heart failure\n"
f" 9. Stroke and transient ischemic attack (TIA)\n\n"
f"Eight groups use CMS Chronic Conditions Data Warehouse (CCW)\n"
f"algorithms; Diabetes uses the ACO-36 v2018a definition.\n"
f"COPD and asthma are combined into a single group.\n\n"
f"Value set: 'UAMCC Cohort' tab — {cohort_info}\n"
f"Joins medical claims on normalized_code → ICD-10-CM code,\n"
f"groups by (person_id, chronic_condition_group)."
)
# ── Denominator ───────────────────────────────────────────────────
denom_stmt_trimmed = (denom_stmt or "")[:600]
passages["uamcc_int_denominator"] = (
f"Build the UAMCC denominator: MCC-eligible beneficiaries aged ≥66.\n\n"
f"Source: {cite_pp}\n\n"
f"UAMCC §3.8 Denominator Statement:\n"
f' "{denom_stmt_trimmed[:400]}"\n\n'
f"Inclusion criteria (MIF §3.9):\n"
f" 1. Age ≥66 at the first day of the measurement period\n"
f" 2. Two or more distinct MCC groups identified in the lookback year\n"
f" 3. Full-time enrollment in Medicare Parts A and B during the year\n"
f" prior to the measurement period\n"
f" 4. Full-time enrollment in Medicare Parts A and B during the\n"
f" measurement year (relaxed if the beneficiary dies or enters\n"
f" hospice during the year)"
)
# ── Denominator exclusions ────────────────────────────────────────
excl_snippet = (denom_excl or "")[:800]
passages["uamcc_int_denominator_exclusion"] = (
f"Identify beneficiaries excluded from the UAMCC denominator.\n\n"
f"Source: {cite_excl}\n\n"
f"UAMCC §3.10 Denominator Exclusions:\n"
f"{_wrap(excl_snippet, width=72)}\n\n"
f"Six exclusion criteria (MIF §3.10):\n"
f" 1. Voluntarily aligned after Jan 1 of the performance year\n"
f" 2. Lacks 12-month continuous Part A/B enrollment in the prior year\n"
f" 3. Lacks continuous Part A/B enrollment in the measurement year\n"
f" (relaxed for death or hospice entry)\n"
f" 4. Enrolled in hospice during the prior year or at period start\n"
f" 5. No qualifying E&M or other visit to any TIN/NPI or CCN/NPI\n"
f" associated with the aligned ACO in measurement or prior year\n"
f" 6. Not at risk for hospitalization at any point during the year"
)
# ── Planned admission (PAA) ───────────────────────────────────────
if num_detail:
# Find the PAA description paragraph
m = re.search(
r"planned admission algorithm.+?(?=\n\n|\Z)",
num_detail,
re.DOTALL | re.IGNORECASE,
)
if m:
m.group(0)[:600]
passages["uamcc_int_planned_admission"] = (
f"Apply PAA v4.0 {py_version} to classify inpatient admissions as planned.\n\n"
f"Source: {cite_paa}\n\n"
f"UAMCC §3.7 Planned Admission Algorithm (PAA):\n"
f" \"The planned admission algorithm was based on CMS's Planned\n"
f" Readmission Algorithm Version 4.0, which CMS originally created\n"
f" to identify planned readmissions for the hospital-wide readmission\n"
f" measure. In brief, the algorithm uses a flowchart and four tables\n"
f' of procedure and/or discharge diagnosis categories."\n\n'
f"PAA Rules (first match wins):\n"
f" Rule 1 — Any procedure in an always-planned CCS category (PAA1).\n"
f" Rule 2 — Principal diagnosis in an always-planned CCS diagnosis\n"
f" category (PAA2).\n"
f" Rule 3 — Any procedure in a potentially-planned CCS category or\n"
f" ICD-10-PCS code (PAA3) AND the principal diagnosis is\n"
f" NOT an acute diagnosis (PAA4).\n\n"
f"Value set tab sizes ({py_version}):\n"
f" PAA1 (always-planned procedures): {paa1_info}\n"
f" PAA2 (always-planned diagnoses): {paa2_info}\n"
f" PAA3 (potentially-planned procs): {paa3_info}\n"
f" PAA4 (acute diagnoses): {paa4_info}"
)
# ── Outcome exclusions ────────────────────────────────────────────
passages["uamcc_int_outcome_exclusion"] = (
f"Flag inpatient admissions excluded from the UAMCC outcome.\n\n"
f"Source: {_cite(pdf_name, '§3.7 Outcome Exclusions', num_page)}\n\n"
f"UAMCC §3.7 — Admissions excluded from the numerator:\n"
f" 1. Planned admissions (PAA v4.0 — see _uamcc_int_planned_admission)\n"
f" 2. Admissions directly from a skilled nursing facility (SNF) or\n"
f" acute rehabilitation facility\n"
f" 3. Admissions within the 10-day buffer period following discharge\n"
f" from a hospital, SNF, or acute rehabilitation facility\n"
f" 4. Admissions occurring after the patient entered hospice\n"
f" 5. Procedure/surgery complications (AHRQ CCS):\n"
f" 145 Intestinal obstruction without hernia\n"
f" 237 Complication of device, implant or graft\n"
f" 238 Complications of surgical procedures or medical care\n"
f" 257 Other aftercare\n"
f" 6. Accidents/injuries — AHRQ CCS E-code categories 26012621:\n"
f" Cut/pierce, drowning, fire/burn, firearm, machinery, MVT,\n"
f" pedal cyclist, pedestrian, transport, natural/environment,\n"
f" overexertion, poisoning, struck by, suffocation, adverse\n"
f" effects of medical care, other specified, unspecified,\n"
f" place of occurrence\n"
f" 7. Admissions before the beneficiary's first qualifying ACO visit\n\n"
f"Exclusion value set: 'UAMCC Exclusions' tab — {excl_info}"
)
# ── Person time ───────────────────────────────────────────────────
passages["uamcc_int_person_time"] = (
f"Calculate at-risk person-time for each UAMCC-eligible beneficiary.\n\n"
f"Source: {_cite(pdf_name, '§3.11', denom_d_page)}\n\n"
f"UAMCC §3.11 Denominator Exclusion Details:\n"
f' "Persons are considered at risk for admission if they are alive,\n'
f" enrolled in Medicare FFS, and not admitted to an acute care\n"
f" hospital. In addition to time spent in the hospital, excluded\n"
f" from at-risk time are:\n"
f" (1) time spent in an SNF or acute rehabilitation facility;\n"
f" (2) time within 10 days following discharge from a hospital,\n"
f" SNF, or acute rehabilitation facility;\n"
f' (3) time after entering hospice care."\n\n'
f"Person-years = at_risk_days / 365.25\n\n"
f"Performance Year: {py_version}."
)
return passages