Population-Level Epidemiology Guide

Introduction & Purpose

Population-Level Epidemiology studies are designed to answer the most fundamental questions in public health: how common is a disease within a population, and is its frequency changing over time? This methodology provides a high-level, panoramic view of a disease’s burden on a community or healthcare system, which is essential for planning public health policies, allocating resources, and identifying emerging health trends.

The purpose is to quantify the frequency of health outcomes at a population level, typically by measuring:

  • Incidence: The rate of new cases of a disease in a population over a specific period. This helps us understand the risk of developing the disease.
  • Prevalence: The proportion of a population that currently has a disease at a specific point in time (point prevalence) or over a period (period prevalence). This helps us understand the overall burden of the disease.

Study Design

The standard design for this analysis is a population-level cohort study. This involves taking the entire population captured within a database and following them over a defined period of calendar time to observe the occurrence of health outcomes.

Participants

The study typically includes the entire source population available in the database who have a minimum period of data visibility (e.g., at least one year) before the study’s start date. Key considerations for defining the participant group include:

  • For Incidence Calculation: To measure only new cases, individuals who have a history of the disease (prevalent cases) are excluded from the analysis. This “washout” period ensures that the cases being counted are genuinely new diagnoses.
  • Subpopulations: The analysis can be restricted to specific subpopulations of interest, for example, by limiting the study to people over a certain age or with a specific clinical history.

Outcomes

The primary outcomes are the calculated rates of incidence and prevalence for the disease of interest. These are typically stratified by:

  • Demographics: Age groups and gender.
  • Time: Calendar year, month, or quarter to observe trends.

Follow-up

The follow-up for the study begins at a pre-defined calendar date (the index date) and continues for a specified period (e.g., one year). This process is often repeated for multiple consecutive periods to generate trend data over several years.

Analyses

The analytical approach is descriptive. The core calculations are:

  • Incidence Rate: The number of newly diagnosed people (the numerator) divided by the total person-time at risk in the population (the denominator).
  • Prevalence: The number of people with the diagnosis (both new and pre-existing) divided by the total number of people in the source population at a specific point in time or over a period.

How to Implement This Study

The OHDSI IncidencePrevalence R package is designed to calculate population-level incidence and prevalence rates from OMOP CDM data, directly addressing the core analytical requirements of this study design.

At a high level, IncidencePrevalence turns OMOP CDM cohorts into tidy, stratified summaries by repeatedly applying three ideas:

  • Denominator time: People contribute at-risk time only when they meet the study rules you set with generateDenominatorCohortSet()/generateTargetDenominatorCohortSet(). Entry is the latest of study start, prior observation met, and minimum age; exit is the earliest of study end, observation end, and age upper limit.
  • Outcome overlap: Outcomes are read from a separate cohort table you prepare (here provided by mockIncidencePrevalence()). Prevalence checks whether a person is in the outcome cohort at a time point/over an interval; incidence counts first qualifying onsets, respecting washout and repeated event rules.
  • Interval slicing: The study time is cut into calendar intervals (years, quarters, months). Each interval gets its own denominator, outcome counts, and derived measures.

Key computations and arguments

  • Point prevalence (estimatePointPrevalence):
    • What: proportion with the outcome at a time point inside each interval.
    • How: outcome presence on the chosen timePoint (“start”/“middle”/“end”) among those in the denominator at that date.
    • Tip: Use when you want a snapshot at a consistent anchor within each interval.
  • Period prevalence (estimatePeriodPrevalence):
    • What: proportion with the outcome at any time during the interval.
    • How: outcome occurs at any day overlapping the interval; denominator includes those present during the interval.
    • fullContribution: if TRUE, only people observed for the entire interval count in the denominator; if FALSE, anyone observed for ≥1 day counts.
    • Tip: Choose fullContribution based on whether incomplete observation could bias proportions for long intervals.
  • Incidence (estimateIncidence):
    • What: new-onset rates per person-time in each interval.
    • How: counts qualifying outcome onsets and divides by person-time contributed in the denominator.
    • outcomeWashout: requires no outcome in the prior N days; use Inf to ensure first-ever incidence.
    • repeatedEvents: if TRUE, allows multiple incident events per person separated by washout; if FALSE, at most one incident event per person.
    • completeDatabaseIntervals: if TRUE, drops intervals where the database cannot establish full observation coverage (e.g., first/last years with partial data), reducing edge bias.

Stratification and groups

  • Groups: Each combination of denominator settings (age group, sex, prior history, time-at-risk windows, target-based subsets) becomes a separate cohort_definition_id and produces separate rows.
  • Strata: You can facet/colour or summarize by variables such as denominator_age_group, denominator_sex, etc. Use plotIncidence()/plotPrevalence() helpers and available...Grouping() to see valid aesthetics.

Outputs

  • Summarised results: Functions return a “summarised_result” tables with estimate_name/value pairs, plus additional metadata (start/end dates, interval type). These are directly consumable by visOmopResults to tableIncidence/tablePrevalence and plot functions.

Practical guidance

  • Pick intervals aligned to your question and data density (years for long-term trends, months for seasonality).
  • Use outcomeWashout and repeatedEvents to encode “first-ever” vs. “episode-based” incidence.
  • Prefer completeDatabaseIntervals (incidence) and consider fullContribution (period prevalence) when edge effects or intermittent observation could bias estimates.
  • Keep exclusion logic out of outcome cohorts; define restrictions on the denominator instead.

Our workflow will follow a simple, powerful pattern common across OHDSI packages: summarise -> table/plot.

Step 1: Load Libraries & Connect to the GiBleed Database

First, we load the necessary libraries. CDMConnector provides the connection to the OMOP CDM database, CohortConstructor builds clinical cohorts from standard OMOP concepts, IncidencePrevalence calculates population-level epidemiology rates, and visOmopResults formats the results into publication-ready tables and plots.

We connect to the standardized Eunomia GiBleed dataset using DuckDB:

library(CDMConnector)
library(CohortConstructor)
library(IncidencePrevalence)
library(visOmopResults)
library(dplyr)
# Ensure GiBleed dataset is available and connect via DuckDB
Sys.setenv(EUNOMIA_DATA_FOLDER = Sys.getenv("EUNOMIA_DATA_FOLDER", tempdir()))
if (!eunomiaIsAvailable("GiBleed")) {
  downloadEunomiaData("GiBleed")
}
## 
## Download completed!
con <- DBI::dbConnect(duckdb::duckdb(), eunomiaDir("GiBleed"))
cdm <- cdmFromCon(con, cdmSchema = "main", writeSchema = "main")

Step 2: Define Outcome and Denominator Cohorts

In this study, our outcome of interest is Gastrointestinal Hemorrhage (concept_id = 192671). We instantiate the outcome cohort directly from the OMOP CDM condition table:

# Create the gastrointestinal hemorrhage outcome cohort
cdm$outcome <- conceptCohort(
  cdm = cdm,
  conceptSet = list(gi_bleed = 192671L),
  name = "outcome"
)

Next, we define our source study population (the denominator). We will study the population observed between 2010 and 2019, stratifying by age groups (0–49 and 50–100) and by sex:

# Define the denominator cohort for the study period
cdm <- generateDenominatorCohortSet(
  cdm = cdm,
  name = "denominator",
  cohortDateRange = as.Date(c("2010-01-01", "2019-01-01")),
  ageGroup = list(
    c(0, 49),
    c(50, 100)
  ),
  sex = c("Male", "Female")
)

Step 3: Calculate Incidence & Prevalence (The summarise Step)

This step calculates incidence and prevalence rates across the defined strata.

3.1: Estimate Incidence

We calculate annual incidence rates for gastrointestinal hemorrhage. We set outcomeWashout = Inf to include only first-ever occurrences and repeatedEvents = FALSE. We then visualize the results with plotIncidence() and generate a structured table with tableIncidence():

# Calculate annual incidence rates
inc <- estimateIncidence(
  cdm = cdm,
  denominatorTable = "denominator",
  outcomeTable = "outcome",
  interval = "years",
  outcomeWashout = Inf,
  repeatedEvents = FALSE
)

# Plot incidence rates faceted by age group and colored by sex
plotIncidence(inc, facet = "denominator_age_group", colour = "denominator_sex")

# Display concise 5-year recent trend table
inc |>
  filterAdditional(incidence_start_date >= as.Date("2014-01-01")) |>
  tableIncidence()
Incidence start date Incidence end date Denominator age group Denominator sex
Estimate name
Denominator (N) Person-years Outcome (N) Incidence 100,000 person-years [95% CI]
Synthea; gi_bleed
2014-01-01 2014-12-31 0 to 49 Female 510 498.47 6 1,203.68 (441.73 - 2,619.90)
2015-01-01 2015-12-31 0 to 49 Female 487 469.42 4 852.12 (232.17 - 2,181.76)
2016-01-01 2016-12-31 0 to 49 Female 450 433.06 5 1,154.57 (374.88 - 2,694.38)
2017-01-01 2017-12-31 0 to 49 Female 413 375.99 8 2,127.72 (918.60 - 4,192.46)
2018-01-01 2018-12-31 0 to 49 Female 324 237.49 5 2,105.38 (683.61 - 4,913.25)
2014-01-01 2014-12-31 0 to 49 Male 477 459.10 3 653.45 (134.76 - 1,909.66)
2015-01-01 2015-12-31 0 to 49 Male 441 425.36 8 1,880.75 (811.97 - 3,705.82)
2016-01-01 2016-12-31 0 to 49 Male 410 390.78 4 1,023.59 (278.89 - 2,620.79)
2017-01-01 2017-12-31 0 to 49 Male 371 334.98 9 2,686.76 (1,228.56 - 5,100.31)
2018-01-01 2018-12-31 0 to 49 Male 280 190.53 5 2,624.26 (852.09 - 6,124.14)
2014-01-01 2014-12-31 50 to 100 Female 583 573.66 0 0.00 (0.00 - 643.04)
2015-01-01 2015-12-31 50 to 100 Female 614 595.25 0 0.00 (0.00 - 619.72)
2016-01-01 2016-12-31 50 to 100 Female 635 622.14 0 0.00 (0.00 - 592.94)
2017-01-01 2017-12-31 50 to 100 Female 658 643.15 0 0.00 (0.00 - 573.56)
2018-01-01 2018-12-31 50 to 100 Female 674 591.38 0 0.00 (0.00 - 623.78)
2014-01-01 2014-12-31 50 to 100 Male 598 575.42 0 0.00 (0.00 - 641.07)
2015-01-01 2015-12-31 50 to 100 Male 610 595.87 0 0.00 (0.00 - 619.08)
2016-01-01 2016-12-31 50 to 100 Male 634 619.35 0 0.00 (0.00 - 595.61)
2017-01-01 2017-12-31 50 to 100 Male 656 638.86 0 0.00 (0.00 - 577.41)
2018-01-01 2018-12-31 50 to 100 Male 683 593.91 0 0.00 (0.00 - 621.12)

3.2: Estimate Point Prevalence

Next, we estimate annual point prevalence at the start of each year (timePoint = "start"):

# Calculate annual point prevalence
prev_point <- estimatePointPrevalence(
  cdm = cdm,
  denominatorTable = "denominator",
  outcomeTable = "outcome",
  interval = "years",
  timePoint = "start"
)

# Plot point prevalence faceted by age group and colored by sex
plotPrevalence(prev_point, facet = "denominator_age_group", colour = "denominator_sex")

# Display concise 5-year recent trend table
prev_point |>
  filterAdditional(prevalence_start_date >= as.Date("2014-01-01")) |>
  tablePrevalence()
Prevalence start date Prevalence end date Denominator age group Denominator sex
Estimate name
Denominator (N) Outcome (N) Prevalence [95% CI]
Synthea; gi_bleed
2014-01-01 2014-01-01 0 to 49 Female 581 0 0.00 (0.00 - 0.01)
2015-01-01 2015-01-01 0 to 49 Female 554 0 0.00 (0.00 - 0.01)
2016-01-01 2016-01-01 0 to 49 Female 516 0 0.00 (0.00 - 0.01)
2017-01-01 2017-01-01 0 to 49 Female 475 0 0.00 (0.00 - 0.01)
2018-01-01 2018-01-01 0 to 49 Female 377 0 0.00 (0.00 - 0.01)
2019-01-01 2019-01-01 0 to 49 Female 156 0 0.00 (0.00 - 0.02)
2014-01-01 2014-01-01 0 to 49 Male 549 0 0.00 (0.00 - 0.01)
2015-01-01 2015-01-01 0 to 49 Male 510 0 0.00 (0.00 - 0.01)
2016-01-01 2016-01-01 0 to 49 Male 480 0 0.00 (0.00 - 0.01)
2017-01-01 2017-01-01 0 to 49 Male 442 0 0.00 (0.00 - 0.01)
2018-01-01 2018-01-01 0 to 49 Male 338 0 0.00 (0.00 - 0.01)
2019-01-01 2019-01-01 0 to 49 Male 127 0 0.00 (0.00 - 0.03)
2014-01-01 2014-01-01 50 to 100 Female 683 0 0.00 (0.00 - 0.01)
2015-01-01 2015-01-01 50 to 100 Female 708 0 0.00 (0.00 - 0.01)
2016-01-01 2016-01-01 50 to 100 Female 739 0 0.00 (0.00 - 0.01)
2017-01-01 2017-01-01 50 to 100 Female 770 0 0.00 (0.00 - 0.00)
2018-01-01 2018-01-01 50 to 100 Female 798 0 0.00 (0.00 - 0.00)
2019-01-01 2019-01-01 50 to 100 Female 482 0 0.00 (0.00 - 0.01)
2014-01-01 2014-01-01 50 to 100 Male 681 0 0.00 (0.00 - 0.01)
2015-01-01 2015-01-01 50 to 100 Male 707 0 0.00 (0.00 - 0.01)
2016-01-01 2016-01-01 50 to 100 Male 730 0 0.00 (0.00 - 0.01)
2017-01-01 2017-01-01 50 to 100 Male 756 0 0.00 (0.00 - 0.01)
2018-01-01 2018-01-01 50 to 100 Male 783 0 0.00 (0.00 - 0.00)
2019-01-01 2019-01-01 50 to 100 Male 456 0 0.00 (0.00 - 0.01)