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.
On this page
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 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.
- 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.
- 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.
- 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.
- 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.
- Follow-up & censoring. From `index_date` to the composite event date, censoring at disenrollment, end of data, and (for as-treated) discontinuation.
- 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.
Decision diagram
flowchart TD
Comp[Per-component case definitions<br/>DEATH / MI / STROKE<br/>each with its own PPV + code-position rule] --> Dedup[Deduplicate same-component<br/>acute episodes within clean window]
Dedup --> Min[Composite event = EARLIEST component date<br/>store date AND winning component label]
Min --> Estimand{Estimand?}
Estimand -->|components similar severity| TTE[Time-to-first-event<br/>cause-specific Cox / pooled logistic]
Estimand -->|severity differs| Win[Hierarchical / win-ratio<br/>death before hospitalization]
Estimand -->|non-fatal piece, death competes| FG[Fine-Gray subdistribution<br/>+ cause-specific hazard]
Estimand -->|repeated events matter| Rec[Recurrent events<br/>Andersen-Gill / LWYY]
TTE --> Report[Always report component breakdown<br/>+ sensitivity on code-position and dedup window]
Win --> Report
FG --> Report
Rec --> Reportgantt title One patient's component events and the composite event date (claims) dateFormat YYYY-MM-DD axisFormat %b %Y section Follow-up Index date -> follow-up start :milestone, idx, 2024-01-01, 0d section Components Non-fatal MI (I21 primary, inpatient) :crit, mi, 2024-04-10, 1d Stroke claim (within MI clean window -> deduped) :done, st, 2024-04-25, 1d Death (linked mortality) :active, dth, 2024-09-15, 1d section Composite Composite event = earliest = MI; component label retained :milestone, comp, 2024-04-10, 0d
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_id | event_date | component | source |
|---|---|---|---|
| 7741 | 2024-04-10 | MI | inpatient claim, I21.9 primary |
| 7741 | 2024-07-22 | STROKE | inpatient claim, I63.9 primary |
| 7741 | 2024-10-15 | DEATH | linked mortality file |
Steps
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.
Trade-offs
Runnable example
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 disenroll/death/data end) comp_events: one row per qualifying...
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"]]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 time, event indicator, and the winning component label.
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)]
}Composite construction (PROC SQL) plus the analyses the estimand requires. Required inputs (post data-management): work.cohort : person_id, index_date, fu_end work.comp_events: person_id, event_date, component ('DEATH'/'MI'/'STROKE') # each already validated per component Step 1 deduplicates same-component acute...
%let dedup = 30;
/* Step 1: keep in-window events; deduplicate same-component episodes to the earliest claim within &dedup days. */
proc sql;
create table ev as
select e.person_id, e.event_date, e.component, c.index_date, c.fu_end
from work.comp_events e
inner join work.cohort c on e.person_id = c.person_id
where e.event_date > c.index_date and e.event_date <= c.fu_end;
quit;
proc sort data=ev; by person_id component event_date; run;
data ev_dedup;
set ev; by person_id component event_date;
retain last_date;
/* Anchored rule: gap measured from the LAST KEPT episode (last_date updates only on output), matching
the Python/R helpers and the acute-event-deduplication-window concept. */
if first.component then last_date = .;
if last_date = . or (event_date - last_date) > &dedup then do;
last_date = event_date; output;
end;
run;
/* Step 2: composite event = earliest qualifying component; keep WHICH component fired. */
proc sort data=ev_dedup; by person_id event_date; run;
data composite;
merge work.cohort(in=c)
ev_dedup(keep=person_id event_date component rename=(event_date=composite_date component=composite_component));
by person_id;
if c;
if first.person_id; /* earliest component only */
event = (composite_date ne .);
if event then end_date = composite_date; else end_date = fu_end;
time_days = end_date - index_date;
run;
/* Step 3: time-to-first-event analysis (cumulative incidence + cause-specific hazard ratio). */
proc lifetest data=composite plots=(cif);
time time_days*event(0);
strata arm; /* arm joined from cohort upstream */
run;
proc phreg data=composite;
class arm(ref='COMPARATOR');
model time_days*event(0) = arm; /* cause-specific composite hazard ratio */
run;
/* Step 4: single non-fatal component (MI) as endpoint, death competing.
status: 1=MI, 2=death (competing), 0=censored. Fine-Gray subdistribution via eventcode=. */
data fg;
set composite;
if event=1 and composite_component='MI' then status=1;
else if event=1 and composite_component='DEATH' then status=2;
else status=0;
run;
proc phreg data=fg;
class arm(ref='COMPARATOR');
model time_days*status(0) = arm / eventcode=1; /* Fine-Gray: subdistribution HR for MI */
run;Citations
- [1]Freemantle N, Calvert M, Wood J, Eastaugh J, Griffin C. Composite outcomes in randomized trials: greater precision but with greater uncertainty? JAMA. 2003;289(19):2554-2559.
- [2]Cordoba G, Schwartz L, Woloshin S, Bae H, Gøtzsche PC. Definition, reporting, and interpretation of composite outcomes in clinical trials: systematic review. BMJ. 2010;340:c3920.
- [3]Ferreira-González I, Permanyer-Miralda G, Domingo-Salvany A, et al. Problems with use of composite end points in cardiovascular trials: systematic review of randomised controlled trials. BMJ. 2007;334(7597):786.