TL Catalog
  1. Tables
  2. Clinical Laboratory Evaluation
  3. TSFLAB01A
  • Introduction

  • Index

  • Tables
    • Adverse Events
      • TSFAE01A
      • TSFAE01B
      • TSFAE02
      • TSFAE02A
      • TSFAE03
      • TSFAE03A
      • TSFAE04
      • TSFAE04A
      • TSFAE05
      • TSFAE05A
      • TSFAE06A
      • TSFAE06B
      • TSFAE07A
      • TSFAE07B
      • TSFAE08
      • TSFAE09
      • TSFAE10
      • TSFAE11
      • TSFAE12
      • TSFAE13
      • TSFAE14
      • TSFAE15
      • TSFAE16
      • TSFAE17A
      • TSFAE17B
      • TSFAE17C
      • TSFAE17D
      • TSFAE19A
      • TSFAE19B
      • TSFAE19C
      • TSFAE19D
      • TSFAE20A
      • TSFAE20B
      • TSFAE20C
      • TSFAE21A
      • TSFAE21B
      • TSFAE21C
      • TSFAE21D
      • TSFAE22A
      • TSFAE22B
      • TSFAE22C
      • TSFAE23A
      • TSFAE23B
      • TSFAE23C
      • TSFAE23D
      • TSFAE24A
      • TSFAE24B
      • TSFAE24C
      • TSFAE24D
      • TSFAE24F
      • TSFDTH01
    • Clinical Laboratory Evaluation
      • TSFLAB01
      • TSFLAB01A
      • TSFLAB02
      • TSFLAB02A
      • TSFLAB02B
      • TSFLAB03
      • TSFLAB03A
      • TSFLAB04A
      • TSFLAB04B
      • TSFLAB05
      • TSFLAB06
      • TSFLAB07
    • Demographic
      • TSIDEM01
      • TSIDEM02
      • TSIMH01
    • Disposition of Subjects
      • TSIDS01
      • TSIDS02
      • TSIDS02A
    • Electrocardiograms
      • TSFECG01
      • TSFECG01A
      • TSFECG02
      • TSFECG03
      • TSFECG04
      • TSFECG05
    • Exposure
      • TSIEX01
      • TSIEX02
      • TSIEX03
      • TSIEX04
      • TSIEX06
      • TSIEX07
      • TSIEX08
      • TSIEX09
      • TSIEX10
      • TSIEX11
    • Pharmacokinetics
      • TPK01A
      • TPK01B
      • TPK02
      • TPK03
    • Prior and Concomitant Therapies
      • TSICM01
      • TSICM02
      • TSICM03
      • TSICM04
      • TSICM05
      • TSICM06
      • TSICM07
      • TSICM08
    • Vital Signs and Physical Findings
      • TSFVIT01
      • TSFVIT01A
      • TSFVIT02
      • TSFVIT03
      • TSFVIT04
      • TSFVIT05
      • TSFVIT06
  • Listings
    • Adverse Events
      • LSFAE01
      • LSFAE02
      • LSFAE03
      • LSFAE04
      • LSFAE05
      • LSFAE06A
      • LSFAE06B
      • LSFDTH01
    • Clinical Laboratory Evaluation
      • LSFLAB01
    • Demographic
      • LSIDEM01
      • LSIDEM02
      • LSIMH01
    • Disposition of Subjects
      • LSIDS01
      • LSIDS02
      • LSIDS03
      • LSIDS04
      • LSIDS05
    • Electrocardiograms
      • LSFECG01
      • LSFECG02
    • Exposure
      • LSIEX01
      • LSIEX02
      • LSIEX03
    • Prior and Concomitant Therapies
      • LSICM01
    • Vital Signs and Physical Findings
      • LSFVIT01
      • LSFVIT02

  • Reproducibility

  • Changelog

On this page

  • Output
  • Edit this page
  • Report an issue
  1. Tables
  2. Clinical Laboratory Evaluation
  3. TSFLAB01A

TSFLAB01A

Mean Change From Baseline for Laboratory Category Laboratory Data Over Time by Subgroup


Output

  • Preview
Code
# Program Name:              tsflab01a

# Prep Environment

library(envsetup)
library(tern)
library(dplyr)
library(rtables)
library(junco)

# Define script level parameters:

tblid <- "TSFLAB01a"
fileid <- tblid
titles <- get_titles_from_file(input_path = '../../_data/', tblid)
string_map <- default_str_map

popfl <- "SAFFL"
trtvar <- "TRT01A"
ctrl_grp <- "Placebo"

# Note on ancova parameter
# when ancova = TRUE
# ancova model will be used to calculate all mean/mean change columns
# not just those from the Difference column
# model specification
summ_vars <- list(arm = trtvar, covariates = NULL)

# when ancova = FALSE, all mean/mean change columns will be from descriptive stats
# for the difference column descriptive stats will be based upon two-sample t-test
ancova <- FALSE


comp_btw_group <- TRUE


subgrpvar <- "AGEGR1"
subgrplbl <- "Age: %s years"

page_by <- TRUE # Set page_by TRUE/FALSE if you (do not) wish to start a new page after a new subgroup
indent_adj <- 0L
if (page_by) {
  indent_adj <- 1L
}

## For analysis on SI units: use adlb dataset
## For analysis on Conventional units: use adlbc dataset -- shell is in conventional units

ad_domain <- "ADLB"

# see further, an alternative method to identify all non-unscheduled visits based upon data
selvisit <- c("Screening", "Baseline", "Cycle 02", "Cycle 03", "Cycle 04")

### note : this shell covers multiple tables depending on parcat3 selections

## allowed PARCAT3 selections

# parcat3sel <- "General chemistry"
# parcat3sel <- "Kidney function"
# parcat3sel <- "Liver biochemistry"
# parcat3sel <- "Lipids"
#
# ### Hematology (HM) : has 3 subcategories that should be included on one table
# parcat3sel <- c("Complete blood count","WBC differential","Coagulation studies")

# per DPS specifications, the output identifier should include the abbreviation for the category

# 1. Present laboratory tests using separate outputs for each category as follows:
#   General chemistry (GC): Sodium, Potassium, Chloride, Bicarbonate, Urea Nitrogen, Glucose, Calcium, Magnesium, Phosphate, Protein, Albumin, Creatine Kinase, Amylase, Lipase
#   Kidney function (KF): Creatinine, GFR from Creatinine
#   Liver biochemistry (LV): Alkaline Phosphatase, Alanine Aminotransferase, Aspartate Aminotransferase, Bilirubin, Prothrombin Intl. Normalized Ratio, Gamma Glutamyl Transferase
#   Lipids (LP): Cholesterol, HDL Cholesterol, LDL Cholesterol, Triglycerides
#   Hematology (HM):  Subcategory rows should be included for Complete Blood Count, White Blood Cell Differential and for Coagulation Studies
#     Complete blood count: Leukocytes, Hemoglobin, Platelets;
#     WBC differential: Lymphocytes, Neutrophils, Eosinophils;
#     Coagulation studies: Prothrombin Time, Activated Partial Thromboplastin Time.

# The output identifier should include the abbreviation for the laboratory category (eg, TSFLAB02GC for General Chemistry)

# In current template program, only 1 version is created, without the proper abbreviation appended
# The reason for this is that TSFLAB02GC is not included in the DPS system - only the core version TSFLAB02

get_abbreviation <- function(parcat3sel) {
  parcat3sel <- toupper(parcat3sel)
  abbr <- NULL
  if (length(parcat3sel) == 1) {
    if (parcat3sel == toupper("General chemistry")) {
      abbr <- "GC"
    }
    # the following line should be removed for a true study, global jjcs standards in DPS system does not include the abbreviation
    if (parcat3sel == toupper("General chemistry")) {
      abbr <- ""
    }
    #
    if (parcat3sel == toupper("Kidney function")) {
      abbr <- "KF"
    }
    if (parcat3sel == toupper("Liver biochemistry")) {
      abbr <- "LV"
    }
    if (parcat3sel == toupper("Lipids")) abbr <- "LP"
  }
  if (length(parcat3sel) > 1) {
    if (
      all(
        parcat3sel %in%
          toupper(c(
            "Complete blood count",
            "WBC differential",
            "Coagulation studies"
          ))
      )
    ) {
      abbr <- "HM"
    }
  }

  if (is.null(abbr)) {
    message("Incorrect specification of parcat3sel")
  }

  abbr
}

get_tblid <- function(tblid, parcat3sel, method = c("after", "inbetween")) {
  abbr <- get_abbreviation(parcat3sel)

  method <- match.arg(method)
  # when inbetween, the abbreviation will be added prior to the number part of the table identifier
  # when after (default), the abbreviation will be added at the end of the table identifier

  x <- 0
  if (method == "inbetween") {
    x <- regexpr(pattern = "[0-9]", tblid)[1]
  }

  if (x > 0) {
    tblid1 <- substr(tblid, 1, x - 1)
    tblid2 <- substring(tblid, x)
    tblid_new <- paste0(tblid1, abbr, tblid2)
  } else {
    tblid_new <- paste0(tblid, abbr)
  }

  return(tblid_new)
}

## parcat3 options :
# current data: Liver biochemistry, General chemistry, Lipids, Kidney function, Complete blood count, WBC differential
# according shell: General chemistry, Kidney function, Liver biochemistry, Lipids, Hematology

## not in shell: Complete blood count, WBC differential
## not in data:  Hematology

availparcat3 <- c(
  "General chemistry",
  "Kidney function",
  "Liver biochemistry",
  "Lipids",
  "Complete blood count",
  "WBC differential",
  ""
)

# Process Data:

adsl <- pharmaverseadamjnj::adsl %>%
  filter(.data[[popfl]] == "Y") %>%
  select(
    STUDYID,
    USUBJID,
    all_of(c(popfl, trtvar, subgrpvar)),
    SEX,
    AGEGR1,
    RACE,
    ETHNIC,
    AGE
  )

msubgrp <- adsl %>%
  group_by(across(all_of(c(trtvar, subgrpvar)))) %>%
  summarize(count = n())

adsl$colspan_trt <- factor(
  ifelse(adsl[[trtvar]] == ctrl_grp, " ", "Active Study Agent"),
  levels = c("Active Study Agent", " ")
)

adsl$rrisk_header <- "Difference in Mean Change (95% CI)"
adsl$rrisk_label <- paste(adsl[[trtvar]], paste("vs", ctrl_grp))


colspan_trt_map <- create_colspan_map(
  adsl,
  non_active_grp = ctrl_grp,
  non_active_grp_span_lbl = " ",
  active_grp_span_lbl = "Active Study Agent",
  colspan_var = "colspan_trt",
  trt_var = trtvar
)
ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)


## For analysis on SI units: use adlb dataset
adlb_complete <- pharmaverseadamjnj::adlb

# selection of all non-unscheduled visits from data
visits <- adlb_complete %>%
  select(AVISIT) %>%
  filter(!grepl("UNSCHEDULED", toupper(AVISIT)))

visits$AVISIT <- droplevels(visits$AVISIT)
selvisit_data <- levels(visits$AVISIT)

### if preferred to get it from data, rather than hardcoded list of visits
# selvisit <- selvisit_data

adlb00 <- adlb_complete %>%
  select(
    USUBJID,
    AVISITN,
    AVISIT,
    starts_with("PAR"),
    AVAL,
    BASE,
    CHG,
    PCHG,
    starts_with("ANL"),
    ABLFL,
    APOBLFL
  ) %>%
  mutate(inlbdata = "Y") %>%
  inner_join(adsl) %>%
  relocate(USUBJID, PARAMCD, AVISIT, ANL02FL, ABLFL, APOBLFL)

parcat <- unique(adlb00 %>% select(starts_with("PARCAT"), PARAMCD, PARAM))

## retrieve the precision of AVAL on the input dataset
## review outcome and make updates manually if needed
## the precision variable will be used for the parameter-based formats in layout

## decimal = 4 is a cap in this derivation: if decimal precision of variable > decimal, the result will end up as decimal
## eg if AVAL has precision of 6 for parameter x, and decimal = 4, the resulting decimal value for parameter x is 4

## note that precision is on the raw values, as we are presenting mean/ci, and extra digit will be added
## eg precision = 2 will result in mean/ci format xx.xxx (xx.xxx, xx.xxx)

lb_precision <- tidytlg:::make_precision_data(
  df = adlb00,
  decimal = 3,
  precisionby = "PARAMCD",
  precisionon = "AVAL"
)

### data preparation

filtered_adlb_00 <- adlb00 %>%
  filter(AVISIT %in% selvisit) %>%
  ### unique record per timepoint:
  filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y"))

#### perform check on unique record per subject/param/timepoint
check_unique <- filtered_adlb_00 %>%
  group_by(USUBJID, PARAMCD, AVISIT) %>%
  mutate(n_recsub = n()) %>%
  filter(n_recsub > 1)

#### perform check on unique record per subject/param/timepoint
check_unique <- filtered_adlb_00 %>%
  group_by(USUBJID, PARAMCD, AVISIT) %>%
  mutate(n_recsub = n()) %>%
  filter(n_recsub > 1)

if (nrow(check_unique) > 0) {
  stop(
    "Your input dataset needs extra attention, as some subjects have more than one record per parameter/visit"
  )
  ### you will run into issues with fraction portion in count_denom_fraction, as count > denom, and fraction > 1 if you don't adjust your input dataset

  # Possible extra derivation - just to ensure program can run without issues
  ### Study team is responsible for adding this derivation onto ADaM dataset and ensure proper derivation rule for ANL02FL is implemented !!!!!!!!!!
  filtered_adlb_00x <- adlb00 %>%
    filter(PARAMCD %in% selparamcd) %>%
    filter(AVISIT %in% selvisit) %>%
    ### unique record per timepoint:
    filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y")) %>%
    group_by(USUBJID, PARAM, AVISIT) %>%
    mutate(n_sub = n()) %>%
    arrange(USUBJID, PARAM, AVISIT, ADT) %>%
    mutate(i = vctrs::vec_group_id(ADT)) %>%
    mutate(
      ANL02FL = case_when(
        n_sub == 1 ~ "Y",
        i == 1 ~ "Y"
      )
    ) %>%
    select(-c(i, n_sub)) %>%
    ungroup()

  filtered_adlb_00 <- filtered_adlb_00x %>%
    filter(PARAMCD %in% selparamcd) %>%
    filter(AVISIT %in% selvisit) %>%
    ### unique record per timepoint:
    filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y"))

  ## now your data should contain 1 record per subject per parameter
}

### for denominator per timepoint: all records from adlb on this timepoint: ignoring anl01fl/anl02fl/param
filtered_adlb_timepoints <- unique(
  adlb00 %>%
    filter(AVISIT %in% selvisit) %>%
    select(USUBJID, AVISITN, AVISIT, inlbdata)
) %>%
  inner_join(adsl)

# Core function to produce shell for specific parcat3 selection

build_result_parcat3 <- function(
  df = filtered_adlb_00,
  df_timepoints = filtered_adlb_timepoints,
  df_timepoints_subgroups = adlb_timepoints_subgroups,
  df_orig = adlb00,
  PARCAT3sel = NULL,
  .adsl = adsl,
  tblid,
  save2rtf = TRUE,
  .summ_vars = summ_vars,
  .trtvar = trtvar,
  .ref_path = ref_path,
  .ctrl_grp = ctrl_grp,
  .subgrpvar = subgrpvar,
  .subgrplbl = subgrplbl,
  .page_by = page_by,
  .selvisit = selvisit
) {
  tblidx <- get_tblid(tblid, PARCAT3sel)
  titles2 <- get_titles_from_file(input_path = '../../_data/', tblidx)

  .ctrl_grp <- utils::tail(.ref_path, n = 1)
  multivars <- c("AVAL", "AVAL", "CHG")

  extra_args_3col <- list(
    format_na_str = rep("NA", 3),
    d = "decimal",
    ref_path = .ref_path,
    ancova = ancova,
    comp_btw_group = comp_btw_group,
    indatavar = "inlbdata",
    multivars = multivars
  )

  ### continue with data preparation
  if (!is.null(PARCAT3sel)) {
    df <- df %>%
      filter(PARCAT3 %in% PARCAT3sel)
  }

  params <- unique(df %>% select(PARAMCD, PARAM))
  selparamcd <- params$PARAMCD
  sel_param <- params$PARAM

  df_timepoints <- df_timepoints %>%
    mutate(dummy_join = 1) %>%
    full_join(
      params %>% mutate(dummy_join = 1),
      relationship = "many-to-many"
    ) %>%
    select(-dummy_join)

  ### identify subjects in df_timepoints and not in df

  extra_sub <- anti_join(df_timepoints, df) %>%
    mutate(extra_sub = "Y")

  attr(extra_sub$extra_sub, "label") <- "Extra Subject step 1"

  ### only add these extra_sub to
  ### this will ensure we still meet the one record per subject per timepoint
  ### this will ensure length(x) can be used for the denominator derivation inside summarize_aval_chg_diff function

  df <- bind_rows(df, extra_sub) %>%
    arrange(USUBJID, PARAM, AVISITN)

  df <- df %>%
    inner_join(lb_precision, by = "PARAMCD")

  #### Only In case we want the subgroup N to come from ADSL, and not just from ADVS

  ### also add adsl subjects that have no vs data --- for subgroup counts from adsl

  adlb_timepoints_subgroups <-
    .adsl %>%
    select(USUBJID) %>%
    # define factor PARAMCD/AVISIT with one category, all levels we need
    mutate(
      PARAMCD = factor(selparamcd[1], levels = selparamcd),
      AVISIT = factor(.selvisit[1], levels = .selvisit)
    ) %>%
    # expand dataset to show all levels
    tidyr::complete(., USUBJID, PARAMCD, AVISIT)

  extra_sub2 <-
    anti_join(
      df_timepoints_subgroups,
      df %>% select(USUBJID, AVISITN, AVISIT, PARAMCD, PARAM)
    ) %>%
    left_join(
      .,
      unique(df_orig %>% select(AVISITN, AVISIT, PARAMCD, PARAM))
    ) %>%
    anti_join(., extra_sub) %>%
    inner_join(.adsl) %>%
    mutate(extra_sub2 = "Y")

  attr(extra_sub2$extra_sub2, "label") <- "Extra Subject step 2"

  ### add these extra_sub dataframe as well
  ### this will ensure we still meet the one record per subject per timepoint
  ### However, by adding also subjects without data in vs, we can no longer use length(x) for the denominator derivation inside summarize_aval_chg_diff function
  df <- bind_rows(df, extra_sub2) %>%
    arrange(USUBJID, PARAM, AVISITN)

  ### important: previous actions lost the label of variables
  ### in order to be able to use obj_label(filtered_adlb$PARAM) in layout, need to redefine the label

  ## do these 2 manually, as these are not available on advs00
  attr(df$extra_sub, "label") <- "Extra Subject step 1"
  attr(df$extra_sub2, "label") <- "Extra Subject step 2"

  df <- var_relabel_list(df, var_labels(df_orig, fill = T)) %>%
    relocate(USUBJID, PARAMCD, PARAM, AVISIT, AGEGR1, AVAL, CHG)

  df$PARAM <- factor(as.character(df$PARAM), levels = sel_param)

  ### important: previous actions lost the label of variables
  ### in order to be able to use obj_label(filtered_adlb$PARAM) in layout, need to redefine the label
  df <- var_relabel_list(df, var_labels(df_orig, fill = T))

  ################################################################################
  # Define layout and build table:
  ################################################################################

  lyt <- basic_table(show_colcounts = FALSE, colcount_format = "N=xx") %>%
    ### first columns
    split_cols_by(
      "colspan_trt",
      split_fun = trim_levels_to_map(map = colspan_trt_map)
    ) %>%
    split_cols_by(.trtvar, show_colcounts = TRUE, colcount_format = "N=xx") %>%
    split_rows_by(
      .subgrpvar,
      label_pos = "hidden",
      section_div = " ",
      split_fun = drop_split_levels,
      page_by = .page_by
    ) %>%
    ### just show number of subjects in current level of subgrpvar
    ### only show this number in the first AVAL column
    summarize_row_groups(
      var = .subgrpvar,
      cfun = a_freq_j,
      extra_args = list(
        label_fstr = .subgrplbl,
        extrablankline = TRUE,
        restr_columns = "AVAL",
        .stats = c("n_altdf"),
        riskdiff = FALSE,
        denom_by = subgrpvar
      )
    ) %>%
    split_rows_by(
      "PARAM",
      label_pos = "topleft",
      split_label = "Laboratory Test",
      section_div = " ",
      split_fun = drop_split_levels
    ) %>%
    ## note the child_labels = hidden for AVISIT, these labels will be taken care off by
    ## applying function summarize_aval_chg_diff further in the layout
    split_rows_by(
      "AVISIT",
      label_pos = "topleft",
      split_label = "Study Visit",
      split_fun = drop_split_levels,
      child_labels = "hidden"
    ) %>%
    ## set up a 3 column split
    split_cols_by_multivar(
      multivars,
      varlabels = c(
        "n/N (%)",
        "Mean (95% CI)",
        "Mean Change From Baseline (95% CI)"
      )
    ) %>%
    ### restart for the rrisk_header columns - note the nested = FALSE option
    ### also note the child_labels = "hidden" in both PARAM and AVISIT
    split_cols_by("rrisk_header", nested = FALSE) %>%
    split_cols_by(
      .trtvar,
      split_fun = remove_split_levels(.ctrl_grp),
      labels_var = "rrisk_label",
      show_colcounts = TRUE,
      colcount_format = "N=xx"
    ) %>%
    ### difference columns : just 1 column & analysis needs to be done on change
    split_cols_by_multivar(multivars[3], varlabels = c(" ")) %>%
    ### the variable passed here in analyze is not used (STUDYID), it is a dummy var passing,
    ### the function summarize_aval_chg_diff grabs the required vars from cols_by_multivar calls
    analyze(
      "STUDYID",
      afun = a_summarize_aval_chg_diff_j,
      extra_args = extra_args_3col
    )

  if (nrow(df) > 0) {
    result <- build_table(lyt, df, alt_counts_df = .adsl)

    ################################################################################
    # Post-Processing:
    # - Prune table to remove when n = 0 in all columns
    # - Remove the N=xx column headers for the difference vs PBO columns
    ################################################################################

    ### alhtough this is not really likely to occur in real data, this is a problem in the current synthetic data
    ### also here, try to remove this issue

    # rps_result <- row_paths_summary(result)

    ### below code is based upon tern pruning function has_count_in_any_col, with updates to internal function h_row_first_values for the 3 column - format we are using here

    my_has_count_in_any_col <- function(atleast, ...) {
      checkmate::assert_count(atleast)
      CombinationFunction(function(table_row) {
        row_counts <- my_h_row_counts(table_row, ...)
        ### small update compared to tern::has_count_in_any_col
        ## > vs >=
        any(row_counts > atleast)
      })
    }

    my_h_row_counts <-
      function(table_row, col_names = NULL, col_indices = NULL) {
        ## no updates compared to tern::h_row_counts, beyond using the customized my_h_row_first_values function
        counts <- my_h_row_first_values(table_row, col_names, col_indices)
        checkmate::assert_integerish(counts)
        counts
      }

    my_h_row_first_values <- function(
      table_row,
      col_names = NULL,
      col_indices = NULL
    ) {
      col_indices <- tern:::check_names_indices(
        table_row,
        col_names,
        col_indices
      )
      checkmate::assert_integerish(col_indices)
      checkmate::assert_subset(col_indices, seq_len(ncol(table_row)))

      # Main values are extracted
      row_vals <- row_values(table_row)[col_indices]

      ### specific updates to current situation -- 3 column layout, I want to grab the information from the n/N column, which is in first analysis of AVAL
      specific_cols <- names(row_vals)
      specific_cols <- specific_cols[stringr::str_ends(specific_cols, "AVAL")]

      row_vals <- row_vals[specific_cols]

      # Main return
      vapply(
        row_vals,
        function(rv) {
          if (is.null(rv)) {
            NA_real_
          } else {
            rv[1L]
          }
        },
        FUN.VALUE = numeric(1)
      )
    }

    more_than_0 <- my_has_count_in_any_col(atleast = 0)

    ## seem to work ok, not clear why it goes through each row twice?
    result <- prune_table(result, keep_rows(more_than_0))

    ## Remove the N=xx column headers for the difference vs PBO columns
    remove_col_count2 <- function(result, string = paste("vs", ctrl_grp)) {
      mcdf <- make_col_df(result, visible_only = FALSE)
      mcdfsel <- mcdf %>%
        filter(stringr::str_detect(toupper(label), toupper(string))) %>%
        pull(path)

      for (i in seq_along(mcdfsel)) {
        facet_colcount(result, mcdfsel[[i]]) <- NA
      }

      return(result)
    }

    result <- remove_col_count2(result)
  } else {
    result <- NULL
    message(paste0(
      "Parcat3 [",
      PARCAT3sel,
      "] is not present on input dataset"
    ))
    return(result)
  }

  ################################################################################
  # Set title
  ################################################################################

  result <- set_titles(result, titles2)

  if (save2rtf) {
    ################################################################################
    # Convert to tbl file and output table
    ################################################################################
    ### add the proper abbreviation to the tblid, and add opath path
fileid <- tblid

    tt_to_tlgrtf(string_map = string_map, tt = 
      result,
      file = fileid,
      orientation = "landscape",
      nosplitin = list(cols = c(.trtvar, "rrisk_header"))
    )
  }

  return(result)
}

# Apply core function to all specified levels of parcat3 selection

### note : the same core tblid (TSFLAB01a) will be used for all, inside the core function build_result_parcat3 the proper abbreviation will be added

### titles will not be retrieved for these, as the table identifiers are not in the DPS system
### study teams will have to ensure all versions that are needed are included in DPS system
result1 <- build_result_parcat3(
  PARCAT3sel = "Liver biochemistry",
  tblid = tblid,
  save2rtf = TRUE
)
result2 <- build_result_parcat3(
  PARCAT3sel = "Kidney function",
  tblid = tblid,
  save2rtf = TRUE
)
result3 <- build_result_parcat3(
  PARCAT3sel = "Lipids",
  tblid = tblid,
  save2rtf = TRUE
)

result4 <- build_result_parcat3(
  PARCAT3sel = c(
    "Complete blood count",
    "WBC differential",
    "Coagulation studies"
  ),
  tblid = tblid,
  save2rtf = TRUE
)

### if a certain category is not present, no rtf will be generated

result <- build_result_parcat3(PARCAT3sel = "General chemistry", tblid = tblid)

TSFLAB01a: Mean Change From Baseline for [Laboratory Category] Laboratory Data Over Time by [Subgroup]; Safety Analysis Set (Study jjcs - core)

Active Study Agent

Difference in Mean Change (95% CI)

Xanomeline High Dose

Xanomeline Low Dose

Placebo

Xanomeline High Dose vs Placebo

Xanomeline Low Dose vs Placebo

Laboratory Test

N=53

N=73

N=59

Study Visit

n/N (%)

Mean (95% CI)

Mean Change From Baseline (95% CI)

n/N (%)

Mean (95% CI)

Mean Change From Baseline (95% CI)

n/N (%)

Mean (95% CI)

Mean Change From Baseline (95% CI)

Age: ≥18 to <65 years

8

5

7

Calcium (mmol/L)

Baseline

8/8 (100.0%)

2.2923 (2.2328, 2.3517)

5/5 (100.0%)

2.2654 (2.1370, 2.3939)

7/7 (100.0%)

2.3453 (2.2447, 2.4459)

Cycle 02

3/8 (37.5%)

2.2122 (2.0832, 2.3412)

-0.0333 (-0.1764, 0.1099)

3/5 (60.0%)

2.2954 (2.0251, 2.5657)

0.0499 (-0.1980, 0.2978)

3/7 (42.9%)

2.2788 (2.1840, 2.3735)

0.0083 (-0.1477, 0.1643)

-0.0416 (-0.1786, 0.0954)

0.0416 (-0.1622, 0.2454)

Cycle 03

3/8 (37.5%)

2.2455 (1.9753, 2.5156)

-0.1331 (-0.3836, 0.1174)

1/5 (20.0%)

2.1956 (NE, NE)

-0.1248 (NE, NE)

2/7 (28.6%)

2.3578 (2.1996, 2.5159)

-0.0749 (-0.3919, 0.2422)

-0.0582 (-0.2771, 0.1607)

-0.0499 (NE, NE)

Cycle 04

2/8 (25.0%)

2.1956 (0.6099, 3.7813)

-0.1372 (-0.9298, 0.6553)

3/3 (100.0%)

2.3203 (2.0363, 2.6044)

0.0333 (-0.1844, 0.2509)

1/5 (20.0%)

2.1956 (NE, NE)

0.0250 (NE, NE)

-0.1622 (NE, NE)

0.0083 (NE, NE)

Glucose (mmol/L)

Baseline

7/8 (87.5%)

4.8690 (4.3508, 5.3873)

5/5 (100.0%)

5.2623 (4.8804, 5.6442)

7/7 (100.0%)

4.9800 (4.4337, 5.5264)

Cycle 02

4/8 (50.0%)

7.6742 (-1.9714, 17.3199)

1.2490 (-2.9572, 5.4552)

2/5 (40.0%)

4.4130 (-4.4032, 13.2293)

-0.6106 (-7.6638, 6.4426)

2/7 (28.6%)

6.0228 (-4.9096, 16.9552)

0.7216 (-14.7954, 16.2387)

0.5273 (-4.9711, 6.0258)

-1.3322 (-10.2987, 7.6342)

Cycle 03

3/8 (37.5%)

5.3475 (4.1909, 6.5040)

0.2776 (-0.5612, 1.1163)

3/5 (60.0%)

4.2928 (3.7187, 4.8669)

-0.9252 (-2.2645, 0.4141)

1/7 (14.3%)

4.6628 (NE, NE)

-0.2775 (NE, NE)

0.5551 (NE, NE)

-0.6476 (NE, NE)

Cycle 04

4/8 (50.0%)

5.6759 (3.7755, 7.5763)

0.6384 (-0.8632, 2.1400)

1/3 (33.3%)

5.8841 (NE, NE)

0.3331 (NE, NE)

2/5 (40.0%)

5.9118 (5.5593, 6.2644)

0.7216 (-5.6263, 7.0695)

-0.0833 (-2.3468, 2.1803)

-0.3886 (NE, NE)

Potassium (mmol/L)

Baseline

8/8 (100.0%)

4.24 (4.05, 4.43)

5/5 (100.0%)

4.12 (3.96, 4.28)

7/7 (100.0%)

4.44 (3.86, 5.02)

Cycle 02

5/8 (62.5%)

4.12 (3.80, 4.44)

-0.14 (-0.40, 0.12)

1/5 (20.0%)

4.20 (NE, NE)

0.20 (NE, NE)

6/7 (85.7%)

4.45 (3.89, 5.01)

0.02 (-0.42, 0.45)

-0.16 (-0.61, 0.29)

0.18 (NE, NE)

Cycle 03

6/8 (75.0%)

4.00 (3.64, 4.36)

-0.22 (-0.43, -0.00)

0/5 (0.0%)

NE (NE, NE)

NE (NE, NE)

6/7 (85.7%)

4.38 (3.88, 4.88)

-0.02 (-0.62, 0.59)

-0.20 (-0.81, 0.41)

NE (NE, NE)

Cycle 04

6/8 (75.0%)

3.90 (3.72, 4.08)

-0.32 (-0.51, -0.12)

2/3 (66.7%)

3.80 (-5.09, 12.69)

-0.45 (-8.71, 7.81)

3/5 (60.0%)

4.23 (2.96, 5.51)

0.00 (-0.25, 0.25)

-0.32 (-0.54, -0.09)

-0.45 (-8.44, 7.54)

Sodium (mmol/L)

Baseline

8/8 (100.0%)

138.8 (136.3, 141.2)

5/5 (100.0%)

139.6 (137.9, 141.3)

7/7 (100.0%)

140.4 (137.5, 143.3)

Cycle 02

3/8 (37.5%)

137.3 (123.7, 151.0)

-2.3 (-7.5, 2.8)

4/5 (80.0%)

139.2 (135.7, 142.8)

-0.8 (-4.0, 2.5)

4/7 (57.1%)

139.8 (137.4, 142.1)

-1.2 (-6.5, 4.0)

-1.1 (-6.4, 4.2)

0.5 (-4.5, 5.5)

Cycle 03

4/8 (50.0%)

138.5 (133.7, 143.3)

-2.0 (-4.6, 0.6)

4/5 (80.0%)

142.0 (138.6, 145.4)

2.8 (-2.5, 8.0)

4/7 (57.1%)

138.2 (135.0, 141.5)

-2.2 (-8.7, 4.2)

0.2 (-5.8, 6.3)

5.0 (-1.4, 11.4)

Cycle 04

4/8 (50.0%)

137.8 (134.2, 141.3)

-1.8 (-4.8, 1.3)

1/3 (33.3%)

141.0 (NE, NE)

0.0 (NE, NE)

4/5 (80.0%)

140.5 (135.6, 145.4)

0.2 (-4.3, 4.8)

-2.0 (-6.4, 2.4)

-0.2 (NE, NE)

Age: ≥65 to <75 years

16

17

19

Calcium (mmol/L)

Baseline

16/16 (100.0%)

2.3203 (2.2609, 2.3798)

17/17 (100.0%)

2.2822 (2.2287, 2.3357)

17/19 (89.5%)

2.2866 (2.2463, 2.3269)

Cycle 02

8/16 (50.0%)

2.3484 (2.2869, 2.4099)

0.0218 (-0.0473, 0.0910)

6/16 (37.5%)

2.2580 (2.1632, 2.3528)

-0.1081 (-0.2531, 0.0369)

13/19 (68.4%)

2.2685 (2.2007, 2.3364)

-0.0154 (-0.0729, 0.0422)

0.0372 (-0.0461, 0.1205)

-0.0928 (-0.2389, 0.0534)

Cycle 03

7/16 (43.8%)

2.3453 (2.2006, 2.4900)

0.0214 (-0.0623, 0.1050)

5/10 (50.0%)

2.2555 (2.1718, 2.3392)

0.0100 (-0.0894, 0.1094)

11/19 (57.9%)

2.2432 (2.1973, 2.2891)

-0.0227 (-0.0727, 0.0273)

0.0441 (-0.0459, 0.1340)

0.0327 (-0.0664, 0.1317)

Cycle 04

9/15 (60.0%)

2.2399 (2.1903, 2.2895)

-0.0582 (-0.1196, 0.0032)

4/9 (44.4%)

2.2393 (2.0492, 2.4293)

-0.0499 (-0.2183, 0.1185)

7/19 (36.8%)

2.2776 (2.1755, 2.3796)

0.0036 (-0.1450, 0.1522)

-0.0618 (-0.2137, 0.0902)

-0.0535 (-0.2369, 0.1300)

Glucose (mmol/L)

Baseline

16/16 (100.0%)

5.1590 (4.6692, 5.6488)

17/17 (100.0%)

5.2016 (4.6787, 5.7245)

18/19 (94.7%)

5.1008 (4.8030, 5.3985)

Cycle 02

7/16 (43.8%)

4.8135 (3.9354, 5.6916)

-0.0317 (-0.9213, 0.8579)

7/16 (43.8%)

5.0673 (4.4263, 5.7083)

-0.5789 (-1.8506, 0.6928)

13/19 (68.4%)

5.3204 (4.3573, 6.2835)

0.1751 (-0.6520, 1.0022)

-0.2068 (-1.3187, 0.9051)

-0.7540 (-2.1518, 0.6439)

Cycle 03

10/16 (62.5%)

6.3948 (4.4308, 8.3587)

0.9992 (-0.3407, 2.3390)

3/10 (30.0%)

6.0691 (2.9847, 9.1535)

0.1850 (-0.4952, 0.8653)

9/19 (47.4%)

5.7977 (4.6869, 6.9086)

0.4071 (-0.8660, 1.6802)

0.5921 (-1.1162, 2.3004)

-0.2220 (-1.5185, 1.0744)

Cycle 04

7/15 (46.7%)

5.3131 (3.9917, 6.6346)

0.4361 (-1.3230, 2.1953)

4/9 (44.4%)

5.4677 (3.7226, 7.2128)

-0.0555 (-1.7652, 1.6542)

5/19 (26.3%)

4.6961 (4.2532, 5.1391)

-0.2442 (-0.6680, 0.1795)

0.6804 (-1.0830, 2.4438)

0.1887 (-1.4562, 1.8337)

Potassium (mmol/L)

Baseline

15/16 (93.8%)

4.43 (4.24, 4.62)

17/17 (100.0%)

4.43 (4.24, 4.62)

19/19 (100.0%)

4.16 (3.95, 4.36)

Cycle 02

9/16 (56.2%)

4.57 (4.25, 4.88)

0.26 (-0.08, 0.59)

10/16 (62.5%)

4.17 (3.87, 4.47)

-0.20 (-0.60, 0.20)

8/19 (42.1%)

4.11 (3.83, 4.39)

-0.03 (-0.37, 0.32)

0.28 (-0.16, 0.72)

-0.17 (-0.66, 0.31)

Cycle 03

8/16 (50.0%)

4.26 (4.03, 4.50)

-0.36 (-0.64, -0.08)

6/10 (60.0%)

4.22 (3.77, 4.66)

-0.13 (-0.49, 0.22)

9/19 (47.4%)

3.93 (3.78, 4.09)

-0.20 (-0.50, 0.10)

-0.16 (-0.53, 0.21)

0.07 (-0.34, 0.48)

Cycle 04

8/15 (53.3%)

4.16 (4.04, 4.29)

-0.41 (-0.70, -0.13)

3/9 (33.3%)

4.13 (3.37, 4.89)

-0.10 (-1.48, 1.28)

10/19 (52.6%)

4.26 (4.00, 4.52)

0.06 (-0.21, 0.33)

-0.47 (-0.83, -0.11)

-0.16 (-1.36, 1.04)

Sodium (mmol/L)

Baseline

15/16 (93.8%)

140.3 (138.9, 141.7)

17/17 (100.0%)

140.1 (138.7, 141.6)

18/19 (94.7%)

140.4 (139.3, 141.5)

Cycle 02

10/16 (62.5%)

138.8 (137.5, 140.1)

-1.9 (-4.1, 0.3)

8/16 (50.0%)

139.4 (138.4, 140.4)

0.0 (-3.2, 3.2)

12/19 (63.2%)

142.0 (140.0, 144.0)

2.4 (-0.1, 4.9)

-4.3 (-7.4, -1.2)

-2.4 (-6.2, 1.3)

Cycle 03

8/16 (50.0%)

139.2 (137.9, 140.6)

-1.5 (-3.7, 0.7)

4/10 (40.0%)

141.5 (138.7, 144.3)

-0.2 (-2.6, 2.1)

11/19 (57.9%)

140.5 (139.1, 141.8)

0.2 (-1.8, 2.2)

-1.7 (-4.4, 1.1)

-0.4 (-3.0, 2.2)

Cycle 04

2/15 (13.3%)

138.5 (81.3, 195.7)

-1.5 (-71.4, 68.4)

4/9 (44.4%)

139.8 (135.2, 144.3)

-0.2 (-3.0, 2.5)

9/19 (47.4%)

139.9 (138.3, 141.4)

-0.6 (-2.8, 1.7)

-0.9 (-62.9, 61.0)

0.3 (-2.6, 3.2)

Age: ≥75 years

29

51

33

Calcium (mmol/L)

Baseline

28/29 (96.6%)

2.2918 (2.2512, 2.3324)

46/50 (92.0%)

2.3057 (2.2699, 2.3415)

31/33 (93.9%)

2.3131 (2.2837, 2.3424)

Cycle 02

18/29 (62.1%)

2.2732 (2.2059, 2.3405)

-0.0236 (-0.0790, 0.0318)

22/46 (47.8%)

2.3011 (2.2362, 2.3659)

-0.0079 (-0.0561, 0.0403)

17/32 (53.1%)

2.2646 (2.2189, 2.3103)

-0.0470 (-0.0990, 0.0051)

0.0234 (-0.0498, 0.0966)

0.0390 (-0.0295, 0.1076)

Cycle 03

11/29 (37.9%)

2.2773 (2.2206, 2.3339)

0.0045 (-0.0722, 0.0813)

15/37 (40.5%)

2.2954 (2.2424, 2.3484)

-0.0283 (-0.0666, 0.0101)

11/30 (36.7%)

2.2636 (2.2095, 2.3177)

-0.0340 (-0.0850, 0.0170)

0.0386 (-0.0485, 0.1257)

0.0057 (-0.0547, 0.0662)

Cycle 04

13/27 (48.1%)

2.2935 (2.2520, 2.3350)

-0.0038 (-0.0737, 0.0660)

18/35 (51.4%)

2.2857 (2.2287, 2.3427)

0.0069 (-0.0332, 0.0470)

16/27 (59.3%)

2.2891 (2.2318, 2.3465)

-0.0312 (-0.0847, 0.0223)

0.0273 (-0.0567, 0.1114)

0.0381 (-0.0263, 0.1025)

Glucose (mmol/L)

Baseline

27/29 (93.1%)

5.4852 (4.9345, 6.0359)

48/50 (96.0%)

5.5001 (5.1602, 5.8400)

32/33 (97.0%)

5.5042 (5.0416, 5.9668)

Cycle 02

16/29 (55.2%)

7.3308 (4.8598, 9.8018)

1.4988 (-0.3368, 3.3343)

21/46 (45.7%)

5.5959 (4.9945, 6.1974)

0.3516 (-0.2345, 0.9377)

16/32 (50.0%)

5.4781 (4.8872, 6.0691)

-0.1596 (-0.7538, 0.4346)

1.6584 (-0.2425, 3.5593)

0.5112 (-0.2929, 1.3152)

Cycle 03

14/29 (48.3%)

6.3678 (4.9685, 7.7671)

0.8009 (0.1175, 1.4844)

24/37 (64.9%)

5.4585 (4.9706, 5.9464)

0.0601 (-0.3467, 0.4670)

12/30 (40.0%)

5.0468 (4.3048, 5.7888)

-0.3654 (-1.3988, 0.6679)

1.1664 (-0.0153, 2.3481)

0.4256 (-0.6595, 1.5107)

Cycle 04

12/27 (44.4%)

7.6743 (4.1117, 11.2368)

1.4756 (-1.3474, 4.2987)

16/35 (45.7%)

5.2179 (4.8509, 5.5850)

-0.2984 (-0.8830, 0.2863)

14/27 (51.9%)

5.7849 (4.4980, 7.0718)

0.0634 (-0.9333, 1.0602)

1.4122 (-1.5146, 4.3390)

-0.3618 (-1.4765, 0.7529)

Potassium (mmol/L)

Baseline

28/29 (96.6%)

4.19 (4.05, 4.34)

46/50 (92.0%)

4.31 (4.20, 4.42)

31/33 (93.9%)

4.27 (4.13, 4.42)

Cycle 02

15/29 (51.7%)

4.24 (4.03, 4.45)

0.17 (-0.02, 0.36)

20/46 (43.5%)

4.30 (4.08, 4.51)

0.02 (-0.18, 0.22)

16/32 (50.0%)

4.30 (4.10, 4.50)

0.09 (-0.15, 0.33)

0.08 (-0.22, 0.37)

-0.07 (-0.37, 0.23)

Cycle 03

11/29 (37.9%)

4.32 (4.02, 4.61)

-0.13 (-0.50, 0.25)

18/37 (48.6%)

4.39 (4.14, 4.65)

0.03 (-0.13, 0.19)

13/30 (43.3%)

4.17 (3.87, 4.47)

-0.08 (-0.34, 0.18)

-0.05 (-0.48, 0.38)

0.10 (-0.19, 0.40)

Cycle 04

10/27 (37.0%)

4.21 (3.91, 4.51)

0.05 (-0.24, 0.34)

19/35 (54.3%)

4.32 (4.12, 4.52)

0.03 (-0.18, 0.24)

15/27 (55.6%)

4.29 (4.10, 4.48)

-0.02 (-0.28, 0.24)

0.07 (-0.29, 0.43)

0.05 (-0.27, 0.37)

Sodium (mmol/L)

Baseline

29/29 (100.0%)

140.0 (139.1, 141.0)

47/50 (94.0%)

139.7 (138.9, 140.5)

32/33 (97.0%)

141.0 (139.9, 142.0)

Cycle 02

9/29 (31.0%)

139.0 (137.0, 141.0)

-0.2 (-3.2, 2.7)

23/46 (50.0%)

139.7 (138.3, 141.2)

0.8 (-0.6, 2.2)

15/32 (46.9%)

139.3 (137.5, 141.2)

-0.6 (-2.1, 0.9)

0.4 (-2.8, 3.5)

1.4 (-0.6, 3.4)

Cycle 03

14/29 (48.3%)

140.0 (138.3, 141.7)

0.8 (-1.1, 2.6)

16/37 (43.2%)

140.1 (138.7, 141.5)

0.5 (-0.9, 1.9)

11/30 (36.7%)

139.5 (137.3, 141.6)

-1.6 (-3.8, 0.6)

2.4 (-0.3, 5.2)

2.1 (-0.4, 4.6)

Cycle 04

20/27 (74.1%)

141.2 (140.1, 142.4)

1.6 (0.4, 2.7)

15/35 (42.9%)

140.5 (139.3, 141.6)

1.1 (-0.1, 2.3)

9/27 (33.3%)

140.7 (139.5, 141.9)

-1.4 (-4.2, 1.3)

3.0 (0.1, 5.9)

2.5 (-0.4, 5.4)

Download RTF file

TSFLAB01
TSFLAB02
Source Code
---
title: TSFLAB01A
subtitle: Mean Change From Baseline for Laboratory Category Laboratory Data Over Time by Subgroup
---

------------------------------------------------------------------------

{{< include ../../_utils/envir_hook.qmd >}}

```{r setup, echo = FALSE, warning = FALSE, message = FALSE}
options(docx.add_datetime = FALSE, tidytlg.add_datetime = FALSE)
envsetup_config_name <- "default"

# Path to the combined config file
envsetup_file_path <- file.path("../..", "envsetup.yml")

Sys.setenv(ENVSETUP_ENVIRON = '')
library(envsetup)
loaded_config <- config::get(config = envsetup_config_name, file = envsetup_file_path)
envsetup::rprofile(loaded_config)


dpscomp <- compound
dpspdr <- paste(protocol,dbrelease,rpteff,sep="__")

aptcomp <- compound
aptpdr <- paste(protocol,dbrelease,rpteff,sep="__")

###### Study specific updates (formerly in envre)

dpscomp <- "standards"
dpspdr <- "jjcs__NULL__jjcs - core"

apt <- FALSE
library(junco)
default_str_map <- rbind(default_str_map, c("&ctcae", "5.0"))


```

## Output

:::: panel-tabset
## {{< fa regular file-lines sm fw >}} Preview

```{r variant1, results='hide', warning = FALSE, message = FALSE}

# Program Name:              tsflab01a

# Prep Environment

library(envsetup)
library(tern)
library(dplyr)
library(rtables)
library(junco)

# Define script level parameters:

tblid <- "TSFLAB01a"
fileid <- tblid
titles <- get_titles_from_file(input_path = '../../_data/', tblid)
string_map <- default_str_map

popfl <- "SAFFL"
trtvar <- "TRT01A"
ctrl_grp <- "Placebo"

# Note on ancova parameter
# when ancova = TRUE
# ancova model will be used to calculate all mean/mean change columns
# not just those from the Difference column
# model specification
summ_vars <- list(arm = trtvar, covariates = NULL)

# when ancova = FALSE, all mean/mean change columns will be from descriptive stats
# for the difference column descriptive stats will be based upon two-sample t-test
ancova <- FALSE


comp_btw_group <- TRUE


subgrpvar <- "AGEGR1"
subgrplbl <- "Age: %s years"

page_by <- TRUE # Set page_by TRUE/FALSE if you (do not) wish to start a new page after a new subgroup
indent_adj <- 0L
if (page_by) {
  indent_adj <- 1L
}

## For analysis on SI units: use adlb dataset
## For analysis on Conventional units: use adlbc dataset -- shell is in conventional units

ad_domain <- "ADLB"

# see further, an alternative method to identify all non-unscheduled visits based upon data
selvisit <- c("Screening", "Baseline", "Cycle 02", "Cycle 03", "Cycle 04")

### note : this shell covers multiple tables depending on parcat3 selections

## allowed PARCAT3 selections

# parcat3sel <- "General chemistry"
# parcat3sel <- "Kidney function"
# parcat3sel <- "Liver biochemistry"
# parcat3sel <- "Lipids"
#
# ### Hematology (HM) : has 3 subcategories that should be included on one table
# parcat3sel <- c("Complete blood count","WBC differential","Coagulation studies")

# per DPS specifications, the output identifier should include the abbreviation for the category

# 1. Present laboratory tests using separate outputs for each category as follows:
#   General chemistry (GC): Sodium, Potassium, Chloride, Bicarbonate, Urea Nitrogen, Glucose, Calcium, Magnesium, Phosphate, Protein, Albumin, Creatine Kinase, Amylase, Lipase
#   Kidney function (KF): Creatinine, GFR from Creatinine
#   Liver biochemistry (LV): Alkaline Phosphatase, Alanine Aminotransferase, Aspartate Aminotransferase, Bilirubin, Prothrombin Intl. Normalized Ratio, Gamma Glutamyl Transferase
#   Lipids (LP): Cholesterol, HDL Cholesterol, LDL Cholesterol, Triglycerides
#   Hematology (HM):  Subcategory rows should be included for Complete Blood Count, White Blood Cell Differential and for Coagulation Studies
#     Complete blood count: Leukocytes, Hemoglobin, Platelets;
#     WBC differential: Lymphocytes, Neutrophils, Eosinophils;
#     Coagulation studies: Prothrombin Time, Activated Partial Thromboplastin Time.

# The output identifier should include the abbreviation for the laboratory category (eg, TSFLAB02GC for General Chemistry)

# In current template program, only 1 version is created, without the proper abbreviation appended
# The reason for this is that TSFLAB02GC is not included in the DPS system - only the core version TSFLAB02

get_abbreviation <- function(parcat3sel) {
  parcat3sel <- toupper(parcat3sel)
  abbr <- NULL
  if (length(parcat3sel) == 1) {
    if (parcat3sel == toupper("General chemistry")) {
      abbr <- "GC"
    }
    # the following line should be removed for a true study, global jjcs standards in DPS system does not include the abbreviation
    if (parcat3sel == toupper("General chemistry")) {
      abbr <- ""
    }
    #
    if (parcat3sel == toupper("Kidney function")) {
      abbr <- "KF"
    }
    if (parcat3sel == toupper("Liver biochemistry")) {
      abbr <- "LV"
    }
    if (parcat3sel == toupper("Lipids")) abbr <- "LP"
  }
  if (length(parcat3sel) > 1) {
    if (
      all(
        parcat3sel %in%
          toupper(c(
            "Complete blood count",
            "WBC differential",
            "Coagulation studies"
          ))
      )
    ) {
      abbr <- "HM"
    }
  }

  if (is.null(abbr)) {
    message("Incorrect specification of parcat3sel")
  }

  abbr
}

get_tblid <- function(tblid, parcat3sel, method = c("after", "inbetween")) {
  abbr <- get_abbreviation(parcat3sel)

  method <- match.arg(method)
  # when inbetween, the abbreviation will be added prior to the number part of the table identifier
  # when after (default), the abbreviation will be added at the end of the table identifier

  x <- 0
  if (method == "inbetween") {
    x <- regexpr(pattern = "[0-9]", tblid)[1]
  }

  if (x > 0) {
    tblid1 <- substr(tblid, 1, x - 1)
    tblid2 <- substring(tblid, x)
    tblid_new <- paste0(tblid1, abbr, tblid2)
  } else {
    tblid_new <- paste0(tblid, abbr)
  }

  return(tblid_new)
}

## parcat3 options :
# current data: Liver biochemistry, General chemistry, Lipids, Kidney function, Complete blood count, WBC differential
# according shell: General chemistry, Kidney function, Liver biochemistry, Lipids, Hematology

## not in shell: Complete blood count, WBC differential
## not in data:  Hematology

availparcat3 <- c(
  "General chemistry",
  "Kidney function",
  "Liver biochemistry",
  "Lipids",
  "Complete blood count",
  "WBC differential",
  ""
)

# Process Data:

adsl <- pharmaverseadamjnj::adsl %>%
  filter(.data[[popfl]] == "Y") %>%
  select(
    STUDYID,
    USUBJID,
    all_of(c(popfl, trtvar, subgrpvar)),
    SEX,
    AGEGR1,
    RACE,
    ETHNIC,
    AGE
  )

msubgrp <- adsl %>%
  group_by(across(all_of(c(trtvar, subgrpvar)))) %>%
  summarize(count = n())

adsl$colspan_trt <- factor(
  ifelse(adsl[[trtvar]] == ctrl_grp, " ", "Active Study Agent"),
  levels = c("Active Study Agent", " ")
)

adsl$rrisk_header <- "Difference in Mean Change (95% CI)"
adsl$rrisk_label <- paste(adsl[[trtvar]], paste("vs", ctrl_grp))


colspan_trt_map <- create_colspan_map(
  adsl,
  non_active_grp = ctrl_grp,
  non_active_grp_span_lbl = " ",
  active_grp_span_lbl = "Active Study Agent",
  colspan_var = "colspan_trt",
  trt_var = trtvar
)
ref_path <- c("colspan_trt", " ", trtvar, ctrl_grp)


## For analysis on SI units: use adlb dataset
adlb_complete <- pharmaverseadamjnj::adlb

# selection of all non-unscheduled visits from data
visits <- adlb_complete %>%
  select(AVISIT) %>%
  filter(!grepl("UNSCHEDULED", toupper(AVISIT)))

visits$AVISIT <- droplevels(visits$AVISIT)
selvisit_data <- levels(visits$AVISIT)

### if preferred to get it from data, rather than hardcoded list of visits
# selvisit <- selvisit_data

adlb00 <- adlb_complete %>%
  select(
    USUBJID,
    AVISITN,
    AVISIT,
    starts_with("PAR"),
    AVAL,
    BASE,
    CHG,
    PCHG,
    starts_with("ANL"),
    ABLFL,
    APOBLFL
  ) %>%
  mutate(inlbdata = "Y") %>%
  inner_join(adsl) %>%
  relocate(USUBJID, PARAMCD, AVISIT, ANL02FL, ABLFL, APOBLFL)

parcat <- unique(adlb00 %>% select(starts_with("PARCAT"), PARAMCD, PARAM))

## retrieve the precision of AVAL on the input dataset
## review outcome and make updates manually if needed
## the precision variable will be used for the parameter-based formats in layout

## decimal = 4 is a cap in this derivation: if decimal precision of variable > decimal, the result will end up as decimal
## eg if AVAL has precision of 6 for parameter x, and decimal = 4, the resulting decimal value for parameter x is 4

## note that precision is on the raw values, as we are presenting mean/ci, and extra digit will be added
## eg precision = 2 will result in mean/ci format xx.xxx (xx.xxx, xx.xxx)

lb_precision <- tidytlg:::make_precision_data(
  df = adlb00,
  decimal = 3,
  precisionby = "PARAMCD",
  precisionon = "AVAL"
)

### data preparation

filtered_adlb_00 <- adlb00 %>%
  filter(AVISIT %in% selvisit) %>%
  ### unique record per timepoint:
  filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y"))

#### perform check on unique record per subject/param/timepoint
check_unique <- filtered_adlb_00 %>%
  group_by(USUBJID, PARAMCD, AVISIT) %>%
  mutate(n_recsub = n()) %>%
  filter(n_recsub > 1)

#### perform check on unique record per subject/param/timepoint
check_unique <- filtered_adlb_00 %>%
  group_by(USUBJID, PARAMCD, AVISIT) %>%
  mutate(n_recsub = n()) %>%
  filter(n_recsub > 1)

if (nrow(check_unique) > 0) {
  stop(
    "Your input dataset needs extra attention, as some subjects have more than one record per parameter/visit"
  )
  ### you will run into issues with fraction portion in count_denom_fraction, as count > denom, and fraction > 1 if you don't adjust your input dataset

  # Possible extra derivation - just to ensure program can run without issues
  ### Study team is responsible for adding this derivation onto ADaM dataset and ensure proper derivation rule for ANL02FL is implemented !!!!!!!!!!
  filtered_adlb_00x <- adlb00 %>%
    filter(PARAMCD %in% selparamcd) %>%
    filter(AVISIT %in% selvisit) %>%
    ### unique record per timepoint:
    filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y")) %>%
    group_by(USUBJID, PARAM, AVISIT) %>%
    mutate(n_sub = n()) %>%
    arrange(USUBJID, PARAM, AVISIT, ADT) %>%
    mutate(i = vctrs::vec_group_id(ADT)) %>%
    mutate(
      ANL02FL = case_when(
        n_sub == 1 ~ "Y",
        i == 1 ~ "Y"
      )
    ) %>%
    select(-c(i, n_sub)) %>%
    ungroup()

  filtered_adlb_00 <- filtered_adlb_00x %>%
    filter(PARAMCD %in% selparamcd) %>%
    filter(AVISIT %in% selvisit) %>%
    ### unique record per timepoint:
    filter(ANL02FL == "Y" & (ABLFL == "Y" | APOBLFL == "Y"))

  ## now your data should contain 1 record per subject per parameter
}

### for denominator per timepoint: all records from adlb on this timepoint: ignoring anl01fl/anl02fl/param
filtered_adlb_timepoints <- unique(
  adlb00 %>%
    filter(AVISIT %in% selvisit) %>%
    select(USUBJID, AVISITN, AVISIT, inlbdata)
) %>%
  inner_join(adsl)

# Core function to produce shell for specific parcat3 selection

build_result_parcat3 <- function(
  df = filtered_adlb_00,
  df_timepoints = filtered_adlb_timepoints,
  df_timepoints_subgroups = adlb_timepoints_subgroups,
  df_orig = adlb00,
  PARCAT3sel = NULL,
  .adsl = adsl,
  tblid,
  save2rtf = TRUE,
  .summ_vars = summ_vars,
  .trtvar = trtvar,
  .ref_path = ref_path,
  .ctrl_grp = ctrl_grp,
  .subgrpvar = subgrpvar,
  .subgrplbl = subgrplbl,
  .page_by = page_by,
  .selvisit = selvisit
) {
  tblidx <- get_tblid(tblid, PARCAT3sel)
  titles2 <- get_titles_from_file(input_path = '../../_data/', tblidx)

  .ctrl_grp <- utils::tail(.ref_path, n = 1)
  multivars <- c("AVAL", "AVAL", "CHG")

  extra_args_3col <- list(
    format_na_str = rep("NA", 3),
    d = "decimal",
    ref_path = .ref_path,
    ancova = ancova,
    comp_btw_group = comp_btw_group,
    indatavar = "inlbdata",
    multivars = multivars
  )

  ### continue with data preparation
  if (!is.null(PARCAT3sel)) {
    df <- df %>%
      filter(PARCAT3 %in% PARCAT3sel)
  }

  params <- unique(df %>% select(PARAMCD, PARAM))
  selparamcd <- params$PARAMCD
  sel_param <- params$PARAM

  df_timepoints <- df_timepoints %>%
    mutate(dummy_join = 1) %>%
    full_join(
      params %>% mutate(dummy_join = 1),
      relationship = "many-to-many"
    ) %>%
    select(-dummy_join)

  ### identify subjects in df_timepoints and not in df

  extra_sub <- anti_join(df_timepoints, df) %>%
    mutate(extra_sub = "Y")

  attr(extra_sub$extra_sub, "label") <- "Extra Subject step 1"

  ### only add these extra_sub to
  ### this will ensure we still meet the one record per subject per timepoint
  ### this will ensure length(x) can be used for the denominator derivation inside summarize_aval_chg_diff function

  df <- bind_rows(df, extra_sub) %>%
    arrange(USUBJID, PARAM, AVISITN)

  df <- df %>%
    inner_join(lb_precision, by = "PARAMCD")

  #### Only In case we want the subgroup N to come from ADSL, and not just from ADVS

  ### also add adsl subjects that have no vs data --- for subgroup counts from adsl

  adlb_timepoints_subgroups <-
    .adsl %>%
    select(USUBJID) %>%
    # define factor PARAMCD/AVISIT with one category, all levels we need
    mutate(
      PARAMCD = factor(selparamcd[1], levels = selparamcd),
      AVISIT = factor(.selvisit[1], levels = .selvisit)
    ) %>%
    # expand dataset to show all levels
    tidyr::complete(., USUBJID, PARAMCD, AVISIT)

  extra_sub2 <-
    anti_join(
      df_timepoints_subgroups,
      df %>% select(USUBJID, AVISITN, AVISIT, PARAMCD, PARAM)
    ) %>%
    left_join(
      .,
      unique(df_orig %>% select(AVISITN, AVISIT, PARAMCD, PARAM))
    ) %>%
    anti_join(., extra_sub) %>%
    inner_join(.adsl) %>%
    mutate(extra_sub2 = "Y")

  attr(extra_sub2$extra_sub2, "label") <- "Extra Subject step 2"

  ### add these extra_sub dataframe as well
  ### this will ensure we still meet the one record per subject per timepoint
  ### However, by adding also subjects without data in vs, we can no longer use length(x) for the denominator derivation inside summarize_aval_chg_diff function
  df <- bind_rows(df, extra_sub2) %>%
    arrange(USUBJID, PARAM, AVISITN)

  ### important: previous actions lost the label of variables
  ### in order to be able to use obj_label(filtered_adlb$PARAM) in layout, need to redefine the label

  ## do these 2 manually, as these are not available on advs00
  attr(df$extra_sub, "label") <- "Extra Subject step 1"
  attr(df$extra_sub2, "label") <- "Extra Subject step 2"

  df <- var_relabel_list(df, var_labels(df_orig, fill = T)) %>%
    relocate(USUBJID, PARAMCD, PARAM, AVISIT, AGEGR1, AVAL, CHG)

  df$PARAM <- factor(as.character(df$PARAM), levels = sel_param)

  ### important: previous actions lost the label of variables
  ### in order to be able to use obj_label(filtered_adlb$PARAM) in layout, need to redefine the label
  df <- var_relabel_list(df, var_labels(df_orig, fill = T))

  ################################################################################
  # Define layout and build table:
  ################################################################################

  lyt <- basic_table(show_colcounts = FALSE, colcount_format = "N=xx") %>%
    ### first columns
    split_cols_by(
      "colspan_trt",
      split_fun = trim_levels_to_map(map = colspan_trt_map)
    ) %>%
    split_cols_by(.trtvar, show_colcounts = TRUE, colcount_format = "N=xx") %>%
    split_rows_by(
      .subgrpvar,
      label_pos = "hidden",
      section_div = " ",
      split_fun = drop_split_levels,
      page_by = .page_by
    ) %>%
    ### just show number of subjects in current level of subgrpvar
    ### only show this number in the first AVAL column
    summarize_row_groups(
      var = .subgrpvar,
      cfun = a_freq_j,
      extra_args = list(
        label_fstr = .subgrplbl,
        extrablankline = TRUE,
        restr_columns = "AVAL",
        .stats = c("n_altdf"),
        riskdiff = FALSE,
        denom_by = subgrpvar
      )
    ) %>%
    split_rows_by(
      "PARAM",
      label_pos = "topleft",
      split_label = "Laboratory Test",
      section_div = " ",
      split_fun = drop_split_levels
    ) %>%
    ## note the child_labels = hidden for AVISIT, these labels will be taken care off by
    ## applying function summarize_aval_chg_diff further in the layout
    split_rows_by(
      "AVISIT",
      label_pos = "topleft",
      split_label = "Study Visit",
      split_fun = drop_split_levels,
      child_labels = "hidden"
    ) %>%
    ## set up a 3 column split
    split_cols_by_multivar(
      multivars,
      varlabels = c(
        "n/N (%)",
        "Mean (95% CI)",
        "Mean Change From Baseline (95% CI)"
      )
    ) %>%
    ### restart for the rrisk_header columns - note the nested = FALSE option
    ### also note the child_labels = "hidden" in both PARAM and AVISIT
    split_cols_by("rrisk_header", nested = FALSE) %>%
    split_cols_by(
      .trtvar,
      split_fun = remove_split_levels(.ctrl_grp),
      labels_var = "rrisk_label",
      show_colcounts = TRUE,
      colcount_format = "N=xx"
    ) %>%
    ### difference columns : just 1 column & analysis needs to be done on change
    split_cols_by_multivar(multivars[3], varlabels = c(" ")) %>%
    ### the variable passed here in analyze is not used (STUDYID), it is a dummy var passing,
    ### the function summarize_aval_chg_diff grabs the required vars from cols_by_multivar calls
    analyze(
      "STUDYID",
      afun = a_summarize_aval_chg_diff_j,
      extra_args = extra_args_3col
    )

  if (nrow(df) > 0) {
    result <- build_table(lyt, df, alt_counts_df = .adsl)

    ################################################################################
    # Post-Processing:
    # - Prune table to remove when n = 0 in all columns
    # - Remove the N=xx column headers for the difference vs PBO columns
    ################################################################################

    ### alhtough this is not really likely to occur in real data, this is a problem in the current synthetic data
    ### also here, try to remove this issue

    # rps_result <- row_paths_summary(result)

    ### below code is based upon tern pruning function has_count_in_any_col, with updates to internal function h_row_first_values for the 3 column - format we are using here

    my_has_count_in_any_col <- function(atleast, ...) {
      checkmate::assert_count(atleast)
      CombinationFunction(function(table_row) {
        row_counts <- my_h_row_counts(table_row, ...)
        ### small update compared to tern::has_count_in_any_col
        ## > vs >=
        any(row_counts > atleast)
      })
    }

    my_h_row_counts <-
      function(table_row, col_names = NULL, col_indices = NULL) {
        ## no updates compared to tern::h_row_counts, beyond using the customized my_h_row_first_values function
        counts <- my_h_row_first_values(table_row, col_names, col_indices)
        checkmate::assert_integerish(counts)
        counts
      }

    my_h_row_first_values <- function(
      table_row,
      col_names = NULL,
      col_indices = NULL
    ) {
      col_indices <- tern:::check_names_indices(
        table_row,
        col_names,
        col_indices
      )
      checkmate::assert_integerish(col_indices)
      checkmate::assert_subset(col_indices, seq_len(ncol(table_row)))

      # Main values are extracted
      row_vals <- row_values(table_row)[col_indices]

      ### specific updates to current situation -- 3 column layout, I want to grab the information from the n/N column, which is in first analysis of AVAL
      specific_cols <- names(row_vals)
      specific_cols <- specific_cols[stringr::str_ends(specific_cols, "AVAL")]

      row_vals <- row_vals[specific_cols]

      # Main return
      vapply(
        row_vals,
        function(rv) {
          if (is.null(rv)) {
            NA_real_
          } else {
            rv[1L]
          }
        },
        FUN.VALUE = numeric(1)
      )
    }

    more_than_0 <- my_has_count_in_any_col(atleast = 0)

    ## seem to work ok, not clear why it goes through each row twice?
    result <- prune_table(result, keep_rows(more_than_0))

    ## Remove the N=xx column headers for the difference vs PBO columns
    remove_col_count2 <- function(result, string = paste("vs", ctrl_grp)) {
      mcdf <- make_col_df(result, visible_only = FALSE)
      mcdfsel <- mcdf %>%
        filter(stringr::str_detect(toupper(label), toupper(string))) %>%
        pull(path)

      for (i in seq_along(mcdfsel)) {
        facet_colcount(result, mcdfsel[[i]]) <- NA
      }

      return(result)
    }

    result <- remove_col_count2(result)
  } else {
    result <- NULL
    message(paste0(
      "Parcat3 [",
      PARCAT3sel,
      "] is not present on input dataset"
    ))
    return(result)
  }

  ################################################################################
  # Set title
  ################################################################################

  result <- set_titles(result, titles2)

  if (save2rtf) {
    ################################################################################
    # Convert to tbl file and output table
    ################################################################################
    ### add the proper abbreviation to the tblid, and add opath path
fileid <- tblid

    tt_to_tlgrtf(string_map = string_map, tt = 
      result,
      file = fileid,
      orientation = "landscape",
      nosplitin = list(cols = c(.trtvar, "rrisk_header"))
    )
  }

  return(result)
}

# Apply core function to all specified levels of parcat3 selection

### note : the same core tblid (TSFLAB01a) will be used for all, inside the core function build_result_parcat3 the proper abbreviation will be added

### titles will not be retrieved for these, as the table identifiers are not in the DPS system
### study teams will have to ensure all versions that are needed are included in DPS system
result1 <- build_result_parcat3(
  PARCAT3sel = "Liver biochemistry",
  tblid = tblid,
  save2rtf = TRUE
)
result2 <- build_result_parcat3(
  PARCAT3sel = "Kidney function",
  tblid = tblid,
  save2rtf = TRUE
)
result3 <- build_result_parcat3(
  PARCAT3sel = "Lipids",
  tblid = tblid,
  save2rtf = TRUE
)

result4 <- build_result_parcat3(
  PARCAT3sel = c(
    "Complete blood count",
    "WBC differential",
    "Coagulation studies"
  ),
  tblid = tblid,
  save2rtf = TRUE
)

### if a certain category is not present, no rtf will be generated

result <- build_result_parcat3(PARCAT3sel = "General chemistry", tblid = tblid)
```
```{r result1, echo=FALSE, message=FALSE, warning=FALSE, test = list(result_v1 = "result")}
tt_to_flextable_j(result, tblid, string_map = string_map) 
```

[Download RTF file](`r paste0(tolower(tblid), '.rtf')`)
::::

Made with ❤️ by the J&J Team

  • Edit this page
  • Report an issue
Cookie Preferences