The OMOP CDM is a person-centric model. The person table contains records that uniquely identify each individual along with some of their demographic information. Below we create a mock CDM reference which, as is standard, has a person table which contains fields which indicate an individual’s date of birth, gender, race, and ethnicity. Each of these, except for date of birth, are represented by a concept ID (and as the person table contains one record per person these fields are treated as time-invariant).
library(PatientProfiles)
library(duckdb)
library(dplyr)
cdm <- mockPatientProfiles(numberIndividuals = 10000)
cdm$person %>%
dplyr::glimpse()
## Rows: ??
## Columns: 5
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15…
## $ gender_concept_id <int> 8532, 8507, 8532, 8507, 8507, 8532, 8532, 8507, 8…
## $ year_of_birth <int> 1913, 1901, 1972, 1901, 1965, 1948, 1939, 1928, 1…
## $ race_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ ethnicity_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
As well as the person table, every CDM reference will include an observation period table. This table contains spans of times during which an individual is considered to being under observation. Individuals can have multiple observation periods, but they cannot overlap.
cdm$observation_period %>%
dplyr::glimpse()
## Rows: ??
## Columns: 5
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1…
## $ observation_period_start_date <date> 1913-01-01, 1901-01-01, 1972-01-01, 190…
## $ observation_period_end_date <date> 1946-04-30, 1947-09-06, 2005-07-09, 193…
## $ period_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ observation_period_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1…
When performing analyses we will often be interested in working with the person and observation period tables to identify individuals’ characteristics on some date of interest. PatientProfiles provides a number of functions that can help us do this.
Let’s say we’re working with the condition occurrence table.
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 6
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 1708, 330, 2025, 4527, 8364, 5559, 2271, 501…
## $ condition_start_date <date> 1971-04-26, 1974-04-26, 1962-09-15, 1967-10…
## $ condition_end_date <date> 1972-04-14, 1977-07-07, 1975-05-22, 1971-10…
## $ condition_occurrence_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
## $ condition_concept_id <int> 8, 8, 9, 4, 2, 7, 9, 7, 6, 9, 6, 6, 9, 5, 9,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
This table contains diagnoses of individuals and we might, for
example, want to identify their age on their date of diagnosis. This
involves linking back to the person table which contains their date of
birth (split across three different columns). PatientProfiles provides a
simple function for this. addAge()
will add a new column to
the table containing each patient’s age relative to the specified index
date.
cdm$condition_occurrence <- cdm$condition_occurrence %>%
addAge(indexDate = "condition_start_date")
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 7
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 1708, 330, 2025, 4527, 8364, 5559, 2271, 501…
## $ condition_start_date <date> 1971-04-26, 1974-04-26, 1962-09-15, 1967-10…
## $ condition_end_date <date> 1972-04-14, 1977-07-07, 1975-05-22, 1971-10…
## $ condition_occurrence_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
## $ condition_concept_id <int> 8, 8, 9, 4, 2, 7, 9, 7, 6, 9, 6, 6, 9, 9, 1,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 13, 4, 8, 7, 38, 12, 5, 19, 45, 23, 28, 0, 1…
As well as calculating age, we can also create age groups at the same time. Here we create three age groups: those aged 0 to 17, those 18 to 65, and those 66 or older.
cdm$condition_occurrence <- cdm$condition_occurrence %>%
addAge(
indexDate = "condition_start_date",
ageGroup = list(
"0 to 17" = c(0, 17),
"18 to 65" = c(18, 65),
">= 66" = c(66, Inf)
)
)
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 8
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
By default, when adding age the new column will have been called “age” and will have been calculated using all available information on date of birth contained in the person. We can though also alter these defaults. Here, for example, we impose that month of birth is January and day of birth is the 1st for all individuals.
cdm$condition_occurrence <- cdm$condition_occurrence %>%
addAge(
indexDate = "condition_start_date",
ageName = "age_from_year_of_birth",
ageMissingMonth = 1,
ageMissingDay = 1,
ageImposeMonth = TRUE,
ageImposeDay = TRUE
)
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 9
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
## $ age_from_year_of_birth <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
As well as age at diagnosis, we might also want identify patients’
sex. PatientProfiles provides the addSex()
function that
will add this for us. Because this is treated as time-invariant, we will
not have to specify any index variable.
cdm$condition_occurrence <- cdm$condition_occurrence %>%
addSex()
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 10
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
## $ age_from_year_of_birth <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ sex <chr> "Female", "Male", "Male", "Male", "Male", "M…
Similarly, we could also identify whether an individual was in observation at the time of their diagnosis (i.e. had an observation period that overlaps with their diagnosis date), as well as identifying how much prior observation time they had on this date and how much they have following it.
cdm$condition_occurrence <- cdm$condition_occurrence %>%
addInObservation(indexDate = "condition_start_date") %>%
addPriorObservation(indexDate = "condition_start_date") %>%
addFutureObservation(indexDate = "condition_start_date")
cdm$condition_occurrence %>%
glimpse()
## Rows: ??
## Columns: 13
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
## $ age_from_year_of_birth <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ sex <chr> "Female", "Male", "Male", "Male", "Male", "M…
## $ in_observation <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ prior_observation <int> 3179, 1958, 15670, 5053, 5145, 2862, 14259, …
## $ future_observation <int> 11102, 8518, 3231, 7461, 3028, 12688, 3364, …
For these functions which work with information from the observation
table, it is important to note that the results will be based on the
observation period during which the index date falls within. Moreover,
if a patient is not under observation at the specified date,
addPriorObservation()
and
addFutureObservation()
functions will return NA.
When checking whether someone is in observation the default is that we are checking whether someone was in observation on the index date. We could though expand this and consider a window of time around this date. For example here we add a variable indicating whether someone was in observation from 180 days before the index date to 30 days following it.
cdm$condition_occurrence %>%
addInObservation(
indexDate = "condition_start_date",
window = c(-180, 30)
) %>%
glimpse()
## Rows: ??
## Columns: 13
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
## $ age_from_year_of_birth <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ sex <chr> "Female", "Male", "Male", "Male", "Male", "M…
## $ prior_observation <int> 3179, 1958, 15670, 5053, 5145, 2862, 14259, …
## $ future_observation <int> 11102, 8518, 3231, 7461, 3028, 12688, 3364, …
## $ in_observation <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
We can also specify a window and require that an individual is present for only some days within it. Here we add a variable indicating whether the individual was in observation at least a year in the future,
cdm$condition_occurrence %>%
addInObservation(
indexDate = "condition_start_date",
window = c(365, Inf),
completeInterval = FALSE
) %>%
glimpse()
## Rows: ??
## Columns: 13
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ person_id <int> 2025, 29, 6492, 2223, 1457, 9840, 9217, 305,…
## $ condition_start_date <date> 1962-09-15, 1938-05-13, 1945-11-26, 1953-11…
## $ condition_end_date <date> 1975-05-22, 1958-03-07, 1950-10-07, 1956-02…
## $ condition_occurrence_id <int> 3, 15, 23, 27, 34, 54, 59, 64, 68, 71, 89, 9…
## $ condition_concept_id <int> 9, 9, 1, 7, 8, 5, 1, 3, 6, 2, 3, 9, 2, 7, 3,…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17",…
## $ age_from_year_of_birth <int> 8, 5, 42, 13, 14, 7, 39, 48, 0, 22, 13, 24, …
## $ sex <chr> "Female", "Male", "Male", "Male", "Male", "M…
## $ prior_observation <int> 3179, 1958, 15670, 5053, 5145, 2862, 14259, …
## $ future_observation <int> 11102, 8518, 3231, 7461, 3028, 12688, 3364, …
## $ in_observation <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,…
The above functions can be used on both standard OMOP CDM tables and cohort tables. Note as the default index date in the functions is “cohort_start_date” we can now omit this.
cdm$cohort1 %>%
glimpse()
## Rows: ??
## Columns: 4
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 1, 3, 1, 2, 3, 3, 2, 3, 2, 1, 1, 2, 3, 3, 2, 3, 2…
## $ subject_id <int> 9136, 6714, 4050, 1585, 3391, 7079, 3943, 2992, 7…
## $ cohort_start_date <date> 1970-01-14, 1965-09-01, 1950-07-30, 1942-08-12, …
## $ cohort_end_date <date> 1980-12-17, 1967-12-11, 1958-05-25, 1944-04-12, …
cdm$cohort1 <- cdm$cohort1 %>%
addAge(ageGroup = list(
"0 to 17" = c(0, 17),
"18 to 65" = c(18, 65),
">= 66" = c(66, Inf)
)) %>%
addSex() %>%
addInObservation() %>%
addPriorObservation() %>%
addFutureObservation()
cdm$cohort1 %>%
glimpse()
## Rows: ??
## Columns: 10
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 1, 2, 1, 1, 2, 2, 2, 1, 1, 1, 3, 1, 1, 2, 2, 1, 1…
## $ subject_id <int> 1, 2, 3, 4, 6, 7, 8, 10, 11, 12, 13, 14, 16, 18, …
## $ cohort_start_date <date> 1913-11-10, 1908-11-17, 1998-01-18, 1901-09-13, …
## $ cohort_end_date <date> 1921-05-12, 1921-01-19, 2003-05-14, 1916-01-18, …
## $ age <int> 0, 7, 26, 0, 8, 3, 27, 8, 13, 22, 10, 10, 0, 3, 1…
## $ age_group <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17", "0 t…
## $ sex <chr> "Female", "Male", "Female", "Male", "Female", "Fe…
## $ in_observation <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ prior_observation <int> 313, 2877, 9514, 255, 2971, 1387, 9953, 3132, 485…
## $ future_observation <int> 11859, 14172, 2729, 13129, 10589, 2501, 5605, 141…
The above functions, which are chained together, each fetch the
related information one by one. In the cases where we are interested in
adding multiple characteristics, we can add these all at the same time
using the more general addDemographics()
functions. This
will be more efficient that adding characteristics as it requires fewer
joins between our table of interest and the person and observation
period tables.
cdm$cohort2 %>%
glimpse()
## Rows: ??
## Columns: 4
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 3, 3, 1, 1, 2, 3, 1, 3, 2, 1, 1, 3, 3, 1, 2, 3, 1…
## $ subject_id <int> 9655, 4608, 1712, 1788, 5199, 6874, 9674, 1802, 8…
## $ cohort_start_date <date> 1985-07-06, 1970-07-22, 1944-02-28, 1963-03-15, …
## $ cohort_end_date <date> 1995-12-30, 1971-10-07, 1951-12-17, 1964-10-04, …
tictoc::tic()
cdm$cohort2 %>%
addAge(ageGroup = list(
"0 to 17" = c(0, 17),
"18 to 65" = c(18, 65),
">= 66" = c(66, Inf)
)) %>%
addSex() %>%
addInObservation() %>%
addPriorObservation() %>%
addFutureObservation()
## # Source: table<og_270_1729815600> [?? x 10]
## # Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## cohort_definition_id subject_id cohort_start_date cohort_end_date age
## <int> <int> <date> <date> <int>
## 1 1 1 1944-10-10 1945-01-16 31
## 2 3 3 1993-05-03 1999-10-30 21
## 3 1 4 1925-01-28 1932-06-22 24
## 4 3 5 1976-06-24 1984-01-27 11
## 5 2 6 1962-01-08 1967-04-28 14
## 6 1 7 1948-09-19 1948-09-23 9
## 7 1 9 1974-10-11 1978-03-02 10
## 8 1 10 1950-11-01 1967-03-18 0
## 9 1 12 1927-02-14 1954-05-02 17
## 10 1 13 1970-08-22 1989-09-20 10
## # ℹ more rows
## # ℹ 5 more variables: age_group <chr>, sex <chr>, in_observation <int>,
## # prior_observation <int>, future_observation <int>
tictoc::toc()
## 0.454 sec elapsed
tictoc::tic()
cdm$cohort2 %>%
addDemographics(
age = TRUE,
ageName = "age",
ageGroup = list(
"0 to 17" = c(0, 17),
"18 to 65" = c(18, 65),
">= 66" = c(66, Inf)
),
sex = TRUE,
sexName = "sex",
priorObservation = TRUE,
priorObservationName = "prior_observation",
futureObservation = FALSE,
) %>%
glimpse()
## Rows: ??
## Columns: 8
## Database: DuckDB v1.1.0 [root@Darwin 24.0.0:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 1, 3, 1, 3, 2, 1, 1, 1, 1, 1, 3, 1, 1, 2, 2, 3, 2…
## $ subject_id <int> 1, 3, 4, 5, 6, 7, 9, 10, 12, 13, 14, 15, 16, 17, …
## $ cohort_start_date <date> 1944-10-10, 1993-05-03, 1925-01-28, 1976-06-24, …
## $ cohort_end_date <date> 1945-01-16, 1999-10-30, 1932-06-22, 1984-01-27, …
## $ age <int> 31, 21, 24, 11, 14, 9, 10, 0, 17, 10, 20, 14, 8, …
## $ age_group <chr> "18 to 65", "18 to 65", "18 to 65", "0 to 17", "0…
## $ sex <chr> "Female", "Female", "Male", "Male", "Female", "Fe…
## $ prior_observation <int> 11605, 7793, 8793, 4192, 5121, 3549, 3936, 304, 6…
tictoc::toc()
## 0.175 sec elapsed
In our small mock dataset we see a small improvement in performance, but this difference will become much more noticeable when working with real data that will typically be far larger.