Case-Control Study Design
An observational design that samples on outcome status — identifying cases who experience the event and controls who do not — and compares prior exposure odds between them, estimating an odds ratio that, under incidence-density (risk-set) sampling, equals the rate ratio without any rare-disease assumption.
On this page
A case-control study works backwards from the result. You start by finding people who already have the outcome you care about (the cases) and a comparison group who do not (the controls), then you look back in time to ask who had been exposed to the thing you suspect. Because you can hunt down rare cases directly instead of waiting for them to appear, this design is fast and cheap for uncommon events. The catch: it tells you whether exposure and outcome travel together (an odds ratio), not how many people overall will get sick, and a poorly chosen control group can quietly distort the answer.
The case-control (CC) design reverses the logic of a cohort: instead of following exposed and unexposed forward to the outcome, it samples on the outcome — identifying everyone (or a sample) who became a case and a sample of controls who did not — and then looks backward at exposure in an etiologically relevant window. Its reason for existing is efficiency: when an outcome is rare or its ascertainment is expensive (adjudication, chart review, genotyping, biospecimen assay), enrolling 300 cases and 1,200 controls answers the question that would otherwise require following hundreds of thousands of people in a cohort.
Core conceptual distinction — what the odds ratio actually estimates
This is the point reviewers test first, and the point most casual descriptions get wrong. The estimand depends entirely on how controls are sampled:
- Incidence-density (risk-set) sampling. Controls are sampled from the population still at risk and event-free at the instant each case occurs (the case's index date). Under this scheme the exposure odds ratio estimates the incidence rate ratio directly — with no rare-disease assumption (Greenland & Thomas). This is the correct mental model for pharmacoepidemiology and the one that makes a nested CC algebraically equivalent to a Cox model on the full cohort (Breslow's partial likelihood for a matched risk set is the conditional-logistic likelihood).
- Cumulative ("traditional") sampling. Controls are sampled from those still non-cases at the end of the study period. Here the OR estimates the risk (cumulative-incidence) odds ratio, which approximates the risk ratio only under the rare-disease assumption (outcome < ~10%).
- Case-cohort sampling. Controls are a random subcohort sampled at baseline (a "base series"); with appropriate weighting the OR estimates the risk ratio and one subcohort can serve multiple outcomes. Conflating these three is the classic error: density-sampled CC does not need a rare disease, cumulative CC does. State the sampling scheme and the resulting estimand explicitly in the SAP.
Pros, cons, and trade-offs
- vs a full cohort (cohort-prospective / cohort-retrospective): CC needs orders of magnitude less data collection for rare outcomes and lets you spend an expensive ascertainment budget (adjudication, assays) only on cases plus a handful of controls. Cost: it cannot produce absolute risks, incidence, or numbers-needed-to-treat without known sampling fractions; it is more exposed to selection bias through control choice; and it is harder to explain to clinical stakeholders. Prefer CC when the outcome is rare or its measurement is the binding cost constraint. Prefer a cohort when you need absolute risk, multiple outcomes from one exposure, or the outcome is common.
- vs nested case-control: A nested CC samples cases and risk-set controls from inside an already-defined cohort (claims, EHR, registry). It inherits the cohort's clean source population, time-zero, and density-sampling validity while keeping CC efficiency — it is the modern default whenever a usable source cohort exists. Stand-alone CC (case registry + controls sampled from eligibility files) is what you fall back to when no such cohort is available. Prefer nested essentially always when you have the cohort.
- vs self-controlled designs (self-controlled-case-series, case-crossover, case-time-control): Self-controlled designs use each case as their own control and therefore null out all time-invariant confounding (genetics, chronic frailty, baseline SES) by construction — something CC can only attempt through measurement and matching. But they require transient, repeatable exposures and (for SCCS) recurrent or at least non-fatal events, and they cannot study fixed exposures. Prefer CC for one-time or chronic exposures, fatal/non-recurrent outcomes, or when between-person factors are themselves of interest; prefer self-controlled for acute drug triggers of recurrent events.
When NOT to use — and when it is actively misleading
- The outcome is common and you used cumulative sampling. The OR then overstates the risk ratio (the rare-disease approximation fails). Switch to density sampling, a case-cohort design, or report the OR as an odds ratio only.
- Controls are not drawn from the population that gave rise to the cases. This is the cardinal sin (Wacholder's "study base" principle). Hospital controls for a community case series, or controls whose exposure prevalence differs from the source base for reasons unrelated to disease, produce selection bias that no analysis can repair. Berkson's bias (hospital-based controls) is the canonical example.
- Differential exposure ascertainment by case status. In primary-data CC this is recall bias; in claims/EHR it is surveillance/detection bias — cases, by virtue of being sicker or in more contact with the system, have more complete exposure capture than controls, inflating the OR. Match or adjust on healthcare utilization to blunt it.
- Outcome misclassification. Low PPV of the case algorithm contaminates the case series with non-cases and biases the OR toward the null; this is why CC validity is downstream of the outcome phenotype (see claims-outcome-algorithm-ppv- sensitivity-rwe).
- Prevalent exposure mixed in. If "exposed" includes long-term prevalent users, depletion-of-susceptibles and immortal-time issues from the source cohort leak in; apply a new-user/incident restriction inside the exposure window.
Data-source operational depth
- Claims (FFS vs MA, commercial): Cases come from validated outcome algorithms (e.g., 1 inpatient or 2 outpatient claims with the diagnosis in a primary/qualifying position, or a procedure + diagnosis). The index date is the case-defining event date; for risk-set controls it is the case's index date assigned to a still-at-risk enrollee. Exposure comes from pharmacy claims (NDC + `fill_date` + `days_supply`) for drugs or medical claims (CPT/HCPCS/J-codes, ICD-10-PCS) for procedures/devices, ascertained in a pre-specified pre-index window (e.g., 90 days for an acute trigger, cumulative for chronic exposure). Failure modes: (1) Medicare Advantage person-time lacks fee-for-service claims — a control's "no exposure" can be unobserved rather than truly unexposed, so require both medical and pharmacy benefit and exclude MA-only person-time. (2) Differential competing risks by exposure in the elderly — if the exposure raises short-term mortality, those people die before becoming cases and are absent from the risk set, distorting the OR; restrict risk sets to those alive and enrolled at the case's index date and run a competing-risks sensitivity analysis. (3) Detection bias — match controls on prior utilization (counts of visits, baseline cost quartile) so cases are not spuriously "more exposed" merely because they are in more contact with the system. (4) Claim reversals/adjudication lag — net by `claim_id` and avoid the most recent, incompletely adjudicated months.
- EHR: Cases via structured codes + NLP phenotyping, validated on a chart-reviewed subset. Exposure = the medication order/administration, not a dispensing, so link to pharmacy fills where possible. The structural threat is visit-driven (informative) observation: a control who is healthy and rarely visits looks "unexposed" simply because little is recorded. Use encounter counts as a matching/adjustment proxy and define the observation window explicitly.
- Registry / linked: Disease and product registries are often case-enriched and ideal for rare outcomes with adjudicated case status; link to claims for complete pharmacy/exposure history and to a death index to firm up the risk set. Linkage introduces selection (only the linkable subset) and date-discrepancy issues that must be reconciled before index-date assignment.
Worked claims example (incidence-density nested CC)
Question: does current exposure to a high-risk oral NSAID raise the risk of hospitalized upper-gastrointestinal (UGI) bleeding among adults ≥40 in a commercial + Medicare FFS database?
- Source cohort: enrollees with ≥365 days of continuous medical and pharmacy enrollment (Parts A/B/D or commercial equivalent), excluding MA-only person-time so that absence of a fill is observed, not missing.
- Cases: first hospitalization with a validated UGI-bleed algorithm (qualifying ICD-10-CM in the primary position on an inpatient claim); index_date = admission date; exclude anyone with a prior UGI bleed in the lookback (incident cases only).
- Risk-set controls: for each case, sample m = 4 controls from enrollees alive, enrolled, and event-free on that case's index_date, matched on age (±2 y), sex, and index calendar month (matching on time = density sampling).
- Exposure: "current use" = an NSAID `fill_date` with `days_supply` covering the case's index_date, or a fill ending within a 14-day carryover; the reference is non-use in the 90-day pre-index window.
- Covariates: anticoagulant/antiplatelet/PPI use, prior GI diagnoses, and baseline visit count (utilization, to control detection bias), all measured strictly before index_date.
- Analysis: conditional logistic regression stratified on the matched set (`clogit` / `STRATA match_id`), reporting the exposure OR as a rate ratio; sensitivity analyses vary the carryover window, the matching ratio, and add a negative-control outcome (e.g., a condition the NSAID should not cause) to detect residual confounding.
Decision diagram
flowchart TD Base[Source population / eligibility + enrollment spans] --> Cases[Identify incident cases<br/>validated outcome algorithm; index_date = event date] Base --> Risk[Risk set at each case index_date<br/>alive, enrolled, FFS-observable, event-free] Cases --> Sample[Density sampling: m controls per case<br/>matched on index_date, age, sex, prior utilization] Risk --> Sample Sample --> Exp[Ascertain exposure + covariates in the<br/>pre-index window only, anchored to index_date] Exp --> Fit[Conditional logistic on matched strata<br/>OR = incidence rate ratio under density sampling] Fit --> Sens[Sensitivity: exposure window, matching ratio,<br/>negative-control outcome, competing-risks check]
flowchart TB
subgraph Selection[Two ways control sampling biases the OR]
A[Controls NOT from the study base<br/>e.g. hospital controls for community cases] -->|Berkson / selection bias| Bias1[OR distorted; unrepairable by analysis]
B[Differential exposure capture by case status<br/>sicker cases have more complete records] -->|detection / surveillance bias| Bias2[OR inflated away from null]
end
Fix1[Sample controls from the same population that produced the cases<br/>Wacholder study-base principle] --> A
Fix2[Match / adjust on prior healthcare utilization] --> BWorked example
Scenario
We want to know whether taking a high-risk NSAID pain reliever is linked to being hospitalized for an upper-gastrointestinal (GI) bleed. We assemble 100 cases (adults hospitalized for a GI bleed) and 100 controls (similar adults with no such bleed), then check each person's pharmacy records to see who had filled an NSAID before their index date. We count everyone into a 2x2 table and compute the odds ratio.
Dataset
The 2x2 table an analyst builds after classifying each of the 200 people by exposure and case status. Cell letters a, b, c, d are labeled for the odds-ratio formula.
| Cases | Controls | |
|---|---|---|
| Exposed (took NSAID) | a = 90 | b = 60 |
| Unexposed (no NSAID) | c = 10 | d = 40 |
Steps
Result
OR = (a*d)/(b*c) = (90*40)/(60*10) = 3600/600 = 6.0. Cases had 6 times the odds of prior NSAID exposure as controls, suggesting NSAID use is associated with GI bleeding.
Trade-offs
Runnable example
Incidence-density risk-set sampling and 1:m matched case-control construction from claims-style inputs. Required inputs (already cleaned and de-duplicated): cases : one row per incident case -> person_id, index_date (datetime), age, sex cohort : everyone in the source population -> person_id, age, sex (the...
import numpy as np
import pandas as pd
EXPOSURE_WINDOW_DAYS = 90 # pre-index etiologic window for "current use"
CARRYOVER_DAYS = 14 # supply may end shortly before index and still count as current
AGE_CALIPER = 2
M_CONTROLS = 4
RNG = np.random.default_rng(20240601)
def _enrolled_observable(person_ids, on_date, enroll):
"""Person_ids with continuous, non-MA-only enrollment covering `on_date`."""
e = enroll[enroll["person_id"].isin(person_ids)]
ok = e[(e["enroll_start"] <= on_date) & (e["enroll_end"] >= on_date) & (~e["ma_only"])]
return set(ok["person_id"])
def build_riskset_matched_cc(cases, cohort, enroll, m=M_CONTROLS):
case_ids = set(cases["person_id"])
rows = []
for _, case in cases.iterrows():
idx = case["index_date"]
# Candidate controls: event-free at idx (not yet a case or a case with a later index date),
# enrolled/observable on idx, and within the matching calipers.
future_or_noncase = cohort[
(~cohort["person_id"].isin(case_ids))
| (cohort["person_id"].map(
cases.set_index("person_id")["index_date"]).fillna(pd.Timestamp.max) > idx)
]
observable = _enrolled_observable(future_or_noncase["person_id"], idx, enroll)
pool = future_or_noncase[
(future_or_noncase["person_id"].isin(observable))
& (future_or_noncase["person_id"] != case["person_id"])
& (future_or_noncase["sex"] == case["sex"])
& ((future_or_noncase["age"] - case["age"]).abs() <= AGE_CALIPER)
]
picks = pool.sample(n=min(m, len(pool)), random_state=int(RNG.integers(1e9)))
match_id = f"set_{case['person_id']}"
rows.append({"person_id": case["person_id"], "match_id": match_id,
"is_case": 1, "index_date": idx})
for pid in picks["person_id"]:
rows.append({"person_id": pid, "match_id": match_id,
"is_case": 0, "index_date": idx}) # control inherits the case index date
return pd.DataFrame(rows)
def add_current_exposure(cc, rx, drug_class):
"""Flag 'current use': a fill whose [fill_date, fill_date+days_supply+carryover] covers index_date."""
r = rx[rx["drug_class"] == drug_class].merge(cc[["person_id", "match_id", "index_date"]],
on="person_id")
r["cov_end"] = r["fill_date"] + pd.to_timedelta(r["days_supply"] + CARRYOVER_DAYS, unit="D")
r["covers"] = (r["fill_date"] <= r["index_date"]) & (r["cov_end"] >= r["index_date"])
exposed = r.loc[r["covers"], ["person_id", "match_id"]].drop_duplicates()
exposed["exposed"] = 1
out = cc.merge(exposed, on=["person_id", "match_id"], how="left")
out["exposed"] = out["exposed"].fillna(0).astype(int)
return out
# cc = build_riskset_matched_cc(cases, cohort, enroll)
# cc = add_current_exposure(cc, rx, drug_class="NSAID")
# -> downstream: conditional logistic on `exposed` stratified by `match_id` (see R/SAS).Conditional logistic regression for the risk-set matched case-control file produced above. Input `cc` has one row per subject: person_id, match_id (the risk set), is_case (1/0), and one column per exposure/covariate measured strictly in the pre-index window.
library(survival)
library(data.table)
# --- Risk-set sampling skeleton (density sampling, m controls per case) ---------------
sample_riskset <- function(cases, cohort, enroll, m = 4L, age_caliper = 2L) {
setDT(cases); setDT(cohort); setDT(enroll)
out <- vector("list", nrow(cases))
for (i in seq_len(nrow(cases))) {
idx <- cases$index_date[i]; cid <- cases$person_id[i]
observable <- enroll[enroll_start <= idx & enroll_end >= idx & !ma_only, unique(person_id)]
pool <- cohort[person_id %in% observable & person_id != cid &
sex == cases$sex[i] & abs(age - cases$age[i]) <= age_caliper, person_id]
ctrls <- if (length(pool) > m) sample(pool, m) else pool
out[[i]] <- data.table(
person_id = c(cid, ctrls),
match_id = paste0("set_", cid),
is_case = c(1L, rep(0L, length(ctrls))),
index_date = idx)
}
rbindlist(out)
}
# --- Conditional logistic on the matched analytic file --------------------------------
# cc: person_id, match_id, is_case, exposed, plus pre-index covariates
fit <- clogit(
is_case ~ exposed + anticoagulant + prior_gi_dx + utilization_q + strata(match_id),
data = cc, method = "exact")
summary(fit) # exp(coef) of `exposed` = incidence rate ratio under density samplingSAS analysis for the matched case-control. Two genuinely correct paths are shown. (1) PROC LOGISTIC with STRATA fits the conditional likelihood for matched risk sets directly. (2) PROC PHREG with a counting-process (start,stop) setup is the equivalent route for incidence-density nested CC -- the Breslow/partial...
/* ---- Path 1: conditional logistic via STRATA (matched-sets analysis) ---- */
proc logistic data=work.cc;
strata match_id; /* one stratum per risk set */
model is_case(event='1') = exposed anticoagulant prior_gi_dx utilization_q
/ clodds=pl; /* profile-likelihood CIs for the OR (=RR) */
run;
/* ---- Path 2: equivalent incidence-density fit via PROC PHREG ----
Encode each matched risk set so all members share a (t0,t1] interval and the case has status=1.
Breslow ties + the matched strata reproduce the conditional-logistic estimate (the rate ratio). */
proc phreg data=work.cc_pheg;
strata match_id;
model (t0, t1)*is_case(0) = exposed anticoagulant prior_gi_dx utilization_q
/ ties=breslow risklimits;
run;Citations
- [1]Schulz KF, Grimes DA. Case-control studies: research in reverse. The Lancet. 2002;359(9304):431-434.
- [2]Wacholder S, McLaughlin JK, Silverman DT, Mandel JS. Selection of controls in case-control studies: I. Principles. American Journal of Epidemiology. 1992;135(9):1019-1028.
- [3]Schneeweiss S, Avorn J. A review of uses of health care utilization databases for epidemiologic research on therapeutics. Journal of Clinical Epidemiology. 2005;58(4):323-337.
- [4]Suissa S. The case-time-control design. Epidemiology. 1995;6(3):248-253.