Files
stack/src/lead/express/__init__.py
kert cb9976e160
All checks were successful
CI / lint (push) Successful in 44s
CI / notebooks-smoke (push) Successful in 1m33s
Deploy / changes (push) Successful in 14s
CI / test (push) Successful in 3m9s
Infra CI / zotero (push) Successful in 14s
Infra CI / notebooks (push) Successful in 55s
Infra CI / api (push) Successful in 1m9s
Infra CI / docs (push) Successful in 1m23s
Infra CI / llm (push) Successful in 1m1s
Infra CI / mc (push) Successful in 16s
Infra CI / bcda (push) Successful in 1m43s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Package Supply Chain / pkg-supply-chain (push) Successful in 1m30s
Deploy / api (push) Successful in 1m17s
Deploy / llm (push) Successful in 1m18s
Deploy / mc (push) Has been skipped
Deploy / bcda (push) Successful in 1m42s
Deploy / report (push) Successful in 1m36s
feat(lead): LEAD PY2027 financial methodology — settlement and benchmark engines
Ports the public CMS "LEAD Model PY 2027 Alignment and Financial
Methodology Paper" v1.2 (bib E4PEU3HV) as generic engines: parameters
come from the paper, everything CMS computes per ACO or per year comes
from the caller. No participant data, no organization-specific rules.

- lead.params: Table 5.2.1 discounts, 3% Quality Withhold, 2% retention
  incentive, Box 5.3.3 Stop-Loss bands, Tables 5.4.1/5.4.3 corridors, 2%
  sequestration, base-year weights, Table 3.5.1 ACPT guardrails,
  risk-score growth caps, REA/PSA shares and caps, voluntary guardrail
  and 500 minimum, Table 2.6.1 base-year alignment minimums.
- lead.settlement: Final Financial Settlement, Table 5.6.1 line by line
  (settle(SettlementInputs) -> SettlementLines).
- lead.stop_loss: Box 5.3.3 charge, banded payout, neutrality factor,
  application to PY expenditure.
- lead.monies_owed: Table 5.5.1 Total Monies Owed, APO reconciliation.
- lead.timing: run-out end, settlement-basis rule, settlement-stage inputs.
- lead.benchmark: §3 formulas — risk standardization, two-/three-way
  trend, base-year weights and sufficiency, voluntary guardrail,
  regional FFS, spending designation, REA, PSA, risk-score cap, hybrid
  alignment dollars, category blend.
- lead.express: the same as narwhals Exprs; express.settlement.settle
  settles a frame of scenarios on any backend.

Rounding: every dollar line rounds to the cent (half away from zero) as
it is produced; PBPMs, rates and factors never do. It is the only rule
under which Tables 3.3.3, 3.5.3.2, 3.5.4, 5.5.1 and 5.6.1 all reproduce
(3.5.4 line 24 sums cent-rounded cells; 3.3.3 carries unrounded PBPMs).
The Expr cents snaps its ÷100 with round(2): polars 2.0 divides a column
by a literal as a reciprocal multiply (k × 0.01), one ulp off k/100 for
~15% of values.

Missing inputs stay missing (None/null or a refused input model); the
only zeros are the paper's own (Professional "N/A" discount, no
Stop-Loss election, sequestration on losses, ineligible REA/PSA).

Paper inconsistencies recorded in tests: Table 5.5.1 line 2 is a cent
below Table 5.6.1 line 28 (.43 vs .44); Box 3.3.2 prints 1.104 then says
1.103; §3.5.1 prose widens the ACPT lower bound a year later than
Table 3.5.1 (the table is followed).

Tests: 283 in tests/lead — every published line to the cent, Expr
builders on polars and DuckDB, bit-for-bit scalar/frame parity over 160
scenarios; 100% of src/lead.
2026-10-27 14:11:10 -04:00

63 lines
2.4 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.
"""narwhals ``Expr`` builders for the LEAD financial methodology.
Each builder takes column names or ``nw.Expr`` objects and returns an
``nw.Expr``, so the same methodology runs on polars, DuckDB, pandas, or any
other narwhals backend, one row per scenario, beneficiary, or benchmark
cell. The scalar twins live in ``lead.settlement``, ``lead.stop_loss``,
``lead.benchmark``; both sides perform the same float operations, so a
frame and a loop over the scalars agree to the bit.
Null in, null out: a missing input produces a null line, never a silent
zero. The zeros these builders do produce are the paper's own, as in the
scalars.
"""
from __future__ import annotations
import narwhals as nw
IntoExpr = str | nw.Expr
"""A column name or a narwhals expression."""
def col(value: IntoExpr) -> nw.Expr:
"""Return ``value`` as an expression (a string names a column)."""
return nw.col(value) if isinstance(value, str) else value
def lit_or_col(value: IntoExpr | float | int) -> nw.Expr:
"""Return a literal for a number, otherwise :func:`col`."""
return nw.lit(value) if isinstance(value, int | float) else col(value)
def cents(value: IntoExpr) -> nw.Expr:
"""Round a dollar expression to the cent, half away from zero.
The rounding is :func:`lead.money.cents`'s: ``floor(|x| × 100 + 0.5)``,
not the backends' ``round``, which differ on ties (polars half to even,
DuckDB away from zero). The final ``÷ 100`` is snapped with
``round(2)`` because polars divides a column by a literal as a multiply
by its reciprocal (``k × 0.01``), one ulp off ``k / 100`` for about one
value in seven; its input is within an ulp of a whole cent, so no tie
rule is involved and every backend returns the scalar's float.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 5.6.1 / Table 3.5.4.
"""
amount = col(value)
whole_cents = (
nw.when(amount < 0)
.then(-((-amount * 100.0 + 0.5).floor()))
.otherwise((amount * 100.0 + 0.5).floor())
)
return (whole_cents / 100.0).round(2)
def ratio(numerator: IntoExpr, denominator: IntoExpr) -> nw.Expr:
"""numerator ÷ denominator; null unless the denominator is positive.
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3 (every ratio it
defines has a positive denominator).
"""
den = col(denominator)
return nw.when(den > 0).then(col(numerator) / den)