Creation of standard in-text tables with the inTextSummaryTable package
Author: Laure Cougnaud
Date: 2025-09-24

# Creation of standard in-text tables with the `inTextSummaryTable` package

#### Laure Cougnaud

#### September 24, 2025

# 1 Introduction

        library(clinUtils)
        library(tools)# toTitleCase
        library(plyr) # for ddply, rbind.fill
        library(inTextSummaryTable)

## 1.1 Data format

The package is demonstrated with a subset of the ADaM datasets from the CDISC Pilot 01 dataset, available in the `clinUtils` package.

        # load example data
        library(clinUtils)

        # load example data
        data(dataADaMCDISCP01)

        dataAll <- dataADaMCDISCP01
        labelVars <- attr(dataAll, "labelVars")

Typical in-text table for the CSR are included in the following sections.

Please note that the table content e.g. variables, statistics of interest depends strongly on the study at hand and personal preferences.

# 2 Subject information

## 2.1 Subject disposition

        # data of interest
        dataDM <- dataAll$ADSL

        varDMFL <- grep("FL$", colnames(dataDM), value = TRUE)
        varDMFLLabel <- sub(" Flag$", "", labelVars[varDMFL])

        getSummaryStatisticsTable(
            data = dataDM,
            var = varDMFL, varFlag = varDMFL, varGeneralLab = "Analysis Set, N",
            varLab = varDMFLLabel,
            stats = getStats("n (%)"),
            colVar = "TRT01P",
            labelVars = labelVars,
            colTotalInclude = TRUE, colTotalLab = "All subjects",
            varInclude0 = TRUE,
            title = toTitleCase("Table: subject disposition"),
            file = file.path("tables_CSR", "Table_subjectDisposition.docx")
        )

Table: Subject Disposition
Analysis Set, N | Placebo (N=2) | Xanomeline High Dose (N=3) | Xanomeline Low Dose (N=2) | All subjects (N=7)
--- | --- | --- | --- | ---
Safety Population | 2 (100) | 3 (100) | 2 (100) | 7 (100)
Intent-to-Treat Population | 2 (100) | 3 (100) | 2 (100) | 7 (100)
Efficacy Population | 1 (50.0) | 3 (100) | 2 (100) | 6 (85.7)
Completers of Week 8 Population | 1 (50.0) | 3 (100) | 2 (100) | 6 (85.7)
Completers of Week 16 Population | 1 (50.0) | 1 (33.3) | 1 (50.0) | 3 (42.9)
Completers of Week 24 Population | 1 (50.0) | 1 (33.3) | 1 (50.0) | 3 (42.9)
Did the Subject Discontinue the Study? | 2 (100) | 2 (66.7) | 1 (50.0) | 5 (71.4)
Discontinued due to AE? | 0 | 1 (33.3) | 0 | 1 (14.3)
Subject Died? | 2 (100) | 0 | 1 (50.0) | 3 (42.9)

## 2.2 Demographics

        # data of interest
        dataDM <- subset(dataAll$ADSL, SAFFL == "Y")

        # variables of interest
        # Note: if available: ethnicity is included
        varsDM <- c(
            "SEX", "AGE", "AGEGR1",
            "RACE", "ETHNIC",
            "HEIGHTBL", "WEIGHTBL",
            "BMIBL", "BMIBLGR1"
        )

        # Sort variables according to corresponding numeric variable
        dataDM$AGEGR1 <- with(dataDM, reorder(AGEGR1, AGEGR1N))
        dataDM$RACE <- with(dataDM, reorder(RACE, RACEN))
        dataDM$TRT01P <- with(dataDM, reorder(TRT01P, TRT01PN))

        ## Define set of statistics of interest:
        statsDM <- getStatsData(
            data = dataDM, var = varsDM,
            # different for continuous and categorical variable
            type = c(cont = "median (range)", cat = "n (%)"),
            # for categorical variable, statistic name (here: 'n (%)')
            # should not be included in the table
            args = list(cat = list(includeName = FALSE))
        )

        ## create the table:

        getSummaryStatisticsTable(
            data = dataDM,
            # variables to summarize
            var = varsDM,
            varGeneralLab = "Parameter",
            # column
            colVar = "TRT01P", colTotalInclude = TRUE, colTotalLab = "All subjects",
            # statistics
            stats = statsDM,
            statsGeneralLab = "",
            labelVars = labelVars,
            # if only one category, should be included in separated row (e.g. RACE: White)
            rowAutoMerge = FALSE,
            rowInclude0 = FALSE, emptyValue = 0,
            title = toTitleCase("Table: Demographic Data (safety Analysis Set)"),
            file = file.path("tables_CSR", "Table_demographicData.docx")
        )

Table: Demographic Data (Safety Analysis Set)
Parameter | Placebo (N=2) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3) | All subjects (N=7)
--- | --- | --- | --- | ---
Variable group
Sex |  |  |  |
F | 1 (50.0) | 2 (100) | 2 (66.7) | 5 (71.4)
M | 1 (50.0) | 0 | 1 (33.3) | 2 (28.6)
Age |  |  |  |
Median (range) | 82.0 (75,89) | 78.0 (76,80) | 69.0 (57,74) | 75.0 (57,89)
Pooled Age Group 1 |  |  |  |
<65 | 0 | 0 | 1 (33.3) | 1 (14.3)
65-80 | 1 (50.0) | 2 (100) | 2 (66.7) | 5 (71.4)
>80 | 1 (50.0) | 0 | 0 | 1 (14.3)
Race |  |  |  |
WHITE | 2 (100) | 2 (100) | 2 (66.7) | 6 (85.7)
BLACK OR AFRICAN AMERICAN | 0 | 0 | 1 (33.3) | 1 (14.3)
Ethnicity |  |  |  |
NOT HISPANIC OR LATINO | 2 (100) | 2 (100) | 3 (100) | 7 (100)
Baseline Height (cm) |  |  |  |
Median (range) | 167.65 (157.5,177.8) | 155.55 (151.1,160.0) | 158.80 (154.9,175.3) | 158.80 (151.1,177.8)
Baseline Weight (kg) |  |  |  |
Median (range) | 59.65 (47.2,72.1) | 54.45 (45.4,63.5) | 66.70 (51.7,87.1) | 63.50 (45.4,87.1)
Baseline BMI (kg/m^2) |  |  |  |
Median (range) | 20.90 (19.0,22.8) | 22.75 (17.7,27.8) | 27.80 (20.5,28.3) | 22.80 (17.7,28.3)
Pooled Baseline BMI Group 1 |  |  |  |
25-<30 | 0 | 1 (50.0) | 2 (66.7) | 3 (42.9)
<25 | 2 (100) | 1 (50.0) | 1 (33.3) | 4 (57.1)

## 2.3 Baseline disease characteristics

Please note that the content of the table strongly depends on the study.

        # data of interest
        dataBDC <- subset(dataAll$ADSL, SAFFL == "Y")

        # create table
        getSummaryStatisticsTable(
            data = dataBDC,
            var = c("DURDIS", "EDUCLVL"), varGeneralLab = "Parameter",
            colVar = "TRT01P", colTotalInclude = TRUE, colTotalLab = "All subjects",
            stats = getStats("median\n(range)"), statsGeneralLab = "",
            rowAutoMerge = FALSE,
            labelVars = labelVars,
            title = toTitleCase("Table: Baseline Disease Characteristics (safety analysis set)"),
            file = file.path("tables_CSR", "Table_BaselineCharacteristics.docx")
        )

Table: Baseline Disease Characteristics (Safety Analysis Set)
Parameter | Placebo (N=2) | Xanomeline High Dose (N=3) | Xanomeline Low Dose (N=2) | All subjects (N=7)
--- | --- | --- | --- | ---
Duration of Disease (Months) | 20.65 (17.6,23.7) | 31.4 (2.2,32.1) | 35.6 (31.4,39.8) | 31.4 (2.2,39.8)
Years of Education | 13 (12,14) | 15 (10,16) | 12 (12,12) | 12 (10,16)

## 2.4 Medical History and Concomitant Diseases

        dataCM <- subset(dataAll$ADCM, SAFFL == "Y")

        # sort variable according to corresponding numeric variables
        dataCM$TRTA <- with(dataCM, reorder(TRTA, TRTAN))

        # Terms should be in lower-case
        dataCM$CMDECOD <- simpleCap(tolower(dataCM$CMDECOD))
        dataCM$CMCLAS <- simpleCap(tolower(dataCM$CMCLAS))

        getSummaryStatisticsTable(
            data = dataCM,
            colVar = "TRTA", colTotalInclude = TRUE, colTotalLab = "All subjects",
            rowVar = c("CMCLAS", "CMDECOD"),
            # include total across generic terms and across ATC4 classes
            rowVarTotalInclude = c("CMCLAS", "CMDECOD"),
            rowTotalLab = "Any prior and concomitant medication",
            stats = getStats("n (%)"),
            # sort rows based on counts of subjects in the total column
            rowOrder = "total",
            labelVars = labelVars,
            emptyValue = 0,
            title = toTitleCase(paste("Prior and concomitant therapies",
                "by medication class and generic term (safety analyis set)"
            )),
            file = file.path("tables_CSR", "Table_CM.docx")
        )

Prior and Concomitant Therapies by Medication Class and Generic Term (Safety Analyis Set)
Medication Class | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=1) | All subjects (N=3)
--- | --- | --- | ---
Standardized Medication Name
Any prior and concomitant medication | 2 (100) | 1 (100) | 3 (100)
Systemic hormonal preparations, excl. | 2 (100) | 1 (100) | 3 (100)
Hydrocortisone | 2 (100) | 1 (100) | 3 (100)
Uncoded | 2 (100) | 1 (100) | 3 (100)
Uncoded | 2 (100) | 1 (100) | 3 (100)
Respiratory system | 1 (50.0) | 0 | 1 (33.3)
Salbutamol sulfate | 1 (50.0) | 0 | 1 (33.3)

# 3 Efficacy Analyses

The example dataset has has two primary endpoints:

- ADAS-Cog (11), a.k.a Alzheimer’s Disease Assessment Scale - Cognitive Subscale a metric containing 11 items, available in the `ADQSADAS` dataset
- CIBIC+ score a.k.a Video-referenced Clinician’s Interview-based Impression of Change available in the `ADQSCIBC` dataset

        dataAdasCog11 <- subset(dataAll$ADQSADAS, PARAMCD == "ACTOT")
        dataCIBIC <- subset(dataAll$ADQSCIBC, PARAMCD == "CIBICVAL")

        dataEfficacy <- plyr::rbind.fill(dataAdasCog11, dataCIBIC)

        dataEfficacy$TRTP <- with(dataEfficacy, reorder(TRTP, TRTPN))
        dataEfficacy$AVISIT <- with(dataEfficacy, reorder(AVISIT, AVISITN))

        stats <- getStatsData(
            data = dataEfficacy,
            var = c("AVAL", "CHG"),
            type = c("n", "mean (se)", "median (range)")
        )

        getSummaryStatisticsTable(
            data = dataEfficacy,
            rowVar = "PARAM",
            colVar = c("TRTP", "AVISIT"),
            var = c("AVAL", "CHG"),
            stats = stats,
            labelVars = labelVars,
            title = paste("Table: efficacy endpoints",
                toTitleCase("actual value and changes from baseline per time point"
            )),
            file = file.path("tables_CSR", "Table_efficacy.docx")
        )

Table: efficacy endpoints Actual Value and Changes from Baseline per Time Point
Parameter | Placebo | Xanomeline Low Dose | Xanomeline High Dose
--- | --- | --- | ---
Variable
Statistic | Baseline (N=2) | Week 8 (N=2) | Week 16 (N=2) | Week 24 (N=2) | Baseline (N=2) | Week 8 (N=2) | Week 16 (N=2) | Week 24 (N=2) | Baseline (N=3) | Week 8 (N=3) | Week 16 (N=3) | Week 24 (N=3)
Adas-Cog(11) Subscore |  |  |  |  |  |  |  |  |  |  |  |
Analysis Value |  |  |  |  |  |  |  |  |  |  |  |
n | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 3 | 3 | 3 | 3
Mean (SE) | 14.0 (6.00) | 16.5 (3.50) | 17.5 (2.50) | 17.5 (2.50) | 32.0 (23.00) | 37.0 (25.00) | 38.5 (23.50) | 37.0 (25.00) | 15.7 (6.36) | 16.7 (6.36) | 15.3 (5.21) | 17.7 (5.36)
Median (range) | 14.0 (8,20) | 16.5 (13,20) | 17.5 (15,20) | 17.5 (15,20) | 32.0 (9,55) | 37.0 (12,62) | 38.5 (15,62) | 37.0 (12,62) | 15.0 (5,27) | 16.0 (6,28) | 16.0 (6,24) | 22.0 (7,24)
Change from Baseline |  |  |  |  |  |  |  |  |  |  |  |
n | - | 2 | 2 | 2 | - | 2 | 2 | 2 | - | 3 | 3 | 3
Mean (SE) | - | 2.5 (2.50) | 3.5 (3.50) | 3.5 (3.50) | - | 5.0 (2.00) | 6.5 (0.50) | 5.0 (2.00) | - | 1.0 (0.00) | -0.3 (1.33) | 2.0 (2.89)
Median (range) | - | 2.5 (0,5) | 3.5 (0,7) | 3.5 (0,7) | - | 5.0 (3,7) | 6.5 (6,7) | 5.0 (3,7) | - | 1.0 (1,1) | 1.0 (-3,1) | 2.0 (-3,7)
CIBIC Score |  |  |  |  |  |  |  |  |  |  |  |
Analysis Value |  |  |  |  |  |  |  |  |  |  |  |
n | - | 1 | 1 | 1 | - | 2 | 2 | 2 | - | 2 | 3 | 3
Mean (SE) | - | 6.0 ( NA) | 5.0 ( NA) | 5.0 ( NA) | - | 5.0 (0.00) | 4.5 (0.50) | 4.5 (0.50) | - | 4.5 (0.50) | 4.0 (0.00) | 4.3 (0.33)
Median (range) | - | 6.0 (6,6) | 5.0 (5,5) | 5.0 (5,5) | - | 5.0 (5,5) | 4.5 (4,5) | 4.5 (4,5) | - | 4.5 (4,5) | 4.0 (4,4) | 4.0 (4,5)

# 4 Safety Analyses

## 4.1 Adverse Events

### 4.1.1 Treatment-emergent summary table

        ## data of interest: safety analysis set and treatment-emergent
        dataTEAE <- subset(dataAll$ADAE, SAFFL == "Y" & TRTEMFL == "Y")

        # order treatment and severity categories
        dataTEAE$TRTA <- with(dataTEAE, reorder(TRTA, TRTAN))

        ## data considered for the total
        dataTotalAE <- subset(dataAll$ADSL, SAFFL == "Y")
        dataTotalAE$TRTA <- with(dataTotalAE, reorder(TRT01A, TRT01AN))

        # TEAE with worst intensity
        # build worst-case scenario
        dataTEAE$AESEV <- factor(dataTEAE$AESEV, levels = c("MILD", "MODERATE", "SEVERE"))
        dataTEAE$AESEVN <- as.numeric(dataTEAE$AESEV)
        dataTEAE <- ddply(dataTEAE, c("USUBJID", "TRTA"), function(x)
            cbind.data.frame(x,
                WORSTINT = with(x, ifelse(AESEVN == max(AESEVN), as.character(AESEV), NA_character_))
        ))
        dataTEAE$WORSTINT <- factor(dataTEAE$WORSTINT, levels = levels(dataTEAE$AESEV))

        ## specify labels for each variable:
        varsAE <- c("TRTEMFL", "AESER", "AESDTH", "AEREL")

        # create the table
        getSummaryStatisticsTable(
            data = dataTEAE,
            colVar = "TRTA",
            # define variables to compute statistics on
            var = c("TRTEMFL", "AESER", "WORSTINT", "AESDTH", "AEREL"),
            varFlag = c("TRTEMFL", "AESER", "AESDTH"),
            varLab = c(TRTEMFL = "Treatment-Emergent", WORSTINT = "Worst-case severity:"),
            varGeneralLab = "Subjects with, n(%):",
            # force the inclusion of lines for variable without count:
            varInclude0 = TRUE,
            # include the total for the worst-case scenario
            varTotalInclude = "WORSTINT",
            # statistics:
            stats = getStats('n (%)'),
            emptyValue = "0",
            labelVars = labelVars,
            # dataset used for the total in the header column (and for percentage as default)
            dataTotal = dataTotalAE,
            # title/export
            title = toTitleCase("Table: Summary Table of Treatment-emergent Adverse Events (safety analysis set)"),
            file = file.path("tables_CSR", "Table_TEAE_summary.docx")
        )

Table: Summary Table of Treatment-Emergent Adverse Events (Safety Analysis Set)
Subjects with, n(%): | Placebo (N=2) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3)
--- | --- | --- | ---
Variable group
Treatment-Emergent | 2 (100) | 2 (100) | 3 (100)
Serious Event | 0 | 0 | 1 (33.3)
Worst-case severity: | 2 (100) | 2 (100) | 3 (100)
MILD | 0 | 0 | 0
MODERATE | 0 | 1 (50.0) | 1 (33.3)
SEVERE | 2 (100) | 1 (50.0) | 2 (66.7)
Results in Death | 2 (100) | 1 (50.0) | 0
Causality |  |  |
NONE | 1 (50.0) | 2 (100) | 3 (100)
POSSIBLE | 1 (50.0) | 1 (50.0) | 2 (66.7)
PROBABLE | 0 | 1 (50.0) | 3 (100)
REMOTE | 0 | 0 | 1 (33.3)

### 4.1.2 Treatment-emergent incidence table

#### 4.1.2.1 Events occuring in at least one subject

        dataTEAE <- subset(dataAll$ADAE, SAFFL == "Y" & TRTEMFL == "Y")

        # order treatment and severity categories
        dataTEAE$TRTA <- with(dataTEAE, reorder(TRTA, TRTAN))

        ## data considered for the total
        dataTotalAE <- subset(dataAll$ADSL, SAFFL == "Y")
        dataTotalAE$TRTA <- with(dataTotalAE, reorder(TRT01A, TRT01AN))

        getSummaryStatisticsTable(
            data = dataTEAE,
            rowVar = c("AESOC", "AEDECOD"),
            colVar = "TRTA",
            ## total
            # data
            dataTotal = dataTotalAE,
            # row total
            rowVarTotalInclude = c("AESOC", "AEDECOD"), rowTotalLab = "Any TEAE",
            stats = getStats("n (%)"),
            labelVars = labelVars,
            rowVarLab = c('AESOC' = "TEAE by SOC and Preferred Term,\nn (%)"),
            # sort rows based on the total column:
            rowOrder = "total",
            rowOrderTotalFilterFct = function(x) subset(x, TRTA == "Total"),
            title = paste("Table: Treatment-emergent Adverse Events by System Organ Class",
                "and Preferred Term (Safety Analysis Set)"
            ),
            file = file.path("tables_CSR", "Table_TEAE_SOCPT_atLeast1Subject.docx")
        )

Table: Treatment-emergent Adverse Events by System Organ Class and Preferred Term (Safety Analysis Set)
TEAE by SOC and Preferred Term, n (%) | Placebo (N=2) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3)
--- | --- | --- | ---
Dictionary-Derived Term
Any TEAE | 2 (100) | 2 (100) | 3 (100)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS | 0 | 2 (100) | 3 (100)
APPLICATION SITE PRURITUS | 0 | 2 (100) | 2 (66.7)
APPLICATION SITE ERYTHEMA | 0 | 2 (100) | 1 (33.3)
APPLICATION SITE IRRITATION | 0 | 1 (50.0) | 1 (33.3)
APPLICATION SITE DERMATITIS | 0 | 0 | 1 (33.3)
FATIGUE | 0 | 0 | 1 (33.3)
SECRETION DISCHARGE | 0 | 1 (50.0) | 0
SUDDEN DEATH | 0 | 1 (50.0) | 0
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS | 0 | 2 (100) | 2 (66.7)
BACK PAIN | 0 | 0 | 1 (33.3)
FLANK PAIN | 0 | 0 | 1 (33.3)
MUSCULAR WEAKNESS | 0 | 1 (50.0) | 0
SHOULDER PAIN | 0 | 1 (50.0) | 0
PSYCHIATRIC DISORDERS | 1 (50.0) | 1 (50.0) | 1 (33.3)
COMPLETED SUICIDE | 1 (50.0) | 0 | 0
CONFUSIONAL STATE | 0 | 1 (50.0) | 0
HALLUCINATION, VISUAL | 0 | 0 | 1 (33.3)
GASTROINTESTINAL DISORDERS | 0 | 0 | 2 (66.7)
NAUSEA | 0 | 0 | 2 (66.7)
INFECTIONS AND INFESTATIONS | 0 | 1 (50.0) | 1 (33.3)
LOWER RESPIRATORY TRACT INFECTION | 0 | 0 | 1 (33.3)
PNEUMONIA | 0 | 1 (50.0) | 0
NERVOUS SYSTEM DISORDERS | 0 | 0 | 2 (66.7)
AMNESIA | 0 | 0 | 1 (33.3)
LETHARGY | 0 | 0 | 1 (33.3)
PARTIAL SEIZURES WITH SECONDARY GENERALISATION | 0 | 0 | 1 (33.3)
RENAL AND URINARY DISORDERS | 0 | 1 (50.0) | 1 (33.3)
CALCULUS URETHRAL | 0 | 0 | 1 (33.3)
INCONTINENCE | 0 | 1 (50.0) | 0
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS | 0 | 1 (50.0) | 1 (33.3)
DYSPNOEA | 0 | 1 (50.0) | 0
EPISTAXIS | 0 | 0 | 1 (33.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS | 0 | 1 (50.0) | 1 (33.3)
ACTINIC KERATOSIS | 0 | 0 | 1 (33.3)
ERYTHEMA | 0 | 1 (50.0) | 0
CARDIAC DISORDERS | 1 (50.0) | 0 | 0
MYOCARDIAL INFARCTION | 1 (50.0) | 0 | 0
INJURY, POISONING AND PROCEDURAL COMPLICATIONS | 0 | 1 (50.0) | 0
JOINT DISLOCATION | 0 | 1 (50.0) | 0
SKIN LACERATION | 0 | 1 (50.0) | 0
INVESTIGATIONS | 0 | 1 (50.0) | 0
NASAL MUCOSA BIOPSY | 0 | 1 (50.0) | 0
METABOLISM AND NUTRITION DISORDERS | 0 | 0 | 1 (33.3)
DECREASED APPETITE | 0 | 0 | 1 (33.3)

#### 4.1.2.2 Events occuring in at least 25% of all subjects

        getSummaryStatisticsTable(
            data = dataTEAE,
            rowVar = c("AESOC", "AEDECOD"),
            colVar = "TRTA",
            ## total
            # data
            dataTotal = dataTotalAE,
            # row total
            rowVarTotalInclude = c("AESOC", "AEDECOD"), rowTotalLab = "Any TEAE",
            stats = getStats("n (%)"),
            labelVars = labelVars,
            rowVarLab = c('AESOC' = "SOC and Preferred Term,\nn (%)"),
            # sort rows based on the total column:
            rowOrder = "total",
            rowOrderTotalFilterFct = function(x) subset(x, TRTA == "Total"),
            title = paste("Table: Treatment-emergent Adverse Events by System Organ Class",
                "and Preferred Term reported in at least 25% of the subjects",
                "in any treatment group (Safety Analysis Set)"
            ),
            file = file.path("tables_CSR", "Table_TEAE_SOCPT_atLeast25PercentsSubject.docx"),
            # include only events occuring in at least 25% for at least one preferred term:
            filterFct = function(x)
                ddply(x, "AESOC", function(x){ # per AESOC to include the total
                    ddply(x, "AEDECOD", function(y){
                        yTotal <- subset(y, grepl("Total", TRTA))
                        if(any(yTotal$statPercN >= 25)) y
                    })
                })
        )

Table: Treatment-emergent Adverse Events by System Organ Class and Preferred Term reported in at least 25% of the subjects in any treatment group (Safety Analysis Set)
SOC and Preferred Term, n (%) | Placebo (N=2) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3)
--- | --- | --- | ---
Dictionary-Derived Term
Any TEAE | 2 (100) | 2 (100) | 3 (100)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS | 0 | 2 (100) | 3 (100)
APPLICATION SITE PRURITUS | 0 | 2 (100) | 2 (66.7)
APPLICATION SITE ERYTHEMA | 0 | 2 (100) | 1 (33.3)
APPLICATION SITE IRRITATION | 0 | 1 (50.0) | 1 (33.3)
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS | 0 | 2 (100) | 2 (66.7)
PSYCHIATRIC DISORDERS | 1 (50.0) | 1 (50.0) | 1 (33.3)
GASTROINTESTINAL DISORDERS | 0 | 0 | 2 (66.7)
NAUSEA | 0 | 0 | 2 (66.7)
INFECTIONS AND INFESTATIONS | 0 | 1 (50.0) | 1 (33.3)
NERVOUS SYSTEM DISORDERS | 0 | 0 | 2 (66.7)
RENAL AND URINARY DISORDERS | 0 | 1 (50.0) | 1 (33.3)
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS | 0 | 1 (50.0) | 1 (33.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS | 0 | 1 (50.0) | 1 (33.3)

### 4.1.3 Treatment-emergent worst-case table

        dataTEAE <- subset(dataAll$ADAE, SAFFL == "Y" & TRTEMFL == "Y")

        # order treatment and severity categories
        dataTEAE$TRTA <- with(dataTEAE, reorder(TRTA, TRTAN))

        ## data considered for the total
        dataTotalAE <- subset(dataAll$ADSL, SAFFL == "Y")
        dataTotalAE$TRTA <- with(dataTotalAE, reorder(TRT01A, TRT01AN))

        # TEAE with worst intensity
        dataTEAE$AESEV <- factor(dataTEAE$AESEV, levels = c("MILD", "MODERATE", "SEVERE"))
        dataTEAE$AESEVN <- as.numeric(dataTEAE$AESEV)

        # extract worst-case scenario data (only one record if multiple with same severity)
        dataAEWC <- ddply(dataTEAE, c("AESOC", "AEDECOD", "USUBJID", "TRTA"), function(x){
            x[which.max(x$AESEVN), ]
        })
        # worst-case scenario in lower case
        dataAEWC$WORSTINT <- simpleCap(tolower(dataAEWC$AESEV))
        labelVars["WORSTINT"] <- "Worst-case scenario"

        ## datasets used for the total:
        # for total: compute worst-case across SOC and across AE term
        # (otherwise patient counted in multiple categories if present different categories for different AEs)
        dataTotalRow <- list(
            # within SOC (across AEDECOD)
            'AEDECOD' = ddply(dataAEWC, c("AESOC", "USUBJID", "TRTA"), function(x){
                x[which.max(x$AESEVN), ]
            }),
            # across SOC
            'AESOC' = ddply(dataAEWC, c("USUBJID", "TRTA"), function(x){
                x[which.max(x$AESEVN), ]
            })
        )

        getSummaryStatisticsTable(
            data = dataAEWC,
            ## row variables:
            rowVar = c("AESOC", "AEDECOD", "WORSTINT"), rowVarInSepCol = "WORSTINT",
            # include total across SOC and across AEDECOD
            rowVarTotalInclude = c("AESOC", "AEDECOD"), dataTotalRow = dataTotalRow,
            rowVarTotalByVar = "WORSTINT", # count for each severity category for the total
            rowTotalLab = "Any TEAE", rowVarLab = c(AESOC = "Subjects with, n(%):", WORSTINT = "Worst-case scenario"),
            # sort per total in the total column
            rowOrder = "total",
            ## column variables
            colVar = "TRTA",
            stats = getStats("n (%)"),
            emptyValue = "0",
            labelVars = labelVars,
            dataTotal = dataTotalAE,
            title = toTitleCase(paste("Table: Treatment-emergent Adverse",
                "Events by system organ",
                "and preferred term by worst-case (safety Analysis Set)"
            )),
            file = file.path("tables_CSR", "Table_TEAE_Severity.docx")
        )

Table: Treatment-Emergent Adverse Events by System Organ and Preferred Term by Worst-Case (Safety Analysis Set)
Subjects with, n(%): | Worst-case scenario | Placebo (N=2) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3)
--- | --- | --- | --- | ---
Dictionary-Derived Term
Any TEAE | Severe | 2 (100) | 1 (50.0) | 2 (66.7)
Moderate | 0 | 1 (50.0) | 1 (33.3)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS | Severe | 0 | 1 (50.0) | 0
Moderate | 0 | 0 | 2 (66.7)
Mild | 0 | 1 (50.0) | 1 (33.3)
APPLICATION SITE PRURITUS | Moderate | 0 | 0 | 1 (33.3)
Mild | 0 | 2 (100) | 1 (33.3)
APPLICATION SITE ERYTHEMA | Mild | 0 | 2 (100) | 1 (33.3)
APPLICATION SITE IRRITATION | Moderate | 0 | 0 | 1 (33.3)
Mild | 0 | 1 (50.0) | 0
APPLICATION SITE DERMATITIS | Moderate | 0 | 0 | 1 (33.3)
FATIGUE | Mild | 0 | 0 | 1 (33.3)
SECRETION DISCHARGE | Mild | 0 | 1 (50.0) | 0
SUDDEN DEATH | Severe | 0 | 1 (50.0) | 0
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS | Moderate | 0 | 1 (50.0) | 1 (33.3)
Mild | 0 | 1 (50.0) | 1 (33.3)
BACK PAIN | Mild | 0 | 0 | 1 (33.3)
FLANK PAIN | Moderate | 0 | 0 | 1 (33.3)
MUSCULAR WEAKNESS | Moderate | 0 | 1 (50.0) | 0
SHOULDER PAIN | Mild | 0 | 1 (50.0) | 0
PSYCHIATRIC DISORDERS | Severe | 1 (50.0) | 0 | 0
Moderate | 0 | 1 (50.0) | 1 (33.3)
COMPLETED SUICIDE | Severe | 1 (50.0) | 0 | 0
CONFUSIONAL STATE | Moderate | 0 | 1 (50.0) | 0
HALLUCINATION, VISUAL | Moderate | 0 | 0 | 1 (33.3)
NERVOUS SYSTEM DISORDERS | Severe | 0 | 0 | 1 (33.3)
Moderate | 0 | 0 | 1 (33.3)
AMNESIA | Mild | 0 | 0 | 1 (33.3)
LETHARGY | Moderate | 0 | 0 | 1 (33.3)
PARTIAL SEIZURES WITH SECONDARY GENERALISATION | Severe | 0 | 0 | 1 (33.3)
GASTROINTESTINAL DISORDERS | Severe | 0 | 0 | 1 (33.3)
Mild | 0 | 0 | 1 (33.3)
NAUSEA | Severe | 0 | 0 | 1 (33.3)
Mild | 0 | 0 | 1 (33.3)
INFECTIONS AND INFESTATIONS | Moderate | 0 | 1 (50.0) | 1 (33.3)
LOWER RESPIRATORY TRACT INFECTION | Moderate | 0 | 0 | 1 (33.3)
PNEUMONIA | Moderate | 0 | 1 (50.0) | 0
RENAL AND URINARY DISORDERS | Moderate | 0 | 0 | 1 (33.3)
Mild | 0 | 1 (50.0) | 0
CALCULUS URETHRAL | Moderate | 0 | 0 | 1 (33.3)
INCONTINENCE | Mild | 0 | 1 (50.0) | 0
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS | Moderate | 0 | 1 (50.0) | 0
Mild | 0 | 0 | 1 (33.3)
DYSPNOEA | Moderate | 0 | 1 (50.0) | 0
EPISTAXIS | Mild | 0 | 0 | 1 (33.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS | Mild | 0 | 1 (50.0) | 1 (33.3)
ACTINIC KERATOSIS | Mild | 0 | 0 | 1 (33.3)
ERYTHEMA | Mild | 0 | 1 (50.0) | 0
INJURY, POISONING AND PROCEDURAL COMPLICATIONS | Moderate | 0 | 1 (50.0) | 0
JOINT DISLOCATION | Moderate | 0 | 1 (50.0) | 0
SKIN LACERATION | Mild | 0 | 1 (50.0) | 0
CARDIAC DISORDERS | Severe | 1 (50.0) | 0 | 0
MYOCARDIAL INFARCTION | Severe | 1 (50.0) | 0 | 0
INVESTIGATIONS | Mild | 0 | 1 (50.0) | 0
NASAL MUCOSA BIOPSY | Mild | 0 | 1 (50.0) | 0
METABOLISM AND NUTRITION DISORDERS | Moderate | 0 | 0 | 1 (33.3)
DECREASED APPETITE | Moderate | 0 | 0 | 1 (33.3)

## 4.2 Laboratory safety

### 4.2.1 Table of laboratory abnormalities

        dataLBAbn <- subset(dataAll$ADLBC, SAFFL == "Y" & LBNRIND != "NORMAL")

        dataLBAbn$PARAM <- with(dataLBAbn, reorder(PARAM, PARAMN))
        dataLBAbn$TRTA <- with(dataLBAbn, reorder(TRTA, TRTAN))
        dataLBAbn$LBNRIND <- factor(dataLBAbn$LBNRIND, levels = c("LOW", "HIGH"))

        dataLBAbnTotal <- subset(dataAll$ADSL, SAFFL == "Y")
        dataLBAbnTotal$TRTA <- with(dataLBAbnTotal, reorder(TRT01A, TRT01AN))

        getSummaryStatisticsTable(
            data = dataLBAbn,
            rowVar = c("PARCAT1", "PARAM"),
            rowVarTotalInclude = c("PARCAT1", "PARAM"),
            colVar = "TRTA",
            var = "LBNRIND",
            rowVarInSepCol = "variableGroup", varSubgroupLab = "Abnormality",
            rowVarLab = c('PARCAT1' = "Laboratory Parameter\nn (%)"),
            stats = getStats("n (%)"),
            labelVars = labelVars,
            rowOrder = c("PARCAT1" = "total", "PARAM" = "total", "variableGroup" = "auto"),
            dataTotal = dataLBAbnTotal,
            title = toTitleCase(paste("Table: Treatment-emergent",
                "Worst-case Laboratory Abnormalities (safety analysis set)"
            )),
            emptyValue = "0",
            file = file.path("tables_CSR", "Table_Lab_Severity.docx")
        )

Table: Treatment-Emergent Worst-Case Laboratory Abnormalities (Safety Analysis Set)
Laboratory Parameter n (%) | Abnormality | Placebo (N=2) | Xanomeline Low Dose (N=2)
--- | --- | --- | ---
Parameter
Any Laboratory Parameter n (%), Parameter | LOW | 0 | 2 (100)
HIGH | 2 (100) | 2 (100)
CHEM | LOW | 0 | 2 (100)
HIGH | 2 (100) | 2 (100)
Alkaline Phosphatase (U/L) | HIGH | 0 | 2 (100)
Albumin (g/L) | LOW | 0 | 2 (100)
Alkaline Phosphatase (U/L) change from previous visit, relative to normal range | HIGH | 0 | 2 (100)
Albumin (g/L) change from previous visit, relative to normal range | LOW | 0 | 2 (100)
Sodium (mmol/L) | LOW | 0 | 1 (50.0)
Bilirubin (umol/L) | HIGH | 0 | 1 (50.0)
Gamma Glutamyl Transferase (U/L) | HIGH | 0 | 1 (50.0)
Alanine Aminotransferase (U/L) | HIGH | 1 (50.0) | 0
Aspartate Aminotransferase (U/L) | HIGH | 1 (50.0) | 0
Blood Urea Nitrogen (mmol/L) | HIGH | 0 | 1 (50.0)
Cholesterol (mmol/L) | HIGH | 1 (50.0) | 0
Creatine Kinase (U/L) | HIGH | 1 (50.0) | 0
Sodium (mmol/L) change from previous visit, relative to normal range | LOW | 0 | 1 (50.0)
Bilirubin (umol/L) change from previous visit, relative to normal range | HIGH | 0 | 1 (50.0)
Gamma Glutamyl Transferase (U/L) change from previous visit, relative to normal range | HIGH | 0 | 1 (50.0)
Alanine Aminotransferase (U/L) change from previous visit, relative to normal range | HIGH | 1 (50.0) | 0
Aspartate Aminotransferase (U/L) change from previous visit, relative to normal range | HIGH | 1 (50.0) | 0
Blood Urea Nitrogen (mmol/L) change from previous visit, relative to normal range | HIGH | 0 | 1 (50.0)
Cholesterol (mmol/L) change from previous visit, relative to normal range | HIGH | 1 (50.0) | 0
Creatine Kinase (U/L) change from previous visit, relative to normal range | HIGH | 1 (50.0) | 0

## 4.3 Electrocardiogram

Please note that there is no ECG dataset in the CDISC Pilot dataset used for the examples, so this table is not effectively created in the vignette.

Nevertheless, an example code is provided below to create a standard table of summary statistics for the ECG parameters.

        # data of interest
        paramsECG <- c("QT", "QTCF", "QRS", "PR", "RR", "EGHR")

        dataECG <- subset(dataAll$ADEG, SAFFL == "Y" & PARAMCD %in% paramsECG)
        dataECG$TRTA <- with(dataECG, reorder(TRTA, TRTAN))
        dataECG$PARAM <- with(dataECG, reorder(PARAM, PARAMN))

        # consider all non-missing post-baseline records
        dataECGPostBaseline <- subset(dataECG,
            AVISIT %in% c("Screening", "Baseline", "Worst-case post-baseline")
        )

        # worst-case scenario:
        dataECGWC <- subset(dataECG, AVISIT == "Worst-case post-baseline")
        # treatment-emergent
        dataECGWC$TRTEMFL <- with(dataECGWC, ifelse(BASECAT1 != CHGCAT1, "Y", "N"))
        dataECGWCTE <- subset(dataECGWC, TRTEMFL == "Y")
        dataECGWC <- convertVarToFactor(dataECGWC,
            var = c("AVALCAT1", "CHGCAT1"),
            varNum = c("AVALCA1N", "CHGCAT1N")
        )

        # create the table
        getSummaryStatisticsTable(
            data = dataECGWC,
            # layout:
            colVar = "TRTA",
            rowVar = "PARAM", rowVarLab = c('PARAM' = "ECG Parameter"),
            # metrics to compute statistics on
            var = c("AVALCAT1", "CHGCAT1"),
            # in a separated column
            rowVarInSepCol = c("variable", "variableGroup"),
            # labels
            varGeneralLab = "Abnormality",
            varSubgroupLab = "Worst-Case Post-Baseline",
            stats = getStats("n (%)"),
            labelVars = labelVars,
            # total: all post-baseline
            dataTotal = dataECGPostBaseline,
            emptyValue = "0",
            rowVarTotalPerc = "PARAM", # total per parameter
            # ensure that categories are below the type of abnormality
            rowAutoMerge = FALSE,
            # only retain abnormalities:
            filterFct = function(x){
                subset(x, !variableGroup %in% c("<= 450 msec", "<= 30 msec"))
            },
            title = toTitleCase(paste("Table: Treatment-emergent worst-case",
                "ECG abnormalities and change from baseline ECG abnormalities (safety analysis set)"
            )),
            file = file.path("tables_CSR", "Table_ECG.docx")
        )

## 4.4 Vital signs

### 4.4.1 Treatment-emergent vital signs abnormalities

        # analyis set and parameters of interest
        dataVS <- subset(dataAll$ADVS,
            SAFFL == "Y" & ANL01FL == "Y" & VISIT != "BASELINE"
        )

        dataVS$PARAM <- with(dataVS, reorder(PARAM, PARAMN))
        dataVS$ANRIND <- with(dataVS, reorder(PARAM, PARAMN))
        dataVS$TRTA <- with(dataVS, reorder(TRTA, TRTAN))
        dataVS$SHIFT1 <- with(dataVS, factor(ifelse(SHIFT1 == "", NA_character_, SHIFT1)))

        getSummaryStatisticsTable(
            data = dataVS,
            rowVar = "PARAM",
            rowVarInSepCol = "variableGroup",
            rowVarInclude0 = TRUE,
            colVar = "TRTA",
            var = "SHIFT1", varTotalInclude = TRUE,
            emptyValue = 0,
            stats = getStats("n (%)"),
            rowVarTotalPerc = "PARAM",
            labelVars = labelVars,
            title = toTitleCase(paste("Table: Treatment-emergent Worst-case",
                "Vital Sign Abnormalities (Safety Analysis Set)"
            )),
            file = file.path("tables_CSR", "Table_VitalSigns_Severity.docx")
        )

Table: Treatment-Emergent Worst-Case Vital Sign Abnormalities (Safety Analysis Set)
Parameter | Variable group | Placebo (N=1) | Xanomeline Low Dose (N=2) | Xanomeline High Dose (N=3)
--- | --- | --- | --- | ---
Systolic Blood Pressure (mmHg) | Total | 0 | 0 | 0
Diastolic Blood Pressure (mmHg) | Total | 1 (100) | 2 (100) | 3 (100)
High/High | 0 | 0 | 1 (33.3)
High/Low | 0 | 0 | 1 (33.3)
High/Normal | 0 | 0 | 1 (33.3)
Low/Low | 0 | 1 (50.0) | 1 (33.3)
Low/Normal | 0 | 1 (50.0) | 2 (66.7)
Normal/High | 0 | 0 | 1 (33.3)
Normal/Low | 0 | 2 (100) | 1 (33.3)
Normal/Normal | 1 (100) | 2 (100) | 3 (100)
Pulse Rate (BEATS/MIN) | Total | 0 | 0 | 0
Weight (kg) | Total | 0 | 0 | 0
Temperature (C) | Total | 0 | 0 | 0

# 5 Pharmacokinetics analysis

Please note that this example pharmacodynamics dataset contains different subjects than the other datasets used in the vignette.

        paramcdPK <- c("AUCINFO", "CMAX", "TMAX")
        dataPK <- subset(dataAll$ADPP, PKFL == "Y" & PARAMCD %in% paramcdPK)

        dataPK$PARCAT1 <- with(dataPK, reorder(PARCAT1, PARCAT1N))
        dataPK$PARAMCD <- with(dataPK, reorder(PARAMCD, PARAMN))
        dataPK$TRTA <- with(dataPK, reorder(TRTA, TRTAN))
        dataPK$PARAMCD <- with(dataPK, reorder(PARAMCD, PARAMN))

        # build pretty labels
        labelsPK <- c(
            AUCINFO = "AUC_{Inf,obs}\n(h*ng/mL)",
            CMAX = "C_{max}\n(ng/mL)",
            TMAX = "t_{max}\n(h)"
        )
        dataPK$PARAM <- factor(dataPK$PARAMCD,
            levels = levels(dataPK$PARAMCD),
            labels = labelsPK[levels(dataPK$PARAMCD)]
        )

        statsPK <- dlply(dataPK, "PARAM", function(dataParam){
            getStatsData(
                data = dataParam,
                var = "AVAL",
                type = "median\n(range)",
                includeName = FALSE
            )[[1]]
        })

        getSummaryStatisticsTable(
            data = dataPK,
            rowVar = c("PARCAT1", "PARAM"), colVar = "TRTA",
            var = "AVAL",
    #       rowVarLab = c('PARCAT1' = "PK parameters"),
            stats = statsPK, statsVarBy = "PARAM",
            emptyValue = "-",
            title = toTitleCase("Table: Summary of PK parameters (pharmacokinetics analysis set)"),
            file = file.path("tables_CSR", "Table_PK_Parameters.docx"),
            labelVars = labelVars
        )

Table: Summary of PK Parameters (Pharmacokinetics Analysis Set)
Parameter Category 1 | A (N=4) | C (N=3)
--- | --- | ---
Parameter
DRUG ANAL 1 Plasma |  |
AUCInf,obs (h*ng/mL) | 4180.5 (2655,6570) | 4988.6 (3838,5040)
Cmax (ng/mL) | 3665.0 (1860,6440) | 2320.0 (2190,4170)
tmax (h) | 0.5083 (0.500,1.000) | 1.0000 (0.500,1.500)
DRUG ANAL 2 Plasma |  |
AUCInf,obs (h*ng/mL) | 164397.2 (112551,299210) | 164709.3 (127237,251953)
Cmax (ng/mL) | 16900.0 (7920,26400) | 19700.0 (11500,21700)
tmax (h) | 2.0000 (1.500,4.000) | 3.0000 (2.000,4.033)
DRUG ANAL 3 Plasma |  |
AUCInf,obs (h*ng/mL) | 29012.2 (14477,40242) | 51233.8 (9731,61760)
Cmax (ng/mL) | 3915.0 (2880,5990) | 8700.0 (1900,10900)
tmax (h) | 1.5000 (1.500,4.033) | 1.0000 (0.983,3.000)

# 6 Appendix

## 6.1 Session information

R version 4.5.1 (2025-06-13) Platform: x86_64-pc-linux-gnu Running under: Ubuntu 24.04.2 LTS

Matrix products: default BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0

locale: [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C LC_TIME=en_US.UTF-8 LC_COLLATE=C LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
[7] LC_PAPER=en_US.UTF-8 LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C

time zone: Etc/UTC tzcode source: system (glibc)

attached base packages: [1] tools stats graphics grDevices utils datasets methods base

other attached packages: [1] plyr_1.8.9 clinUtils_0.2.0 inTextSummaryTable_3.3.5 knitr_1.50

loaded via a namespace (and not attached): [1] sass_0.4.10 fontLiberation_0.1.0 xml2_1.4.0 stringi_1.8.7 hms_1.1.3 digest_0.6.37
[7] magrittr_2.0.3 evaluate_1.0.5 grid_4.5.1 RColorBrewer_1.1-3 flextable_0.9.10 fastmap_1.2.0
[13] jsonlite_2.0.0 ggrepel_0.9.6 zip_2.3.3 crosstalk_1.2.2 viridisLite_0.4.2 scales_1.4.0
[19] fontBitstreamVera_0.1.1 textshaping_1.0.1 jquerylib_0.1.4 cli_3.6.5 rlang_1.1.6 fontquiver_0.2.1
[25] cowplot_1.2.0 withr_3.0.2 cachem_1.1.0 yaml_2.3.10 gdtools_0.4.3 officer_0.6.10
[31] reshape2_1.4.4 uuid_1.2-1 ggplot2_4.0.0 DT_0.33 forcats_1.0.0 vctrs_0.6.5
[37] R6_2.6.1 lifecycle_1.0.4 stringr_1.5.1 htmlwidgets_1.6.4 ragg_1.4.0 pkgconfig_2.0.3
[43] bslib_0.9.0 pillar_1.11.0 gtable_0.3.6 data.table_1.17.8 glue_1.8.0 Rcpp_1.1.0
[49] systemfonts_1.2.3 haven_2.5.5 xfun_0.53 tibble_3.3.0 farver_2.1.2 htmltools_0.5.8.1
[55] labeling_0.4.3 rmarkdown_2.29 compiler_4.5.1 S7_0.2.0 askpass_1.2.1 openssl_2.3.3