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
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.
285 lines
10 KiB
Python
285 lines
10 KiB
Python
"""lead.settlement reproduces Table 5.6.1 line by line and keeps the
|
|
paper's edge cases: no Professional discount, the "N/A" discount, losses,
|
|
corridor boundaries, the retention incentive, and Stop-Loss elections."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, replace
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from lead.params import DISCOUNT_RATES, RISK_CORRIDORS
|
|
from lead.settlement import (
|
|
SettlementInputs,
|
|
adjust_benchmark,
|
|
apply_discount,
|
|
benchmark_discount_rate,
|
|
corridor_exposure,
|
|
gross_savings_losses,
|
|
py_expenditure,
|
|
quality_earn_back,
|
|
quality_withhold,
|
|
retention_incentive_adjustment,
|
|
sequestration_amount,
|
|
settle,
|
|
shared_savings_losses,
|
|
)
|
|
from tests.lead.paper import TABLE_5_6_1, TABLE_5_6_1_INPUTS, TABLE_5_6_1_LINES
|
|
|
|
|
|
def table_5_6_1(risk_option: str, **changes) -> SettlementInputs:
|
|
return SettlementInputs(
|
|
**{**TABLE_5_6_1_INPUTS, "risk_option": risk_option, **changes}
|
|
)
|
|
|
|
|
|
# ── Table 5.6.1 ───────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize("risk_option", ["Global", "Professional"])
|
|
@pytest.mark.parametrize(
|
|
"line,field", TABLE_5_6_1_LINES, ids=[f"line {n}" for n, _ in TABLE_5_6_1_LINES]
|
|
)
|
|
def test_table_5_6_1_every_line_to_the_cent(risk_option, line, field):
|
|
lines = asdict(settle(table_5_6_1(risk_option)))
|
|
assert lines[field] == TABLE_5_6_1[risk_option][line]
|
|
|
|
|
|
def test_table_5_2_2_is_lines_1_to_9_of_table_5_6_1():
|
|
# Table 5.2.2 prints the same benchmark adjustments, reductions positive.
|
|
for option, rate in (("Global", 0.03), ("Professional", 0.0)):
|
|
adjusted = adjust_benchmark(150_000_000.0, rate, 0.95)
|
|
assert adjusted.benchmark_after_adjustments == TABLE_5_6_1[option]["9"]
|
|
assert adjusted.net_quality_withhold == 225_000.00
|
|
|
|
|
|
def test_table_5_3_2_py_expenditure():
|
|
expenditure = py_expenditure(10_000_000, 1_003_442, 33_435_084, 91_355_457)
|
|
assert (expenditure.total_ffs_payments, expenditure.py_expenditure) == (
|
|
125_793_983.00,
|
|
135_793_983.00,
|
|
)
|
|
|
|
|
|
def test_tables_5_4_2_and_5_4_4_corridor_lines():
|
|
global_ = shared_savings_losses(9_229_682.72, 145_275_000.0, "Global")
|
|
professional = shared_savings_losses(13_729_682.72, 149_775_000.0, "Professional")
|
|
assert global_.retained_by_corridor == (9_229_682.72, 0.0, 0.0, 0.0)
|
|
assert professional.retained_by_corridor == (8_237_809.63, 0.0, 0.0, 0.0)
|
|
assert (global_.scenario, professional.scenario) == ("savings", "savings")
|
|
|
|
|
|
# ── §5.2 benchmark adjustments ────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"designation,year,rate",
|
|
[(d, y, r) for d, by_year in DISCOUNT_RATES.items() for y, r in by_year.items()],
|
|
)
|
|
def test_table_5_2_1_discount_schedule(designation, year, rate):
|
|
assert benchmark_discount_rate("Global", designation, year) == rate
|
|
|
|
|
|
def test_discount_schedule_values():
|
|
higher = DISCOUNT_RATES["Higher-spending"]
|
|
assert [higher[y] for y in range(2027, 2033)] == [
|
|
0.0175,
|
|
0.02,
|
|
0.0225,
|
|
0.025,
|
|
0.0275,
|
|
0.03,
|
|
]
|
|
assert set(DISCOUNT_RATES["Lower-spending"].values()) == {0.03}
|
|
|
|
|
|
def test_professional_option_has_no_discount_whatever_the_designation():
|
|
for designation in ("Lower-spending", "Higher-spending", None):
|
|
assert benchmark_discount_rate("Professional", designation, 2031) == 0.0
|
|
|
|
|
|
def test_higher_spending_2027_discount_dollars():
|
|
lines = settle(table_5_6_1("Global", spending_designation="Higher-spending"))
|
|
assert (lines.discount_rate, lines.discount_amount) == (0.0175, 2_625_000.00)
|
|
assert lines.benchmark_after_adjustments == 147_150_000.00
|
|
|
|
|
|
def test_discount_rate_refuses_what_it_cannot_look_up():
|
|
with pytest.raises(ValueError, match="spending designation"):
|
|
benchmark_discount_rate("Global", None, 2027)
|
|
with pytest.raises(ValueError, match="outside the LEAD model"):
|
|
benchmark_discount_rate("Global", "Lower-spending", 2037)
|
|
with pytest.raises(ValueError, match="unknown risk option"):
|
|
benchmark_discount_rate("global", "Lower-spending", 2027)
|
|
|
|
|
|
def test_missing_discount_rate_is_the_papers_na_and_removes_nothing():
|
|
discount = apply_discount(150_000_000.0, None)
|
|
assert (discount.discount_rate, discount.discount_amount) == (0.0, 0.0)
|
|
assert discount.benchmark_after_discount == 150_000_000.0
|
|
|
|
|
|
def test_quality_withhold_is_on_the_benchmark_before_the_discount():
|
|
assert quality_withhold(150_000_000.0) == 4_500_000.00
|
|
assert quality_earn_back(4_500_000.0, 0.0) == 0.0
|
|
assert quality_earn_back(4_500_000.0, 1.0) == 4_500_000.0
|
|
with pytest.raises(ValueError, match="quality score"):
|
|
quality_earn_back(4_500_000.0, 1.2)
|
|
|
|
|
|
def test_retention_incentive_reduces_the_benchmark_by_two_percent():
|
|
assert retention_incentive_adjustment(150_000_000.0, False) == 0.0
|
|
lines = settle(table_5_6_1("Global", retention_incentive_applies=True))
|
|
assert lines.retention_incentive_adjustment == 3_000_000.00
|
|
assert lines.benchmark_after_adjustments == 145_275_000.00 - 3_000_000.00
|
|
|
|
|
|
# ── §5.4 corridors, losses, sequestration ─────────────────────────────────
|
|
|
|
B = 100_000_000.0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"option,share,expected",
|
|
[
|
|
# Global: 100% to 15%, 50% to 35%, 25% to 50%, 10% beyond.
|
|
("Global", 0.15, 15_000_000.00),
|
|
("Global", 0.35, 15_000_000.00 + 10_000_000.00),
|
|
("Global", 0.50, 25_000_000.00 + 3_750_000.00),
|
|
("Global", 0.60, 28_750_000.00 + 1_000_000.00),
|
|
# Professional savings: 60% to 10%, 35% to 15%, 15% to 20%, 5% beyond.
|
|
("Professional", 0.10, 6_000_000.00),
|
|
("Professional", 0.15, 6_000_000.00 + 1_750_000.00),
|
|
("Professional", 0.20, 7_750_000.00 + 750_000.00),
|
|
("Professional", 0.30, 8_500_000.00 + 500_000.00),
|
|
# Professional losses: corridor 1 shares 50%.
|
|
("Professional", -0.10, -5_000_000.00),
|
|
(
|
|
"Professional",
|
|
-0.30,
|
|
-(5_000_000.00 + 1_750_000.00 + 750_000.00 + 500_000.00),
|
|
),
|
|
("Global", -0.60, -29_750_000.00),
|
|
],
|
|
)
|
|
def test_corridor_boundaries(option, share, expected):
|
|
shared = shared_savings_losses(B * share, B, option)
|
|
assert shared.shared_savings_losses == expected
|
|
assert shared.scenario == ("savings" if share > 0 else "losses")
|
|
assert sum(shared.retained_by_corridor) == pytest.approx(expected)
|
|
|
|
|
|
def test_each_corridor_takes_only_its_band():
|
|
shared = shared_savings_losses(B * 0.60, B, "Global")
|
|
assert shared.retained_by_corridor == (
|
|
15_000_000.0,
|
|
10_000_000.0,
|
|
3_750_000.0,
|
|
1_000_000.0,
|
|
)
|
|
bands = RISK_CORRIDORS[("Global", "savings")]
|
|
assert [corridor_exposure(B * 0.2, B, c.lower, c.upper) for c in bands] == [
|
|
15_000_000.0,
|
|
5_000_000.0,
|
|
0.0,
|
|
0.0,
|
|
]
|
|
|
|
|
|
def test_unknown_risk_option_is_refused():
|
|
with pytest.raises(ValueError, match="unknown risk option"):
|
|
shared_savings_losses(1.0, B, "Hybrid")
|
|
|
|
|
|
def test_shared_losses_are_not_sequestered():
|
|
assert sequestration_amount(-1_000_000.0) == 0.0
|
|
assert sequestration_amount(0.0) == 0.0
|
|
assert sequestration_amount(9_229_682.72) == 184_593.65
|
|
|
|
|
|
def test_loss_scenario_end_to_end():
|
|
# Expenditure 20 percent above a Global benchmark after adjustments.
|
|
lines = settle(
|
|
table_5_6_1(
|
|
"Global",
|
|
non_aco_provider_claims=91_355_457.00
|
|
+ 0.20 * 145_275_000.00
|
|
+ 9_229_682.72,
|
|
)
|
|
)
|
|
assert lines.gross_savings_losses == -29_055_000.00
|
|
# 15% at 100% (21,791,250) + 5% at 50% (3,631,875).
|
|
assert lines.retained_corridor_1 == -21_791_250.00
|
|
assert lines.retained_corridor_2 == -3_631_875.00
|
|
assert lines.shared_savings_losses == -25_423_125.00
|
|
assert lines.sequestration_amount == 0.0
|
|
assert lines.shared_savings_losses_after_sequestration == -25_423_125.00
|
|
|
|
|
|
def test_break_even_shares_nothing():
|
|
assert gross_savings_losses(100.0, 100.0) == 0.0
|
|
shared = shared_savings_losses(0.0, B, "Professional")
|
|
assert (shared.scenario, shared.shared_savings_losses) == ("savings", 0.0)
|
|
|
|
|
|
# ── Stop-Loss elections and input completeness ───────────────────────────
|
|
|
|
|
|
def test_no_stop_loss_election_leaves_expenditure_unchanged():
|
|
lines = settle(
|
|
table_5_6_1(
|
|
"Global",
|
|
stop_loss_elected=False,
|
|
stop_loss_charge=None,
|
|
stop_loss_payout=None,
|
|
stop_loss_neutrality_factor=None,
|
|
)
|
|
)
|
|
assert (lines.stop_loss_charge, lines.adjusted_stop_loss_payout) == (0.0, 0.0)
|
|
assert lines.stop_loss_neutrality_factor is None
|
|
assert lines.net_stop_loss_impact == 0.0
|
|
assert (
|
|
lines.py_expenditure_after_stop_loss == lines.py_expenditure == 135_793_983.00
|
|
)
|
|
assert lines.gross_savings_losses == 145_275_000.00 - 135_793_983.00
|
|
|
|
|
|
def test_zero_payout_needs_no_neutrality_factor():
|
|
lines = settle(
|
|
table_5_6_1("Global", stop_loss_payout=0.0, stop_loss_neutrality_factor=None)
|
|
)
|
|
assert lines.adjusted_stop_loss_payout == 0.0
|
|
assert lines.net_stop_loss_impact == 2_948_334.28
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"changes,message",
|
|
[
|
|
({"spending_designation": None}, "spending designation"),
|
|
({"performance_year": 2026}, "outside the LEAD model"),
|
|
({"stop_loss_charge": None}, "charge and payout are required"),
|
|
({"stop_loss_neutrality_factor": None}, "needs the neutrality factor"),
|
|
({"stop_loss_elected": False}, "not elected"),
|
|
({"quality_score": 1.5}, "less than or equal to 1"),
|
|
],
|
|
)
|
|
def test_incomplete_scenarios_are_refused(changes, message):
|
|
with pytest.raises(ValidationError, match=message):
|
|
table_5_6_1("Global", **changes)
|
|
|
|
|
|
def test_professional_needs_no_designation():
|
|
lines = settle(table_5_6_1("Professional", spending_designation=None))
|
|
assert (
|
|
lines.shared_savings_losses_after_sequestration
|
|
== TABLE_5_6_1["Professional"]["28"]
|
|
)
|
|
|
|
|
|
def test_settlement_lines_are_frozen():
|
|
lines = settle(table_5_6_1("Global"))
|
|
with pytest.raises(AttributeError):
|
|
lines.py_benchmark = 0.0 # type: ignore[misc]
|
|
assert replace(lines, py_benchmark=1.0).py_benchmark == 1.0
|