fix(notebooks): stop rendering years as 2,027 — plain_years display cast (closes #643)

marimo's data-table viewer formats integer columns with thousands
separators, so int32/int64 year columns (DuckDB SELECT year, registry-
built frames) displayed as "2,027". New conf.display.plain_years casts
year-like integer columns (year, *_year, *_period; autodetected or
explicit, polars + pandas) to strings at the display boundary only —
analysis frames keep integer dtypes, chart encodings (already :O) are
untouched.

Applied at every affected display site: pfs_calcs carrier/SQL result
tables, pfs_reconciliation delta table, cy2026/cy2027 APM-threshold
tables, cms_quality_measures pipeline-result accordions. All five
notebooks re-executed headlessly in the notebooks container
(nb_integration ci-smoke set): pass=5, displayed year values now
serialize as strings.
This commit is contained in:
kert
2026-08-18 10:07:47 -04:00
parent 77751de9fc
commit 73017081ef
7 changed files with 163 additions and 20 deletions

View File

@@ -43,6 +43,7 @@ def _():
import polars as pl
from conf import connect
from conf.display import plain_years
con = connect.duckdb()
@@ -50,7 +51,7 @@ def _():
"""Run SQL query and return a Polars DataFrame."""
return con.execute(sql).pl()
return con, inspect, pl, q, textwrap
return con, inspect, pl, plain_years, q, textwrap
@app.cell(hide_code=True)
@@ -731,7 +732,7 @@ def _(con, inspect, mo, pl):
@app.cell(hide_code=True)
def _(ex, mo, pl, run_step, uamcc_pp):
def _(ex, mo, pl, plain_years, run_step, uamcc_pp):
_uamcc_pipeline = [
# Staging tables (_stg_medical_claim, _stg_medical_claim_condition) are
# materialized in DuckDB and loaded on demand by load().
@@ -786,7 +787,7 @@ def _(ex, mo, pl, run_step, uamcc_pp):
_short = _name.split(".")[-1]
if _name in uamcc_results:
_df = uamcc_results[_name]
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(_df.head(50))
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(plain_years(_df.head(50)))
elif _name in _uamcc_errors:
_items[f"{_short} (ERROR)"] = mo.md(f"```\n{_uamcc_errors[_name]}\n```")
@@ -801,7 +802,7 @@ def _(ex, mo, pl, run_step, uamcc_pp):
@app.cell(hide_code=True)
def _(acr_pp, ex, mo, pl, run_step):
def _(acr_pp, ex, mo, pl, plain_years, run_step):
_acr_pipeline = [
("cms_quality_measures._acr_performance_period", ex.acr_performance_period),
("cms_quality_measures._acr_int_index_admission", ex.acr_int_index_admission),
@@ -844,7 +845,7 @@ def _(acr_pp, ex, mo, pl, run_step):
_short = _name.split(".")[-1]
if _name in acr_results:
_df = acr_results[_name]
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(_df.head(50))
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(plain_years(_df.head(50)))
elif _name in _acr_errors:
_items[f"{_short} (ERROR)"] = mo.md(f"```\n{_acr_errors[_name]}\n```")
@@ -859,7 +860,7 @@ def _(acr_pp, ex, mo, pl, run_step):
@app.cell(hide_code=True)
def _(ex, hwr_pp, mo, pl, run_step):
def _(ex, hwr_pp, mo, pl, plain_years, run_step):
_hwr_pipeline = [
("cms_quality_measures._hwr_performance_period", ex.hwr_performance_period),
("cms_quality_measures._hwr_int_denominator", ex.hwr_int_denominator),
@@ -901,7 +902,7 @@ def _(ex, hwr_pp, mo, pl, run_step):
_short = _name.split(".")[-1]
if _name in hwr_results:
_df = hwr_results[_name]
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(_df.head(50))
_items[f"{_short} ({len(_df)} rows)"] = mo.ui.table(plain_years(_df.head(50)))
elif _name in _hwr_errors:
_items[f"{_short} (ERROR)"] = mo.md(f"```\n{_hwr_errors[_name]}\n```")

View File

@@ -49,6 +49,7 @@ def _():
import polars as pl
from conf import connect
from conf.display import plain_years
from pfs.rules import RULES
from qpp import QPP, for_payment_year
@@ -66,7 +67,7 @@ def _():
def q(sql):
return con.execute(sql).pl()
return AMBER, QPP, RULES, TEAL, alt, con, for_payment_year, pl, q
return AMBER, QPP, RULES, TEAL, alt, con, for_payment_year, pl, plain_years, q
# ── 1. The conversion-factor walk ─────────────────────────────────────
@@ -572,7 +573,7 @@ def _(for_payment_year, mo):
@app.cell(hide_code=True)
def _(QPP, pl):
def _(QPP, pl, plain_years):
_rows = []
for _perf_year, _qy in sorted(QPP.items()):
_prop = _qy.proposed
@@ -596,7 +597,7 @@ def _(QPP, pl):
}
)
apm_thresholds = pl.DataFrame(_rows)
apm_thresholds
plain_years(apm_thresholds)
return (apm_thresholds,)

View File

@@ -45,6 +45,7 @@ def _():
import polars as pl
from conf import connect
from conf.display import plain_years
from pfs.rules import RULES, proposed_for
from qpp import QPP, for_payment_year
@@ -104,6 +105,7 @@ def _():
fr_md,
pfs_payment,
pl,
plain_years,
proposed_for,
q,
)
@@ -1342,7 +1344,7 @@ def _(for_payment_year, mo):
@app.cell(hide_code=True)
def _(QPP, pl):
def _(QPP, pl, plain_years):
_rows = []
for _perf_year, _qy in sorted(QPP.items()):
_prop = _qy.proposed
@@ -1364,7 +1366,7 @@ def _(QPP, pl):
}
)
apm_thresholds = pl.DataFrame(_rows)
apm_thresholds
plain_years(apm_thresholds)
return (apm_thresholds,)

View File

@@ -27,6 +27,7 @@ def _():
import polars as pl
from conf import connect
from conf.display import plain_years
# PFS reference data lives in the DuckLake lakehouse (M5, #514);
# queries are unchanged — the lake is the default database.
@@ -35,7 +36,7 @@ def _():
def q(sql):
return con.execute(sql).pl()
return con, pl, q
return con, pl, plain_years, q
@app.cell(hide_code=True)
@@ -95,7 +96,7 @@ def _(mo):
@app.cell
def _(hcpcs_input, locality_picker, q, year_picker):
def _(hcpcs_input, locality_picker, plain_years, q, year_picker):
_mac, _loc = locality_picker.value.split("|")
carrier_result = q(f"""
SELECT year, mac, locality, hcpcs, mod,
@@ -108,7 +109,7 @@ def _(hcpcs_input, locality_picker, q, year_picker):
AND hcpcs = '{hcpcs_input.value.strip()}'
ORDER BY mod
""")
carrier_result
plain_years(carrier_result)
return (carrier_result,)
@@ -183,7 +184,7 @@ def _(mo):
@app.cell
def _(hcpcs_input, locality_picker, q, year_picker):
def _(hcpcs_input, locality_picker, plain_years, q, year_picker):
from pfs.rules import RULES as _RULES
_year = int(year_picker.value)
@@ -225,7 +226,7 @@ def _(hcpcs_input, locality_picker, q, year_picker):
AND g.locality = '{_loc}'
ORDER BY r.mod
""")
sql_result
plain_years(sql_result)
return (sql_result,)

View File

@@ -26,6 +26,7 @@ def _(mo):
@app.cell(hide_code=True)
def _():
from conf import connect
from conf.display import plain_years
from rec.engine import reconcile
from rec.pricers.pfs import PfsPricer
@@ -33,7 +34,7 @@ def _():
# queries are unchanged — the lake is the default database.
con = connect.ducklake()
pricer = PfsPricer()
return con, pricer, reconcile
return con, plain_years, pricer, reconcile
@app.cell(hide_code=True)
@@ -84,8 +85,8 @@ def _(mo):
@app.cell(hide_code=True)
def _(result):
_deltas = result.deltas if result is not None else None
def _(plain_years, result):
_deltas = plain_years(result.deltas) if result is not None else None
_deltas
return

59
src/conf/display.py Normal file
View File

@@ -0,0 +1,59 @@
"""Display-time formatting helpers for marimo notebooks.
marimo's data-table viewer (raw DataFrame cell output and
``mo.ui.table``) renders integer columns with locale thousands
separators, so an int32/int64 ``year`` column displays as "2,027"
(#643). ``plain_years`` casts year-like integer columns to strings at
the display boundary; analysis frames keep their integer dtypes.
"""
from __future__ import annotations
import re
from typing import Any
_YEAR_COL = re.compile(r"(^|_)(year|period)$")
def plain_years(df: Any, *cols: str) -> Any:
"""Return ``df`` with year-like integer columns cast to strings.
With no explicit ``cols``, integer columns named ``year``,
``*_year``, ``period``, or ``*_period`` are autodetected; explicit
``cols`` must exist in the frame (KeyError otherwise) but are
still skipped when non-integer. Accepts polars or pandas frames
and never mutates the input. Use only where a frame is handed to
a display, never on frames that feed further computation.
"""
try:
import polars as pl
except ImportError: # pragma: no cover - polars is a hard dep in practice
pl = None # type: ignore[assignment]
import pandas as pd
is_polars = pl is not None and isinstance(df, pl.DataFrame)
if not is_polars and not isinstance(df, pd.DataFrame):
raise TypeError(f"plain_years: unsupported frame type {type(df).__name__}")
if cols:
missing = [c for c in cols if c not in df.columns]
if missing:
raise KeyError(f"plain_years: columns not in frame: {missing}")
names = list(cols)
else:
names = [c for c in df.columns if _YEAR_COL.search(c)]
if is_polars:
targets = [c for c in names if df.schema[c].is_integer()]
if not targets:
return df
return df.with_columns(pl.col(c).cast(pl.String) for c in targets)
targets = [c for c in names if pd.api.types.is_integer_dtype(df[c])]
if not targets:
return df
out = df.copy()
for c in targets:
converted = df[c].map(lambda v: str(int(v)), na_action="ignore")
out[c] = converted.astype(object).where(df[c].notna(), None)
return out

View File

@@ -0,0 +1,78 @@
"""Tests for conf.display.plain_years (#643)."""
from __future__ import annotations
import pandas as pd
import polars as pl
import pytest
from conf.display import plain_years
def test_autodetects_year_like_int_columns_polars() -> None:
df = pl.DataFrame(
{
"year": pl.Series([2026, 2027], dtype=pl.Int32),
"gpci_year": pl.Series([2027, 2027], dtype=pl.Int64),
"qp_performance_period": [2025, 2026],
"rows": [14518, 14169],
}
)
out = plain_years(df)
assert out["year"].to_list() == ["2026", "2027"]
assert out["gpci_year"].to_list() == ["2027", "2027"]
assert out["qp_performance_period"].to_list() == ["2025", "2026"]
# non-year integer columns keep their dtype (counts SHOULD group)
assert out.schema["rows"].is_integer()
# input frame is untouched
assert df.schema["year"] == pl.Int32
def test_explicit_columns_only() -> None:
df = pl.DataFrame({"year": [2027], "payment_year": [2029]})
out = plain_years(df, "year")
assert out.schema["year"] == pl.String
assert out.schema["payment_year"].is_integer()
def test_explicit_missing_column_raises() -> None:
df = pl.DataFrame({"year": [2027]})
with pytest.raises(KeyError):
plain_years(df, "yeer")
def test_non_integer_year_columns_are_left_alone() -> None:
df = pl.DataFrame({"year": ["2027"], "fiscal_year": [2026.5]})
out = plain_years(df)
assert out.schema == df.schema
def test_no_year_columns_is_a_noop() -> None:
df = pl.DataFrame({"hcpcs": ["99213"], "work_rvu": [1.3]})
assert plain_years(df) is df
def test_nulls_survive_polars() -> None:
df = pl.DataFrame({"year": pl.Series([2027, None], dtype=pl.Int64)})
assert plain_years(df)["year"].to_list() == ["2027", None]
def test_pandas_frames_supported() -> None:
df = pd.DataFrame({"performance_year": [2024, 2025], "codes": [10, 20]})
out = plain_years(df)
assert out["performance_year"].tolist() == ["2024", "2025"]
assert pd.api.types.is_integer_dtype(out["codes"])
# input untouched
assert pd.api.types.is_integer_dtype(df["performance_year"])
def test_pandas_nullable_int_with_na() -> None:
df = pd.DataFrame({"year": pd.array([2027, None], dtype="Int64")})
out = plain_years(df)
assert out["year"].tolist()[0] == "2027"
assert out["year"].tolist()[1] is None
def test_unsupported_type_raises() -> None:
with pytest.raises(TypeError):
plain_years({"year": [2027]})