← Methods repository
CONCEPTFOUNDATIONALPYTHON · R · SAS5 citations

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.

Study Designcase-controlodds-ratioincidence-density-samplingrisk-set-matchingconditional-logistic-regressionpharmacoepidemiologyrare-outcomes
On this page
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

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.

When to use it
Rare outcomes, or when costly outcome/exposure measurement is the binding constraint.
When the outcome is rare and primary or costly ascertainment on cases/controls is the bottleneck.
Use nested CC when a claims/EHR/registry source cohort exists; reserve stand-alone CC for mixed-source settings without one.
Watch out for
Cannot estimate absolute risk, incidence, or NNT without known sampling fractions; more exposed to control- selection bias; less intuitive to clinical stakeholders.
A retrospective cohort can yield incidence and absolute risk from the full source population if data are complete; CC must mimic that base through careful sampling.
Nested CC inherits a clean source population, time-zero, and density-sampling validity and is the modern default whenever a usable cohort exists.

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?

  1. 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.
  2. 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).
  3. 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).
  4. 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.
  5. Covariates: anticoagulant/antiplatelet/PPI use, prior GI diagnoses, and baseline visit count (utilization, to control detection bias), all measured strictly before index_date.
  6. 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]
Operational flow for an incidence-density (nested) matched case-control in real-world data. Risk-set sampling on the case index date is what makes the odds ratio equal the rate ratio; exposure and covariates are measured only before index.
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] --> B
The two selection threats that define case-control validity and their fixes. Berkson-type bias from off-base controls cannot be repaired analytically; detection bias is mitigated by matching or adjusting on utilization.

Worked 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.

CasesControls
Exposed (took NSAID)a = 90b = 60
Unexposed (no NSAID)c = 10d = 40

Steps

1Sort each person into one of four boxes: a = exposed cases (90), b = exposed controls (60), c = unexposed cases (10), d = unexposed controls (40).
2The odds of having been exposed among cases is a / c = 90 / 10 = 9.
3The odds of having been exposed among controls is b / d = 60 / 40 = 1.5.
4The odds ratio compares those two odds: OR = (a / c) / (b / d), which rearranges to the cross-product OR = (a * d) / (b * c).
5Plug in the cells: OR = (90 * 40) / (60 * 10) = 3600 / 600 = 6.0.

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

Pros of this
Orders-of-magnitude less data collection for rare outcomes; concentrates expensive ascertainment (adjudication, assays) on cases plus a small control sample.
Pros of this
Same efficiency advantage in existing databases; easy to add bespoke ascertainment to a small case+control set.
Pros of this
Stand-alone CC can be built from a case registry plus controls sampled from eligibility files when no source cohort is available.
Pros of this
Handles one-time/chronic exposures and fatal or non-recurrent outcomes; permits study of between-person factors (genetics, frailty) via matching/measurement.
Cons of this
Must measure and adjust for time-invariant confounders that SCCS removes by design; vulnerable to control- selection and detection bias that within-person designs avoid.
When to prefer Non-recurrent or fatal outcomes, fixed/chronic exposures, or when between-person comparison is the scientific goal.
Pros of this
This entry supplies the design and sampling logic; the algorithm entry supplies the case-definition validation (PPV, sensitivity) on which CC validity depends.
Cons of this
Case-algorithm validation is a prerequisite — low PPV biases the OR toward the null regardless of design quality.
When to prefer Use both: validate the outcome algorithm first (or in a substudy), then apply it as the case definition in the CC.

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...

requires: pandas · numpy
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).

Citations

FOUNDATIONAL / METHODS
  1. [1]Schulz KF, Grimes DA. Case-control studies: research in reverse. The Lancet. 2002;359(9304):431-434.
  2. [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. [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. [4]Suissa S. The case-time-control design. Epidemiology. 1995;6(3):248-253.
APPLIED EXAMPLES
  1. [5]Filion KB, Azoulay L, Platt RW, et al. A multicenter observational study of incretin-based drugs and heart failure. New England Journal of Medicine. 2016;374(12):1145-1154.