Files
stack/src/lead/stop_loss.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

325 lines
11 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.
"""LEAD Stop-Loss Reinsurance (§5.3.3, Box 5.3.3).
Per beneficiary: Predicted Expenditure = regional expenditure PBPM × risk
score × months of alignment; Residual Expenditure = PY expenditure −
Predicted Expenditure; once the Residual passes the Attachment Point it
pays out by the banded schedule. Per ACO: the charge is the trended,
risk-adjusted reference expenditure × the 3-year average payout
percentage; at settlement the neutrality-adjusted payout nets against the
charge, and the net is added to PY expenditure (Table 5.6.1 lines 16–22).
CMS supplies the Attachment Point, the reference-year expenditure and
payout percentages, and the model-wide neutrality factor; none of them is
a constant here.
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3.
"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from lead.money import cents
from lead.params import (
STOP_LOSS_BANDS,
STOP_LOSS_REFERENCE_YEAR_COUNT,
STOP_LOSS_REFERENCE_YEAR_GAP,
StopLossBand,
)
BELOW_ATTACHMENT_POINT = "below_attachment_point"
def reference_years(performance_year: int) -> tuple[int, ...]:
"""Return the three Stop-Loss reference years: the most recent three
before the PY, with a one-year gap (2023–2025 for PY 2027).
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Attachment Point).
"""
last = performance_year - STOP_LOSS_REFERENCE_YEAR_GAP - 1
return tuple(range(last - STOP_LOSS_REFERENCE_YEAR_COUNT + 1, last + 1))
def trended_attachment_point(
reference_attachment_point: float, national_trend_factor: float
) -> float:
"""Trend the most recent reference year's Attachment Point to PY
dollars with the national trend factor.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Attachment Point).
"""
return cents(reference_attachment_point * national_trend_factor)
def predicted_expenditure(
regional_expenditure_pbpm: float, risk_score: float, months_aligned: float
) -> float:
"""Return Predicted Expenditure = LEAD regional expenditure PBPM ×
beneficiary risk score × months of alignment in the PY.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Residual Expenditure).
"""
return cents(regional_expenditure_pbpm * risk_score * months_aligned)
def residual_expenditure(actual_spend: float, predicted_spend: float) -> float:
"""Return Residual Expenditure = PY expenditure − Predicted Expenditure.
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3
(illustration line 4).
"""
return cents(actual_spend - predicted_spend)
@dataclass(frozen=True)
class BandPayout:
"""One band of a beneficiary's Stop-Loss payout."""
band: int
exposure: float
payout_rate: float
payout: float
@dataclass(frozen=True)
class StopLossPayout:
"""The illustration table of Box 5.3.3 for one beneficiary."""
residual_expenditure: float
attachment_point: float
below_attachment_point: float
bands: tuple[BandPayout, ...]
total_payout: float
def band_exposure(
residual: float, attachment_point: float, band: StopLossBand
) -> float:
"""Return the Residual Expenditure that falls inside one band.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Payment Schedule).
"""
capped = (
residual
if band.end_multiple is None
else min(residual, attachment_point * band.end_multiple)
)
return max(0.0, capped - attachment_point * band.start_multiple)
def stop_loss_payout(
residual: float,
attachment_point: float,
bands: Sequence[StopLossBand] = STOP_LOSS_BANDS,
) -> StopLossPayout:
"""Pay the Residual Expenditure above the Attachment Point by band.
Nothing is covered below the Attachment Point; band 1 (100–200 percent
of it) pays 80 percent and band 2 (above 200 percent) pays 100 percent.
A $400,000 Residual against a $150,000 Attachment Point pays $220,000.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Payout; illustration lines 1–5).
"""
exposures = [band_exposure(residual, attachment_point, band) for band in bands]
paid = tuple(
BandPayout(
band=band.band,
exposure=cents(exposure),
payout_rate=band.payout_rate,
payout=cents(exposure * band.payout_rate),
)
for band, exposure in zip(bands, exposures, strict=True)
)
return StopLossPayout(
residual_expenditure=residual,
attachment_point=attachment_point,
below_attachment_point=cents(min(max(residual, 0.0), attachment_point)),
bands=paid,
total_payout=cents(sum(band.payout for band in paid)),
)
def band_status(residual: float, attachment_point: float) -> str:
"""Name the band the Residual Expenditure reaches: ``band_2``,
``band_1`` or ``below_attachment_point``.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Payment Schedule).
"""
for band in reversed(STOP_LOSS_BANDS):
if residual > attachment_point * band.start_multiple:
return f"band_{band.band}"
return BELOW_ATTACHMENT_POINT
def payout_percentage(
total_payout: float, reference_expenditure: float
) -> float | None:
"""Return a reference year's payout percentage: total Stop-Loss payout
÷ total reference-year expenditure (None without expenditure).
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Charge).
"""
return total_payout / reference_expenditure if reference_expenditure > 0 else None
def average_payout_percentage(percentages: Sequence[float | None]) -> float | None:
"""Return the simple average of the reference years' payout
percentages, or None if any year is missing.
Not rounded: Box 5.3.3 prints 2.03% but its charge (line 6) only
reproduces with the unrounded mean of 1.96%, 2.09% and 2.05%.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Calculation of
Stop-Loss Charge, line 5).
"""
known = [p for p in percentages if p is not None]
if not known or len(known) != len(percentages):
return None
return sum(known) / len(known)
@dataclass(frozen=True)
class StopLossCharge:
"""Box 5.3.3, Calculation of Stop-Loss Charge, lines 1–6."""
average_reference_expenditure_pbpm_trended: float
aligned_beneficiary_months: float
average_risk_score: float
trended_risk_adjusted_reference_expenditure: float
average_payout_percentage: float
stop_loss_charge: float
def stop_loss_charge(
average_reference_expenditure_pbpm_trended: float,
aligned_beneficiary_months: float,
average_risk_score: float,
average_payout_pct: float,
) -> StopLossCharge:
"""Return the PY Stop-Loss charge: trended reference PBPM × aligned
months × average PY risk score (line 4), × average payout percentage
(line 6).
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Calculation of
Stop-Loss Charge).
"""
reference = cents(
average_reference_expenditure_pbpm_trended
* aligned_beneficiary_months
* average_risk_score
)
return StopLossCharge(
average_reference_expenditure_pbpm_trended=average_reference_expenditure_pbpm_trended,
aligned_beneficiary_months=aligned_beneficiary_months,
average_risk_score=average_risk_score,
trended_risk_adjusted_reference_expenditure=reference,
average_payout_percentage=average_payout_pct,
stop_loss_charge=cents(reference * average_payout_pct),
)
def charge_pbpm(charge: float, aligned_beneficiary_months: float) -> float | None:
"""Express the charge per aligned beneficiary month (None without months).
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 (the charge is a
per-beneficiary per-month amount).
"""
return charge / aligned_beneficiary_months if aligned_beneficiary_months else None
def neutrality_factor(
total_model_charges: float, total_model_payouts: float
) -> float | None:
"""Return the Stop-Loss Neutrality Factor: all LEAD Stop-Loss charges ÷
all LEAD Stop-Loss payouts in the PY.
With no payouts model-wide the ratio is undefined and None is returned;
every ACO's payout is then zero, so nothing needs scaling.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
Payout Adjustment Factor).
"""
return total_model_charges / total_model_payouts if total_model_payouts else None
@dataclass(frozen=True)
class StopLossApplication:
"""Table 5.6.1 lines 16–22."""
py_expenditure: float
stop_loss_charge: float
stop_loss_payout: float
stop_loss_neutrality_factor: float | None
adjusted_stop_loss_payout: float
net_stop_loss_impact: float
py_expenditure_after_stop_loss: float
def adjusted_payout(payout: float, factor: float | None) -> float:
"""Scale the ACO's payout by the neutrality factor (Table 5.6.1 line 20).
A None factor is accepted only with a zero payout.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Application of
Stop-Loss, line 20).
"""
if factor is None:
if payout:
raise ValueError("a Stop-Loss payout needs the neutrality factor")
return 0.0
return cents(payout * factor)
def apply_stop_loss(
py_expenditure: float,
charge: float,
payout: float,
factor: float | None,
) -> StopLossApplication:
"""Add the net of charge and neutrality-adjusted payout to the PY
expenditure (Table 5.6.1 lines 16–22).
Stop-Loss is virtual: the charge increases PY expenditure and the
payout reduces it.
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Application of
Stop-Loss).
"""
adjusted = adjusted_payout(payout, factor)
net = cents(charge - adjusted)
return StopLossApplication(
py_expenditure=py_expenditure,
stop_loss_charge=charge,
stop_loss_payout=payout,
stop_loss_neutrality_factor=factor,
adjusted_stop_loss_payout=adjusted,
net_stop_loss_impact=net,
py_expenditure_after_stop_loss=cents(py_expenditure + net),
)
def without_stop_loss(py_expenditure: float) -> StopLossApplication:
"""Lines 16–22 for an ACO that did not elect Stop-Loss: no charge, no
payout, and PY expenditure unchanged.
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 (only ACOs that
elected Stop-Loss have PY expenditures adjusted).
"""
return StopLossApplication(
py_expenditure=py_expenditure,
stop_loss_charge=0.0,
stop_loss_payout=0.0,
stop_loss_neutrality_factor=None,
adjusted_stop_loss_payout=0.0,
net_stop_loss_impact=0.0,
py_expenditure_after_stop_loss=py_expenditure,
)