Descriptive Epidemiology in RWE
The estimation of disease and exposure frequency in real-world data — incidence rates per unit of observed person-time, cumulative incidence (risk), prevalence, and age/sex-standardized rates — built on explicitly constructed denominators rather than on the comparative contrasts of causal analysis.
On this page
Explore this method family
Descriptive epidemiology answers three questions about a disease or condition in a population: who gets it, where it occurs, and when it happens — without asking whether any particular cause is responsible. In real-world data studies, analysts count events (such as hospitalizations or diagnoses) and divide by the total time the population was under observation to get a rate, or snapshot a moment in time to measure how common a condition currently is. A descriptive study never tests a hypothesis; it maps the terrain so future causal research knows where to look.
Descriptive epidemiology in real-world data
answers "how much, in whom, and when" rather than "does exposure cause the outcome." Its deliverables are frequency measures: the incidence rate (new events divided by accrued person-time among those at risk), cumulative incidence / risk (the proportion developing the event over a fixed horizon, properly accounting for competing events and censoring), prevalence (point, period, or annual — the proportion alive and meeting a state definition at or during a window), and standardized rates (age/sex-adjusted via direct standardization to an external standard population, or indirectly via the SMR/SIR when stratum-specific rates are unstable). It is the quantitative backbone of disease burden estimates, drug-utilization studies, natural-history characterization, label expansion support, and the "Table 1 + base rates" that every comparative RWE protocol depends on. This entry is both the working reference for these measures and the hub for the operational child concepts that handle each piece in depth (linked under Relations).
Core conceptual distinction
. Three frequency measures answer three different questions and are not interchangeable.
- The incidence rate has person-time in the denominator (events per 1,000 person-years); it is the right measure when follow-up is censored, enrollment is dynamic, or the at-risk period varies across people — the dominant situation in claims and EHR.
- Cumulative incidence (risk) is a probability bounded in [0,1] over a stated horizon; it requires either complete follow-up or a survival estimator (Kaplan–Meier complement, or the Aalen–Johansen / cumulative-incidence function when competing risks such as death are present). Naively dividing events by the closed cohort overstates risk whenever anyone is censored early.
- Prevalence is a snapshot of existing state, governed by both incidence and duration (prevalence ≈ incidence × average duration in steady state); it must never be reported as a rate or a risk. Standardization is orthogonal to all three: it removes the confounding-by-composition that makes a crude rate uninterpretable when comparing populations with different age/sex structures. Direct standardization applies study stratum-specific rates to a fixed standard population; indirect standardization (SMR/SIR) applies standard rates to the study population's person-time and is preferred when study stratum counts are too sparse for stable direct estimates. The choice of measure, horizon, competing-event handling, and standard population must be pre-specified in the estimand, not chosen after seeing the data.
Pros, cons, and trade-offs
.
- Incidence rate vs cumulative incidence: The rate is robust to censoring and dynamic enrollment and is the natural output of Poisson/person-time models, but it assumes a roughly constant hazard over the interval and is not a probability a clinician can interpret directly. Cumulative incidence is directly interpretable and is what payers and patients want, but it demands correct handling of competing risks and censoring — a 1-minus-Kaplan–Meier "risk" overestimates the event when death is common (e.g., elderly cohorts), where the cumulative incidence function is mandatory. Prefer the rate for surveillance, utilization, and when follow-up is heavily censored; prefer the CIF for fixed-horizon clinical risk communication.
- Crude vs standardized rates: Crude rates describe the actual burden in the population as it exists (correct for resource planning); standardized rates enable fair across-population or across-time comparison but describe a hypothetical population and can hide important effect modification. Report both, and never compare crude rates across populations with different age/sex mixes.
- Direct vs indirect standardization (SMR/SIR): Direct standardization yields rates comparable across any pair of populations sharing the standard, but is unstable when study strata have few events; indirect standardization (SMR/SIR) is stable with sparse data but only validly compares each study population to the standard, not study populations to each other. Prefer indirect when stratum-specific study rates are sparse or unstable.
- Descriptive vs causal framing: Descriptive measures require no exchangeability or positivity assumptions and are far less fragile than causal contrasts — but precisely because they make no causal claim, a between-group descriptive difference (e.g., higher event rate in treated patients) must not be read as a treatment effect. Confusing the two is the single most common misuse.
When NOT to use — and when it is actively misleading or dangerous
.
- Do not present descriptive between-group differences as causal. A crude rate ratio between exposed and unexposed groups carries every confounder; reporting it without the causal caveats invites readers to infer effects the design cannot support.
- Do not compute risk by dividing events by the baseline cohort when follow-up is censored. This understates the denominator-at-risk and, in the presence of competing risks, the 1−KM complement overstates the event probability — use person-time rates or the CIF.
- Do not compare crude rates across populations with different age/sex structure — composition, not biology, can drive the entire difference. Standardize first.
- Do not report prevalence as incidence or risk. A point-prevalence "rate" conflates frequency with duration; chronic, low-mortality conditions look common on prevalence and rare on incidence.
- Do not estimate rates on person-time you cannot observe. Counting events while undercounting denominator (or vice versa) is the dominant failure mode in claims (see below) and can move a rate by an order of magnitude.
Data-source operational depth
.
- Claims (FFS): Denominator = sum of continuously enrolled, observable person-time with the relevant benefit (Parts A/B for medical events, Part D for drug events); numerator = first qualifying event coded in the claim stream during that observed time. Failure modes: (1) Medicare Advantage gaps — MA-only enrollment does not generate FFS encounter claims, so person-time accrued under MA must be excluded from the denominator or events will be missed while time is counted, deflating the rate; restrict to A/B(/D)-eligible FFS person-time. (2) Differential competing risks by exposure in the elderly — death competes with the event of interest and is captured incompletely in claims (use a death-index linkage); if one group dies faster, naive risk estimates diverge from the CIF. (3) Prevalent vs incident counting — without a washout/clean lookback, a recurrent or chronic condition's first observed code is mistaken for a first ever event, inflating incidence at enrollment ("look-back ramp"). (4) Immortal time in procedure/initiation studies — defining the at-risk start after a procedure that itself requires survival builds guaranteed event-free time into the denominator. Workarounds: require continuous enrollment + washout before time zero, anchor the denominator to observable benefit windows, and link to a mortality source to handle the competing risk.
- EHR: Capture is visit-driven and within-system. Denominator-at-risk is ambiguous — a patient who stops visiting is not necessarily event-free, just unobserved — so rates computed on "all patients with a record" are biased by informal disenrollment. Define an explicit observation period (e.g., activity-anchored windows) and treat care delivered outside the network as missing, not absent. Phenotype the numerator with a validated algorithm; raw single-code numerators overcount.
- Registry: Strong for adjudicated numerators (validated incident cases, cancer stage) but the denominator (catchment person-time, eligibility) is often the weak link; link to claims or census person-time for valid rates, and verify completeness/ascertainment before reporting incidence.
- Linked claims–EHR–vital-records: The ideal substrate — EHR/registry for numerator validity, claims for complete observable person-time, vital records for the competing-risk denominator — but the rate must be computed on the linkable subset, which is a selected population; report the linkage proportion and assess selection before generalizing.
Worked example (claims)
Question: the 2022 annual incidence rate of hospitalized acute myocardial infarction (AMI) among adults age ≥65 in U.S. Medicare fee-for-service.
- Denominator population: enrollees with continuous Parts A+B FFS enrollment (no MA-only months) at any point in 2022.
- Person-time: for each person, accrue observed days from the later of 2022-01-01 or FFS-enrollment start to the earliest of first AMI, death, switch to MA, disenrollment, or 2022-12-31; sum observed person-days and divide by 365.25 to get person-years.
- Numerator (incident, not prevalent): first inpatient claim with ICD-10-CM I21.x in the primary position and a 12-month clean lookback with continuous enrollment and no prior I21.x/I22.x, so a reinfarction or a chronic-history code is not counted as incident.
- Rate = events ÷ person-years × 1,000, with an exact-Poisson 95% CI.
- Because crude rates are not comparable across regions or years with different age/sex mixes, also compute the age-/sex-standardized rate by direct standardization to the 2000 U.S. standard million: estimate stratum-specific rates within 5-year age × sex cells, weight by the standard population proportions, and sum — yielding a single adjusted rate per 1,000 person-years whose CI uses a normal-approximation (Wald) interval on the Dobson stratum-variance estimator. Report crude and standardized rates side by side, the person-time accounting, and the attrition funnel (eligible → continuously enrolled → clean-lookback → contributing person-time).
Interpreting the output
A Medicare FFS descriptive study of hospitalized AMI reports age-group-specific incidence rates of 2.00 per 1,000 person-years (age 65–74), 4.50 per 1,000 person-years (age 75–84), and 5.20 per 1,000 person-years (age 85+), alongside a directly age-/sex-standardized rate for cross-year or cross-region comparison.
(1) Formal interpretation. Each rate is the count of incident AMI hospitalizations per 1,000 person- years of FFS-enrolled observation time in that age stratum, with a clean 12-month lookback to exclude prevalent AMI. The denominator accrues person-days from FFS enrollment start (or January 1) to the first of: AMI event, death, MA switch, disenrollment, or December 31, then converts to person-years (days / 365.25). Rates are not risks: they are not bounded at 1, they are expressed per 1,000 person- years of observed time, and they should not be compared to study-period cumulative incidence figures. The standardized rate removes the confounding effect of age composition when comparing populations with different age structures; the crude rates are required alongside it to show the actual distribution of events.
(2) Practical interpretation. The 2.6-fold gradient from the 65–74 stratum (2.00) to the 85+ stratum (5.20) quantifies the age dependency of AMI incidence in this population and provides the baseline rate inputs needed to size a prevention trial, project budget impact, or calculate an externally transported NNT anchored to a local baseline. These are descriptive measures only — the rates do not estimate a causal effect of age on AMI, and no confounding adjustment is implied.
Decision diagram
flowchart TD
Q[Descriptive frequency question] --> K{What is being counted?}
K -->|New events over varying follow-up| RATE[Incidence RATE<br/>events / person-time]
K -->|Probability over a fixed horizon| RISK{Competing risks present?}
K -->|Existing state at a time/window| PREV[PREVALENCE<br/>point / period / annual]
RISK -->|Yes e.g. death| CIF[Cumulative incidence function<br/>Aalen-Johansen]
RISK -->|No| KM[1 - Kaplan-Meier]
RATE --> CMP{Compare across populations or time?}
CMP -->|Yes| STD{Study strata sparse?}
CMP -->|No| CRUDE[Report crude rate + exact-Poisson CI]
STD -->|No| DIR[Direct standardization to a standard population]
STD -->|Yes| IND[Indirect standardization SMR / SIR]flowchart LR Enr[Continuous FFS enrollment<br/>relevant benefit, no MA-only months] --> Wash[Clean lookback / washout<br/>so first observed = first incident] Wash --> PT[Accrue observable person-time<br/>start -> first event / death / disenroll / data end] PT --> Num[Numerator: first qualifying coded<br/>incident event in observed time] Num --> Rate[Crude rate = events / person-years] Rate --> Adj[Standardize by age x sex<br/>to external standard population] Adj --> Out[Report crude + standardized rates,<br/>person-time accounting, attrition funnel]
Worked example
Scenario
A health economist wants to describe the burden of acute heart attacks (acute myocardial infarction, AMI) among US Medicare fee-for-service enrollees in 2022, broken down by age group. She is not comparing a drug or intervention — she just wants to know how many heart attacks occur in each age band per year of follow-up. She pulls three summary rows from a claims analysis: the number of first-ever AMI hospitalizations and the total person-years of observation in each group.
Dataset
Summary of incident AMI hospitalizations and observed follow-up by age group, US Medicare fee-for-service 2022.
| age_group | incident_ami_events | observed_person_years | rate_per_1000_py |
|---|---|---|---|
| 65-74 | 8200 | 4100000 | 2.0 |
| 75-84 | 14175 | 3150000 | 4.5 |
| 85+ | 9100 | 1750000 | 5.2 |
Steps
Result
Rates per 1,000 person-years: 65-74 = 2.00, 75-84 = 4.50, 85+ = 5.20. AMI incidence more than doubles from the youngest to the oldest age band. This is a descriptive finding — age is associated with higher rates, but this table alone cannot say why.
Trade-offs
Runnable example
Crude and age/sex-standardized incidence rate from claims-style inputs. Required tables (already cleaned): person_time : person_id, age_band, sex, py (observed person-YEARS at risk after enrollment/washout rules) events : person_id, age_band, sex (one row per INCIDENT event during the observed at-risk window)...
import pandas as pd
import numpy as np
from scipy.stats import chi2
SCALE = 1000.0 # report per 1,000 person-years
def standardized_rate(person_time: pd.DataFrame, events: pd.DataFrame,
standard: pd.DataFrame) -> dict:
keys = ["age_band", "sex"]
pt = person_time.groupby(keys, as_index=False)["py"].sum()
ev = events.groupby(keys, as_index=False).size().rename(columns={"size": "events"})
strata = pt.merge(ev, on=keys, how="left").fillna({"events": 0})
strata["rate"] = strata["events"] / strata["py"] # stratum-specific rate
# Crude rate with exact-Poisson 95% CI on the total event count.
tot_ev, tot_py = strata["events"].sum(), strata["py"].sum()
crude = tot_ev / tot_py
lo = chi2.ppf(0.025, 2 * tot_ev) / 2 / tot_py if tot_ev > 0 else 0.0
hi = chi2.ppf(0.975, 2 * (tot_ev + 1)) / 2 / tot_py
# Direct standardization: weight stratum rates by the external standard population.
s = strata.merge(standard, on=keys, how="inner")
std_rate = float(np.sum(s["std_weight"] * s["rate"]))
# Variance of the directly standardized rate (Dobson-style on stratum events).
var = float(np.sum((s["std_weight"] ** 2) * s["events"] / s["py"] ** 2))
se = np.sqrt(var)
return {
"crude_per_1000": crude * SCALE,
"crude_ci": (lo * SCALE, hi * SCALE),
"standardized_per_1000": std_rate * SCALE,
"standardized_ci": ((std_rate - 1.96 * se) * SCALE,
(std_rate + 1.96 * se) * SCALE),
}Crude and directly-standardized incidence rate with data.table. Inputs mirror the Python version: person_time : person_id, age_band, sex, py (observed person-years after enrollment/washout rules) events : person_id, age_band, sex (one row per incident event in the observed window) standard : age_band, sex,...
library(data.table)
SCALE <- 1000
standardized_rate <- function(person_time, events, standard) {
setDT(person_time); setDT(events); setDT(standard)
pt <- person_time[, .(py = sum(py)), by = .(age_band, sex)]
ev <- events[, .(events = .N), by = .(age_band, sex)]
strata <- merge(pt, ev, by = c("age_band", "sex"), all.x = TRUE)
strata[is.na(events), events := 0]
strata[, rate := events / py] # stratum-specific rate
tot_ev <- sum(strata$events); tot_py <- sum(strata$py)
crude <- tot_ev / tot_py
# Exact-Poisson 95% CI on the total count.
lo <- if (tot_ev > 0) qchisq(0.025, 2 * tot_ev) / 2 / tot_py else 0
hi <- qchisq(0.975, 2 * (tot_ev + 1)) / 2 / tot_py
s <- merge(strata, standard, by = c("age_band", "sex"))
std_rate <- sum(s$std_weight * s$rate) # direct standardization
v <- sum(s$std_weight^2 * s$events / s$py^2) # variance (Dobson-style)
se <- sqrt(v)
list(crude_per_1000 = crude * SCALE,
crude_ci = c(lo, hi) * SCALE,
standardized_per_1000 = std_rate * SCALE,
standardized_ci = c(std_rate - 1.96 * se, std_rate + 1.96 * se) * SCALE)
}Direct and indirect (SMR) age/sex standardization with PROC STDRATE, plus a person-time Poisson rate model. Required input datasets (post data-management): work.events_pt : age_band, sex, events (incident events), py (observed person-years at risk) built from continuously enrolled FFS person-time with a clean lookback;
/* Directly standardized incidence rate per 1,000 person-years to an external standard population. */
proc stdrate data=work.events_pt
refdata=work.standard
method=direct
stat=rate(mult=1000)
plots=none;
population event=events pyears=py; /* study stratum events and person-time */
reference total=std_pop; /* standard population weights by stratum */
strata age_band sex / stats; /* age x sex strata + stratum-specific rates */
run;
/* Indirect standardization (SMR): observed vs expected using standard stratum rates. */
proc stdrate data=work.events_pt
refdata=work.standard
method=indirect
stat=rate(mult=1000)
plots=none;
population event=events pyears=py;
reference event=std_events pyears=std_pop; /* standard events + person-time -> reference rates */
strata age_band sex;
run;
/* Person-time rate model: log-linear Poisson with an offset = log(person-years). */
data work.model; set work.events_pt; log_py = log(py); run;
proc genmod data=work.model;
class age_band sex / param=ref;
model events = age_band sex / dist=poisson link=log offset=log_py;
estimate 'rate per 1000 PY (ref stratum)' intercept 1 / exp; /* exp(beta) = rate */
run;Citations
- [1]von Elm E, Altman DG, Egger M, Pocock SJ, Gøtzsche PC, Vandenbroucke JP. The Strengthening the Reporting of Observational Studies in Epidemiology (STROBE) statement. PLoS Medicine. 2007;4(10):e296.
- [2]Benchimol EI, Smeeth L, Guttmann A, et al. The REporting of studies Conducted using Observational Routinely-collected health Data (RECORD) statement. PLoS Medicine. 2015;12(10):e1001885.
- [3]Langan SM, Schmidt SA, Wing K, et al. The reporting of studies conducted using observational routinely collected health data statement for pharmacoepidemiology (RECORD-PE). BMJ. 2018;363:k3532.