← Methods repository
concept

Therapeutic-Area-Specific RWE Challenges — Oncology

The design, endpoint, and data-operational adaptations that oncology forces on real-world evidence studies because the outcomes that matter (response, progression, survival) are imaging- and pathology-defined rather than claims-coded, treatment is short and line-structured, and ethics push toward single-arm trials with external controls.

Study_Designoncologyreal-world-progressionline-of-therapyexternal-controlrwpfs-proxyiv-vs-oralehr-fragmentationcompeting-risks
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

In cancer research, the outcomes that matter most — did the tumor shrink, did the disease spread — exist only in radiology reports and pathology slides that routine insurance records never capture. Cancer treatment also runs in ordered rounds called lines of therapy, and injectable drugs are billed through a completely different part of the insurance system than pills, so a pipeline that looks only at pharmacy data silently misses the entire IV armamentarium. Because randomizing advanced-cancer patients to a placebo is often unethical, analysts must build a comparison group from historical records instead of enrolling one alongside treated patients. These three features — outcomes invisible in claims, line-structured multi-route treatment, and external comparison groups — make oncology the hardest therapeutic area in real-world evidence.

Oncology is the therapeutic area where the gap between what is clinically meaningful and what is captured in routine real-world data is widest. The endpoints regulators and clinicians care about — objective response (RECIST), progression, and overall survival — are defined by serial imaging, pathology, and physician assessment, none of which is reliably coded in administrative claims and only partially structured in EHRs. Treatment is short, sequential, and organized into lines of therapy rather than the open-ended chronic dosing of cardiometabolic or rheumatologic disease. And because randomizing advanced-cancer patients to placebo is often unethical, oncology generates an unusually high proportion of single-arm trials that lean on external/historical controls built from real-world data. This entry is about the concrete design and operational consequences of those facts — not a generic data-quality checklist.

Core conceptual distinction

In most therapeutic areas the outcome is the hard part to define and the exposure is easy (a drug fill, an event code); in oncology the relationship inverts and compounds. (1) Outcome is latent. Progression is the central efficacy endpoint, yet routine claims contain no progression flag, so analysts substitute a real-world progression proxy — typically time to treatment discontinuation, switch to a new line, or death — which is a behavioral surrogate for a biological event and is differentially biased by drug class and disease pace. (2) Exposure is line-structured. "On treatment" is not a single drug but a regimen within a line, with route-driven capture (oral TKIs via pharmacy NDC + `days_supply`; IV/infused agents via medical claims with HCPCS J-codes and CPT 96413/96365 administration codes), so episode construction must stitch multi-claim regimens and detect line advancement, not just gaps in a single NDC. (3) Comparison is often external. Single-arm accelerated approvals require an external control arm, shifting the methodological burden from confounding control inside one cohort to transportability and outcome-ascertainment alignment across two data sources collected under different rules. These three shifts — latent outcome, line-structured exposure, external comparison — are what distinguish oncology RWE from a chronic-disease active-comparator new-user study.

Pros, cons, and trade-offs

- rwPFS proxy (treatment discontinuation / next-line / death) vs trial-grade RECIST progression: The proxy is the only progression-like endpoint available at scale in claims and unabstracted EHR, and it correlates reasonably with OS in some tumors. Cost: it conflates toxicity holds, insurance churn, and patient preference with true progression, and the error is directional and differential — it overestimates true PFS for indolent disease (patients stay on a tolerable drug past radiographic progression) and underestimates it for toxicity-driven discontinuation, so two arms with different toxicity profiles are not comparably measured. Prefer abstracted/curated rwPFS (chart- or imaging-derived, e.g., Flatiron-style abstraction) when the estimand is efficacy; reserve the discontinuation proxy for utilization, treatment-pattern, and HCRU questions where it is the intended construct. - External/historical control vs concurrent active comparator: When a single-arm trial is the only ethical option, an external control is the only path to a comparative effect. Cost: it reintroduces every bias an active-comparator new-user design was built to remove — calendar-time drift in standard of care, differential outcome ascertainment between the trial and the RWD source, and selection of the linkable/abstractable subset. Prefer a concurrent active comparator (see active-comparator-new-user) whenever two real-world regimens are genuinely used for the same indication; fall back to external controls only for true single-arm settings, and then borrow rigor from rare-disease-external-controls-rwe and generalizability-transportability-external-validity-rwe. - Claims-only vs EHR-only vs linked oncology data: Claims give complete drug/procedure/cost capture but no stage, histology, biomarker, or response; EHR gives pathology, imaging, and notes but is visit-driven and fragmented across community and academic sites; linkage gives both at the cost of selection into the linkable subset. Prefer linked or curated EHR for any efficacy/progression estimand; claims alone are defensible only for utilization, adherence, and cost endpoints.

When to use

This lens applies whenever the study population is defined by a cancer diagnosis and the question touches efficacy (response, rwPFS, OS), treatment sequencing (lines of therapy, switching), oncology HCRU/cost (infusion visits, supportive care, end-of-life utilization), or a single-arm trial that needs an external comparator. It is the prerequisite framing for biomarker-defined cohorts, real-world progression endpoints, and oncology external-control studies.

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

- Do not use a discontinuation-based rwPFS proxy as an efficacy endpoint in a comparative study of differently-tolerated regimens. The differential measurement error (indolent overestimation vs toxicity-hold underestimation) manufactures or masks a treatment effect that is an artifact of measurement, not biology — this is the single most dangerous oncology RWE error and will not survive review. - Do not set time zero at the cancer diagnosis date when the exposure is first systemic therapy. The interval between diagnosis and treatment initiation is immortal (the patient must survive and be worked up to be treated), so anchoring follow-up at diagnosis manufactures an apparent survival advantage for whoever gets treated — align time zero to first qualifying systemic claim (see immortal-time-bias-handling, time-zero-index-date-alignment-rwe). - Do not compare arms on a progression proxy without confronting competing risk of death. In elderly or advanced-cancer claims, death is frequent and differential by arm; treating death as independent censoring for a progression endpoint biases the cumulative incidence — model cause-specific or subdistribution hazards (see competing-risks-cause-specific-fine-gray-rwe). - Do not pool community and academic EHR capture without modeling informative presence. Sicker patients visit more, generating more documented progression and richer covariates, so apparent outcome rates track visit intensity, not biology (informative presence / informative observation bias). - Do not build an external control from a different calendar era. Standard of care in oncology turns over in 2–3 years; a historical control predating the current backbone regimen confounds the comparison with secular change.

Data-source operational depth

- Claims (FFS vs MA vs commercial): Oral agents appear as pharmacy NDC + `fill_date` + `days_supply`; IV/infused agents appear only in medical claims as HCPCS J-codes (drug) plus CPT 96413/96365/96367 (administration), so a claims pipeline that looks only at pharmacy will silently drop the entire IV oncology armamentarium. Medicare Advantage and capitated arrangements do not adjudicate FFS line-item J-codes/CPTs the way Parts A/B do, so MA-only person-time has incomplete infusion and administration capture — restrict to FFS Parts A/B/D or exclude MA-only spans before constructing IV regimens. For orals, 340B dispensing, free samples, and 90-day mail order distort `days_supply` and corrupt gap-based discontinuation. Claims carry no stage, histology, biomarker, ECOG, or response. - EHR (community vs academic): Adds the oncology-specific data claims lack — pathology reports (histology, grade), radiology (response/progression), molecular reports (EGFR, ALK, PD-L1, MSI), and ECOG — but most of it is unstructured and requires NLP or manual abstraction. Capture is visit-driven and fragmented: a patient who progresses and transfers to hospice or another health system is differentially lost, and informative-presence bias means documented-event rates confound severity with engagement. Site-of-care matters: community and academic centers differ systematically in documentation, trial enrollment, and patient mix. - Registry (e.g., SEER, NCDB, disease-specific): Gold standard for stage, histology, and incidence/survival, but thin on line-by-line treatment and longitudinal utilization. Link to claims (SEER-Medicare) for treatment and cost, accepting that the linkable population is older and FFS-skewed. - Linked claims–EHR–vital records: The substrate for credible efficacy RWE — EHR/registry severity + claims completeness + a reliable death index for OS and for competing-risk censoring of progression — but linkage selects the linkable subset and introduces order/fill/service/abstraction date discrepancies that must be reconciled before time-zero assignment.

Worked claims example — rwPFS proxy and line-of-therapy episode for metastatic NSCLC

Question: real-world time-to-discontinuation on first-line therapy in a commercial + Medicare FFS database. (1) Cohort: ≥2 claims with an ICD-10 lung cancer code (C34.x) plus ≥1 secondary/metastatic code (C78.x/C79.x), age ≥18, and 365 days of continuous medical + pharmacy enrollment with FFS Parts A/B (exclude MA-only spans so IV J-code/CPT administration is observable). (2) Index (time zero): the first systemic anticancer claim after the metastatic diagnosis — a pharmacy NDC for an oral TKI or a medical J-code with a paired CPT 96413/96365 administration on the same date for an IV agent — not the diagnosis date, which would inject immortal time. Assign the first-line regimen from the drug(s) seen in a 28-day window around index. (3) Discontinuation = a gap of >90 days with no fill/administration of any first-line agent (90 days, not 60, because 21-day infusion cycles plus scheduling slack routinely exceed 60). (4) Switch/next line = appearance of a new systemic agent not in the first-line regimen, which ends the first-line episode even without a gap. (5) rwPFS proxy = days from index to the earliest of discontinuation, next-line start, or death (death from a linked vital-records index, since claims-inferred death is incomplete). (6) Censor at disenrollment and end of data; treat death as a competing risk for the discontinuation event, not as independent censoring. (7) Sensitivity: vary the gap (60/90/120 days), add a toxicity-hold rule (allow re-initiation within the gap to distinguish a hold from a true stop), and compare the proxy against an abstracted-progression subset where available — the divergence quantifies the differential measurement error that makes this proxy unsafe as a comparative efficacy endpoint.

Worked example

Scenario

A researcher wants to measure how long patients with metastatic lung cancer stay on their first-line treatment using a commercial claims database. The table below shows four patients and the challenges that arise for each — the same five challenges that make oncology RWE uniquely hard. For each challenge, the table describes what the data actually looks like and what the analyst must do about it.

Dataset

Five oncology-specific RWE challenges, with a concrete data situation and the method used to address each

challengewhat the data looks likewhy it causes a problemhow analysts handle it
Measuring progression (rwPFS)Patient 1001 has pharmacy fills for an oral pill and medical claims for clinic visits, but no imaging or pathology report showing the tumor grewProgression is a biological event visible only on a scan; claims carry no progression flag, so the analyst cannot directly observe when the cancer worsenedBuild a behavioral proxy: progression = the day the patient stopped the drug for more than 90 days, switched to a new drug, or died — whichever came first (Stewart et al. 2019)
Lines of therapy — what counts as first-line?Patient 1002 has claims for Drug A starting 2023-01-10, Drug B added 2023-01-24 (14 days later), and Drug C starting 2023-09-15 after a 120-day gapA cancer regimen is often multiple drugs started within days of each other; a later drug appearing after a long gap signals a new line, not a combinationDefine a regimen window (e.g., 28 days after the first drug) to bundle co-started drugs into one line; a new drug appearing after a 90-day gap counts as line 2
IV drugs invisible in pharmacy dataPatient 1003 receives pembrolizumab by infusion every 3 weeks; the pharmacy table has zero rows for this patient, but the medical table has repeated J9271 claims paired with CPT 96413IV oncology drugs are billed as medical claims using HCPCS J-codes — a pipeline that searches only pharmacy NDC records will find no treatment for this patientCombine pharmacy NDC fills (oral drugs) with medical J-code plus CPT 96413/96365 administration pairs (IV drugs); exclude Medicare Advantage spans where these codes are often missing
Immortal time around diagnosisPatient 1004 is diagnosed with metastatic disease on 2023-03-01 and starts chemotherapy on 2023-04-15 after 6 weeks of staging scans and biopsyIf follow-up starts at the diagnosis date, those 45 days before treatment look like survival time but the patient could not possibly have experienced the drug's effect yet — this artificially inflates survival estimatesSet time zero at the date of the first systemic treatment claim, not the diagnosis date, so the immortal pre-treatment interval is excluded from follow-up
Death as a competing risk for progressionPatient 1005 dies on day 120 without any gap or switch in claims; the proxy event never triggersDeath prevents progression from ever being recorded; if death is treated as a simple dropout (censoring), it understates the true rate of the combined endpoint in sicker patientsClassify death as its own event type in the analysis rather than censoring it; use cause-specific or Fine-Gray subdistribution models so death and progression are estimated together

Steps

  • Start by building the cancer cohort: require at least two ICD-10 C34.x (lung) claims plus at least one metastatic or secondary code (C78.x or C79.x), and restrict to patients with fee-for-service Parts A, B, and D enrollment so IV drug administrations are visible in medical claims.

  • Assign time zero at the first systemic treatment claim on or after the metastatic diagnosis date — either a pharmacy NDC row (oral drug) or a medical J-code paired with CPT 96413/96365 on the same date (IV drug). Never use the diagnosis date as time zero, because the staging and biopsy period before treatment is an interval where the patient is alive by necessity, not because of the drug.

  • Define the first-line regimen as all drugs seen within 28 days of time zero; this bundles combination partners (e.g., a checkpoint inhibitor added 10 days after a chemotherapy backbone) into a single regimen rather than miscounting them as a new line.

  • Build the rwPFS proxy endpoint: follow the patient forward and record the earliest of (a) a gap greater than 90 days with no fill or administration of any first-line drug, (b) the first appearance of a new drug not in the first-line regimen, or (c) death from a linked vital-records source.

  • Treat death as a competing risk, not a dropout: a patient who dies has not stopped treatment due to progression, and censoring them as though they simply left the study would undercount the true endpoint rate in the sicker arm. Apply cause-specific or Fine-Gray methods (see competing-risks-cause-specific-fine-gray-rwe) to separate the two events.

Result

The five challenges are addressed by: (1) using the earliest of gap/switch/death as the rwPFS proxy because imaging data is absent; (2) using a 28-day regimen window to bundle co-started drugs and a 90-day gap threshold to detect line advancement; (3) combining pharmacy NDC rows with medical J-code plus CPT pairs to capture both oral and IV drugs; (4) anchoring time zero at first systemic treatment to exclude the immortal staging interval; and (5) classifying death as a competing event rather than censoring it. Each fix addresses a different reason why oncology claims data alone cannot simply be read at face value.

Runnable example

python implementation

Oncology line-of-therapy and rwPFS-proxy episode construction from claims-style inputs. Required inputs (already cleaned, de-duplicated, and restricted to FFS-observable person-time so IV administration is captured): dx : diagnosis claims -> person_id,...

import pandas as pd
import numpy as np

GAP_DAYS = 90          # > one infusion cycle + scheduling slack; 60d falsely flags routine IV gaps
REGIMEN_WINDOW = 28    # agents seen within this window of time zero define the first-line regimen

def build_first_line_episode(dx: pd.DataFrame, sys: pd.DataFrame, death: pd.DataFrame) -> pd.DataFrame:
    # Metastatic anchor: first secondary/metastatic code (C77-C79) per person.
    met = (dx[dx["icd10"].str.startswith(("C77", "C78", "C79"))]
             .groupby("person_id")["dx_date"].min()
             .reset_index(name="met_date"))

    # Time zero = first systemic claim on/after the metastatic date (NOT the diagnosis date -> avoids immortal time).
    sys = sys.merge(met, on="person_id", how="inner")
    post = sys[sys["claim_date"] >= sys["met_date"]].sort_values(["person_id", "claim_date"])
    t0 = post.groupby("person_id")["claim_date"].min().reset_index(name="index_date")
    ep = post.merge(t0, on="person_id")

    # First-line regimen = agents within REGIMEN_WINDOW days of time zero.
    in_window = ep[ep["claim_date"] <= ep["index_date"] + pd.Timedelta(days=REGIMEN_WINDOW)]
    regimen = in_window.groupby("person_id")["agent"].apply(lambda s: frozenset(s)).rename("fl_regimen")
    ep = ep.merge(regimen, on="person_id")

    rows = []
    for pid, g in ep.groupby("person_id"):
        g = g.sort_values("claim_date")
        t0_date = g["index_date"].iloc[0]
        fl = g["fl_regimen"].iloc[0]
        fl_claims = g[g["agent"].isin(fl)].copy()

        # Per-claim coverage end: orals use days_supply; IV agents covered through the cycle (use GAP as cycle proxy).
        fl_claims["cov_end"] = np.where(
            fl_claims["route"].eq("oral"),
            fl_claims["claim_date"] + pd.to_timedelta(fl_claims["days_supply"].fillna(0), unit="D"),
            fl_claims["claim_date"] + pd.Timedelta(days=GAP_DAYS),
        )
        last_cov = fl_claims["cov_end"].max()
        discontinuation = last_cov  # end of observed first-line coverage

        # Switch / next line = first systemic agent NOT in the first-line regimen after time zero.
        nextline = g[~g["agent"].isin(fl)]
        switch_date = nextline["claim_date"].min() if not nextline.empty else pd.NaT

        d_row = death.loc[death["person_id"] == pid, "death_date"]
        death_date = d_row.iloc[0] if (len(d_row) and pd.notna(d_row.iloc[0])) else pd.NaT

        # rwPFS proxy = earliest of discontinuation, switch, or death. Death is a COMPETING event, not censoring.
        candidates = {"discontinuation": discontinuation, "switch": switch_date, "death": death_date}
        candidates = {k: v for k, v in candidates.items() if pd.notna(v)}
        event_type = min(candidates, key=candidates.get)
        event_date = candidates[event_type]

        rows.append({
            "person_id": pid,
            "index_date": t0_date,
            "fl_regimen": tuple(sorted(fl)),
            "event_type": event_type,                 # 'discontinuation' | 'switch' | 'death'
            "event_date": event_date,
            "rwpfs_proxy_days": (event_date - t0_date).days,
        })
    return pd.DataFrame(rows)
r implementation

Oncology first-line / rwPFS-proxy episode construction with data.table. Inputs mirror the Python version: dx : person_id, dx_date (Date), icd10 sys : person_id, claim_date (Date), agent, route in {'oral','iv'}, days_supply (NA for IV) death : person_id,...

library(data.table)
GAP_DAYS       <- 90L   # > infusion cycle + slack; 60d falsely flags routine IV gaps
REGIMEN_WINDOW <- 28L   # agents within this window of time zero define the first-line regimen

build_first_line_episode <- function(dx, sys, death) {
  setDT(dx); setDT(sys); setDT(death)

  met <- dx[grepl("^C7[789]", icd10), .(met_date = min(dx_date)), by = person_id]

  sys <- merge(sys, met, by = "person_id")
  post <- sys[claim_date >= met_date][order(person_id, claim_date)]
  t0 <- post[, .(index_date = min(claim_date)), by = person_id]
  ep <- merge(post, t0, by = "person_id")

  regimen <- ep[claim_date <= index_date + REGIMEN_WINDOW,
                .(fl_regimen = list(unique(agent))), by = person_id]
  ep <- merge(ep, regimen, by = "person_id")

  ep[, .build := {
    fl <- fl_regimen[[1L]]
    flc <- .SD[agent %chin% fl]
    cov_end <- fifelse(flc$route == "oral",
                       flc$claim_date + fifelse(is.na(flc$days_supply), 0L, flc$days_supply),
                       flc$claim_date + GAP_DAYS)
    discontinuation <- max(cov_end)
    nl <- .SD[!agent %chin% fl, claim_date]
    switch_date <- if (length(nl)) min(nl) else as.Date(NA)
    dd <- death[person_id == .BY$person_id, death_date]
    death_date <- if (length(dd) && !is.na(dd[1L])) dd[1L] else as.Date(NA)

    cand <- c(discontinuation = discontinuation, switch = switch_date, death = death_date)
    cand <- cand[!is.na(cand)]
    et <- names(cand)[which.min(cand)]
    ed <- as.Date(min(cand), origin = "1970-01-01")
    .(index_date = index_date[1L], event_type = et, event_date = ed,
      rwpfs_proxy_days = as.integer(ed - index_date[1L]))
  }, by = person_id]

  unique(ep[, .(person_id, index_date, event_type = .build.event_type,
                event_date = .build.event_date, rwpfs_proxy_days = .build.rwpfs_proxy_days)])
}