Skip to contents

Installation

install.packages("pak")
pak::pak("Gilead-BioStats/clindata")
pak::pak("Gilead-BioStats/gsm.core")
pak::pak("Gilead-BioStats/gsm.mapping")
pak::pak("Gilead-BioStats/gsm.kri")
pak::pak("Gilead-BioStats/gsm.reporting")
pak::pak("IMPALA-Consortium/gsm.simaerep")

Load

suppressPackageStartupMessages(library(dplyr))
library(gsm.core)
library(gsm.mapping)
library(gsm.kri)
library(gsm.reporting)
library(gsm.studykri)
#> 
#> Attaching package: 'gsm.studykri'
#> The following object is masked from 'package:gsm.reporting':
#> 
#>     BindResults

Introduction

{gsm.studykir} functions can be executed on a database back-end instead of relying on in-memory calculations.

The package supports various database backends including DuckDB, PostgreSQL, and Snowflake. For backends that use large integer values for random number generation (such as Snowflake), use the vDbIntRandomRange parameter in bootstrap functions to ensure proper normalization of random values.

Database Backend Compatibility

Random Number Generation

Most SQL databases translate R’s runif() to a RANDOM() function that returns decimal values between 0 and 1. However, some databases (notably Snowflake) return large integers instead. For these backends, you must specify the integer range using the vDbIntRandomRange parameter.

Common Random Number Ranges:

  • Snowflake (signed 64-bit): c(-9223372036854775808, 9223372036854775807) (range: −2^63 to 2^63−1)
  • Standard databases (DuckDB, PostgreSQL, etc.): NULL (default, returns 0-1 decimals)

When vDbIntRandomRange is specified, the package automatically normalizes the integer values to the 0-1 range using the formula:

normalized_value = (random_value - min_value) / (max_value - min_value)

How to determine your database’s random range:

If you’re unsure whether your database backend needs normalization, run this query:

SELECT RANDOM() FROM your_table LIMIT 10;

If the values are between 0 and 1, use the default (vDbIntRandomRange = NULL). If they’re large integers, specify the appropriate range.

Simulate Data Set and Save to DuckDb

We use the clindata package which includes data from one clinical study to simulate new study data. We create a portfolio with study AA-4 oversampling patients with low AE counts.

lRaw <- list(
  Raw_SITE = clindata::ctms_site,
  Raw_STUDY = clindata::ctms_study,
  Raw_PD = clindata::ctms_protdev,
  Raw_DATAENT = clindata::edc_data_pages,
  Raw_DATACHG = clindata::edc_data_points,
  Raw_QUERY = clindata::edc_queries,
  Raw_AE = clindata::rawplus_ae,
  Raw_SUBJ = clindata::rawplus_dm,
  Raw_ENROLL = clindata::rawplus_enroll,
  Raw_Randomization = clindata::rawplus_ixrsrand,
  Raw_LB = clindata::rawplus_lb,
  Raw_SDRGCOMP = clindata::rawplus_sdrgcomp,
  Raw_STUDCOMP = clindata::rawplus_studcomp,
  Raw_IE = clindata::rawplus_ie,
  Raw_VISIT = clindata::rawplus_visdt %>%
    mutate(visit_dt = lubridate::ymd(visit_dt))
)

# Add required subject identifier columns for resampling
lRaw$Raw_SUBJ$subjectname <- lRaw$Raw_SUBJ$subject_nsv
lRaw$Raw_SUBJ$subjectenrollmentnumber <- lRaw$Raw_SUBJ$subjid

lPortfolio <- SimulatePortfolio(
  lRaw = lRaw,
  nStudies = 4,
  vSubjectIDs = c("subjid", "subjectenrollmentnumber", "subject_nsv", "subjectname", "subjectid"),
  dfConfig = tibble(
    studyid = c("AA-1", "AA-2", "AA-3", "AA-4"),
    nSubjects = c(500, 750, 150, 200),
    strOversampleDomain = rep("Raw_AE", 4),
    vOversamplQuantileRange_min = c(0, 0, 0, 0),
    vOversamplQuantileRange_max = c(1, 1, 1, 0.75)
  )
)
#> Filtered to 1016 subjects with Raw_AE records in 0.00-1.00 quantile range (1-31 records)
#> Filtered to 1016 subjects with Raw_AE records in 0.00-1.00 quantile range (1-31 records)
#> Filtered to 1016 subjects with Raw_AE records in 0.00-1.00 quantile range (1-31 records)
#> Filtered to 773 subjects with Raw_AE records in 0.00-0.75 quantile range (1-6 records)

con <- DBI::dbConnect(duckdb::duckdb(), ":memory:")

purrr::walk2(names(lPortfolio), lPortfolio, ~ DBI::dbWriteTable(con, .x, .y))

dbPortfolio <- purrr::map(names(lPortfolio), ~ tbl(con, .x))
names(dbPortfolio) <- names(lPortfolio)

Here we create our data set.

tblInput1 <- gsm.studykri::Input_CountSiteByMonth(
  dfSubjects = dbPortfolio$Raw_SUBJ,
  dfNumerator = dbPortfolio$Raw_AE,
  dfDenominator = dbPortfolio$Raw_VISIT,
  strStudyCol = "studyid",
  strGroupCol = "invid",
  strGroupLevel = "Site",
  strSubjectCol = "subjid",
  strNumeratorDateCol = "aest_dt",
  strDenominatorDateCol = "visit_dt",
  strDenominatorType = "Visit"
)

tblInput2 <- gsm.studykri::Input_CountSiteByMonth(
  dfSubjects = dbPortfolio$Raw_SUBJ,
  dfNumerator = dbPortfolio$Raw_AE %>%
    filter(aeser == "Y"),
  dfDenominator = dbPortfolio$Raw_VISIT,
  strStudyCol = "studyid",
  strGroupCol = "invid",
  strGroupLevel = "Site",
  strSubjectCol = "subjid",
  strNumeratorDateCol = "aest_dt",
  strDenominatorDateCol = "visit_dt",
  strDenominatorType = "Visit"
)

tblInput <- dplyr::union_all(
  tblInput1 %>%
    mutate(MetricID = "kri0001"),
  tblInput2 %>%
    mutate(MetricID = "kri0002"),
)

# Load metric metadata from workflow files
metrics_wf <- gsm.core::MakeWorkflowList(
  strNames = c("kri0001.yaml", "kri0002.yaml"),
  strPath = system.file("workflow/2_metrics", package = "gsm.studykri"),
  strPackage = NULL
)

dfMetrics <- gsm.reporting::MakeMetric(metrics_wf)

lJoined <- JoinKRIByDenominator(tblInput, dfMetrics)

lJoined
#> $Visit
#> # Source:   SQL [?? x 7]
#> # Database: DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#>    GroupID    GroupLevel Denominator StudyID MonthYYYYMM Numerator_kri0001
#>    <chr>      <chr>            <dbl> <chr>         <dbl>             <dbl>
#>  1 AA-1_0X035 Site                 1 AA-1         200508                 0
#>  2 AA-1_0X069 Site                 2 AA-1         200610                 1
#>  3 AA-1_0X069 Site                 2 AA-1         200605                 0
#>  4 AA-1_0X161 Site                 3 AA-1         200911                 3
#>  5 AA-1_0X096 Site                 1 AA-1         200711                 1
#>  6 AA-1_0X096 Site                 1 AA-1         200806                 0
#>  7 AA-1_0X096 Site                 1 AA-1         200812                 0
#>  8 AA-1_0X096 Site                 1 AA-1         200902                 0
#>  9 AA-1_0X096 Site                 1 AA-1         200905                 0
#> 10 AA-1_0X062 Site                 1 AA-1         201509                 0
#> # ℹ more rows
#> # ℹ 1 more variable: Numerator_kri0002 <dbl>

Next we prepare for the actual bootstrap simulation. For this we need to perform multiple cross joins that require the creation of temporary tables in the backend. {gsm.studykri} will attempt to create them using the connection attached to the lazy tables. However if write priveleges are not sufficient we can also create the tables manually and pass them to the relevant functions.

dfStudyRef <- tibble(
  StudyID = "AA-1",
  StudyRef = c("AA-2", "AA-3", "AA-4")
)

dfRep <- tibble(
  BootstrapRep = seq(1, 100)
)

dfMonths <- tibble(
  YYYY = seq(2003, 2020)
) %>%
  cross_join(
    tibble(
      MM = seq(1, 12)
    )
  ) %>%
  mutate(
    MonthYYYYMM = YYYY * 100 + MM
  ) %>%
  select(MonthYYYYMM)

DBI::dbWriteTable(con, "StudyRef", dfStudyRef, overwrite = TRUE)
DBI::dbWriteTable(con, "Rep", dfRep, overwrite = TRUE)
DBI::dbWriteTable(con, "Months", dfMonths, overwrite = TRUE)


tblStudyRef <- tbl(con, "StudyRef")
tblRep <- tbl(con, "Rep")
tblMonths <- tbl(con, "Months")


Bounds_Wide <- purrr::map(
  lJoined, ~ Analyze_StudyKRI_PredictBounds(
    .,
    dfStudyRef = tblStudyRef,
    tblBootstrapReps = tblRep,
    tblMonthSequence = tblMonths
  )
)

BoundsRef_Wide <- purrr::map(
  lJoined, ~ Analyze_StudyKRI_PredictBoundsRef(
    .,
    dfStudyRef = tblStudyRef,
    tblBootstrapReps = tblRep,
    tblMonthSequence = tblMonths
  )
)
#> Calculated minimum group count: 73

tblBounds <- Transform_Long(Bounds_Wide)

tblBoundsRef <- Transform_Long(BoundsRef_Wide)

tblTransformed <- union_all(
  tblInput1 %>%
    Transform_CumCount(vBy = "StudyID", tblMonthSequence = tblMonths) %>%
    mutate(MetricID = "kri0001"),
  tblInput2 %>%
    Transform_CumCount(vBy = "StudyID", tblMonthSequence = tblMonths) %>%
    mutate(MetricID = "kri0002"),
)


tblBounds
#> # Source:     SQL [?? x 8]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyID, StudyMonth
#>    MetricID DenominatorType StudyID StudyMonth BootstrapCount Median  Lower
#>    <chr>    <chr>           <chr>        <dbl>          <int>  <dbl>  <dbl>
#>  1 kri0001  Visit           AA-1             2            100  0.211 0     
#>  2 kri0001  Visit           AA-1            10            100  0.2   0.0972
#>  3 kri0001  Visit           AA-1            18            100  0.136 0.0953
#>  4 kri0001  Visit           AA-1            28            100  0.159 0.123 
#>  5 kri0001  Visit           AA-1            29            100  0.163 0.132 
#>  6 kri0001  Visit           AA-1            34            100  0.181 0.145 
#>  7 kri0001  Visit           AA-1            39            100  0.181 0.148 
#>  8 kri0001  Visit           AA-1            42            100  0.186 0.153 
#>  9 kri0001  Visit           AA-1            47            100  0.186 0.155 
#> 10 kri0001  Visit           AA-1            49            100  0.185 0.157 
#> # ℹ more rows
#> # ℹ 1 more variable: Upper <dbl>

tblBoundsRef
#> Warning: Missing values are always removed in SQL aggregation functions.
#> Use `na.rm = TRUE` to silence this warning
#> This warning is displayed once every 8 hours.
#> # Source:     SQL [?? x 11]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyMonth
#>    MetricID DenominatorType StudyMonth BootstrapCount GroupCount StudyCount
#>    <chr>    <chr>                <dbl>          <int>      <dbl>      <int>
#>  1 kri0001  Visit                  143            100         73          3
#>  2 kri0001  Visit                  154            100         73          3
#>  3 kri0001  Visit                  155            100         73          3
#>  4 kri0001  Visit                  160            100         73          3
#>  5 kri0001  Visit                  162            100         73          3
#>  6 kri0001  Visit                  163            100         73          3
#>  7 kri0001  Visit                  165            100         73          3
#>  8 kri0001  Visit                  166            100         73          3
#>  9 kri0001  Visit                  168            100         73          3
#> 10 kri0001  Visit                  172            100         73          3
#> # ℹ more rows
#> # ℹ 5 more variables: StudyID <chr>, StudyRefID <chr>, Median <dbl>,
#> #   Lower <dbl>, Upper <dbl>

tblTransformed
#> # Source:     SQL [?? x 8]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyID, StudyMonth
#>    StudyID MonthYYYYMM StudyMonth Numerator Denominator Metric GroupCount
#>    <chr>         <dbl>      <dbl>     <dbl>       <dbl>  <dbl>      <dbl>
#>  1 AA-3         200311          1         0           1 0               1
#>  2 AA-3         200312          2         0           2 0               1
#>  3 AA-3         200401          3         1           5 0.2             3
#>  4 AA-3         200402          4         1          10 0.1             4
#>  5 AA-3         200403          5         1          13 0.0769          3
#>  6 AA-3         200404          6         6          16 0.375           3
#>  7 AA-3         200405          7         7          20 0.35            3
#>  8 AA-3         200406          8         7          23 0.304           3
#>  9 AA-3         200407          9        12          26 0.462           3
#> 10 AA-3         200408         10        17          29 0.586           3
#> # ℹ more rows
#> # ℹ 1 more variable: MetricID <chr>

Finally before plotting we need to collect the results into memory.

metrics_wf <- gsm.core::MakeWorkflowList(
  strNames = NULL,
  strPath = system.file("workflow/2_metrics", package = "gsm.studykri"),
  strPackage = NULL
)

dfMetrics <- gsm.reporting::MakeMetric(metrics_wf)


lCharts <- gsm.studykri::MakeCharts_StudyKRI(
  dfResults = collect(tblTransformed),
  dfBounds = collect(tblBounds),
  dfBoundsRef = collect(tblBoundsRef),
  dfMetrics = dfMetrics
)

lCharts
#> $`AA-1_kri0001`

#> 
#> $`AA-1_kri0002`

Using workflows

Unfortunately the data specfication checks from gsm.core will not work on lazy tables. We therefore need to skip the mapping workflow and ignore the warnings for the missing specifications.

We might also scrub some of the specifications from the yaml files to begin with.

lMapped <- list(
  Mapped_AE = dbPortfolio$Raw_AE,
  Mapped_Visit = dbPortfolio$Raw_VISIT,
  Mapped_SUBJ = dbPortfolio$Raw_SUBJ %>%
    filter(enrollyn == "Y"),
  Mapped_StudyRef = tbl(con, "StudyRef")
)

metrics_wf <- gsm.core::MakeWorkflowList(
  strNames = "kri0001.yaml",
  strPath = system.file("workflow/2_metrics", package = "gsm.studykri"),
  strPackage = NULL
)

lAnalyzed <- gsm.core::RunWorkflows(lWorkflows = metrics_wf, lData = lMapped)
#> 
#> ── Running 1 Workflows ─────────────────────────────────────────────────────────
#> 
#> ── Initializing `Analysis_kri0001` Workflow ────────────────────────────────────
#> 
#> ── Checking data against spec
#> → All 2 data.frame(s) in the spec are present in the data: Mapped_AE, Mapped_SUBJ
#> Warning: Not all columns of Mapped_AE in the spec are in the expected format, improperly
#> formatted columns are: subjid
#> Not all columns of Mapped_AE in the spec are in the expected format, improperly
#> formatted columns are: aest_dt
#> Warning: Not all columns of Mapped_SUBJ in the spec are in the expected format,
#> improperly formatted columns are: subjid
#> Not all columns of Mapped_SUBJ in the spec are in the expected format,
#> improperly formatted columns are: invid
#> Not all columns of Mapped_SUBJ in the spec are in the expected format,
#> improperly formatted columns are: studyid
#> Not all columns of Mapped_SUBJ in the spec are in the expected format,
#> improperly formatted columns are: firstparticipantdate
#> Not all columns of Mapped_SUBJ in the spec are in the expected format,
#> improperly formatted columns are: lastparticipantdate
#> Warning: Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_AE$subjid
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_AE$aest_dt
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_SUBJ$subjid
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_SUBJ$invid
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_SUBJ$studyid
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_SUBJ$firstparticipantdate
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_SUBJ$lastparticipantdate
#> 
#> ── Workflow Step 1 of 3: `gsm.studykri::Input_CountSiteByMonth` ──
#> 
#> ── Evaluating 12 parameter(s) for `gsm.studykri::Input_CountSiteByMonth`
#> ✔ dfSubjects = Mapped_SUBJ: Passing lData$Mapped_SUBJ.
#> ✔ dfNumerator = Mapped_AE: Passing lData$Mapped_AE.
#> ✔ dfDenominator = Mapped_SUBJ: Passing lData$Mapped_SUBJ.
#> ℹ strStudyCol = studyid: No matching data found. Passing 'studyid' as a string.
#> ℹ strGroupCol = invid: No matching data found. Passing 'invid' as a string.
#> ✔ strGroupLevel = GroupLevel: Passing lMeta$GroupLevel.
#> ℹ strSubjectCol = subjid: No matching data found. Passing 'subjid' as a string.
#> ℹ strNumeratorDateCol = aest_dt: No matching data found. Passing 'aest_dt' as a string.
#> ℹ strDenominatorDateCol = firstparticipantdate: No matching data found. Passing 'firstparticipantdate' as a string.
#> ℹ strDenominatorEndDateCol = lastparticipantdate: No matching data found. Passing 'lastparticipantdate' as a string.
#> ✔ strDenominatorType = Denominator: Passing lMeta$Denominator.
#> ✔ nMinDenominator = AccrualThreshold: Passing lMeta$AccrualThreshold.
#> 
#> ── Calling `gsm.studykri::Input_CountSiteByMonth`
#> 
#> ── list of length 2 saved as `lData$Analysis_Input`.
#> 
#> ── Workflow Step 2 of 3: `gsm.studykri::Transform_CumCount` ──
#> 
#> ── Evaluating 2 parameter(s) for `gsm.studykri::Transform_CumCount`
#> ✔ dfInput = Analysis_Input: Passing lData$Analysis_Input.
#> ℹ vBy = StudyID: No matching data found. Passing 'StudyID' as a string.
#> 
#> ── Calling `gsm.studykri::Transform_CumCount`
#> 
#> ── list of length 2 saved as `lData$Analysis_Transformed`.
#> 
#> ── Workflow Step 3 of 3: `list` ──
#> 
#> ── Evaluating 3 parameter(s) for `list`
#> ✔ ID = ID: Passing lMeta$ID.
#> ✔ Analysis_Input = Analysis_Input: Passing lData$Analysis_Input.
#> ✔ Analysis_Transformed = Analysis_Transformed: Passing lData$Analysis_Transformed.
#> 
#> ── Calling `list`
#> 
#> ── list of length 3 saved as `lData$lAnalysis`.
#> 
#> ── Returning results from final step: list of length 3`. ──
#> 
#> ── Completed `Analysis_kri0001` Workflow ───────────────────────────────────────

reporting_wf <- gsm.core::MakeWorkflowList(
  strNames = c("Bounds", "BoundsRef", "Input", "Join", "Metrics", "Results"),
  strPath = system.file("workflow/3_reporting", package = "gsm.studykri")
)

reporting_wf$BoundsRef$meta$BootstrapReps <- 100
reporting_wf$Bounds$meta$BootstrapReps <- 100

lReporting <- gsm.core::RunWorkflows(
  lWorkflows = reporting_wf,
  lData = c(
    lMapped,
    list(
      lAnalyzed = lAnalyzed,
      lWorkflows = metrics_wf
    )
  )
)
#> 
#> ── Running 6 Workflows ─────────────────────────────────────────────────────────
#> 
#> ── Initializing `Reporting_Results` Workflow ───────────────────────────────────
#> 
#> ── No spec found in workflow. Proceeding without checking data.
#> 
#> ── Workflow Step 1 of 1: `gsm.studykri::BindResults` ──
#> 
#> ── Evaluating 2 parameter(s) for `gsm.studykri::BindResults`
#> ✔ lAnalysis = lAnalyzed: Passing lData$lAnalyzed.
#> ℹ strName = Analysis_Transformed: No matching data found. Passing 'Analysis_Transformed' as a string.
#> 
#> ── Calling `gsm.studykri::BindResults`
#> 
#> ── list of length 2 saved as `lData$lResults`.
#> 
#> ── Returning results from final step: list of length 2`. ──
#> 
#> ── Completed `Reporting_Results` Workflow ──────────────────────────────────────
#> 
#> ── Initializing `Reporting_Input` Workflow ─────────────────────────────────────
#> 
#> ── No spec found in workflow. Proceeding without checking data.
#> 
#> ── Workflow Step 1 of 1: `gsm.studykri::BindResults` ──
#> 
#> ── Evaluating 2 parameter(s) for `gsm.studykri::BindResults`
#> ✔ lAnalysis = lAnalyzed: Passing lData$lAnalyzed.
#> ℹ strName = Analysis_Input: No matching data found. Passing 'Analysis_Input' as a string.
#> 
#> ── Calling `gsm.studykri::BindResults`
#> 
#> ── list of length 2 saved as `lData$Reporting_Input`.
#> 
#> ── Returning results from final step: list of length 2`. ──
#> 
#> ── Completed `Reporting_Input` Workflow ────────────────────────────────────────
#> 
#> ── Initializing `Reporting_Metrics` Workflow ───────────────────────────────────
#> 
#> ── No spec found in workflow. Proceeding without checking data.
#> 
#> ── Workflow Step 1 of 1: `gsm.reporting::MakeMetric` ──
#> 
#> ── Evaluating 1 parameter(s) for `gsm.reporting::MakeMetric`
#> ✔ lWorkflows = lWorkflows: Passing lData$lWorkflows.
#> 
#> ── Calling `gsm.reporting::MakeMetric`
#> 
#> ── 1x16 data.frame saved as `lData$Reporting_Metrics`.
#> 
#> ── Returning results from final step: 1x16 data.frame`. ──
#> 
#> ── Completed `Reporting_Metrics` Workflow ──────────────────────────────────────
#> 
#> ── Initializing `Reporting_Join` Workflow ──────────────────────────────────────
#> 
#> ── Checking data against spec
#> → All 2 data.frame(s) in the spec are present in the data: Reporting_Input, Reporting_Metrics
#> Warning: Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: MetricID
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: GroupID
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: GroupLevel
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: Numerator
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: Denominator
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: StudyID
#> Not all columns of Reporting_Input in the spec are in the expected format,
#> improperly formatted columns are: MonthYYYYMM
#> Warning: Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$MetricID
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$GroupID
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$GroupLevel
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$Numerator
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$Denominator
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$StudyID
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Reporting_Input$MonthYYYYMM
#> 
#> ── Workflow Step 1 of 1: `gsm.studykri::JoinKRIByDenominator` ──
#> 
#> ── Evaluating 2 parameter(s) for `gsm.studykri::JoinKRIByDenominator`
#> ✔ dfInput = Reporting_Input: Passing lData$Reporting_Input.
#> ✔ dfMetrics = Reporting_Metrics: Passing lData$Reporting_Metrics.
#> 
#> ── Calling `gsm.studykri::JoinKRIByDenominator`
#> 
#> ── list of length 1 saved as `lData$Joined_Analysis_Input`.
#> 
#> ── Returning results from final step: list of length 1`. ──
#> 
#> ── Completed `Reporting_Join` Workflow ─────────────────────────────────────────
#> 
#> ── Initializing `Reporting_Bounds` Workflow ────────────────────────────────────
#> 
#> ── Checking data against spec
#> → All 2 data.frame(s) in the spec are present in the data: Reporting_Join, Mapped_StudyRef
#> → All specified columns in Mapped_StudyRef are in the expected format
#> Warning: Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_StudyRef$study
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_StudyRef$studyref
#> 
#> ── Workflow Step 1 of 3: `gsm.studykri::ParseFunction` ──
#> 
#> ── Evaluating 1 parameter(s) for `gsm.studykri::ParseFunction`
#> ℹ strFunction = gsm.studykri::Analyze_StudyKRI_PredictBounds: No matching data found. Passing 'gsm.studykri::Analyze_StudyKRI_PredictBounds' as a string.
#> 
#> ── Calling `gsm.studykri::ParseFunction`
#> 
#> ── closure of length 1 saved as `lData$PredictBounds_Func`.
#> 
#> ── Workflow Step 2 of 3: `purrr::map` ──
#> 
#> ── Evaluating 5 parameter(s) for `purrr::map`
#> ✔ .x = Reporting_Join: Passing lData$Reporting_Join.
#> ✔ .f = PredictBounds_Func: Passing lData$PredictBounds_Func.
#> ✔ dfStudyRef = Mapped_StudyRef: Passing lData$Mapped_StudyRef.
#> ✔ nBootstrapReps = BootstrapReps: Passing lMeta$BootstrapReps.
#> ✔ nConfLevel = Threshold: Passing lMeta$Threshold.
#> 
#> ── Calling `purrr::map`
#> 
#> ── list of length 1 saved as `lData$Analysis_Bounds_Wide`.
#> 
#> ── Workflow Step 3 of 3: `gsm.studykri::Transform_Long` ──
#> 
#> ── Evaluating 1 parameter(s) for `gsm.studykri::Transform_Long`
#> ✔ lWide = Analysis_Bounds_Wide: Passing lData$Analysis_Bounds_Wide.
#> 
#> ── Calling `gsm.studykri::Transform_Long`
#> 
#> ── list of length 2 saved as `lData$Reporting_Bounds`.
#> 
#> ── Returning results from final step: list of length 2`. ──
#> 
#> ── Completed `Reporting_Bounds` Workflow ───────────────────────────────────────
#> 
#> ── Initializing `Reporting_BoundsRef` Workflow ─────────────────────────────────
#> 
#> ── Checking data against spec
#> → All 2 data.frame(s) in the spec are present in the data: Reporting_Join, Mapped_StudyRef
#> → All specified columns in Mapped_StudyRef are in the expected format
#> Warning: Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_StudyRef$study
#> Not all specified columns in the spec are present in the data, missing columns
#> are: Mapped_StudyRef$studyref
#> 
#> ── Workflow Step 1 of 3: `gsm.studykri::ParseFunction` ──
#> 
#> ── Evaluating 1 parameter(s) for `gsm.studykri::ParseFunction`
#> ℹ strFunction = gsm.studykri::Analyze_StudyKRI_PredictBoundsRef: No matching data found. Passing 'gsm.studykri::Analyze_StudyKRI_PredictBoundsRef' as a string.
#> 
#> ── Calling `gsm.studykri::ParseFunction`
#> 
#> ── closure of length 1 saved as `lData$PredictBoundsRef_Func`.
#> 
#> ── Workflow Step 2 of 3: `purrr::map` ──
#> 
#> ── Evaluating 6 parameter(s) for `purrr::map`
#> ✔ .x = Reporting_Join: Passing lData$Reporting_Join.
#> ✔ .f = PredictBoundsRef_Func: Passing lData$PredictBoundsRef_Func.
#> ✔ dfStudyRef = Mapped_StudyRef: Passing lData$Mapped_StudyRef.
#> ✔ nBootstrapReps = BootstrapReps: Passing lMeta$BootstrapReps.
#> ✔ nConfLevel = Threshold: Passing lMeta$Threshold.
#> ✔ bMixStudies = bMixStudies: Passing lMeta$bMixStudies.
#> 
#> ── Calling `purrr::map`
#> Calculated minimum group count: 73
#> 
#> ── list of length 1 saved as `lData$Analysis_BoundsRef_Wide`.
#> 
#> ── Workflow Step 3 of 3: `gsm.studykri::Transform_Long` ──
#> 
#> ── Evaluating 1 parameter(s) for `gsm.studykri::Transform_Long`
#> ✔ lWide = Analysis_BoundsRef_Wide: Passing lData$Analysis_BoundsRef_Wide.
#> 
#> ── Calling `gsm.studykri::Transform_Long`
#> 
#> ── list of length 2 saved as `lData$Reporting_BoundsRef`.
#> 
#> ── Returning results from final step: list of length 2`. ──
#> 
#> ── Completed `Reporting_BoundsRef` Workflow ────────────────────────────────────

lReporting
#> $Reporting_Results
#> # Source:     SQL [?? x 9]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyID, StudyMonth
#>    StudyID MonthYYYYMM StudyMonth Numerator Denominator Metric GroupCount
#>    <chr>         <dbl>      <dbl>     <dbl>       <dbl>  <dbl>      <dbl>
#>  1 AA-3         200410          1        22          65 0.338           5
#>  2 AA-3         200411          2        22         209 0.105           5
#>  3 AA-3         200412          3        25         333 0.0751          4
#>  4 AA-3         200501          4        26         457 0.0569          4
#>  5 AA-3         200502          5        26         581 0.0448          7
#>  6 AA-3         200503          6        27         873 0.0309         10
#>  7 AA-3         200504          7        32        1259 0.0254         13
#>  8 AA-3         200505          8        35        1693 0.0207         13
#>  9 AA-3         200506          9        39        2113 0.0185         13
#> 10 AA-3         200507         10        41        2559 0.0160         14
#> # ℹ more rows
#> # ℹ 2 more variables: MetricID <chr>, SnapshotDate <date>
#> 
#> $Reporting_Input
#> # Source:   SQL [?? x 10]
#> # Database: DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#>    GroupID    GroupLevel Numerator Denominator Metric StudyID MonthYYYYMM
#>    <chr>      <chr>          <dbl>       <dbl>  <dbl> <chr>         <dbl>
#>  1 AA-1_0X041 Site               1          60 0.0167 AA-1         200906
#>  2 AA-1_0X041 Site               3          16 0.188  AA-1         200909
#>  3 AA-1_0X035 Site               1          31 0.0323 AA-1         200407
#>  4 AA-1_0X035 Site               1          30 0.0333 AA-1         200404
#>  5 AA-1_0X035 Site               1          31 0.0323 AA-1         200512
#>  6 AA-1_0X035 Site               1          31 0.0323 AA-1         200408
#>  7 AA-1_0X162 Site               1          31 0.0323 AA-1         201805
#>  8 AA-1_0X069 Site               1          31 0.0323 AA-1         200608
#>  9 AA-1_0X023 Site               1          93 0.0108 AA-1         201112
#> 10 AA-1_0X161 Site               1          30 0.0333 AA-1         201106
#> # ℹ more rows
#> # ℹ 3 more variables: DenominatorType <chr>, MetricID <chr>,
#> #   SnapshotDate <date>
#> 
#> $Reporting_Metrics
#> # A tibble: 1 × 16
#>   Type    ID    GroupLevel Abbreviation Metric Numerator Denominator Model Score
#>   <chr>   <chr> <chr>      <chr>        <chr>  <chr>     <chr>       <chr> <chr>
#> 1 Analys… kri0… Site       AE           Adver… Adverse … Days on St… Boot… Boot…
#> # ℹ 7 more variables: AnalysisType <chr>, Threshold <dbl>,
#> #   AccrualThreshold <int>, AccrualMetric <chr>, BootstrapReps <int>,
#> #   Priority <dbl>, MetricID <chr>
#> 
#> $Reporting_Join
#> $Reporting_Join$`Days on Study`
#> # Source:   SQL [?? x 6]
#> # Database: DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#>    GroupID    GroupLevel Denominator StudyID MonthYYYYMM Numerator_Analysis_kr…¹
#>    <chr>      <chr>            <dbl> <chr>         <dbl>                   <dbl>
#>  1 AA-1_0X041 Site                60 AA-1         200906                       1
#>  2 AA-1_0X035 Site                31 AA-1         200408                       1
#>  3 AA-1_0X162 Site                31 AA-1         201805                       1
#>  4 AA-1_0X069 Site                31 AA-1         200608                       1
#>  5 AA-1_0X161 Site                30 AA-1         201106                       1
#>  6 AA-1_0X154 Site                31 AA-1         201008                       1
#>  7 AA-1_0X153 Site                31 AA-1         201703                       1
#>  8 AA-1_0X093 Site                30 AA-1         201206                       1
#>  9 AA-1_0X154 Site                29 AA-1         201602                       1
#> 10 AA-1_0X052 Site               120 AA-1         200806                       1
#> # ℹ more rows
#> # ℹ abbreviated name: ¹​Numerator_Analysis_kri0001
#> 
#> 
#> $Reporting_Bounds
#> # Source:     SQL [?? x 8]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyID, StudyMonth
#>    MetricID    DenominatorType StudyID StudyMonth BootstrapCount  Median   Lower
#>    <chr>       <chr>           <chr>        <dbl>          <int>   <dbl>   <dbl>
#>  1 Analysis_k… Days on Study   AA-1             1            100 0.0411  0.02   
#>  2 Analysis_k… Days on Study   AA-1             7            100 0.00825 0.00475
#>  3 Analysis_k… Days on Study   AA-1             8            100 0.00692 0.00372
#>  4 Analysis_k… Days on Study   AA-1            13            100 0.00572 0.00358
#>  5 Analysis_k… Days on Study   AA-1            19            100 0.00573 0.00389
#>  6 Analysis_k… Days on Study   AA-1            21            100 0.00526 0.00382
#>  7 Analysis_k… Days on Study   AA-1            25            100 0.00632 0.00467
#>  8 Analysis_k… Days on Study   AA-1            27            100 0.00604 0.00442
#>  9 Analysis_k… Days on Study   AA-1            46            100 0.00564 0.00468
#> 10 Analysis_k… Days on Study   AA-1            48            100 0.00563 0.00461
#> # ℹ more rows
#> # ℹ 1 more variable: Upper <dbl>
#> 
#> $Reporting_BoundsRef
#> # Source:     SQL [?? x 11]
#> # Database:   DuckDB 1.5.1 [unknown@Linux 6.17.0-1008-azure:R 4.5.3/:memory:]
#> # Ordered by: StudyMonth
#>    MetricID      DenominatorType StudyMonth BootstrapCount GroupCount StudyCount
#>    <chr>         <chr>                <dbl>          <int>      <dbl>      <int>
#>  1 Analysis_kri… Days on Study          165            100         73          3
#>  2 Analysis_kri… Days on Study          166            100         73          3
#>  3 Analysis_kri… Days on Study          168            100         73          3
#>  4 Analysis_kri… Days on Study          172            100         73          3
#>  5 Analysis_kri… Days on Study          177            100         73          3
#>  6 Analysis_kri… Days on Study          179            100         73          3
#>  7 Analysis_kri… Days on Study          180            100         73          3
#>  8 Analysis_kri… Days on Study            5            100         73          3
#>  9 Analysis_kri… Days on Study            9            100         73          3
#> 10 Analysis_kri… Days on Study           11            100         73          3
#> # ℹ more rows
#> # ℹ 5 more variables: StudyID <chr>, StudyRefID <chr>, Median <dbl>,
#> #   Lower <dbl>, Upper <dbl>

Next we collect the data and create the charts.

lCharts <- gsm.studykri::MakeCharts_StudyKRI(
  dfResults = collect(lReporting$Reporting_Results),
  dfBounds = collect(lReporting$Reporting_Bounds),
  dfBoundsRef = collect(lReporting$Reporting_BoundsRef),
  dfMetrics = lReporting$Reporting_Metrics
)

lCharts
#> $`AA-1_Analysis_kri0001`

Using Snowflake or Other Integer Random Backends

If you’re using Snowflake or another database that returns large integers for RANDOM(), add the vDbIntRandomRange parameter to your bootstrap function calls:

# For Snowflake: specify signed 64-bit integer range
Bounds_Wide_Snowflake <- purrr::map(
  lJoined, ~ Analyze_StudyKRI_PredictBounds(
    .,
    dfStudyRef = tblStudyRef,
    tblBootstrapReps = tblRep,
    tblMonthSequence = tblMonths,
    vDbIntRandomRange = c(-9223372036854775808, 9223372036854775807)
  )
)

BoundsRef_Wide_Snowflake <- purrr::map(
  lJoined, ~ Analyze_StudyKRI_PredictBoundsRef(
    .,
    dfStudyRef = tblStudyRef,
    tblBootstrapReps = tblRep,
    tblMonthSequence = tblMonths,
    vDbIntRandomRange = c(-9223372036854775808, 9223372036854775807)
  )
)

The vDbIntRandomRange parameter should be added to:

This ensures that random values are properly normalized regardless of the database backend’s random number generation method.

Performance Optimization for Database Backends

Optimization Strategy 1: Pass nMinGroups via StudyRef

The most impactful optimization is avoiding the collection of the full dataset to calculate minimum group counts. By pre-calculating and including a MinGroups column in your StudyRef table, you can avoid materializing potentially millions of rows.

How to calculate MinGroups:

For each target study in your StudyRef mapping, calculate the minimum group count across all its reference studies:

-- Example: Calculate MinGroups for each target-reference set
WITH GroupCounts AS (
  SELECT 
    StudyID,
    COUNT(DISTINCT GroupID) AS group_count
  FROM Analysis_Input
  WHERE StudyID IN ('REF_STUDY_1', 'REF_STUDY_2', 'REF_STUDY_3')
  GROUP BY StudyID
)
SELECT 
  'TARGET_STUDY' AS studyid,
  'REF_STUDY_1' AS studyrefid,  -- repeat for each reference
  MIN(group_count) AS MinGroups
FROM GroupCounts;

Then include it in your StudyRef table:

The StudyRef.yaml workflow selects only the required studyid and studyrefid columns. To use the MinGroups optimization, you have two options:

Option 1: Add MinGroups directly to your StudyRef data

# Create Raw_StudyRef with MinGroups column
dfStudyRef <- tibble(
  studyid = "AA-1",
  studyrefid = c("AA-2", "AA-3", "AA-4"),
  MinGroups = 8 # Pre-calculated minimum across reference studies
)

# Write to database
DBI::dbWriteTable(con, "StudyRef", dfStudyRef, overwrite = TRUE)
tblStudyRef <- tbl(con, "StudyRef")

# Use in analysis - MinGroups will be automatically detected and used
BoundsRef_Optimized <- Analyze_StudyKRI_PredictBoundsRef(
  dfInput = tblInput,
  dfStudyRef = tblStudyRef,
  tblBootstrapReps = tblRep,
  tblMonthSequence = tblMonths
)

Option 2: Use a custom mapping query

If you need to transform the column name, create a custom StudyRef.yaml workflow:

steps:
  - output: Mapped_StudyRef
    name: gsm.core::RunQuery
    params:
      df: Raw_StudyRef
      strQuery: "SELECT studyid AS study, studyrefid AS studyref, mingroups AS MinGroups FROM df"

Option 3: Use a custom column name

dfStudyRef_Custom <- tibble(
  studyid = "AA-1",
  studyrefid = c("AA-2", "AA-3", "AA-4"),
  min_grp_cnt = 8
)

BoundsRef_Custom <- Analyze_StudyKRI_PredictBoundsRef(
  dfInput = tblInput,
  dfStudyRef = tblStudyRef_Custom,
  strMinGroupsCol = "min_grp_cnt", # Specify custom column name
  tblBootstrapReps = tblRep,
  tblMonthSequence = tblMonths
)

Optimization Strategy 2: Skip Validation Checks

The JoinKRIByDenominator() function performs several validation checks that require collect() operations. When confident in data quality, skip these:

lJoined <- JoinKRIByDenominator(
  tblInput,
  dfMetrics,
  bSkipValidation = TRUE # Disable validation collect() operations
)

Optimization Strategy 3: Pre-generate Temporary Tables

Provide pre-generated bootstrap replicates and month sequences to avoid repeated temp table creation:

# Create once, reuse many times
dfRep <- tibble(BootstrapRep = seq(1, 1000))
dfMonths <- tibble(
  YYYY = seq(2003, 2020)
) %>%
  cross_join(tibble(MM = seq(1, 12))) %>%
  mutate(MonthYYYYMM = YYYY * 100 + MM) %>%
  select(MonthYYYYMM)

DBI::dbWriteTable(con, "Rep", dfRep)
DBI::dbWriteTable(con, "Months", dfMonths)

tblRep <- tbl(con, "Rep")
tblMonths <- tbl(con, "Months")

# Pass to all functions
Bounds <- Analyze_StudyKRI_PredictBounds(
  dfInput,
  tblBootstrapReps = tblRep,
  tblMonthSequence = tblMonths
)

Optimization Strategy 4: Use funCompute to Cache Intermediate Results

When working with large database queries, you can use the funCompute parameter to cache intermediate results in the database. This can significantly improve performance by materializing temporary tables rather than re-executing complex queries.

The funCompute parameter accepts a function that will be applied to intermediate database results. The most common use case is dplyr::compute():

# Cache intermediate results during bootstrap calculations
BoundsRef_Cached <- Analyze_StudyKRI_PredictBoundsRef(
  dfInput = tblInput,
  dfStudyRef = tblStudyRef,
  nBootstrapReps = 1000,
  tblBootstrapReps = tblRep,
  tblMonthSequence = tblMonths,
  funCompute = dplyr::compute # Cache intermediate results
)

You can also provide a custom function for more control:

# Custom compute function - let dplyr auto-generate unique names
# (Important: don't specify fixed names as funCompute may be called multiple times)
custom_cache <- function(x) {
  dplyr::compute(x, temporary = TRUE)
}

BoundsRef_Custom <- Analyze_StudyKRI_PredictBoundsRef(
  dfInput = tblInput,
  dfStudyRef = tblStudyRef,
  nBootstrapReps = 1000,
  funCompute = custom_cache
)

When to use funCompute:

  • Complex queries that are re-used multiple times
  • Very large datasets where materialization is faster than re-computation
  • When database query optimizer struggles with complex lazy query plans

Performance trade-offs:

  • Benefit: Avoids re-executing expensive queries; can reduce query complexity
  • Cost: Requires database write permissions; creates temporary tables that use storage

Note: funCompute is primarily beneficial for large datasets (millions of rows), complex multi-study portfolios, or queries that are re-executed multiple times. For small datasets or simple queries, the overhead of creating temporary tables may outweigh benefits.

Best Practices Summary

  1. Add MinGroups column to StudyRef (or use strMinGroupsCol parameter for custom names) for the biggest performance gain
  2. Use bSkipValidation = TRUE in JoinKRIByDenominator() when data quality is assured
  3. Pre-generate temp tables (tblBootstrapReps, tblMonthSequence) to avoid repeated creation
  4. Use funCompute to cache intermediate results when working with large datasets or complex queries