ECOG Performance Status Score
A 0-5 clinician-rated oncology functional-status scale, from fully active to dead, used for trial eligibility, prognosis, treatment selection, confounding control, and external-control comparability.
In plain language
ECOG is a 0 to 5 oncology score for how well a patient is functioning. Lower is better. It is crucial in cancer studies because two patients with the same diagnosis can have very different prognosis if one is ECOG 0 and the other is ECOG 3.
ECOG Performance Status is a concise oncology functional-status score. It describes a patient's ability to work, ambulate, perform self-care, and remain out of bed. ECOG 0 means fully active; ECOG 5 means dead. In oncology RWE, ECOG is often a decisive prognostic variable and eligibility criterion, especially for external controls, treatment sequencing studies, and comparative effectiveness analyses.
The measurement problem is that ECOG is unevenly captured in routine care. It may appear as a structured field, a trial/registry variable, a clinician note phrase, or not at all. Claims data generally do not contain ECOG. EHR extraction can use structured oncology flowsheets or NLP, but missingness is often informative because sicker patients and oncology centers may document performance status differently.
Treat ECOG as a score with provenance. Store the numeric value, source field or note text, assessment date, assessor/source, and relationship to index date. For baseline confounding control, define an allowed window before or near treatment start and report missingness by study arm. Do not impute ECOG into claims-only data as if it were observed.
Pros, cons, and trade-offs
ECOG is one of the most clinically meaningful oncology covariates because it summarizes functional status, prognosis, treatment tolerance, and trial eligibility in one familiar score. The trade-off is measurement quality. In routine EHR data it can be missing, stale, copied forward, buried in notes, or recorded after treatment decisions. NLP can recover values, but it creates validation and provenance requirements. Collapsing ECOG into 0-1 versus 2+ improves stability but loses clinically important gradients.
When to use
Use ECOG when oncology treatment selection, external-control comparability, eligibility transportability, baseline prognosis, or utility mapping depends on functional status and the score is observed or validly abstracted. Use a pre-specified baseline window and keep post-index ECOG separate when it may be a mediator or outcome.
When NOT to use - and when it is actively misleading
Do not treat ECOG as observable in claims-only data. Do not fill missing ECOG with a favorable value or infer it from treatment receipt without labeling the result as a proxy. It is actively misleading to use post-progression ECOG for baseline confounding control or to compare EHR sites without reporting documentation missingness.
Index definitions
Source-backed definitions and variants for the index or checklist family.
| name | definition | source | use | notes |
|---|---|---|---|---|
| ECOG 0 | Fully active; able to carry on all pre-disease performance without restriction. | ECOG-ACRIN | Trial eligibility, baseline prognosis, and Table 1 covariate. | Best functional status category. |
| ECOG 1 | Restricted in physically strenuous activity but ambulatory and able to do light or sedentary work. | ECOG-ACRIN | Common eligibility category in oncology trials and external-control matching. | Often grouped with ECOG 0 as ECOG 0-1. |
| ECOG 2 | Ambulatory and capable of all self-care but unable to work; up and about more than 50 percent of waking hours. | ECOG-ACRIN | Prognostic adjustment and subgrouping. | Trial eligibility often changes at ECOG 2. |
| ECOG 3 | Capable of limited self-care; confined to bed or chair more than 50 percent of waking hours. | ECOG-ACRIN | Prognostic stratification and clinical-context adjustment. | Often underrepresented in trials. |
| ECOG 4 | Completely disabled; cannot carry on self-care; totally confined to bed or chair. | ECOG-ACRIN | Severe functional impairment marker. | Rarely eligible for interventional trials. |
| ECOG 5 | Dead. | ECOG-ACRIN | Terminal category in the original scale. | In RWE, death is usually modeled as an outcome rather than carried as a baseline ECOG state. |
Worked example
Scenario
An external-control oncology study needs baseline ECOG at treatment start. A patient has ECOG 0 documented 75 days before index, ECOG 1 on the index date, and ECOG 2 after progression. The protocol uses the nearest value from 30 days before through 7 days after index.
Dataset
Candidate ECOG values around index.
| date_relative_to_index | ecog_value | source | eligible_for_baseline |
|---|---|---|---|
| -75 | oncology note | no; outside window | |
| 1 | structured oncology field | True | |
| 178 | 2 | oncology note | no; post-index deterioration |
Steps
Define the allowed baseline window before extracting values.
Prefer structured ECOG on or closest before index if multiple values qualify.
Exclude post-progression ECOG from baseline adjustment because it may be a mediator or outcome.
Report missingness and extraction source in Table 1.
Result
Baseline ECOG = 1. The ECOG 2 value is retained as follow-up deterioration evidence, not baseline confounding control.
Runnable example
python implementation
Select baseline ECOG from structured or abstracted EHR/registry rows. Inputs: index : person_id, index_date ecog : person_id, assessment_date, ecog_value, source The example uses the nearest value in a pre-specified baseline window and rejects impossible...
import pandas as pd
def baseline_ecog(index, ecog, lookback_days=30, lookforward_days=7):
e = ecog.copy()
e["ecog_value"] = pd.to_numeric(e["ecog_value"], errors="coerce")
e = e[e["ecog_value"].between(0, 5, inclusive="both")]
e = e.merge(index[["person_id", "index_date"]], on="person_id", how="inner")
e["days_from_index"] = (e["assessment_date"] - e["index_date"]).dt.days
e = e[e["days_from_index"].between(-lookback_days, lookforward_days)]
e["abs_days"] = e["days_from_index"].abs()
e = e.sort_values(["person_id", "abs_days", "days_from_index"])
first = e.groupby("person_id", as_index=False).first()
return index.merge(
first[["person_id", "ecog_value", "assessment_date", "source", "days_from_index"]],
on="person_id",
how="left",
)r implementation
R/data.table version for selecting baseline ECOG in a reproducible window.
library(data.table)
baseline_ecog <- function(index, ecog, lookback_days = 30L, lookforward_days = 7L) {
setDT(index); setDT(ecog)
e <- copy(ecog)
e[, ecog_value := suppressWarnings(as.numeric(ecog_value))]
e <- e[ecog_value >= 0 & ecog_value <= 5]
e <- merge(e, index[, .(person_id, index_date)], by = "person_id")
e[, days_from_index := as.integer(assessment_date - index_date)]
e <- e[days_from_index >= -lookback_days & days_from_index <= lookforward_days]
e[, abs_days := abs(days_from_index)]
setorder(e, person_id, abs_days, days_from_index)
first <- e[, .SD[1], by = person_id]
merge(index, first[, .(person_id, ecog_value, assessment_date, source, days_from_index)],
by = "person_id", all.x = TRUE)
}