← Methods repository
concept

Exposure Episode Construction

The algorithm that stitches a person's sequence of pharmacy dispensings (fill_date + days_supply) into continuous treatment episodes by applying a grace period for permissible gaps, a stockpiling/carry-over rule for early refills, and end-of-episode logic, yielding the on-treatment time windows used for exposure classification and follow-up.

Exposure_Definitionexposure-episode-constructiontreatment-episodegrace-periodstockpilingdays-supplypersistenceas-treatedpharmacoepidemiology
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

Exposure episode construction is how a researcher turns a list of prescription fills into continuous stretches of time when a patient actually had medication on hand. Because people refill early, refill late, or take breaks from their drug, the raw fill dates alone do not tell you when someone was truly on therapy — you have to stitch them together using rules: a short gap between one fill running out and the next fill arriving is treated as continuous use, while a long gap closes that stretch and the next fill starts a fresh one. The result is a set of date ranges — episodes — that define when each patient was exposed, which every downstream analysis (persistence, adherence, as-treated follow-up) depends on. The biggest caveat is that the rules you choose — especially how large a gap you allow before splitting an episode — materially change how many episodes you count and how long they last, so those choices must be set in advance and tested in sensitivity analyses.

Exposure episode construction

is the operational bridge between raw dispensing records and an analyzable exposure variable. A claims database does not contain "treatment episodes"; it contains discrete fills, each carrying a `fill_date` and a `days_supply`. The analyst must decide when a person is on therapy and when they are off it, because real refill behavior is irregular: people refill early (stockpiling), refill late, take drug holidays, are hospitalized, switch within class, and stop. Episode construction is the rule set — grace period, carry-over/stockpiling rule, inpatient bridging, and restart logic — that collapses a fill history into one or more `[episode_start, episode_end]` intervals. Those intervals then drive new-user/washout definitions, on-treatment ("as-treated") risk windows, persistence and adherence metrics (PDC/MPR), and time-varying exposure in causal models. Get the rules wrong and every downstream estimate inherits exposure misclassification that is frequently differential by outcome.

Core conceptual distinction

Three rules, each independently consequential, define an episode. (1) Grace period (the permissible gap): the maximum number of days between the projected supply-end of one fill and the start of the next fill that still counts as continuous therapy. A short grace (0–15d) splits a single course into many short episodes and manufactures spurious discontinuations; a long grace (≥90d) glues distinct courses together and carries exposure status far past the last pill — exactly the over-counting that produces immortal-time–like bias in an as-treated analysis. Gardarsdottir et al. showed episode counts and durations swing materially with this single parameter. (2) Stockpiling / carry-over: when a refill arrives before the prior supply is exhausted (overlap), do you cap exposure at the supply you could plausibly have taken (cap-at-1, no carry-over) or roll the surplus forward (carry-over), shifting the projected supply-end later? Carry-over is realistic for hoarders but inflates persistence if oversupply reflects gaming or 90-day-mail switches. (3) End-of-episode / restart: a gap exceeding the grace closes the episode at the last projected supply-end; the next fill opens a new episode (and may be a "restart"/"rechallenge"). The estimand you are serving dictates the rules: an intention-to-treat / first-episode contrast cares mainly about episode start; an as-treated / per-protocol contrast lives and dies by the grace period and carry-over rule because they define the on-treatment window over which person-time and events are counted. Episode construction is therefore a measurement decision that encodes an estimand — it must be pre-specified, not tuned to the result.

Pros, cons, and trade-offs

(specific and comparative, naming the alternatives). - Episode construction (refill-gap stitching) vs. fixed-window "ever-exposed after index": Fixed-window approaches (e.g., "exposed for 365 days after first fill regardless of refills") are trivial to code and avoid gap parameters, but they assign exposure to people who stopped after one fill — pure immortal-time/exposure misclassification. Episode construction tracks actual supply and discontinuation. Prefer episode construction whenever the outcome can occur during gaps or after stopping (most safety and many effectiveness questions). - Episode construction vs. "current-use" single-fill exposure (e.g., exposed only for days_supply of the last fill, no grace): Single-fill current-use is the strictest and least biased toward over-counting, but it fragments therapy and discards person-time around normal refill lateness, throwing away power and creating artificial discontinuation/restart events that can themselves be modeled into bias. Prefer a modest grace period (commonly 30–60d, or half the typical days_supply) over zero-grace current-use for chronic therapies. - Carry-over (stockpiling) vs. cap-at-1 (no carry-over): Carry-over honors realistic hoarding and avoids spurious gaps when patients refill 90-day supplies early, but it can extend episodes long past true use and inflate PDC/MPR above 1.0 if not capped. Cap-at-1 is conservative and is the default for adherence metrics; carry-over is defensible for persistence when oversupply is genuine. Name the rule in the SAP and run it as a sensitivity analysis. - vs. handing the problem to a black-box vendor "treatment episode" table: Convenient, but the vendor's hidden grace and carry-over defaults may not match your estimand; you cannot reproduce or defend a number you did not construct.

When to use

Any drug-exposure RWE study in dispensing/claims or linked EHR-pharmacy data where exposure is time-varying or where on-treatment follow-up, persistence, adherence (PDC/MPR), discontinuation, switching, or restart/rechallenge must be measured. It is the prerequisite step for as-treated risk-window construction, for new-user washout checks (the washout is itself an "absence-of-episode" check), and for time-varying exposure in marginal structural models / pooled logistic regression.

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

- When the estimand is purely initiation (ITT-like first-line strategy). If you only need "initiated A vs B at time zero" and you carry exposure forward by protocol regardless of refills, elaborate gap stitching adds nothing and a mis-tuned grace can create bias by censoring people you intended to follow. Use it only to detect switching/crossover, not to define the primary contrast. - When days_supply is unreliable. For injectables, samples, inpatient-administered drugs, titrated/PRN regimens, or fields known to be defaulted to 30, the supply-end projection is fiction and episode boundaries are noise. A long grace on a 90-day-mail population glued to a short grace on a retail population produces differential misclassification by channel. - When the chosen grace introduces immortal time. Carrying exposure status past the last fill while events accrue means deaths/outcomes during the "phantom" supply are attributed to exposure — an as-treated mirror of immortal-time bias. If the outcome itself causes discontinuation (e.g., the drug is stopped at the event), a long grace silently moves the event inside the exposed window. - When competing risks differ by exposure in an elderly claims cohort. Episodes that end at death must be handled as competing events, not as administrative discontinuation, or persistence is overstated in the arm with lower mortality.

Data-source operational depth

- Administrative claims (FFS): The natural substrate — pharmacy claims carry `fill_date`, `days_supply`, `quantity`, and NDC. Project supply-end = `fill_date + days_supply`; stitch fills within the grace; close episodes at the first gap exceeding grace. Failure modes: (a) Medicare Advantage / capitated person-time lacks FFS pharmacy and medical claims — a "gap" may be unobserved enrollment, not a true drug holiday, so require continuous Part D (or commercial pharmacy) coverage across the episode and treat MA-only spans as non-observable, not as off-treatment. (b) 90-day mail-order and stockpiling make early refills routine; without a carry-over decision, overlapping supply double-counts person-time. (c) `days_supply` defaulted to 30 for some pharmacies/products corrupts supply-end. - EHR: Exposure is the prescription order (or e-prescribing record), not a confirmed dispensing — primary non-adherence (written but never filled) inflates apparent initiation; link to pharmacy fills where possible. Medication-administration records (MAR) exist only for the inpatient setting, and care delivered outside the system is invisible, so episodes break artificially when a patient gets refills at an outside pharmacy. - Registry: Often captures treatment lines or regimens with adjudicated start/stop dates but rarely complete refill granularity; link to claims to reconstruct supply-based episodes, and reconcile registry-recorded stop dates against the last projected supply-end. - Linked claims–EHR–registry: Best substrate (orders + fills + clinical context + mortality for competing-risk handling) but introduces date-discrepancy reconciliation (order date vs. fill date vs. administration date) and linkage selection that must precede episode construction.

Inpatient bridging (a recurring real-world failure mode)

During a hospitalization, outpatient pharmacy fills stop because drugs are administered inpatient and bundled into the facility claim. A naive algorithm sees a gap and closes the episode mid-stay, then opens a spurious "restart" at discharge — manufacturing discontinuations and immortal time. The fix is to bridge: suspend the gap clock (or extend supply-end) across inpatient days identified from facility claims so the episode spans the admission.

Worked claims example

Question: persistence on a once-daily oral anticoagulant among adults with atrial fibrillation in a commercial + Medicare FFS database. Inputs: `rx (person_id, fill_date, days_supply, ndc)` restricted to the drug-class NDC list, plus `enroll` spans with continuous Part D / pharmacy benefit. Rules (pre-specified in the SAP): grace period = 30 days; stockpiling = carry-over allowed but supply-end capped so cumulative overlap never exceeds the current fill's `days_supply` (cap-at-1 for adherence; carry-over sensitivity for persistence); inpatient days bridged. Algorithm: (1) sort fills by person and date; (2) project `supply_end = fill_date + days_supply`; (3) for each subsequent fill, if `fill_date <= prior_supply_end + 30`, it belongs to the same episode and (under carry-over) the running supply-end advances; if `fill_date > prior_supply_end + 30`, close the episode at `prior_supply_end` and open a new one; (4) censor open episodes at disenrollment, death, or end of data. Output: one row per episode with `episode_start`, `episode_end`, `n_fills`, `total_days_supply`, and a `gap_days` flag. Persistence = time from first `episode_start` to the end of the first episode; PDC over a fixed denominator window = covered days within the window ÷ window length. Sensitivity analyses: re-run with grace ∈ {0, 15, 60, 90} and with/without carry-over, and report how median persistence and the discontinuation rate move — the Gardarsdottir result in your own data.

Worked example

Scenario

Patient 1001 is taking rivaroxaban (an oral anticoagulant) and has five pharmacy fills over about seven months in 2024. We want to identify how many continuous treatment episodes she had and how many total days she was covered. We use a grace period of 30 days (a gap up to 30 days is still treated as continuous use) and the cap-at-1 rule (an early refill does not double-count days she already had pills for).

Dataset

Raw pharmacy fills for patient 1001 — exactly the rows an analyst sees in a claims pharmacy table.

person_idfill_datedrugdays_supply
10012024-01-05rivaroxaban30
10012024-02-01rivaroxaban30
10012024-03-10rivaroxaban30
10012024-06-01rivaroxaban30
10012024-07-01rivaroxaban30

Steps

  • Fill A (Jan 5, 30 days) covers Jan 5–Feb 3. The last day with pills on hand is Feb 3.

  • Fill B arrives Feb 1 — three days before Fill A runs out. It overlaps, so there is no gap. Under the cap-at-1 rule, the new fill only adds coverage starting from where Fill A ends: last covered day advances to Mar 1 (Feb 4 + 26 remaining days of Fill B, i.e., Feb 1 + 29 = Mar 1 is later than Feb 3, so Mar 1 wins). Still Episode 1.

  • Fill C arrives Mar 10. Fill B's supply runs until Mar 1. The gap is Mar 2–Mar 9 — 8 days. 8 ≤ 30 (the grace period), so this is still continuous. Last covered day advances to Apr 7 (Mar 10 + 29). Still Episode 1.

  • Episode 1 closes at Apr 7 (the last day Fill C covers). Episode 1 spans Jan 5–Apr 7 = 94 covered days.

  • Fill D arrives Jun 1. The gap from Apr 8 through May 31 is 54 days. 54 > 30 (exceeds the grace period), so Episode 1 is definitively closed and a NEW Episode 2 opens on Jun 1. Last covered day = Jun 30.

  • Fill E arrives Jul 1. Gap = Jul 1 − Jun 30 = 1 day. 1 ≤ 30, so still Episode 2. Last covered day advances to Jul 30 (Jul 1 + 29). Episode 2 closes at Jul 30.

  • Episode 2 spans Jun 1–Jul 30 = 60 covered days.

  • Total exposed days = 94 (Episode 1) + 60 (Episode 2) = 154 days across 2 episodes.

Result

2 treatment episodes; 154 total exposed days. Episode 1: Jan 5 – Apr 7 (94 days, 3 fills joined by gaps ≤ 30 days). Episode 2: Jun 1 – Jul 30 (60 days, 2 fills joined by a 1-day gap). The 54-day gap between Apr 8 and May 31 exceeded the 30-day grace and split the record into two distinct episodes.

Timeline Spec

Title

Two treatment episodes for one rivaroxaban patient (grace = 30 days, cap-at-1)

Window
Start

2024-01-05

End

2024-07-30

Label

Observation window: Jan 5 – Jul 30, 2024

Events
  • Label

    Fill A

    Start

    2024-01-05

    Length Days

    30

    Quantity

    30 days_supply

  • Label

    Fill B (early refill, 3-day overlap)

    Start

    2024-02-01

    Length Days

    30

    Quantity

    30 days_supply

  • Label

    Fill C (8-day gap, within grace)

    Start

    2024-03-10

    Length Days

    30

    Quantity

    30 days_supply

  • Label

    Fill D (new episode starts)

    Start

    2024-06-01

    Length Days

    30

    Quantity

    30 days_supply

  • Label

    Fill E (1-day gap, within grace)

    Start

    2024-07-01

    Length Days

    30

    Quantity

    30 days_supply

Spans
  • Kind

    covered

    Start

    2024-01-05

    End

    2024-04-07

    Label

    Episode 1: 94 covered days (Fills A+B+C merged)

  • Kind

    gap

    Start

    2024-04-08

    End

    2024-05-31

    Label

    54-day gap → exceeds 30-day grace → new episode

  • Kind

    covered

    Start

    2024-06-01

    End

    2024-07-30

    Label

    Episode 2: 60 covered days (Fills D+E merged)

Result
Label

2 episodes | 94 + 60 = 154 total exposed days

Value

154

Caption

Three fills close enough together (gaps of 0 and 8 days, both under the 30-day grace) merge into one 94-day episode. A 54-day gap then exceeds the grace, closing Episode 1 and opening Episode 2 when the patient refills in June. Episode 2's two fills are separated by only 1 day and merge into a 60-day episode.

Alt Text

Horizontal timeline from January to July 2024. Five fill bars are shown. Fills A, B, and C are connected by a green 'Episode 1' span covering Jan 5 to Apr 7 (94 days). A red gap bar covers Apr 8 to May 31 (54 days) labeled 'gap exceeds grace.' Fills D and E are connected by a second green 'Episode 2' span covering Jun 1 to Jul 30 (60 days).

Runnable example

python implementation

Construct continuous treatment episodes from claims-style pharmacy fills. Required input (already cleaned, de-duplicated, and restricted to the target drug-class NDCs): rx : one row per fill -> person_id, fill_date (datetime64), days_supply (int) Output:...

import pandas as pd

GRACE = 30  # permissible gap (days) between supply-end and next fill that still counts as continuous

def build_episodes(rx: pd.DataFrame, grace: int = GRACE, carry_over: bool = False) -> pd.DataFrame:
    rx = rx.sort_values(["person_id", "fill_date"]).copy()
    out = []
    for pid, g in rx.groupby("person_id", sort=False):
        ep_id = 0
        ep_start = None          # first fill_date of the open episode
        supply_end = None        # running projected end of available supply
        n_fills = 0
        total_supply = 0
        last_close = None        # supply_end of the just-closed episode, for gap reporting
        for _, row in g.iterrows():
            f, dsup = row["fill_date"], int(row["days_supply"])
            proj_end = f + pd.Timedelta(days=dsup)
            if ep_start is None:
                ep_start, supply_end = f, proj_end
                n_fills, total_supply = 1, dsup
                continue
            # Within grace of the running supply-end -> same episode.
            if f <= supply_end + pd.Timedelta(days=grace):
                if carry_over:
                    # Roll surplus forward: extend from the later of (current supply-end, this fill).
                    supply_end = max(supply_end, f) + pd.Timedelta(days=dsup)
                else:
                    # Cap-at-1: no double counting of overlapping days.
                    supply_end = max(supply_end + pd.Timedelta(days=dsup), proj_end)
                n_fills += 1
                total_supply += dsup
            else:
                # Gap exceeds grace -> close current episode, open a new one.
                out.append((pid, ep_id, ep_start, supply_end, n_fills, total_supply,
                            (f - supply_end).days))
                ep_id += 1
                ep_start, supply_end = f, proj_end
                n_fills, total_supply = 1, dsup
        if ep_start is not None:
            out.append((pid, ep_id, ep_start, supply_end, n_fills, total_supply, None))
    return pd.DataFrame(out, columns=["person_id", "episode_id", "episode_start",
                                      "episode_end", "n_fills", "total_days_supply", "gap_days"])
r implementation

Construct treatment episodes with data.table by-group processing. Input mirrors the Python version: rx : person_id, fill_date (Date), days_supply (integer), restricted to the target drug-class NDCs. Output: person_id, episode_id, episode_start, episode_end,...

library(data.table)

build_episodes <- function(rx, grace = 30L, carry_over = FALSE) {
  setDT(rx)
  setorder(rx, person_id, fill_date)

  one_person <- function(fd, ds) {
    n <- length(fd)
    ep <- integer(n); ep_start <- as.Date(rep(NA, n)); supply_end <- as.Date(rep(NA, n))
    cur_ep <- 0L; cur_start <- fd[1]; cur_end <- fd[1] + ds[1]
    ep[1] <- 0L; ep_start[1] <- cur_start; supply_end[1] <- cur_end
    if (n >= 2L) for (i in 2:n) {
      if (fd[i] <= cur_end + grace) {                       # within grace -> same episode
        cur_end <- if (carry_over) max(cur_end, fd[i]) + ds[i]
                   else max(cur_end + ds[i], fd[i] + ds[i]) # cap-at-1: no overlap double-count
      } else {                                              # gap exceeds grace -> new episode
        cur_ep <- cur_ep + 1L; cur_start <- fd[i]; cur_end <- fd[i] + ds[i]
      }
      ep[i] <- cur_ep; ep_start[i] <- cur_start; supply_end[i] <- cur_end
    }
    data.table(episode_id = ep, episode_start = ep_start, supply_end = supply_end)
  }

  ep <- rx[, one_person(fill_date, days_supply), by = person_id]
  rx[, c("episode_id", "episode_start", "episode_end") :=
       .(ep$episode_id, ep$episode_start, ep$supply_end)]

  out <- rx[, .(episode_start = episode_start[1],
                episode_end   = max(episode_end),
                n_fills       = .N,
                total_days_supply = sum(days_supply)),
            by = .(person_id, episode_id)]
  setorder(out, person_id, episode_id)
  out[, gap_days := as.integer(shift(episode_start, type = "lead") - episode_end),
      by = person_id]
  out[]
}