Claude
Skills
Sign in
Back

policyengine-healthcare

Included with Lifetime
$97 forever

Healthcare program modeling in PolicyEngine-US — Medicaid, ACA marketplace, CHIP, and Medicare. Covers encoding rules, running analyses, and navigating the unique complexity of US healthcare programs. Triggers: "healthcare", "health insurance", "Medicaid", "ACA", "CHIP", "Medicare", "marketplace", "premium tax credit", "APTC", "PTC", "SLCSP", "benchmark plan", "rating area", "age curve", "family tier", "coverage gap", "Medicaid expansion", "MAGI", "medicaid_magi", "aca_magi", "medicaid_income_level", "medicaid_category", "enrollment", "takeup", "take-up", "per capita", "CSR", "cost sharing", "insurance premium", "second lowest silver", "required contribution percentage", "42 CFR", "IRC 36B", "categorical eligibility", "expansion adult", "healthcare reform", "healthcare analysis", "health policy".

General

What this skill does


# PolicyEngine healthcare programs

> **Scope:** This skill covers the healthcare domain — Medicaid, ACA marketplace, CHIP, and Medicare as modeled in PolicyEngine-US. For general PolicyEngine-US patterns, see the `policyengine-us` skill. For variable/parameter implementation patterns, see the `policyengine-variable-patterns` and `policyengine-parameter-patterns` skills.

## For users

### What healthcare programs does PolicyEngine model?

PolicyEngine-US models four interconnected healthcare programs:

| Program | What it does | Key variable |
|---------|-------------|--------------|
| **Medicaid** | Free/low-cost coverage for low-income individuals | `medicaid` |
| **ACA marketplace** | Subsidized private insurance via premium tax credits | `aca_ptc` |
| **CHIP** | Children's health coverage above Medicaid thresholds | `per_capita_chip` |
| **Medicare** | Coverage for seniors and disabled individuals | `medicare` |

These programs are **mutually exclusive by design** — Medicaid eligibility disqualifies you from ACA subsidies, and CHIP covers children who don't qualify for Medicaid. This interconnection is a key modeling challenge.

### Why healthcare is different from other benefits

Most benefit programs (SNAP, TANF, EITC) have a single income test and a single benefit amount. Healthcare programs are different:

- **9 eligibility categories** for Medicaid alone (infant, young child, older child, young adult, adult expansion, parent, pregnant, SSI recipient, senior/disabled)
- **51 different Medicaid programs** — every state sets its own income limits and rules
- **~500 ACA rating areas** — premiums vary by geographic zone, not just by state
- **Actuarial calculations** — ACA subsidies depend on the second-lowest silver plan premium in your area, adjusted by age
- **Program interactions** — losing Medicaid doesn't automatically mean gaining ACA access (the "coverage gap")

## For analysts

### Healthcare variables reference

#### Medicaid

| Variable | Entity | Description |
|----------|--------|-------------|
| `medicaid` | person | Annual Medicaid benefit amount |
| `is_medicaid_eligible` | person | Overall eligibility (combines all checks) |
| `medicaid_enrolled` | person | Actually enrolled (eligibility x take-up) |
| `medicaid_category` | person | Enum: SSI_RECIPIENT, INFANT, YOUNG_CHILD, OLDER_CHILD, PREGNANT, PARENT, YOUNG_ADULT, ADULT, SENIOR_OR_DISABLED, NONE |
| `medicaid_income_level` | person | Person's tax unit MAGI as fraction of FPL |
| `medicaid_magi` | tax_unit | Modified AGI for Medicaid (= AGI + state additions) |
| `takes_up_medicaid_if_eligible` | person | Pseudo-random take-up flag (default rate: 93%) |
| `medicaid_cost` | person | Per-capita cost by eligibility group and state |

#### ACA marketplace

| Variable | Entity | Description |
|----------|--------|-------------|
| `aca_ptc` | tax_unit | Annual premium tax credit amount |
| `is_aca_ptc_eligible` | person | PTC eligibility (income 100-400%+ FPL, no other coverage) |
| `aca_magi` | tax_unit | Delegates to `medicaid_magi` via `adds = ["medicaid_magi"]` |
| `aca_magi_fraction` | tax_unit | Income as percentage of FPL |
| `aca_required_contribution_percentage` | tax_unit | Household's required premium contribution rate |
| `slcsp` | tax_unit | Second-lowest silver plan premium (monthly, definition_period=MONTH). **Returns $0 for ineligible people** — the model skips the calculation when someone doesn't qualify for ACA. To get the unsubsidized benchmark premium regardless of eligibility, look at the underlying rating area cost parameters directly. |
| `takes_up_aca_if_eligible` | tax_unit | Take-up flag (default rate varies) |

#### CHIP

| Variable | Entity | Description |
|----------|--------|-------------|
| `per_capita_chip` | person | CHIP benefit per capita |
| `is_chip_eligible` | person | CHIP eligibility |
| `chip_category` | person | Enum: CHILD, PREGNANT_STANDARD, PREGNANT_FCEP, NONE |

### Household setup for healthcare analysis

Healthcare calculations require the state to be specified (unlike some federal-only programs):

```python
from policyengine_us import Simulation

situation = {
    "people": {
        "person1": {
            "age": {"2026": 35},
            "employment_income": {"2026": 25_000},
            "is_tax_unit_head": {"2026": True},
        },
        "person2": {
            "age": {"2026": 8},
            "is_tax_unit_dependent": {"2026": True},
        },
    },
    "tax_units": {"tax_unit": {"members": ["person1", "person2"]}},
    "families": {"family": {"members": ["person1", "person2"]}},
    "spm_units": {"spm_unit": {"members": ["person1", "person2"]}},
    "marital_units": {"marital_unit": {"members": ["person1"]}},
    "households": {
        "household": {
            "members": ["person1", "person2"],
            "state_name": {"2026": "NY"},
        }
    },
}

sim = Simulation(situation=situation)

# Check Medicaid eligibility at person level
medicaid_eligible = sim.calculate("is_medicaid_eligible", 2026)
# Check ACA PTC at tax unit level
aca_ptc = sim.calculate("aca_ptc", 2026)
```

### Healthcare reform modeling

#### Medicaid expansion repeal (parameter modification)

```python
from policyengine_core.reforms import Reform
from policyengine_core.periods import instant

YEAR = 2026

def create_medicaid_expansion_repeal(state="UT"):
    """Remove adult Medicaid expansion by setting income limit to -inf."""
    def modify_parameters(parameters):
        parameters.gov.hhs.medicaid.eligibility.categories.adult.income_limit[state].update(
            start=instant(f"{YEAR}-01-01"),
            stop=instant("2100-12-31"),
            value=float("-inf"),
        )
        return parameters

    class reform(Reform):
        def apply(self):
            self.modify_parameters(modify_parameters)
    return reform
```

#### IRA subsidy extension (variable-override reform)

The most common ACA policy question: extending the IRA-enhanced subsidies beyond their 2025 sunset. **`Reform.from_dict()` does not work** for ACA contribution rate parameters because they are list-valued (threshold + initial + final arrays). Use a variable-override reform instead:

```python
from policyengine_us import Microsimulation
from policyengine_core.reforms import Reform

ANALYSIS_YEAR = 2026  # Don't use "YEAR" — it shadows model_api.YEAR

# Look up actual parameter values to build the reform
from policyengine_us import CountryTaxBenefitSystem
p = CountryTaxBenefitSystem().parameters
contrib = p.gov.aca.required_contribution_percentage

# Check what values look like pre/post sunset
print("2025 initial rates:", contrib.initial("2025-01-01"))  # IRA-enhanced
print("2026 initial rates:", contrib.initial("2026-01-01"))  # Post-sunset (higher)

# Option A: Use Reform.from_dict for the scalar parameters that DO work
reform = Reform.from_dict({
    # The 400% FPL cap removal (IRA made subsidies available above 400%)
    'gov.aca.ptc_income_eligibility[2].amount': {
        f'{ANALYSIS_YEAR}-01-01.2100-12-31': True
    },
}, 'policyengine_us')

# Option B: For contribution rates (list-valued), use modify_parameters
from policyengine_core.periods import instant

def create_ira_extension():
    def modify_parameters(parameters):
        # Restore IRA-era contribution percentages
        # You need to set each bracket's initial and final rates
        for bracket_param in ['initial', 'final']:
            getattr(
                parameters.gov.aca.required_contribution_percentage,
                bracket_param,
            ).update(
                start=instant(f"{ANALYSIS_YEAR}-01-01"),
                stop=instant("2100-12-31"),
                value=getattr(contrib, bracket_param)("2025-01-01"),
            )
        return parameters

    class reform(Reform):
        def apply(self):
            self.modify_parameters(modify_parameters)
    return reform
```

Also see existing reforms in `policyengine_us/reforms/aca/` for production examples of variable-ov

Related in General