← Methods repository
concept

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.

Outcome_Measureecogperformance-statusoncologyfunctional-statusexternal-controlsprognostic-factorehrregistry
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

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.

namedefinitionsourceusenotes
ECOG 0Fully active; able to carry on all pre-disease performance without restriction.ECOG-ACRINTrial eligibility, baseline prognosis, and Table 1 covariate.Best functional status category.
ECOG 1Restricted in physically strenuous activity but ambulatory and able to do light or sedentary work.ECOG-ACRINCommon eligibility category in oncology trials and external-control matching.Often grouped with ECOG 0 as ECOG 0-1.
ECOG 2Ambulatory and capable of all self-care but unable to work; up and about more than 50 percent of waking hours.ECOG-ACRINPrognostic adjustment and subgrouping.Trial eligibility often changes at ECOG 2.
ECOG 3Capable of limited self-care; confined to bed or chair more than 50 percent of waking hours.ECOG-ACRINPrognostic stratification and clinical-context adjustment.Often underrepresented in trials.
ECOG 4Completely disabled; cannot carry on self-care; totally confined to bed or chair.ECOG-ACRINSevere functional impairment marker.Rarely eligible for interventional trials.
ECOG 5Dead.ECOG-ACRINTerminal 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_indexecog_valuesourceeligible_for_baseline
-75oncology noteno; outside window
1structured oncology fieldTrue
1782oncology noteno; 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)
}