← Methods repository
concept

Composite Endpoint Construction

The pre-specified rule that combines two or more component events (e.g., death, non-fatal MI, hospitalization) into a single outcome, defining each component's case definition, the event-date assignment (typically the earliest qualifying component), the deduplication window, and the analytic estimand (time-to-first-event, recurrent-event, hierarchical, or competing-risk) in real-world data.

Outcome_Measureoutcome_measurecomposite-endpointmacetime-to-first-eventwin-ratiocompeting-riskscomponent-ascertainmentoutcome-algorithm-construction
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

A composite endpoint combines several serious health events — such as heart attack, stroke, and death — into a single outcome so that a study has enough events to detect a treatment effect. The rule is simple: a patient counts as having the outcome on the earliest date that any one of those events occurs, and follow-up stops there. Because the study records which specific event happened first, researchers can always go back and look at each event separately. One honest catch: if one event (say, a hospitalization) happens far more often than the others, it can dominate the result and make a treatment look effective even if it does nothing for the rarer but more serious events.

A composite endpoint counts a patient as having "the outcome" when any one of several pre-specified component events occurs. The classic example is 3-point MACE (all-cause death + non-fatal myocardial infarction + non-fatal stroke). The default analytic representation is time-to-first-event: the composite event date is the earliest date among the patient's qualifying components, and follow-up is censored at that first event. The deceptively simple `MIN(component_date)` hides every hard decision — what each component's case definition is, how each component is ascertained (and how well), how same-window events are deduplicated, and which estimand the composite is meant to support.

Core conceptual / estimand distinction

The construction rule and the estimand are separable choices that must both be pre-specified, because they drive different models: - Time-to-first-event (default). Event = earliest component; analyze with cause-specific Cox or pooled logistic. Simple and the FDA/regulatory default, but it (a) discards all information after the first event, (b) weights a soft component (e.g., hospitalization) exactly the same as death, and (c) lets a frequent, low-severity, well-ascertained component dominate the hazard ratio while a rare, high-severity component drives clinical meaning. - Recurrent / total events (Andersen–Gill, LWYY, negative-binomial). Counts all qualifying events per patient, not just the first — appropriate when repeated hospitalizations carry information (e.g., heart-failure burden). - Hierarchical / win-ratio (Pocock 2011) and restricted-mean approaches. Compare patients pairwise on the most severe component first, breaking ties on the next, so death is never outweighed by a hospitalization. Respects clinical priority at the cost of an unfamiliar effect measure (win ratio) and no single survival curve. - Cumulative incidence with death as a competing risk. If a non-fatal component is the endpoint of interest and death competes, the cause-specific hazard answers a different question than the subdistribution (Fine–Gray) cumulative incidence. Naively treating death as censoring overstates the non-fatal component's cumulative incidence.

Two operational invariants make all of this auditable: retain the composite event date AND the component identifier that produced it. Every component-level sensitivity analysis, every "which piece drove the effect" question, and every competing-risk re-analysis depends on knowing which component fired and when.

Pros, cons, and trade-offs

- vs analyzing each component separately: A composite raises event counts and statistical power, shortens required follow-up, and side-steps the multiplicity of testing several outcomes. Cost: it presumes the components share a common treatment effect and similar clinical importance — assumptions that are false more often than not (Freemantle 2003; Ferreira-González 2007). Prefer the composite only when components are biologically linked, of comparable severity, and expected to move in the same direction; always report the components individually alongside it. - vs hierarchical / win-ratio analysis: Time-to-first-event is universally understood and maps to a hazard ratio and a Kaplan–Meier curve. The win ratio (Pocock 2011) preserves the severity ordering death > MI > hospitalization, so a drug that prevents deaths but causes extra admissions is not falsely rewarded. Prefer the win ratio / hierarchical estimand when components differ sharply in severity or when a soft component dominates the count; prefer time-to-first-event when components are of similar importance and regulators expect the conventional contrast. - vs a single hard outcome (e.g., all-cause mortality): A single hard endpoint is unambiguous, maximally ascertainable from death indices, and immune to differential surveillance — but is often under-powered. The composite trades that cleanliness for power. Prefer the single hard endpoint when it is adequately powered and when differential ascertainment of soft components by arm is plausible.

When to use

Comparative effectiveness or safety where a single hard endpoint is under-powered and components are clinically linked and of similar severity (cardiovascular, renal, oncologic composites); HTA models needing an event-driven outcome; pre-specified regulatory endpoints (e.g., MACE in cardiovascular-outcomes-trial emulations). The composite definition, each component's case definition and data-source validation (PPV/sensitivity), the deduplication window, and the estimand must all be fixed in the protocol/SAP before programming.

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

- Components have opposing or markedly heterogeneous expected effects. If a drug plausibly lowers MI but raises hospitalization, the composite hazard ratio can hover near the null while masking real, opposite-direction component effects. Require pre-specified, plausible per-component effect estimates before trusting any composite (Freemantle 2003). - A soft, frequent component dominates the count but a hard component drives clinical meaning. When 80% of composite events are hospitalizations and 5% are deaths, the "significant" composite is essentially a hospitalization study wearing a mortality costume. Always tabulate the component breakdown. - Differential ascertainment by arm. Composite event date = `MIN(component_date)`, so the best-ascertained component systematically wins the event-date assignment. If one arm has more healthcare contact (e.g., a drug requiring frequent labs or monitoring visits), its non-fatal, encounter-dependent components are detected earlier and more often — inflating its composite rate for reasons that have nothing to do with biology. This is the dominant RWE-specific failure mode. - Cross-study comparison with inconsistent definitions. 3-point vs 4-point vs 5-point MACE, or different ICD code lists and code-position rules, are not interchangeable; pooling or comparing them is comparing different outcomes. - Death as a component AND a subdistribution interpretation for a non-fatal piece. You cannot coherently treat death as both part of the composite and a competing risk for a sub-component in the same framing — pick one estimand.

Data-source operational depth

- Claims (FFS): Each component is an algorithm with its own validity. All-cause death is near-complete only when linked to SSA Master Death File / NDI (claims-based death proxies — e.g., discharge status, enrollment termination — miss out-of-hospital deaths). Non-fatal MI and stroke are ICD-10-CM algorithms whose PPV depends on code position (primary inpatient diagnosis ≈ high PPV; any-position or outpatient ≈ low PPV) and whether a `1 inpatient OR 2 outpatient` rule is applied. Failure modes: (a) acute-event re-coding — a single MI generates an index hospitalization, a transfer, and follow-up outpatient claims; without a deduplication / clean window these become spurious "recurrent" events; (b) adjudication/claims lag — late-arriving claims shift event dates and can move an event across the end-of-data boundary; (c) differential surveillance by arm as above. - Medicare Advantage-only person-time: Encounter (non-fatal) claims are frequently incomplete or absent, while death is still captured via linked mortality. The composite therefore degrades into a near-death-only outcome in MA segments — silently changing the estimand by enrollment type. Restrict to FFS Parts A/B (and D for exposure) for the components that rely on medical claims, or model enrollment type explicitly. - EHR: Non-fatal components come from problem lists, encounter diagnoses, labs, and notes; capture is visit-driven, so a patient who seeks care outside the system has events that simply never appear (external-care leakage). Death is notoriously incomplete in EHR alone — link to a death index. EHR can sharpen a component definition (troponin for MI, imaging for stroke) where claims cannot. - Registry / linked: Registries often provide adjudicated components (the gold standard for the composite's clinical pieces) but weak longitudinal completeness; link to claims for full follow-up and to a death index for the mortality component. Linkage introduces selection (only the linkable subset) and date-discrepancy issues (claim service date vs adjudicated event date) that must be reconciled before assigning the composite event date.

Worked claims example

Endpoint: 3-point MACE (all-cause death + non-fatal MI + non-fatal stroke) in an SGLT2-inhibitor vs DPP-4-inhibitor active-comparator new-user study in a commercial + Medicare FFS database with linked mortality. (1) Component case definitions. Non-fatal MI = an inpatient claim with ICD-10-CM `I21.x` in the primary position (high-PPV rule; pre-specify a sensitivity analysis using any-position). Non-fatal stroke = inpatient `I63.x` primary. Death = earliest date across the mortality source hierarchy (linked SSA/NDI date, then inpatient discharge status = expired, then enrollment-termination-for-death) — never a claims proxy alone. (2) Continuous enrollment + washout. Require 365 days of continuous FFS A/B (and D for the exposure) before `index_date`; restrict component ascertainment to FFS person-time so MA gaps do not silently drop non-fatal events. (3) Deduplication / clean window. An inpatient MI plus a transfer and two follow-up office visits coded I21 within 30 days is one MI — collapse component claims within a pre-specified acute-event window to the admission date. (4) Event-date assignment. Composite event date = earliest qualifying component date; store both the date and the component label. Edge case: a stroke claim dated three days before a death date — if the death is the index hospitalization's terminal event, count it as a single composite event at the stroke date with component = "non-fatal stroke then death" flagged; the time-to-first-event estimand counts only the first, but the component flag preserves the death for the competing-risk re-analysis. (5) Follow-up & censoring. From `index_date` to the composite event date, censoring at disenrollment, end of data, and (for as-treated) discontinuation. (6) Analysis & sensitivity. Primary = cause-specific Cox for time-to-first MACE; report the component breakdown (how many deaths vs MI vs stroke), a Fine–Gray cumulative-incidence analysis when a single non-fatal component is examined with death as a competing risk, the win-ratio as a severity-weighted alternative, and re-runs under the any-position MI rule and an alternative deduplication window.

Interpreting the output

. In the Patient 7741 example, an MI claim in principal position appears on day 100 from index; a stroke claim appears on day 204 and a death record on day 289. The composite-endpoint algorithm fires on day 100 and labels the event component as "non-fatal MI." Stroke and death are not counted as additional composite events — only the first qualifying component triggers the time-to-event clock.

Formal interpretation: the composite event date is day 100, and the component label is non-fatal MI. The hazard ratio estimated from this composite endpoint applies to the composite (MI, stroke, or death occurring first) — it does not apply to MI alone, stroke alone, or death alone. This is a consequential restriction: a treatment that prevents MI but shifts events toward stroke will produce a composite HR near 1.0 even though the clinical picture changed substantially. The composite is driven by whichever component accumulates events most rapidly, often the softest (most frequently occurring) component; if most composite events are MI and few are stroke or death, the composite HR is essentially an MI HR, and calling it a "MACE" estimate is misleading.

Practical interpretation: always report the component breakdown — number and proportion of composite events attributable to each component — alongside the primary composite HR. If one component dominates, pre-specify component-specific analyses and consider whether the composite still addresses the clinical question. The win-ratio analysis, which weights events by clinical severity, is a useful pre-specified sensitivity analysis when component heterogeneity is anticipated.

Worked example

Scenario

Margaret is a 68-year-old Medicare patient enrolled in an SGLT2-inhibitor study. Her index date — the day she fills her first prescription — is January 1, 2024. Researchers are tracking 3-point MACE: non-fatal heart attack (MI), non-fatal stroke, or death from any cause, whichever comes first. They follow Margaret until one of those events happens, she loses insurance coverage, or the data end on December 31, 2024. She experiences all three component events during the year, but only the earliest one defines her composite endpoint.

Dataset

Component-event claims for one patient (Margaret, person_id 7741). Each row is a qualifying event that passed its own code-based case definition — inpatient primary-diagnosis code for MI and stroke, linked mortality file for death.

person_idevent_datecomponentsource
77412024-04-10MIinpatient claim, I21.9 primary
77412024-07-22STROKEinpatient claim, I63.9 primary
77412024-10-15DEATHlinked mortality file

Steps

  • Start the clock at Margaret's index date: January 1, 2024 (day 0).

  • List every qualifying component event inside her follow-up window, with the date it occurred: MI on April 10 (day 100), stroke on July 22 (day 204), death on October 15 (day 289).

  • Apply the earliest-event rule: composite event date = the minimum of {April 10, July 22, October 15} = April 10.

  • Margaret's composite outcome is recorded as: event = 1 (yes, she had the composite), time_to_composite = 100 days, composite_component = 'MI'.

  • Follow-up is stopped at day 100 — the stroke and death still happened, but they occur after the composite has already fired and are not counted in a time-to-first-event analysis.

  • The composite_component label 'MI' is kept alongside the date so analysts can later ask 'how many composite events were MIs vs strokes vs deaths?' and run sensitivity analyses on each component separately.

Result

Composite endpoint = MI; time-to-composite = 100 days (April 10, 2024). The stroke (day 204) and death (day 289) occurred after the composite fired and do not alter the primary outcome — though the component label stored with the record makes them available for secondary analyses.

Timeline Spec

Title

3-point MACE composite for one patient: earliest component event wins

Window
Start

2024-01-01

End

2024-12-31

Label

Observation window: January 1 – December 31, 2024

Events
  • Label

    MI (I21.9 primary, inpatient)

    Start

    2024-04-10

    Length Days

    1

    Quantity

    day 100 — FIRST event → defines composite

    Flag

    composite_trigger

  • Label

    Stroke (I63.9 primary, inpatient)

    Start

    2024-07-22

    Length Days

    1

    Quantity

    day 204 — occurs after composite fired; not counted

  • Label

    Death (linked mortality file)

    Start

    2024-10-15

    Length Days

    1

    Quantity

    day 289 — occurs after composite fired; not counted

Spans
  • Kind

    followup

    Start

    2024-01-01

    End

    2024-04-10

    Label

    Active follow-up: 100 days

  • Kind

    unexposed

    Start

    2024-04-10

    End

    2024-12-31

    Label

    After composite event: follow-up closed (stroke and death not counted)

Result
Label

Composite endpoint = MI on day 100; time-to-composite = 100 days

Value

100

Caption

Margaret's three component events are shown on the timeline. The heart attack (MI) on April 10 is the earliest — it fires the composite endpoint on day 100 and closes her follow-up. The stroke (day 204) and death (day 289) appear on the diagram to show they happened, but they do not change the composite result because follow-up stopped at the first event. The stored component label ('MI') is what allows analysts to break out the composite into its pieces later.

Alt Text

A horizontal timeline from January 1 to December 31 2024 for one patient. The index date anchors the left end. A marker labeled 'MI day 100' on April 10 is flagged as the composite trigger. A marker labeled 'Stroke day 204' on July 22 and a marker labeled 'Death day 289' on October 15 appear to the right but are shown as inactive because follow-up closed at day 100. A shaded span from January 1 to April 10 is labeled 'Active follow-up: 100 days'.

Runnable example

python implementation

Derive a time-to-first-event composite from claims-style component events. Required inputs (already cleaned and mapped to component case definitions upstream): cohort : one row per patient -> person_id, index_date (datetime), fu_end (datetime: min of...

import pandas as pd
import numpy as np

DEDUP_DAYS = 30  # acute-event clean window: claims of the SAME component within this window = one event

def build_composite(cohort: pd.DataFrame, comp_events: pd.DataFrame) -> pd.DataFrame:
    ev = comp_events.merge(cohort[["person_id", "index_date", "fu_end"]], on="person_id")

    # Keep only component events inside each patient's follow-up window.
    ev = ev[(ev["event_date"] > ev["index_date"]) & (ev["event_date"] <= ev["fu_end"])]

    # Deduplicate same-component acute episodes: a claim opens a new episode only if it falls more than
    # DEDUP_DAYS after the LAST KEPT episode date (anchored, not chained off the immediately prior claim),
    # matching the SAS step and the acute-event-deduplication-window concept.
    ev = ev.sort_values(["person_id", "component", "event_date"])

    def _keep_anchored(dates: pd.Series) -> pd.Series:
        keep, last = [], None
        for d in dates:
            if last is None or (d - last).days > DEDUP_DAYS:
                keep.append(True); last = d
            else:
                keep.append(False)
        return pd.Series(keep, index=dates.index)

    new_episode = ev.groupby(["person_id", "component"])["event_date"].transform(_keep_anchored)
    ev = ev[new_episode]

    # Composite event = earliest qualifying component; preserve WHICH component fired.
    ev = ev.sort_values(["person_id", "event_date"])
    first = ev.groupby("person_id").first().reset_index()
    first = first.rename(columns={"event_date": "composite_date", "component": "composite_component"})

    out = cohort.merge(first[["person_id", "composite_date", "composite_component"]], on="person_id", how="left")
    out["event"] = out["composite_date"].notna().astype(int)
    # Time = composite event date if it occurred, else censoring at follow-up end.
    end = out["composite_date"].fillna(out["fu_end"])
    out["time_days"] = (end - out["index_date"]).dt.days
    return out[["person_id", "index_date", "time_days", "event", "composite_component"]]
r implementation

Time-to-first-event composite with data.table. Inputs mirror the Python version: cohort : person_id, index_date (Date), fu_end (Date) comp_events: person_id, event_date (Date), component in {'DEATH','MI','STROKE'} Output: one row per patient with composite...

library(data.table)
DEDUP_DAYS <- 30L

build_composite <- function(cohort, comp_events) {
  setDT(cohort); setDT(comp_events)
  ev <- merge(comp_events, cohort[, .(person_id, index_date, fu_end)], by = "person_id")
  ev <- ev[event_date > index_date & event_date <= fu_end]

  # Deduplicate same-component acute episodes: a claim opens a new episode only if it falls more than
  # DEDUP_DAYS after the LAST KEPT episode date (anchored, not chained off the immediately prior claim),
  # matching the SAS step and the acute-event-deduplication-window concept.
  setorder(ev, person_id, component, event_date)
  keep_anchored <- function(dates) {
    keep <- logical(length(dates)); last <- NA
    for (i in seq_along(dates)) {
      if (is.na(last) || as.integer(dates[i] - last) > DEDUP_DAYS) {
        keep[i] <- TRUE; last <- dates[i]
      }
    }
    keep
  }
  ev <- ev[, .SD[keep_anchored(event_date)], by = .(person_id, component)]

  # Earliest qualifying component = composite event; keep the component label.
  setorder(ev, person_id, event_date)
  first <- ev[, .(composite_date = event_date[1L], composite_component = component[1L]), by = person_id]

  out <- merge(cohort, first, by = "person_id", all.x = TRUE)
  out[, event := as.integer(!is.na(composite_date))]
  out[, end_date := fifelse(is.na(composite_date), fu_end, composite_date)]
  out[, time_days := as.integer(end_date - index_date)]
  out[, .(person_id, index_date, time_days, event, composite_component)]
}