← Methods repository
concept

Healthcare Costs (PPPM, PPPY, PMPM)

The measurement, attribution, and time-standardization of healthcare expenditures from claims or linked data into per-patient-per-month/year (PPPM/PPPY) or per-member-per-month (PMPM) rates, with explicit cost basis, perspective, place-of-service decomposition, and modeling of the zero-inflated, right-skewed cost distribution.

Health_Economiccostspppmpppypmpmclaimsheorplace-of-servicemedical-pharmacy-split
Methods reference only. Use primary source citations and local policy before applying this in a study protocol, regulatory submission, payer dossier, or clinical decision.

In plain language

PPPM (per-patient-per-month) and PPPY (per-patient-per-year) are the standard ways to report how much healthcare costs per patient when patients are not all followed for the same length of time — you divide the total dollars a patient (or group) ran up by the number of months they were actually observed. This lets you fairly compare costs across patients who enrolled at different times or dropped out early. PMPM (per-member-per-month) is a related number used by health plans: instead of dividing by the time of sick patients alone, it divides total plan spending by the enrolled months of every member — including the many who had no claims at all — so PMPM is a plan-budget measure, not a patient-burden measure.

Healthcare cost measurement converts the resource use captured in administrative data into a monetary outcome that payers, HTA bodies, and budget-impact models can act on. The recurring deliverable is a standardized cost rate — total (or attributable) dollars divided by accrued patient-time — expressed as PPPM (per-patient-per-month), PPPY (per-patient-per-year), or PMPM (per-member-per-month). The metric looks arithmetically trivial; the defensibility lives entirely in the numerator definition (which dollars, which claims, which perspective) and the denominator definition (whose time, observed how).

Core conceptual distinction

. Three choices are doing the work and they are separable. (1) Cost basis: the allowed amount (the negotiated price the plan and patient together owe) is the standard valuation of "cost to the system"; the paid amount (plan liability only) gives the payer perspective; charges (billed amounts) are inflated and payer-specific and should not be used as cost without a cost-to-charge ratio. (2) Attribution: all-cause sums every claim in the window, while disease-attributable restricts to claims carrying the index diagnosis (and incremental subtracts a matched-control cost to recover the causal increment) — see all-cause-vs-attributable-costs-rwe. (3) Denominator population and clock: PPPM/PPPY divide by the observed patient-time of the analytic cohort (patients who entered the study), whereas PMPM divides plan spending by all enrolled member-months, including the many members with zero cost. PMPM is a plan-budget statistic; PPPM is a patient-burden statistic. They are not interchangeable, and conflating them is a common error in dossiers. The estimand must name all three choices before any code runs.

Pros, cons, and trade-offs

. - vs HCRU counts (hcru-healthcare-resource-utilization): Costs collapse intensity, setting, and price into one comparable dollar metric — one prolonged ICU stay outweighs many cheap office visits in a way an event count never shows, and dollars map directly to payer and HTA decisions. Cost: dollars are hostage to negotiated rates, geography, and calendar-year price inflation, so they transport poorly across payers and countries where raw counts travel fine. Prefer costs for value/budget arguments; always report the paired HCRU table to explain why costs moved. - vs raw totals (no standardization): PPPM/PPPY correctly handle the variable follow-up that censoring, death, and disenrollment force on claims cohorts; a fixed-denominator mean silently penalizes short-followed patients. Cost: a rate divided by tiny person-time explodes for a patient with one expensive event in one observed month, so person- time must be derived carefully and decedents handled explicitly. - vs count models (poisson-negative-binomial-count-models): Cost models (two-part logit+GLM, gamma log-link) carry price and service-mix that pure counts miss. Cost: cost distributions are zero-inflated and heavily right-skewed, so naive OLS or log-OLS-with-retransformation is biased (Manning & Mullahy; Buntin & Zaslavsky), and the two-part/GLM machinery is heavier than a Poisson/NB rate model. Report both families for a complete picture.

When to use

. Any analysis whose decision currency is money: budget-impact models, cost-of-illness and burden studies, the cost arm of a cost-effectiveness or cost-utility analysis, formulary and value dossiers, and comparative cost contrasts between treatment arms. Use PPPM/PPPY when the question is per-patient economic burden over variable follow-up; use PMPM when the question is plan-level spend across an enrolled population (including non-users).

When NOT to use — and when it is actively misleading or dangerous

. - A fixed denominator on censored data. Dividing by a constant 12 months when patients are followed 1–12 months biases the rate and breaks between-arm comparability if follow-up differs by arm. Always use observed person-time. - PMPM reported as if it were patient burden. Folding thousands of zero-cost members into the denominator makes a high-cost disease look cheap; quoting PMPM for an affected-patient narrative understates burden by orders of magnitude. Match the denominator to the claim being made. - Charges used as cost. Chargemaster amounts overstate true cost severalfold and vary by facility; reporting them as cost is indefensible to a payer reviewer. - Immortal time in the numerator/denominator of procedure studies. If patients must survive to a procedure to enter, counting pre-procedure cost-free time as observed person-time inflates the denominator and deflates the rate (see time-zero-index-date-alignment-rwe). - Trimming the outcome before the primary model without a sensitivity plan. A handful of catastrophic cases (CAR-T, transplant, prolonged ICU) dominate the mean; deleting them silently changes the estimand and the population (see cost-outlier-handling-rwe). Pre-specify winsorization/capping/robust models instead.

Data-source operational depth

. - US claims (FFS or commercial): The native substrate. Use `allowed_amount` for system cost and `paid_plan` for payer perspective; decompose patient liability into `copay + coinsurance + deductible`. Real failure modes: adjudication lag — recent months are under-reported until claims mature, so the final months of any extract must be censored or the rate trends spuriously downward; reversals/voids — un-netted reversal lines double-count spend, so net by `claim_id` or filter on final claim status; coordination of benefits (COB) — when a secondary payer exists, `paid_plan` is partial and `allowed_amount` is the safer resource valuation; MA-only person-time — Medicare Advantage encounter data lack adjudicated FFS dollar amounts, so MA-only spans contribute observable time but no valid cost numerator and must be excluded (or shadow-priced) from cost denominators to avoid a differentially deflated rate (see medicare-ffs-ma-commercial-claims-differences-rwe); bundled/episode payments — under DRG, BPCI-A, or CJR-style episodes a single payment covers many services and individual line dollars may be zeroed, so HCRU counts inside the episode remain observable but dollar attribution requires external unit costs or shadow pricing. - EHR: Carries charges or chargemaster estimates, not adjudicated cost; usable for resource counts but requires a cost-to-charge ratio or linkage to claims for credible costing. Visit-driven capture also means a patient who leaves the system is differentially lost, distorting both numerator and person-time. - Registry: Strong for clinical outcomes and severity but rarely captures complete cost; link to claims for the fill/medical dollar history and to a death index to firm up the person-time clock. - Linked claims–EHR–vital records: The ideal substrate (severity + dollar completeness + reliable mortality), but linkage introduces selection (only the linkable subset) and date discrepancies between service, fill, and adjudication that must be reconciled before assigning the cost window.

Differential competing risks by exposure

In elderly or comparative cohorts, the arm with higher mortality accrues less downstream person-time and fewer post-event claims; a naive PPPM can therefore look lower in the sicker arm purely because death truncated their cost accrual. Decedent terminal-care spikes pull the other way. Pre-specify how death is handled (censor vs. count terminal costs) and report cost trajectories, not just a pooled rate.

Worked claims example

A commercially insured adult (`person_id` 1001) initiates total knee arthroplasty (TKA) at `index_date` 2025-01-15 and is observed through disenrollment on 2025-05-30 — 4.5 observed months. The claims extract carries the inpatient TKA facility line (POS 21, CPT 27447, ICD M17.11, `allowed` $28,500), inpatient professional lines, outpatient orthopedic follow-ups (POS 11, CPT 99213, ICD Z47.1), a lab panel, an infused biologic billed on the medical benefit (J-code + administration, `allowed` $1,250), and two oral analgesic pharmacy fills (NDC, `clm_type` = pharmacy). Summing all lines gives an all-cause allowed of $30,847.50, of which $30,672.50 is medical (99.4%) and $175.00 is pharmacy; the inpatient stay alone is ~$28,780. The disease-attributable total applies a diagnosis-position filter on M17/Z47, which acts only on the diagnosis-bearing medical claims. It therefore drops both the unrelated hypertension PCP visit (ICD I10, allowed $168.00) and — because pharmacy claims carry no diagnosis code and so can never match an ICD filter — the two analgesic pharmacy fills (allowed $175.00), yielding disease-attributable allowed of $30,504.50 ($30,847.50 − $168.00 − $175.00). Capturing disease-related pharmacy would require a separate NDC-list step; a pure ICD filter cannot. The standardized all-cause PPPM is $30,847.50 / 4.5 = $6,855; the PPPY (annualizing the same observed rate) is $6,855 × 12 = $82,260, a figure only meaningful as a rate because the patient was not observed a full year. The same patient contributes 4.5 member-months to a PMPM denominator that also sums the full-year, near-zero-cost months of every other enrollee, so the plan PMPM is far smaller — illustrating why the denominator must match the question. In production, person-time comes from enrollment spans (sum of eligible days / 30.437), reversals are netted by `claim_id`, the unmatured final month is dropped, and a pre-specified outlier rule (e.g., 99th-percentile winsorization) is applied before the two-part/gamma model.

Interpreting the output

A study reports that 3 commercially insured patients with total knee arthroplasty had a mean all-cause allowed PPPM of $200.00 ($2,400.00 PPPY) over 20 observed member-months and $4,000 in total allowed spend. A 90th-percentile winsorization sensitivity analysis caps one high-cost month at the 90th-percentile value; the winsorized mean PPPM drops to $150.00 (−25%), illustrating how a single high-cost month reshapes the estimate.

(1) Formal interpretation. The all-cause allowed PPPM of $200.00 is the arithmetic mean monthly cost per patient across 20 observed member-months; the PPPY of $2,400.00 is the same rate annualized (×12) and is interpretable only as a rate, because no patient was followed a full year. This is the allowed amount from the plan's perspective — not charges, and not plan-paid net of patient liability. Because the cost distribution is right-skewed and the mean is substantially above the median, it is driven by high-cost months; the mean governs budget projections while the median describes the typical patient. No causal comparison is made; this is a descriptive within-cohort mean.

(2) Practical interpretation. For a payer or formulary team, the $200.00 PPPM signals what a TKA patient costs on average each month of follow-up at allowed rates. If your plan's negotiated rates differ, or if you are comparing across payers using charges, the number is not portable. The winsorized sensitivity shows that one catastrophic month can inflate the headline by 25% — a reason to pre-specify the outlier rule before submitting to an HTA body.

Worked example

Scenario

A researcher wants to know the average monthly healthcare cost for patients newly diagnosed with migraine headache. Three patients are enrolled in the study but each is observed for a different number of months — one enrolled late, one disenrolled early, and one was present all year. Simply averaging their total costs would unfairly penalize the short-followed patients. Instead, the researcher divides total costs by total observed member-months to compute PPPM, then multiplies by 12 to get PPPY.

Dataset

One row per patient showing their total allowed costs and the number of months they were actually enrolled and observable.

person_idtotal_allowed_costobserved_member_months
1001600.03
10022400.012
10031000.05

Steps

  • Add up the total allowed costs across all three patients: $600 + $2,400 + $1,000 = $4,000.

  • Add up the total observed member-months across all three patients: 3 + 12 + 5 = 20 member-months.

  • Divide total costs by total member-months to get PPPM: $4,000 ÷ 20 = $200.00 per patient per month.

  • Multiply PPPM by 12 to annualize it: $200.00 × 12 = $2,400.00 per patient per year (PPPY).

  • Notice why this works: patient 1001 spent $600 over 3 months ($200/month), patient 1002 spent $2,400 over 12 months ($200/month), and patient 1003 spent $1,000 over 5 months ($200/month) — each has the same underlying rate, and the PPPM correctly recovers that instead of being dragged up by the patient with the most total dollars.

Result

PPPM = $4,000 / 20 member-months = $200.00 per patient per month. PPPY = $200.00 × 12 = $2,400.00 per patient per year. Because we divided by each patient's actual observed time rather than a fixed 12 months, short-followed patients do not distort the average.

Runnable example

python implementation

Line-level claims -> patient-level standardized cost rates. Required inputs (cleaned, reversals already netted): claims : person_id, claim_id, service_date, clm_type ('medical'/'pharmacy'), pos, icd, allowed, paid_plan, copay, coinsurance, deductible...

import pandas as pd
import numpy as np

DAYS_PER_MONTH = 30.437          # mean calendar month
ATTRIB_ICD = ("M17", "Z47")      # disease-attributable code prefixes (study-specific)

def observed_months(enroll: pd.DataFrame) -> pd.Series:
    # Sum eligible, FFS-observable days per person (MA-only spans excluded), convert to months.
    ffs = enroll.loc[~enroll["ma_only"]].copy()
    ffs["days"] = (ffs["enroll_end"] - ffs["enroll_start"]).dt.days + 1
    return ffs.groupby("person_id")["days"].sum() / DAYS_PER_MONTH

def patient_cost_rates(claims: pd.DataFrame, enroll: pd.DataFrame) -> pd.DataFrame:
    claims = claims.copy()
    claims["oop"] = claims[["copay", "coinsurance", "deductible"]].sum(axis=1)
    is_med = claims["clm_type"].eq("medical")
    is_attrib = claims["icd"].str.startswith(ATTRIB_ICD)

    g = claims.groupby("person_id")
    pat = pd.DataFrame({
        "allowed_allcause": g["allowed"].sum(),
        "plan_paid":        g["paid_plan"].sum(),
        "patient_oop":      g["oop"].sum(),
        "medical_allowed":  claims.loc[is_med].groupby("person_id")["allowed"].sum(),
        "pharm_allowed":    claims.loc[~is_med].groupby("person_id")["allowed"].sum(),
        "allowed_attrib":   claims.loc[is_attrib].groupby("person_id")["allowed"].sum(),
    }).fillna(0.0)

    pat["fup_months"] = observed_months(enroll).reindex(pat.index)
    # Guard against near-zero person-time producing exploding rates.
    valid = pat["fup_months"] > 0
    pat.loc[valid, "pppm_allcause"] = pat["allowed_allcause"] / pat["fup_months"]
    pat.loc[valid, "pppy_allcause"] = pat["pppm_allcause"] * 12
    pat.loc[valid, "pppm_attrib"]   = pat["allowed_attrib"] / pat["fup_months"]
    return pat.reset_index()
python implementation

Two-part cost model on the patient-level table from the prior step. Input `pat` must contain: total_allowed, fup_months, plus baseline covariates (age, female, charlson) and a binary `treated` exposure. Part 1 models the probability of any cost (the zero...

import numpy as np
import statsmodels.api as sm
import statsmodels.formula.api as smf

def two_part_cost_model(pat):
    pat = pat.copy()
    pat["any_cost"] = (pat["total_allowed"] > 0).astype(int)
    pat["log_fup"] = np.log(pat["fup_months"])

    # Part 1: any cost vs. none (the structural zeros).
    m1 = smf.logit("any_cost ~ age + female + charlson + treated", data=pat).fit(disp=0)

    # Part 2: positive cost as a rate via gamma GLM with log link + person-time offset.
    pos = pat.loc[pat["total_allowed"] > 0]
    m2 = smf.glm("total_allowed ~ age + female + charlson + treated",
                 data=pos, offset=pos["log_fup"],
                 family=sm.families.Gamma(link=sm.families.links.Log())).fit()

    # Recycled-prediction incremental PPPM: E[cost | treated=1] - E[cost | treated=0].
    def expected_pppm(t):
        d = pat.assign(treated=t, log_fup=0.0)               # offset 0 -> per-month rate
        p_any = m1.predict(d)
        mu = m2.predict(d, offset=np.zeros(len(d)))
        return float((p_any * mu).mean())

    incremental_pppm = expected_pppm(1) - expected_pppm(0)
    return m1, m2, incremental_pppm