Skip to contents

Defines a leak-free index-stay study cohort (one stay per patient) from OMOP visits, with an optional outcome. This is a single parameterised template that generalises build_readmission_cohort() and build_end_of_life_cohort(): both now run on the same engine, so a cohort defined here and one built by a legacy builder with equivalent settings are identical row for row. It adds emergency / outpatient / custom index visit types, a study window on the index date, and a cohort-only mode (target_outcome = "none").

Usage

define_study_cohort(
  con,
  visit_type = "inpatient",
  study_window = NULL,
  min_age = 18,
  min_los_days = "auto",
  washin_days = 365,
  followup_days = 30,
  exclude_in_hospital_death = "auto",
  target_outcome = "all_cause_readmission",
  mortality_type = "post_discharge",
  outcome_visit_type = "inpatient",
  gap_days = 0,
  sampling_rule = "random",
  random_state = 42,
  require_verified_followup = TRUE,
  death_discharge_concept_ids = NULL,
  death_sources = NULL,
  followup_evidence = NULL,
  age_method = "year_difference",
  cohort_definition_id = 1,
  outcome_cohort_id = NULL,
  cohort_name = NULL,
  cohort_description = NULL,
  materialise = "auto",
  table_name = NULL,
  schema = NULL,
  attrition = TRUE
)

Arguments

con

Active DuckDB DBI connection to an OMOP CDM v5.4 database. May be read-only (see materialise).

visit_type

Index visit type: "inpatient" (visit_concept_id 9201), "emergency" (9203), "outpatient" (9202), explicit visit_concept_ids, or a list/vector mixing both (c("inpatient", "emergency")). NULL accepts any visit. Site-specific concepts are passed as ids.

study_window

NULL, or a length-2 vector/list c(start, end) of inclusive bounds on the index visit start date ("YYYY-MM-DD" strings or Dates); either element may be NULL/NA.

min_age

Minimum age at the index visit start (see age_method); NULL disables the rule.

min_los_days

Minimum length of stay in days (visit_end_date - visit_start_date); NULL disables the rule. "auto" (default) follows the legacy builders: 1 for readmission, post-discharge mortality, composite and cohort-only; 0 for in-hospital mortality; no restriction for fixed-window mortality.

washin_days

Required prior observation in days (see Definitions); 0/NULL disables the rule.

followup_days

Length F of the follow-up / outcome horizon in days. Required for every outcome except "none" (where NULL also disables follow-up verification) and in-hospital mortality (ignored).

exclude_in_hospital_death

TRUE/FALSE/"auto". When TRUE, drops index stays that ended in death: the stay's discharge disposition is one of death_discharge_concept_ids or the patient's earliest known death date (sources: death_sources) is on/before the stay's end. When FALSE, only stays starting after the patient's death date are dropped. "auto" (default): TRUE except for in-hospital and fixed-window mortality. TRUE is rejected for in-hospital mortality and FALSE for post-discharge / composite outcomes, because either would mislabel deaths.

target_outcome

"all_cause_readmission" (alias "all_cause_readmission_<N>d" with N equal to followup_days): another outcome_visit_type visit starts in (e + G, e + F]. "mortality": see mortality_type. "readmission_or_death" (alias "composite_readmit_or_death"): readmission or death in (e + G, e + F]. "none": cohort only; outcome_flag and outcome_date are NA.

mortality_type

Used when target_outcome = "mortality": "post_discharge" (death in (e + G, e + F]), "fixed_window" (death in (s + G, s + F], SARD-style) or "in_hospital".

outcome_visit_type

Visit types that count as readmissions (default "inpatient").

gap_days

Gap G between the window anchor and the start of the outcome window (grace_days in build_readmission_cohort(), gap_days in build_end_of_life_cohort()). Must be smaller than followup_days.

sampling_rule

"random" (default), "first" or "last": which eligible stay represents a patient – the stay minimising hash(visit_occurrence_id, random_state) (ties by id), the earliest start, or the latest start.

random_state

Seed of the "random" rule (default 42).

require_verified_followup

Keep a stay only if it has an outcome event or verified follow-up (default TRUE). Not applicable to in-hospital mortality or when followup_days is NULL. The result column followup_verified is reported either way.

death_discharge_concept_ids

Discharge-disposition concepts meaning "died". Default 4216643 for readmission / cohort-only, c(4216643, 4155309) for mortality outcomes (the legacy end-of-life convention; 4155309 is 'Ileal part' in Athena and is written by this package's PCORnet ETL for 'discharged against medical advice', so consider passing 4216643).

death_sources

Where a patient's death date comes from: "death_table" and/or "discharge_disposition" (the end date of any visit discharged to a death concept). Default "death_table" for readmission / cohort-only, both for mortality outcomes.

followup_evidence

Records that verify follow-up when dated on/after anchor + F: any of "observation_period" (period end), "visit" (another visit's start), "measurement", "condition", "drug" (record dates) and "death". Default: the four record types for readmission / cohort-only, all six for mortality outcomes.

age_method

"year_difference" (default; legacy, needed for equivalence) is date_diff('year', birth, index_start), the number of calendar-year boundaries crossed, which overstates age by one year before the birthday. "completed_years" is the exact age in whole years. Missing birth month / day count as 1.

cohort_definition_id

Id written to cohort / cohort_definition (default 1).

outcome_cohort_id

Optional id under which outcome events are written (when materialise = TRUE). Readmission outcomes write each qualifying readmission visit (start, end); mortality and composite outcomes write a one-day event on outcome_date.

cohort_name, cohort_description

Metadata for cohort_definition; the description defaults to a summary of all settings.

materialise

TRUE writes the cohort to the OMOP cohort / cohort_definition tables (created if missing), exactly as the legacy builders do; rows for the same ids are replaced, so re-running is idempotent. It raises an error on a read-only connection. FALSE leaves the database untouched. "auto" (default) writes unless the connection is read-only, in which case it warns and behaves like FALSE (so the same call works on read-only connections).

table_name

Optional table name (up to three dot-separated plain identifiers) to persist the full result.

schema

Optional schema (or catalog.schema) holding the CDM tables; default resolved via the search path.

attrition

Compute the stepwise patient attrition via compute_attrition() (default TRUE).

Value

A data.frame, one row per patient ordered by subject_id, with columns cohort_definition_id, subject_id, cohort_start_date (index visit start), cohort_end_date (index visit end), visit_occurrence_id, visit_concept_id, outcome_flag, outcome_date, followup_verified, age_at_index, los_days, discharged_to_concept_id and target_outcome. The attributes attrition (the compute_attrition() table: visit type, study window, age, length of stay, wash-in, in-hospital death, verified follow-up), summary (the get_cohort_summary() row, when materialised into the default schema) and definition (a list of the resolved settings) carry the audit trail.

Definitions

With s the index visit start date and e its end date (DATE values; + n adds n days), F = followup_days and G = gap_days:

  • Index stay: visit_concept_id in the visit type, study_start <= s <= study_end (both inclusive), age at s >= min_age, e - s >= min_los_days, wash-in satisfied, and not an in-hospital death.

  • Wash-in: an observation_period with start <= s - washin_days and end >= s exists, or, as fallback evidence when observation periods are missing, another visit of the patient started on or before s - washin_days (inclusive).

  • Outcome windows (open on the left, closed on the right): readmission e + G < start <= e + F; post-discharge death e + G < death <= e + F; fixed-window death s + G < death <= s + F; in-hospital death s <= death <= e or a discharge disposition in death_discharge_concept_ids.

  • Verified follow-up: an outcome event, or evidence (see followup_evidence) dated >= anchor + F, where the anchor is e (s for fixed-window mortality).

  • Leak-free scoping: history lies at or before s and outcomes strictly after the anchor, so lookback and outcome periods are disjoint. study_window restricts only the index date; follow-up and outcome ascertainment may use records after study_end.

Reproducing the legacy builders

Every mapping below is asserted against stored golden results in the test suite.

  • build_readmission_cohort(target_visit_concept_ids = T, outcome_visit_concept_ids = O, followup_window_days = F, grace_days = G, washin_days = W, index_selection_rule = R, random_state = S, require_verified_followup = V) equals define_study_cohort(visit_type = T, outcome_visit_type = O, followup_days = F, gap_days = G, washin_days = W, sampling_rule = R, random_state = S, require_verified_followup = V, min_age = 18).

  • build_end_of_life_cohort(mortality_type = M, mortality_window_days = F, gap_days = G, min_age = A, ...) equals define_study_cohort(target_outcome = "mortality", mortality_type = M, followup_days = F, gap_days = G, min_age = A, ...) (for "composite_readmit_or_death" use target_outcome = "readmission_or_death").

Examples

db <- tempfile(fileext = ".duckdb")
build_schema(db_path = db)
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpFV3zps/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
#> Schema built at /tmp/RtmpFV3zps/file1c3a191ee442.duckdb 
con <- DBI::dbConnect(duckdb::duckdb(), dbdir = db)
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpFV3zps/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
DBI::dbExecute(con, "
  INSERT INTO person (person_id, gender_concept_id, year_of_birth,
                      race_concept_id, ethnicity_concept_id)
  VALUES (1, 8507, 1960, 8527, 38003564), (2, 8532, 1975, 8527, 38003564)")
#> [1] 2
DBI::dbExecute(con, "
  INSERT INTO visit_occurrence (visit_occurrence_id, person_id, visit_concept_id, visit_start_date,
                                visit_end_date, visit_type_concept_id)
  VALUES (1, 1, 9201, DATE '2019-03-01', DATE '2019-03-05', 32827),
         (2, 1, 9201, DATE '2019-03-20', DATE '2019-03-22', 32827),
         (3, 2, 9201, DATE '2019-05-01', DATE '2019-05-04', 32827),
         (4, 2, 9202, DATE '2019-07-01', DATE '2019-07-01', 32827)")
#> [1] 4
cohort <- define_study_cohort(
  con,
  visit_type = "inpatient",
  study_window = c("2017-01-01", "2020-12-31"),
  min_age = 18,
  min_los_days = 1,
  washin_days = 0,
  followup_days = 30,
  exclude_in_hospital_death = TRUE,
  target_outcome = "all_cause_readmission_30d",
  sampling_rule = "first",
  materialise = FALSE
)
cohort[, c("subject_id", "visit_occurrence_id", "outcome_flag")]
#>   subject_id visit_occurrence_id outcome_flag
#> 1          1                   1            1
#> 2          2                   3            0
attr(cohort, "attrition")
#>   step_number                                                       step_name
#> 1           1                                      Index visit type in (9201)
#> 2           2 Index visit start between 2017-01-01 and 2020-12-31 (inclusive)
#> 3           3                                        Age >= 18 years at index
#> 4           4                                      Length of stay >= 1 day(s)
#> 5           5                              No in-hospital death at index stay
#> 6           6        Verified follow-up (evidence >= 30 days after discharge)
#>   subjects_retained subjects_dropped percent_retained
#> 1                 2                0              100
#> 2                 2                0              100
#> 3                 2                0              100
#> 4                 2                0              100
#> 5                 2                0              100
#> 6                 2                0              100
DBI::dbDisconnect(con, shutdown = TRUE)
unlink(db)