From 3c83e669aef8ec539bcff42d16d379e640c4b208 Mon Sep 17 00:00:00 2001 From: Chris Knoll Date: Mon, 8 Jul 2024 13:36:06 -0400 Subject: [PATCH] Added support for list of age breaks, supporting overlapping age break intervals. Using StandardAnalysisAPI 1.6.0-SNAPSHOT. Updated StrataSettings R6 class with new field: ageBreakList. Fixes #30. --- R/Builders.R | 4 +- R/Classes.R | 22 +- docs/articles/using-cohortincidence.html | 195 +++--------------- docs/news/index.html | 9 + docs/pkgdown.yml | 2 +- docs/reference/StrataSettings.html | 4 + docs/reference/createStrataSettings.html | 8 +- inst/java/CohortIncidence-4.0.0.jar | Bin 20325 -> 20925 bytes ...ardized-analysis-specs-1.6.0-SNAPSHOT.jar} | Bin 84531 -> 84660 bytes .../CohortIncidenceQueryBuilder.java | 60 +++--- .../cohortincidence/sql/ageGroupJoin.sql | 3 + .../cohortincidence/sql/incidenceAnalysis.sql | 14 +- .../sql/outcomeStrataQueryTemplate.sql | 1 + .../sql/tarStrataQueryTemplate.sql | 1 + .../cohortincidence/Incidence_5_0_Test.java | 29 +++ .../timeAtRisk/strataByAgeBreakListTest.json | 43 ++++ .../timeAtRisk/strataByAgeBreakList_PREP.json | 95 +++++++++ .../strataByAgeBreakList_VERIFY.json | 144 +++++++++++++ man/StrataSettings.Rd | 2 + man/createStrataSettings.Rd | 8 +- pom.xml | 2 +- vignettes/using-cohortincidence.Rmd | 26 ++- 22 files changed, 461 insertions(+), 211 deletions(-) rename inst/java/{standardized-analysis-specs-1.5.0.jar => standardized-analysis-specs-1.6.0-SNAPSHOT.jar} (76%) create mode 100644 java/main/resources/resources/cohortincidence/sql/ageGroupJoin.sql create mode 100644 java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakListTest.json create mode 100644 java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_PREP.json create mode 100644 java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_VERIFY.json diff --git a/R/Builders.R b/R/Builders.R index 97aa92c..12e7ad2 100644 --- a/R/Builders.R +++ b/R/Builders.R @@ -150,13 +150,13 @@ createCohortSubgroup <- function (id, name, description, cohortRef) { #' @param ageBreaks a vector of integers indicating the age group bounds. #' @return an R list containing name-value pairs that will serialize into a org.ohdsi.analysis.cohortincidence.design.StratifySettings JSON format. #' @export -createStrataSettings <- function (byAge = F, byGender = F, byYear = F, ageBreaks) { +createStrataSettings <- function (byAge = F, byGender = F, byYear = F, ageBreaks, ageBreakList) { strataSettings <- StrataSettings$new() strataSettings$byAge <- byAge; strataSettings$byGender <- byGender; strataSettings$byYear <- byYear; - if(byAge == T && missing(ageBreaks)) stop ("Error: ageBreaks must be a list of integers with at least 1 element") + if(byAge == T && (missing(ageBreaks) || missing(ageBreakList))) stop ("Error: when byAge = TRUE, ageBreaks or ageBreakList must be provided.") if (!missing(ageBreaks)) strataSettings$ageBreaks <- ageBreaks; return(strataSettings); diff --git a/R/Classes.R b/R/Classes.R index b5e3913..7e0633a 100644 --- a/R/Classes.R +++ b/R/Classes.R @@ -757,7 +757,8 @@ StrataSettings <- R6::R6Class("StrataSettings", .byAge = F, .byGender = F, .byYear = F, - .ageBreaks = NA + .ageBreaks = NA, + .ageBreakList = NA ), active = list ( #' @field byAge enables stratification by age @@ -803,6 +804,21 @@ StrataSettings <- R6::R6Class("StrataSettings", private$.ageBreaks <- ageBreaks self } + }, + #' @field ageBreakList a list of age breaks + ageBreakList = function(ageBreakList) { + if (missing(ageBreakList)) { + private$.ageBreakList + } else { + # check type + checkmate::assertList(as.list(ageBreakList), types="list") + checkmate::assertTRUE(all(sapply(ageBreakList, + function(x) { + checkmate::testList(x) && all(sapply(x, checkmate::testNumeric)) + }))) + private$.ageBreakList <- ageBreakList + self + } } ), public = list( @@ -816,6 +832,7 @@ StrataSettings <- R6::R6Class("StrataSettings", if ("byGender" %in% names (dataList)) self$byGender <- dataList$byGender if ("byYear" %in% names (dataList)) self$byYear <- dataList$byYear if ("ageBreaks" %in% names (dataList)) self$ageBreaks <- dataList$ageBreaks + if ("ageBreakList" %in% names (dataList)) self$ageBreakList <- dataList$ageBreakList }, #' @description @@ -825,7 +842,8 @@ StrataSettings <- R6::R6Class("StrataSettings", byAge = jsonlite::unbox(private$.byAge), byGender = jsonlite::unbox(private$.byGender), byYear = jsonlite::unbox(private$.byYear), - ageBreaks = .toJsonArray(private$.ageBreaks) + ageBreaks = .toJsonArray(private$.ageBreaks), + ageBreakList = lapply(private$.ageBreakList, .toJsonArray) )) }, #' @description diff --git a/docs/articles/using-cohortincidence.html b/docs/articles/using-cohortincidence.html index 5887fba..48eea8d 100644 --- a/docs/articles/using-cohortincidence.html +++ b/docs/articles/using-cohortincidence.html @@ -85,7 +85,7 @@

Using CohortIncidence

Christopher Knoll

-

2024-06-27

+

2024-07-08

Source: vignettes/using-cohortincidence.Rmd @@ -315,13 +315,27 @@

Using age, gender and start year strata settings with the CohortIncidence::createStrataSettings) function:

-irDesignWithStrata <- CohortIncidence::createIncidenceDesign(targetDefs = list(t1),
-                                                   outcomeDefs = list(o1),
-                                                   tars=list(tar1),
-                                                   analysisList = list(analysis1),
-                                                   subgroups = list(subgroup1),
-                                                   #add by age and by gender strata, but don't do by start year.
-                                                   strataSettings = CohortIncidence::createStrataSettings(byGender=T, byAge=T, ageBreaks = c(17,34,65)))
+irDesignWithStrata <- + CohortIncidence::createIncidenceDesign( + targetDefs = list(t1), + outcomeDefs = list(o1), + tars = list(tar1), + analysisList = list(analysis1), + subgroups = list(subgroup1), + #add by age and by gender strata, but don't do by start year. + strataSettings = CohortIncidence::createStrataSettings( + byGender = T, + byAge = T, + ageBreaks = list(17, 34, 65), + ageBreakList = list(list(25), list(65)) + ) + ) +

In the above example, thare are 2 ways of specifying the age breaks: +ageBreaks and ageBreakList. ageBreaks creates a single age break +specification, while ageBreakList allows you to specify a list of +breaks. All breaks defined in ageBreaks and ageBreakList will be used if +specified. If byAge is TRUE, you must specify at least one age break +specification either in ageBreaks or ageBreakList.

Using executeAnalysis() @@ -341,149 +355,11 @@

Using executeAnalysis()executeResults <- CohortIncidence::executeAnalysis(connectionDetails = connectionDetails, incidenceDesign = irDesign, buildOptions = buildOptions)

-

The results will contain the same table structure as the results -schema incidence_summary:

- ---- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameDescription
REF_IDThe reference id specified in buildOptions() to track results to the -analysis execution.
SOURCE_NAMEThe name of the source for these results
TARGET_COHORT_DEFIITION_IDThe cohort ID of the target population
TARGET_NAMEThe name of the target cohort
TAR_IDThe TAR identifier
TAR_START_WITHIndicates if the TAR starts with the ‘start’ or ‘end’ of the target -cohort episode
TAR_START_OFFSETThe days added to the date field specified in TAR_START_WITH
TAR_END_WITHIndicates if the TAR ends with the ‘start’ or ‘end’ of the target -cohort episode
TAR_END_OFFSETThe days added to the date field specified in TAR_END_WITH
SUBGROUP_IDThe subgroup identifier
SUBGROUP_NAMEThe name of the subgroup
OUTCOME_IDThe outcome identifier
OUTCOME_COHORT_DEFINITION_IDThe cohort ID of the outcome population
OUTCOME_NAMEThe outcome name
CLEAN_WINDOWThe clean window for this outcome definition
AGE_IDThe age ID for this strata representing the age band specified in -the strata settings
AGE_GROUP_NAMEThe name for this age group
GENDER_IDThe gender concept ID for this gender strata
GENDER_NAMEThe name of the gender
START_YEARThe year strata, defined by using the year the TAR started
PERSONS_AT_RISK_PEDistinct persons at risk before removing excluded time from TAR
PERSONS_AT_RISKDistinct persons at risk after removing excluded time from TAR. A -person must have at least 1 day TAR to be included.
PERSON_DAYS_PETotal TAR (in days) before excluded time was removed from TAR.
PERSON_DAYSTotal TAR (in days) after excluded time was removed from TAR.
PERSON_OUTCOMES_PEDistinct persons with outcome before removing excluded time from -TAR
PERSON_OUTCOMESDistinct persons with outcome after removing excluded time from TAR. -A person must have at least 1 day TAR to be included.
OUTCOMES_PENumber of cases before excluding TAR.
OUTCOMESNumber of cases after excluding TAR.
INCIDENCE_PROPORTION_P100PThe Incidence Proportion (per 100 people), calculated by -person_outcomes / persons_at_risk * 100
INCIDENCE_RATE_P100PYThe Incidence Rate (per 100 person years), calculated by outcomes / -person_days / 365.25 * 100
+

executeAnalysis() will return a list of dataframes with +the following fields: - incidenceSummary - targetDef - outcomeDef - +tarDef - ageGroupDef - subgroupDef

+

These dataframes follow the same structure as the corresponding +tables described in the Database Preparation section.

@@ -791,7 +667,7 @@

Build analysis SQL from design#> /* #> 5) aggregate tar and excluded+outcome #> */ -#> WITH tar_overall (target_cohort_definition_id, tar_id, subgroup_id, subject_id, start_date, end_date, age_group_id, gender_id, start_year) +#> WITH tar_overall (target_cohort_definition_id, tar_id, subgroup_id, subject_id, start_date, end_date, age, gender_id, start_year) #> AS ( #> SELECT te.cohort_definition_id as target_cohort_definition_id, #> te.tar_id, @@ -799,14 +675,11 @@

Build analysis SQL from design#> te.subject_id, #> te.start_date, #> te.end_date, -#> ag.age_group_id, +#> YEAR(te.start_date) - p.year_of_birth as age, #> p.gender_concept_id as gender_id, #> YEAR(te.start_date) as start_year #> FROM #TTAR_erafied te #> JOIN mycdm.person p on te.subject_id = p.person_id -#> LEFT JOIN myresults.age_group_def ag ON YEAR(te.start_date) - p.year_of_birth >= coalesce(ag.min_age, -999) -#> and YEAR(te.start_date) - p.year_of_birth < coalesce(ag.max_age, 999) -#> and ag.ref_id = 1 #> ) #> select target_cohort_definition_id, tar_id, subgroup_id, age_group_id, gender_id, start_year, person_days_pe, persons_at_risk_pe #> INTO #tar_agg @@ -820,11 +693,12 @@

Build analysis SQL from design#> SUM(CAST((DATEDIFF(day,t1.start_date,t1.end_date) + 1) as bigint)) as person_days_pe, #> COUNT(distinct t1.subject_id) as persons_at_risk_pe #> FROM tar_overall t1 +#> #> GROUP BY t1.target_cohort_definition_id, t1.tar_id, t1.subgroup_id #> ) T_OVERALL #> ; #> -#> WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, age_group_id, gender_id, start_year, excluded_days, tar_days, outcomes_pe, outcomes) +#> WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, age, gender_id, start_year, excluded_days, tar_days, outcomes_pe, outcomes) #> AS ( #> SELECT #> t1.cohort_definition_id as target_cohort_definition_id, @@ -832,7 +706,7 @@

Build analysis SQL from design#> t1.subgroup_id, #> op.outcome_id, #> t1.subject_id, -#> ag.age_group_id, +#> YEAR(t1.start_date) - p.year_of_birth as age, #> p.gender_concept_id as gender_id, #> YEAR(t1.start_date) as start_year, #> coalesce(e1.person_days, 0) as excluded_days, @@ -841,9 +715,6 @@

Build analysis SQL from design#> coalesce(o1.outcomes, 0) as outcomes #> FROM #TTAR_erafied t1 #> JOIN mycdm.person p ON t1.subject_id = p.person_id -#> LEFT JOIN myresults.age_group_def ag ON YEAR(t1.start_date) - p.year_of_birth >= coalesce(ag.min_age, -999) -#> AND YEAR(t1.start_date) - p.year_of_birth < coalesce(ag.max_age, 999) -#> AND ag.ref_id = 1 #> JOIN ( -- get the list of TTSO of anyone with excluded time or outcomes to limit result #> select target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, start_date FROM #excluded_person_days #> UNION -- will remove dupes @@ -886,6 +757,7 @@

Build analysis SQL from design#> SUM(t1.outcomes_pe) as outcomes_pe, #> SUM(t1.outcomes) as outcomes #> FROM outcomes_overall t1 +#> #> GROUP BY target_cohort_definition_id, tar_id, subgroup_id, outcome_id #> ) O_OVERALL #> ; @@ -937,7 +809,6 @@

Build analysis SQL from design#> AND coalesce(ta.age_group_id,-1) = coalesce(oa.age_group_id,-1) #> AND coalesce(ta.gender_id,-1) = coalesce(oa.gender_id,-1) #> AND coalesce(ta.start_year, -1) = coalesce(oa.start_year,-1) -#> LEFT JOIN myresults.age_group_def ag on ag.age_group_id = ta.age_group_id #> LEFT JOIN mycdm.concept c on c.concept_id = ta.gender_id #> ; #> diff --git a/docs/news/index.html b/docs/news/index.html index 0d2d3ae..9ba26fc 100644 --- a/docs/news/index.html +++ b/docs/news/index.html @@ -60,6 +60,15 @@

Changelog

Source:
NEWS.md

+
+ +
+

Major Changes

+

The results DDL has been split into 5 tables: - incidence_summary - target_def - outcome_def - tar_def - age_group_def - subgroup_def

+

executeAnalysis() will return a list of dataframes with the following fields: - incidenceSummary - targetDef - outcomeDef - tarDef - ageGroupDef - subgroupDef

+

These dataframes follow the same structure as the corresponding table.

+
+
diff --git a/docs/pkgdown.yml b/docs/pkgdown.yml index fc41a56..0c78760 100644 --- a/docs/pkgdown.yml +++ b/docs/pkgdown.yml @@ -3,5 +3,5 @@ pkgdown: 2.0.9 pkgdown_sha: ~ articles: using-cohortincidence: using-cohortincidence.html -last_built: 2024-06-27T19:57Z +last_built: 2024-07-08T17:23Z diff --git a/docs/reference/StrataSettings.html b/docs/reference/StrataSettings.html index e4514e6..2ec25c0 100644 --- a/docs/reference/StrataSettings.html +++ b/docs/reference/StrataSettings.html @@ -93,6 +93,10 @@

Active bindings

a list of age breaks with at least 1 member

+
ageBreakList
+

a list of age breaks

+ +

diff --git a/docs/reference/createStrataSettings.html b/docs/reference/createStrataSettings.html index b3b9b3a..1ac774e 100644 --- a/docs/reference/createStrataSettings.html +++ b/docs/reference/createStrataSettings.html @@ -66,7 +66,13 @@

Creates R6 object for StrataSettings

-
createStrataSettings(byAge = F, byGender = F, byYear = F, ageBreaks)
+
createStrataSettings(
+  byAge = F,
+  byGender = F,
+  byYear = F,
+  ageBreaks,
+  ageBreakList
+)
diff --git a/inst/java/CohortIncidence-4.0.0.jar b/inst/java/CohortIncidence-4.0.0.jar index a67ff8b8b6b4a6180bd63600a6b12f4b09a82e95..e451c75718aa722a295f53320c92a083d86dca16 100644 GIT binary patch delta 14287 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 delta 13856 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 diff --git a/inst/java/standardized-analysis-specs-1.5.0.jar b/inst/java/standardized-analysis-specs-1.6.0-SNAPSHOT.jar similarity index 76% rename from inst/java/standardized-analysis-specs-1.5.0.jar rename to inst/java/standardized-analysis-specs-1.6.0-SNAPSHOT.jar index 6956b6765de6f363cb4b7fc889fb8a0943afbf32..3b1d5bd2ecc54fdfe39900b244c73743562f0535 100644 GIT binary patch delta 7180 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 diff --git a/java/main/java/org/ohdsi/cohortincidence/CohortIncidenceQueryBuilder.java b/java/main/java/org/ohdsi/cohortincidence/CohortIncidenceQueryBuilder.java index 80939c3..620dcfa 100644 --- a/java/main/java/org/ohdsi/cohortincidence/CohortIncidenceQueryBuilder.java +++ b/java/main/java/org/ohdsi/cohortincidence/CohortIncidenceQueryBuilder.java @@ -26,6 +26,7 @@ public class CohortIncidenceQueryBuilder { private static final String COHORT_SUBGROUP_TEMPTABLE_TEMPLATE = ResourceHelper.GetResourceAsString("/resources/cohortincidence/sql/cohortSubgroupTempTable.sql"); private static final String TAR_STRATA_QUERY_TEMPTABLE_TEMPLATE = ResourceHelper.GetResourceAsString("/resources/cohortincidence/sql/tarStrataQueryTemplate.sql"); private static final String OUTCOME_STRATA_QUERY_TEMPTABLE_TEMPLATE = ResourceHelper.GetResourceAsString("/resources/cohortincidence/sql/outcomeStrataQueryTemplate.sql"); + private static final String AGE_GROUP_JOIN = ResourceHelper.GetResourceAsString("/resources/cohortincidence/sql/ageGroupJoin.sql"); private static final String AGE_GROUP_SELECT_TEMPLATE = "select CAST(%d as int) as age_group_id, '%s' as age_group_name, cast(%s as int) as min_age, cast(%s as int) as max_age"; private static final String NULL_STRATA = "cast(null as int)"; @@ -273,9 +274,11 @@ private String buildStrataQuery(String strataTemplate, String[] selectCols, Stri private String getStrataQueries(String strataTemplate) { ArrayList queries = new ArrayList<>(); + // Note: because age strata can contain overlapping (ie: non-stratified) age breaks, we need to + // apply the ageGroupJoin only on the subqueries that involve age group stratification. // overall strata queries.add(buildStrataQuery( - strataTemplate, + StringUtils.replace(strataTemplate, "@ageGroupJoin", ""), new String[] {NULL_STRATA + " as age_group_id", NULL_STRATA + " as gender_id", NULL_STRATA + " as start_year"}, new String[] {} )); @@ -283,35 +286,35 @@ private String getStrataQueries(String strataTemplate) { // by age if (this.design.strataSettings != null && this.design.strataSettings.byAge) { queries.add(buildStrataQuery( - strataTemplate, - new String[] {"t1.age_group_id", NULL_STRATA + " as gender_id", NULL_STRATA + " as start_year"}, - new String[] {"t1.age_group_id"} + StringUtils.replace(strataTemplate, "@ageGroupJoin", AGE_GROUP_JOIN), + new String[]{"ag.age_group_id", NULL_STRATA + " as gender_id", NULL_STRATA + " as start_year"}, + new String[]{"ag.age_group_id"} )); // by age, by gender if (this.design.strataSettings.byGender) { queries.add(buildStrataQuery( - strataTemplate, - new String[] {"t1.age_group_id", "t1.gender_id", NULL_STRATA + " as start_year"}, - new String[] {"t1.age_group_id", "t1.gender_id"} + StringUtils.replace(strataTemplate, "@ageGroupJoin", AGE_GROUP_JOIN), + new String[]{"ag.age_group_id", "t1.gender_id", NULL_STRATA + " as start_year"}, + new String[]{"ag.age_group_id", "t1.gender_id"} )); } // by age, by year if (this.design.strataSettings.byYear) { queries.add(buildStrataQuery( - strataTemplate, - new String[] {"t1.age_group_id", NULL_STRATA + " as gender_id", "t1.start_year"}, - new String[] {"t1.age_group_id", "t1.start_year"} + StringUtils.replace(strataTemplate, "@ageGroupJoin", AGE_GROUP_JOIN), + new String[] {"ag.age_group_id", NULL_STRATA + " as gender_id", "t1.start_year"}, + new String[] {"ag.age_group_id", "t1.start_year"} )); } // by age, by gender, by year if (this.design.strataSettings.byGender && this.design.strataSettings.byYear) { queries.add(buildStrataQuery( - strataTemplate, - new String[] {"t1.age_group_id", "t1.gender_id", "t1.start_year"}, - new String[] {"t1.age_group_id", "t1.gender_id", "t1.start_year"} + StringUtils.replace(strataTemplate, "@ageGroupJoin", AGE_GROUP_JOIN), + new String[] {"ag.age_group_id", "t1.gender_id", "t1.start_year"}, + new String[] {"ag.age_group_id", "t1.gender_id", "t1.start_year"} )); } } @@ -319,7 +322,7 @@ private String getStrataQueries(String strataTemplate) { // by gender if (this.design.strataSettings != null && this.design.strataSettings.byGender) { queries.add(buildStrataQuery( - strataTemplate, + StringUtils.replace(strataTemplate, "@ageGroupJoin", ""), new String[]{NULL_STRATA + " as age_group_id", "t1.gender_id", NULL_STRATA + " as start_year"}, new String[]{"t1.gender_id"} )); @@ -327,7 +330,7 @@ private String getStrataQueries(String strataTemplate) { // by gender, by year if (this.design.strataSettings.byYear) { queries.add(buildStrataQuery( - strataTemplate, + StringUtils.replace(strataTemplate, "@ageGroupJoin", ""), new String[] {NULL_STRATA + " as age_group_id", "t1.gender_id", "t1.start_year"}, new String[] {"t1.gender_id", "t1.start_year"} )); @@ -337,7 +340,7 @@ private String getStrataQueries(String strataTemplate) { // by year if (this.design.strataSettings != null && this.design.strataSettings.byYear) { queries.add(buildStrataQuery( - strataTemplate, + StringUtils.replace(strataTemplate, "@ageGroupJoin", ""), new String[]{NULL_STRATA + " as age_group_id", NULL_STRATA + "as gender_id", "t1.start_year"}, new String[]{"t1.start_year"} )); @@ -356,18 +359,27 @@ private String getAgeGroupInsert() { if (this.design.strataSettings == null || this.design.strataSettings.byAge == false) return ""; - if (this.design.strataSettings.ageBreaks.isEmpty()) - throw new IllegalArgumentException("Invalid strataSettings: ageBreaks can not be empty."); + if (this.design.strataSettings.ageBreaks.isEmpty() && + (this.design.strataSettings.ageBreakList.isEmpty() || this.design.strataSettings.ageBreakList.stream().anyMatch(List::isEmpty))) + throw new IllegalArgumentException("Invalid strataSettings: ageBreaks and ageBreaksList can not both be empty."); ArrayList selects = new ArrayList<>(); - List ageBreaks = this.design.strataSettings.ageBreaks; - selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, 1, "<" + ageBreaks.get(0),"null", ageBreaks.get(0))); + List> ageBreakList = new ArrayList<>(this.design.strataSettings.ageBreakList); + if (!this.design.strataSettings.ageBreaks.isEmpty()) { + // put the breaks from ageBreaks in the front of the list (for backwards compatability) + ageBreakList.add(0,this.design.strataSettings.ageBreaks); + } - for (int i = 0; i < ageBreaks.size() - 1; i++) - { - selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, i+2, "" + ageBreaks.get(i) + " - " + (ageBreaks.get(i+1)-1),ageBreaks.get(i), ageBreaks.get(i+1))); + int ageGroupId = 1; + for (List ageBreaks : ageBreakList) { + selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, ageGroupId++, "<" + ageBreaks.get(0),"null", ageBreaks.get(0))); + + for (int i = 0; i < ageBreaks.size() - 1; i++) + { + selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, ageGroupId++, "" + ageBreaks.get(i) + " - " + (ageBreaks.get(i+1)-1),ageBreaks.get(i), ageBreaks.get(i+1))); + } + selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, ageGroupId++, ">=" + ageBreaks.get(ageBreaks.size()-1),ageBreaks.get(ageBreaks.size()-1), "null")); } - selects.add(String.format(AGE_GROUP_SELECT_TEMPLATE, ageBreaks.size()+1, ">=" + ageBreaks.get(ageBreaks.size()-1),ageBreaks.get(ageBreaks.size()-1), "null")); return String.format("insert into @results_database_schema.age_group_def (ref_id, age_group_id, age_group_name, min_age, max_age)\nselect CAST(@ref_id as int) as ref_id, age_group_id, age_group_name, min_age, max_age from (\n%s\n) ag;", StringUtils.join(selects, "\nUNION ALL\n")); diff --git a/java/main/resources/resources/cohortincidence/sql/ageGroupJoin.sql b/java/main/resources/resources/cohortincidence/sql/ageGroupJoin.sql new file mode 100644 index 0000000..31b6401 --- /dev/null +++ b/java/main/resources/resources/cohortincidence/sql/ageGroupJoin.sql @@ -0,0 +1,3 @@ + LEFT JOIN @results_database_schema.age_group_def ag ON ag.ref_id = @ref_id + and t1.age >= coalesce(ag.min_age, -999) + and t1.age < coalesce(ag.max_age, 999) \ No newline at end of file diff --git a/java/main/resources/resources/cohortincidence/sql/incidenceAnalysis.sql b/java/main/resources/resources/cohortincidence/sql/incidenceAnalysis.sql index ebe4217..0b6c445 100644 --- a/java/main/resources/resources/cohortincidence/sql/incidenceAnalysis.sql +++ b/java/main/resources/resources/cohortincidence/sql/incidenceAnalysis.sql @@ -231,7 +231,7 @@ FROM ( /* 5) aggregate tar and excluded+outcome */ -WITH tar_overall (target_cohort_definition_id, tar_id, subgroup_id, subject_id, start_date, end_date, age_group_id, gender_id, start_year) +WITH tar_overall (target_cohort_definition_id, tar_id, subgroup_id, subject_id, start_date, end_date, age, gender_id, start_year) AS ( SELECT te.cohort_definition_id as target_cohort_definition_id, te.tar_id, @@ -239,14 +239,11 @@ AS ( te.subject_id, te.start_date, te.end_date, - ag.age_group_id, + YEAR(te.start_date) - p.year_of_birth as age, p.gender_concept_id as gender_id, YEAR(te.start_date) as start_year FROM #TTAR_erafied te JOIN @cdm_database_schema.person p on te.subject_id = p.person_id - LEFT JOIN @results_database_schema.age_group_def ag ON YEAR(te.start_date) - p.year_of_birth >= coalesce(ag.min_age, -999) - and YEAR(te.start_date) - p.year_of_birth < coalesce(ag.max_age, 999) - and ag.ref_id = @ref_id ) select target_cohort_definition_id, tar_id, subgroup_id, age_group_id, gender_id, start_year, person_days_pe, persons_at_risk_pe INTO #tar_agg @@ -255,7 +252,7 @@ FROM ( ) T_OVERALL ; -WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, age_group_id, gender_id, start_year, excluded_days, tar_days, outcomes_pe, outcomes) +WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, age, gender_id, start_year, excluded_days, tar_days, outcomes_pe, outcomes) AS ( SELECT t1.cohort_definition_id as target_cohort_definition_id, @@ -263,7 +260,7 @@ WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome t1.subgroup_id, op.outcome_id, t1.subject_id, - ag.age_group_id, + YEAR(t1.start_date) - p.year_of_birth as age, p.gender_concept_id as gender_id, YEAR(t1.start_date) as start_year, coalesce(e1.person_days, 0) as excluded_days, @@ -272,9 +269,6 @@ WITH outcomes_overall (target_cohort_definition_id, tar_id, subgroup_id, outcome coalesce(o1.outcomes, 0) as outcomes FROM #TTAR_erafied t1 JOIN @cdm_database_schema.person p ON t1.subject_id = p.person_id - LEFT JOIN @results_database_schema.age_group_def ag ON YEAR(t1.start_date) - p.year_of_birth >= coalesce(ag.min_age, -999) - AND YEAR(t1.start_date) - p.year_of_birth < coalesce(ag.max_age, 999) - AND ag.ref_id = @ref_id JOIN ( -- get the list of TTSO of anyone with excluded time or outcomes to limit result select target_cohort_definition_id, tar_id, subgroup_id, outcome_id, subject_id, start_date FROM #excluded_person_days UNION -- will remove dupes diff --git a/java/main/resources/resources/cohortincidence/sql/outcomeStrataQueryTemplate.sql b/java/main/resources/resources/cohortincidence/sql/outcomeStrataQueryTemplate.sql index 60a149b..e746d7f 100644 --- a/java/main/resources/resources/cohortincidence/sql/outcomeStrataQueryTemplate.sql +++ b/java/main/resources/resources/cohortincidence/sql/outcomeStrataQueryTemplate.sql @@ -12,4 +12,5 @@ SUM(t1.outcomes_pe) as outcomes_pe, SUM(t1.outcomes) as outcomes FROM outcomes_overall t1 +@ageGroupJoin GROUP BY target_cohort_definition_id, tar_id, subgroup_id, outcome_id@groupCols \ No newline at end of file diff --git a/java/main/resources/resources/cohortincidence/sql/tarStrataQueryTemplate.sql b/java/main/resources/resources/cohortincidence/sql/tarStrataQueryTemplate.sql index 850a2fd..7a25dca 100644 --- a/java/main/resources/resources/cohortincidence/sql/tarStrataQueryTemplate.sql +++ b/java/main/resources/resources/cohortincidence/sql/tarStrataQueryTemplate.sql @@ -5,4 +5,5 @@ SUM(CAST((DATEDIFF(day,t1.start_date,t1.end_date) + 1) as bigint)) as person_days_pe, COUNT(distinct t1.subject_id) as persons_at_risk_pe FROM tar_overall t1 +@ageGroupJoin GROUP BY t1.target_cohort_definition_id, t1.tar_id, t1.subgroup_id@groupCols \ No newline at end of file diff --git a/java/test/java/org/ohdsi/cohortincidence/Incidence_5_0_Test.java b/java/test/java/org/ohdsi/cohortincidence/Incidence_5_0_Test.java index cb0fd33..214bd9e 100644 --- a/java/test/java/org/ohdsi/cohortincidence/Incidence_5_0_Test.java +++ b/java/test/java/org/ohdsi/cohortincidence/Incidence_5_0_Test.java @@ -398,6 +398,35 @@ public void strataByAgeTest() throws Exception { this.executeTest(params); } + /** + * Tests multiple people with different age/gender/year strata, but only requests by-age strata + * Person 1: Male, 2 outcomes, 1 excluded, 2 TARs (age 32 and 37). + * Person 2: Female, 2 outcomes, 0 excluded, 2 TARs (age 35 and 37) + * Special notes: Person 1 will start in 1 year but have the TAR exclusion make followup start in next year + * to test that start_year is correct (it should use the erafied-start date) + * + * @throws Exception + */ + @Test + public void strataByAgeBreakListTest() throws Exception { + TestParams params = new TestParams(); + + params.resultSchema = "strata_age_list"; // this must be all lower case for DBUnit to work + params.prepDataSets = new String[]{ + "/datasets/vocabulary.json", + "/cohortincidence/timeAtRisk/strataByAgeBreakList_PREP.json" + }; + params.designJson = ResourceHelper.GetResourceAsString("/cohortincidence/timeAtRisk/strataByAgeBreakListTest.json"); + params.verifyDataSets = new String[]{"/cohortincidence/timeAtRisk/strataByAgeBreakList_VERIFY.json"}; + params.verifyCols = Arrays.asList(new String[]{COL_REF_ID, COL_TARGET_COHORT_ID, COL_TAR_ID, COL_SUBGROUP_ID, COL_OUTCOME_ID, + COL_AGE_GROUP_ID, COL_GENDER_ID, COL_YEAR_ID, + COL_PERSONS_PRE_EXCLUDE, COL_PERSONS_AT_RISK, COL_PERSONS_DAYS_PRE_EXCLUDE, COL_PERSON_DAYS, + COL_PERSON_OUTCOMES_PRE_EXCLUDE, COL_PERSON_OUTCOMES, COL_OUTCOMES_PRE_EXCLUDE, COL_OUTCOMES, + COL_INCIDENCE_PROPORTION_P100P, COL_INCIDENCE_RATE_P100PY}); + + this.executeTest(params); + } + /** * Tests multiple people with different age/gender/year strata, but only requests by-age strata * Person 1: Male, 2 outcomes, 1 excluded, 2 TARs (age 32 and 37). diff --git a/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakListTest.json b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakListTest.json new file mode 100644 index 0000000..dd5527a --- /dev/null +++ b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakListTest.json @@ -0,0 +1,43 @@ +{ + "targetDefs": [ + { + "id": 100, + "name": "Target Cohort 1" + } + ], + "outcomeDefs": [ + { + "id": 1, + "name": "Outcome Cohort 1", + "cohortId": 200, + "cleanWindow": 30 + } + ], + "timeAtRiskDefs": [ + { + "id": 1, + "start": { + "dateField": "start", + "offset": 0 + }, + "end": { + "dateField": "start", + "offset": 90 + } + } + ], + "strataSettings": { + "byAge": true, + "byGender": false, + "byYear": false, + "ageBreaks": [17, 35, 65], + "ageBreakList": [[17], [35], [65]] + }, + "analysisList": [ + { + "targets": [100], + "outcomes": [1], + "tars": [1] + } + ] +} diff --git a/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_PREP.json b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_PREP.json new file mode 100644 index 0000000..2401c02 --- /dev/null +++ b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_PREP.json @@ -0,0 +1,95 @@ +{ + "cdm.person": [ + { + "person_id": 1, + "gender_concept_id": 8507, + "year_of_birth": 1970, + "race_concept_id": 0, + "ethnicity_concept_id": 0 + }, + { + "person_id": 2, + "gender_concept_id": 8532, + "year_of_birth": 1968, + "race_concept_id": 0, + "ethnicity_concept_id": 0 + } + ], + "cdm.observation_period": [ + { + "observation_period_id": 1, + "person_id": 1, + "observation_period_start_date": "2000-01-01", + "observation_period_end_date": "2010-01-01", + "period_type_concept_id": 0 + },{ + "observation_period_id": 2, + "person_id": 2, + "observation_period_start_date": "2000-01-01", + "observation_period_end_date": "2010-01-01", + "period_type_concept_id": 0 + } + ], + "strata_age_list.cohort": [ + { + "cohort_definition_id": 100, + "subject_id": 1, + "cohort_start_date": "2002-12-25", + "cohort_end_date": "2002-12-25" + }, + { + "cohort_definition_id": 100, + "subject_id": 1, + "cohort_start_date": "2007-03-01", + "cohort_end_date": "2007-03-01" + }, + { + "cohort_definition_id": 200, + "subject_id": 1, + "cohort_start_date": "2002-12-15", + "cohort_end_date": "2002-12-15" + }, + { + "cohort_definition_id": 200, + "subject_id": 1, + "cohort_start_date": "2003-02-15", + "cohort_end_date": "2003-02-15" + }, + { + "cohort_definition_id": 200, + "subject_id": 1, + "cohort_start_date": "2007-04-01", + "cohort_end_date": "2007-04-01" + }, + { + "cohort_definition_id": 200, + "subject_id": 1, + "cohort_start_date": "2007-04-05", + "cohort_end_date": "2007-04-05" + }, + { + "cohort_definition_id": 100, + "subject_id": 2, + "cohort_start_date": "2003-03-01", + "cohort_end_date": "2003-03-01" + }, + { + "cohort_definition_id": 100, + "subject_id": 2, + "cohort_start_date": "2005-03-01", + "cohort_end_date": "2005-03-01" + }, + { + "cohort_definition_id": 200, + "subject_id": 2, + "cohort_start_date": "2003-02-01", + "cohort_end_date": "2003-02-01" + }, + { + "cohort_definition_id": 200, + "subject_id": 2, + "cohort_start_date": "2003-05-01", + "cohort_end_date": "2003-05-01" + } + ] +} \ No newline at end of file diff --git a/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_VERIFY.json b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_VERIFY.json new file mode 100644 index 0000000..20921a0 --- /dev/null +++ b/java/test/resources/cohortincidence/timeAtRisk/strataByAgeBreakList_VERIFY.json @@ -0,0 +1,144 @@ +{ + "strata_age_list.incidence_summary": [ + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 2, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 1, + "persons_at_risk": 1, + "person_days_pe": 91, + "person_days": 40, + "person_outcomes_pe": 1, + "person_outcomes": 1, + "outcomes_pe": 1, + "outcomes": 1, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 913.125 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 3, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 2, + "persons_at_risk": 2, + "person_days_pe": 273, + "person_days": 207, + "person_outcomes_pe": 2, + "person_outcomes": 2, + "outcomes_pe": 3, + "outcomes": 2, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 352.8986 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 6, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 2, + "persons_at_risk": 2, + "person_days_pe": 364, + "person_days": 247, + "person_outcomes_pe": 2, + "person_outcomes": 2, + "outcomes_pe": 4, + "outcomes": 3, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 443.6235 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 7, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 1, + "persons_at_risk": 1, + "person_days_pe": 91, + "person_days": 40, + "person_outcomes_pe": 1, + "person_outcomes": 1, + "outcomes_pe": 1, + "outcomes": 1, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 913.1250 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 8, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 2, + "persons_at_risk": 2, + "person_days_pe": 273, + "person_days": 207, + "person_outcomes_pe": 2, + "person_outcomes": 2, + "outcomes_pe": 3, + "outcomes": 2, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 352.8986 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": 9, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 2, + "persons_at_risk": 2, + "person_days_pe": 364, + "person_days": 247, + "person_outcomes_pe": 2, + "person_outcomes": 2, + "outcomes_pe": 4, + "outcomes": 3, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 443.6235 + }, + { + "ref_id": 1, + "target_cohort_definition_id": 100, + "tar_id": 1, + "subgroup_id": 0, + "outcome_id": 1, + "age_group_id": null, + "gender_id": null, + "start_year": null, + "persons_at_risk_pe": 2, + "persons_at_risk": 2, + "person_days_pe": 364, + "person_days": 247, + "person_outcomes_pe": 2, + "person_outcomes": 2, + "outcomes_pe": 4, + "outcomes": 3, + "incidence_proportion_p100p": 100, + "incidence_rate_p100py": 443.6235 + } + ] +} \ No newline at end of file diff --git a/man/StrataSettings.Rd b/man/StrataSettings.Rd index e56790f..6021e94 100644 --- a/man/StrataSettings.Rd +++ b/man/StrataSettings.Rd @@ -26,6 +26,8 @@ before calling jsonlite::toJSON(). \item{\code{byYear}}{enables stratification by start year of TAR} \item{\code{ageBreaks}}{a list of age breaks with at least 1 member} + +\item{\code{ageBreakList}}{a list of age breaks} } \if{html}{\out{
}} } diff --git a/man/createStrataSettings.Rd b/man/createStrataSettings.Rd index 31de637..3e868e4 100644 --- a/man/createStrataSettings.Rd +++ b/man/createStrataSettings.Rd @@ -4,7 +4,13 @@ \alias{createStrataSettings} \title{Creates R6 object for StrataSettings} \usage{ -createStrataSettings(byAge = F, byGender = F, byYear = F, ageBreaks) +createStrataSettings( + byAge = F, + byGender = F, + byYear = F, + ageBreaks, + ageBreakList +) } \arguments{ \item{byAge}{a boolean indicating to stratify by age, defaults to F} diff --git a/pom.xml b/pom.xml index da490c6..387fa70 100644 --- a/pom.xml +++ b/pom.xml @@ -149,7 +149,7 @@ org.ohdsi standardized-analysis-specs - 1.5.0 + 1.6.0-SNAPSHOT org.springframework.boot diff --git a/vignettes/using-cohortincidence.Rmd b/vignettes/using-cohortincidence.Rmd index e4d65fc..cf781f8 100644 --- a/vignettes/using-cohortincidence.Rmd +++ b/vignettes/using-cohortincidence.Rmd @@ -119,15 +119,27 @@ The IR design can also include settings to specify if an analysis should be done To use this function, you create the strata settings with the `CohortIncidence::createStrataSettings)` function: ```{r} -irDesignWithStrata <- CohortIncidence::createIncidenceDesign(targetDefs = list(t1), - outcomeDefs = list(o1), - tars=list(tar1), - analysisList = list(analysis1), - subgroups = list(subgroup1), - #add by age and by gender strata, but don't do by start year. - strataSettings = CohortIncidence::createStrataSettings(byGender=T, byAge=T, ageBreaks = c(17,34,65))) +irDesignWithStrata <- + CohortIncidence::createIncidenceDesign( + targetDefs = list(t1), + outcomeDefs = list(o1), + tars = list(tar1), + analysisList = list(analysis1), + subgroups = list(subgroup1), + #add by age and by gender strata, but don't do by start year. + strataSettings = CohortIncidence::createStrataSettings( + byGender = T, + byAge = T, + ageBreaks = list(17, 34, 65), + ageBreakList = list(list(25), list(65)) + ) + ) ``` +In the above example, thare are 2 ways of specifying the age breaks: ageBreaks and ageBreakList. ageBreaks creates a single age +break specification, while ageBreakList allows you to specify a list of breaks. All breaks defined in ageBreaks and ageBreakList will be +used if specified. If byAge is TRUE, you must specify at least one age break specification either in ageBreaks or ageBreakList. + ## Using executeAnalysis() If there is no need to see the analysis sql or control the output of the analysis to a permenant table, the `executeAnalysis()` function