Washout / Clean / Lookback Period
A pre-index window of continuous, observable history used to define incident (new) use or incident disease, to measure baseline covariates, and to align time zero, by requiring the absence of the qualifying exposure/event and the presence of data coverage before the index date.
In plain language
A clean lookback period is a stretch of time before a patient's first fill of a study drug during which the analyst confirms the patient had no prior fills of that drug — proving they are a brand-new starter, not someone already taking it. The analyst also checks that the patient was continuously enrolled in their insurance plan across that entire stretch, because a gap in coverage means a missing fill would be invisible, not a true absence. One honest limit: it only works with data sources that record every dispensing, so cash-paid fills or fills at out-of-network pharmacies can slip through undetected.
A washout (clean) period and a lookback (baseline/assessment) window are two uses of the same pre-index history, and conflating them is a common protocol error. The washout is an exclusionary window: a patient qualifies as a new (incident) user only if there is no fill of the study drug (and, in an active-comparator design, no fill of the comparator) during the window, so that follow-up starts at first exposure (time zero) for everyone. The lookback is a measurement window: the same (or a different) span over which baseline covariates, comorbidities, prior treatment, and incident-disease status are ascertained. Both require the patient to be observable — continuously enrolled with the relevant benefit (medical + pharmacy in claims; in-system contact in EHR) — across the entire window, otherwise "no prior fill" or "no prior diagnosis" is missingness, not a true clean period. A washout is only as long as the data are observable, which is why washout length, lookback length, and the continuous-enrollment requirement are jointly specified, not chosen independently.
Core conceptual distinction
— three decisions are bundled and must be separated. (1) Washout (clean) period — defines incidence by requiring absence: no qualifying drug/diagnosis in the window. Longer washouts more reliably exclude prevalent users (who carry depletion-of-susceptibles and survivor bias) but shrink the cohort and, in finite-data sources, force exclusion of recent enrollees. (2) Lookback (covariate-assessment) window — defines what is measured about baseline. A fixed window (e.g., 365 days) is comparable across patients but can miss a true chronic condition coded only once years ago; an all-available lookback captures more confounders but introduces differential covariate ascertainment because people with longer observable history accrue more codes (Brunelli 2013; Nakasian 2017). (3) Continuous-enrollment / observability — guarantees the washout and lookback reflect real absence, not unobserved care. The estimand is unchanged by these choices, but the population and the measured confounder set are not: a 90-day washout and a 730-day washout answer the same question on materially different cohorts.
Pros, cons, and trade-offs
(named against the alternatives): - Longer vs shorter washout (e.g., 365 vs 180 vs 90 days): A longer washout more completely removes prevalent users and undercounts incident events less (a short window misclassifies long-quiescent chronic conditions as "incident," exaggerating risk — e.g., short atrial-fibrillation lookbacks inflate baseline stroke risk). Cost: it excludes anyone without that much continuous coverage, biasing toward stably enrolled (often older, commercially insured or continuously Medicare) patients and reducing power. Prefer the longest washout the data reliably support, and report cohort yield at each candidate length. - Fixed vs all-available lookback for covariates: A fixed window gives comparable ascertainment across patients; all-available data captures more true confounders and usually reduces residual confounding for stable chronic conditions, but inflates apparent prevalence in longer-enrolled patients and can introduce surveillance/immortal-time artifacts if exposure-related encounters generate the codes (Brunelli 2013; Nakasian 2017; Preen 2006). Prefer a fixed lookback for the primary analysis with an all-available sensitivity analysis, and never let a covariate be measured after time zero. - vs no/implicit washout (prevalent or ever-user cohorts): Specifying a clean period removes immortal time, depletion of susceptibles, and adjustment for post-initiation mediators. Cost: a smaller, initiator-skewed cohort (see `prevalent-user-bias`, `new-user-design`).
When to use
— any incident-user / new-user design; any active-comparator new-user cohort; any incident-disease cohort built from claims/EHR (first qualifying diagnosis after a disease-free window); any time covariates must be measured pre-exposure to avoid conditioning on mediators. Specify the washout, the lookback, the continuous-enrollment requirement, and which entity (drug class, exact molecule, diagnosis hierarchy) the clean period clears — and pre-register the sensitivity grid over window lengths.
When NOT to use — and when it is actively misleading or dangerous
- Washout longer than reliably observable history. If the database has only 12 months of pre-index data for most patients, a 24-month washout silently selects the minority with long enrollment — a selected, healthier-coverage population. Always tabulate how many patients each window length retains. - Treating absence-of-observation as a clean period. In Medicare Advantage (encounter, not FFS, claims) or during a gap in pharmacy benefit, "no prior fill" is missingness. Restricting to MA-only person-time to satisfy a washout is dangerous: the washout is unfalsifiable. Require A/B/D (or commercial medical+pharmacy) coverage across the window. - Short washout on chronic, intermittently treated conditions. A 90-day clean period will reclassify long-standing AF, diabetes, or heart failure as "incident," producing spuriously high early event rates and exaggerated risk (Czwikla 2017). - Asymmetric or post-index covariate windows. Measuring a covariate using any data after time zero, or using a longer lookback in one arm than the other, conditions on a mediator or creates differential misclassification — a self-inflicted bias. - All-available lookback when encounters are exposure-driven. If the act of initiating treatment generates the work-up that records the confounder, all-available ascertainment manufactures imbalance and immortal time.
Data-source operational depth
(claims vs EHR vs registry vs linked): - Claims (FFS): The washout is implemented as "no NDC/J-code for the drug class and no qualifying diagnosis in the [index_date − washout, index_date) window," gated on continuous medical + pharmacy enrollment over the entire window. Failure modes: (a) MA-only person-time lacks complete FFS claims — encounter data are incomplete and under-captured, so a washout computed on MA-only spans is unverifiable; restrict to FFS Parts A/B/D (or commercial medical+Rx). (b) Claims adjudication lag and reversals — a fill reversed after submission can leave a phantom "prior fill" that wrongly disqualifies a true new user; use paid, non-reversed claims. (c) Left truncation — the first observable date is enrollment start, not birth/disease onset; a washout cannot see exposure before coverage. (d) 90-day mail-order and stockpiling distort where the last pre-index `days_supply` ends and whether the washout is truly drug-free. (e) Differential competing risks — in elderly claims, death/disenrollment differs by exposure, so a covariate measured over an all-available lookback is differentially observed by arm. - EHR: Capture is encounter-driven, so a "clean" lookback reflects in-system contact, not the patient's true history; external care leakage (a fill or diagnosis at another system) breaks the washout. Prefer linkage to pharmacy claims to confirm true non-use; define an explicit minimum-contact rule (e.g., ≥1 encounter per lookback year) to operationalize observability, and treat patients who leave the system as informatively censored. - Registry: Strong for adjudicated incident-disease definitions and severity but typically weak for complete pharmacy exposure; link to claims to verify the drug washout and to a death index to firm up censoring. - Linked claims–EHR–vital records: The ideal substrate (EHR severity + claims completeness + mortality) but linkage selects the linkable subset and creates order/fill/service date discrepancies that must be reconciled before the washout and time-zero are assigned.
Worked claims example
Question: incident heart-failure hospitalization among new users of a second-generation sulfonylurea, in a commercial + Medicare-FFS database. (1) Observability gate: require 365 days of continuous medical and pharmacy enrollment with no MA-only person-time immediately before the candidate index. (2) Drug washout: the candidate index is the first sulfonylurea fill (`fill_date`); the patient qualifies as a new user only if there is no sulfonylurea NDC in [index_date − 365, index_date). A fill on day −400 is fine (outside the window); a reversed fill on day −200 must be dropped so it does not falsely disqualify. (3) Incident-disease washout (outcome side): to study first HF, require no HF diagnosis (inpatient or ≥2 outpatient) in the same 365-day clean window, because a 90-day window would label long-standing HF as incident and inflate early rates (Czwikla 2017). (4) Lookback for covariates: measure comorbidities, prior insulin, HbA1c proxies, and utilization over [index_date − 365, index_date] only — never using any claim dated ≥ index_date — to feed a high-dimensional propensity score. (5) Sensitivity: re-run with 180-day and all-available washout/lookback, reporting cohort yield at each (e.g., 365d retains 41,200; 730d retains 22,900 and shifts older), and verify covariate prevalence is not driven by enrollment length. This grid — not a single number — is the defensible output.
Worked example
Scenario
A pharmacist fills metformin for patient 2041 on 2023-10-15. Before counting this person as a brand-new metformin starter, the analyst looks back 180 days — from 2023-04-18 through 2023-10-14 — to confirm there were no earlier metformin fills in that window. The patient's insurance ran continuously from 2023-01-01, so there are no coverage gaps to worry about. After confirming the clean lookback, the analyst marks 2023-10-15 as the index date and then tracks the patient forward for 365 days of follow-up ending 2024-10-14.
Dataset
Pharmacy claims rows for patient 2041. The analyst sees every fill in the database; the task is to confirm the 180-day window before 2023-10-15 is empty of metformin.
| person_id | fill_date | drug | days_supply |
|---|---|---|---|
| 2041 | 2023-10-15 | metformin | 90 |
| 2041 | 2024-01-13 | metformin | 90 |
| 2041 | 2024-04-13 | metformin | 90 |
Steps
Define the lookback window: 180 days before the candidate index fill, so 2023-04-18 through 2023-10-14 (inclusive).
Scan every pharmacy row for person 2041 with drug = metformin and fill_date inside that window — there are none, so the window is clean.
Confirm continuous insurance enrollment from at least 2023-04-18 through 2023-10-15; the patient's plan started 2023-01-01 with no gaps, so the absence of fills is real non-use, not missing data.
Qualify patient 2041 as a new user with index date = 2023-10-15.
Set follow-up to run from 2023-10-15 through 2024-10-14 (365 days).
Result
- Label
Lookback = 180 clean days (2023-04-18 to 2023-10-14); patient qualifies as a new metformin user; index date = 2023-10-15
- Value
180
Timeline Spec
- Title
180-day clean lookback confirming new-user status for patient 2041 (metformin)
- Window
- Start
2023-04-18
- End
2024-10-14
- Label
Full observation span: lookback + index fill + 365-day follow-up
- Events
- Label
Index fill (Fill A)
- Start
2023-10-15
- Length Days
90
- Quantity
90 days_supply
- Label
Fill B (refill)
- Start
2024-01-13
- Length Days
90
- Quantity
90 days_supply
- Label
Fill C (refill)
- Start
2024-04-13
- Length Days
90
- Quantity
90 days_supply
- Spans
- Kind
washout
- Start
2023-04-18
- End
2023-10-14
- Label
180-day clean lookback — no prior metformin fills
- Kind
followup
- Start
2023-10-15
- End
2024-10-14
- Label
365-day follow-up beginning at index fill
- Result
- Label
Lookback = 180 clean days; patient qualifies as new user at index date 2023-10-15
- Value
180
- Caption
Patient 2041 timeline. The 180-day lookback (grey) runs from 2023-04-18 to 2023-10-14 with zero metformin fills, confirming new-user status. The index fill on 2023-10-15 starts 365 days of follow-up (blue). Continuous enrollment covers the full span.
- Alt Text
Timeline for patient 2041 showing a 180-day clean lookback period from April to October 2023 with no prior metformin fills, followed by an index fill on 2023-10-15 and three 90-day fills during a 365-day follow-up period ending October 2024.
Runnable example
python implementation
New-user cohort construction with a drug washout, a continuous-enrollment observability gate, and a fixed covariate lookback. Required inputs (already cleaned, de-duplicated, paid+non-reversed claims only): rx : pharmacy fills -> person_id, fill_date...
import pandas as pd
WASHOUT_DAYS = 365 # drug-free + disease-free clean period that defines incident use
LOOKBACK_DAYS = 365 # fixed covariate-assessment window (== washout here; can differ)
STUDY_CLASS = "SULFONYLUREA"
DISEASE_CODES = ("I50",) # ICD-10 heart failure prefix for the incident-disease washout
def build_new_user_cohort(rx: pd.DataFrame, dx: pd.DataFrame, enroll: pd.DataFrame) -> pd.DataFrame:
rx = rx.sort_values(["person_id", "fill_date"])
study = rx[rx["drug_class"] == STUDY_CLASS]
# Candidate index = first observed fill of the study class.
idx = (study.groupby("person_id", as_index=False)
.agg(index_date=("fill_date", "min")))
idx["baseline_start"] = idx["index_date"] - pd.Timedelta(days=LOOKBACK_DAYS)
idx["washout_start"] = idx["index_date"] - pd.Timedelta(days=WASHOUT_DAYS)
# Observability gate: continuous medical+pharmacy coverage, no MA-only span, across the FULL washout->index window.
e = enroll.merge(idx[["person_id", "index_date", "washout_start"]], on="person_id")
covered = e[(e["med_rx_covered"]) & (~e["ma_only"]) &
(e["enroll_start"] <= e["washout_start"]) &
(e["enroll_end"] >= e["index_date"])]
idx = idx[idx["person_id"].isin(covered["person_id"])].copy()
# Drug washout: drop anyone with a prior study-class fill inside [washout_start, index_date).
prior_rx = study.merge(idx[["person_id", "index_date", "washout_start"]], on="person_id")
prior_rx = prior_rx[(prior_rx["fill_date"] >= prior_rx["washout_start"]) &
(prior_rx["fill_date"] < prior_rx["index_date"])]
idx = idx[~idx["person_id"].isin(prior_rx["person_id"])].copy()
# Incident-disease washout: drop prevalent disease (IP once OR OP >=2) inside the clean window.
d = dx.merge(idx[["person_id", "index_date", "washout_start"]], on="person_id")
d = d[d["dx_code"].str.startswith(DISEASE_CODES) &
(d["dx_date"] >= d["washout_start"]) & (d["dx_date"] < d["index_date"])]
ip = d.loc[d["care_setting"] == "IP", "person_id"]
op2 = d[d["care_setting"] == "OP"].groupby("person_id").size()
prevalent = set(ip) | set(op2[op2 >= 2].index)
idx = idx[~idx["person_id"].isin(prevalent)].copy()
return idx[["person_id", "index_date", "baseline_start"]].reset_index(drop=True)r implementation
New-user cohort construction with data.table mirroring the Python logic. Inputs: rx : person_id, fill_date (Date), ndc, drug_class, days_supply dx : person_id, dx_date (Date), dx_code, care_setting ('IP'/'OP') enroll : person_id, enroll_start, enroll_end,...
library(data.table)
WASHOUT_DAYS <- 365L
LOOKBACK_DAYS <- 365L
STUDY_CLASS <- "SULFONYLUREA"
DISEASE_RX <- "^I50" # heart-failure ICD-10 prefix
build_new_user_cohort <- function(rx, dx, enroll) {
setDT(rx); setDT(dx); setDT(enroll)
study <- rx[drug_class == STUDY_CLASS]
setorder(study, person_id, fill_date)
idx <- study[, .(index_date = fill_date[1L]), by = person_id]
idx[, baseline_start := index_date - LOOKBACK_DAYS]
idx[, washout_start := index_date - WASHOUT_DAYS]
# Observability gate: continuous med+rx coverage, no MA-only, spanning the full washout window through index.
e <- merge(enroll, idx[, .(person_id, index_date, washout_start)], by = "person_id")
covered <- e[med_rx_covered == TRUE & ma_only == FALSE &
enroll_start <= washout_start & enroll_end >= index_date, unique(person_id)]
idx <- idx[person_id %chin% covered]
# Drug washout: exclude any prior study-class fill inside the clean window.
pr <- merge(study, idx[, .(person_id, index_date, washout_start)], by = "person_id")
prior_ids <- unique(pr[fill_date >= washout_start & fill_date < index_date, person_id])
idx <- idx[!person_id %chin% prior_ids]
# Incident-disease washout: exclude prevalent disease (IP once OR OP >= 2) inside the clean window.
d <- merge(dx, idx[, .(person_id, index_date, washout_start)], by = "person_id")
d <- d[grepl(DISEASE_RX, dx_code) & dx_date >= washout_start & dx_date < index_date]
ip <- unique(d[care_setting == "IP", person_id])
op2 <- d[care_setting == "OP", .N, by = person_id][N >= 2L, person_id]
idx <- idx[!person_id %chin% union(ip, op2)]
idx[, .(person_id, index_date, baseline_start)]
}