Scope. This tutorial is an educational introduction, not a substitute for a protocol, statistical analysis plan, ethics review, or consultation with a biostatistician/epidemiologist. Examples are simplified so that the central ideas remain visible.
Epidemiology studies the distribution and determinants of health-related states or events in specified populations, and applies that knowledge to improve population health. It connects clinical questions, public health action, causal reasoning, measurement, study design, and statistical analysis.
This document is designed for three kinds of use:
All examples use base R, the stats package, or datasets
distributed with R. No internet connection or external data file is
required after the HTML is rendered.
After working through the tutorial, you should be able to:
Descriptive epidemiology begins by asking:
Description is not merely preliminary. It can identify inequities, reveal data quality problems, generate hypotheses, and guide urgent action.
The target population is the population to which the investigators want the answer to apply. The source population gives rise to the observed participants. The sample is the set actually measured. These may differ because of eligibility, access, consent, loss to follow-up, or incomplete records.
The unit may be a person, pregnancy, hospitalization, household, school, community, or person-time interval. Always state the unit. Treating repeated events from one person as if they were independent people can make uncertainty appear too small.
The same variable can play different roles in different questions. Blood pressure could be an exposure, an outcome, a mediator, or a confounder. Its role comes from the scientific question and causal structure, not from its column name.
These goals require different interpretations:
| Goal | Example | Main concern |
|---|---|---|
| Descriptive | What was diabetes prevalence in adults in 2025? | Representative measurement |
| Predictive | Who will be hospitalized in the next 30 days? | Out-of-sample accuracy and calibration |
| Causal | Would vaccination reduce hospitalization compared with no vaccination? | A valid counterfactual comparison |
An excellent prediction model does not automatically identify a causal effect, and a causal model need not predict each individual’s outcome well.
Every measure should specify its numerator, denominator, population, place, and time period. “The rate was 12%” is usually imprecise language: 12% is a proportion unless time is explicitly part of the denominator.
Point prevalence is the proportion of a population with a condition at a specified time:
Period prevalence covers an interval rather than a single instant. Prevalence depends on both incidence and duration. It may rise because more new cases occur, survival improves, recovery slows, or case ascertainment improves.
Risk is the proportion of initially outcome-free people who develop the outcome during a stated interval:
Risk ranges from 0 to 1 and must include a time horizon: for example, “5-year risk.” The simple formula assumes sufficiently complete follow-up. With censoring and unequal follow-up, survival-analysis methods are generally needed.
The incidence rate uses person-time at risk:
Its units might be cases per 1,000 person-years. A person contributes time until the event, loss to follow-up, death, or study end, according to the protocol. Unlike risk, the rate is not a probability and can exceed 1 per chosen unit of time.
population <- 5000
existing_cases_day_1 <- 350
new_cases_one_year <- 90
initially_at_risk <- population - existing_cases_day_1
person_years <- 4380
point_prevalence <- existing_cases_day_1 / population
one_year_risk <- new_cases_one_year / initially_at_risk
incidence_rate <- new_cases_one_year / person_years
c(
point_prevalence = point_prevalence,
one_year_risk = one_year_risk,
cases_per_1000_person_years = incidence_rate * 1000
)## point_prevalence one_year_risk cases_per_1000_person_years
## 0.07000 0.01935 20.54795
Interpretation:
Case fatality describes severity among cases; mortality also depends on how often the disease occurs in the population.
A crude measure combines the entire population. A stratum-specific measure is calculated within a category such as age group. Standardization creates a weighted summary that improves comparability when populations have different compositions.
In direct standardization, apply each study population’s stratum-specific rates to one common standard distribution.
standardization <- data.frame(
age_group = c("0-39", "40-64", "65+"),
standard_population = c(50000, 30000, 20000),
rate_region_A = c(0.001, 0.006, 0.030),
rate_region_B = c(0.0015, 0.005, 0.024)
)
standardization$expected_A <- with(
standardization, standard_population * rate_region_A
)
standardization$expected_B <- with(
standardization, standard_population * rate_region_B
)
age_standardized_A <- sum(standardization$expected_A) /
sum(standardization$standard_population)
age_standardized_B <- sum(standardization$expected_B) /
sum(standardization$standard_population)
standardization## region_A_per_1000 region_B_per_1000
## 8.30 7.05
Standardized rates are comparison summaries, not necessarily the observed rate in either population. Always report the standard population and method.
For a binary exposure and outcome:
| Outcome + | Outcome - | Total | |
|---|---|---|---|
| Exposed | a | b | a + b |
| Unexposed | c | d | c + d |
The exposed risk is a / (a + b) and unexposed risk is
c / (c + d).
epi_2x2 <- function(tab) {
stopifnot(all(dim(tab) == c(2, 2)), all(tab >= 0), all(rowSums(tab) > 0))
a <- as.numeric(tab[1, 1]); b <- as.numeric(tab[1, 2])
c <- as.numeric(tab[2, 1]); d <- as.numeric(tab[2, 2])
p1 <- a / (a + b)
p0 <- c / (c + d)
safe_ratio <- function(num, den) {
if (den == 0) {
if (num == 0) NA_real_ else Inf
} else {
num / den
}
}
rr <- safe_ratio(p1, p0)
or <- safe_ratio(a * d, b * c)
rd <- p1 - p0
z <- qnorm(0.975)
se_rd <- sqrt(p1 * (1 - p1) / (a + b) + p0 * (1 - p0) / (c + d))
rr_ci <- or_ci <- c(NA_real_, NA_real_)
if (a > 0 && c > 0 && is.finite(rr) && rr > 0) {
se_log_rr <- sqrt(1 / a - 1 / (a + b) + 1 / c - 1 / (c + d))
rr_ci <- exp(log(rr) + c(-1, 1) * z * se_log_rr)
}
if (all(c(a, b, c, d) > 0) && is.finite(or)) {
se_log_or <- sqrt(1 / a + 1 / b + 1 / c + 1 / d)
or_ci <- exp(log(or) + c(-1, 1) * z * se_log_or)
}
data.frame(
measure = c("Risk exposed", "Risk unexposed", "Risk ratio",
"Odds ratio", "Risk difference"),
estimate = c(p1, p0, rr, or, rd),
lower_95 = c(NA, NA,
rr_ci[1],
or_ci[1],
rd - z * se_rd),
upper_95 = c(NA, NA,
rr_ci[2],
or_ci[2],
rd + z * se_rd),
row.names = NULL
)
}
tab <- matrix(c(80, 920,
40, 960),
nrow = 2, byrow = TRUE,
dimnames = list(
exposure = c("Exposed", "Unexposed"),
outcome = c("Case", "Non-case")
))
tab## outcome
## exposure Case Non-case
## Exposed 80 920
## Unexposed 40 960
This teaching function reports a point estimate whenever it is
mathematically defined. With zero cells, log-Wald intervals for RR or OR
are returned as NA; exact, penalized, or other
purpose-specific methods may be preferable in real small samples.
The risk ratio (RR) compares risks:
The rate ratio compares incidence rates. The odds ratio (OR) compares odds:
The OR estimates the incidence rate ratio in incidence-density sampled case-control studies, and can approximate the risk ratio when the outcome is rare under suitable sampling. When the outcome is common, an OR can be much farther from 1 than the RR and should not be described as “times the risk.”
The risk difference (RD) is:
Absolute measures are crucial for decisions. A relative reduction of 50% could mean a drop from 40% to 20% (20 percentage points) or from 0.02% to 0.01% (0.01 percentage points).
At a stated follow-up horizon, when a causal interpretation is justified:
1 / absolute risk reduction.1 / absolute risk increase.(RR - 1) / RR for a harmful exposure.NNT and NNH are conventionally rounded up to the next whole person, while the unrounded value may be retained for transparent calculation.
Using the example table, the risk difference is 4 percentage points, corresponding to an NNH of 25 if the association were truly causal and transportable.
A confidence interval describes the range generated by a procedure that would cover the true parameter at the stated frequency under repeated compatible sampling and model assumptions. It is not the probability that this particular interval contains the parameter.
A p-value measures compatibility between the observed data and a specified null model. It does not measure the probability that the null hypothesis is true, effect size, clinical importance, study quality, or absence of bias.
Report the estimate, confidence interval, units, model, and
assumptions. Avoid turning continuous evidence into a binary conclusion
at p = 0.05.
Effect modification can be present on one scale but not another. For example, a treatment may reduce risk by 10 percentage points in two subgroups (constant RD) while the subgroup RRs differ because their baseline risks differ. State the scale before claiming interaction.
| Design | How participants are selected | Typical measure | Strength | Main limitation |
|---|---|---|---|---|
| Case report/series | By unusual outcome or clinical observation | Counts/descriptions | Early signal | No comparison group |
| Ecologic | Groups or populations | Group-level correlation/rate ratio | Policy/context questions | Ecologic fallacy, limited individual control |
| Cross-sectional | Sample at one time/period | Prevalence, prevalence ratio | Burden estimation | Temporal order often unclear |
| Case-control | By outcome status | Odds ratio | Efficient for rare outcomes/long latency | Recall and selection mechanisms |
| Cohort | By exposure/eligibility, then outcome follow-up | Risk/rate ratio, risk difference | Establishes exposure before incident outcome | Time, cost, loss to follow-up |
| Randomized trial | Random assignment to intervention | Risk/rate differences and ratios | Balances causes in expectation | Ethics, adherence, generalizability |
| Quasi-experimental | Exposure assigned by policy/time/threshold | Design-specific contrast | Evaluates natural/policy interventions | Strong design assumptions |
Cross-sectional studies are well suited to estimating burden and describing service needs. Because exposure and outcome are often measured together, it may be unclear whether the exposure preceded the outcome. Prevalence can also preferentially include long-duration survivors (prevalence-incidence bias).
Investigators sample cases with the outcome and controls representing the exposure distribution in the population that produced those cases. Validity depends less on a fixed case-to-control ratio than on correct control selection.
Important variants include:
Matching improves efficiency or controls design variables, but matched factors must be handled appropriately in analysis. Matching does not by itself eliminate confounding.
Prospective cohorts follow participants forward from measurement. Retrospective cohorts reconstruct exposure and follow-up from existing records. Both require a clear time zero, eligibility rule, exposure strategy, follow-up definition, and outcome ascertainment.
A common error is immortal time bias: a period during which a participant must remain event-free to become classified as exposed is incorrectly counted as exposed time. Align eligibility, treatment assignment, and start of follow-up.
Randomization supports exchangeability in expectation, allocation concealment protects the assignment process, and blinding may reduce differential behavior or measurement.
Report harms as well as benefits, participant flow, missing outcomes, protocol changes, and prespecified versus exploratory analyses.
Interrupted time series, difference-in-differences, regression discontinuity, and instrumental-variable designs can answer questions when randomization is unavailable. Each substitutes a specific assumption for random assignment—for example, parallel trends for difference-in-differences or continuity around a threshold for regression discontinuity. These assumptions should be explained and probed, not hidden behind a model name.
Ask in order:
Random error creates imprecision and generally decreases with more information. Systematic error (bias) shifts estimates because of study design, conduct, measurement, or analysis. A very large biased study can produce a very precise wrong answer.
Selection bias occurs when inclusion, participation, retention, or analysis depends on variables in a way that distorts the target comparison. Examples include:
Prevention starts with source-population definition, inclusive recruitment, and active follow-up. Analysis may use standardization, inverse-probability weighting, or sensitivity analysis, but only with defensible measured predictors of selection.
Information bias arises from inaccurate measurement. Examples include recall bias, interviewer bias, diagnostic suspicion, imperfect coding, and instrument drift.
Nondifferential misclassification of a binary exposure often—but not always—biases a ratio toward the null. The direction is not guaranteed for multicategory variables, dependent errors, or complex analyses.
Improve measurement with validated definitions, standardized training, calibration, blinded outcome assessment where feasible, repeated measurements, and adjudication.
Confounding mixes the exposure effect with differences in causes of the outcome between exposure groups. A classical confounder:
These rules are helpful but causal knowledge is more reliable than significance tests or automatic change-in-estimate procedures. Do not adjust for every available variable: mediators, colliders, and poorly measured proxies can increase bias.
Design approaches include randomization, restriction, and matching. Analysis approaches include stratification, standardization, regression adjustment, matching/weighting by a propensity score, and inverse-probability weighting.
Suppose age is a potential confounder. Compare exposure and outcome within age strata before producing a summary.
# Each 2 x 2 table uses rows = exposed/unexposed and columns = case/non-case.
young <- matrix(c(10, 90,
25, 475), nrow = 2, byrow = TRUE)
older <- matrix(c(250, 250,
33, 67), nrow = 2, byrow = TRUE)
strata <- list(younger = young, older = older)
stratum_or <- vapply(strata, function(x) {
(x[1, 1] * x[2, 2]) / (x[1, 2] * x[2, 1])
}, numeric(1))
mh_num <- sum(vapply(strata, function(x) {
x[1, 1] * x[2, 2] / sum(x)
}, numeric(1)))
mh_den <- sum(vapply(strata, function(x) {
x[1, 2] * x[2, 1] / sum(x)
}, numeric(1)))
crude <- Reduce(`+`, strata)
crude_or <- (crude[1, 1] * crude[2, 2]) / (crude[1, 2] * crude[2, 1])
c(crude_or = crude_or, stratum_or,
mantel_haenszel_or = mh_num / mh_den)## crude_or younger older mantel_haenszel_or
## 7.146 2.111 2.030 2.048
The stratum-specific ORs are both close to 2, and the Mantel-Haenszel summary is also about 2, whereas the crude OR is much larger because exposed participants are disproportionately older. This is a deliberately strong confounding example. A pooled summary is meaningful only when stratum-specific effects are sufficiently compatible for the scientific purpose; otherwise, report the variation rather than hiding it.
Effect modification means the effect differs across levels of another
variable. It is not a nuisance to “control away”; it can be the central
finding. Prespecify scientifically important modifiers, report
subgroup-specific estimates and uncertainty, and avoid claiming subgroup
differences merely because one subgroup has p < 0.05 and
another does not. Test or estimate the difference between
effects.
For one person, imagine the outcome under exposure and the outcome under no exposure. Their difference is an individual causal effect, but only one can be observed. Studies therefore compare groups and need a design/analysis that makes the observed comparison stand in for the missing counterfactual.
Common identifying conditions include:
These are scientific assumptions, not properties that software can verify completely.
A DAG encodes assumed causal relationships:
Confounder C ---> Exposure E
Confounder C ---> Outcome Y
Exposure E ---> Outcome Y
Exposure E ---> Mediator M ---> Outcome Y
The edge list states unambiguously that C is a common
cause and that M lies on one causal pathway from
E to Y. Use arrows to state assumptions before
choosing covariates. Adjust for common causes of exposure and outcome to
block backdoor paths. Do not routinely adjust for mediators or
colliders. A DAG cannot tell you whether an omitted arrow is truly
absent; its value is making assumptions explicit and debatable.
Even for observational data, specify the hypothetical trial:
| Component | Question to specify |
|---|---|
| Eligibility | Who would enter, and when? |
| Strategies | What exactly are the exposure/intervention alternatives? |
| Assignment | How is assignment emulated, and which baseline confounders must be controlled? |
| Time zero | When do eligibility, assignment, and follow-up begin? |
| Outcome | What definition and ascertainment window are used? |
| Causal contrast | Intention-to-treat-like or per-protocol-like? |
| Analysis | How are censoring, competing events, and confounding handled? |
This prevents common errors such as using future information to define baseline exposure or giving one group guaranteed event-free time.
Mediation asks how much of an effect operates through a specified pathway. Direct and indirect effects require assumptions beyond total-effect estimation, including control of mediator-outcome confounding and careful definition when exposure affects that confounding.
A competing event prevents the outcome of interest (for example, death before dementia diagnosis). Whether to estimate a cause-specific hazard, cumulative incidence, or a hypothetical risk if the competing event were eliminated depends on the research question. Treating all competing events as ordinary independent censoring is often misleading.
Against a reference standard:
| Disease + | Disease - | |
|---|---|---|
| Test + | True positive (TP) | False positive (FP) |
| Test - | False negative (FN) | True negative (TN) |
TP / (TP + FN)TN / (FP + TN)TP / (TP + FP)TN / (FN + TN)sensitivity / (1 - specificity)(1 - sensitivity) / specificitydiagnostic_metrics <- function(tp, fp, fn, tn) {
sensitivity <- tp / (tp + fn)
specificity <- tn / (fp + tn)
ppv <- tp / (tp + fp)
npv <- tn / (fn + tn)
accuracy <- (tp + tn) / (tp + fp + fn + tn)
c(
sensitivity = sensitivity,
specificity = specificity,
PPV = ppv,
NPV = npv,
LR_positive = sensitivity / (1 - specificity),
LR_negative = (1 - sensitivity) / specificity,
accuracy = accuracy
)
}
diagnostic_metrics(tp = 85, fp = 90, fn = 15, tn = 810)## sensitivity specificity PPV NPV LR_positive LR_negative accuracy
## 0.8500 0.9000 0.4857 0.9818 8.5000 0.1667 0.8950
For fixed sensitivity and specificity, PPV increases as pretest probability/prevalence increases, while NPV generally decreases.
sens <- 0.90
spec <- 0.95
prevalence <- seq(0.001, 0.50, length.out = 300)
ppv <- sens * prevalence /
(sens * prevalence + (1 - spec) * (1 - prevalence))
npv <- spec * (1 - prevalence) /
((1 - sens) * prevalence + spec * (1 - prevalence))
plot(prevalence, ppv, type = "l", lwd = 2, col = "#b2182b",
ylim = c(0, 1), xlab = "Prevalence / pretest probability",
ylab = "Predictive value")
lines(prevalence, npv, lwd = 2, col = "#2166ac")
legend("right", c("PPV", "NPV"), lwd = 2,
col = c("#b2182b", "#2166ac"), bty = "n")This is why a test that performs well in a specialty clinic can yield many false positives in low-risk population screening.
Lowering a continuous test threshold usually increases sensitivity and decreases specificity. A receiver operating characteristic (ROC) curve shows sensitivity versus 1-specificity across thresholds. The area under the curve measures discrimination, not calibration or clinical usefulness.
Choose thresholds using consequences: missed disease, false positives, treatment harms, costs, capacity, equity, and patient preferences. A screening program should also have an important detectable condition, an acceptable test, effective early management, a pathway for follow-up, and evidence that benefits exceed harms.
Accuracy can vary with disease severity, comorbidity, age, and setting (spectrum effects). If only test-positive people receive the reference standard, verification bias can inflate apparent accuracy. Evaluate tests in the intended-use population and report indeterminate results rather than silently excluding them.
Outbreak investigation combines rapid action with disciplined inference. Steps may overlap:
Reporting delays and active case finding also shape the curve, so pattern recognition must be combined with contextual evidence.
An attack rate is a cumulative incidence proportion over an outbreak period.
# 80 of 200 people who ate a food became ill; 20 of 200 non-eaters became ill.
food_table <- matrix(c(80, 120,
20, 180),
nrow = 2, byrow = TRUE,
dimnames = list(
food = c("Ate food", "Did not eat"),
outcome = c("Ill", "Not ill")
))
food_table## outcome
## food Ill Not ill
## Ate food 80 120
## Did not eat 20 180
The exposed attack rate is 40%, the unexposed attack rate is 10%, the RR is 4, and the risk difference is 30 percentage points. Food histories, incubation periods, dose, other menu items, food handling, laboratory findings, and selection into the event still matter for causal interpretation.
The basic reproduction number, R0, is the expected
number of secondary infections from a typical infectious person in a
wholly susceptible population under specified conditions. The effective
reproduction number, Rt, changes with immunity, behavior,
interventions, contact patterns, and pathogen characteristics. It is
inferred from data using a generation/serial interval distribution and
is not an immutable pathogen constant.
Structure an etiologic question with Population, Exposure (or Intervention), Comparator, Outcome, and a time horizon.
Weak question:
Does air pollution affect health?
More answerable question:
Among adults aged 65 years or older living in Calgary during 2022–2026, what is the 7-day change in cardiovascular hospitalization risk associated with a 10 microgram/m3 increase in daily PM2.5, compared with lower exposure days?
The refined question identifies population, contrast, outcome, exposure window, and outcome window. It still needs a causal estimand, exposure model, confounder plan, seasonality control, and strategy for repeated daily observations.
An estimand precisely describes what is being estimated. Examples include:
1 - RR under
a justified design;Specify population, treatment/exposure strategies, outcome, time horizon, summary measure, and handling of intercurrent events.
A protocol should prespecify:
Common questions concern transmission, reservoirs, incubation, vaccine effectiveness, variant dynamics, antimicrobial resistance, contact networks, and control strategies.
Examples:
Important issues include dependent transmission, time-varying immunity, test-seeking, under-ascertainment, and rapidly changing interventions.
Research spans cardiovascular disease, cancer, diabetes, respiratory disease, mental health, and multimorbidity. Long latency, changing exposure, survival, competing risks, and repeated measures are common.
Examples:
Topics include air pollution, heat, water contamination, noise, chemicals, radiation, physical workload, shift work, and workplace injury. Exposure assignment, mixtures, spatial correlation, time trends, healthy-worker bias, and policy confounding are key.
Examples:
Common questions address real-world benefits and harms of medications and devices. Recurring challenges include confounding by indication, contraindication, new-user versus prevalent-user design, adherence, treatment switching, depletion of susceptible people, and immortal time.
An active-comparator new-user design often improves comparability by contrasting people initiating therapies used for the same indication and aligning time zero at initiation.
These fields deal with changing susceptibility, developmental windows, familial clustering, fertility selection, pregnancy as a time-varying process, and survival into older ages. Specify whether inference concerns pregnancies, births, parents, children, or families, and handle repeated pregnancies or siblings appropriately.
Research may examine road design, protective equipment, substance use, firearm policy, falls, or workplace systems. Exposure opportunity is central: counts of crashes, for example, should be interpreted relative to travel distance, trips, time, or another appropriate denominator.
Topics include heritability, genome-wide association, gene-environment interplay, biomarkers, and Mendelian randomization. Population structure, selection, multiple testing, weak instruments, pleiotropy, tissue specificity, and measurement timing need careful attention. Genetic association does not imply deterministic individual fate.
Surveillance continuously and systematically collects, analyzes, interprets, and shares data for action. Evaluate timeliness, sensitivity, representativeness, stability, simplicity, acceptability, data quality, and positive predictive value.
Implementation research asks not only whether an intervention can work, but how reach, adoption, fidelity, adaptation, cost, and context influence real-world impact.
| Area | Descriptive question | Analytic/causal question | Possible design |
|---|---|---|---|
| Infection | What is weekly incidence? | Did vaccination reduce severe disease? | Surveillance + target-trial emulation |
| Cancer | What is stage distribution at diagnosis? | Does screening reduce disease-specific mortality? | Trial or carefully designed cohort |
| Environment | Where are heat-related visits concentrated? | Did cooling centers reduce heat illness? | Spatial description + quasi-experiment |
| Medication safety | What adverse events are reported? | Does drug A increase bleeding versus drug B? | Active-comparator new-user cohort |
| Equity | How does coverage vary by neighborhood? | Did universal coverage narrow absolute disparities? | Repeated cross-sections / policy evaluation |
| Occupation | What is injury incidence by job? | Does a safety program reduce injury rates? | Cluster trial or interrupted time series |
Keep raw data read-only. Record provenance, extraction date, inclusion rules, variable definitions, units, value labels, missing-value codes, and transformations. A minimal data dictionary includes variable name, meaning, type, allowed values, timing, source, and known limitations.
Never display real identifiers or row-level sensitive data in a report. The simulated data below are safe because they do not represent real people.
The simulated participants are disease-free at baseline and all
complete five years of follow-up. The binary outcome
disease_5y therefore represents incident disease by five
years, giving every risk below an explicit and common time horizon.
n <- 2500
cohort <- data.frame(
id = seq_len(n),
age = round(pmin(pmax(rnorm(n, mean = 52, sd = 14), 18), 85)),
sex = factor(sample(c("Female", "Male"), n, replace = TRUE)),
activity = round(pmax(rnorm(n, mean = 150, sd = 70), 0))
)
# Smoking probability varies with age and sex, creating measured confounding.
logit_smoke <- -0.5 + 0.012 * (cohort$age - 50) +
0.25 * (cohort$sex == "Male")
cohort$smoker <- rbinom(n, 1, plogis(logit_smoke))
# Five-year incident disease depends on smoking, age, sex, and activity.
logit_disease_5y <- -3.6 + 0.75 * cohort$smoker +
0.045 * (cohort$age - 50) +
0.20 * (cohort$sex == "Male") -
0.002 * (cohort$activity - 150)
cohort$disease_5y <- rbinom(n, 1, plogis(logit_disease_5y))
head(cohort)Checks should be driven by meaning, not only by software type.
quality_summary <- data.frame(
variable = names(cohort),
class = vapply(cohort, function(x) class(x)[1], character(1)),
missing_n = vapply(cohort, function(x) sum(is.na(x)), integer(1)),
unique_n = vapply(cohort, function(x) length(unique(x)), integer(1)),
row.names = NULL
)
quality_summarystopifnot(
!anyDuplicated(cohort$id),
all(cohort$age >= 18 & cohort$age <= 85),
all(cohort$smoker %in% c(0, 1)),
all(cohort$disease_5y %in% c(0, 1))
)Also examine impossible date sequences, duplicate events, unit changes, denominator coverage, category drift over time, and whether missingness is encoded as values such as 9, 99, blank, or “unknown.”
overall_summary <- data.frame(
n = nrow(cohort),
mean_age = mean(cohort$age),
sd_age = sd(cohort$age),
median_activity = median(cohort$activity),
smoking_prevalence = mean(cohort$smoker),
five_year_disease_risk = mean(cohort$disease_5y)
)
overall_summary## disease_5y
## smoker 0 1
## 0 1399 47
## 1 960 94
## disease_5y
## smoker 0 1
## 0 0.96750 0.03250
## 1 0.91082 0.08918
boxplot(activity ~ smoker, data = cohort,
names = c("Non-smoker", "Smoker"),
ylab = "Activity (minutes/week)",
col = c("#9ecae1", "#fdae6b"))Description reveals coding errors, sparse groups, lack of overlap, nonlinear patterns, and whether the planned estimand is supported by the data.
cohort_tab <- with(cohort, table(
factor(smoker, levels = c(1, 0), labels = c("Smoker", "Non-smoker")),
factor(disease_5y, levels = c(1, 0), labels = c("Case", "Non-case"))
))
cohort_tab##
## Case Non-case
## Smoker 94 960
## Non-smoker 47 1399
This comparison is unadjusted. Because age and sex affect smoking probability and five-year disease risk in the simulation, the crude association does not isolate a smoking effect.
Logistic regression models log odds. Exponentiated coefficients are adjusted odds ratios, conditional on included variables and model form.
fit_crude <- glm(disease_5y ~ smoker, family = binomial(), data = cohort)
fit_adjusted <- glm(
disease_5y ~ smoker + age + sex + activity,
family = binomial(), data = cohort
)
coefficient_table <- function(model) {
sm <- summary(model)$coefficients
data.frame(
term = rownames(sm),
odds_ratio = exp(sm[, "Estimate"]),
lower_95 = exp(sm[, "Estimate"] - 1.96 * sm[, "Std. Error"]),
upper_95 = exp(sm[, "Estimate"] + 1.96 * sm[, "Std. Error"]),
p_value = sm[, "Pr(>|z|)"],
row.names = NULL,
check.names = FALSE
)
}
coefficient_table(fit_crude)Check functional form: the linearly entered age
coefficient assumes a constant change in log odds per year. Splines are
often preferable to arbitrary categorization, but require additional
tools beyond this package-free tutorial.
Odds ratios are not risks. Regression standardization can translate a fitted model to marginal five-year risks under two exposure scenarios.
data_smoke <- transform(cohort, smoker = 1)
data_no_smoke <- transform(cohort, smoker = 0)
risk_if_all_smoked <- mean(predict(
fit_adjusted, newdata = data_smoke, type = "response"
))
risk_if_none_smoked <- mean(predict(
fit_adjusted, newdata = data_no_smoke, type = "response"
))
standardized_effects <- c(
risk_if_all_smoked = risk_if_all_smoked,
risk_if_none_smoked = risk_if_none_smoked,
standardized_risk_difference = risk_if_all_smoked - risk_if_none_smoked,
standardized_risk_ratio = risk_if_all_smoked / risk_if_none_smoked
)
standardized_effects## risk_if_all_smoked risk_if_none_smoked standardized_risk_difference
## 0.08600 0.03346 0.05254
## standardized_risk_ratio
## 2.57041
These g-computation estimates have a causal interpretation only if exchangeability, positivity, consistency, measurement, model, and selection assumptions are adequate. The simple calculation also omits uncertainty intervals.
fit_interaction <- glm(
disease_5y ~ smoker * sex + age + activity,
family = binomial(), data = cohort
)
coefficient_table(fit_interaction)# Standardized smoking risks within sex to assess additive and relative scales.
standardize_within_sex <- function(sex_value) {
subgroup <- cohort[cohort$sex == sex_value, ]
p1 <- mean(predict(fit_interaction,
newdata = transform(subgroup, smoker = 1),
type = "response"))
p0 <- mean(predict(fit_interaction,
newdata = transform(subgroup, smoker = 0),
type = "response"))
c(risk_exposed = p1, risk_unexposed = p0,
risk_difference = p1 - p0, risk_ratio = p1 / p0)
}
rbind(
Female = standardize_within_sex("Female"),
Male = standardize_within_sex("Male")
)## risk_exposed risk_unexposed risk_difference risk_ratio
## Female 0.07410 0.03016 0.04394 2.457
## Male 0.09763 0.03664 0.06099 2.664
The simulation did not include a smoking-by-sex interaction on the log-odds scale. Subgroup RDs and RRs can nevertheless differ systematically because baseline risks differ and interaction is scale-specific; sampling variation adds further differences. Report the effect scale, uncertainty intervals, and scientific context.
Poisson regression can model event counts with log person-time as an offset.
n_units <- 800
rate_data <- data.frame(
exposed = rbinom(n_units, 1, 0.35),
age10 = rnorm(n_units, 0, 1),
person_years = runif(n_units, 0.5, 4)
)
true_rate <- exp(-3.2 + 0.55 * rate_data$exposed + 0.25 * rate_data$age10)
rate_data$events <- rpois(n_units, true_rate * rate_data$person_years)
rate_fit <- glm(
events ~ exposed + age10 + offset(log(person_years)),
family = poisson(), data = rate_data
)
rate_sm <- summary(rate_fit)$coefficients
data.frame(
term = rownames(rate_sm),
rate_ratio = exp(rate_sm[, "Estimate"]),
lower_95 = exp(rate_sm[, "Estimate"] - 1.96 * rate_sm[, "Std. Error"]),
upper_95 = exp(rate_sm[, "Estimate"] + 1.96 * rate_sm[, "Std. Error"]),
row.names = NULL
)dispersion_statistic <- sum(residuals(rate_fit, type = "pearson")^2) /
df.residual(rate_fit)
dispersion_statistic## [1] 1.008
The offset fixes the coefficient of log person-time at 1, converting expected counts to rates. Strong overdispersion, clustering, excess zeros, or recurrent-event dependence requires a more appropriate variance/model strategy.
esophThe built-in esoph dataset contains grouped cases and
controls in a study of esophageal cancer, age, alcohol consumption, and
tobacco consumption. Its grouping variables are ordered factors, so they
are explicitly converted to unordered factors below; R’s default
treatment contrasts then compare each category with the first.
esoph_model <- transform(
esoph,
agegp = factor(agegp, levels = levels(agegp), ordered = FALSE),
alcgp = factor(alcgp, levels = levels(alcgp), ordered = FALSE),
tobgp = factor(tobgp, levels = levels(tobgp), ordered = FALSE)
)
esoph_fit <- glm(
cbind(ncases, ncontrols) ~ agegp + alcgp + tobgp,
family = binomial(), data = esoph_model
)
esoph_sm <- summary(esoph_fit)$coefficients
esoph_results <- data.frame(
term = rownames(esoph_sm),
odds_ratio = exp(esoph_sm[, "Estimate"]),
lower_95 = exp(esoph_sm[, "Estimate"] - 1.96 * esoph_sm[, "Std. Error"]),
upper_95 = exp(esoph_sm[, "Estimate"] + 1.96 * esoph_sm[, "Std. Error"]),
row.names = NULL
)
esoph_resultsThe first level of each factor is the reference category. Interpretation still depends on the original sampling and measurement process; model output alone is not a study design.
TitanicThe Titanic table illustrates stratified risks, although
it is not a modern health study and its categories reflect historical
records.
data(Titanic, package = "datasets")
titanic <- as.data.frame(Titanic)
sex_survival <- xtabs(Freq ~ Sex + Survived, data = titanic)
sex_survival## Survived
## Sex No Yes
## Male 1364 367
## Female 126 344
cbind(
total = rowSums(sex_survival),
survival_risk = sex_survival[, "Yes"] / rowSums(sex_survival)
)## total survival_risk
## Male 1731 0.2120
## Female 470 0.7319
class_survival <- xtabs(Freq ~ Class + Survived, data = titanic)
cbind(
total = rowSums(class_survival),
survival_risk = class_survival[, "Yes"] / rowSums(class_survival)
)## total survival_risk
## 1st 325 0.6246
## 2nd 285 0.4140
## 3rd 706 0.2521
## Crew 885 0.2395
These are crude associations. Differences by passenger class, sex, age, evacuation process, and record completeness are intertwined; the table should not be turned into an individual causal claim.
state.x77state_data <- as.data.frame(state.x77)
correlation <- cor(state_data$Illiteracy, state_data$`Life Exp`)
correlation## [1] -0.5885
plot(state_data$Illiteracy, state_data$`Life Exp`,
pch = 19, col = "#2c7fb8",
xlab = "State-level illiteracy (%)",
ylab = "State-level life expectancy (years)")
abline(lm(`Life Exp` ~ Illiteracy, data = state_data),
col = "#d95f0e", lwd = 2)A state-level association does not establish that individuals with one characteristic experience the corresponding individual outcome. Group composition, historical context, policy, and many shared causes can produce the pattern.
These labels describe assumptions relative to an analysis and observed-variable set. They cannot usually be proven from the incomplete data.
Complete-case analysis may lose precision and be biased. Single mean imputation understates uncertainty and distorts relationships. Multiple imputation can be useful when its model, auxiliary variables, time ordering, and estimand are appropriate, but it does not repair systematically unmeasured information.
cohort_missing <- cohort
missing_probability <- plogis(-3 + 0.025 * (cohort_missing$age - 50) +
0.8 * cohort_missing$disease_5y)
cohort_missing$activity[rbinom(n, 1, missing_probability) == 1] <- NA
c(
missing_activity = mean(is.na(cohort_missing$activity)),
missing_among_cases = with(cohort_missing,
mean(is.na(activity[disease_5y == 1]))),
missing_among_non_cases = with(cohort_missing,
mean(is.na(activity[disease_5y == 0])))
)## missing_activity missing_among_cases missing_among_non_cases
## 0.06080 0.17021 0.05426
Because missingness here depends on outcome and age, simply describing an overall missing percentage hides a meaningful pattern.
Censoring is not synonymous with missing a covariate. In time-to-event analyses it means the event time is only partially observed. Standard survival methods rely on independent/noninformative censoring conditional on modeled variables. If prognosis affects dropout differently across exposure groups, inverse-probability-of-censoring weights or sensitivity analyses may be needed.
Useful sensitivity analyses target the most consequential assumptions:
Do not search across analyses until one becomes “significant.” Prespecify primary choices and explain why each sensitivity analysis probes a specific vulnerability.
Sample-size planning should follow the primary estimand and design. Inputs may include baseline risk/rate, smallest important effect, exposure prevalence or allocation ratio, follow-up, clustering, repeated measures, design effect, expected missingness, and planned adjustment.
Power is a long-run probability under a specified true effect and analysis plan. It is not the probability that the study hypothesis is true. After data collection, an observed confidence interval is more informative than “post hoc power,” which largely restates the p-value.
Rare outcomes can leave few effective events even in a large database. The number of rows is not the same as information: clustering, imbalance, poor overlap, and measurement error reduce effective information.
Epidemiologic quality includes ethical quality.
Small-cell suppression alone may not prevent re-identification when tables can be combined. Follow applicable law, institutional policy, and data-governance agreements.
A strong paragraph usually gives:
Example template:
Among [population] followed for [time], [events/denominator] exposed participants and [events/denominator] comparison participants developed [outcome]. Standardized risks were [x] and [y], corresponding to an RD of [value] and RR of [value] (95% CI [lower, upper]). Estimates adjusted for [prespecified variables]. Residual confounding by [important factor] and [measurement/selection limitation] may remain.
Do not:
Use design-appropriate checklists as aids, not replacements for judgment:
Before release, confirm that:
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Tahoe 26.5.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] C.UTF-8/C.UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
##
## time zone: America/Edmonton
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] survival_3.8-6
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.39 R6_2.6.1 fastmap_1.2.0 Matrix_1.7-5 xfun_0.60
## [6] lattice_0.22-9 splines_4.6.1 cachem_1.1.0 knitr_1.51 htmltools_0.5.9
## [11] rmarkdown_2.31 lifecycle_1.0.5 cli_3.6.6 grid_4.6.1 sass_0.4.10
## [16] jquerylib_0.1.4 compiler_4.6.1 tools_4.6.1 evaluate_1.0.5 bslib_0.12.0
## [21] yaml_2.3.12 rlang_1.3.0 jsonlite_2.0.0
A town has 20,000 residents. On January 1, 1,000 have a chronic condition. During the year, 380 new cases occur among those initially free of the condition. They contribute 18,500 person-years at risk.
prevalence_ex1 <- 1000 / 20000
risk_ex1 <- 380 / (20000 - 1000)
rate_ex1 <- 380 / 18500 * 1000
c(prevalence = prevalence_ex1,
one_year_risk = risk_ex1,
incidence_per_1000_person_years = rate_ex1)## prevalence one_year_risk
## 0.05 0.02
## incidence_per_1000_person_years
## 20.54
The results are 5% prevalence, 2% one-year risk, and about 20.5 cases per 1,000 person-years.
In a cohort, 120 of 1,500 exposed people and 60 of 1,500 unexposed people develop an outcome.
Risk is 8% versus 4%, so RR = 2 and RD = 4 percentage points. The OR is about 2.09 because it compares odds rather than risks. The outcome is uncommon enough that the two relative measures are fairly close, but they are not identical.
A test has 95% sensitivity and 90% specificity. In a population of 10,000, disease prevalence is 1%.
population_ex3 <- 10000
cases_ex3 <- population_ex3 * 0.01
noncases_ex3 <- population_ex3 - cases_ex3
tp_ex3 <- cases_ex3 * 0.95
fn_ex3 <- cases_ex3 - tp_ex3
tn_ex3 <- noncases_ex3 * 0.90
fp_ex3 <- noncases_ex3 - tn_ex3
c(TP = tp_ex3, FN = fn_ex3, FP = fp_ex3, TN = tn_ex3)## TP FN FP TN
## 95 5 990 8910
## sensitivity specificity PPV NPV LR_positive LR_negative accuracy
## 0.95000 0.90000 0.08756 0.99944 9.50000 0.05556 0.90050
There are many more noncases than cases. Even a 10% false-positive fraction applied to 9,900 noncases produces far more false positives than the 95 true positives.
You need to study a very rare cancer with a 20-year latency and have access to archived occupational records. Which design is efficient, and what control-selection principle is essential?
A case-control study is efficient because investigators intentionally sample cases. If a well-defined workforce cohort exists, a nested case-control study may be especially strong. Controls must represent the exposure distribution of the source population that produced the cases and must be eligible to become cases at the relevant time.
A study of air pollution and asthma severity includes only people admitted to hospital. Both pollution exposure and asthma severity affect admission. What can happen when the analysis conditions on hospitalization?
Hospitalization is a common effect (collider) of exposure and severity. Restricting to hospitalized people can open a noncausal path between them, producing collider/selection bias. More covariate adjustment within the selected sample does not automatically fix the problem.
Rewrite “Does exercise prevent dementia?” as a PECO question. Identify at least two likely confounders and one measurement challenge.
Among community-dwelling adults aged 60–75 without dementia at baseline (P), what is the 10-year dementia risk under at least 150 minutes/week of moderate-to-vigorous physical activity (E) compared with less than 30 minutes/week (C), with dementia defined by adjudicated clinical criteria (O)? Age, education, vascular health, and baseline cognition are plausible confounders. Activity may be misclassified by self-report and may decline because of prodromal dementia, creating reverse-causation concerns. Repeated accelerometer measurement and a lag analysis could address parts of these problems.
| Term | Brief definition |
|---|---|
| Absolute risk | Probability of an outcome over a stated interval |
| Attack rate | Cumulative incidence during an outbreak |
| Bias | Systematic error in estimating a target quantity |
| Case definition | Standard criteria for classifying an outbreak/surveillance case |
| Censoring | Event time is only partially observed |
| Collider | Variable caused by two other variables; conditioning can induce association |
| Confounder | Cause/predictor that creates a noncausal exposure-outcome comparison |
| Consistency | Observed outcome under received exposure equals the corresponding defined potential outcome |
| Cumulative incidence | Proportion initially at risk who develop an outcome in a stated period |
| DAG | Directed acyclic graph representing assumed causal relationships |
| Effect modification | Effect differs across levels of another variable on a specified scale |
| Exchangeability | Compared groups are counterfactually comparable, possibly conditional on covariates |
| Incidence rate | New events divided by person-time at risk |
| Interaction | Departure from a specified additive or multiplicative effect model |
| Misclassification | Measured category differs from the true category |
| Odds ratio | Ratio of outcome odds between exposure groups, or exposure odds between case/control groups |
| Person-time | Sum of eligible time contributed while at risk |
| Positivity | Each covariate pattern has possible exposure to every compared strategy |
| Prevalence | Proportion with a condition at a time or during a period |
| Risk difference | Difference in outcome risks between groups |
| Risk ratio | Ratio of outcome risks between groups |
| Selection bias | Distortion caused by mechanisms determining observation/analysis |
| Sensitivity | Probability of a positive test among truly diseased people |
| Specificity | Probability of a negative test among truly nondiseased people |
| Standardization | Averaging stratum-specific measures over a common distribution |
| Target population | Population to which the desired inference applies |
| Time zero | Aligned start of eligibility, assignment/exposure, and follow-up |
10.4 Social epidemiology and health equity
Social epidemiology examines how power, policy, material conditions, discrimination, housing, education, income, and social networks shape health. Categories such as race and ethnicity should be conceptualized in relation to racism and social processes, not treated as innate biological causes without justification.
Equity analyses should ask who benefits, who is harmed, whose data are missing, whether measurement works similarly across groups, and whether interventions change absolute as well as relative disparities.