feat(lead): LEAD PY2027 financial methodology — settlement and benchmark engines
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
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.
This commit is contained in:
@@ -71,6 +71,10 @@ llm = [
|
||||
ccw = [
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
lead = [
|
||||
"stack[conf]",
|
||||
"narwhals>=2.17.0",
|
||||
]
|
||||
cli = [
|
||||
"stack[aco]",
|
||||
"stack[api]",
|
||||
@@ -180,6 +184,7 @@ all = [
|
||||
"stack[rex]",
|
||||
"stack[lake]",
|
||||
"stack[nlm]",
|
||||
"stack[lead]",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
@@ -247,6 +252,6 @@ link-mode = "copy"
|
||||
python-preference = "managed"
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "nlm", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
|
||||
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "lead", "llm", "mail", "nlm", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
|
||||
namespace = true
|
||||
source-exclude = ["compose.yml","infra/**","data/**","notebooks/**","tuva/**","assets/**","docs/**","dev/**","bundle/**","cloud/**"]
|
||||
|
||||
37
src/lead/__init__.py
Normal file
37
src/lead/__init__.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""LEAD Model PY 2027 financial methodology — CMS's published rules.
|
||||
|
||||
The Long-term Enhanced ACO Design (LEAD) Model is CMS Innovation Center's
|
||||
ACO model starting in PY 2027. This package ports the arithmetic of its
|
||||
public methodology paper — "LEAD Model PY 2027 Alignment and Financial
|
||||
Methodology Paper" v1.2, August 27, 2026 (cms.gov/priorities/innovation/
|
||||
files/lead-alignment-finance-paper.pdf; bib item ``E4PEU3HV``) — as
|
||||
generic engines. Parameters come from the paper (:mod:`lead.params`);
|
||||
everything CMS computes per ACO or per year comes from the caller. It holds
|
||||
no participant data and no organization-specific assumptions.
|
||||
|
||||
Modules:
|
||||
|
||||
- :mod:`lead.params` — Table 5.2.1 discounts, Quality Withhold,
|
||||
retention incentive, Stop-Loss bands, risk corridors, sequestration,
|
||||
base-year weights, ACPT guardrails, risk-score caps, REA/PSA shares and
|
||||
caps, voluntary guardrail and minimum, Table 2.6.1 base-year minimums.
|
||||
- :mod:`lead.settlement` — Final Financial Settlement, Table 5.6.1 line by
|
||||
line (:func:`lead.settlement.settle`).
|
||||
- :mod:`lead.stop_loss` — Stop-Loss Reinsurance (Box 5.3.3).
|
||||
- :mod:`lead.monies_owed` — Total Monies Owed (Table 5.5.1).
|
||||
- :mod:`lead.timing` — run-out, settlement basis, settlement-stage inputs.
|
||||
- :mod:`lead.benchmark` — §3 benchmark formulas.
|
||||
- :mod:`lead.express` — the same as narwhals expressions, so a frame of
|
||||
scenarios settles at once on polars, DuckDB, or any narwhals backend.
|
||||
|
||||
Conventions:
|
||||
|
||||
- Dollar lines are rounded to the cent as they are produced
|
||||
(:func:`lead.money.cents`, half away from zero); PBPMs, rates and
|
||||
factors never are. That 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 to the cent.
|
||||
- A missing input stays missing (None/null, or a refused input model),
|
||||
never a silent zero; the zeros that appear are ones the paper defines.
|
||||
- Docstrings cite "LEAD PY2027 methodology (bib E4PEU3HV) §x.y / Table
|
||||
x.y.z".
|
||||
"""
|
||||
652
src/lead/benchmark.py
Normal file
652
src/lead/benchmark.py
Normal file
@@ -0,0 +1,652 @@
|
||||
"""LEAD Performance Year Benchmark formulas (§3), as published.
|
||||
|
||||
The order: Historical Baseline Expenditure PBPMs are risk standardized
|
||||
(§3.3.1), trended to BY 3 with the national-regional blend (§3.3.2) and
|
||||
weighted by base year (§3.3.3–3.3.4) into the Historical Benchmark; the
|
||||
voluntary guardrail applies (§3.2.5); a Regional Efficiency Adjustment or
|
||||
Prior Savings Adjustment is added (§3.4); the Adjusted Historical
|
||||
Benchmark is trended with the three-way blend (§3.5.1), risk adjusted with
|
||||
the growth-capped PY risk score (§3.5.2), multiplied out over full-year and
|
||||
hybrid-aligned months (§3.5.3), and the categories summed (§3.5.4).
|
||||
|
||||
Only the arithmetic CMS publishes is here. Every data-derived input —
|
||||
expenditures, risk scores, months, ACPT, modified USPCC amounts, hybrid
|
||||
factors, CMS's alignment-standard and SSP-TIN determinations — is a caller
|
||||
input. PBPMs, rates and factors are never rounded; dollar totals are
|
||||
rounded to the cent (:func:`lead.money.cents`), which is what reproduces
|
||||
Table 3.5.4 line 24.
|
||||
|
||||
Missing inputs give ``None``, never a stand-in value: a growth rate,
|
||||
market share or PBPM without a positive denominator is None, and so is
|
||||
anything computed from it.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lead.money import cents
|
||||
from lead.params import (
|
||||
ACPT_GUARDRAILS,
|
||||
ACPT_WEIGHT,
|
||||
BASE_YEAR_ALIGNMENT_MINIMUMS,
|
||||
BASE_YEAR_WEIGHTS,
|
||||
BASE_YEARS,
|
||||
BENCHMARK_ADJUSTMENT_CAP_RATE,
|
||||
GLOBAL,
|
||||
HIGHER_SPENDING,
|
||||
LOWER_SPENDING,
|
||||
NEWLY_ENTERING,
|
||||
PRIOR_SAVINGS_PRORATION_CAP,
|
||||
PRIOR_SAVINGS_SHARE,
|
||||
REDUCED_BENCHMARK_ADJUSTMENT_CAP_RATE,
|
||||
REGIONAL_EFFICIENCY_SHARE,
|
||||
RISK_SCORE_GROWTH_CAPS,
|
||||
VOLUNTARY_BENCHMARK_MINIMUM,
|
||||
VOLUNTARY_GUARDRAIL_RATE,
|
||||
AlignmentMinimumType,
|
||||
BeneficiaryCategory,
|
||||
EntryType,
|
||||
RiskOption,
|
||||
SpendingDesignation,
|
||||
model_year,
|
||||
)
|
||||
|
||||
# ── Shared arithmetic ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def ratio(numerator: float | None, denominator: float | None) -> float | None:
|
||||
"""Return numerator ÷ denominator, or None unless both are known and
|
||||
the denominator is positive.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3 (every ratio it
|
||||
defines has a positive denominator).
|
||||
"""
|
||||
if numerator is None or denominator is None or denominator <= 0:
|
||||
return None
|
||||
return numerator / denominator
|
||||
|
||||
|
||||
def month_weighted_average(
|
||||
values_and_months: Iterable[tuple[float | None, float | None]],
|
||||
) -> float | None:
|
||||
"""Return the beneficiary month-weighted average of the values.
|
||||
|
||||
Pairs with a missing value or no positive months carry no weight; with
|
||||
no weight at all the result is None. This is the paper's average risk
|
||||
score, its claims/voluntary PBPM combination, its weighted REA for
|
||||
excluded TINs and Box 5.3.3's month-weighted Stop-Loss values.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.1 / §3.5.2 /
|
||||
§3.5.4 / Table 3.4.3.6 / Box 5.3.3.
|
||||
"""
|
||||
weighted = 0.0
|
||||
months = 0.0
|
||||
for value, weight in values_and_months:
|
||||
if value is None or weight is None or weight <= 0:
|
||||
continue
|
||||
weighted += value * weight
|
||||
months += weight
|
||||
return ratio(weighted, months)
|
||||
|
||||
|
||||
# ── §3.3 Risk standardize, trend, weight ──────────────────────────────────
|
||||
|
||||
|
||||
def expenditure_pbpm(total_expenditure: float, eligible_months: float) -> float | None:
|
||||
"""Return expenditure per beneficiary per month (Table 3.3.3 line 3).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3 / Table 3.3.3 line 3.
|
||||
"""
|
||||
return ratio(total_expenditure, eligible_months)
|
||||
|
||||
|
||||
def risk_standardize(
|
||||
pbpm: float | None, average_risk_score: float | None
|
||||
) -> float | None:
|
||||
"""Divide a PBPM by the same population's average risk score for the
|
||||
same year (Table 3.3.3 line 5).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.1 / Table 3.3.3 line 5.
|
||||
"""
|
||||
return ratio(pbpm, average_risk_score)
|
||||
|
||||
|
||||
def growth_rate(later: float | None, earlier: float | None) -> float | None:
|
||||
"""Return a growth rate later ÷ earlier: BY 3 ÷ BY n FFS expenditures,
|
||||
national (Step 1) or regional (Step 2). None unless both are positive.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Steps 1–2.
|
||||
"""
|
||||
if later is None or later <= 0:
|
||||
return None
|
||||
return ratio(later, earlier)
|
||||
|
||||
|
||||
def national_trend_weight(
|
||||
counties: Sequence[tuple[float, float]],
|
||||
) -> float | None:
|
||||
"""Return the weight on the national growth rate: the ACO's
|
||||
county-weighted average market share.
|
||||
|
||||
Each county is (ACO-aligned months, alignment-eligible months). Market
|
||||
share = aligned ÷ eligible; county weight = aligned ÷ ACO total aligned;
|
||||
the national weight is Σ share × weight. None without aligned months or
|
||||
when a county has no eligible months.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Step 3.
|
||||
"""
|
||||
total_aligned = sum(aligned for aligned, _ in counties)
|
||||
weight_sum = 0.0
|
||||
for aligned, eligible in counties:
|
||||
share = ratio(aligned, eligible)
|
||||
weight = ratio(aligned, total_aligned)
|
||||
if share is None or weight is None:
|
||||
return None
|
||||
weight_sum += share * weight
|
||||
return weight_sum if counties else None
|
||||
|
||||
|
||||
def two_way_blended_trend(
|
||||
national_growth: float | None,
|
||||
regional_growth: float | None,
|
||||
national_weight: float | None,
|
||||
) -> float | None:
|
||||
"""Blend the growth rates: national × weight + regional × (1 − weight).
|
||||
|
||||
Box 3.3.2's example gives 1.120 × 18% + 1.100 × 82% = 1.1036 (printed
|
||||
1.104; its next sentence says 1.103, a slip in the paper).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Step 4.
|
||||
"""
|
||||
if national_growth is None or regional_growth is None or national_weight is None:
|
||||
return None
|
||||
return national_growth * national_weight + regional_growth * (1.0 - national_weight)
|
||||
|
||||
|
||||
def base_year_weights(
|
||||
entry_type: EntryType, sufficient_base_years: Iterable[str]
|
||||
) -> dict[str, float]:
|
||||
"""Return the weight of each sufficient base year.
|
||||
|
||||
Three sufficient years: 10/30/60 (Newly Entering) or equal thirds
|
||||
(Renewing). Two: two-thirds recent / one-third older (Newly Entering)
|
||||
or equal halves (Renewing). One: 100 percent. None sufficient: an empty
|
||||
mapping — no Historical Benchmark can be built.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.3 / §3.3.4.
|
||||
"""
|
||||
if entry_type not in BASE_YEAR_WEIGHTS:
|
||||
raise ValueError(f"unknown entry type {entry_type!r}")
|
||||
sufficient = set(sufficient_base_years)
|
||||
unknown = sufficient - set(BASE_YEARS)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown base years {sorted(unknown)}")
|
||||
years = [year for year in BASE_YEARS if year in sufficient]
|
||||
if len(years) == 3:
|
||||
return dict(BASE_YEAR_WEIGHTS[entry_type])
|
||||
if len(years) == 2:
|
||||
older, recent = years
|
||||
if entry_type == NEWLY_ENTERING:
|
||||
return {older: 1 / 3, recent: 2 / 3}
|
||||
return {older: 0.5, recent: 0.5}
|
||||
return {year: 1.0 for year in years}
|
||||
|
||||
|
||||
def base_year_alignment_minimum(
|
||||
standard: AlignmentMinimumType, performance_year: int
|
||||
) -> int:
|
||||
"""Return the claims-based alignment minimum a base year must meet, for
|
||||
the alignment standard CMS assigned the ACO and the PY.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §2.6 / Table 2.6.1.
|
||||
"""
|
||||
return BASE_YEAR_ALIGNMENT_MINIMUMS[standard][model_year(performance_year) - 1]
|
||||
|
||||
|
||||
def base_year_is_sufficient(
|
||||
claims_aligned_beneficiaries: int,
|
||||
standard: AlignmentMinimumType,
|
||||
performance_year: int,
|
||||
) -> bool:
|
||||
"""Whether a base year has sufficient claims history: ACO-level
|
||||
claims-based alignment meets or exceeds the Table 2.6.1 minimum.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.4.
|
||||
"""
|
||||
return claims_aligned_beneficiaries >= base_year_alignment_minimum(
|
||||
standard, performance_year
|
||||
)
|
||||
|
||||
|
||||
def historical_benchmark(
|
||||
risk_standardized_pbpm: Mapping[str, float],
|
||||
trend_to_by3: Mapping[str, float],
|
||||
weights: Mapping[str, float],
|
||||
) -> float | None:
|
||||
"""Return the Historical Benchmark PBPM: Σ risk-standardized PBPM ×
|
||||
trend to BY 3 × base-year weight over the weighted base years.
|
||||
|
||||
BY 3's trend factor is 1.000. None when a weighted year lacks its PBPM
|
||||
or trend, or when no year is weighted.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.3 / Table 3.3.3
|
||||
lines 5–9.
|
||||
"""
|
||||
if not weights:
|
||||
return None
|
||||
total = 0.0
|
||||
for year, weight in weights.items():
|
||||
pbpm = risk_standardized_pbpm.get(year)
|
||||
trend = trend_to_by3.get(year)
|
||||
if pbpm is None or trend is None:
|
||||
return None
|
||||
total += pbpm * trend * weight
|
||||
return total
|
||||
|
||||
|
||||
# ── §3.2.5 Voluntarily aligned benchmarks ─────────────────────────────────
|
||||
|
||||
WITHIN_GUARDRAIL = "within_voluntary_guardrail"
|
||||
CAPPED_AT_UPPER = "capped_at_110_percent_of_claims_aligned"
|
||||
RAISED_TO_LOWER = "raised_to_90_percent_of_claims_aligned"
|
||||
|
||||
|
||||
def voluntary_guardrail(
|
||||
voluntary_historical_pbpm: float,
|
||||
claims_historical_pbpm: float,
|
||||
rate: float = VOLUNTARY_GUARDRAIL_RATE,
|
||||
) -> tuple[float, str]:
|
||||
"""Hold the voluntarily aligned Historical Benchmark within ±10 percent
|
||||
of the claims-aligned one for the same category, after base-year
|
||||
blending and before any REA or PSA. Returns (benchmark, status).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5 ($1,000 claims →
|
||||
$900–$1,100).
|
||||
"""
|
||||
lower = claims_historical_pbpm * (1.0 - rate)
|
||||
upper = claims_historical_pbpm * (1.0 + rate)
|
||||
if voluntary_historical_pbpm > upper:
|
||||
return upper, CAPPED_AT_UPPER
|
||||
if voluntary_historical_pbpm < lower:
|
||||
return lower, RAISED_TO_LOWER
|
||||
return voluntary_historical_pbpm, WITHIN_GUARDRAIL
|
||||
|
||||
|
||||
def contributes_to_voluntary_benchmark(
|
||||
category: BeneficiaryCategory, by3_category: BeneficiaryCategory | None
|
||||
) -> bool:
|
||||
"""Whether a voluntarily aligned beneficiary contributes base-year
|
||||
expenditures (and counts toward the 500 minimum): every A&D and ESRD
|
||||
beneficiary, and a High Needs one only if High Needs in BY 3.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
return category != "High Needs" or by3_category == "High Needs"
|
||||
|
||||
|
||||
def voluntary_benchmark_minimum_met(contributing_beneficiaries: int) -> bool:
|
||||
"""Whether at least 500 contributing voluntarily aligned beneficiaries
|
||||
exist; otherwise the claims-aligned benchmarks apply to them.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
return contributing_beneficiaries >= VOLUNTARY_BENCHMARK_MINIMUM
|
||||
|
||||
|
||||
# ── §3.4 ACO-specific benchmark adjustments ───────────────────────────────
|
||||
|
||||
|
||||
def regional_ffs_expenditure(
|
||||
counties: Sequence[tuple[float | None, float | None, float]],
|
||||
) -> float | None:
|
||||
"""Return regional FFS expenditure: Σ county weight × risk-standardized
|
||||
county FFS PBPM.
|
||||
|
||||
Each county is (county PBPM FFS expenditure, county average risk score,
|
||||
ACO-aligned months in the county); the weight is the county's share of
|
||||
the ACO's aligned months. None if any county lacks a PBPM or score, or
|
||||
the ACO has no aligned months.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.1.
|
||||
"""
|
||||
standardized = [risk_standardize(pbpm, risk) for pbpm, risk, _ in counties]
|
||||
if None in standardized:
|
||||
return None
|
||||
return month_weighted_average(
|
||||
(value, months)
|
||||
for value, (_, _, months) in zip(standardized, counties, strict=True)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SpendingDesignationResult:
|
||||
"""The §3.4.2 ACO-level spending difference and the designation."""
|
||||
|
||||
weighted_difference: float
|
||||
designation: SpendingDesignation | None
|
||||
|
||||
|
||||
def spending_designation(
|
||||
categories: Sequence[tuple[float, float, float]],
|
||||
) -> SpendingDesignationResult:
|
||||
"""Designate the ACO higher or lower spending.
|
||||
|
||||
Each category is (BY 3 regional FFS PBPM, Historical Benchmark PBPM,
|
||||
BY 3 aligned months). The ACO-level difference is Σ (regional −
|
||||
historical) × the category's share of BY 3 aligned months: positive is
|
||||
lower spending, negative higher. The paper names no tie, so an exact
|
||||
zero is left undetermined (``designation=None``).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.2 / Table 3.4.2.
|
||||
"""
|
||||
total_months = sum(months for _, _, months in categories)
|
||||
if total_months <= 0:
|
||||
raise ValueError("the spending designation needs BY 3 aligned months")
|
||||
difference = sum(
|
||||
(regional - historical) * months / total_months
|
||||
for regional, historical, months in categories
|
||||
)
|
||||
designation: SpendingDesignation | None = (
|
||||
LOWER_SPENDING
|
||||
if difference > 0
|
||||
else HIGHER_SPENDING
|
||||
if difference < 0
|
||||
else None
|
||||
)
|
||||
return SpendingDesignationResult(difference, designation)
|
||||
|
||||
|
||||
def benchmark_adjustment_cap_rate(reduced: bool) -> float:
|
||||
"""Return the benchmark adjustment cap rate: 5 percent, or 3 percent
|
||||
when CMS finds a lower-spending ACO has more than 40 percent of its
|
||||
Participant TINs from a Shared Savings Program ACO (``reduced``).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4 / §3.4.3.
|
||||
"""
|
||||
return (
|
||||
REDUCED_BENCHMARK_ADJUSTMENT_CAP_RATE
|
||||
if reduced
|
||||
else BENCHMARK_ADJUSTMENT_CAP_RATE
|
||||
)
|
||||
|
||||
|
||||
def benchmark_adjustment_cap(
|
||||
uspcc_amount: float, py_average_risk_score: float, cap_rate: float
|
||||
) -> float:
|
||||
"""Return the risk-adjusted cap: modified-USPCC amount × PY average risk
|
||||
score × cap rate ($3,500 × 1.030 × 5% = $180.25).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Table 3.4.3.3.
|
||||
"""
|
||||
return uspcc_amount * py_average_risk_score * cap_rate
|
||||
|
||||
|
||||
def regional_efficiency_eligible(
|
||||
spending: SpendingDesignation, risk_option: RiskOption
|
||||
) -> bool:
|
||||
"""Whether the Regional Efficiency Adjustment applies: lower-spending
|
||||
ACOs in the Global Risk track only.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3.
|
||||
"""
|
||||
return spending == LOWER_SPENDING and risk_option == GLOBAL
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegionalEfficiencyAdjustment:
|
||||
"""Tables 3.4.3.1 and 3.4.3.3 for one category and alignment type."""
|
||||
|
||||
difference: float
|
||||
uncapped: float
|
||||
cap: float | None
|
||||
applied: float | None
|
||||
|
||||
|
||||
def regional_efficiency_adjustment(
|
||||
regional_ffs_pbpm: float,
|
||||
historical_benchmark_pbpm: float,
|
||||
*,
|
||||
eligible: bool,
|
||||
uspcc_amount: float | None,
|
||||
py_average_risk_score: float | None,
|
||||
cap_rate: float,
|
||||
) -> RegionalEfficiencyAdjustment:
|
||||
"""Return the REA for one category and alignment type.
|
||||
|
||||
Uncapped = 50 percent of the positive regional − historical difference;
|
||||
applied = the lesser of it and the cap. An ineligible ACO, or one at or
|
||||
above regional spending, gets zero. A positive uncapped REA whose cap
|
||||
inputs are missing is None — never the uncapped amount.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Tables
|
||||
3.4.3.1–3.4.3.4.
|
||||
"""
|
||||
difference = regional_ffs_pbpm - historical_benchmark_pbpm
|
||||
uncapped = max(0.0, difference) * REGIONAL_EFFICIENCY_SHARE
|
||||
cap = (
|
||||
None
|
||||
if uspcc_amount is None or py_average_risk_score is None
|
||||
else benchmark_adjustment_cap(uspcc_amount, py_average_risk_score, cap_rate)
|
||||
)
|
||||
if not eligible or uncapped == 0.0:
|
||||
applied: float | None = 0.0
|
||||
else:
|
||||
applied = None if cap is None else min(uncapped, cap)
|
||||
return RegionalEfficiencyAdjustment(difference, uncapped, cap, applied)
|
||||
|
||||
|
||||
def prior_savings_pbpm(
|
||||
py_benchmark: float, py_expenditure: float, aligned_months: float
|
||||
) -> float | None:
|
||||
"""Return a prior year's gross savings (losses) PBPM: (benchmark −
|
||||
expenditure) ÷ aligned beneficiary months.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.4 / Table 3.4.3.7.
|
||||
"""
|
||||
return ratio(py_benchmark - py_expenditure, aligned_months)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PriorSavingsAdjustment:
|
||||
"""§3.4.4 Examples 1–3."""
|
||||
|
||||
average_savings_pbpm: float
|
||||
eligible: bool
|
||||
proration_factor: float
|
||||
prorated_average_savings: float
|
||||
uncapped: float
|
||||
cap: float | None
|
||||
applied: float | None
|
||||
|
||||
|
||||
def prior_savings_adjustment(
|
||||
average_savings_pbpm: float,
|
||||
predecessor_months: float,
|
||||
py_months: float,
|
||||
cap: float | None,
|
||||
) -> PriorSavingsAdjustment:
|
||||
"""Return the Prior Savings Adjustment PBPM.
|
||||
|
||||
Eligible only when the average PBPM savings is positive. Proration =
|
||||
predecessor average months ÷ PY months, capped at 100 percent;
|
||||
uncapped = 50 percent of the prorated average; applied = the lesser of
|
||||
it and the ACO-level cap (None when an eligible ACO's cap is missing).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.4 (Examples 1–3).
|
||||
"""
|
||||
if py_months <= 0:
|
||||
raise ValueError("the proration factor needs PY beneficiary months")
|
||||
eligible = average_savings_pbpm > 0
|
||||
proration = min(predecessor_months / py_months, PRIOR_SAVINGS_PRORATION_CAP)
|
||||
prorated = average_savings_pbpm * proration
|
||||
uncapped = prorated * PRIOR_SAVINGS_SHARE if eligible else 0.0
|
||||
if not eligible:
|
||||
applied: float | None = 0.0
|
||||
else:
|
||||
applied = None if cap is None else min(uncapped, cap)
|
||||
return PriorSavingsAdjustment(
|
||||
average_savings_pbpm, eligible, proration, prorated, uncapped, cap, applied
|
||||
)
|
||||
|
||||
|
||||
def prior_savings_cap(
|
||||
subpopulations: Sequence[tuple[float, float, float]], cap_rate: float
|
||||
) -> float:
|
||||
"""Return the ACO-level Prior Savings Adjustment cap: Σ sub-population
|
||||
cap × its share of PY alignment, each sub-population being (share,
|
||||
USPCC-based amount, PY average risk score).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.4 / Table 3.4.3.9.
|
||||
"""
|
||||
return sum(
|
||||
share * benchmark_adjustment_cap(uspcc, risk, cap_rate)
|
||||
for share, uspcc, risk in subpopulations
|
||||
)
|
||||
|
||||
|
||||
# ── §3.5 Update to the Performance Year ───────────────────────────────────
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThreeWayTrend:
|
||||
"""§3.5.1: the raw three-way factor, its guardrail band and the factor
|
||||
applied."""
|
||||
|
||||
raw: float
|
||||
lower_bound: float
|
||||
upper_bound: float
|
||||
applied: float
|
||||
|
||||
|
||||
def three_way_blended_trend(
|
||||
two_way_trend: float, acpt: float, performance_year: int
|
||||
) -> ThreeWayTrend:
|
||||
"""Blend two-thirds two-way trend and one-third ACPT, then hold the
|
||||
result within the PY's guardrail around the two-way trend.
|
||||
|
||||
Both factors are cumulative from BY 3 to the PY.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.1 / Table 3.5.1.
|
||||
"""
|
||||
raw = (1.0 - ACPT_WEIGHT) * two_way_trend + ACPT_WEIGHT * acpt
|
||||
low, high = ACPT_GUARDRAILS[model_year(performance_year)]
|
||||
lower, upper = two_way_trend + low, two_way_trend + high
|
||||
return ThreeWayTrend(raw, lower, upper, min(max(raw, lower), upper))
|
||||
|
||||
|
||||
def cumulative_acpt(annual_rates: Sequence[float | None]) -> float | None:
|
||||
"""Return the cumulative BY 3 → PY ACPT factor: Π (1 + each year's
|
||||
ACPT). Each year keeps its original prospective value even when a
|
||||
guardrail bound an earlier year. One missing year leaves it None.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.1 (ACPT Guardrail
|
||||
Policy).
|
||||
"""
|
||||
if not annual_rates:
|
||||
return None
|
||||
factor = 1.0
|
||||
for rate in annual_rates:
|
||||
if rate is None:
|
||||
return None
|
||||
factor *= 1.0 + rate
|
||||
return factor
|
||||
|
||||
|
||||
WITHIN_CAP = "within_risk_score_growth_cap"
|
||||
CAPPED = "capped_at_risk_score_growth_cap"
|
||||
FLOORED = "raised_to_risk_score_growth_floor"
|
||||
|
||||
|
||||
def capped_risk_score(
|
||||
by3_risk_score: float, py_risk_score: float, category: BeneficiaryCategory
|
||||
) -> tuple[float, str]:
|
||||
"""Hold the PY average risk score within BY 3 × (1 ± cap): 3 percent
|
||||
for A&D and ESRD, 4 percent for High Needs. Returns (score, status).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.2 (Risk Score
|
||||
Growth Cap).
|
||||
"""
|
||||
cap = RISK_SCORE_GROWTH_CAPS[category]
|
||||
upper = by3_risk_score * (1.0 + cap)
|
||||
lower = by3_risk_score * (1.0 - cap)
|
||||
if py_risk_score > upper:
|
||||
return upper, CAPPED
|
||||
if py_risk_score < lower:
|
||||
return lower, FLOORED
|
||||
return py_risk_score, WITHIN_CAP
|
||||
|
||||
|
||||
def performance_year_benchmark_pbpm(
|
||||
historical_benchmark_pbpm: float,
|
||||
benchmark_adjustment: float | None,
|
||||
trend_factor: float,
|
||||
capped_py_risk_score: float,
|
||||
) -> float | None:
|
||||
"""Return the PY Benchmark PBPM: (Historical Benchmark + REA or PSA) ×
|
||||
three-way trend × capped PY risk score ($1,100 × 1.030 = $1,133).
|
||||
|
||||
A missing adjustment (None, e.g. an uncomputable cap) stays missing.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5 / §3.5.2.
|
||||
"""
|
||||
if benchmark_adjustment is None:
|
||||
return None
|
||||
adjusted = historical_benchmark_pbpm + benchmark_adjustment
|
||||
return adjusted * trend_factor * capped_py_risk_score
|
||||
|
||||
|
||||
def hybrid_adjustment_factor(
|
||||
partial_period_pbpm: float, full_year_pbpm: float
|
||||
) -> float | None:
|
||||
"""Return a hybrid alignment adjustment factor: average PBPM over the
|
||||
partial-year period ÷ average full-year PBPM.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.3 / Table 3.5.3.1.
|
||||
"""
|
||||
return ratio(partial_period_pbpm, full_year_pbpm)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BenchmarkDollars:
|
||||
"""Table 3.5.3.2 lines 6–8 for one category and alignment type."""
|
||||
|
||||
full_year: float
|
||||
hybrid: float
|
||||
total: float
|
||||
|
||||
|
||||
def benchmark_dollars(
|
||||
py_benchmark_pbpm: float,
|
||||
full_year_months: float,
|
||||
hybrid_months_and_factors: Sequence[tuple[float, float]] = (),
|
||||
) -> BenchmarkDollars:
|
||||
"""Multiply a PY Benchmark PBPM out to dollars over full-year months
|
||||
and hybrid-aligned months at their adjustment factors.
|
||||
|
||||
``total`` = cents(PBPM × [full months + Σ hybrid months × factor]), one
|
||||
product as Table 3.5.4 lines 8 and 22 state it; ``full_year`` and
|
||||
``hybrid`` are the Table 3.5.3.2 lines 6 and 7 components.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.3 / Table 3.5.3.2 /
|
||||
Table 3.5.4 lines 8 and 22.
|
||||
"""
|
||||
hybrid_months = sum(months * factor for months, factor in hybrid_months_and_factors)
|
||||
return BenchmarkDollars(
|
||||
full_year=cents(py_benchmark_pbpm * full_year_months),
|
||||
hybrid=cents(py_benchmark_pbpm * hybrid_months),
|
||||
total=cents(py_benchmark_pbpm * (full_year_months + hybrid_months)),
|
||||
)
|
||||
|
||||
|
||||
def total_benchmark(category_dollars: Iterable[float]) -> float:
|
||||
"""Sum aggregate benchmark dollars (claims + voluntary per category,
|
||||
then A&D + ESRD + High Needs) into the PY Benchmark.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.4 / Table 3.5.4
|
||||
line 24.
|
||||
"""
|
||||
return cents(sum(category_dollars))
|
||||
62
src/lead/express/__init__.py
Normal file
62
src/lead/express/__init__.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""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)
|
||||
350
src/lead/express/benchmark.py
Normal file
350
src/lead/express/benchmark.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""Performance Year Benchmark formulas (§3) as narwhals expressions.
|
||||
|
||||
Row-wise builders for a frame with one row per benchmark cell (category ×
|
||||
alignment type, county, base year…), plus :func:`month_weighted_mean` for
|
||||
``group_by(...).agg(...)``. Scalar twins: ``lead.benchmark``. A null or
|
||||
non-positive denominator gives null, never a stand-in value.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import narwhals as nw
|
||||
|
||||
from lead.benchmark import (
|
||||
CAPPED,
|
||||
CAPPED_AT_UPPER,
|
||||
FLOORED,
|
||||
RAISED_TO_LOWER,
|
||||
WITHIN_CAP,
|
||||
WITHIN_GUARDRAIL,
|
||||
)
|
||||
from lead.express import IntoExpr, cents, col, lit_or_col, ratio
|
||||
from lead.params import (
|
||||
ACPT_GUARDRAILS,
|
||||
ACPT_WEIGHT,
|
||||
FIRST_PERFORMANCE_YEAR,
|
||||
GLOBAL,
|
||||
HIGH_NEEDS,
|
||||
LOWER_SPENDING,
|
||||
REGIONAL_EFFICIENCY_SHARE,
|
||||
RISK_SCORE_GROWTH_CAPS,
|
||||
VOLUNTARY_GUARDRAIL_RATE,
|
||||
)
|
||||
|
||||
RISK_CAP_REFERENCE_MISSING = "risk_growth_cap_reference_missing"
|
||||
_NULL = nw.lit(None, dtype=nw.Float64())
|
||||
|
||||
# ── §3.3 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def risk_standardized_pbpm(
|
||||
expenditure: IntoExpr, eligible_months: IntoExpr, average_risk_score: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Expenditure ÷ eligible months ÷ average risk score (Table 3.3.3
|
||||
lines 3 and 5); null without positive months and score.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.1 / Table 3.3.3.
|
||||
"""
|
||||
return ratio(ratio(expenditure, eligible_months), average_risk_score)
|
||||
|
||||
|
||||
def growth_rate(later: IntoExpr, earlier: IntoExpr) -> nw.Expr:
|
||||
"""later ÷ earlier FFS expenditures; null unless both are positive.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Steps 1–2.
|
||||
"""
|
||||
return nw.when(col(later) > 0).then(ratio(later, earlier))
|
||||
|
||||
|
||||
def county_market_share(aligned_months: IntoExpr, eligible_months: IntoExpr) -> nw.Expr:
|
||||
"""ACO-aligned ÷ alignment-eligible months in a county.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Step 3.1.
|
||||
"""
|
||||
return ratio(aligned_months, eligible_months)
|
||||
|
||||
|
||||
def county_weight(aligned_months: IntoExpr, aco_aligned_months: IntoExpr) -> nw.Expr:
|
||||
"""A county's share of the ACO's aligned months (trend weights and
|
||||
regional FFS expenditures alike).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Step 3.2 / §3.4.1.
|
||||
"""
|
||||
return ratio(aligned_months, aco_aligned_months)
|
||||
|
||||
|
||||
def two_way_blended_trend(
|
||||
national_growth: IntoExpr, regional_growth: IntoExpr, national_weight: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""national × weight + regional × (1 − weight).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 3.3.2 Step 4.
|
||||
"""
|
||||
weight = col(national_weight)
|
||||
return col(national_growth) * weight + col(regional_growth) * (1.0 - weight)
|
||||
|
||||
|
||||
def trended_weighted_pbpm(
|
||||
risk_standardized: IntoExpr, trend_to_by3: IntoExpr, base_year_weight: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""One base year's contribution to the Historical Benchmark: PBPM ×
|
||||
trend to BY 3 × weight (Table 3.3.3 line 9); sum it over base years.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.3 / Table 3.3.3.
|
||||
"""
|
||||
return col(risk_standardized) * col(trend_to_by3) * col(base_year_weight)
|
||||
|
||||
|
||||
# ── §3.2.5 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def voluntary_guardrail(
|
||||
voluntary_historical_pbpm: IntoExpr,
|
||||
claims_historical_pbpm: IntoExpr,
|
||||
rate: float = VOLUNTARY_GUARDRAIL_RATE,
|
||||
) -> nw.Expr:
|
||||
"""The voluntary Historical Benchmark held within ±10 percent of the
|
||||
claims-aligned one; null when either is null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
claims = col(claims_historical_pbpm)
|
||||
return nw.when(~claims.is_null()).then(
|
||||
col(voluntary_historical_pbpm).clip(
|
||||
lower_bound=claims * (1.0 - rate), upper_bound=claims * (1.0 + rate)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def voluntary_guardrail_status(
|
||||
voluntary_historical_pbpm: IntoExpr,
|
||||
claims_historical_pbpm: IntoExpr,
|
||||
rate: float = VOLUNTARY_GUARDRAIL_RATE,
|
||||
) -> nw.Expr:
|
||||
"""Which side of the guardrail the voluntary benchmark landed on; null
|
||||
when either input is null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
voluntary, claims = col(voluntary_historical_pbpm), col(claims_historical_pbpm)
|
||||
return (
|
||||
nw.when(voluntary.is_null() | claims.is_null())
|
||||
.then(nw.lit(None, dtype=nw.String()))
|
||||
.when(voluntary > claims * (1.0 + rate))
|
||||
.then(nw.lit(CAPPED_AT_UPPER))
|
||||
.when(voluntary < claims * (1.0 - rate))
|
||||
.then(nw.lit(RAISED_TO_LOWER))
|
||||
.otherwise(nw.lit(WITHIN_GUARDRAIL))
|
||||
)
|
||||
|
||||
|
||||
def contributes_to_voluntary_benchmark(
|
||||
category: IntoExpr, by3_category: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""A voluntarily aligned beneficiary contributes unless High Needs in
|
||||
the PY without being High Needs in BY 3 (a null BY 3 category is not
|
||||
High Needs).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
was_high_needs = (col(by3_category) == HIGH_NEEDS).fill_null(False)
|
||||
return (col(category) != HIGH_NEEDS) | was_high_needs
|
||||
|
||||
|
||||
# ── §3.4 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def regional_efficiency_eligible(
|
||||
spending_designation: IntoExpr, risk_option: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Lower-spending ACOs in the Global Risk track.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3.
|
||||
"""
|
||||
return (col(spending_designation) == LOWER_SPENDING) & (col(risk_option) == GLOBAL)
|
||||
|
||||
|
||||
def uncapped_regional_efficiency_adjustment(
|
||||
regional_ffs_pbpm: IntoExpr, historical_benchmark_pbpm: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""50 percent of the positive regional − historical difference.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Table 3.4.3.1.
|
||||
"""
|
||||
difference = col(regional_ffs_pbpm) - col(historical_benchmark_pbpm)
|
||||
return difference.clip(lower_bound=0.0) * REGIONAL_EFFICIENCY_SHARE
|
||||
|
||||
|
||||
def benchmark_adjustment_cap(
|
||||
uspcc_amount: IntoExpr, py_average_risk_score: IntoExpr, cap_rate: IntoExpr | float
|
||||
) -> nw.Expr:
|
||||
"""USPCC-based amount × PY average risk score × cap rate.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Table 3.4.3.3.
|
||||
"""
|
||||
return col(uspcc_amount) * col(py_average_risk_score) * lit_or_col(cap_rate)
|
||||
|
||||
|
||||
def regional_efficiency_adjustment(
|
||||
regional_ffs_pbpm: IntoExpr,
|
||||
historical_benchmark_pbpm: IntoExpr,
|
||||
*,
|
||||
eligible: IntoExpr,
|
||||
uspcc_amount: IntoExpr,
|
||||
py_average_risk_score: IntoExpr,
|
||||
cap_rate: IntoExpr | float,
|
||||
) -> nw.Expr:
|
||||
"""Applied REA: zero when ineligible or not positive, else the lesser
|
||||
of the uncapped REA and the cap; null when a needed input is null (a
|
||||
positive REA never falls back to its uncapped amount).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Tables
|
||||
3.4.3.1–3.4.3.4.
|
||||
"""
|
||||
uncapped = uncapped_regional_efficiency_adjustment(
|
||||
regional_ffs_pbpm, historical_benchmark_pbpm
|
||||
)
|
||||
cap = benchmark_adjustment_cap(uspcc_amount, py_average_risk_score, cap_rate)
|
||||
flag = col(eligible)
|
||||
return (
|
||||
nw.when(uncapped.is_null() | flag.is_null())
|
||||
.then(_NULL)
|
||||
.when(~flag | (uncapped == 0))
|
||||
.then(nw.lit(0.0))
|
||||
.when(cap.is_null())
|
||||
.then(_NULL)
|
||||
.otherwise(nw.min_horizontal(uncapped, cap))
|
||||
)
|
||||
|
||||
|
||||
# ── §3.5 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _guardrail(performance_year: IntoExpr | int, side: int) -> nw.Expr:
|
||||
year = lit_or_col(performance_year)
|
||||
bound = None
|
||||
for model_year, bounds in ACPT_GUARDRAILS.items():
|
||||
condition = year == FIRST_PERFORMANCE_YEAR + model_year - 1
|
||||
bound = (nw.when if bound is None else bound.when)(condition).then(
|
||||
nw.lit(bounds[side])
|
||||
)
|
||||
return bound.otherwise(_NULL)
|
||||
|
||||
|
||||
def three_way_blended_trend(
|
||||
two_way_trend: IntoExpr, acpt: IntoExpr, performance_year: IntoExpr | int
|
||||
) -> nw.Expr:
|
||||
"""Two-thirds two-way trend + one-third ACPT, held within the PY's
|
||||
guardrail around the two-way trend; null for a PY outside the model.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.1 / Table 3.5.1.
|
||||
"""
|
||||
two_way = col(two_way_trend)
|
||||
raw = (1.0 - ACPT_WEIGHT) * two_way + ACPT_WEIGHT * col(acpt)
|
||||
lower = two_way + _guardrail(performance_year, 0)
|
||||
upper = two_way + _guardrail(performance_year, 1)
|
||||
return nw.when(~lower.is_null()).then(
|
||||
raw.clip(lower_bound=lower, upper_bound=upper)
|
||||
)
|
||||
|
||||
|
||||
def risk_score_growth_cap(category: IntoExpr) -> nw.Expr:
|
||||
"""The category's growth cap: 3 percent A&D and ESRD, 4 percent High
|
||||
Needs; null for anything else.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.2.
|
||||
"""
|
||||
return col(category).replace_strict(
|
||||
dict(RISK_SCORE_GROWTH_CAPS), default=None, return_dtype=nw.Float64()
|
||||
)
|
||||
|
||||
|
||||
def capped_risk_score(
|
||||
by3_risk_score: IntoExpr, py_risk_score: IntoExpr, category: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""PY risk score held within BY 3 × (1 ± cap). Null without a BY 3
|
||||
score or a known category: the uncapped score never passes through.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.2.
|
||||
"""
|
||||
by3, cap = col(by3_risk_score), risk_score_growth_cap(category)
|
||||
return nw.when(~by3.is_null() & ~cap.is_null()).then(
|
||||
col(py_risk_score).clip(
|
||||
lower_bound=by3 * (1.0 - cap), upper_bound=by3 * (1.0 + cap)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def risk_growth_cap_status(
|
||||
by3_risk_score: IntoExpr, py_risk_score: IntoExpr, category: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Whether the PY risk score was capped, raised to the floor, kept, or
|
||||
could not be capped for a missing reference.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.2.
|
||||
"""
|
||||
by3, py, cap = (
|
||||
col(by3_risk_score),
|
||||
col(py_risk_score),
|
||||
risk_score_growth_cap(category),
|
||||
)
|
||||
return (
|
||||
nw.when(by3.is_null() | py.is_null() | cap.is_null())
|
||||
.then(nw.lit(RISK_CAP_REFERENCE_MISSING))
|
||||
.when(py > by3 * (1.0 + cap))
|
||||
.then(nw.lit(CAPPED))
|
||||
.when(py < by3 * (1.0 - cap))
|
||||
.then(nw.lit(FLOORED))
|
||||
.otherwise(nw.lit(WITHIN_CAP))
|
||||
)
|
||||
|
||||
|
||||
def performance_year_benchmark_pbpm(
|
||||
historical_benchmark_pbpm: IntoExpr,
|
||||
benchmark_adjustment: IntoExpr,
|
||||
trend_factor: IntoExpr,
|
||||
capped_py_risk_score: IntoExpr,
|
||||
) -> nw.Expr:
|
||||
"""(Historical Benchmark + REA or PSA) × three-way trend × capped PY
|
||||
risk score; a null adjustment stays null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5 / §3.5.2.
|
||||
"""
|
||||
adjusted = col(historical_benchmark_pbpm) + col(benchmark_adjustment)
|
||||
return adjusted * col(trend_factor) * col(capped_py_risk_score)
|
||||
|
||||
|
||||
def benchmark_dollars(
|
||||
py_benchmark_pbpm: IntoExpr,
|
||||
full_year_months: IntoExpr,
|
||||
hybrid_months_and_factors: Sequence[tuple[IntoExpr, IntoExpr]] = (),
|
||||
) -> nw.Expr:
|
||||
"""cents(PBPM × [full-year months + Σ hybrid months × factor]).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.3 / Table 3.5.3.2 /
|
||||
Table 3.5.4 lines 8 and 22.
|
||||
"""
|
||||
hybrid = nw.lit(0.0)
|
||||
for months, factor in hybrid_months_and_factors:
|
||||
hybrid = hybrid + col(months) * col(factor)
|
||||
return cents(col(py_benchmark_pbpm) * (col(full_year_months) + hybrid))
|
||||
|
||||
|
||||
def month_weighted_mean(value: IntoExpr, months: IntoExpr) -> nw.Expr:
|
||||
"""Aggregation: Σ value × months ÷ Σ months, over rows whose value is
|
||||
known and months positive; null without such rows. Use inside
|
||||
``group_by(...).agg(...)`` (average risk scores, the claims/voluntary
|
||||
PBPM combination, regional FFS expenditure).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.1 / §3.4.1 /
|
||||
§3.5.2 / §3.5.4.
|
||||
"""
|
||||
amount, weight = col(value), col(months)
|
||||
counted = ~amount.is_null() & (weight > 0)
|
||||
numerator = nw.when(counted).then(amount * weight).otherwise(nw.lit(0.0)).sum()
|
||||
denominator = nw.when(counted).then(weight).otherwise(nw.lit(0.0)).sum()
|
||||
return ratio(numerator, denominator)
|
||||
290
src/lead/express/settlement.py
Normal file
290
src/lead/express/settlement.py
Normal file
@@ -0,0 +1,290 @@
|
||||
"""Final Financial Settlement (Table 5.6.1) as narwhals expressions.
|
||||
|
||||
:func:`settle` settles a frame of scenarios at once — one row per ACO,
|
||||
risk option or what-if — and returns the same lines as
|
||||
:func:`lead.settlement.settle`, on any narwhals backend. The line builders
|
||||
are public so a caller can assemble a partial chain.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2–§5.6 / Table 5.6.1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import narwhals as nw
|
||||
from narwhals.typing import FrameT
|
||||
|
||||
from lead.express import IntoExpr, cents, col
|
||||
from lead.params import (
|
||||
DISCOUNT_RATES,
|
||||
FIRST_PERFORMANCE_YEAR,
|
||||
GLOBAL,
|
||||
LAST_PERFORMANCE_YEAR,
|
||||
PROFESSIONAL,
|
||||
QUALITY_WITHHOLD_RATE,
|
||||
RETENTION_INCENTIVE_RATE,
|
||||
RISK_CORRIDORS,
|
||||
SEQUESTRATION_RATE,
|
||||
)
|
||||
from lead.settlement import SettlementInputs, SettlementLines
|
||||
|
||||
INPUT_COLUMNS = tuple(SettlementInputs.model_fields)
|
||||
"""Columns :func:`settle` reads — the fields of ``SettlementInputs``."""
|
||||
|
||||
LINE_COLUMNS = tuple(SettlementLines.__dataclass_fields__)
|
||||
"""Columns :func:`settle` writes — the fields of ``SettlementLines``."""
|
||||
|
||||
_NULL = nw.lit(None, dtype=nw.Float64())
|
||||
|
||||
|
||||
def discount_rate(
|
||||
risk_option: IntoExpr, spending_designation: IntoExpr, performance_year: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Look up the benchmark discount rate (Table 5.6.1 line 2).
|
||||
|
||||
Professional: 0.0. Global: Table 5.2.1 by designation and PY. Anything
|
||||
else — an unknown option or designation, a PY outside 2027–2036 — is
|
||||
null, and so is every line computed from it.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.1 / Table 5.2.1.
|
||||
"""
|
||||
option, designation, year = (
|
||||
col(risk_option),
|
||||
col(spending_designation),
|
||||
col(performance_year),
|
||||
)
|
||||
in_model = year.is_between(FIRST_PERFORMANCE_YEAR, LAST_PERFORMANCE_YEAR)
|
||||
rate = nw.when((option == PROFESSIONAL) & in_model).then(nw.lit(0.0))
|
||||
for name, by_year in DISCOUNT_RATES.items():
|
||||
for py, value in by_year.items():
|
||||
rate = rate.when(
|
||||
(option == GLOBAL) & (designation == name) & (year == py)
|
||||
).then(nw.lit(value))
|
||||
return rate.otherwise(_NULL)
|
||||
|
||||
|
||||
def discount_amount(py_benchmark: IntoExpr, rate: IntoExpr) -> nw.Expr:
|
||||
"""Total Discount = PY Benchmark × rate (Table 5.6.1 line 3).
|
||||
|
||||
A null rate is the paper's "N/A" (the Professional option's Total
|
||||
Discount) and removes nothing. Use it on a rate column that writes the
|
||||
Professional option as null; :func:`settle` instead derives the rate
|
||||
with :func:`discount_rate` and lets a null rate null the chain.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.1 / Table 5.2.2 line 3.
|
||||
"""
|
||||
return cents(col(py_benchmark) * nw.coalesce(col(rate), nw.lit(0.0)))
|
||||
|
||||
|
||||
def quality_withhold(py_benchmark: IntoExpr) -> nw.Expr:
|
||||
"""Quality Withhold = 3 percent of the PY Benchmark before the discount
|
||||
(Table 5.6.1 line 5).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.2 / Table 5.2.2 line 5.
|
||||
"""
|
||||
return cents(col(py_benchmark) * QUALITY_WITHHOLD_RATE)
|
||||
|
||||
|
||||
def quality_earn_back(withhold: IntoExpr, quality_score: IntoExpr) -> nw.Expr:
|
||||
"""Earned Quality Withhold = withhold × quality score (line 7); null
|
||||
when the score is missing or outside [0, 1].
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.2 / Table 5.2.2 line 7.
|
||||
"""
|
||||
score = col(quality_score)
|
||||
return nw.when(score.is_between(0.0, 1.0)).then(cents(col(withhold) * score))
|
||||
|
||||
|
||||
def retention_incentive_adjustment(
|
||||
py_benchmark: IntoExpr, applies: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""2 percent of the PY Benchmark when the ACO terminated after its
|
||||
first PY, else 0.0; null when ``applies`` is null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.3.
|
||||
"""
|
||||
flag = col(applies)
|
||||
return (
|
||||
nw.when(flag)
|
||||
.then(cents(col(py_benchmark) * RETENTION_INCENTIVE_RATE))
|
||||
.when(~flag)
|
||||
.then(nw.lit(0.0))
|
||||
.otherwise(_NULL)
|
||||
)
|
||||
|
||||
|
||||
def adjusted_stop_loss_payout(payout: IntoExpr, factor: IntoExpr) -> nw.Expr:
|
||||
"""Payout × neutrality factor (Table 5.6.1 line 20). A null factor is
|
||||
accepted only with a zero payout (no payouts model-wide); otherwise null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Application of
|
||||
Stop-Loss, line 20).
|
||||
"""
|
||||
amount, scale = col(payout), col(factor)
|
||||
return (
|
||||
nw.when(scale.is_null() & (amount == 0))
|
||||
.then(nw.lit(0.0))
|
||||
.otherwise(cents(amount * scale))
|
||||
)
|
||||
|
||||
|
||||
def _corridor_exposure(
|
||||
magnitude: nw.Expr, benchmark: nw.Expr, lower: float, upper: float | None
|
||||
) -> nw.Expr:
|
||||
capped = (
|
||||
magnitude if upper is None else nw.min_horizontal(magnitude, benchmark * upper)
|
||||
)
|
||||
return (capped - benchmark * lower).clip(lower_bound=0.0)
|
||||
|
||||
|
||||
def retained_in_corridor(
|
||||
gross: IntoExpr,
|
||||
benchmark_after_adjustments: IntoExpr,
|
||||
risk_option: IntoExpr,
|
||||
corridor: int,
|
||||
) -> nw.Expr:
|
||||
"""Savings (losses) retained in one risk corridor, signed like ``gross``
|
||||
(the "Retained ... in Corridor n" lines of Table 5.6.1); null for an
|
||||
unknown risk option or a null input.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.1 /
|
||||
Table 5.4.3.
|
||||
"""
|
||||
amount, benchmark, option = (
|
||||
col(gross),
|
||||
col(benchmark_after_adjustments),
|
||||
col(risk_option),
|
||||
)
|
||||
sign = nw.when(amount < 0).then(nw.lit(-1.0)).otherwise(nw.lit(1.0))
|
||||
expr = None
|
||||
for (name, scenario), corridors in RISK_CORRIDORS.items():
|
||||
band = corridors[corridor - 1]
|
||||
in_scenario = (amount >= 0) if scenario == "savings" else (amount < 0)
|
||||
value = cents(
|
||||
sign
|
||||
* _corridor_exposure(amount.abs(), benchmark, band.lower, band.upper)
|
||||
* band.rate
|
||||
)
|
||||
condition = (option == name) & in_scenario & ~benchmark.is_null()
|
||||
expr = (nw.when if expr is None else expr.when)(condition).then(value)
|
||||
return expr.otherwise(_NULL)
|
||||
|
||||
|
||||
def sequestration_amount(shared: IntoExpr) -> nw.Expr:
|
||||
"""2 percent of Shared Savings; zero on Shared Losses (line 27).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.2 line 27.
|
||||
"""
|
||||
return cents(col(shared).clip(lower_bound=0.0) * SEQUESTRATION_RATE)
|
||||
|
||||
|
||||
@nw.narwhalify
|
||||
def settle(scenarios: FrameT) -> FrameT:
|
||||
"""Settle a frame of scenarios: add every Table 5.6.1 line as a column.
|
||||
|
||||
Reads the ``SettlementInputs`` fields (``INPUT_COLUMNS``) and writes the
|
||||
``SettlementLines`` fields (``LINE_COLUMNS``), overwriting the Stop-Loss
|
||||
input columns with lines 17–19 (zero charge and payout, null factor,
|
||||
for a row that did not elect Stop-Loss). Other columns pass through.
|
||||
|
||||
A missing input column raises. Unlike ``SettlementInputs``, no row is
|
||||
refused: a null or invalid input nulls the lines that depend on it. A
|
||||
row the scalar accepts settles to exactly what
|
||||
:func:`lead.settlement.settle` returns.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.6 / Table 5.6.1.
|
||||
"""
|
||||
present = set(scenarios.collect_schema().names())
|
||||
missing = [c for c in INPUT_COLUMNS if c not in present]
|
||||
if missing:
|
||||
raise ValueError(f"settle() needs columns {missing}")
|
||||
c = nw.col
|
||||
elected = c("stop_loss_elected")
|
||||
return (
|
||||
scenarios.with_columns(
|
||||
discount_rate=discount_rate(
|
||||
"risk_option", "spending_designation", "performance_year"
|
||||
),
|
||||
retention_incentive_adjustment=retention_incentive_adjustment(
|
||||
"py_benchmark", "retention_incentive_applies"
|
||||
),
|
||||
quality_withhold=quality_withhold("py_benchmark"),
|
||||
total_ffs_payments=cents(
|
||||
c("participant_provider_claims")
|
||||
+ c("preferred_provider_claims")
|
||||
+ c("non_aco_provider_claims")
|
||||
),
|
||||
adjusted_stop_loss_payout=nw.when(elected)
|
||||
.then(
|
||||
adjusted_stop_loss_payout(
|
||||
"stop_loss_payout", "stop_loss_neutrality_factor"
|
||||
)
|
||||
)
|
||||
.when(~elected)
|
||||
.then(nw.lit(0.0)),
|
||||
)
|
||||
.with_columns(
|
||||
discount_amount=cents(c("py_benchmark") * c("discount_rate")),
|
||||
quality_earn_back=quality_earn_back("quality_withhold", "quality_score"),
|
||||
py_expenditure=cents(c("capitation_payments") + c("total_ffs_payments")),
|
||||
stop_loss_charge=nw.when(elected)
|
||||
.then(c("stop_loss_charge"))
|
||||
.when(~elected)
|
||||
.then(nw.lit(0.0)),
|
||||
stop_loss_payout=nw.when(elected)
|
||||
.then(c("stop_loss_payout"))
|
||||
.when(~elected)
|
||||
.then(nw.lit(0.0)),
|
||||
stop_loss_neutrality_factor=nw.when(elected).then(
|
||||
c("stop_loss_neutrality_factor")
|
||||
),
|
||||
)
|
||||
.with_columns(
|
||||
benchmark_after_discount=cents(c("py_benchmark") - c("discount_amount")),
|
||||
net_quality_withhold=cents(c("quality_withhold") - c("quality_earn_back")),
|
||||
net_stop_loss_impact=cents(
|
||||
c("stop_loss_charge") - c("adjusted_stop_loss_payout")
|
||||
),
|
||||
)
|
||||
.with_columns(
|
||||
benchmark_after_adjustments=cents(
|
||||
c("benchmark_after_discount")
|
||||
- c("retention_incentive_adjustment")
|
||||
- c("net_quality_withhold")
|
||||
),
|
||||
py_expenditure_after_stop_loss=cents(
|
||||
c("py_expenditure") + c("net_stop_loss_impact")
|
||||
),
|
||||
)
|
||||
.with_columns(
|
||||
gross_savings_losses=cents(
|
||||
c("benchmark_after_adjustments") - c("py_expenditure_after_stop_loss")
|
||||
)
|
||||
)
|
||||
.with_columns(
|
||||
**{
|
||||
f"retained_corridor_{n}": retained_in_corridor(
|
||||
"gross_savings_losses",
|
||||
"benchmark_after_adjustments",
|
||||
"risk_option",
|
||||
n,
|
||||
)
|
||||
for n in (1, 2, 3, 4)
|
||||
}
|
||||
)
|
||||
.with_columns(
|
||||
shared_savings_losses=cents(
|
||||
c("retained_corridor_1")
|
||||
+ c("retained_corridor_2")
|
||||
+ c("retained_corridor_3")
|
||||
+ c("retained_corridor_4")
|
||||
)
|
||||
)
|
||||
.with_columns(
|
||||
sequestration_amount=sequestration_amount("shared_savings_losses")
|
||||
)
|
||||
.with_columns(
|
||||
shared_savings_losses_after_sequestration=cents(
|
||||
c("shared_savings_losses") - c("sequestration_amount")
|
||||
)
|
||||
)
|
||||
)
|
||||
89
src/lead/express/stop_loss.py
Normal file
89
src/lead/express/stop_loss.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Beneficiary-grain Stop-Loss (Box 5.3.3) as narwhals expressions.
|
||||
|
||||
One row per beneficiary: Predicted Expenditure, Residual Expenditure, the
|
||||
distance to the Attachment Point, the banded payout and the band reached.
|
||||
Scalar twins: ``lead.stop_loss``.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import narwhals as nw
|
||||
|
||||
from lead.express import IntoExpr, cents, col
|
||||
from lead.params import STOP_LOSS_BANDS
|
||||
from lead.stop_loss import BELOW_ATTACHMENT_POINT
|
||||
|
||||
|
||||
def predicted_expenditure(
|
||||
regional_expenditure_pbpm: IntoExpr, risk_score: IntoExpr, months_aligned: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Predicted Expenditure = regional expenditure PBPM × risk score ×
|
||||
months of alignment in the PY.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
|
||||
Residual Expenditure).
|
||||
"""
|
||||
return cents(col(regional_expenditure_pbpm) * col(risk_score) * col(months_aligned))
|
||||
|
||||
|
||||
def residual_expenditure(actual_spend: IntoExpr, predicted_spend: IntoExpr) -> nw.Expr:
|
||||
"""Residual Expenditure = PY expenditure − Predicted Expenditure.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3.
|
||||
"""
|
||||
return cents(col(actual_spend) - col(predicted_spend))
|
||||
|
||||
|
||||
def distance_to_attachment_point(
|
||||
attachment_point: IntoExpr, residual: IntoExpr
|
||||
) -> nw.Expr:
|
||||
"""Attachment Point − Residual: positive is headroom before Stop-Loss
|
||||
attaches, negative is how far past it the beneficiary is.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 (protection begins
|
||||
once the Residual passes the Attachment Point).
|
||||
"""
|
||||
return cents(col(attachment_point) - col(residual))
|
||||
|
||||
|
||||
def stop_loss_payout(residual: IntoExpr, attachment_point: IntoExpr) -> nw.Expr:
|
||||
"""Banded payout on the Residual above the Attachment Point: 80 percent
|
||||
up to 200 percent of it, 100 percent beyond; null if either is null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
|
||||
Payment Schedule; illustration: $400,000 residual, $150,000 AP → $220,000).
|
||||
"""
|
||||
amount, point = col(residual), col(attachment_point)
|
||||
paid = []
|
||||
for band in STOP_LOSS_BANDS:
|
||||
capped = (
|
||||
amount
|
||||
if band.end_multiple is None
|
||||
else nw.min_horizontal(amount, point * band.end_multiple)
|
||||
)
|
||||
exposure = (capped - point * band.start_multiple).clip(lower_bound=0.0)
|
||||
paid.append(cents(exposure * band.payout_rate))
|
||||
total = paid[0]
|
||||
for band_payout in paid[1:]:
|
||||
total = total + band_payout
|
||||
return nw.when(~amount.is_null() & ~point.is_null()).then(cents(total))
|
||||
|
||||
|
||||
def band_status(residual: IntoExpr, attachment_point: IntoExpr) -> nw.Expr:
|
||||
"""The band the Residual reaches: ``band_2``, ``band_1`` or
|
||||
``below_attachment_point``; null if either input is null.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Box 5.3.3 (Stop-Loss
|
||||
Payment Schedule).
|
||||
"""
|
||||
amount, point = col(residual), col(attachment_point)
|
||||
status = nw.when(amount.is_null() | point.is_null()).then(
|
||||
nw.lit(None, dtype=nw.String())
|
||||
)
|
||||
for band in reversed(STOP_LOSS_BANDS):
|
||||
status = status.when(amount > point * band.start_multiple).then(
|
||||
nw.lit(f"band_{band.band}")
|
||||
)
|
||||
return status.otherwise(nw.lit(BELOW_ATTACHMENT_POINT))
|
||||
36
src/lead/money.py
Normal file
36
src/lead/money.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Cent rounding for LEAD dollar lines.
|
||||
|
||||
The paper prints every dollar line rounded to the cent and carries PBPMs,
|
||||
rates and factors at full precision; its worked examples only reproduce
|
||||
that way (Table 3.5.4 line 24 sums the cent-rounded category totals of
|
||||
lines 8 and 22; Table 3.3.3 and Table 3.5.3.2 multiply unrounded PBPMs).
|
||||
So every function in this package that produces a dollar line rounds it
|
||||
with :func:`cents`, and nothing else is rounded.
|
||||
|
||||
Rounding is half away from zero on the amount scaled to cents
|
||||
(``floor(|x| × 100 + 0.5)``). ``lead.express.cents`` performs the same
|
||||
float operations, so scalar and frame results agree bit for bit on every
|
||||
backend — unlike the engines' own ``round``, which differ (polars rounds
|
||||
half to even, DuckDB half away from zero).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 5.6.1 / Table 3.5.4.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
|
||||
def cents(amount: float) -> float:
|
||||
"""Round a dollar amount to the cent, half away from zero.
|
||||
|
||||
>>> cents(184_593.6544)
|
||||
184593.65
|
||||
>>> cents(-0.125)
|
||||
-0.13
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 5.6.1.
|
||||
"""
|
||||
scaled = math.floor(abs(amount) * 100.0 + 0.5)
|
||||
# ``+ 0.0`` turns a negative zero into zero.
|
||||
return math.copysign(scaled, amount) / 100.0 + 0.0
|
||||
108
src/lead/monies_owed.py
Normal file
108
src/lead/monies_owed.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""LEAD Total Monies Owed (§5.5, Table 5.5.1).
|
||||
|
||||
Final Shared Savings/Losses settle as an adjustment to what Provisional
|
||||
Settlement already paid or collected; the payment-arrangement under (over)
|
||||
payments and the High-Performers Pool incentive are added. Every amount is
|
||||
signed the way Table 5.5.1 prints it: positive is owed to the ACO,
|
||||
negative is owed to CMS.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.5 / Table 5.5.1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from lead.money import cents
|
||||
|
||||
|
||||
def apo_adjustment(fee_reductions: float, apo_payments: float) -> float:
|
||||
"""Reconcile the Advanced Payment Option against actual claims
|
||||
reductions: reductions above the APO payments are credited to the ACO
|
||||
(positive), reductions below them are recouped by CMS (negative).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.5 (Adjustments for
|
||||
ACOs participating in EPCC and/or APO).
|
||||
"""
|
||||
return cents(fee_reductions - apo_payments)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaymentArrangementAdjustments:
|
||||
"""Table 5.5.1 line 4 and its three component lines."""
|
||||
|
||||
capitation_under_over_payment: float
|
||||
enhanced_pcc_repayment: float
|
||||
apo_adjustment: float
|
||||
payment_arrangement_adjustments: float
|
||||
|
||||
|
||||
def payment_arrangement_adjustments(
|
||||
*,
|
||||
capitation_under_over_payment: float,
|
||||
enhanced_pcc_paid: float,
|
||||
apo_adjustment: float,
|
||||
) -> PaymentArrangementAdjustments:
|
||||
"""Return Under (Over) Payments from Payment Arrangements (line 4).
|
||||
|
||||
``capitation_under_over_payment`` is positive when CMS underpaid the
|
||||
ACO's capitation. Enhanced PCC is recouped in full, so its repayment is
|
||||
``-enhanced_pcc_paid``. An ACO that elected neither EPCC nor APO (the
|
||||
TCC ACO of Table 5.5.1, footnote 1) passes zero for both.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.5 / Table 5.5.1 line 4.
|
||||
"""
|
||||
repayment = cents(-enhanced_pcc_paid)
|
||||
return PaymentArrangementAdjustments(
|
||||
capitation_under_over_payment=capitation_under_over_payment,
|
||||
enhanced_pcc_repayment=repayment,
|
||||
apo_adjustment=apo_adjustment,
|
||||
payment_arrangement_adjustments=cents(
|
||||
capitation_under_over_payment + repayment + apo_adjustment
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TotalMoniesOwed:
|
||||
"""Table 5.5.1 lines 1–7."""
|
||||
|
||||
provisional_shared_savings_losses: float
|
||||
final_shared_savings_losses: float
|
||||
shared_savings_losses_owed: float
|
||||
payment_arrangement_adjustments: float
|
||||
high_performers_pool_incentive: float
|
||||
adjustments_owed: float
|
||||
total_monies_owed: float
|
||||
|
||||
|
||||
def total_monies_owed(
|
||||
*,
|
||||
provisional_shared_savings_losses: float,
|
||||
final_shared_savings_losses: float,
|
||||
payment_arrangement_adjustments: float,
|
||||
high_performers_pool_incentive: float,
|
||||
) -> TotalMoniesOwed:
|
||||
"""Return Total Monies Owed at Final Settlement (lines 1–7).
|
||||
|
||||
Line 3 = final − provisional Shared Savings (Losses); line 6 = payment
|
||||
arrangement adjustments + High-Performers Pool incentive; line 7 =
|
||||
line 3 + line 6. Every input is required: an ACO that did not select
|
||||
Provisional Settlement, or earned no High-Performers Pool incentive,
|
||||
passes zero for it.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.5 / Table 5.5.1.
|
||||
"""
|
||||
owed = cents(final_shared_savings_losses - provisional_shared_savings_losses)
|
||||
adjustments = cents(
|
||||
payment_arrangement_adjustments + high_performers_pool_incentive
|
||||
)
|
||||
return TotalMoniesOwed(
|
||||
provisional_shared_savings_losses=provisional_shared_savings_losses,
|
||||
final_shared_savings_losses=final_shared_savings_losses,
|
||||
shared_savings_losses_owed=owed,
|
||||
payment_arrangement_adjustments=payment_arrangement_adjustments,
|
||||
high_performers_pool_incentive=high_performers_pool_incentive,
|
||||
adjustments_owed=adjustments,
|
||||
total_monies_owed=cents(owed + adjustments),
|
||||
)
|
||||
332
src/lead/params.py
Normal file
332
src/lead/params.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""LEAD PY2027 financial-methodology parameters, transcribed from the public paper.
|
||||
|
||||
Every constant here is printed in the CMS "LEAD Model PY 2027 Alignment and
|
||||
Financial Methodology Paper" v1.2 (bib ``E4PEU3HV``). Values that CMS
|
||||
computes per year or per ACO and does not print — the Stop-Loss Attachment
|
||||
Point, ACPT values, hybrid alignment factors, modified USPCC amounts, the
|
||||
Stop-Loss neutrality factor — are caller inputs, never constants. Worked-
|
||||
example figures are test fixtures, never parameters.
|
||||
|
||||
The mappings are read-only (``MappingProxyType``) so a caller cannot change
|
||||
a published rate for everyone in the process.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import MappingProxyType
|
||||
from typing import Final, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
# ── Labels ────────────────────────────────────────────────────────────────
|
||||
|
||||
RiskOption = Literal["Global", "Professional"]
|
||||
GLOBAL: Final = "Global"
|
||||
PROFESSIONAL: Final = "Professional"
|
||||
RISK_OPTIONS: Final = (GLOBAL, PROFESSIONAL)
|
||||
"""LEAD risk-sharing options. Source: LEAD PY2027 methodology (bib E4PEU3HV) §1.3."""
|
||||
|
||||
SpendingDesignation = Literal["Lower-spending", "Higher-spending"]
|
||||
LOWER_SPENDING: Final = "Lower-spending"
|
||||
HIGHER_SPENDING: Final = "Higher-spending"
|
||||
SPENDING_DESIGNATIONS: Final = (LOWER_SPENDING, HIGHER_SPENDING)
|
||||
"""Spending designations. Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.2."""
|
||||
|
||||
BeneficiaryCategory = Literal["A&D", "ESRD", "High Needs"]
|
||||
AGED_DISABLED: Final = "A&D"
|
||||
ESRD: Final = "ESRD"
|
||||
HIGH_NEEDS: Final = "High Needs"
|
||||
BENEFICIARY_CATEGORIES: Final = (AGED_DISABLED, ESRD, HIGH_NEEDS)
|
||||
"""Beneficiary categories. Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.2."""
|
||||
|
||||
EntryType = Literal["Newly Entering", "Renewing"]
|
||||
NEWLY_ENTERING: Final = "Newly Entering"
|
||||
RENEWING: Final = "Renewing"
|
||||
ENTRY_TYPES: Final = (NEWLY_ENTERING, RENEWING)
|
||||
"""ACO entry types. Source: LEAD PY2027 methodology (bib E4PEU3HV) §1.1."""
|
||||
|
||||
BASE_YEARS: Final = ("BY1", "BY2", "BY3")
|
||||
"""The three historical base years, oldest first.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2 / §3.3.3.
|
||||
"""
|
||||
|
||||
# ── Model calendar ────────────────────────────────────────────────────────
|
||||
|
||||
FIRST_PERFORMANCE_YEAR: Final = 2027
|
||||
LAST_PERFORMANCE_YEAR: Final = 2036
|
||||
"""LEAD runs ten Performance Years, PY 2027 through PY 2036.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 5.2.1 (2032−2036) /
|
||||
Table 2.6.1 (PY 1–10).
|
||||
"""
|
||||
|
||||
|
||||
def model_year(performance_year: int) -> int:
|
||||
"""Return the model PY index (PY 2027 = 1, PY 2036 = 10).
|
||||
|
||||
Raises ``ValueError`` outside the model's ten Performance Years.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 2.6.1 / Table 3.5.1.
|
||||
"""
|
||||
if not FIRST_PERFORMANCE_YEAR <= performance_year <= LAST_PERFORMANCE_YEAR:
|
||||
raise ValueError(
|
||||
f"performance year {performance_year} is outside the LEAD model "
|
||||
f"({FIRST_PERFORMANCE_YEAR}-{LAST_PERFORMANCE_YEAR})"
|
||||
)
|
||||
return performance_year - FIRST_PERFORMANCE_YEAR + 1
|
||||
|
||||
|
||||
# ── §5.2 Performance Year Benchmark adjustments ───────────────────────────
|
||||
|
||||
DISCOUNT_RATES: Final = MappingProxyType(
|
||||
{
|
||||
LOWER_SPENDING: MappingProxyType({year: 0.03 for year in range(2027, 2037)}),
|
||||
HIGHER_SPENDING: MappingProxyType(
|
||||
{
|
||||
2027: 0.0175,
|
||||
2028: 0.02,
|
||||
2029: 0.0225,
|
||||
2030: 0.025,
|
||||
2031: 0.0275,
|
||||
**{year: 0.03 for year in range(2032, 2037)},
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
"""Global Risk Option benchmark discount by spending designation and PY.
|
||||
|
||||
Lower-spending ACOs: a static 3.0 percent. Higher-spending ACOs: 1.75 percent
|
||||
in 2027, rising 0.25 points a year to 3.0 percent in 2032−2036. The
|
||||
Professional option carries no discount (Table 5.2.2 line 2: 0.00%).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.1 / Table 5.2.1.
|
||||
"""
|
||||
|
||||
QUALITY_WITHHOLD_RATE: Final = 0.03
|
||||
"""Quality Withhold: 3 percent of the PY Benchmark before the discount.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.2 / Table 5.2.2 line 5.
|
||||
"""
|
||||
|
||||
RETENTION_INCENTIVE_RATE: Final = 0.02
|
||||
"""Retention incentive: 2 percent benchmark reduction for an ACO that
|
||||
terminates after its first Performance Year.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.3.
|
||||
"""
|
||||
|
||||
# ── §5.3.3 Stop-Loss Reinsurance ──────────────────────────────────────────
|
||||
|
||||
|
||||
class StopLossBand(BaseModel):
|
||||
"""One Stop-Loss payout band, in multiples of the Attachment Point.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3
|
||||
(Stop-Loss Payment Schedule).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
band: int = Field(ge=1)
|
||||
start_multiple: float = Field(ge=0.0, description="Band start, × Attachment Point.")
|
||||
end_multiple: float | None = Field(
|
||||
default=None, description="Band end, × Attachment Point; None = no upper limit."
|
||||
)
|
||||
payout_rate: float = Field(ge=0.0, le=1.0)
|
||||
|
||||
|
||||
STOP_LOSS_BANDS: Final = (
|
||||
StopLossBand(band=1, start_multiple=1.0, end_multiple=2.0, payout_rate=0.80),
|
||||
StopLossBand(band=2, start_multiple=2.0, end_multiple=None, payout_rate=1.00),
|
||||
)
|
||||
"""Residual Expenditure between 100 and 200 percent of the Attachment Point
|
||||
pays at 80 percent; above 200 percent it pays at 100 percent.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3
|
||||
(Stop-Loss Payment Schedule).
|
||||
"""
|
||||
|
||||
STOP_LOSS_ATTACHMENT_PAYOUT_SHARE: Final = 0.02
|
||||
"""CMS sets each reference year's Attachment Point so that total model
|
||||
payout equals 2 percent of total Medicare expenditures. Recorded for
|
||||
reference: the Attachment Point itself is a CMS input to this package.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3
|
||||
(Stop-Loss Attachment Point).
|
||||
"""
|
||||
|
||||
STOP_LOSS_REFERENCE_YEAR_COUNT: Final = 3
|
||||
STOP_LOSS_REFERENCE_YEAR_GAP: Final = 1
|
||||
"""Three Stop-Loss reference years, with a one-year gap before the PY
|
||||
(2023, 2024, 2025 for PY 2027).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3.3 / Box 5.3.3
|
||||
(Stop-Loss Attachment Point).
|
||||
"""
|
||||
|
||||
# ── §5.4 Risk corridors and sequestration ─────────────────────────────────
|
||||
|
||||
CorridorScenario = Literal["savings", "losses"]
|
||||
|
||||
|
||||
class Corridor(BaseModel):
|
||||
"""One risk corridor: a band of gross savings or losses, as a share of
|
||||
the PY Benchmark after adjustments, shared at ``rate``.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.1 /
|
||||
Table 5.4.3.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
corridor: int = Field(ge=1)
|
||||
lower: float = Field(ge=0.0, description="Band start, share of benchmark.")
|
||||
upper: float | None = Field(
|
||||
default=None, description="Band end, share of benchmark; None = no limit."
|
||||
)
|
||||
rate: float = Field(ge=0.0, le=1.0)
|
||||
|
||||
|
||||
def _corridors(
|
||||
bounds: tuple[float, ...], rates: tuple[float, ...]
|
||||
) -> tuple[Corridor, ...]:
|
||||
edges = (0.0, *bounds, None)
|
||||
return tuple(
|
||||
Corridor(corridor=i + 1, lower=edges[i], upper=edges[i + 1], rate=rate)
|
||||
for i, rate in enumerate(rates)
|
||||
)
|
||||
|
||||
|
||||
_GLOBAL_BOUNDS = (0.15, 0.35, 0.50)
|
||||
_PROFESSIONAL_BOUNDS = (0.10, 0.15, 0.20)
|
||||
|
||||
RISK_CORRIDORS: Final = MappingProxyType(
|
||||
{
|
||||
(GLOBAL, "savings"): _corridors(_GLOBAL_BOUNDS, (1.00, 0.50, 0.25, 0.10)),
|
||||
(GLOBAL, "losses"): _corridors(_GLOBAL_BOUNDS, (1.00, 0.50, 0.25, 0.10)),
|
||||
(PROFESSIONAL, "savings"): _corridors(
|
||||
_PROFESSIONAL_BOUNDS, (0.60, 0.35, 0.15, 0.05)
|
||||
),
|
||||
(PROFESSIONAL, "losses"): _corridors(
|
||||
_PROFESSIONAL_BOUNDS, (0.50, 0.35, 0.15, 0.05)
|
||||
),
|
||||
}
|
||||
)
|
||||
"""Risk corridors by (risk option, savings/losses).
|
||||
|
||||
Global: up to 15% / 15–35% / 35–50% / over 50% of benchmark at 100 / 50 /
|
||||
25 / 10 percent, savings and losses alike. Professional: up to 10% / 10–15% /
|
||||
15–20% / over 20% at 60 (savings) or 50 (losses) / 35 / 15 / 5 percent.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4.1 / Table 5.4.1;
|
||||
§5.4.2 / Table 5.4.3.
|
||||
"""
|
||||
|
||||
SEQUESTRATION_RATE: Final = 0.02
|
||||
"""Sequestration: 2 percent of the Shared Savings retained by the ACO
|
||||
(Budget Control Act of 2011); it never applies to Shared Losses.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.2 line 27.
|
||||
"""
|
||||
|
||||
# ── §3 Benchmark ─────────────────────────────────────────────────────────
|
||||
|
||||
BASE_YEAR_WEIGHTS: Final = MappingProxyType(
|
||||
{
|
||||
NEWLY_ENTERING: MappingProxyType({"BY1": 0.10, "BY2": 0.30, "BY3": 0.60}),
|
||||
RENEWING: MappingProxyType({"BY1": 1 / 3, "BY2": 1 / 3, "BY3": 1 / 3}),
|
||||
}
|
||||
)
|
||||
"""Base-year weights when all three base years are sufficient.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.3.3 / Table 3.3.3 line 8.
|
||||
"""
|
||||
|
||||
ACPT_WEIGHT: Final = 1 / 3
|
||||
"""Share of the three-way blended trend set by the ACPT; the observed
|
||||
national-regional blend carries the other two-thirds.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.1.
|
||||
"""
|
||||
|
||||
ACPT_GUARDRAILS: Final = MappingProxyType(
|
||||
{
|
||||
1: (-0.002, 0.003),
|
||||
2: (-0.004, 0.006),
|
||||
3: (-0.006, 0.009),
|
||||
4: (-0.008, 0.012),
|
||||
**{year: (-0.010, 0.015) for year in range(5, 11)},
|
||||
}
|
||||
)
|
||||
"""(lower, upper) guardrail on the three-way trend factor, in factor units
|
||||
(percentage points / 100) around the two-way blend, by model PY.
|
||||
|
||||
The §3.5.1 prose widens the lower bound one year later than Table 3.5.1
|
||||
("-0.4 percentage points in 2029"); the table is followed here.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.1 / Table 3.5.1.
|
||||
"""
|
||||
|
||||
RISK_SCORE_GROWTH_CAPS: Final = MappingProxyType(
|
||||
{AGED_DISABLED: 0.03, ESRD: 0.03, HIGH_NEEDS: 0.04}
|
||||
)
|
||||
"""Symmetric PY risk-score growth cap relative to BY 3, by category.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.5.2 (Risk Score Growth Cap).
|
||||
"""
|
||||
|
||||
REGIONAL_EFFICIENCY_SHARE: Final = 0.50
|
||||
"""Regional Efficiency Adjustment: 50 percent of the positive gap between
|
||||
BY 3 regional FFS expenditures and the Historical Benchmark.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.3 / Table 3.4.3.1.
|
||||
"""
|
||||
|
||||
PRIOR_SAVINGS_SHARE: Final = 0.50
|
||||
PRIOR_SAVINGS_PRORATION_CAP: Final = 1.0
|
||||
"""Prior Savings Adjustment: 50 percent of the prorated average PBPM
|
||||
savings; the proration factor is capped at 100 percent.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4.4.
|
||||
"""
|
||||
|
||||
BENCHMARK_ADJUSTMENT_CAP_RATE: Final = 0.05
|
||||
REDUCED_BENCHMARK_ADJUSTMENT_CAP_RATE: Final = 0.03
|
||||
"""Benchmark adjustment cap: 5 percent of the modified USPCC amount, risk
|
||||
adjusted; 3 percent for lower-spending ACOs with more than 40 percent of
|
||||
Participant TINs in a Shared Savings Program ACO in the prior two years
|
||||
(CMS determines that eligibility; it is a caller input here).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.4 / §3.4.3.
|
||||
"""
|
||||
|
||||
VOLUNTARY_GUARDRAIL_RATE: Final = 0.10
|
||||
"""The voluntarily aligned Historical Benchmark stays within ±10 percent of
|
||||
the claims-aligned Historical Benchmark of the same category.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
|
||||
VOLUNTARY_BENCHMARK_MINIMUM: Final = 500
|
||||
"""Fewer than 500 contributing voluntarily aligned beneficiaries in BY 3:
|
||||
the claims-aligned benchmarks apply to the voluntarily aligned population.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §3.2.5.
|
||||
"""
|
||||
|
||||
AlignmentMinimumType = Literal["standard", "newly_entering", "high_needs_esrd"]
|
||||
|
||||
BASE_YEAR_ALIGNMENT_MINIMUMS: Final = MappingProxyType(
|
||||
{
|
||||
"standard": (3000,) * 10,
|
||||
"newly_entering": (600, 1200, 1800, 2400) + (3000,) * 6,
|
||||
"high_needs_esrd": (500, 625, 750, 825) + (1000,) * 6,
|
||||
}
|
||||
)
|
||||
"""Claims-based alignment minimum a base year must meet to count toward
|
||||
the Historical Benchmark, by the CMS-determined alignment standard and
|
||||
model PY 1–10.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §2.6 / Table 2.6.1; §3.3.4.
|
||||
"""
|
||||
476
src/lead/settlement.py
Normal file
476
src/lead/settlement.py
Normal file
@@ -0,0 +1,476 @@
|
||||
"""LEAD Final Financial Settlement: Table 5.6.1, line by line.
|
||||
|
||||
The chain is: PY Benchmark (line 1) → Global discount (lines 2–4) →
|
||||
retention incentive (§5.2.3) → Quality Withhold and Earn Back (lines 5–9)
|
||||
→ PY expenditure (lines 10–15) → Stop-Loss (lines 16–22) → gross
|
||||
savings/losses (lines 23–25) → risk corridors (line 26 and its corridor
|
||||
lines) → sequestration (lines 27–28). :func:`settle` runs it for one
|
||||
scenario; ``lead.express.settlement.settle`` runs it for a frame of
|
||||
scenarios on any narwhals backend and returns the same numbers.
|
||||
|
||||
Every dollar line is rounded to the cent as it is produced
|
||||
(:func:`lead.money.cents`); rates and the quality score are not rounded.
|
||||
The paper prints reductions in parentheses in Table 5.6.1 (lines 5 and 8);
|
||||
here they are positive amounts that the chain subtracts.
|
||||
|
||||
Missing inputs are never silent zeros: :class:`SettlementInputs` refuses
|
||||
an incomplete scenario. The zeros that do appear are the paper's own: the
|
||||
Professional option's discount ("N/A", Table 5.6.1 line 3), the Stop-Loss
|
||||
lines of an ACO that did not elect Stop-Loss (§5.3.3), and sequestration of
|
||||
Shared Losses (§5.4).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2–§5.6 / Table 5.6.1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from lead.money import cents
|
||||
from lead.params import (
|
||||
DISCOUNT_RATES,
|
||||
GLOBAL,
|
||||
PROFESSIONAL,
|
||||
QUALITY_WITHHOLD_RATE,
|
||||
RETENTION_INCENTIVE_RATE,
|
||||
RISK_CORRIDORS,
|
||||
SEQUESTRATION_RATE,
|
||||
CorridorScenario,
|
||||
RiskOption,
|
||||
SpendingDesignation,
|
||||
model_year,
|
||||
)
|
||||
from lead.stop_loss import apply_stop_loss, without_stop_loss
|
||||
|
||||
# ── §5.2 Performance Year Benchmark adjustments ───────────────────────────
|
||||
|
||||
|
||||
def benchmark_discount_rate(
|
||||
risk_option: RiskOption,
|
||||
spending_designation: SpendingDesignation | None,
|
||||
performance_year: int,
|
||||
) -> float:
|
||||
"""Return the benchmark discount rate for a risk option, designation and PY.
|
||||
|
||||
The Professional option has no discount (0.0). The Global option reads
|
||||
Table 5.2.1; an unknown designation or a year outside the model raises,
|
||||
because a Global benchmark with no scheduled discount is not computable.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.1 / Table 5.2.1 /
|
||||
Table 5.2.2 line 2.
|
||||
"""
|
||||
model_year(performance_year)
|
||||
if risk_option == PROFESSIONAL:
|
||||
return 0.0
|
||||
if risk_option != GLOBAL:
|
||||
raise ValueError(f"unknown risk option {risk_option!r}")
|
||||
if spending_designation not in DISCOUNT_RATES:
|
||||
raise ValueError(
|
||||
"the Global discount needs a spending designation, "
|
||||
f"got {spending_designation!r}"
|
||||
)
|
||||
return DISCOUNT_RATES[spending_designation][performance_year]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Discount:
|
||||
"""Table 5.6.1 lines 2–4."""
|
||||
|
||||
discount_rate: float
|
||||
discount_amount: float
|
||||
benchmark_after_discount: float
|
||||
|
||||
|
||||
def apply_discount(py_benchmark: float, discount_rate: float | None) -> Discount:
|
||||
"""Reduce the PY Benchmark by the discount (Table 5.6.1 lines 2–4).
|
||||
|
||||
A missing rate (``None``) is the paper's "N/A" — the Professional
|
||||
option's Total Discount — and removes nothing.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.1 / Table 5.2.2
|
||||
lines 2–4.
|
||||
"""
|
||||
rate = 0.0 if discount_rate is None else discount_rate
|
||||
amount = cents(py_benchmark * rate)
|
||||
return Discount(rate, amount, cents(py_benchmark - amount))
|
||||
|
||||
|
||||
def quality_withhold(
|
||||
py_benchmark: float, withhold_rate: float = QUALITY_WITHHOLD_RATE
|
||||
) -> float:
|
||||
"""Return the Quality Withhold: 3 percent of the PY Benchmark before
|
||||
the discount (Table 5.6.1 line 5).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.2 / Table 5.2.2 line 5.
|
||||
"""
|
||||
return cents(py_benchmark * withhold_rate)
|
||||
|
||||
|
||||
def quality_earn_back(withhold: float, quality_score: float) -> float:
|
||||
"""Return the Earned Quality Withhold: withhold × quality score
|
||||
(Table 5.6.1 line 7). The score is a share between 0 and 1.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.2 / Table 5.2.2 line 7.
|
||||
"""
|
||||
if not 0.0 <= quality_score <= 1.0:
|
||||
raise ValueError(f"quality score must be within [0, 1], got {quality_score}")
|
||||
return cents(withhold * quality_score)
|
||||
|
||||
|
||||
def retention_incentive_adjustment(py_benchmark: float, applies: bool) -> float:
|
||||
"""Return the retention incentive reduction: 2 percent of the PY
|
||||
Benchmark when the ACO terminated after its first Performance Year,
|
||||
otherwise zero (the ACO earns it back).
|
||||
|
||||
The paper says the PY Benchmark is "reduced by 2 percent" and orders the
|
||||
reduction after the discount and before the Quality Withhold; like the
|
||||
Quality Withhold it is taken here on the PY Benchmark before the
|
||||
discount.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2.3.
|
||||
"""
|
||||
return cents(py_benchmark * RETENTION_INCENTIVE_RATE) if applies else 0.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BenchmarkAdjustments:
|
||||
"""Table 5.6.1 lines 1–9 (plus the §5.2.3 retention incentive)."""
|
||||
|
||||
py_benchmark: float
|
||||
discount_rate: float
|
||||
discount_amount: float
|
||||
benchmark_after_discount: float
|
||||
retention_incentive_adjustment: float
|
||||
quality_withhold: float
|
||||
quality_score: float
|
||||
quality_earn_back: float
|
||||
net_quality_withhold: float
|
||||
benchmark_after_adjustments: float
|
||||
|
||||
|
||||
def adjust_benchmark(
|
||||
py_benchmark: float,
|
||||
discount_rate: float | None,
|
||||
quality_score: float,
|
||||
*,
|
||||
retention_incentive_applies: bool = False,
|
||||
) -> BenchmarkAdjustments:
|
||||
"""Apply the discount, retention incentive and net Quality Withhold to
|
||||
the PY Benchmark (Table 5.6.1 lines 1–9).
|
||||
|
||||
Line 9 = line 4 − retention incentive − (line 5 − line 7).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.2 / Table 5.2.2 /
|
||||
Table 5.6.1 lines 1–9.
|
||||
"""
|
||||
discount = apply_discount(py_benchmark, discount_rate)
|
||||
retention = retention_incentive_adjustment(
|
||||
py_benchmark, retention_incentive_applies
|
||||
)
|
||||
withhold = quality_withhold(py_benchmark)
|
||||
earned = quality_earn_back(withhold, quality_score)
|
||||
net_quality = cents(withhold - earned)
|
||||
return BenchmarkAdjustments(
|
||||
py_benchmark=py_benchmark,
|
||||
discount_rate=discount.discount_rate,
|
||||
discount_amount=discount.discount_amount,
|
||||
benchmark_after_discount=discount.benchmark_after_discount,
|
||||
retention_incentive_adjustment=retention,
|
||||
quality_withhold=withhold,
|
||||
quality_score=quality_score,
|
||||
quality_earn_back=earned,
|
||||
net_quality_withhold=net_quality,
|
||||
benchmark_after_adjustments=cents(
|
||||
discount.benchmark_after_discount - retention - net_quality
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# ── §5.3 Performance Year expenditure ─────────────────────────────────────
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PYExpenditure:
|
||||
"""Table 5.6.1 lines 10–15."""
|
||||
|
||||
capitation_payments: float
|
||||
participant_provider_claims: float
|
||||
preferred_provider_claims: float
|
||||
non_aco_provider_claims: float
|
||||
total_ffs_payments: float
|
||||
py_expenditure: float
|
||||
|
||||
|
||||
def py_expenditure(
|
||||
capitation_payments: float,
|
||||
participant_provider_claims: float,
|
||||
preferred_provider_claims: float,
|
||||
non_aco_provider_claims: float,
|
||||
) -> PYExpenditure:
|
||||
"""Sum capitation and the three FFS claim components into the PY
|
||||
expenditure, on a pre-sequestration basis (Table 5.6.1 lines 10–15).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.3 / Table 5.3.2.
|
||||
"""
|
||||
total_ffs = cents(
|
||||
participant_provider_claims
|
||||
+ preferred_provider_claims
|
||||
+ non_aco_provider_claims
|
||||
)
|
||||
return PYExpenditure(
|
||||
capitation_payments=capitation_payments,
|
||||
participant_provider_claims=participant_provider_claims,
|
||||
preferred_provider_claims=preferred_provider_claims,
|
||||
non_aco_provider_claims=non_aco_provider_claims,
|
||||
total_ffs_payments=total_ffs,
|
||||
py_expenditure=cents(capitation_payments + total_ffs),
|
||||
)
|
||||
|
||||
|
||||
# ── §5.4 Gross savings/losses, risk corridors, sequestration ──────────────
|
||||
|
||||
|
||||
def gross_savings_losses(
|
||||
benchmark_after_adjustments: float, py_expenditure_after_stop_loss: float
|
||||
) -> float:
|
||||
"""Return gross savings (positive) or losses (negative): benchmark after
|
||||
adjustments less PY expenditure after Stop-Loss (Table 5.6.1 line 25).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.2 line 25.
|
||||
"""
|
||||
return cents(benchmark_after_adjustments - py_expenditure_after_stop_loss)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SharedSavingsLosses:
|
||||
"""Table 5.6.1 line 26 and its "Retained ... in Corridor n" lines."""
|
||||
|
||||
risk_option: RiskOption
|
||||
scenario: CorridorScenario
|
||||
retained_by_corridor: tuple[float, ...]
|
||||
shared_savings_losses: float
|
||||
|
||||
|
||||
def corridor_exposure(
|
||||
amount: float, benchmark: float, lower: float, upper: float | None
|
||||
) -> float:
|
||||
"""Return the part of ``|amount|`` that falls in one corridor, whose
|
||||
bounds are shares of ``benchmark``.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.1 /
|
||||
Table 5.4.3.
|
||||
"""
|
||||
magnitude = abs(amount)
|
||||
capped = magnitude if upper is None else min(magnitude, benchmark * upper)
|
||||
return max(0.0, capped - benchmark * lower)
|
||||
|
||||
|
||||
def shared_savings_losses(
|
||||
gross: float, benchmark_after_adjustments: float, risk_option: RiskOption
|
||||
) -> SharedSavingsLosses:
|
||||
"""Apply the risk corridors to gross savings or losses (Table 5.6.1
|
||||
line 26).
|
||||
|
||||
Corridor bounds are shares of the PY Benchmark after the discount,
|
||||
Quality Withhold and Earn Back. Each corridor's retained amount keeps
|
||||
the sign of ``gross`` and is rounded to the cent; line 26 is their sum,
|
||||
so the printed corridor lines add up to it.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.1 /
|
||||
Table 5.4.3 / Table 5.4.2 / Table 5.4.4.
|
||||
"""
|
||||
if risk_option not in (GLOBAL, PROFESSIONAL):
|
||||
raise ValueError(f"unknown risk option {risk_option!r}")
|
||||
scenario: CorridorScenario = "savings" if gross >= 0 else "losses"
|
||||
sign = 1.0 if gross >= 0 else -1.0
|
||||
retained = tuple(
|
||||
cents(
|
||||
sign
|
||||
* corridor_exposure(gross, benchmark_after_adjustments, c.lower, c.upper)
|
||||
* c.rate
|
||||
)
|
||||
for c in RISK_CORRIDORS[(risk_option, scenario)]
|
||||
)
|
||||
return SharedSavingsLosses(risk_option, scenario, retained, cents(sum(retained)))
|
||||
|
||||
|
||||
def sequestration_amount(shared: float, rate: float = SEQUESTRATION_RATE) -> float:
|
||||
"""Return sequestration: 2 percent of Shared Savings; zero on Shared
|
||||
Losses, which are not sequestered (Table 5.6.1 line 27).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.4 / Table 5.4.2 line 27.
|
||||
"""
|
||||
return cents(max(shared, 0.0) * rate)
|
||||
|
||||
|
||||
# ── The whole chain ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class SettlementInputs(BaseModel):
|
||||
"""One settlement scenario: the Table 5.6.1 inputs.
|
||||
|
||||
``spending_designation`` is required for the Global option (it selects
|
||||
the discount) and unused for the Professional option. The Stop-Loss
|
||||
charge and payout are required exactly when Stop-Loss was elected; the
|
||||
neutrality factor is required unless the payout is zero.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) Table 5.6.1 lines 1, 6,
|
||||
10–13, 17–19; §5.2.3.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
py_benchmark: float = Field(description="Line 1: PY Benchmark, all aligned.")
|
||||
risk_option: RiskOption
|
||||
spending_designation: SpendingDesignation | None = None
|
||||
performance_year: int
|
||||
quality_score: float = Field(ge=0.0, le=1.0, description="Line 6, as a share.")
|
||||
capitation_payments: float = Field(description="Line 10.")
|
||||
participant_provider_claims: float = Field(description="Line 11.")
|
||||
preferred_provider_claims: float = Field(description="Line 12.")
|
||||
non_aco_provider_claims: float = Field(description="Line 13.")
|
||||
stop_loss_elected: bool
|
||||
stop_loss_charge: float | None = Field(default=None, description="Line 17.")
|
||||
stop_loss_payout: float | None = Field(default=None, description="Line 18.")
|
||||
stop_loss_neutrality_factor: float | None = Field(
|
||||
default=None, description="Line 19 (model-wide)."
|
||||
)
|
||||
retention_incentive_applies: bool = Field(
|
||||
default=False,
|
||||
description="True only when the ACO terminated after its first PY (§5.2.3).",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _complete(self) -> SettlementInputs:
|
||||
model_year(self.performance_year)
|
||||
if self.risk_option == GLOBAL and self.spending_designation is None:
|
||||
raise ValueError("the Global option needs a spending designation")
|
||||
amounts = (self.stop_loss_charge, self.stop_loss_payout)
|
||||
if self.stop_loss_elected:
|
||||
if None in amounts:
|
||||
raise ValueError(
|
||||
"Stop-Loss was elected: charge and payout are required"
|
||||
)
|
||||
if self.stop_loss_neutrality_factor is None and self.stop_loss_payout:
|
||||
raise ValueError("a Stop-Loss payout needs the neutrality factor")
|
||||
elif amounts != (None, None) or self.stop_loss_neutrality_factor is not None:
|
||||
raise ValueError("Stop-Loss amounts given but Stop-Loss was not elected")
|
||||
return self
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SettlementLines:
|
||||
"""Every line of Table 5.6.1 (line 16 = line 15, line 23 = line 22,
|
||||
line 24 = line 9), plus the §5.2.3 retention incentive."""
|
||||
|
||||
py_benchmark: float # 1
|
||||
discount_rate: float # 2
|
||||
discount_amount: float # 3
|
||||
benchmark_after_discount: float # 4
|
||||
retention_incentive_adjustment: float # §5.2.3
|
||||
quality_withhold: float # 5
|
||||
quality_score: float # 6
|
||||
quality_earn_back: float # 7
|
||||
net_quality_withhold: float # 8
|
||||
benchmark_after_adjustments: float # 9, 24
|
||||
capitation_payments: float # 10
|
||||
participant_provider_claims: float # 11
|
||||
preferred_provider_claims: float # 12
|
||||
non_aco_provider_claims: float # 13
|
||||
total_ffs_payments: float # 14
|
||||
py_expenditure: float # 15, 16
|
||||
stop_loss_charge: float # 17
|
||||
stop_loss_payout: float # 18
|
||||
stop_loss_neutrality_factor: float | None # 19
|
||||
adjusted_stop_loss_payout: float # 20
|
||||
net_stop_loss_impact: float # 21
|
||||
py_expenditure_after_stop_loss: float # 22, 23
|
||||
gross_savings_losses: float # 25
|
||||
shared_savings_losses: float # 26
|
||||
retained_corridor_1: float
|
||||
retained_corridor_2: float
|
||||
retained_corridor_3: float
|
||||
retained_corridor_4: float
|
||||
sequestration_amount: float # 27
|
||||
shared_savings_losses_after_sequestration: float # 28
|
||||
|
||||
|
||||
def settle(inputs: SettlementInputs) -> SettlementLines:
|
||||
"""Run Final Financial Settlement for one scenario (Table 5.6.1).
|
||||
|
||||
An ACO that did not elect Stop-Loss has no charge, payout or net
|
||||
impact: its PY expenditure after Stop-Loss is its PY expenditure.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.6 / Table 5.6.1.
|
||||
"""
|
||||
rate = benchmark_discount_rate(
|
||||
inputs.risk_option, inputs.spending_designation, inputs.performance_year
|
||||
)
|
||||
benchmark = adjust_benchmark(
|
||||
inputs.py_benchmark,
|
||||
rate,
|
||||
inputs.quality_score,
|
||||
retention_incentive_applies=inputs.retention_incentive_applies,
|
||||
)
|
||||
expenditure = py_expenditure(
|
||||
inputs.capitation_payments,
|
||||
inputs.participant_provider_claims,
|
||||
inputs.preferred_provider_claims,
|
||||
inputs.non_aco_provider_claims,
|
||||
)
|
||||
# SettlementInputs guarantees the charge and payout exist when elected.
|
||||
stop_loss = (
|
||||
apply_stop_loss(
|
||||
expenditure.py_expenditure,
|
||||
inputs.stop_loss_charge,
|
||||
inputs.stop_loss_payout,
|
||||
inputs.stop_loss_neutrality_factor,
|
||||
)
|
||||
if inputs.stop_loss_elected
|
||||
else without_stop_loss(expenditure.py_expenditure)
|
||||
)
|
||||
gross = gross_savings_losses(
|
||||
benchmark.benchmark_after_adjustments,
|
||||
stop_loss.py_expenditure_after_stop_loss,
|
||||
)
|
||||
shared = shared_savings_losses(
|
||||
gross, benchmark.benchmark_after_adjustments, inputs.risk_option
|
||||
)
|
||||
sequestration = sequestration_amount(shared.shared_savings_losses)
|
||||
c1, c2, c3, c4 = shared.retained_by_corridor
|
||||
return SettlementLines(
|
||||
py_benchmark=benchmark.py_benchmark,
|
||||
discount_rate=benchmark.discount_rate,
|
||||
discount_amount=benchmark.discount_amount,
|
||||
benchmark_after_discount=benchmark.benchmark_after_discount,
|
||||
retention_incentive_adjustment=benchmark.retention_incentive_adjustment,
|
||||
quality_withhold=benchmark.quality_withhold,
|
||||
quality_score=benchmark.quality_score,
|
||||
quality_earn_back=benchmark.quality_earn_back,
|
||||
net_quality_withhold=benchmark.net_quality_withhold,
|
||||
benchmark_after_adjustments=benchmark.benchmark_after_adjustments,
|
||||
capitation_payments=expenditure.capitation_payments,
|
||||
participant_provider_claims=expenditure.participant_provider_claims,
|
||||
preferred_provider_claims=expenditure.preferred_provider_claims,
|
||||
non_aco_provider_claims=expenditure.non_aco_provider_claims,
|
||||
total_ffs_payments=expenditure.total_ffs_payments,
|
||||
py_expenditure=expenditure.py_expenditure,
|
||||
stop_loss_charge=stop_loss.stop_loss_charge,
|
||||
stop_loss_payout=stop_loss.stop_loss_payout,
|
||||
stop_loss_neutrality_factor=stop_loss.stop_loss_neutrality_factor,
|
||||
adjusted_stop_loss_payout=stop_loss.adjusted_stop_loss_payout,
|
||||
net_stop_loss_impact=stop_loss.net_stop_loss_impact,
|
||||
py_expenditure_after_stop_loss=stop_loss.py_expenditure_after_stop_loss,
|
||||
gross_savings_losses=gross,
|
||||
shared_savings_losses=shared.shared_savings_losses,
|
||||
retained_corridor_1=c1,
|
||||
retained_corridor_2=c2,
|
||||
retained_corridor_3=c3,
|
||||
retained_corridor_4=c4,
|
||||
sequestration_amount=sequestration,
|
||||
shared_savings_losses_after_sequestration=cents(
|
||||
shared.shared_savings_losses - sequestration
|
||||
),
|
||||
)
|
||||
324
src/lead/stop_loss.py
Normal file
324
src/lead/stop_loss.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""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,
|
||||
)
|
||||
139
src/lead/timing.py
Normal file
139
src/lead/timing.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""LEAD settlement timing and which basis a settlement run states.
|
||||
|
||||
Final Financial Settlement includes claims run out through March 31 of the
|
||||
year after the PY; Provisional Settlement, before then, applies a
|
||||
completion factor instead (§5.1 / Table 5.1.1). So a settlement computed
|
||||
before the run-out closes states a projection, and partial-year actual
|
||||
claims are labelled as such, never the headline.
|
||||
|
||||
A run declares one of three modes: ``projection`` (may state a labelled
|
||||
projection), ``actual`` (every PY number must come from PY data; a run
|
||||
before the run-out closes fails), or ``rewind_test`` (development only,
|
||||
behaves as ``projection``). Which basis a run may use before the run-out
|
||||
closes is a run-configuration choice, not a CMS rule; every result names
|
||||
the basis it used.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 / Table 5.1.1.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import Final
|
||||
|
||||
PROJECTION: Final = "projection"
|
||||
ACTUAL: Final = "actual"
|
||||
REWIND_TEST: Final = "rewind_test"
|
||||
RUN_MODES: Final = (PROJECTION, ACTUAL, REWIND_TEST)
|
||||
|
||||
PROJECTED_BASIS: Final = "projected_from_base_year_experience"
|
||||
ACTUAL_CLAIMS_BASIS: Final = "actual_performance_year_claims"
|
||||
PROJECTED_STATUS: Final = "projected"
|
||||
PARTIAL_YEAR_STATUS: Final = "actual_to_date_partial_year"
|
||||
COMPUTED_STATUS: Final = "computed_from_calculated_inputs"
|
||||
PY_CLAIMS_INPUT: Final = "performance_year_claims_by_provider_class"
|
||||
RUNOUT_REMEDY: Final = (
|
||||
"wait for the Performance Year claims run-out (through Q1 of the following "
|
||||
"year) or run in projection mode"
|
||||
)
|
||||
|
||||
CMS_SETTLEMENT_REPORT: Final = "cms_settlement_report"
|
||||
CMS_SETTLEMENT_REPORT_MISSING: Final = "cms_settlement_report_source_missing"
|
||||
PROVISIONAL_NOT_YET_MADE: Final = "provisional_settlement_not_yet_made"
|
||||
FINAL_NOT_YET_MADE: Final = "final_settlement_not_yet_made"
|
||||
|
||||
|
||||
def validate_run_mode(run_mode: str) -> str:
|
||||
"""Return the run mode, or raise naming the three modes.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 (run-configuration
|
||||
choice, not a CMS rule).
|
||||
"""
|
||||
if run_mode not in RUN_MODES:
|
||||
raise ValueError(
|
||||
f"run_mode must be one of {', '.join(RUN_MODES)}; got {run_mode!r}"
|
||||
)
|
||||
return run_mode
|
||||
|
||||
|
||||
def require_projection_allowed(run_mode: str, input_name: str, remedy: str) -> None:
|
||||
"""Raise in ``actual`` mode, where a PY input without rows may not be
|
||||
replaced by a projection; ``projection`` and ``rewind_test`` continue.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 (run-configuration
|
||||
choice, not a CMS rule).
|
||||
"""
|
||||
if validate_run_mode(run_mode) == ACTUAL:
|
||||
raise ValueError(
|
||||
f"{input_name} has no complete performance-year rows as of the as-of "
|
||||
f"date and run_mode={ACTUAL!r} forbids a projection; {remedy}"
|
||||
)
|
||||
|
||||
|
||||
def settlement_runout_end(performance_year: int) -> date:
|
||||
"""Return the last day of the Final Settlement claims run-out: March 31
|
||||
of the year after the PY.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 / Table 5.1.1
|
||||
(Claims Run-Out).
|
||||
"""
|
||||
return date(performance_year + 1, 3, 31)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SettlementBasis:
|
||||
"""The basis a run states, the status of its headline rows and of its
|
||||
actual-claims rows."""
|
||||
|
||||
basis: str
|
||||
headline_status: str
|
||||
actual_rows_status: str
|
||||
as_of_date: date
|
||||
|
||||
|
||||
def settlement_basis_for_run(
|
||||
run_mode: str, as_of_date: date, performance_year: int, py_claim_rows: int
|
||||
) -> SettlementBasis:
|
||||
"""Decide the basis of one settlement run.
|
||||
|
||||
Actual once PY claim rows exist and the run-out has closed; before that
|
||||
a projection (refused in ``actual`` mode), with any partial-year actual
|
||||
rows labelled ``actual_to_date_partial_year``. PY claim rows observed
|
||||
before the PY starts are data from the future: a feed error.
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 / Table 5.1.1.
|
||||
"""
|
||||
if py_claim_rows and as_of_date < date(performance_year, 1, 1):
|
||||
raise ValueError(
|
||||
f"{PY_CLAIMS_INPUT}: {py_claim_rows} performance-period rows dated after "
|
||||
f"the as-of date {as_of_date.isoformat()}; data from the future is a "
|
||||
"feed error"
|
||||
)
|
||||
if py_claim_rows and as_of_date > settlement_runout_end(performance_year):
|
||||
return SettlementBasis(
|
||||
ACTUAL_CLAIMS_BASIS, COMPUTED_STATUS, COMPUTED_STATUS, as_of_date
|
||||
)
|
||||
require_projection_allowed(run_mode, PY_CLAIMS_INPUT, RUNOUT_REMEDY)
|
||||
rows_status = PARTIAL_YEAR_STATUS if py_claim_rows else PROJECTED_STATUS
|
||||
return SettlementBasis(PROJECTED_BASIS, PROJECTED_STATUS, rows_status, as_of_date)
|
||||
|
||||
|
||||
def settlement_stage_input(
|
||||
as_of_date: date, stage_start: date, reported: float | None, not_yet_status: str
|
||||
) -> tuple[float | None, str]:
|
||||
"""Return a settlement-stage amount and its status.
|
||||
|
||||
The amount is CMS's settlement-report figure when supplied; zero while
|
||||
the as-of date precedes the stage (nothing has been settled yet); and
|
||||
otherwise missing (None) until the report arrives. Used for what
|
||||
Provisional Settlement distributed (Table 5.5.1 line 1) and the
|
||||
High-Performers Pool incentive (line 5).
|
||||
|
||||
Source: LEAD PY2027 methodology (bib E4PEU3HV) §5.1 / §5.5.
|
||||
"""
|
||||
if reported is not None:
|
||||
return float(reported), CMS_SETTLEMENT_REPORT
|
||||
if as_of_date < stage_start:
|
||||
return 0.0, not_yet_status
|
||||
return None, CMS_SETTLEMENT_REPORT_MISSING
|
||||
6
tests/lead/__init__.py
Normal file
6
tests/lead/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Tests for lead — the LEAD Model PY2027 financial methodology.
|
||||
|
||||
The expected values are the worked examples CMS printed in the LEAD PY2027
|
||||
methodology paper (bib E4PEU3HV), transcribed in ``tests/lead/paper.py``;
|
||||
every Expr test runs on polars and on DuckDB through narwhals.
|
||||
"""
|
||||
44
tests/lead/conftest.py
Normal file
44
tests/lead/conftest.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Backends for the lead Expr tests: every frame test runs on polars and on
|
||||
DuckDB through narwhals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from typing import Any
|
||||
|
||||
import duckdb
|
||||
import narwhals as nw
|
||||
import polars as pl
|
||||
import pytest
|
||||
|
||||
|
||||
class Backend:
|
||||
"""Build a native frame on one backend and read results back as rows."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.con = duckdb.connect() if name == "duckdb" else None
|
||||
|
||||
def frame(self, data: Any, schema: dict[str, Any] | None = None) -> Any:
|
||||
df = pl.DataFrame(data, schema=schema)
|
||||
return df if self.con is None else self.con.from_arrow(df.to_arrow())
|
||||
|
||||
def rows(self, native: Any) -> list[dict[str, Any]]:
|
||||
return nw.from_native(native).lazy().collect("polars").to_native().to_dicts()
|
||||
|
||||
def select(self, data: Any, schema: dict[str, Any] | None = None, **exprs: nw.Expr):
|
||||
frame = nw.from_native(self.frame(data, schema))
|
||||
return self.rows(frame.select(**exprs).to_native())
|
||||
|
||||
def close(self) -> None:
|
||||
if self.con is not None:
|
||||
self.con.close()
|
||||
|
||||
|
||||
@pytest.fixture(params=["polars", "duckdb"])
|
||||
def backend(request: pytest.FixtureRequest) -> Iterator[Backend]:
|
||||
engine = Backend(request.param)
|
||||
try:
|
||||
yield engine
|
||||
finally:
|
||||
engine.close()
|
||||
250
tests/lead/paper.py
Normal file
250
tests/lead/paper.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Worked examples printed in the LEAD PY2027 methodology (bib E4PEU3HV).
|
||||
|
||||
Test fixtures only — never runtime parameters. Each value is copied from
|
||||
the paper as printed; percentages are written as shares.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# ── Table 5.6.1: Full Settlement Calculation ──────────────────────────────
|
||||
|
||||
TABLE_5_6_1_INPUTS = {
|
||||
"py_benchmark": 150_000_000.00, # line 1
|
||||
"spending_designation": "Lower-spending", # 3.00% discount (line 2)
|
||||
"performance_year": 2027,
|
||||
"quality_score": 0.95, # line 6
|
||||
"capitation_payments": 10_000_000.00, # line 10
|
||||
"participant_provider_claims": 1_003_442.00, # line 11
|
||||
"preferred_provider_claims": 33_435_084.00, # line 12
|
||||
"non_aco_provider_claims": 91_355_457.00, # line 13
|
||||
"stop_loss_elected": True,
|
||||
"stop_loss_charge": 2_948_334.28, # line 17
|
||||
"stop_loss_payout": 2_900_000.00, # line 18
|
||||
"stop_loss_neutrality_factor": 0.930, # line 19
|
||||
"retention_incentive_applies": False,
|
||||
}
|
||||
|
||||
# (paper line, SettlementLines field). Lines 16, 23 and 24 restate 15, 22
|
||||
# and 9; "26.1"–"26.4" are the "Retained ... in Corridor n" lines.
|
||||
TABLE_5_6_1_LINES = (
|
||||
("1", "py_benchmark"),
|
||||
("2", "discount_rate"),
|
||||
("3", "discount_amount"),
|
||||
("4", "benchmark_after_discount"),
|
||||
("5", "quality_withhold"),
|
||||
("6", "quality_score"),
|
||||
("7", "quality_earn_back"),
|
||||
("8", "net_quality_withhold"),
|
||||
("9", "benchmark_after_adjustments"),
|
||||
("10", "capitation_payments"),
|
||||
("11", "participant_provider_claims"),
|
||||
("12", "preferred_provider_claims"),
|
||||
("13", "non_aco_provider_claims"),
|
||||
("14", "total_ffs_payments"),
|
||||
("15", "py_expenditure"),
|
||||
("16", "py_expenditure"),
|
||||
("17", "stop_loss_charge"),
|
||||
("18", "stop_loss_payout"),
|
||||
("19", "stop_loss_neutrality_factor"),
|
||||
("20", "adjusted_stop_loss_payout"),
|
||||
("21", "net_stop_loss_impact"),
|
||||
("22", "py_expenditure_after_stop_loss"),
|
||||
("23", "py_expenditure_after_stop_loss"),
|
||||
("24", "benchmark_after_adjustments"),
|
||||
("25", "gross_savings_losses"),
|
||||
("26", "shared_savings_losses"),
|
||||
("26.1", "retained_corridor_1"),
|
||||
("26.2", "retained_corridor_2"),
|
||||
("26.3", "retained_corridor_3"),
|
||||
("26.4", "retained_corridor_4"),
|
||||
("27", "sequestration_amount"),
|
||||
("28", "shared_savings_losses_after_sequestration"),
|
||||
)
|
||||
|
||||
_COMMON = {
|
||||
"1": 150_000_000.00,
|
||||
"5": 4_500_000.00, # printed ($4,500,000.00): a reduction
|
||||
"6": 0.95,
|
||||
"7": 4_275_000.00,
|
||||
"8": 225_000.00, # printed ($225,000.00): a reduction
|
||||
"10": 10_000_000.00,
|
||||
"11": 1_003_442.00,
|
||||
"12": 33_435_084.00,
|
||||
"13": 91_355_457.00,
|
||||
"14": 125_793_983.00,
|
||||
"15": 135_793_983.00,
|
||||
"16": 135_793_983.00,
|
||||
"17": 2_948_334.28,
|
||||
"18": 2_900_000.00,
|
||||
"19": 0.930,
|
||||
"20": 2_697_000.00,
|
||||
"21": 251_334.28,
|
||||
"22": 136_045_317.28,
|
||||
"23": 136_045_317.28,
|
||||
"26.2": 0.00,
|
||||
"26.3": 0.00,
|
||||
"26.4": 0.00,
|
||||
}
|
||||
|
||||
TABLE_5_6_1 = {
|
||||
"Global": {
|
||||
**_COMMON,
|
||||
"2": 0.03,
|
||||
"3": 4_500_000.00,
|
||||
"4": 145_500_000.00,
|
||||
"9": 145_275_000.00,
|
||||
"24": 145_275_000.00,
|
||||
"25": 9_229_682.72,
|
||||
"26": 9_229_682.72,
|
||||
"26.1": 9_229_682.72,
|
||||
"27": 184_593.65,
|
||||
"28": 9_045_089.07,
|
||||
},
|
||||
"Professional": {
|
||||
**_COMMON,
|
||||
"2": 0.00,
|
||||
"3": 0.00, # printed "N/A"
|
||||
"4": 150_000_000.00,
|
||||
"9": 149_775_000.00,
|
||||
"24": 149_775_000.00,
|
||||
"25": 13_729_682.72,
|
||||
"26": 8_237_809.63,
|
||||
"26.1": 8_237_809.63,
|
||||
"27": 164_756.19,
|
||||
"28": 8_073_053.44,
|
||||
},
|
||||
}
|
||||
|
||||
# ── Table 5.5.1: Calculation of Total Monies Owed (Professional, TCC) ─────
|
||||
|
||||
TABLE_5_5_1 = {
|
||||
"1": 6_456_540.00, # Provisional Settlement Shared Savings (Losses)
|
||||
"2": 8_073_053.43, # Final Settlement Shared Savings (Losses)
|
||||
"3": 1_616_513.43, # Shared Savings (Losses) Owed
|
||||
"4": 160_700.00, # Under (Over) Payments from Payment Arrangements
|
||||
"4.capitation": 160_700.00,
|
||||
"5": 100_000.00, # High-Performers Pool Incentive
|
||||
"6": 260_700.00, # Adjustments Owed
|
||||
"7": 1_877_213.43, # Total Monies Owed
|
||||
}
|
||||
|
||||
# ── Box 5.3.3: Stop-Loss charge and payout illustration ───────────────────
|
||||
|
||||
STOP_LOSS_CHARGE = {
|
||||
"1": 946.97, # Average Reference Year Expenditure PBPM, Trended
|
||||
"2": 132_000, # ACO-Aligned Beneficiary Months in PY
|
||||
"3": 1.160, # ACO Average Risk Score in PY
|
||||
"4": 145_000_046.40, # Total Trended, Risk-Adjusted Reference Expenditure
|
||||
"5": 0.0203, # 3-Year Average Payout Percentage, printed 2.03%
|
||||
"RY": (0.0196, 0.0209, 0.0205),
|
||||
"6": 2_948_334.28, # PY Stop-Loss Charge
|
||||
}
|
||||
|
||||
STOP_LOSS_PAYOUT = {
|
||||
"attachment_point": 150_000.00,
|
||||
"predicted": 100_000.00,
|
||||
"actual": 500_000.00,
|
||||
"residual": 400_000.00,
|
||||
"below_attachment_point": 150_000.00,
|
||||
"band_1_exposure": 150_000.00,
|
||||
"band_2_exposure": 100_000.00,
|
||||
"total_payout": 220_000.00,
|
||||
"band_1_payout": 120_000.00,
|
||||
"band_2_payout": 100_000.00,
|
||||
}
|
||||
|
||||
# ── Table 3.3.3: Historical Benchmark Calculation (Newly Entering, A&D) ───
|
||||
|
||||
TABLE_3_3_3 = {
|
||||
# base year: (payments, months, risk, line 3, line 5, trend, line 7, weight, line 9)
|
||||
"BY1": (87_856_003.26, 91_366, 1.122, 961.58, 857.03, 1.104, 946.16, 0.100, 94.62),
|
||||
"BY2": (93_375_409.42, 94_577, 1.115, 987.30, 885.47, 1.076, 952.76, 0.300, 285.83),
|
||||
"BY3": (
|
||||
106_794_359.82,
|
||||
104_671,
|
||||
1.087,
|
||||
1_020.29,
|
||||
938.63,
|
||||
1.000,
|
||||
938.63,
|
||||
0.600,
|
||||
563.18,
|
||||
),
|
||||
"historical_benchmark": 943.62,
|
||||
}
|
||||
|
||||
# ── Table 3.4.2 / Tables 3.4.3.1–3.4.3.4 ──────────────────────────────────
|
||||
|
||||
# category: (regional FFS PBPM, Historical Benchmark PBPM, BY 3 weight,
|
||||
# uncapped REA, USPCC, PY risk score, REA cap, applied REA,
|
||||
# Adjusted Historical Benchmark)
|
||||
REA_TABLES = {
|
||||
"A&D": (1_050, 1_000, 0.80, 25, 1_000, 1.02, 51, 25, 1_025),
|
||||
"ESRD": (8_000, 8_500, 0.05, 0, 8_000, 0.99, 396, 0, 8_500),
|
||||
"High Needs": (3_700, 3_200, 0.15, 250, 3_500, 1.03, 180.25, 180.25, 3_380.25),
|
||||
}
|
||||
TABLE_3_4_2_DIFFERENCE = 90.0
|
||||
|
||||
# ── Table 3.5.3.2: Hybrid Alignment Benchmark Adjustment ──────────────────
|
||||
|
||||
TABLE_3_5_3_2 = {
|
||||
"1": 1_125.00,
|
||||
"2": 60_000,
|
||||
"3": 4_500,
|
||||
"4": 1.0099,
|
||||
"5": 1_136.14,
|
||||
"6": 67_500_000.00,
|
||||
"7": 5_112_618.75,
|
||||
"8": 72_612_618.75,
|
||||
}
|
||||
|
||||
# ── Table 3.5.4: Combined Benchmark Calculation ───────────────────────────
|
||||
|
||||
# category: (adjusted HB PBPM, trend, PY risk, full months, hybrid months,
|
||||
# hybrid factor, line 8)
|
||||
TABLE_3_5_4_CLAIMS = {
|
||||
"ESRD": (10_175.00, 1.034, 1.007, 981, 120, 1.0099, 11_677_237.29),
|
||||
"A&D": (796.00, 1.042, 1.045, 56_677, 11_570, 1.0085, 59_238_767.92),
|
||||
"High Needs": (3_739.00, 1.037, 1.020, 9_806, 2_864, 1.0111, 50_234_182.06),
|
||||
}
|
||||
# category: (adjusted HB PBPM, trend, PY risk, full+Q1 months,
|
||||
# (Q2, Q3, Q4 months), (Q2, Q3, Q4 factors), line 22)
|
||||
TABLE_3_5_4_VOLUNTARY = {
|
||||
"ESRD": (
|
||||
9_543.00,
|
||||
1.034,
|
||||
0.990,
|
||||
421,
|
||||
(76, 62, 51),
|
||||
(1.0099, 1.0236, 1.0352),
|
||||
5_998_140.95,
|
||||
),
|
||||
"A&D": (
|
||||
716.00,
|
||||
1.042,
|
||||
0.866,
|
||||
5_514,
|
||||
(825, 753, 538),
|
||||
(1.0085, 1.0247, 1.0431),
|
||||
4_961_259.64,
|
||||
),
|
||||
"High Needs": (
|
||||
4_109.00,
|
||||
1.037,
|
||||
1.061,
|
||||
2_393,
|
||||
(734, 549, 240),
|
||||
(1.0111, 1.0233, 1.0527),
|
||||
17_855_909.55,
|
||||
),
|
||||
}
|
||||
TABLE_3_5_4_TOTALS = {
|
||||
"8": 121_150_187.27,
|
||||
"22": 28_815_310.14,
|
||||
"24": {
|
||||
"ESRD": 17_675_378.24,
|
||||
"A&D": 64_200_027.56,
|
||||
"High Needs": 68_090_091.61,
|
||||
"TOTAL": 149_965_497.41,
|
||||
},
|
||||
}
|
||||
412
tests/lead/test_benchmark.py
Normal file
412
tests/lead/test_benchmark.py
Normal file
@@ -0,0 +1,412 @@
|
||||
"""lead.benchmark reproduces the §3 worked examples: Box 3.3.2, Table 3.3.3,
|
||||
Table 3.4.2, Tables 3.4.3.1–3.4.3.9, §3.5.1–3.5.2, Table 3.5.3.2 and
|
||||
Table 3.5.4."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from lead.benchmark import (
|
||||
CAPPED,
|
||||
CAPPED_AT_UPPER,
|
||||
FLOORED,
|
||||
RAISED_TO_LOWER,
|
||||
WITHIN_CAP,
|
||||
WITHIN_GUARDRAIL,
|
||||
base_year_alignment_minimum,
|
||||
base_year_is_sufficient,
|
||||
base_year_weights,
|
||||
benchmark_adjustment_cap,
|
||||
benchmark_adjustment_cap_rate,
|
||||
benchmark_dollars,
|
||||
capped_risk_score,
|
||||
contributes_to_voluntary_benchmark,
|
||||
cumulative_acpt,
|
||||
expenditure_pbpm,
|
||||
growth_rate,
|
||||
historical_benchmark,
|
||||
hybrid_adjustment_factor,
|
||||
month_weighted_average,
|
||||
national_trend_weight,
|
||||
performance_year_benchmark_pbpm,
|
||||
prior_savings_adjustment,
|
||||
prior_savings_cap,
|
||||
prior_savings_pbpm,
|
||||
ratio,
|
||||
regional_efficiency_adjustment,
|
||||
regional_efficiency_eligible,
|
||||
regional_ffs_expenditure,
|
||||
risk_standardize,
|
||||
spending_designation,
|
||||
three_way_blended_trend,
|
||||
total_benchmark,
|
||||
two_way_blended_trend,
|
||||
voluntary_benchmark_minimum_met,
|
||||
voluntary_guardrail,
|
||||
)
|
||||
from lead.money import cents
|
||||
from tests.lead.paper import (
|
||||
REA_TABLES,
|
||||
TABLE_3_3_3,
|
||||
TABLE_3_4_2_DIFFERENCE,
|
||||
TABLE_3_5_3_2,
|
||||
TABLE_3_5_4_CLAIMS,
|
||||
TABLE_3_5_4_TOTALS,
|
||||
TABLE_3_5_4_VOLUNTARY,
|
||||
)
|
||||
|
||||
# ── Box 3.3.2 and Table 3.3.3 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_box_3_3_2_national_regional_blended_trend():
|
||||
national = growth_rate(1_120, 1_000)
|
||||
regional = growth_rate(1_155, 1_050)
|
||||
weight = national_trend_weight([(20_000, 100_000), (5_000, 50_000)])
|
||||
assert (national, round(regional, 3), round(weight, 6)) == (1.12, 1.1, 0.18)
|
||||
trend = two_way_blended_trend(national, regional, weight)
|
||||
assert round(trend, 3) == 1.104 # printed 1.104; the text's "1.103" is a slip
|
||||
|
||||
|
||||
def test_trend_inputs_without_positive_denominators_are_missing():
|
||||
assert growth_rate(0, 1_000) is None
|
||||
assert growth_rate(1_000, 0) is None
|
||||
assert national_trend_weight([]) is None
|
||||
assert national_trend_weight([(10, 0)]) is None
|
||||
assert two_way_blended_trend(None, 1.1, 0.2) is None
|
||||
assert ratio(None, 1.0) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("year", ["BY1", "BY2", "BY3"])
|
||||
def test_table_3_3_3_lines_3_to_9(year):
|
||||
payments, months, risk, line3, line5, trend, line7, weight, line9 = TABLE_3_3_3[
|
||||
year
|
||||
]
|
||||
pbpm = expenditure_pbpm(payments, months)
|
||||
standardized = risk_standardize(pbpm, risk)
|
||||
# PBPMs are printed to the cent but carried unrounded: line 5 is
|
||||
# $857.03 from the unrounded line 3, $857.02 from the printed $961.58.
|
||||
assert cents(pbpm) == line3
|
||||
assert cents(standardized) == line5
|
||||
assert cents(standardized * trend) == line7
|
||||
assert cents(standardized * trend * weight) == line9
|
||||
|
||||
|
||||
def test_table_3_3_3_historical_benchmark():
|
||||
years = ("BY1", "BY2", "BY3")
|
||||
standardized = {
|
||||
y: risk_standardize(expenditure_pbpm(*TABLE_3_3_3[y][:2]), TABLE_3_3_3[y][2])
|
||||
for y in years
|
||||
}
|
||||
trends = {y: TABLE_3_3_3[y][5] for y in years}
|
||||
weights = base_year_weights("Newly Entering", years)
|
||||
benchmark = historical_benchmark(standardized, trends, weights)
|
||||
assert cents(benchmark) == TABLE_3_3_3["historical_benchmark"]
|
||||
# The printed line 9 cents sum to 943.63: the total is unrounded too.
|
||||
assert sum(TABLE_3_3_3[y][8] for y in years) == pytest.approx(943.63)
|
||||
|
||||
|
||||
def test_historical_benchmark_needs_every_weighted_year():
|
||||
assert historical_benchmark({"BY3": 900.0}, {"BY3": 1.0}, {}) is None
|
||||
assert historical_benchmark({}, {"BY3": 1.0}, {"BY3": 1.0}) is None
|
||||
assert historical_benchmark({"BY3": 900.0}, {"BY3": 1.0}, {"BY3": 1.0}) == 900.0
|
||||
|
||||
|
||||
def test_risk_standardize_needs_a_positive_score():
|
||||
assert risk_standardize(1_000.0, 0.0) is None
|
||||
assert expenditure_pbpm(1_000.0, 0) is None
|
||||
|
||||
|
||||
# ── §3.3.3–3.3.4 base-year weights and sufficiency ────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entry,years,weights",
|
||||
[
|
||||
("Newly Entering", ("BY1", "BY2", "BY3"), {"BY1": 0.1, "BY2": 0.3, "BY3": 0.6}),
|
||||
("Renewing", ("BY1", "BY2", "BY3"), {"BY1": 1 / 3, "BY2": 1 / 3, "BY3": 1 / 3}),
|
||||
("Newly Entering", ("BY1", "BY3"), {"BY1": 1 / 3, "BY3": 2 / 3}),
|
||||
("Renewing", ("BY2", "BY3"), {"BY2": 0.5, "BY3": 0.5}),
|
||||
("Renewing", ("BY2",), {"BY2": 1.0}),
|
||||
("Newly Entering", (), {}),
|
||||
],
|
||||
)
|
||||
def test_base_year_weights(entry, years, weights):
|
||||
assert base_year_weights(entry, years) == weights
|
||||
|
||||
|
||||
def test_base_year_weights_refuse_unknown_labels():
|
||||
with pytest.raises(ValueError, match="unknown entry type"):
|
||||
base_year_weights("New", ("BY3",))
|
||||
with pytest.raises(ValueError, match="unknown base years"):
|
||||
base_year_weights("Renewing", ("BY4",))
|
||||
|
||||
|
||||
def test_table_2_6_1_base_year_minimums():
|
||||
assert base_year_alignment_minimum("standard", 2027) == 3_000
|
||||
assert base_year_alignment_minimum("newly_entering", 2029) == 1_800
|
||||
assert base_year_alignment_minimum("high_needs_esrd", 2030) == 825
|
||||
assert base_year_alignment_minimum("high_needs_esrd", 2036) == 1_000
|
||||
assert base_year_is_sufficient(600, "newly_entering", 2027)
|
||||
assert not base_year_is_sufficient(599, "newly_entering", 2027)
|
||||
|
||||
|
||||
# ── §3.2.5 voluntary benchmarks ───────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"voluntary,expected",
|
||||
[
|
||||
(1_150.0, (1_100.0, CAPPED_AT_UPPER)),
|
||||
(850.0, (900.0, RAISED_TO_LOWER)),
|
||||
(1_050.0, (1_050.0, WITHIN_GUARDRAIL)),
|
||||
],
|
||||
)
|
||||
def test_voluntary_guardrail_example(voluntary, expected):
|
||||
benchmark, status = voluntary_guardrail(voluntary, 1_000.0)
|
||||
assert (round(benchmark, 6), status) == expected
|
||||
|
||||
|
||||
def test_voluntary_contribution_and_minimum():
|
||||
assert contributes_to_voluntary_benchmark("A&D", None)
|
||||
assert contributes_to_voluntary_benchmark("High Needs", "High Needs")
|
||||
assert not contributes_to_voluntary_benchmark("High Needs", "A&D")
|
||||
assert not contributes_to_voluntary_benchmark("High Needs", None)
|
||||
assert voluntary_benchmark_minimum_met(500)
|
||||
assert not voluntary_benchmark_minimum_met(499)
|
||||
|
||||
|
||||
# ── §3.4.1–3.4.2 regional FFS and spending designation ────────────────────
|
||||
|
||||
|
||||
def test_regional_ffs_expenditure_weights_risk_standardized_counties():
|
||||
# County A: $1,100 / 1.10 = $1,000 at 75%; county B: $1,200 / 1.00 at 25%.
|
||||
assert regional_ffs_expenditure(
|
||||
[(1_100.0, 1.10, 750.0), (1_200.0, 1.0, 250.0)]
|
||||
) == pytest.approx(1_050.0)
|
||||
assert regional_ffs_expenditure([(1_100.0, None, 750.0)]) is None
|
||||
assert regional_ffs_expenditure([(1_100.0, 1.1, 0.0)]) is None
|
||||
|
||||
|
||||
def test_table_3_4_2_spending_designation():
|
||||
rows = [(r[0], r[1], r[2]) for r in REA_TABLES.values()]
|
||||
result = spending_designation(rows)
|
||||
assert result.weighted_difference == pytest.approx(TABLE_3_4_2_DIFFERENCE)
|
||||
assert result.designation == "Lower-spending"
|
||||
assert (
|
||||
spending_designation([(900.0, 1_000.0, 1.0)]).designation == "Higher-spending"
|
||||
)
|
||||
# The paper names no tie.
|
||||
assert spending_designation([(1_000.0, 1_000.0, 1.0)]).designation is None
|
||||
with pytest.raises(ValueError, match="BY 3 aligned months"):
|
||||
spending_designation([(1_000.0, 900.0, 0.0)])
|
||||
|
||||
|
||||
# ── §3.4.3 Regional Efficiency Adjustment ─────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("category", list(REA_TABLES))
|
||||
def test_tables_3_4_3_1_to_3_4_3_4(category):
|
||||
regional, historical, _, uncapped, uspcc, risk, cap, applied, adjusted = REA_TABLES[
|
||||
category
|
||||
]
|
||||
rea = regional_efficiency_adjustment(
|
||||
regional,
|
||||
historical,
|
||||
eligible=regional_efficiency_eligible("Lower-spending", "Global"),
|
||||
uspcc_amount=uspcc,
|
||||
py_average_risk_score=risk,
|
||||
cap_rate=benchmark_adjustment_cap_rate(reduced=False),
|
||||
)
|
||||
assert rea.uncapped == uncapped
|
||||
assert cents(rea.cap) == cap
|
||||
assert cents(rea.applied) == applied
|
||||
assert cents(historical + rea.applied) == adjusted
|
||||
|
||||
|
||||
def test_rea_eligibility_and_missing_cap_inputs():
|
||||
assert not regional_efficiency_eligible("Higher-spending", "Global")
|
||||
assert not regional_efficiency_eligible("Lower-spending", "Professional")
|
||||
kwargs = dict(uspcc_amount=None, py_average_risk_score=1.0, cap_rate=0.05)
|
||||
# A positive REA never falls back to its uncapped amount.
|
||||
assert (
|
||||
regional_efficiency_adjustment(1_050, 1_000, eligible=True, **kwargs).applied
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
regional_efficiency_adjustment(950, 1_000, eligible=True, **kwargs).applied
|
||||
== 0.0
|
||||
)
|
||||
assert (
|
||||
regional_efficiency_adjustment(1_050, 1_000, eligible=False, **kwargs).applied
|
||||
== 0.0
|
||||
)
|
||||
|
||||
|
||||
def test_reduced_cap_rate():
|
||||
assert benchmark_adjustment_cap_rate(reduced=True) == 0.03
|
||||
assert benchmark_adjustment_cap(1_000, 1.02, 0.03) == pytest.approx(30.6)
|
||||
|
||||
|
||||
def test_table_3_4_3_6_weighted_rea_with_an_excluded_tin():
|
||||
rea = regional_efficiency_adjustment(
|
||||
3_700,
|
||||
3_000,
|
||||
eligible=True,
|
||||
uspcc_amount=3_500,
|
||||
py_average_risk_score=1.03,
|
||||
cap_rate=0.05,
|
||||
)
|
||||
assert (rea.difference, rea.uncapped, cents(rea.applied)) == (700, 350, 180.25)
|
||||
assert cents(month_weighted_average([(rea.applied, 8_000), (0.0, 2_000)])) == 144.20
|
||||
|
||||
|
||||
# ── §3.4.4 Prior Savings Adjustment ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_table_3_4_3_7_and_example_1():
|
||||
rows = [
|
||||
(120_000_000, 114_250_000, 115_000),
|
||||
(125_000_000, 126_200_000, 120_000),
|
||||
(130_000_000, 121_250_000, 125_000),
|
||||
]
|
||||
pbpm = [prior_savings_pbpm(*row) for row in rows]
|
||||
assert pbpm == [50.0, -10.0, 70.0]
|
||||
average = sum(pbpm) / 3
|
||||
assert cents(average) == 36.67
|
||||
psa = prior_savings_adjustment(average, 120_000, 240_000, cap=None)
|
||||
assert psa.eligible and psa.proration_factor == 0.5
|
||||
assert cents(psa.prorated_average_savings) == 18.33
|
||||
assert cents(psa.uncapped) == 9.17
|
||||
assert psa.applied is None # eligible, but no cap supplied
|
||||
|
||||
|
||||
def test_table_3_4_3_8_9_examples_2_and_3():
|
||||
average = month_weighted_average([(30.0, 80_000), (70.0, 20_000)])
|
||||
assert average == pytest.approx(38.0)
|
||||
cap = prior_savings_cap(
|
||||
[(0.80, 1_000, 1.02), (0.05, 8_000, 0.99), (0.15, 3_500, 1.03)], cap_rate=0.05
|
||||
)
|
||||
assert cents(cap) == 87.64
|
||||
psa = prior_savings_adjustment(average, 100_000, 125_000, cap)
|
||||
assert psa.proration_factor == 0.8
|
||||
assert cents(psa.prorated_average_savings) == 30.40
|
||||
assert cents(psa.applied) == 15.20
|
||||
|
||||
|
||||
def test_prior_savings_eligibility_and_proration_cap():
|
||||
not_eligible = prior_savings_adjustment(-5.0, 100_000, 100_000, cap=50.0)
|
||||
assert (not_eligible.eligible, not_eligible.applied) == (False, 0.0)
|
||||
shrank = prior_savings_adjustment(40.0, 200_000, 100_000, cap=5.0)
|
||||
assert shrank.proration_factor == 1.0
|
||||
assert shrank.applied == 5.0 # capped
|
||||
with pytest.raises(ValueError, match="PY beneficiary months"):
|
||||
prior_savings_adjustment(40.0, 1, 0, cap=None)
|
||||
assert prior_savings_pbpm(1.0, 0.0, 0) is None
|
||||
|
||||
|
||||
# ── §3.5.1 three-way trend and ACPT ───────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"year,acpt,applied",
|
||||
[
|
||||
(2027, 1.070, 1.053), # raw 1.0567 capped at two-way + 0.3 points
|
||||
(2027, 1.020, 1.048), # raw 1.04 raised to two-way − 0.2 points
|
||||
(2027, 1.055, (2 / 3) * 1.05 + 1.055 / 3), # within the band
|
||||
(2028, 1.070, 1.056), # PY 2 caps at +0.6 points
|
||||
(2028, 1.065, 1.055), # within PY 2's wider band
|
||||
(2032, 0.950, 1.04), # PY 6: −1.0 points
|
||||
],
|
||||
)
|
||||
def test_three_way_blended_trend_guardrail(year, acpt, applied):
|
||||
trend = three_way_blended_trend(1.05, acpt, year)
|
||||
assert trend.applied == pytest.approx(applied)
|
||||
assert trend.lower_bound <= trend.applied <= trend.upper_bound
|
||||
|
||||
|
||||
def test_cumulative_acpt():
|
||||
assert cumulative_acpt([0.04, 0.05]) == pytest.approx(1.092)
|
||||
assert cumulative_acpt([0.04, None]) is None
|
||||
assert cumulative_acpt([]) is None
|
||||
|
||||
|
||||
# ── §3.5.2 risk adjustment and the growth cap ─────────────────────────────
|
||||
|
||||
|
||||
def test_section_3_5_2_examples():
|
||||
assert performance_year_benchmark_pbpm(1_100.0, 0.0, 1.0, 1.02) == pytest.approx(
|
||||
1_122.0
|
||||
)
|
||||
score, status = capped_risk_score(1.000, 1.050, "A&D")
|
||||
assert (score, status) == (1.03, CAPPED)
|
||||
assert performance_year_benchmark_pbpm(1_100.0, 0.0, 1.0, score) == pytest.approx(
|
||||
1_133.0
|
||||
)
|
||||
|
||||
|
||||
def test_risk_score_cap_by_category():
|
||||
assert capped_risk_score(1.0, 0.90, "ESRD") == (0.97, FLOORED)
|
||||
assert capped_risk_score(1.0, 1.035, "High Needs") == (1.035, WITHIN_CAP)
|
||||
assert capped_risk_score(1.0, 1.05, "High Needs") == (1.04, CAPPED)
|
||||
|
||||
|
||||
def test_missing_adjustment_stays_missing():
|
||||
assert performance_year_benchmark_pbpm(1_000.0, None, 1.03, 1.0) is None
|
||||
|
||||
|
||||
# ── §3.5.3 hybrid alignment and §3.5.4 combination ────────────────────────
|
||||
|
||||
|
||||
def test_table_3_5_3_2_hybrid_alignment():
|
||||
t = TABLE_3_5_3_2
|
||||
assert cents(t["1"] * t["4"]) == t["5"]
|
||||
dollars = benchmark_dollars(t["1"], t["2"], [(t["3"], t["4"])])
|
||||
assert (dollars.full_year, dollars.hybrid, dollars.total) == (
|
||||
t["6"],
|
||||
t["7"],
|
||||
t["8"],
|
||||
)
|
||||
assert hybrid_adjustment_factor(1_009.9, 1_000.0) == pytest.approx(1.0099)
|
||||
assert hybrid_adjustment_factor(1.0, 0.0) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("category", list(TABLE_3_5_4_CLAIMS))
|
||||
def test_table_3_5_4_claims_line_8(category):
|
||||
pbpm, trend, risk, full, hybrid, factor, line8 = TABLE_3_5_4_CLAIMS[category]
|
||||
py_pbpm = performance_year_benchmark_pbpm(pbpm, 0.0, trend, risk)
|
||||
assert benchmark_dollars(py_pbpm, full, [(hybrid, factor)]).total == line8
|
||||
|
||||
|
||||
@pytest.mark.parametrize("category", list(TABLE_3_5_4_VOLUNTARY))
|
||||
def test_table_3_5_4_voluntary_line_22(category):
|
||||
pbpm, trend, risk, full, months, factors, line22 = TABLE_3_5_4_VOLUNTARY[category]
|
||||
py_pbpm = performance_year_benchmark_pbpm(pbpm, 0.0, trend, risk)
|
||||
hybrid = list(zip(months, factors, strict=True))
|
||||
assert benchmark_dollars(py_pbpm, full, hybrid).total == line22
|
||||
|
||||
|
||||
def test_table_3_5_4_totals_sum_the_cent_rounded_cells():
|
||||
claims = {c: v[-1] for c, v in TABLE_3_5_4_CLAIMS.items()}
|
||||
voluntary = {c: v[-1] for c, v in TABLE_3_5_4_VOLUNTARY.items()}
|
||||
assert total_benchmark(claims.values()) == TABLE_3_5_4_TOTALS["8"]
|
||||
assert total_benchmark(voluntary.values()) == TABLE_3_5_4_TOTALS["22"]
|
||||
line24 = {c: total_benchmark([claims[c], voluntary[c]]) for c in claims}
|
||||
for category, expected in TABLE_3_5_4_TOTALS["24"].items():
|
||||
if category != "TOTAL":
|
||||
assert line24[category] == expected
|
||||
assert total_benchmark(line24.values()) == TABLE_3_5_4_TOTALS["24"]["TOTAL"]
|
||||
|
||||
|
||||
def test_table_3_5_4_esrd_line_24_needs_cent_rounded_cells():
|
||||
# Unrounded, ESRD line 8 + line 22 = 17,675,378.2467 → .25, not the printed .24.
|
||||
pbpm, trend, risk, full, hybrid, factor, _ = TABLE_3_5_4_CLAIMS["ESRD"]
|
||||
claims = pbpm * trend * risk * (full + hybrid * factor)
|
||||
pbpm, trend, risk, full, months, factors, _ = TABLE_3_5_4_VOLUNTARY["ESRD"]
|
||||
voluntary = (
|
||||
pbpm
|
||||
* trend
|
||||
* risk
|
||||
* (full + sum(m * f for m, f in zip(months, factors, strict=True)))
|
||||
)
|
||||
assert cents(claims + voluntary) == 17_675_378.25
|
||||
assert TABLE_3_5_4_TOTALS["24"]["ESRD"] == 17_675_378.24
|
||||
335
tests/lead/test_express_benchmark.py
Normal file
335
tests/lead/test_express_benchmark.py
Normal file
@@ -0,0 +1,335 @@
|
||||
"""lead.express.benchmark: the §3 worked examples as frames on polars and
|
||||
DuckDB, plus nulls for missing inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import narwhals as nw
|
||||
import polars as pl
|
||||
import pytest
|
||||
|
||||
from lead import benchmark as scalar
|
||||
from lead.express import cents
|
||||
from lead.express.benchmark import (
|
||||
RISK_CAP_REFERENCE_MISSING,
|
||||
benchmark_adjustment_cap,
|
||||
benchmark_dollars,
|
||||
capped_risk_score,
|
||||
contributes_to_voluntary_benchmark,
|
||||
county_market_share,
|
||||
county_weight,
|
||||
growth_rate,
|
||||
month_weighted_mean,
|
||||
performance_year_benchmark_pbpm,
|
||||
regional_efficiency_adjustment,
|
||||
regional_efficiency_eligible,
|
||||
risk_growth_cap_status,
|
||||
risk_standardized_pbpm,
|
||||
three_way_blended_trend,
|
||||
trended_weighted_pbpm,
|
||||
two_way_blended_trend,
|
||||
uncapped_regional_efficiency_adjustment,
|
||||
voluntary_guardrail,
|
||||
voluntary_guardrail_status,
|
||||
)
|
||||
from lead.money import cents as scalar_cents
|
||||
from tests.lead.paper import (
|
||||
REA_TABLES,
|
||||
TABLE_3_3_3,
|
||||
TABLE_3_5_4_CLAIMS,
|
||||
TABLE_3_5_4_TOTALS,
|
||||
TABLE_3_5_4_VOLUNTARY,
|
||||
)
|
||||
|
||||
|
||||
def test_box_3_3_2_as_a_county_frame(backend):
|
||||
counties = {
|
||||
"aligned": [20_000.0, 5_000.0],
|
||||
"eligible": [100_000.0, 50_000.0],
|
||||
"total": [25_000.0, 25_000.0],
|
||||
}
|
||||
rows = backend.select(
|
||||
counties,
|
||||
contribution=county_market_share("aligned", "eligible")
|
||||
* county_weight("aligned", "total"),
|
||||
)
|
||||
weight = sum(r["contribution"] for r in rows)
|
||||
assert weight == pytest.approx(0.18)
|
||||
(trend,) = backend.select(
|
||||
{
|
||||
"nat_by3": [1_120.0],
|
||||
"nat_by1": [1_000.0],
|
||||
"reg_by3": [1_155.0],
|
||||
"reg_by1": [1_050.0],
|
||||
"w": [weight],
|
||||
},
|
||||
trend=two_way_blended_trend(
|
||||
growth_rate("nat_by3", "nat_by1"), growth_rate("reg_by3", "reg_by1"), "w"
|
||||
),
|
||||
)
|
||||
assert round(trend["trend"], 3) == 1.104
|
||||
|
||||
|
||||
def test_growth_and_shares_are_null_without_positive_denominators(backend):
|
||||
data = {"a": [0.0, 10.0, None], "b": [5.0, 0.0, 5.0]}
|
||||
rows = backend.select(
|
||||
data,
|
||||
{"a": pl.Float64, "b": pl.Float64},
|
||||
g=growth_rate("a", "b"),
|
||||
s=county_market_share("a", "b"),
|
||||
)
|
||||
assert [(r["g"], r["s"]) for r in rows] == [(None, 0.0), (None, None), (None, None)]
|
||||
|
||||
|
||||
def test_table_3_3_3_as_a_base_year_frame(backend):
|
||||
years = ("BY1", "BY2", "BY3")
|
||||
data = {
|
||||
"year": list(years),
|
||||
"paid": [TABLE_3_3_3[y][0] for y in years],
|
||||
"months": [float(TABLE_3_3_3[y][1]) for y in years],
|
||||
"risk": [TABLE_3_3_3[y][2] for y in years],
|
||||
"trend": [TABLE_3_3_3[y][5] for y in years],
|
||||
"weight": [TABLE_3_3_3[y][7] for y in years],
|
||||
}
|
||||
rs = risk_standardized_pbpm("paid", "months", "risk")
|
||||
rows = backend.select(
|
||||
data,
|
||||
year=nw.col("year"),
|
||||
line5=cents(rs),
|
||||
line9=cents(trended_weighted_pbpm(rs, "trend", "weight")),
|
||||
raw=trended_weighted_pbpm(rs, "trend", "weight"),
|
||||
)
|
||||
by_year = {r["year"]: r for r in rows}
|
||||
for year in years:
|
||||
assert by_year[year]["line5"] == TABLE_3_3_3[year][4]
|
||||
assert by_year[year]["line9"] == TABLE_3_3_3[year][8]
|
||||
assert (
|
||||
scalar_cents(sum(r["raw"] for r in rows)) == TABLE_3_3_3["historical_benchmark"]
|
||||
)
|
||||
|
||||
|
||||
def test_rea_tables_as_a_category_frame(backend):
|
||||
data = {
|
||||
"category": list(REA_TABLES),
|
||||
"regional": [float(v[0]) for v in REA_TABLES.values()],
|
||||
"historical": [float(v[1]) for v in REA_TABLES.values()],
|
||||
"uspcc": [float(v[4]) for v in REA_TABLES.values()],
|
||||
"risk": [v[5] for v in REA_TABLES.values()],
|
||||
"designation": ["Lower-spending"] * 3,
|
||||
"option": ["Global"] * 3,
|
||||
}
|
||||
rea = regional_efficiency_adjustment(
|
||||
"regional",
|
||||
"historical",
|
||||
eligible=regional_efficiency_eligible("designation", "option"),
|
||||
uspcc_amount="uspcc",
|
||||
py_average_risk_score="risk",
|
||||
cap_rate=0.05,
|
||||
)
|
||||
rows = backend.select(
|
||||
data,
|
||||
uncapped=uncapped_regional_efficiency_adjustment("regional", "historical"),
|
||||
cap=cents(benchmark_adjustment_cap("uspcc", "risk", 0.05)),
|
||||
applied=cents(rea),
|
||||
adjusted=cents(nw.col("historical") + rea),
|
||||
)
|
||||
for values, result in zip(REA_TABLES.values(), rows, strict=True):
|
||||
assert (
|
||||
result["uncapped"],
|
||||
result["cap"],
|
||||
result["applied"],
|
||||
result["adjusted"],
|
||||
) == (values[3], values[6], values[7], values[8])
|
||||
|
||||
|
||||
def test_rea_nulls_and_zeros(backend):
|
||||
data = {
|
||||
"regional": [1_050.0, 1_050.0, 950.0, 1_050.0, None],
|
||||
"historical": [1_000.0] * 5,
|
||||
"uspcc": [None, 1_000.0, None, 1_000.0, 1_000.0],
|
||||
"eligible": [True, None, True, False, True],
|
||||
}
|
||||
schema = {
|
||||
"regional": pl.Float64,
|
||||
"historical": pl.Float64,
|
||||
"uspcc": pl.Float64,
|
||||
"eligible": pl.Boolean,
|
||||
}
|
||||
rea = regional_efficiency_adjustment(
|
||||
"regional",
|
||||
"historical",
|
||||
eligible="eligible",
|
||||
uspcc_amount="uspcc",
|
||||
py_average_risk_score=nw.lit(1.0),
|
||||
cap_rate="cap_rate",
|
||||
)
|
||||
frame_rows = backend.select(
|
||||
{**data, "cap_rate": [0.05] * 5}, {**schema, "cap_rate": pl.Float64}, rea=rea
|
||||
)
|
||||
# missing cap on a positive REA, unknown eligibility, at/above regional,
|
||||
# ineligible, missing regional
|
||||
assert [r["rea"] for r in frame_rows] == [None, None, 0.0, 0.0, None]
|
||||
|
||||
|
||||
def test_three_way_trend_per_row_performance_year(backend):
|
||||
data = {
|
||||
"two_way": [1.05, 1.05, 1.05, 1.05],
|
||||
"acpt": [1.07, 1.02, 1.07, 1.07],
|
||||
"py": [2027, 2027, 2028, 2040],
|
||||
}
|
||||
rows = backend.select(data, t=three_way_blended_trend("two_way", "acpt", "py"))
|
||||
expected = [
|
||||
scalar.three_way_blended_trend(1.05, a, y).applied
|
||||
for a, y in ((1.07, 2027), (1.02, 2027), (1.07, 2028))
|
||||
]
|
||||
assert [r["t"] for r in rows] == [*expected, None]
|
||||
(literal,) = backend.select(
|
||||
{"two_way": [1.05], "acpt": [1.07]},
|
||||
t=three_way_blended_trend("two_way", "acpt", 2027),
|
||||
)
|
||||
assert literal["t"] == expected[0]
|
||||
|
||||
|
||||
def test_capped_risk_score_and_status(backend):
|
||||
data = {
|
||||
"by3": [1.0, 1.0, 1.0, None, 1.0],
|
||||
"py": [1.05, 0.90, 1.035, 1.2, 1.2],
|
||||
"cat": ["A&D", "ESRD", "High Needs", "A&D", "Other"],
|
||||
}
|
||||
rows = backend.select(
|
||||
data,
|
||||
{"by3": pl.Float64, "py": pl.Float64, "cat": pl.String},
|
||||
score=capped_risk_score("by3", "py", "cat"),
|
||||
status=risk_growth_cap_status("by3", "py", "cat"),
|
||||
)
|
||||
expected = [
|
||||
scalar.capped_risk_score(1.0, py, cat)
|
||||
for py, cat in ((1.05, "A&D"), (0.90, "ESRD"), (1.035, "High Needs"))
|
||||
]
|
||||
assert [(r["score"], r["status"]) for r in rows[:3]] == expected
|
||||
# No BY 3 reference or unknown category: no silent pass-through.
|
||||
assert [(r["score"], r["status"]) for r in rows[3:]] == [
|
||||
(None, RISK_CAP_REFERENCE_MISSING)
|
||||
] * 2
|
||||
|
||||
|
||||
def test_voluntary_guardrail_and_contribution(backend):
|
||||
data = {
|
||||
"vol": [1_150.0, 850.0, 1_050.0, 1_000.0],
|
||||
"claims": [1_000.0, 1_000.0, 1_000.0, None],
|
||||
}
|
||||
rows = backend.select(
|
||||
data,
|
||||
{"vol": pl.Float64, "claims": pl.Float64},
|
||||
b=voluntary_guardrail("vol", "claims"),
|
||||
s=voluntary_guardrail_status("vol", "claims"),
|
||||
)
|
||||
expected = [
|
||||
scalar.voluntary_guardrail(v, 1_000.0) for v in (1_150.0, 850.0, 1_050.0)
|
||||
]
|
||||
assert [(r["b"], r["s"]) for r in rows[:3]] == expected
|
||||
assert (rows[3]["b"], rows[3]["s"]) == (None, None)
|
||||
cats = {
|
||||
"cat": ["A&D", "High Needs", "High Needs", "High Needs"],
|
||||
"by3": ["A&D", "High Needs", "A&D", None],
|
||||
}
|
||||
flags = backend.select(
|
||||
cats,
|
||||
{"cat": pl.String, "by3": pl.String},
|
||||
f=contributes_to_voluntary_benchmark("cat", "by3"),
|
||||
)
|
||||
assert [r["f"] for r in flags] == [True, True, False, False]
|
||||
|
||||
|
||||
def test_table_3_5_4_as_a_frame(backend):
|
||||
rows = []
|
||||
for category, (
|
||||
pbpm,
|
||||
trend,
|
||||
risk,
|
||||
full,
|
||||
hybrid,
|
||||
factor,
|
||||
_,
|
||||
) in TABLE_3_5_4_CLAIMS.items():
|
||||
rows.append(
|
||||
{
|
||||
"category": category,
|
||||
"pbpm": pbpm,
|
||||
"trend": trend,
|
||||
"risk": risk,
|
||||
"full": float(full),
|
||||
"m2": float(hybrid),
|
||||
"f2": factor,
|
||||
"m3": 0.0,
|
||||
"f3": 1.0,
|
||||
"m4": 0.0,
|
||||
"f4": 1.0,
|
||||
}
|
||||
)
|
||||
for category, (
|
||||
pbpm,
|
||||
trend,
|
||||
risk,
|
||||
full,
|
||||
months,
|
||||
factors,
|
||||
_,
|
||||
) in TABLE_3_5_4_VOLUNTARY.items():
|
||||
rows.append(
|
||||
{
|
||||
"category": category,
|
||||
"pbpm": pbpm,
|
||||
"trend": trend,
|
||||
"risk": risk,
|
||||
"full": float(full),
|
||||
**{f"m{q}": float(m) for q, m in zip((2, 3, 4), months, strict=True)},
|
||||
**{f"f{q}": f for q, f in zip((2, 3, 4), factors, strict=True)},
|
||||
}
|
||||
)
|
||||
py_pbpm = performance_year_benchmark_pbpm("pbpm", nw.lit(0.0), "trend", "risk")
|
||||
dollars = benchmark_dollars(
|
||||
py_pbpm, "full", [("m2", "f2"), ("m3", "f3"), ("m4", "f4")]
|
||||
)
|
||||
cells = backend.select(rows, category=nw.col("category"), dollars=dollars)
|
||||
expected = [v[-1] for v in TABLE_3_5_4_CLAIMS.values()] + [
|
||||
v[-1] for v in TABLE_3_5_4_VOLUNTARY.values()
|
||||
]
|
||||
assert [c["dollars"] for c in cells] == expected
|
||||
line24 = {}
|
||||
for cell in cells:
|
||||
line24[cell["category"]] = scalar.total_benchmark(
|
||||
[line24.get(cell["category"], 0.0), cell["dollars"]]
|
||||
)
|
||||
assert line24 == {k: v for k, v in TABLE_3_5_4_TOTALS["24"].items() if k != "TOTAL"}
|
||||
|
||||
|
||||
def test_benchmark_dollars_without_hybrid_months(backend):
|
||||
(result,) = backend.select(
|
||||
{"pbpm": [1_125.0], "months": [60_000.0]}, d=benchmark_dollars("pbpm", "months")
|
||||
)
|
||||
assert result["d"] == 67_500_000.0
|
||||
|
||||
|
||||
def test_month_weighted_mean_in_a_group_by(backend):
|
||||
data = {
|
||||
"cat": ["A&D", "A&D", "A&D", "ESRD", "HN"],
|
||||
"score": [1.0, 1.2, None, 0.9, 1.5],
|
||||
"months": [10.0, 30.0, 50.0, 12.0, 0.0],
|
||||
}
|
||||
frame = nw.from_native(
|
||||
backend.frame(
|
||||
data, {"cat": pl.String, "score": pl.Float64, "months": pl.Float64}
|
||||
)
|
||||
)
|
||||
out = (
|
||||
frame.group_by("cat")
|
||||
.agg(avg=month_weighted_mean("score", "months"))
|
||||
.sort("cat")
|
||||
)
|
||||
rows = backend.rows(out.to_native())
|
||||
assert [r["cat"] for r in rows] == ["A&D", "ESRD", "HN"]
|
||||
assert rows[0]["avg"] == pytest.approx(
|
||||
scalar.month_weighted_average([(1.0, 10), (1.2, 30), (None, 50)])
|
||||
)
|
||||
assert rows[1]["avg"] == pytest.approx(0.9)
|
||||
assert rows[2]["avg"] is None
|
||||
230
tests/lead/test_express_settlement.py
Normal file
230
tests/lead/test_express_settlement.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""lead.express.settlement settles frames of scenarios on polars and DuckDB:
|
||||
Table 5.6.1 to the cent, bit-for-bit parity with the scalar engine over a
|
||||
grid of scenarios, and nulls — not zeros — for missing inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import asdict
|
||||
|
||||
import polars as pl
|
||||
import pytest
|
||||
|
||||
from lead.express import cents
|
||||
from lead.express.settlement import (
|
||||
INPUT_COLUMNS,
|
||||
LINE_COLUMNS,
|
||||
discount_amount,
|
||||
discount_rate,
|
||||
settle,
|
||||
)
|
||||
from lead.money import cents as scalar_cents
|
||||
from lead.params import DISCOUNT_RATES
|
||||
from lead.settlement import SettlementInputs
|
||||
from lead.settlement import settle as settle_scalar
|
||||
from tests.lead.paper import TABLE_5_6_1, TABLE_5_6_1_INPUTS, TABLE_5_6_1_LINES
|
||||
|
||||
SCHEMA = {
|
||||
"py_benchmark": pl.Float64,
|
||||
"risk_option": pl.String,
|
||||
"spending_designation": pl.String,
|
||||
"performance_year": pl.Int64,
|
||||
"quality_score": pl.Float64,
|
||||
"capitation_payments": pl.Float64,
|
||||
"participant_provider_claims": pl.Float64,
|
||||
"preferred_provider_claims": pl.Float64,
|
||||
"non_aco_provider_claims": pl.Float64,
|
||||
"stop_loss_elected": pl.Boolean,
|
||||
"stop_loss_charge": pl.Float64,
|
||||
"stop_loss_payout": pl.Float64,
|
||||
"stop_loss_neutrality_factor": pl.Float64,
|
||||
"retention_incentive_applies": pl.Boolean,
|
||||
}
|
||||
|
||||
|
||||
def row(**changes):
|
||||
return {**TABLE_5_6_1_INPUTS, "risk_option": "Global", **changes}
|
||||
|
||||
|
||||
def settled(backend, rows):
|
||||
return backend.rows(settle(backend.frame(rows, SCHEMA)))
|
||||
|
||||
|
||||
def test_schema_matches_the_input_model():
|
||||
assert tuple(SCHEMA) == INPUT_COLUMNS
|
||||
assert "shared_savings_losses_after_sequestration" in LINE_COLUMNS
|
||||
|
||||
|
||||
def test_table_5_6_1_every_line_on_each_backend(backend):
|
||||
rows = settled(
|
||||
backend, [row(risk_option="Global"), row(risk_option="Professional")]
|
||||
)
|
||||
for result in rows:
|
||||
expected = TABLE_5_6_1[result["risk_option"]]
|
||||
for line, field in TABLE_5_6_1_LINES:
|
||||
assert result[field] == expected[line], (result["risk_option"], line)
|
||||
|
||||
|
||||
def scenario_grid() -> list[dict]:
|
||||
"""Valid scenarios across options, designations, years, quality,
|
||||
elections, and gross results from deep losses to deep savings."""
|
||||
rng = random.Random(2027)
|
||||
scenarios = []
|
||||
for i in range(160):
|
||||
benchmark = rng.choice([150_000_000.0, 48_250_731.17, 9_999_999.99])
|
||||
option = rng.choice(["Global", "Professional"])
|
||||
elected = rng.random() < 0.7
|
||||
share = rng.choice(
|
||||
[
|
||||
-0.62,
|
||||
-0.5,
|
||||
-0.35,
|
||||
-0.2,
|
||||
-0.15,
|
||||
-0.1,
|
||||
-0.0001,
|
||||
0.0,
|
||||
0.07,
|
||||
0.1,
|
||||
0.15,
|
||||
0.2,
|
||||
0.35,
|
||||
0.5,
|
||||
0.71,
|
||||
]
|
||||
)
|
||||
payout = rng.choice([0.0, 1_234_567.89, 2_900_000.0])
|
||||
scenarios.append(
|
||||
row(
|
||||
py_benchmark=benchmark,
|
||||
risk_option=option,
|
||||
spending_designation=rng.choice(
|
||||
[
|
||||
"Lower-spending",
|
||||
"Higher-spending",
|
||||
None if option == "Professional" else "Lower-spending",
|
||||
]
|
||||
),
|
||||
performance_year=rng.choice([2027, 2028, 2030, 2031, 2036]),
|
||||
quality_score=rng.choice([0.0, 0.333, 0.95, 1.0]),
|
||||
capitation_payments=round(benchmark * rng.uniform(0.0, 0.2), 2),
|
||||
participant_provider_claims=round(benchmark * rng.uniform(0.0, 0.1), 2),
|
||||
preferred_provider_claims=round(benchmark * rng.uniform(0.0, 0.2), 2),
|
||||
non_aco_provider_claims=round(benchmark * (0.5 - share), 2),
|
||||
stop_loss_elected=elected,
|
||||
stop_loss_charge=round(benchmark * 0.02, 2) if elected else None,
|
||||
stop_loss_payout=payout if elected else None,
|
||||
stop_loss_neutrality_factor=(
|
||||
rng.choice([0.93, 1.07, None])
|
||||
if payout == 0.0
|
||||
else rng.choice([0.93, 1.07])
|
||||
)
|
||||
if elected
|
||||
else None,
|
||||
retention_incentive_applies=i % 7 == 0,
|
||||
)
|
||||
)
|
||||
return scenarios
|
||||
|
||||
|
||||
def test_frame_matches_scalar_bit_for_bit(backend):
|
||||
scenarios = scenario_grid()
|
||||
rows = settled(backend, scenarios)
|
||||
assert len(rows) == len(scenarios)
|
||||
signs = {
|
||||
(r["shared_savings_losses"] > 0) - (r["shared_savings_losses"] < 0)
|
||||
for r in rows
|
||||
}
|
||||
assert signs == {1, -1} # savings and losses both exercised
|
||||
for scenario, result in zip(scenarios, rows, strict=True):
|
||||
expected = asdict(settle_scalar(SettlementInputs(**scenario)))
|
||||
assert {k: result[k] for k in expected} == expected
|
||||
|
||||
|
||||
def test_other_columns_pass_through(backend):
|
||||
frame = backend.frame([{**row(), "aco": "A1"}], {**SCHEMA, "aco": pl.String})
|
||||
assert backend.rows(settle(frame))[0]["aco"] == "A1"
|
||||
|
||||
|
||||
def test_not_elected_rows_get_the_papers_zeros(backend):
|
||||
(result,) = settled(
|
||||
backend,
|
||||
[
|
||||
row(
|
||||
stop_loss_elected=False,
|
||||
stop_loss_charge=None,
|
||||
stop_loss_payout=None,
|
||||
stop_loss_neutrality_factor=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
assert (result["stop_loss_charge"], result["stop_loss_payout"]) == (0.0, 0.0)
|
||||
assert result["stop_loss_neutrality_factor"] is None
|
||||
assert result["net_stop_loss_impact"] == 0.0
|
||||
assert result["py_expenditure_after_stop_loss"] == result["py_expenditure"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes,first_null",
|
||||
[
|
||||
({"quality_score": None}, "quality_earn_back"),
|
||||
({"quality_score": 1.2}, "quality_earn_back"),
|
||||
({"spending_designation": None}, "discount_rate"),
|
||||
({"spending_designation": "lower-spending"}, "discount_rate"),
|
||||
({"performance_year": 2037}, "discount_rate"),
|
||||
({"risk_option": "global"}, "discount_rate"),
|
||||
({"stop_loss_charge": None}, "net_stop_loss_impact"),
|
||||
({"stop_loss_neutrality_factor": None}, "adjusted_stop_loss_payout"),
|
||||
({"stop_loss_elected": None}, "adjusted_stop_loss_payout"),
|
||||
({"retention_incentive_applies": None}, "retention_incentive_adjustment"),
|
||||
({"non_aco_provider_claims": None}, "total_ffs_payments"),
|
||||
],
|
||||
)
|
||||
def test_missing_inputs_null_the_chain_never_zero_it(backend, changes, first_null):
|
||||
(result,) = settled(backend, [row(**changes)])
|
||||
assert result[first_null] is None
|
||||
assert result["shared_savings_losses_after_sequestration"] is None
|
||||
assert result["sequestration_amount"] is None
|
||||
|
||||
|
||||
def test_missing_columns_are_named(backend):
|
||||
frame = backend.frame([{"py_benchmark": 1.0}])
|
||||
with pytest.raises(ValueError, match="quality_score"):
|
||||
settle(frame)
|
||||
|
||||
|
||||
def test_discount_rate_lookup(backend):
|
||||
data = [
|
||||
{"o": "Global", "d": d, "y": y}
|
||||
for d, by_year in DISCOUNT_RATES.items()
|
||||
for y in by_year
|
||||
] + [
|
||||
{"o": "Professional", "d": None, "y": 2030},
|
||||
{"o": "Professional", "d": "Lower-spending", "y": 2026},
|
||||
{"o": "Global", "d": "Lower-spending", "y": 2026},
|
||||
]
|
||||
rates = [r["rate"] for r in backend.select(data, rate=discount_rate("o", "d", "y"))]
|
||||
assert rates[:-3] == [
|
||||
r for by_year in DISCOUNT_RATES.values() for r in by_year.values()
|
||||
]
|
||||
assert rates[-3:] == [0.0, None, None]
|
||||
|
||||
|
||||
def test_missing_discount_rate_is_zero_in_the_na_helper(backend):
|
||||
data = {"benchmark": [150_000_000.0, 150_000_000.0], "rate": [None, 0.03]}
|
||||
result = backend.select(
|
||||
data,
|
||||
{"benchmark": pl.Float64, "rate": pl.Float64},
|
||||
amount=discount_amount("benchmark", "rate"),
|
||||
)
|
||||
assert [r["amount"] for r in result] == [0.0, 4_500_000.0]
|
||||
|
||||
|
||||
def test_cents_expr_matches_the_scalar_bit_for_bit(backend):
|
||||
rng = random.Random(5)
|
||||
values = [rng.uniform(-1e9, 1e9) for _ in range(500)]
|
||||
values += [k / 1000 for k in range(-2000, 2001, 5)] # half-cent ties
|
||||
values += [0.125, -0.125, 2.675, 1.005, -0.001, 0.0, None]
|
||||
result = backend.select({"x": values}, {"x": pl.Float64}, y=cents("x"))
|
||||
for value, got in zip(values, (r["y"] for r in result), strict=True):
|
||||
assert got == (None if value is None else scalar_cents(value)), value
|
||||
87
tests/lead/test_express_stop_loss.py
Normal file
87
tests/lead/test_express_stop_loss.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""lead.express.stop_loss: beneficiary-grain Stop-Loss on polars and DuckDB,
|
||||
matching Box 5.3.3 and the scalar engine."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
import polars as pl
|
||||
|
||||
from lead.express.stop_loss import (
|
||||
band_status,
|
||||
distance_to_attachment_point,
|
||||
predicted_expenditure,
|
||||
residual_expenditure,
|
||||
stop_loss_payout,
|
||||
)
|
||||
from lead.stop_loss import band_status as scalar_band_status
|
||||
from lead.stop_loss import stop_loss_payout as scalar_payout
|
||||
from tests.lead.paper import STOP_LOSS_PAYOUT
|
||||
|
||||
SCHEMA = {"actual": pl.Float64, "predicted": pl.Float64, "ap": pl.Float64}
|
||||
|
||||
|
||||
def test_box_5_3_3_illustration(backend):
|
||||
data = [
|
||||
{
|
||||
"actual": STOP_LOSS_PAYOUT["actual"],
|
||||
"predicted": STOP_LOSS_PAYOUT["predicted"],
|
||||
"ap": STOP_LOSS_PAYOUT["attachment_point"],
|
||||
}
|
||||
]
|
||||
residual = residual_expenditure("actual", "predicted")
|
||||
(result,) = backend.select(
|
||||
data,
|
||||
SCHEMA,
|
||||
residual=residual,
|
||||
payout=stop_loss_payout(residual, "ap"),
|
||||
status=band_status(residual, "ap"),
|
||||
distance=distance_to_attachment_point("ap", residual),
|
||||
)
|
||||
assert result == {
|
||||
"residual": 400_000.0,
|
||||
"payout": 220_000.0,
|
||||
"status": "band_2",
|
||||
"distance": -250_000.0,
|
||||
}
|
||||
|
||||
|
||||
def test_predicted_expenditure(backend):
|
||||
data = {"regional": [1_000.0], "risk": [1.2], "months": [12.0]}
|
||||
(result,) = backend.select(
|
||||
data, predicted=predicted_expenditure("regional", "risk", "months")
|
||||
)
|
||||
assert result["predicted"] == 14_400.0
|
||||
|
||||
|
||||
def test_payout_and_band_match_the_scalar(backend):
|
||||
rng = random.Random(3)
|
||||
data = [
|
||||
{
|
||||
"actual": rng.uniform(0, 900_000),
|
||||
"predicted": rng.uniform(0, 200_000),
|
||||
"ap": rng.choice([150_000.0, 98_765.43]),
|
||||
}
|
||||
for _ in range(200)
|
||||
]
|
||||
data += [
|
||||
{"actual": None, "predicted": 1.0, "ap": 150_000.0},
|
||||
{"actual": 1.0, "predicted": 0.0, "ap": None},
|
||||
]
|
||||
residual = residual_expenditure("actual", "predicted")
|
||||
rows = backend.select(
|
||||
data,
|
||||
SCHEMA,
|
||||
residual=residual,
|
||||
payout=stop_loss_payout(residual, "ap"),
|
||||
status=band_status(residual, "ap"),
|
||||
)
|
||||
for inputs, result in zip(data, rows, strict=True):
|
||||
if inputs["actual"] is None or inputs["ap"] is None:
|
||||
assert (result["payout"], result["status"]) == (None, None)
|
||||
continue
|
||||
assert (
|
||||
result["payout"]
|
||||
== scalar_payout(result["residual"], inputs["ap"]).total_payout
|
||||
)
|
||||
assert result["status"] == scalar_band_status(result["residual"], inputs["ap"])
|
||||
71
tests/lead/test_monies_owed.py
Normal file
71
tests/lead/test_monies_owed.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""lead.monies_owed reproduces Table 5.5.1 and keeps who-owes-whom signs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from lead.monies_owed import (
|
||||
apo_adjustment,
|
||||
payment_arrangement_adjustments,
|
||||
total_monies_owed,
|
||||
)
|
||||
from lead.settlement import SettlementInputs, settle
|
||||
from tests.lead.paper import TABLE_5_5_1, TABLE_5_6_1_INPUTS
|
||||
|
||||
|
||||
def test_table_5_5_1_every_line():
|
||||
# The example ACO elected TCC: no Enhanced PCC or APO (footnote 1).
|
||||
arrangements = payment_arrangement_adjustments(
|
||||
capitation_under_over_payment=TABLE_5_5_1["4.capitation"],
|
||||
enhanced_pcc_paid=0.0,
|
||||
apo_adjustment=0.0,
|
||||
)
|
||||
assert arrangements.capitation_under_over_payment == TABLE_5_5_1["4.capitation"]
|
||||
assert arrangements.payment_arrangement_adjustments == TABLE_5_5_1["4"]
|
||||
owed = total_monies_owed(
|
||||
provisional_shared_savings_losses=TABLE_5_5_1["1"],
|
||||
final_shared_savings_losses=TABLE_5_5_1["2"],
|
||||
payment_arrangement_adjustments=arrangements.payment_arrangement_adjustments,
|
||||
high_performers_pool_incentive=TABLE_5_5_1["5"],
|
||||
)
|
||||
assert owed.shared_savings_losses_owed == TABLE_5_5_1["3"]
|
||||
assert owed.high_performers_pool_incentive == TABLE_5_5_1["5"]
|
||||
assert owed.adjustments_owed == TABLE_5_5_1["6"]
|
||||
assert owed.total_monies_owed == TABLE_5_5_1["7"]
|
||||
|
||||
|
||||
def test_table_5_5_1_line_2_is_a_cent_below_table_5_6_1_line_28():
|
||||
# The paper's own inconsistency: Table 5.5.1 carries the Professional
|
||||
# final Shared Savings as $8,073,053.43, Table 5.6.1 line 28 as .44
|
||||
# (60% × 13,729,682.72 = 8,237,809.632; less 2% = 8,073,053.439).
|
||||
final = settle(SettlementInputs(**TABLE_5_6_1_INPUTS, risk_option="Professional"))
|
||||
assert final.shared_savings_losses_after_sequestration == 8_073_053.44
|
||||
owed = total_monies_owed(
|
||||
provisional_shared_savings_losses=TABLE_5_5_1["1"],
|
||||
final_shared_savings_losses=final.shared_savings_losses_after_sequestration,
|
||||
payment_arrangement_adjustments=TABLE_5_5_1["4"],
|
||||
high_performers_pool_incentive=TABLE_5_5_1["5"],
|
||||
)
|
||||
assert owed.total_monies_owed == 1_877_213.44 # Table 5.5.1 line 7: .43
|
||||
|
||||
|
||||
def test_signs_follow_who_owes_whom():
|
||||
# Overpaid capitation and the Enhanced PCC recoupment are owed to CMS.
|
||||
lines = payment_arrangement_adjustments(
|
||||
capitation_under_over_payment=-5_000.0,
|
||||
enhanced_pcc_paid=120_000.0,
|
||||
apo_adjustment=0.0,
|
||||
)
|
||||
assert lines.enhanced_pcc_repayment == -120_000.0
|
||||
assert lines.payment_arrangement_adjustments == -125_000.0
|
||||
owed = total_monies_owed(
|
||||
provisional_shared_savings_losses=500_000.0,
|
||||
final_shared_savings_losses=-200_000.0,
|
||||
payment_arrangement_adjustments=lines.payment_arrangement_adjustments,
|
||||
high_performers_pool_incentive=0.0,
|
||||
)
|
||||
assert owed.total_monies_owed == -825_000.0
|
||||
|
||||
|
||||
def test_apo_reconciliation():
|
||||
# Reductions above the APO payment are credited; below it, recouped.
|
||||
assert apo_adjustment(110_000.0, 100_000.0) == 10_000.0
|
||||
assert apo_adjustment(90_000.0, 100_000.0) == -10_000.0
|
||||
113
tests/lead/test_params_money.py
Normal file
113
tests/lead/test_params_money.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""lead.params carries the paper's printed parameters; lead.money rounds
|
||||
dollar lines to the cent half away from zero."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from lead.money import cents
|
||||
from lead.params import (
|
||||
ACPT_GUARDRAILS,
|
||||
BASE_YEAR_ALIGNMENT_MINIMUMS,
|
||||
BASE_YEAR_WEIGHTS,
|
||||
DISCOUNT_RATES,
|
||||
QUALITY_WITHHOLD_RATE,
|
||||
RETENTION_INCENTIVE_RATE,
|
||||
RISK_CORRIDORS,
|
||||
RISK_SCORE_GROWTH_CAPS,
|
||||
SEQUESTRATION_RATE,
|
||||
model_year,
|
||||
)
|
||||
|
||||
|
||||
def test_settlement_rates():
|
||||
assert (QUALITY_WITHHOLD_RATE, RETENTION_INCENTIVE_RATE, SEQUESTRATION_RATE) == (
|
||||
0.03,
|
||||
0.02,
|
||||
0.02,
|
||||
)
|
||||
|
||||
|
||||
def test_tables_5_4_1_and_5_4_3_risk_corridors():
|
||||
def table(option, scenario):
|
||||
return [(c.lower, c.upper, c.rate) for c in RISK_CORRIDORS[(option, scenario)]]
|
||||
|
||||
for scenario in ("savings", "losses"):
|
||||
assert table("Global", scenario) == [
|
||||
(0.0, 0.15, 1.00),
|
||||
(0.15, 0.35, 0.50),
|
||||
(0.35, 0.50, 0.25),
|
||||
(0.50, None, 0.10),
|
||||
]
|
||||
assert table("Professional", "savings")[0] == (0.0, 0.10, 0.60)
|
||||
assert table("Professional", "losses")[0] == (0.0, 0.10, 0.50)
|
||||
assert (
|
||||
table("Professional", "losses")[1:]
|
||||
== table("Professional", "savings")[1:]
|
||||
== [
|
||||
(0.10, 0.15, 0.35),
|
||||
(0.15, 0.20, 0.15),
|
||||
(0.20, None, 0.05),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_table_3_5_1_acpt_guardrails():
|
||||
assert ACPT_GUARDRAILS[1] == (-0.002, 0.003)
|
||||
assert ACPT_GUARDRAILS[4] == (-0.008, 0.012)
|
||||
assert {ACPT_GUARDRAILS[y] for y in range(5, 11)} == {(-0.010, 0.015)}
|
||||
|
||||
|
||||
def test_benchmark_parameters():
|
||||
assert dict(RISK_SCORE_GROWTH_CAPS) == {
|
||||
"A&D": 0.03,
|
||||
"ESRD": 0.03,
|
||||
"High Needs": 0.04,
|
||||
}
|
||||
assert dict(BASE_YEAR_WEIGHTS["Newly Entering"]) == {
|
||||
"BY1": 0.1,
|
||||
"BY2": 0.3,
|
||||
"BY3": 0.6,
|
||||
}
|
||||
assert sum(BASE_YEAR_WEIGHTS["Renewing"].values()) == pytest.approx(1.0)
|
||||
assert BASE_YEAR_ALIGNMENT_MINIMUMS["newly_entering"][:5] == (
|
||||
600,
|
||||
1200,
|
||||
1800,
|
||||
2400,
|
||||
3000,
|
||||
)
|
||||
assert BASE_YEAR_ALIGNMENT_MINIMUMS["high_needs_esrd"][9] == 1000
|
||||
|
||||
|
||||
def test_parameters_are_read_only():
|
||||
with pytest.raises(TypeError):
|
||||
DISCOUNT_RATES["Lower-spending"][2027] = 0.0 # type: ignore[index]
|
||||
|
||||
|
||||
def test_model_year():
|
||||
assert (model_year(2027), model_year(2036)) == (1, 10)
|
||||
with pytest.raises(ValueError, match="outside the LEAD model"):
|
||||
model_year(2037)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"amount,expected",
|
||||
[
|
||||
(184_593.6544, 184_593.65),
|
||||
(164_756.19264, 164_756.19),
|
||||
(2_948_334.2768, 2_948_334.28),
|
||||
(0.125, 0.13), # half away from zero, not half to even
|
||||
(-0.125, -0.13),
|
||||
(2.5, 2.5),
|
||||
(-0.001, 0.0),
|
||||
(0.0, 0.0),
|
||||
(1_877_213.43, 1_877_213.43),
|
||||
],
|
||||
)
|
||||
def test_cents(amount, expected):
|
||||
assert cents(amount) == expected
|
||||
|
||||
|
||||
def test_cents_never_returns_negative_zero():
|
||||
assert str(cents(-0.001)) == "0.0"
|
||||
284
tests/lead/test_settlement.py
Normal file
284
tests/lead/test_settlement.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""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
|
||||
134
tests/lead/test_stop_loss.py
Normal file
134
tests/lead/test_stop_loss.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""lead.stop_loss reproduces Box 5.3.3: the charge calculation, the payout
|
||||
illustration, and the application of Stop-Loss to PY expenditure."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from lead.money import cents
|
||||
from lead.params import STOP_LOSS_ATTACHMENT_PAYOUT_SHARE, STOP_LOSS_BANDS
|
||||
from lead.stop_loss import (
|
||||
adjusted_payout,
|
||||
apply_stop_loss,
|
||||
average_payout_percentage,
|
||||
band_status,
|
||||
charge_pbpm,
|
||||
neutrality_factor,
|
||||
payout_percentage,
|
||||
predicted_expenditure,
|
||||
reference_years,
|
||||
residual_expenditure,
|
||||
stop_loss_charge,
|
||||
stop_loss_payout,
|
||||
trended_attachment_point,
|
||||
without_stop_loss,
|
||||
)
|
||||
from tests.lead.paper import STOP_LOSS_CHARGE, STOP_LOSS_PAYOUT
|
||||
|
||||
|
||||
def test_box_5_3_3_charge_calculation():
|
||||
pct = average_payout_percentage(STOP_LOSS_CHARGE["RY"])
|
||||
charge = stop_loss_charge(
|
||||
STOP_LOSS_CHARGE["1"], STOP_LOSS_CHARGE["2"], STOP_LOSS_CHARGE["3"], pct
|
||||
)
|
||||
assert charge.trended_risk_adjusted_reference_expenditure == STOP_LOSS_CHARGE["4"]
|
||||
assert round(charge.average_payout_percentage, 4) == STOP_LOSS_CHARGE["5"]
|
||||
assert charge.stop_loss_charge == STOP_LOSS_CHARGE["6"]
|
||||
|
||||
|
||||
def test_box_5_3_3_charge_uses_the_unrounded_average_payout_percentage():
|
||||
# Line 5 prints 2.03%; multiplying line 4 by the printed rate misses line 6.
|
||||
assert cents(STOP_LOSS_CHARGE["4"] * STOP_LOSS_CHARGE["5"]) == 2_943_500.94
|
||||
assert cents(STOP_LOSS_CHARGE["4"] * STOP_LOSS_CHARGE["5"]) != STOP_LOSS_CHARGE["6"]
|
||||
|
||||
|
||||
def test_box_5_3_3_payout_illustration():
|
||||
residual = residual_expenditure(
|
||||
STOP_LOSS_PAYOUT["actual"], STOP_LOSS_PAYOUT["predicted"]
|
||||
)
|
||||
assert residual == STOP_LOSS_PAYOUT["residual"]
|
||||
payout = stop_loss_payout(residual, STOP_LOSS_PAYOUT["attachment_point"])
|
||||
band_1, band_2 = payout.bands
|
||||
assert payout.below_attachment_point == STOP_LOSS_PAYOUT["below_attachment_point"]
|
||||
assert (band_1.exposure, band_2.exposure) == (
|
||||
STOP_LOSS_PAYOUT["band_1_exposure"],
|
||||
STOP_LOSS_PAYOUT["band_2_exposure"],
|
||||
)
|
||||
assert (band_1.payout, band_2.payout) == (
|
||||
STOP_LOSS_PAYOUT["band_1_payout"],
|
||||
STOP_LOSS_PAYOUT["band_2_payout"],
|
||||
)
|
||||
assert payout.total_payout == STOP_LOSS_PAYOUT["total_payout"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"residual,status,total",
|
||||
[
|
||||
(-5_000.0, "below_attachment_point", 0.0),
|
||||
(150_000.0, "below_attachment_point", 0.0),
|
||||
(150_000.01, "band_1", 0.01),
|
||||
(300_000.0, "band_1", 120_000.0),
|
||||
(300_000.01, "band_2", 120_000.01),
|
||||
],
|
||||
)
|
||||
def test_band_boundaries(residual, status, total):
|
||||
assert band_status(residual, 150_000.0) == status
|
||||
assert stop_loss_payout(residual, 150_000.0).total_payout == total
|
||||
|
||||
|
||||
def test_negative_residual_is_nothing_below_the_attachment_point():
|
||||
assert stop_loss_payout(-5_000.0, 150_000.0).below_attachment_point == 0.0
|
||||
|
||||
|
||||
def test_payment_schedule_and_attachment_share():
|
||||
assert [
|
||||
(b.start_multiple, b.end_multiple, b.payout_rate) for b in STOP_LOSS_BANDS
|
||||
] == [
|
||||
(1.0, 2.0, 0.80),
|
||||
(2.0, None, 1.00),
|
||||
]
|
||||
assert STOP_LOSS_ATTACHMENT_PAYOUT_SHARE == 0.02
|
||||
|
||||
|
||||
def test_reference_years_leave_a_one_year_gap():
|
||||
assert reference_years(2027) == (2023, 2024, 2025)
|
||||
assert reference_years(2030) == (2026, 2027, 2028)
|
||||
|
||||
|
||||
def test_predicted_expenditure_and_trended_attachment_point():
|
||||
assert predicted_expenditure(1_000.0, 1.2, 12) == 14_400.00
|
||||
assert trended_attachment_point(150_000.0, 1.05) == 157_500.00
|
||||
|
||||
|
||||
def test_payout_percentages():
|
||||
assert payout_percentage(196.0, 10_000.0) == 0.0196
|
||||
assert payout_percentage(1.0, 0.0) is None
|
||||
assert average_payout_percentage([0.02, None, 0.03]) is None
|
||||
assert average_payout_percentage([]) is None
|
||||
|
||||
|
||||
def test_charge_pbpm_and_neutrality_factor():
|
||||
assert charge_pbpm(2_948_334.28, 132_000) == pytest.approx(22.3359, abs=1e-4)
|
||||
assert charge_pbpm(1.0, 0) is None
|
||||
assert neutrality_factor(93.0, 100.0) == 0.93
|
||||
# No payouts model-wide: the ratio is undefined, not 1.0.
|
||||
assert neutrality_factor(93.0, 0.0) is None
|
||||
|
||||
|
||||
def test_application_of_stop_loss_lines_16_to_22():
|
||||
applied = apply_stop_loss(135_793_983.00, 2_948_334.28, 2_900_000.00, 0.930)
|
||||
assert applied.adjusted_stop_loss_payout == 2_697_000.00
|
||||
assert applied.net_stop_loss_impact == 251_334.28
|
||||
assert applied.py_expenditure_after_stop_loss == 136_045_317.28
|
||||
|
||||
|
||||
def test_adjusted_payout_without_a_factor():
|
||||
assert adjusted_payout(0.0, None) == 0.0
|
||||
with pytest.raises(ValueError, match="neutrality factor"):
|
||||
adjusted_payout(10.0, None)
|
||||
|
||||
|
||||
def test_without_stop_loss():
|
||||
applied = without_stop_loss(100.0)
|
||||
assert applied.py_expenditure_after_stop_loss == 100.0
|
||||
assert applied.stop_loss_neutrality_factor is None
|
||||
100
tests/lead/test_timing.py
Normal file
100
tests/lead/test_timing.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""lead.timing: the run-out, the settlement basis rule and settlement-stage
|
||||
inputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from lead.timing import (
|
||||
ACTUAL_CLAIMS_BASIS,
|
||||
CMS_SETTLEMENT_REPORT,
|
||||
CMS_SETTLEMENT_REPORT_MISSING,
|
||||
COMPUTED_STATUS,
|
||||
FINAL_NOT_YET_MADE,
|
||||
PARTIAL_YEAR_STATUS,
|
||||
PROJECTED_BASIS,
|
||||
PROJECTED_STATUS,
|
||||
PROVISIONAL_NOT_YET_MADE,
|
||||
SettlementBasis,
|
||||
require_projection_allowed,
|
||||
settlement_basis_for_run,
|
||||
settlement_runout_end,
|
||||
settlement_stage_input,
|
||||
validate_run_mode,
|
||||
)
|
||||
|
||||
|
||||
def test_runout_ends_march_31_of_the_following_year():
|
||||
assert settlement_runout_end(2027) == date(2028, 3, 31)
|
||||
|
||||
|
||||
def test_before_the_py_a_projection_is_projected():
|
||||
basis = settlement_basis_for_run("projection", date(2026, 10, 20), 2027, 0)
|
||||
assert basis == SettlementBasis(
|
||||
PROJECTED_BASIS, PROJECTED_STATUS, PROJECTED_STATUS, date(2026, 10, 20)
|
||||
)
|
||||
|
||||
|
||||
def test_inside_the_py_rows_are_partial_actuals_under_a_projected_headline():
|
||||
basis = settlement_basis_for_run("projection", date(2027, 6, 1), 2027, 10)
|
||||
assert (basis.basis, basis.headline_status, basis.actual_rows_status) == (
|
||||
PROJECTED_BASIS,
|
||||
PROJECTED_STATUS,
|
||||
PARTIAL_YEAR_STATUS,
|
||||
)
|
||||
|
||||
|
||||
def test_after_the_runout_rows_are_actual():
|
||||
for mode in ("projection", "actual"):
|
||||
basis = settlement_basis_for_run(mode, date(2028, 4, 1), 2027, 10)
|
||||
assert basis.basis == ACTUAL_CLAIMS_BASIS
|
||||
assert basis.headline_status == basis.actual_rows_status == COMPUTED_STATUS
|
||||
|
||||
|
||||
def test_after_the_runout_without_rows_is_still_a_projection():
|
||||
assert (
|
||||
settlement_basis_for_run("rewind_test", date(2028, 4, 1), 2027, 0).basis
|
||||
== PROJECTED_BASIS
|
||||
)
|
||||
|
||||
|
||||
def test_rows_before_the_py_are_a_feed_error():
|
||||
with pytest.raises(ValueError, match="data from the future"):
|
||||
settlement_basis_for_run("projection", date(2026, 10, 20), 2027, 3)
|
||||
|
||||
|
||||
def test_actual_mode_before_the_runout_fails_naming_the_input():
|
||||
for as_of, rows in ((date(2026, 10, 20), 0), (date(2027, 6, 1), 10)):
|
||||
with pytest.raises(
|
||||
ValueError, match="performance_year_claims_by_provider_class"
|
||||
):
|
||||
settlement_basis_for_run("actual", as_of, 2027, rows)
|
||||
|
||||
|
||||
def test_run_modes_are_validated():
|
||||
assert validate_run_mode("rewind_test") == "rewind_test"
|
||||
with pytest.raises(ValueError, match="run_mode must be one of"):
|
||||
validate_run_mode("Projection")
|
||||
require_projection_allowed("projection", "x", "y")
|
||||
|
||||
|
||||
def test_settlement_stage_inputs_follow_the_as_of_date():
|
||||
start = date(2028, 1, 1)
|
||||
assert settlement_stage_input(
|
||||
date(2027, 12, 31), start, None, PROVISIONAL_NOT_YET_MADE
|
||||
) == (
|
||||
0.0,
|
||||
PROVISIONAL_NOT_YET_MADE,
|
||||
)
|
||||
assert settlement_stage_input(start, start, None, FINAL_NOT_YET_MADE) == (
|
||||
None,
|
||||
CMS_SETTLEMENT_REPORT_MISSING,
|
||||
)
|
||||
assert settlement_stage_input(
|
||||
start, start, 6_456_540, PROVISIONAL_NOT_YET_MADE
|
||||
) == (
|
||||
6_456_540.0,
|
||||
CMS_SETTLEMENT_REPORT,
|
||||
)
|
||||
13
uv.lock
generated
13
uv.lock
generated
@@ -14,7 +14,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-10-20T16:53:32.422989299Z"
|
||||
exclude-newer = "2026-10-20T17:26:12.492023543Z"
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[[package]]
|
||||
@@ -4660,6 +4660,12 @@ lake = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "sqlglot" },
|
||||
]
|
||||
lead = [
|
||||
{ name = "duckdb" },
|
||||
{ name = "httpx" },
|
||||
{ name = "narwhals" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
llm = [
|
||||
{ name = "duckdb" },
|
||||
{ name = "fastapi" },
|
||||
@@ -4809,6 +4815,7 @@ requires-dist = [
|
||||
{ name = "narwhals", marker = "extra == 'aco'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'bcda'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'cms'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'lead'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'opps'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'pfs'", specifier = ">=2.17.0" },
|
||||
{ name = "narwhals", marker = "extra == 'rec'", specifier = ">=2.17.0" },
|
||||
@@ -4862,6 +4869,7 @@ requires-dist = [
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'api'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'bcda'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'bib'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'lead'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'llm'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'nlm'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'opps'" },
|
||||
@@ -4872,6 +4880,7 @@ requires-dist = [
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'rec'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'rex'" },
|
||||
{ name = "stack", extras = ["lake"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["lead"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["llm"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["llm"], marker = "extra == 'cli'" },
|
||||
{ name = "stack", extras = ["mail"], marker = "extra == 'cli'" },
|
||||
@@ -4893,7 +4902,7 @@ requires-dist = [
|
||||
{ name = "uvicorn", marker = "extra == 'llm'", specifier = ">=0.41.0" },
|
||||
{ name = "xlrd", specifier = ">=2.0.2" },
|
||||
]
|
||||
provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "cli", "nlm", "mail", "cms", "opps", "pfs", "qpp", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"]
|
||||
provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "lead", "cli", "nlm", "mail", "cms", "opps", "pfs", "qpp", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user