Adding patient demographics

Introduction

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.2 [unknown@Linux 6.5.0-1025-azure: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> 8507, 8507, 8507, 8532, 8507, 8532, 8507, 8532, 8…
## $ year_of_birth        <int> 1938, 1977, 1924, 1974, 1929, 1911, 1952, 1930, 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.2 [unknown@Linux 6.5.0-1025-azure: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> 1938-01-01, 1977-01-01, 1924-01-01, 197…
## $ observation_period_end_date   <date> 1986-11-18, 1995-12-15, 1962-12-23, 200…
## $ 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.

Adding characteristics to OMOP CDM tables

Let’s say we’re working with the condition occurrence table.

cdm$condition_occurrence %>%
  glimpse()
## Rows: ??
## Columns: 6
## Database: DuckDB v1.1.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 2166, 2155, 337, 4556, 5192, 1992, 1079, 134…
## $ condition_start_date      <date> 1937-07-19, 1964-02-09, 1966-03-21, 1940-02…
## $ condition_end_date        <date> 1953-01-24, 1967-11-04, 1973-05-30, 1966-01…
## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
## $ condition_concept_id      <int> 1, 7, 6, 3, 7, 4, 1, 2, 3, 5, 9, 7, 5, 8, 1,…
## $ 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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 2166, 2155, 4556, 1992, 1079, 1344, 8483, 84…
## $ condition_start_date      <date> 1937-07-19, 1964-02-09, 1940-02-15, 1925-04…
## $ condition_end_date        <date> 1953-01-24, 1967-11-04, 1966-01-24, 1927-10…
## $ condition_occurrence_id   <int> 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,…
## $ condition_concept_id      <int> 1, 7, 3, 4, 1, 2, 3, 5, 9, 7, 5, 8, 1, 10, 4…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 1, 8, 14, 0, 21, 38, 15, 18, 24, 11, 19, 18,…

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
## $ age_from_year_of_birth    <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
## $ age_from_year_of_birth    <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ sex                       <chr> "Female", "Male", "Female", "Female", "Male"…

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
## $ age_from_year_of_birth    <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ sex                       <chr> "Female", "Male", "Female", "Female", "Male"…
## $ in_observation            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
## $ prior_observation         <int> 180, 1154, 2141, 353, 4950, 2594, 7348, 5297…
## $ future_observation        <int> 13709, 11405, 428, 8387, 10102, 6576, 8988, …

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
## $ age_from_year_of_birth    <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ sex                       <chr> "Female", "Male", "Female", "Female", "Male"…
## $ prior_observation         <int> 180, 1154, 2141, 353, 4950, 2594, 7348, 5297…
## $ future_observation        <int> 13709, 11405, 428, 8387, 10102, 6576, 8988, …
## $ 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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ person_id                 <int> 6, 13, 14, 21, 23, 24, 26, 30, 31, 34, 37, 3…
## $ condition_start_date      <date> 1911-06-30, 1983-02-28, 1918-11-12, 1970-12…
## $ condition_end_date        <date> 1930-05-28, 2013-07-07, 1919-03-07, 1986-12…
## $ condition_occurrence_id   <int> 984, 1184, 5036, 4960, 1750, 2993, 1397, 317…
## $ condition_concept_id      <int> 3, 2, 1, 3, 8, 7, 4, 6, 7, 2, 9, 7, 4, 6, 10…
## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ age                       <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
## $ age_from_year_of_birth    <int> 0, 3, 5, 0, 13, 7, 20, 14, 24, 13, 0, 6, 8, …
## $ sex                       <chr> "Female", "Male", "Female", "Female", "Male"…
## $ prior_observation         <int> 180, 1154, 2141, 353, 4950, 2594, 7348, 5297…
## $ future_observation        <int> 13709, 11405, 428, 8387, 10102, 6576, 8988, …
## $ in_observation            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…

Adding characteristics to a cohort tables

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 3, 2, 1, 2, 1, 3, 3, 1, 2, 2, 3, 1, 2, 3, 1, 1, 1…
## $ subject_id           <int> 2830, 7685, 5151, 3959, 1582, 8737, 7536, 3252, 8…
## $ cohort_start_date    <date> 1953-06-17, 1950-12-25, 1922-09-26, 1969-12-16, …
## $ cohort_end_date      <date> 1955-08-11, 1959-02-25, 1923-06-14, 1972-06-23, …
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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 2, 1, 1, 1, 1, 2, 1, 3, 3, 1, 1, 2, 1, 2, 3, 2, 2…
## $ subject_id           <int> 1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 19, …
## $ cohort_start_date    <date> 1948-03-19, 1958-10-03, 1942-02-04, 1921-05-17, …
## $ cohort_end_date      <date> 1981-05-01, 1962-05-05, 1942-03-30, 1932-01-26, …
## $ age                  <int> 10, 34, 13, 10, 0, 4, 10, 15, 2, 9, 3, 10, 22, 16…
## $ age_group            <chr> "0 to 17", "18 to 65", "0 to 17", "0 to 17", "0 t…
## $ sex                  <chr> "Male", "Male", "Male", "Female", "Male", "Female…
## $ in_observation       <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ prior_observation    <int> 3730, 12694, 4782, 3789, 330, 1732, 3668, 5758, 9…
## $ future_observation   <int> 14123, 1542, 128, 10100, 7870, 1708, 9946, 4993, …

Getting multiple characteristics at once

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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 1, 2, 2, 1, 3, 2, 3, 2, 3, 3, 2, 2, 2, 1, 3, 1, 3…
## $ subject_id           <int> 3187, 796, 4543, 4533, 464, 6669, 2438, 6779, 523…
## $ cohort_start_date    <date> 1938-06-25, 1974-08-11, 1933-10-29, 1993-02-14, …
## $ cohort_end_date      <date> 1941-03-10, 1974-11-13, 1939-07-15, 1996-02-24, …
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_1730355488> [?? x 10]
## # Database: DuckDB v1.1.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
##    cohort_definition_id subject_id cohort_start_date cohort_end_date   age
##                   <int>      <int> <date>            <date>          <int>
##  1                    3          1 1952-02-14        1978-08-26         14
##  2                    3          2 1981-10-23        1988-02-21          4
##  3                    1          4 1986-05-17        1989-12-21         12
##  4                    1          6 1937-03-29        1940-10-20         26
##  5                    2          7 1954-08-25        1969-06-30          2
##  6                    3          8 1933-08-10        1936-11-13          3
##  7                    3          9 1972-05-05        1979-11-23         21
##  8                    3         10 1996-02-18        1998-11-17         26
##  9                    2         11 1947-04-22        1947-11-22          3
## 10                    3         12 1937-02-28        1950-08-16          1
## # ℹ more rows
## # ℹ 5 more variables: age_group <chr>, sex <chr>, in_observation <int>,
## #   prior_observation <int>, future_observation <int>
tictoc::toc()
## 1.277 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.2 [unknown@Linux 6.5.0-1025-azure:R 4.4.1/:memory:]
## $ cohort_definition_id <int> 3, 3, 1, 1, 2, 3, 3, 3, 2, 3, 1, 3, 1, 2, 1, 3, 2…
## $ subject_id           <int> 1, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, …
## $ cohort_start_date    <date> 1952-02-14, 1981-10-23, 1986-05-17, 1937-03-29, …
## $ cohort_end_date      <date> 1978-08-26, 1988-02-21, 1989-12-21, 1940-10-20, …
## $ age                  <int> 14, 4, 12, 26, 2, 3, 21, 26, 3, 1, 24, 5, 3, 2, 1…
## $ age_group            <chr> "0 to 17", "0 to 17", "0 to 17", "18 to 65", "0 t…
## $ sex                  <chr> "Male", "Male", "Female", "Female", "Male", "Fema…
## $ prior_observation    <int> 5157, 1756, 4519, 9584, 967, 1317, 7795, 9544, 12…
tictoc::toc()
## 0.502 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.