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:
@@ -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```")
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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,)
|
||||
|
||||
|
||||
|
||||
@@ -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
59
src/conf/display.py
Normal 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
|
||||
78
tests/conf/test_display.py
Normal file
78
tests/conf/test_display.py
Normal 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]})
|
||||
Reference in New Issue
Block a user