library(DBI)
library(here)
library(dplyr)
library(omock)
library(omopgenerics)
library(CDMConnector)
<- omock::mockCdmReference() |>
cdm_local ::mockPerson(nPerson = 100) |>
omock::mockObservationPeriod() |>
omock::mockConditionOccurrence() |>
omock::mockDrugExposure() |>
omock::mockObservation() |>
omock::mockMeasurement() |>
omock::mockVisitOccurrence() |>
omock::mockProcedureOccurrence()
omock
<- DBI::dbConnect(duckdb::duckdb())
db ::copyCdmTo(con = db,
CDMConnectorcdm = cdm_local,
schema ="main",
overwrite = TRUE)
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.
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).
<- cdmFromCon(db,
cdm 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.
<- cdmFromCon(con = db,
cdm 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.
$person cdm
# 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>
"observation_period"]] cdm[[
# 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.
<- cdmFromCon(db,
cdm cdmSchema = "main",
writeSchema = "main")
$cdm_source cdm
# 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.
<- cdmFromCon(db,
cdm 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.
$person <- cdm$person |>
cdmfilter(year_of_birth == 1970)
$person cdm
# 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.
$person |>
cdmtally()
# 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.
<- cdmFromCon(con = db,
cdm cdmSchema = "main",
writeSchema = "main",
cdmName = "Synthea Covid-19 data")
$person |>
cdmtally()
# 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.
$person <- cdm$person |>
cdmrename("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.
$person_new <- cdm$person |>
cdmrename("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)