5  Creating a cdm reference

5.1 The OMOP CDM layout

The OMOP CDM standardises the structure of health care data. Data is stored across a system of tables with established relationships between them. In other words, the OMOP CDM provides a relational database structure, with version 5.4 of the OMOP CDM shown below.

5.2 Creating a reference to the OMOP common data model

As we saw in Chapter 4, creating a data model in R to represent the OMOP CDM can provide a basis for analytic pipleines using the data. Luckily for us we won’t have to create functions and methods for this ourselves. Instead we will use the omopgenerics package which defines a data model for OMOP CDM data and the CDMConnector package which provides functions for connecting to a OMOP CDM data held in a database.

To see how this works we will use the omock to create example data in the format of the OMOP CDM, which we then copy to a duckdb database.

library(DBI)
library(here)
library(dplyr)
library(omock)
library(omopgenerics)
library(CDMConnector)

cdm_local <- omock::mockCdmReference() |>
    omock::mockPerson(nPerson = 100) |>
    omock::mockObservationPeriod() |>
    omock::mockConditionOccurrence() |>
    omock::mockDrugExposure() |>
    omock::mockObservation() |>
    omock::mockMeasurement() |>
    omock::mockVisitOccurrence() |>
    omock::mockProcedureOccurrence()

db <- DBI::dbConnect(duckdb::duckdb())
CDMConnector::copyCdmTo(con = db,
                        cdm = cdm_local,
                        schema ="main", 
                        overwrite = TRUE)

Now we have OMOP CDM data in a database we can use cdmFromCon() to create our cdm reference. Note that as well as specifying the schema containing our OMOP CDM tables, we will also specify a write schema where any database tables we create during our analysis will be stored (often our OMOP CDM tables will be in a schema that we only have read-access to and we’ll have another schema where we can have write-access where intermediate tables can be created for a given a study).

cdm <- cdmFromCon(db, 
                  cdmSchema = "main", 
                  writeSchema = "main",
                  cdmName = "example_data")
cdm
── # OMOP CDM reference (duckdb) of example_data ───────────────────────────────
• omop tables: person, observation_period, visit_occurrence,
condition_occurrence, drug_exposure, procedure_occurrence, measurement,
observation, cdm_source, concept, vocabulary, concept_relationship,
concept_synonym, concept_ancestor, drug_strength
• cohort tables: -
• achilles tables: -
• other tables: -

We can also specify a write prefix and this will be used whenever permanent tables are created in the write schema. This can be useful when we’re sharing our write schema with others and want to avoid table name conflicts and easily drop tables created as part of a particular study.

cdm <- cdmFromCon(con = db,
                  cdmSchema = "main", 
                  writeSchema = "main", 
                  writePrefix = "my_study_",
                  cdmName = "example_data")

We can see that we now have an object that contains references to all the OMOP CDM tables. We can reference specific tables using the “$” or “[[ … ]]” operators.

cdm$person
# Source:   table<main.person> [?? x 18]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
   person_id gender_concept_id year_of_birth month_of_birth day_of_birth
       <int>             <int>         <int>          <int>        <int>
 1         1              8507          1996              5           27
 2         2              8532          1990             11           22
 3         3              8532          1978              6           13
 4         4              8532          1961              9           27
 5         5              8532          1966              2           23
 6         6              8532          1968              6           11
 7         7              8532          1959              2           15
 8         8              8507          1981             12            8
 9         9              8532          1998              8           17
10        10              8507          1975              5            2
# ℹ more rows
# ℹ 13 more variables: race_concept_id <int>, ethnicity_concept_id <int>,
#   birth_datetime <dttm>, location_id <int>, provider_id <int>,
#   care_site_id <int>, person_source_value <chr>, gender_source_value <chr>,
#   gender_source_concept_id <int>, race_source_value <chr>,
#   race_source_concept_id <int>, ethnicity_source_value <chr>,
#   ethnicity_source_concept_id <int>
cdm[["observation_period"]]
# Source:   table<main.observation_period> [?? x 5]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
   observation_period_id person_id observation_period_s…¹ observation_period_e…²
                   <int>     <int> <date>                 <date>                
 1                     1         1 2004-07-12             2008-05-29            
 2                     2         2 2011-06-08             2016-06-12            
 3                     3         3 1995-05-10             2007-09-22            
 4                     4         4 2011-03-25             2015-02-27            
 5                     5         5 1993-03-08             1996-02-26            
 6                     6         6 2002-05-15             2017-07-19            
 7                     7         7 1984-06-12             1997-08-05            
 8                     8         8 1988-08-31             1998-11-18            
 9                     9         9 2018-05-31             2018-07-10            
10                    10        10 1976-12-05             2001-04-07            
# ℹ more rows
# ℹ abbreviated names: ¹​observation_period_start_date,
#   ²​observation_period_end_date
# ℹ 1 more variable: period_type_concept_id <int>

5.3 CDM attributes

5.3.1 CDM name

Our cdm reference will be associated with a name. By default this name will be taken from the cdm source name field from the cdm source table.

cdm <- cdmFromCon(db,
  cdmSchema = "main", 
  writeSchema = "main")
cdm$cdm_source
# Source:   table<main.cdm_source> [?? x 10]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
  cdm_source_name cdm_source_abbreviation cdm_holder source_description
  <chr>           <lgl>                   <lgl>      <lgl>             
1 mock            NA                      NA         NA                
# ℹ 6 more variables: source_documentation_reference <lgl>,
#   cdm_etl_reference <lgl>, source_release_date <lgl>, cdm_release_date <lgl>,
#   cdm_version <dbl>, vocabulary_version <lgl>
cdmName(cdm)
[1] "mock"
cdmName(cdm$person)
[1] "mock"

However, we can instead set this name when creating our cdm reference.

cdm <- cdmFromCon(db,
  cdmSchema = "main", 
  writeSchema = "main", 
  cdmName = "my_cdm")
cdmName(cdm)
[1] "my_cdm"
cdmName(cdm$person)
[1] "my_cdm"

The cdm reference we have has a class of cdm_reference, while each of the tables have

class(cdm)
[1] "cdm_reference"
class(cdm$person)
[1] "omop_table"            "cdm_table"             "tbl_duckdb_connection"
[4] "tbl_dbi"               "tbl_sql"               "tbl_lazy"             
[7] "tbl"                  

We can see that cdmName() is a generic function, which works for both the cdm reference as a whole and individual tables.

library(sloop)
s3_dispatch(cdmName(cdm))
   cdmName.cdm_reference
   cdmName.default
s3_dispatch(cdmName(cdm$person))
   cdmName.omop_table
   cdmName.cdm_table
   cdmName.tbl_duckdb_connection
   cdmName.tbl_dbi
   cdmName.tbl_sql
   cdmName.tbl_lazy
   cdmName.tbl
   cdmName.default

5.3.2 CDM version

We can also easily check the OMOP CDM version that is being used

cdmVersion(cdm)
[1] "5.3"

5.3.3 CDM Source

Although typically we won’t need to use them for writing study code, we can also access lower-level information on the source, such as the database connection.

attr(cdmSource(cdm), "dbcon")
<duckdb_connection f8700 driver=<duckdb_driver dbdir=':memory:' read_only=FALSE bigint=numeric>>

5.4 Mutability of the cdm reference

An important characteristic of our cdm reference is that we can alter the tables in R, but the OMOP CDM data will not be affected.

For example, let’s say we want to perform a study with only people born in 1970. For this we could filter our person table to only people born in this year.

cdm$person <- cdm$person |> 
  filter(year_of_birth == 1970)

cdm$person
# Source:   SQL [?? x 18]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
  person_id gender_concept_id year_of_birth month_of_birth day_of_birth
      <int>             <int>         <int>          <int>        <int>
1        55              8532          1970              7           14
2        61              8507          1970              4           24
3        74              8532          1970              1            6
# ℹ 13 more variables: race_concept_id <int>, ethnicity_concept_id <int>,
#   birth_datetime <dttm>, location_id <int>, provider_id <int>,
#   care_site_id <int>, person_source_value <chr>, gender_source_value <chr>,
#   gender_source_concept_id <int>, race_source_value <chr>,
#   race_source_concept_id <int>, ethnicity_source_value <chr>,
#   ethnicity_source_concept_id <int>

From now on, when we work with our cdm reference this restriction will continue to have been applied.

cdm$person |> 
    tally()
# Source:   SQL [?? x 1]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
      n
  <dbl>
1     3

The original OMOP CDM data itself however will remain unaffected. And we can see if we create our reference again that the underlying data is unchanged.

cdm <- cdmFromCon(con = db,
                  cdmSchema = "main", 
                  writeSchema = "main", 
                  cdmName = "Synthea Covid-19 data")
cdm$person |> 
    tally()
# Source:   SQL [?? x 1]
# Database: DuckDB v1.1.3 [eburn@Windows 10 x64:R 4.4.0/:memory:]
      n
  <dbl>
1   100

The mutability of our cdm reference is a useful feature for studies as it means we can easily tweak our OMOP CDM data if needed. Meanwhile, leaving the underlying data unchanged is essential so that other study code can run against the data unaffected by any of our changes.

One thing we can’t do though is alter the structure of OMOP CDM tables. For example this code would cause an error as the person table must have the column person_id.

cdm$person <- cdm$person |> 
    rename("new_id" = "person_id")
Error in `newOmopTable()`:
! person_id is not present in table person

In such a case we would have to call the table something else.

cdm$person_new <- cdm$person |> 
    rename("new_id" = "person_id") |> 
    compute()

Now we would have this new table as an additional table in our cdm reference, knowing it was not in the format of one of the core OMOP CDM tables.

cdm
── # OMOP CDM reference (duckdb) of Synthea Covid-19 data ──────────────────────
• omop tables: person, observation_period, visit_occurrence,
condition_occurrence, drug_exposure, procedure_occurrence, measurement,
observation, cdm_source, concept, vocabulary, concept_relationship,
concept_synonym, concept_ancestor, drug_strength
• cohort tables: -
• achilles tables: -
• other tables: -

6 Disconnecting

Once we have finished our analysis we can close our connection to the database behind our cdm reference like so.

cdmDisconnect(cdm) 

7 Further reading