# Adding patient demographics

## 1.1 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, source = "duckdb")

    cdm$person |>
      dplyr::glimpse()

    ## Rows: ??
    ## Columns: 5
    ## Database: DuckDB 1.5.0 [root@Darwin 25.5.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> 8507, 8507, 8532, 8507, 8507, 8532, 8532, 8532, 8…
    ## $ year_of_birth        <int> 1936, 1908, 1964, 1941, 1968, 1922, 1939, 1933, 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 1.5.0 [root@Darwin 25.5.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> 1936-01-01, 1908-01-01, 1964-01-01, 194…
    ## $ observation_period_end_date   <date> 1962-02-13, 1946-10-30, 1976-09-06, 199…
    ## $ 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.

## 1.2 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …

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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
    ## $ age_from_year_of_birth    <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …

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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
    ## $ age_from_year_of_birth    <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ sex                       <chr> "Male", "Female", "Female", "Male", "Female"…

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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
    ## $ age_from_year_of_birth    <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ sex                       <chr> "Male", "Female", "Female", "Male", "Female"…
    ## $ in_observation            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
    ## $ prior_observation         <dbl> 3762, 2461, 2049, 774, 4096, 2618, 7325, 936…
    ## $ future_observation        <dbl> 4757, 6112, 8187, 7104, 583, 7029, 9841, 109…

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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
    ## $ age_from_year_of_birth    <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ sex                       <chr> "Male", "Female", "Female", "Male", "Female"…
    ## $ in_observation            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
    ## $ prior_observation         <dbl> 3762, 2461, 2049, 774, 4096, 2618, 7325, 936…
    ## $ future_observation        <dbl> 4757, 6112, 8187, 7104, 583, 7029, 9841, 109…

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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ person_id                 <int> 6983, 5432, 3094, 8512, 6623, 4975, 1435, 69…
    ## $ condition_start_date      <date> 1946-04-20, 1944-09-27, 1917-08-11, 1954-02…
    ## $ condition_end_date        <date> 1957-03-24, 1960-07-23, 1924-08-15, 1973-03…
    ## $ condition_occurrence_id   <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 1…
    ## $ condition_concept_id      <int> 4094814, 4134304, 258780, 4294548, 4296205, …
    ## $ condition_type_concept_id <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
    ## $ age                       <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ age_group                 <chr> "0 to 17", "0 to 17", "0 to 17", "0 to 17", …
    ## $ age_from_year_of_birth    <dbl> 10, 6, 5, 2, 11, 7, 20, 25, 25, 5, 5, 2, 3, …
    ## $ sex                       <chr> "Male", "Female", "Female", "Male", "Female"…
    ## $ in_observation            <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
    ## $ prior_observation         <dbl> 3762, 2461, 2049, 774, 4096, 2618, 7325, 936…
    ## $ future_observation        <dbl> 4757, 6112, 8187, 7104, 583, 7029, 9841, 109…

## 1.3 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ cohort_definition_id <int> 3, 3, 1, 3, 3, 2, 3, 1, 3, 1, 2, 2, 2, 1, 1, 1, 2…
    ## $ subject_id           <int> 8984, 7352, 722, 1198, 8018, 284, 9311, 1892, 737…
    ## $ cohort_start_date    <date> 1963-04-06, 1949-12-24, 1936-11-11, 1963-12-07, …
    ## $ cohort_end_date      <date> 1963-05-31, 1957-05-19, 1953-10-01, 1993-05-05, …

    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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ cohort_definition_id <int> 3, 3, 1, 3, 3, 2, 3, 1, 3, 1, 2, 2, 2, 1, 1, 1, 2…
    ## $ subject_id           <int> 8984, 7352, 722, 1198, 8018, 284, 9311, 1892, 737…
    ## $ cohort_start_date    <date> 1963-04-06, 1949-12-24, 1936-11-11, 1963-12-07, …
    ## $ cohort_end_date      <date> 1963-05-31, 1957-05-19, 1953-10-01, 1993-05-05, …
    ## $ age                  <dbl> 11, 0, 25, 3, 6, 1, 0, 8, 20, 47, 25, 28, 22, 22,…
    ## $ age_group            <chr> "0 to 17", "0 to 17", "18 to 65", "0 to 17", "0 t…
    ## $ sex                  <chr> "Male", "Male", "Female", "Male", "Female", "Fema…
    ## $ in_observation       <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
    ## $ prior_observation    <dbl> 4113, 357, 9446, 1436, 2430, 440, 271, 3159, 7440…
    ## $ future_observation   <dbl> 11180, 5800, 6875, 12109, 5782, 7649, 5733, 12670…

## 1.4 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ cohort_definition_id <int> 2, 3, 2, 2, 2, 1, 3, 1, 3, 2, 3, 3, 2, 2, 2, 2, 1…
    ## $ subject_id           <int> 2126, 3574, 7237, 8691, 3931, 5894, 27, 7379, 847…
    ## $ cohort_start_date    <date> 1969-01-12, 1955-02-25, 1959-03-10, 1975-07-01, …
    ## $ cohort_end_date      <date> 1972-04-06, 1959-10-03, 1963-05-05, 1993-09-01, …

    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_157_1784971889> [?? x 10]
    ## # Database: DuckDB 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ##    cohort_definition_id subject_id cohort_start_date cohort_end_date   age
    ##                   <int>      <int> <date>            <date>          <dbl>
    ##  1                    2       2126 1969-01-12        1972-04-06          8
    ##  2                    3       3574 1955-02-25        1959-10-03          8
    ##  3                    2       7237 1959-03-10        1963-05-05         22
    ##  4                    2       8691 1975-07-01        1993-09-01          7
    ##  5                    2       3931 1952-10-07        1956-06-21         35
    ##  6                    1       5894 1975-06-18        1977-12-31          8
    ##  7                    3         27 1937-06-02        1946-11-23          8
    ##  8                    1       7379 1939-03-10        1943-04-09         33
    ##  9                    3       8477 1934-08-14        1947-05-21         27
    ## 10                    2       1106 1925-05-14        1932-12-17         16
    ## # ℹ more rows
    ## # ℹ 5 more variables: age_group <chr>, sex <chr>, in_observation <dbl>,
    ## #   prior_observation <dbl>, future_observation <dbl>

    tictoc::toc()

    ## 0.654 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 1.5.0 [root@Darwin 25.5.0:R 4.4.1/:memory:]
    ## $ cohort_definition_id <int> 2, 2, 1, 2, 1, 3, 1, 1, 2, 2, 1, 2, 2, 3, 1, 1, 1…
    ## $ subject_id           <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15…
    ## $ cohort_start_date    <date> 1952-01-01, 1920-01-24, 1968-12-31, 1983-09-29, …
    ## $ cohort_end_date      <date> 1957-07-13, 1921-08-13, 1975-04-26, 1997-05-16, …
    ## $ age                  <dbl> 16, 12, 4, 42, 12, 3, 50, 6, 4, 5, 0, 1, 2, 39, 1…
    ## $ age_group            <chr> "0 to 17", "0 to 17", "0 to 17", "18 to 65", "0 t…
    ## $ sex                  <chr> "Male", "Male", "Female", "Male", "Male", "Female…
    ## $ prior_observation    <dbl> 5844, 4406, 1826, 15611, 4723, 1113, 18623, 2424,…

    tictoc::toc()

    ## 0.242 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.