Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/713.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Modeled Medicare Part B premiums from enrollment and premium schedules, netted a cycle-free MSP standard-premium offset, and documented the national Part B calibration target as an approximate beneficiary-paid out-of-pocket benchmark rather than gross CMS premium income.
11 changes: 10 additions & 1 deletion policyengine_us_data/datasets/cps/cps.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
assign_takeup_with_reported_anchors,
reported_subsidized_marketplace_by_tax_unit,
)
from policyengine_us_data.utils.policyengine import (
supports_medicare_enrollment_input,
supports_modeled_medicare_part_b_inputs,
)


CURRENT_HEALTH_COVERAGE_REPORTED_VAR_MAP = {
Expand Down Expand Up @@ -820,7 +824,12 @@ def add_personal_income_variables(cps: h5py.File, person: DataFrame, year: int):
cps["health_insurance_premiums_without_medicare_part_b"] = person.PHIP_VAL
cps["over_the_counter_health_expenses"] = person.POTC_VAL
cps["other_medical_expenses"] = person.PMED_VAL
cps["medicare_part_b_premiums"] = person.PEMCPREM
if supports_medicare_enrollment_input():
cps["medicare_enrolled"] = person.MCARE == 1
if supports_modeled_medicare_part_b_inputs():
cps["medicare_part_b_premiums_reported"] = person.PEMCPREM
else:
cps["medicare_part_b_premiums"] = person.PEMCPREM

# Get QBI simulation parameters ---
yamlfilename = (
Expand Down
7 changes: 6 additions & 1 deletion policyengine_us_data/datasets/cps/extended_cps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
impute_tax_unit_mortgage_balance_hints,
)
from policyengine_us_data.utils.policyengine import has_policyengine_us_variables
from policyengine_us_data.utils.policyengine import (
supports_modeled_medicare_part_b_inputs,
)
from policyengine_us_data.utils.retirement_limits import (
get_retirement_limits,
get_se_pension_limits,
Expand Down Expand Up @@ -150,7 +153,6 @@ def _supports_structural_mortgage_inputs() -> bool:
"health_insurance_premiums_without_medicare_part_b",
"over_the_counter_health_expenses",
"other_medical_expenses",
"medicare_part_b_premiums",
"child_support_expense",
# Hours/employment
"weekly_hours_worked",
Expand All @@ -164,6 +166,9 @@ def _supports_structural_mortgage_inputs() -> bool:
"self_employment_income_last_year",
]

if not supports_modeled_medicare_part_b_inputs():
CPS_ONLY_IMPUTED_VARIABLES.append("medicare_part_b_premiums")

# Set for O(1) lookup in the splice loop.
_CPS_ONLY_SET = set(CPS_ONLY_IMPUTED_VARIABLES)

Expand Down
4 changes: 3 additions & 1 deletion policyengine_us_data/datasets/puf/puf.py
Original file line number Diff line number Diff line change
Expand Up @@ -802,11 +802,13 @@ class PUF_2024(PUF):
url = "release://policyengine/irs-soi-puf/1.8.0/puf_2024.h5"


# Leave Medicare Part B out of the generic PUF medical-expense split:
# the baseline model now derives Part B premiums separately.
MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS = {
"health_insurance_premiums_without_medicare_part_b": 0.453,
"other_medical_expenses": 0.325,
"medicare_part_b_premiums": 0.137,
"over_the_counter_health_expenses": 0.085,
"medicare_part_b_premiums": 0.137,
}

if __name__ == "__main__":
Expand Down
17 changes: 14 additions & 3 deletions policyengine_us_data/db/etl_national_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
from policyengine_us_data.storage.calibration_targets.soi_metadata import (
RETIREMENT_CONTRIBUTION_TARGETS,
)
from policyengine_us_data.utils.cms_medicare import (
get_beneficiary_paid_medicare_part_b_premiums_notes,
get_beneficiary_paid_medicare_part_b_premiums_source,
get_beneficiary_paid_medicare_part_b_premiums_target,
)
from policyengine_us_data.utils.db import (
DEFAULT_YEAR,
etl_argparser,
Expand Down Expand Up @@ -152,9 +157,15 @@ def extract_national_targets(year: int = DEFAULT_YEAR):
},
{
"variable": "medicare_part_b_premiums",
"value": 112e9,
"source": "CMS Medicare data",
"notes": "Medicare Part B premium payments",
"value": get_beneficiary_paid_medicare_part_b_premiums_target(
HARDCODED_YEAR
),
"source": get_beneficiary_paid_medicare_part_b_premiums_source(
HARDCODED_YEAR
),
"notes": get_beneficiary_paid_medicare_part_b_premiums_notes(
HARDCODED_YEAR
),
"year": HARDCODED_YEAR,
},
{
Expand Down
41 changes: 41 additions & 0 deletions policyengine_us_data/utils/cms_medicare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
MEDICARE_PART_B_GROSS_PREMIUM_INCOME = {
2024: 139.837e9,
}


MEDICARE_STATE_BUY_IN_MINIMUM_BENEFICIARIES = {
2024: 10_000_000,
}


BENEFICIARY_PAID_MEDICARE_PART_B_PREMIUM_TARGETS = {
2024: 112e9,
}


def get_beneficiary_paid_medicare_part_b_premiums_target(year: int) -> float:
try:
return BENEFICIARY_PAID_MEDICARE_PART_B_PREMIUM_TARGETS[year]
except KeyError as exc:
raise ValueError(
f"No beneficiary-paid Medicare Part B premium target sourced for {year}."
) from exc


def get_beneficiary_paid_medicare_part_b_premiums_source(year: int) -> str:
gross_income = MEDICARE_PART_B_GROSS_PREMIUM_INCOME[year] / 1e9
minimum_buy_in = MEDICARE_STATE_BUY_IN_MINIMUM_BENEFICIARIES[year]
return (
"CMS 2025 Medicare Trustees Report Table III.C3 actual 2024 Part B "
f"premium income (${gross_income:.3f}B), plus CMS State Buy-In FAQ "
f"noting states paid Part B premiums for over {minimum_buy_in:,} people"
)


def get_beneficiary_paid_medicare_part_b_premiums_notes(year: int) -> str:
return (
"Approximate beneficiary-paid Medicare Part B out-of-pocket premiums "
"for SPM/MOOP calibration. This intentionally does not target gross "
"trust-fund premium income because Medicaid and other MSP pathways pay "
"premiums on behalf of some enrollees."
)
7 changes: 6 additions & 1 deletion policyengine_us_data/utils/loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from policyengine_us_data.storage.calibration_targets.soi_metadata import (
RETIREMENT_CONTRIBUTION_TARGETS,
)
from policyengine_us_data.utils.cms_medicare import (
get_beneficiary_paid_medicare_part_b_premiums_target,
)
from policyengine_us_data.db.etl_irs_soi import get_national_geography_soi_target
from policyengine_core.reforms import Reform
from policyengine_us_data.utils.soi import pe_to_soi, get_soi
Expand All @@ -26,7 +29,9 @@
HARD_CODED_TOTALS = {
"health_insurance_premiums_without_medicare_part_b": 385e9,
"other_medical_expenses": 278e9,
"medicare_part_b_premiums": 112e9,
"medicare_part_b_premiums": get_beneficiary_paid_medicare_part_b_premiums_target(
2024
),
"over_the_counter_health_expenses": 72e9,
"spm_unit_spm_threshold": 3_945e9,
"child_support_expense": 33e9,
Expand Down
10 changes: 10 additions & 0 deletions policyengine_us_data/utils/policyengine.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,13 @@ def has_policyengine_us_variables(*variables: str) -> bool:
return False

return set(variables).issubset(available_variables)


def supports_medicare_enrollment_input() -> bool:
return has_policyengine_us_variables("medicare_enrolled")


def supports_modeled_medicare_part_b_inputs() -> bool:
return has_policyengine_us_variables(
"medicare_part_b_premiums_reported",
)
25 changes: 25 additions & 0 deletions tests/unit/test_cms_medicare_targets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest

from policyengine_us_data.utils.cms_medicare import (
get_beneficiary_paid_medicare_part_b_premiums_notes,
get_beneficiary_paid_medicare_part_b_premiums_source,
get_beneficiary_paid_medicare_part_b_premiums_target,
)


def test_beneficiary_paid_medicare_part_b_target_2024_is_sourced():
assert get_beneficiary_paid_medicare_part_b_premiums_target(2024) == pytest.approx(
112e9
)


def test_beneficiary_paid_medicare_part_b_source_mentions_primary_sources():
source = get_beneficiary_paid_medicare_part_b_premiums_source(2024)
assert "2025 Medicare Trustees Report" in source
assert "State Buy-In FAQ" in source


def test_beneficiary_paid_medicare_part_b_notes_describe_out_of_pocket_semantics():
notes = get_beneficiary_paid_medicare_part_b_premiums_notes(2024)
assert "out-of-pocket" in notes
assert "gross trust-fund premium income" in notes
27 changes: 27 additions & 0 deletions tests/unit/test_medicare_part_b_inputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from policyengine_us_data.datasets.cps.extended_cps import (
CPS_ONLY_IMPUTED_VARIABLES,
supports_modeled_medicare_part_b_inputs,
)
from policyengine_us_data.datasets.puf.puf import MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS
from policyengine_us_data.utils import policyengine as policyengine_utils


def test_medicare_part_b_clone_imputation_matches_installed_model_support():
assert ("medicare_part_b_premiums" in set(CPS_ONLY_IMPUTED_VARIABLES)) is (
not supports_modeled_medicare_part_b_inputs()
)


def test_puf_medical_breakdown_still_sums_to_one():
assert sum(MEDICAL_EXPENSE_CATEGORY_BREAKDOWNS.values()) == 1.0


def test_supports_medicare_enrollment_input_allows_partial_support(monkeypatch):
monkeypatch.setattr(
policyengine_utils,
"has_policyengine_us_variables",
lambda *variables: variables == ("medicare_enrolled",),
)

assert policyengine_utils.supports_medicare_enrollment_input() is True
assert policyengine_utils.supports_modeled_medicare_part_b_inputs() is False
Loading