← Methods repository
concept

Case-Time-Control Design

A self-controlled, case-only design that augments the case-crossover by dividing each case's within-person exposure odds ratio by the analogous odds ratio estimated in a separate series of (typically future) cases or non-cases, intending to remove the exposure-time trend that biases the case-crossover when exposure prevalence drifts over calendar time.

Study_Designself-controlledcase-crossover-extensionexposure-time-trend-adjustmenttransient-effectspharmacoepidemiologycase-only
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

The case-time-control design is a way to study whether a short-acting drug briefly raises the chance of a sudden event — like a hip fracture — by comparing what each patient was taking right before their event versus what they were taking a few weeks earlier. Because each person acts as their own comparison, most stable differences between people (like age or underlying frailty) cancel out. The twist this design adds on top of the simpler case-crossover is a second group of people — called time controls — whose drug records are examined over the same two windows so the study can subtract out any background drift in how often the drug was being prescribed over those years. In practice, researchers almost always prefer to build that second group from future patients who will eventually have the same kind of event, because those people are more similar to the cases in ways that matter.

The case-time-control (CTC) design (Suissa, 1995) was proposed as a correction to the case-crossover design. In a case-crossover, each case is its own control: exposure during a short hazard (case) window immediately before an acute event is contrasted with exposure during one or more earlier referent (control) windows in the same person, and the effect is estimated by conditional logistic regression. This eliminates all time-invariant between-person confounding by construction. Its fatal vulnerability is an exposure-time trend: if the population probability of being exposed is rising (or falling) over calendar time for reasons unrelated to the outcome — a drug being launched, a formulary change, a guideline shift — then the referent windows (further in the past) are systematically less (or more) exposed than the hazard window, and the case-crossover odds ratio is biased even when the drug has no effect. CTC tries to measure that trend in a control group and divide it out: it computes the case-crossover odds ratio in the cases (OR_cc), computes the same within-person odds ratio in a series of time controls (a separate sample of subjects who do not contribute the event at the index time — classically future cases or matched non-cases), and reports CTC_OR = OR_cc / OR_tc. Algebraically this is a single conditional logistic model on the pooled person-windows with a case-status x exposure interaction term; the exponentiated interaction coefficient is the CTC estimate.

Core conceptual / estimand distinction

The estimand is the transient, within-person rate ratio of an abrupt event for a time-varying ("on" vs "off") exposure, purged of the exposure-time trend captured by the control series. CTC does not estimate a chronic or cumulative effect, and it does not estimate a between-person contrast of ever- vs never-users. The defining — and contested — identifying assumption is that the exposure-time trend in the control series equals the counterfactual exposure-time trend the cases would have experienced absent the event, conditional on the same within-person comparison. Greenland (1996) showed that this is far stronger than it looks: because the time controls are different people, the OR_tc carries the between-person confounding that the case-crossover was specifically designed to avoid, and dividing by it re-injects that confounding unless the case and control series are exchangeable on those confounders. Suissa (1998) substantially conceded the point and narrowed the conditions under which CTC is valid. This is why CTC is best understood not as a default tool but as a historically important bridge between the case-crossover and the modern self-controlled toolkit.

Pros, cons, and trade-offs

- vs the plain case-crossover: CTC's one advantage is that it attempts to remove exposure-time-trend bias, which the case-crossover cannot. Cost: it does so by importing a between-person control series, so it trades a known, characterizable bias (the trend) for a generally uncontrollable one (between-person confounding in OR_tc). Prefer the case-crossover — with a bidirectional or symmetric referent-window scheme — when the trend is mild and the exposure is genuinely transient; prefer neither over the options below when the trend is strong. - vs the case-case-time-control design (Wang et al., 2011): This is the decisive comparison. Case-case-time-control keeps CTC's ratio structure but makes the control series future cases of the same outcome, so the control odds ratio reflects the exposure trend among people who are exchangeable with the cases on outcome-related confounders. It removes both the time-invariant confounding and the trend without Greenland's re-confounding problem. Prefer case-case-time-control over CTC essentially whenever a future-case series is obtainable — it is the recommended modern successor. - vs the self-controlled case series (SCCS): SCCS models the full observation time of each case with conditional Poisson regression and can include a calendar-time (age/period) covariate to absorb the exposure-time trend within-person, avoiding any external control group. Prefer SCCS when the exposure is well measured across each person's whole observation window and the event does not censor future exposure; it is generally more efficient and avoids CTC's confounding trap. - vs the active comparator, new-user cohort: For a sustained effect of a chronically used drug, no self-controlled design applies; a cohort with an active comparator and time-zero alignment is the right tool. CTC is only ever a candidate for transient effects of intermittent exposures on abrupt events.

When to use

Acute, abrupt-onset outcome (hip fracture, MI, motor-vehicle crash, seizure, anaphylaxis); a transient, intermittent exposure whose effect, if any, is short-lived (a benzodiazepine fill, an NSAID course, a triptan); a credible concern that exposure prevalence is trending over calendar time so that a bare case-crossover would be biased; and a setting where the cleaner alternatives (case-case-time-control, SCCS) are infeasible — for example, future cases cannot be assembled in the available data window and full per-person exposure histories needed for SCCS are unavailable.

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

- A future-case or exchangeable control series is available. Then use case-case-time-control. Using CTC with arbitrary non-cases when you could have used future cases is choosing the design with the known confounding flaw. - The control series is not exchangeable with the cases on the time-invariant confounders. This is the Greenland (1996) failure: OR_tc then carries between-person confounding, and CTC_OR = OR_cc / OR_tc is biased — and the direction is unpredictable. There is no diagnostic that fully verifies exchangeability, so a "balanced" presentation overstates CTC's safety. Treat a non-exchangeable control series as disqualifying, not as a limitation to footnote. - The effect is chronic or cumulative, or the exposure is continuous. Self-controlled within-person contrasts have no "unexposed" referent and the design collapses; use a cohort design. - Event-dependent exposure / event-dependent observation. If the event changes the probability of subsequent exposure (e.g., the fracture stops the benzodiazepine) or truncates observation (death), using future windows as referents is invalid; SCCS extensions for event-dependent exposure or strictly pre-event (unidirectional) referent windows are required. - The exposure-time trend itself is the causal pathway (e.g., a prescribing surge driven by early outcomes). Then the trend is not nuisance to be divided out and CTC removes signal.

Data-source operational depth

- Claims (FFS): The natural substrate. Exposure on a given day is read from `fill_date` + `days_supply` (a day is "exposed" if it falls inside an active supply interval). Require continuous medical + pharmacy enrollment spanning the full referent-through-hazard window so that "unexposed" days are observed, not missing. Failure modes: Medicare Advantage / capitated person-time lacks fee-for-service claims, so apparent non-exposure in a referent window can be pure missingness — restrict to A/B/D (or commercial medical+pharmacy) enrollees and exclude MA-only spans. 90-day mail-order and stockpiling stretch `days_supply` and blur window assignment; free samples and inpatient administrations are invisible in pharmacy claims, biasing exposure classification non-differentially toward the null. - EHR: Order/administration dates can place exposure more precisely than dispensing, but visit-driven capture means referent windows in quiet periods look spuriously unexposed; link to pharmacy fills before trusting "off" days. Outcome onset timing (essential for placing the 1-day-resolution hazard window) is often better in EHR than claims. - Registry: Strong for adjudicated, precisely dated acute events (the design's chief requirement) but typically weak for complete intermittent-exposure histories; link to claims for fills. Registries rarely support assembling an exchangeable time-control series, which often pushes the analysis toward SCCS instead. - Linked claims-EHR-registry: Best dating of the acute event (registry/EHR) with complete exposure (claims). Reconcile order/fill/service-date discrepancies before fixing window boundaries, and beware that the linkable subset is selected, which can break exchangeability between the case and control series.

Worked claims example

Question: does a new benzodiazepine fill transiently raise the risk of hip fracture in older adults, in a FFS database where benzodiazepine use is rising over the study years (so a bare case-crossover would be biased upward by the trend)? (1) Cases: members aged >=65 with a first inpatient hip-fracture claim (`event_date`), with continuous A/B/D enrollment and no MA-only span over the 60 days before the event. (2) Hazard window: days 1-7 before `event_date`; a member is "exposed" in that window if any benzodiazepine `days_supply` interval (`fill_date` to `fill_date + days_supply - 1`) overlaps it. (3) Referent window: days 31-37 before `event_date`, classified identically — a strictly pre-event (unidirectional) scheme, because a fracture plausibly changes later benzodiazepine use. (4) Time-control series: a sample of future hip-fracture cases (people who fracture later in the data) assigned a pseudo-`event_date` at their own future fracture; their day 1-7 vs day 31-37 windows are classified the same way. Using future cases (rather than arbitrary non-cases) is what keeps the control series exchangeable on fracture-related confounders and avoids the Greenland re-confounding problem — i.e., this worked example is really a case-case-time-control specification, which is the defensible way to run CTC's machinery. (5) Estimation: pool all person-windows; fit conditional logistic regression stratified on person, with terms for `exposed`, `case_status`, and their interaction; exp(interaction coefficient) is the CTC/case-case-time-control estimate. The ratio OR_cc / OR_tc divides out the rising-prevalence trend; the residual reflects the transient hazard. (6) Sensitivity: vary hazard/referent window lengths and gaps, add multiple referent windows, restrict `days_supply` handling (cap stockpiling), and run a negative-control exposure (a drug with no plausible fracture mechanism) to detect residual trend or confounding.

Worked example

Scenario

A claims database covers adults aged 65 and older from January 2018 through December 2019. Benzodiazepine prescribing has been rising steadily over those two years — a background trend unrelated to any one patient's fracture risk. We want to know whether having an active benzodiazepine fill in the week before a hip fracture raises fracture risk. We select two people: Patient A (a case) who fractured on 2019-03-15, and Patient B (a time control — a future case who fractured later, on 2019-11-20) who contributes the same two windows anchored on their own fracture date. For each person we check whether any benzodiazepine prescription covered the hazard window (days 1-7 before fracture) and the referent window (days 31-37 before fracture).

Dataset

Pharmacy fill records for the two study participants. Each row is one prescription dispensing.

person_idfill_datedrugdays_supplycase_status
A-0012019-02-15lorazepam301
B-0022019-10-30lorazepam30

Steps

  • Patient A (case) fractured on 2019-03-15. Hazard window = 2019-03-08 through 2019-03-14. Referent window = 2019-02-06 through 2019-02-12.

  • Patient A's lorazepam fill started 2019-02-15 and covered 30 days, meaning the supply lasted through 2019-03-16. That interval (Feb 15 to Mar 16) overlaps the hazard window (Mar 8-14): EXPOSED in hazard window = 1. It does NOT overlap the referent window (Feb 6-12): EXPOSED in referent window = 0. So within Patient A, the exposure odds ratio (OR_cc) numerator favors exposure near the event.

  • Patient B (time control) had their fracture on 2019-11-20. Hazard window = 2019-11-13 through 2019-11-19. Referent window = 2019-10-14 through 2019-10-20.

  • Patient B's lorazepam fill started 2019-10-30 and covered 30 days through 2019-11-28. That supply overlaps BOTH windows: hazard window (Nov 13-19) = EXPOSED 1, referent window (Oct 14-20) = EXPOSED 0. Same pattern as Patient A.

  • OR_cc (cases only): exposed in hazard vs referent = (1/0) pattern — in a full cohort this ratio measures the association in cases. OR_tc (time controls only): same exposed-in-hazard / not-in-referent pattern. The CTC estimate = OR_cc divided by OR_tc. If OR_tc equals 1.0 (no trend), the adjustment does nothing. If the rising prescribing trend had made OR_tc = 1.4, then CTC_OR = OR_cc divided by 1.4, shrinking the raw case-crossover estimate toward the truth.

Result

With one case and one matched time control the numbers are illustrative, not a real p-value. The key arithmetic: if the raw within-person odds ratio in cases (OR_cc) were 2.8 and the time controls showed an OR_tc of 1.4 driven purely by the rising prescribing trend, then CTC_OR = 2.8 divided by 1.4 = 2.0. The design divided out the trend and left the estimated 2-fold transient fracture hazard during the first week of an active benzodiazepine fill.

Timeline Spec

Title

Case-time-control windows for one case (Patient A) and one time control (Patient B)

Window
Start

2019-02-06

End

2019-03-15

Label

Study observation span shown (Patient A)

Events
  • Label

    Patient A fill (lorazepam, 30-day supply)

    Start

    2019-02-15

    Length Days

    30

    Quantity

    30 days_supply

  • Label

    Patient B fill (lorazepam, 30-day supply)

    Start

    2019-10-30

    Length Days

    30

    Quantity

    30 days_supply

Spans
  • Kind

    unexposed

    Start

    2019-02-06

    End

    2019-02-12

    Label

    Patient A referent window (days 31-37 pre-fracture): NOT covered by fill

  • Kind

    covered

    Start

    2019-02-15

    End

    2019-03-16

    Label

    Patient A: fill active (30 days)

  • Kind

    exposed

    Start

    2019-03-08

    End

    2019-03-14

    Label

    Patient A hazard window (days 1-7 pre-fracture): covered by fill

  • Kind

    unexposed

    Start

    2019-10-14

    End

    2019-10-20

    Label

    Patient B referent window (days 31-37 pre-fracture): NOT covered by fill

  • Kind

    covered

    Start

    2019-10-30

    End

    2019-11-28

    Label

    Patient B: fill active (30 days)

  • Kind

    exposed

    Start

    2019-11-13

    End

    2019-11-19

    Label

    Patient B hazard window (days 1-7 pre-fracture): covered by fill

Result
Label

CTC_OR = OR_cc / OR_tc = 2.8 / 1.4 = 2.0 after dividing out the prescribing trend

Value

2.0

Caption

Timeline showing the hazard window (days 1-7 before fracture, shaded as exposed) and referent window (days 31-37 before fracture, shaded as unexposed) for Patient A (a case) and Patient B (a time control). The fill bar shows when the 30-day lorazepam supply was active. Both patients were covered during the hazard window but not the referent window, consistent with the rising background trend. Dividing OR_cc by OR_tc removes that trend.

Alt Text

Two horizontal timeline rows, one per patient. Each row has a referent window bar (grey, not covered by fill) 31-37 days before fracture, a fill bar (blue) showing when the prescription was active, and a hazard window bar (orange, covered by fill) 1-7 days before fracture. An annotation shows the CTC division: OR_cc divided by OR_tc equals 2.0.

Runnable example

python implementation

Case-time-control / case-case-time-control estimation from claims-style inputs. Required inputs (cleaned, de-duplicated): events : one row per subject with an (real or pseudo) index date -> person_id, event_date (datetime), case_status in {1=case,...

import pandas as pd
import numpy as np
from statsmodels.discrete.conditional_models import ConditionalLogit

HAZARD = (1, 7)     # days before index that form the hazard ("case") window
REFERENT = (31, 37) # earlier, pre-index referent ("control") window (unidirectional)

def _exposed_in_window(person_fills, lo_date, hi_date):
    """1 if any active fill interval [fill_date, fill_date+days_supply-1] overlaps [lo_date, hi_date]."""
    if person_fills.empty:
        return 0
    start = person_fills["fill_date"]
    end = start + pd.to_timedelta(person_fills["days_supply"] - 1, unit="D")
    overlap = (start <= hi_date) & (end >= lo_date)
    return int(overlap.any())

def build_ctc_windows(events: pd.DataFrame, fills: pd.DataFrame) -> pd.DataFrame:
    fills = fills.sort_values(["person_id", "fill_date"])
    by_person = dict(tuple(fills.groupby("person_id")))
    rows = []
    for _, r in events.iterrows():
        pf = by_person.get(r["person_id"], fills.iloc[0:0])
        # window = "case" (hazard) vs "referent" within each subject; both anchored on the index date
        haz_lo = r["event_date"] - pd.Timedelta(days=HAZARD[1])
        haz_hi = r["event_date"] - pd.Timedelta(days=HAZARD[0])
        ref_lo = r["event_date"] - pd.Timedelta(days=REFERENT[1])
        ref_hi = r["event_date"] - pd.Timedelta(days=REFERENT[0])
        rows.append({"person_id": r["person_id"], "case_status": r["case_status"], "window": 1,
                     "exposed": _exposed_in_window(pf, haz_lo, haz_hi)})
        rows.append({"person_id": r["person_id"], "case_status": r["case_status"], "window": 0,
                     "exposed": _exposed_in_window(pf, ref_lo, ref_hi)})
    return pd.DataFrame(rows)

def estimate_ctc(windows: pd.DataFrame):
    # Outcome = being the hazard window (window=1); strata = person. Conditional logit removes
    # all time-invariant person-level confounding. The case_status x exposed interaction is the CTC term.
    d = windows.copy()
    d["exp_x_case"] = d["exposed"] * d["case_status"]
    X = d[["exposed", "exp_x_case"]]  # case_status main effect drops out (constant within person stratum)
    model = ConditionalLogit(d["window"], X, groups=d["person_id"])
    res = model.fit(disp=False)
    ctc_or = np.exp(res.params["exp_x_case"])
    ci = np.exp(res.conf_int().loc["exp_x_case"])
    return {"ctc_or": ctc_or, "ci_low": ci[0], "ci_high": ci[1], "result": res}
r implementation

Case-time-control / case-case-time-control estimation with survival::clogit. Inputs mirror the Python version: events : person_id, event_date (Date), case_status (1 = case, 0 = time-control) fills : person_id, fill_date (Date), days_supply (integer) Builds...

library(data.table)
library(survival)

HAZARD   <- c(1L, 7L)    # days before index: hazard window
REFERENT <- c(31L, 37L)  # days before index: referent window (unidirectional)

exposed_in_window <- function(pf, lo, hi) {
  if (nrow(pf) == 0L) return(0L)
  start <- pf$fill_date
  end   <- pf$fill_date + pf$days_supply - 1L
  as.integer(any(start <= hi & end >= lo))
}

build_ctc_windows <- function(events, fills) {
  setDT(events); setDT(fills); setorder(fills, person_id, fill_date)
  out <- vector("list", nrow(events))
  for (i in seq_len(nrow(events))) {
    e  <- events[i]
    pf <- fills[person_id == e$person_id]
    haz_lo <- e$event_date - HAZARD[2];   haz_hi <- e$event_date - HAZARD[1]
    ref_lo <- e$event_date - REFERENT[2]; ref_hi <- e$event_date - REFERENT[1]
    out[[i]] <- data.table(
      person_id   = e$person_id,
      case_status = e$case_status,
      window      = c(1L, 0L),  # 1 = hazard, 0 = referent
      exposed     = c(exposed_in_window(pf, haz_lo, haz_hi),
                      exposed_in_window(pf, ref_lo, ref_hi)))
  }
  rbindlist(out)
}

estimate_ctc <- function(windows) {
  # Conditional logit stratified on person; the exposed:case_status interaction is the CTC term.
  fit <- clogit(window ~ exposed + exposed:case_status + strata(person_id), data = windows)
  cf  <- summary(fit)$coefficients["exposed:case_status", ]
  ci  <- exp(confint(fit)["exposed:case_status", ])
  list(ctc_or = exp(cf["coef"]), ci_low = ci[1], ci_high = ci[2], fit = fit)
}