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/8095.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Split medical expenses into program-specific variables and remove the generic medical out-of-pocket expense aggregate.
58 changes: 27 additions & 31 deletions docs-quarto/methodology/moop-decomposition.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -7,77 +7,73 @@ subtitle: "Layering rules-based premium computations on CPS-imputed medical cost

The Supplemental Poverty Measure subtracts medical out-of-pocket expenses (MOOP) from household resources. PolicyEngine US imports MOOP from the Current Population Survey's reported private health insurance premiums and other medical expenses. Imputed MOOP is accurate on average for the year the CPS was collected, but it does not respond to policy reform. If a user eliminates the ACA premium tax credit, simulated PTC goes to zero while imputed MOOP stays at the pre-reform value, understating the true cost to households.

The fix is to decompose MOOP into a **rules-based** part (variables computable from statute) and an **imputed residual** (everything not modeled). Reforms move the rules-based part; the residual covers copays, over-the-counter costs, non-modeled employer premiums, etc.
The fix is to decompose MOOP into a **rules-based** part (variables computable from statute) and imputed other components for costs the model cannot yet calculate directly. Reforms move the rules-based part; the imputed other components cover copays, over-the-counter costs, non-modeled employer premiums, etc.

## What's shipped today

The SPM-unit-level wrapper combines imputed and computed pieces:
The SPM-unit-level wrapper now combines explicit premium and non-premium
pieces:

```python
def formula(spm_unit, period, parameters):
imputed_moop = add(spm_unit, period, ["medical_out_of_pocket_expenses"])
imputed_part_b = add(spm_unit, period, ["medicare_part_b_premiums"])
computed_part_b = add(
spm_unit, period, ["income_adjusted_part_b_premium"]
)
chip_premium = add(spm_unit, period, ["chip_premium"])
medicaid_premium = add(spm_unit, period, ["medicaid_premium"])
return (
imputed_moop
- imputed_part_b
+ computed_part_b
+ chip_premium
+ medicaid_premium
)
spm_unit_medical_out_of_pocket_expenses = (
spm_unit_health_insurance_premiums
+ spm_unit_non_premium_medical_out_of_pocket_expenses
)
```

| Component | How it flows | Double-count risk |
|---|---|---|
| Imputed person-level MOOP (private premiums + other medical) | In via `medical_out_of_pocket_expenses` | — |
| Medicare Part B | Imputed subtracted, rules-based added (base + IRMAA) | No — clean swap |
| CHIP premium (`chip_premium`) | Added on top of imputed MOOP | Yes — small overstatement for CHIP-paying families until CPS imputation strips the CHIP share at baseline |
| Medicaid expansion premium (`medicaid_premium`) | Added on top of imputed MOOP | Yes — same pattern; IN HIP is the only state with a live (if currently suspended) schedule |
| Marketplace net premium (`marketplace_net_premium`) | **Not wired in yet** | — |
| Other health insurance premiums | In via `other_health_insurance_premiums` from data construction | No once data are built with baseline modeled premiums removed |
| Medicare Part B | Computed by `medicare_part_b_premium` (base + IRMAA) | No |
| CHIP premium (`chip_premium`) | Computed by state schedules | No once data are built with baseline modeled premiums removed |
| Medicaid premium (`medicaid_premium`) | Computed by state schedules | No once data are built with baseline modeled premiums removed |
| Marketplace net premium (`marketplace_net_premium`) | Computed from selected plan premium minus used PTC | No once data are built with baseline modeled premiums removed |
| Other medical expenses and OTC health expenses | In via `other_medical_expenses` and `over_the_counter_health_expenses` | No |

Person-level `medical_out_of_pocket_expenses` is deliberately left untouched so downstream consumers that use it directly — SNAP excess medical deduction, state itemized medical deductions — continue to read the imputed figure. Only SPM resources see the decomposed value.
The old generic `medical_out_of_pocket_expenses` input is removed. Program
rules outside SPM use their own statutory medical-expense wrappers rather than
the SPM MOOP definition.

## The target architecture

Once the CPS-imputation side strips each premium's baseline share from the reported aggregate, the wrapper becomes cleanly layered:
Once the CPS-imputation side strips each premium's baseline share from the reported aggregate, the wrapper is cleanly layered:

```
spm_unit_MOOP = imputed_residual
spm_unit_MOOP = other_health_insurance_premiums
+ computed_chip_premium
+ computed_medicare_part_b_premium
+ computed_marketplace_net_premium
+ computed_medicaid_premium
+ other_medical_expenses
+ over_the_counter_health_expenses
```

with

```
imputed_residual = imputed_MOOP_total − baseline_computed_premium_sum
other_health_insurance_premiums =
reported_non_medicare_premiums − baseline_computed_premium_sum
```

where `baseline_computed_premium_sum` is evaluated under the CPS reference year's rules during data construction. Baseline totals reconcile to CPS-reported MOOP exactly; reforms propagate through the computed pieces. Medicare Part B already works this way. CHIP and Medicaid will follow once per-household baseline premiums are pre-subtracted in `policyengine-us-data`.
where `baseline_computed_premium_sum` is evaluated under the CPS reference year's rules during data construction. Baseline totals reconcile to CPS-reported premiums; reforms propagate through the computed pieces.

## Components computable today

| Component | Variable | Entity | Status |
|---|---|---|---|
| CHIP premium | `chip_premium` | TaxUnit | 17 states encoded (see [CHIP](../programs/chip.qmd)) |
| Medicare Part B | `income_adjusted_part_b_premium` | Person | Rules-based (base + IRMAA tiers); **fully swapped** in SPM wrapper |
| Marketplace net premium | `marketplace_net_premium` | TaxUnit | `SLCSP × benchmark_ratio − used_PTC`; benchmark ratio imputed per-household via CPS back-out; not yet wired into the SPM wrapper |
| Medicare Part B | `medicare_part_b_premium` | Person | Rules-based (base + IRMAA tiers) |
| Marketplace net premium | `marketplace_net_premium` | TaxUnit | `SLCSP × benchmark_ratio − used_PTC`; benchmark ratio imputed per-household via CPS back-out |
| Medicaid expansion premium | `medicaid_premium` | TaxUnit | IN HIP POWER Account (suspended since 2020, schedule reform-ready) |

## Why Marketplace net premium is layered differently

CPS private-premium reports conflate Marketplace, employer, and direct-purchase coverage in a single aggregate. `selected_marketplace_plan_benchmark_ratio` is now populated per tax unit via back-out from the reported premium, but the baseline subtraction of "imputed Marketplace premium" requires isolating the Marketplace share from the CPS aggregate — in-progress as a data-side follow-up. Until then, `marketplace_net_premium` is available as a variable but not wired into the SPM wrapper.
CPS private-premium reports conflate Marketplace, employer, and direct-purchase coverage in a single aggregate. `selected_marketplace_plan_benchmark_ratio` is populated per tax unit via back-out from the reported premium. Data construction then subtracts baseline `marketplace_net_premium` from reported non-Medicare premiums so SPM can add the modeled Marketplace premium without double counting.

## Gaps

- **Copays** — the federal 5 % of income CHIP cost-sharing cap (42 CFR 457.560) binds on premiums + copays combined. Without copay modeling, the cap never bites on premium alone in realistic cases.
- **Employer plan employee share** — stays in the imputed residual; employer plans aren't statutory and can't be computed from rules without a separate employer-plan model.
- **Employer plan employee share** — stays in `other_health_insurance_premiums`; employer plans aren't statutory and can't be computed from rules without a separate employer-plan model.
- **Michigan and Montana historical Medicaid expansion premiums** — eliminated 2024-01-01 and 2023-01-01 respectively; historical schedules not yet encoded for reform analysis.

## See also
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
age: 77
employment_income: 200
ssi_countable_resources: 1_000
medical_out_of_pocket_expenses: 500
other_medical_expenses: 500
person_2:
age: 66
employment_income: 100
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,14 @@
2019: 0
output:
medicare_cost: 9_298 # $11,080 - $1,782 Part B (2021: $148.50*12)

- name: unit test 6 - direct Part B premium input
period: 2025
input:
age: 65
is_medicare_eligible: true
medicare_enrolled: true
medicare_quarters_of_coverage: 40
medicare_part_b_premium: 3_000
output:
medicare_cost: 11_500 # $14,500 spending - $3,000 Part B premium
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 2_220 # $185 * 12 months
medicare_part_b_premium: 2_220 # $185 * 12 months

- name: unit test 2 - married, extra premium
period: 2025
Expand All @@ -22,7 +22,7 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 4_440 # $370 * 12 months
medicare_part_b_premium: 4_440 # $370 * 12 months

- name: unit test 3 - single, extra premium
period: 2025
Expand All @@ -35,7 +35,7 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 7_546.8 # $628.90 * 12 months
medicare_part_b_premium: 7_546.8 # $628.90 * 12 months

- name: unit test 4 - HOH, extra premium
period: 2025
Expand All @@ -48,7 +48,7 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 3_108 # $259 * 12 months
medicare_part_b_premium: 3_108 # $259 * 12 months

- name: unit test 5 - married, no extra premium
period: 2025
Expand All @@ -61,7 +61,7 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 2_220 # $185 * 12 months
medicare_part_b_premium: 2_220 # $185 * 12 months

- name: unit test 6 - married, extra premium, rich
period: 2025
Expand All @@ -74,4 +74,4 @@
2023: 0
is_medicare_eligible: true
output:
income_adjusted_part_b_premium: 7_546.8 # $628.90 * 12 months
medicare_part_b_premium: 7_546.8 # $628.90 * 12 months
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODOs:
# - Tests involving is_hud_elderly_disabled_family
# - Tests involving childcare_expenses
# - Tests involving medical_out_of_pocket_expenses (which only applies to elderly/disabled families)
# - Tests involving other_medical_expenses (which only applies to elderly/disabled families)

- name: Default value is zero.
period: 2022
Expand Down Expand Up @@ -94,8 +94,8 @@
period: 2022
input:
hud_annual_income: 10_000
age: 70
medical_out_of_pocket_expenses: 300
age: 62
other_medical_expenses: 300
output:
hud_adjusted_income: 9_600 # $10,000 - $400 elderly/disabled - $300 medical expense deduction

Expand All @@ -104,7 +104,7 @@
input:
hud_annual_income: 10_000
is_disabled: true
medical_out_of_pocket_expenses: 301
other_medical_expenses: 301
output:
hud_adjusted_income: 9_599

Expand All @@ -113,14 +113,14 @@
input:
hud_annual_income: 10_000
is_disabled: true
medical_out_of_pocket_expenses: 20_000
other_medical_expenses: 20_000
output:
hud_adjusted_income: 0

- name: Single person with no disabilites with medical expenses does not have medical expense deductions.
period: 2022
input:
hud_annual_income: 10_000
medical_out_of_pocket_expenses: 20_000
other_medical_expenses: 20_000
output:
hud_adjusted_income: 10_000
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- name: Itemized medical expenses sum medical out-of-pocket expenses.
period: 2025
input:
people:
person1:
other_medical_expenses: 1_200
person2:
other_medical_expenses: 300
tax_units:
tax_unit:
members: [person1, person2]
output:
itemized_medical_expenses: 1_500
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- name: Medical expense deduction applies the AGI floor to itemized medical expenses.
period: 2025
input:
other_medical_expenses: 10_000
positive_agi: 100_000
output:
itemized_medical_expenses: 10_000
medical_expense_deduction: 2_500
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
age: 19
was_in_foster_care: true
employment_income: 11_111
medical_out_of_pocket_expenses: 1_332
other_medical_expenses: 1_332
rent: 1_332
is_in_foster_care: true
is_tax_unit_head_or_spouse: true
Expand All @@ -92,7 +92,7 @@
age: 19
was_in_foster_care: true
employment_income: 11_111
medical_out_of_pocket_expenses: 1_332
other_medical_expenses: 1_332
rent: 1_332
is_in_foster_care: true
is_tax_unit_head_or_spouse: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
you:
age: 44
immigration_status_str: CITIZEN
medical_out_of_pocket_expenses: 600
other_medical_expenses: 600
is_aca_eshi_eligible: false
is_pregnant: false
ca_calworks_child_care_time_category: MONTHLY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
period: 2021
input:
state_code: AL
medical_out_of_pocket_expenses: 2_000
other_medical_expenses: 2_000
al_agi: 80_000
output:
al_medical_expense_deduction: 0
Expand All @@ -12,7 +12,7 @@
period: 2021
input:
state_code: AL
medical_out_of_pocket_expenses: 7_000
other_medical_expenses: 7_000
al_agi: 90_000
output:
al_medical_expense_deduction: 3_400
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
input:
people:
person1:
medical_out_of_pocket_expenses: 5_000
other_medical_expenses: 5_000
ar_agi_indiv: 120_000
person2:
medical_out_of_pocket_expenses: 18_000
other_medical_expenses: 18_000
ar_agi_indiv: 120_000
tax_units:
tax_unit:
Expand All @@ -23,10 +23,10 @@
input:
people:
person1:
medical_out_of_pocket_expenses: 5_000
other_medical_expenses: 5_000
ar_agi_indiv: 240_000
person2:
medical_out_of_pocket_expenses: 18_000
other_medical_expenses: 18_000
ar_agi_indiv: 0
tax_units:
tax_unit:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
input:
people:
person1:
medical_out_of_pocket_expenses: 5_000
other_medical_expenses: 5_000
ar_agi_joint: 120_000
person2:
medical_out_of_pocket_expenses: 18_000
other_medical_expenses: 18_000
ar_agi_joint: 120_000
tax_units:
tax_unit:
Expand All @@ -23,10 +23,10 @@
input:
people:
person1:
medical_out_of_pocket_expenses: 5_000
other_medical_expenses: 5_000
ar_agi_joint: 240_000
person2:
medical_out_of_pocket_expenses: 18_000
other_medical_expenses: 18_000
ar_agi_joint: 0
tax_units:
tax_unit:
Expand Down
Loading
Loading