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")).NULLaccepts any visit. Site-specific concepts are passed as ids.- study_window
NULL, or a length-2 vector/listc(start, end)of inclusive bounds on the index visit start date ("YYYY-MM-DD"strings or Dates); either element may beNULL/NA.- min_age
Minimum age at the index visit start (see
age_method);NULLdisables the rule.- min_los_days
Minimum length of stay in days (
visit_end_date - visit_start_date);NULLdisables 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/NULLdisables the rule.- followup_days
Length
Fof the follow-up / outcome horizon in days. Required for every outcome except"none"(whereNULLalso disables follow-up verification) and in-hospital mortality (ignored).- exclude_in_hospital_death
TRUE/FALSE/"auto". WhenTRUE, drops index stays that ended in death: the stay's discharge disposition is one ofdeath_discharge_concept_idsor the patient's earliest known death date (sources:death_sources) is on/before the stay's end. WhenFALSE, only stays starting after the patient's death date are dropped."auto"(default):TRUEexcept for in-hospital and fixed-window mortality.TRUEis rejected for in-hospital mortality andFALSEfor post-discharge / composite outcomes, because either would mislabel deaths.- target_outcome
"all_cause_readmission"(alias"all_cause_readmission_<N>d"withNequal tofollowup_days): anotheroutcome_visit_typevisit starts in(e + G, e + F]."mortality": seemortality_type."readmission_or_death"(alias"composite_readmit_or_death"): readmission or death in(e + G, e + F]."none": cohort only;outcome_flagandoutcome_dateareNA.- 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
Gbetween the window anchor and the start of the outcome window (grace_daysinbuild_readmission_cohort(),gap_daysinbuild_end_of_life_cohort()). Must be smaller thanfollowup_days.- sampling_rule
"random"(default),"first"or"last": which eligible stay represents a patient – the stay minimisinghash(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 whenfollowup_daysisNULL. The result columnfollowup_verifiedis reported either way.- death_discharge_concept_ids
Discharge-disposition concepts meaning "died". Default
4216643for 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 passing4216643).- 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) isdate_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 onoutcome_date.- cohort_name, cohort_description
Metadata for
cohort_definition; the description defaults to a summary of all settings.- materialise
TRUEwrites the cohort to the OMOPcohort/cohort_definitiontables (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.FALSEleaves the database untouched."auto"(default) writes unless the connection is read-only, in which case it warns and behaves likeFALSE(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()(defaultTRUE).
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_idin the visit type,study_start <= s <= study_end(both inclusive), age ats>=min_age,e - s >= min_los_days, wash-in satisfied, and not an in-hospital death.Wash-in: an
observation_periodwithstart <= s - washin_daysandend >= sexists, or, as fallback evidence when observation periods are missing, another visit of the patient started on or befores - washin_days(inclusive).Outcome windows (open on the left, closed on the right): readmission
e + G < start <= e + F; post-discharge deathe + G < death <= e + F; fixed-window deaths + G < death <= s + F; in-hospital deaths <= death <= eor a discharge disposition indeath_discharge_concept_ids.Verified follow-up: an outcome event, or evidence (see
followup_evidence) dated>= anchor + F, where the anchor ise(sfor fixed-window mortality).Leak-free scoping: history lies at or before
sand outcomes strictly after the anchor, so lookback and outcome periods are disjoint.study_windowrestricts only the index date; follow-up and outcome ascertainment may use records afterstudy_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)equalsdefine_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, ...)equalsdefine_study_cohort(target_outcome = "mortality", mortality_type = M, followup_days = F, gap_days = G, min_age = A, ...)(for"composite_readmit_or_death"usetarget_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)
