--- title: "Table Properties" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Table Properties} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) library(tplyr2) library(knitr) ``` ## Introduction In tplyr2, a table is defined by its **specification**. The `tplyr_spec()` function captures the full configuration -- column variables, filters, treatment groups, population data, and layers -- as a pure description of what you want. No data processing happens until you call `tplyr_build()`. This vignette covers the spec-level parameters that control the overall structure of your table. Every tplyr2 workflow follows two steps: **define** a spec with `tplyr_spec()`, then **build** the table with `tplyr_build(spec, data)`. Let's look at an example using the included `tplyr_adsl` dataset. ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count(target_var = "SEX") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` Note how the `cols` parameter defines the column structure of the output. Each unique value of `TRT01P` becomes a result column, and the column labels automatically include the group count as `(N=xx)`. ## Column Variables The `cols` parameter accepts a character vector of one or more variable names that define the columns of your output table. The most common case is a single treatment variable: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count(target_var = "AGEGR1") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` ### Multiple Column Variables When you provide multiple variables, tplyr2 creates a cross of all combinations. This is useful when you need columns split by treatment and another variable. ```{r} spec <- tplyr_spec( cols = c("TRT01P", "SEX"), layers = tplyr_layers( group_count(target_var = "AGEGR1") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` Notice that the column labels use a `" | "` separator to show the cross of treatment and sex, and each combination gets its own N. ## Table-Level Filtering with `where` The `where` parameter applies a filter to all data before any layer processing begins. This is useful when records should be excluded from the entire table. ```{r} spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers( group_count(target_var = "AGEGR1", by = "Age Group"), group_desc( target_var = "AGE", by = "Age (Years)", settings = layer_settings( format_strings = list( "n" = f_str("xxx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd"), "Median" = f_str("xx.x", "median"), "Min, Max" = f_str("xx, xx", "min", "max") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", "rowlabel2", grep("^res", names(result), value = TRUE))]) ``` Both the count and descriptive statistics layers are computed on the safety population. Individual layers can also have their own `where` filters, which are applied in addition to the table-level filter. ## Treatment Groups Clinical tables often need columns beyond the individual treatment arms. tplyr2 provides **total groups** and **custom groups** for this purpose. ### Total Groups A total group creates a synthetic column that includes all subjects by duplicating every row with the column variable set to the total group label. ```{r} spec <- tplyr_spec( cols = "TRT01P", total_groups = list( total_group("TRT01P", label = "Total") ), layers = tplyr_layers( group_count(target_var = "SEX") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` The "Total" column now appears alongside the individual treatment arms, with its N reflecting the sum of all subjects. ### Custom Groups Custom groups combine specific treatment levels into a new group. For example, you might pool the two active dose groups together. ```{r} spec <- tplyr_spec( cols = "TRT01P", custom_groups = list( custom_group( "TRT01P", "Xanomeline" = c("Xanomeline High Dose", "Xanomeline Low Dose") ) ), layers = tplyr_layers( group_count(target_var = "SEX") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` The "Xanomeline" column includes all subjects from both dose groups, while the original dose-level columns are preserved. ### Combining Total and Custom Groups You can use both together. Each synthetic column is built by duplicating the rows it needs: a custom group copies the rows of the levels it pools, and a total group copies the original rows. The total group deliberately ignores the custom group's copies, so every subject is counted exactly once in the "Total" column -- its N matches the study population, not the population plus the pooled arms. ```{r} spec <- tplyr_spec( cols = "TRT01P", custom_groups = list( custom_group( "TRT01P", "Xanomeline" = c("Xanomeline High Dose", "Xanomeline Low Dose") ) ), total_groups = list( total_group("TRT01P", label = "Total") ), layers = tplyr_layers( group_count(target_var = "SEX") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` ## Population Data In many clinical analyses, denominators and header Ns should come from a different dataset than the analysis data. The classic example is an adverse event table: `ADAE` only contains subjects who experienced events, but percentages should reflect the full safety population from `ADSL`. The `pop_data()` configuration specifies how the population dataset maps to the spec. The actual data is provided at build time. ```{r} spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01A")), layers = tplyr_layers( group_count( target_var = "AEBODSYS", settings = layer_settings( distinct_by = "USUBJID" ) ) ) ) result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) kable(head(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))], 8)) ``` A few things to note: - `cols = "TRTA"` matches the treatment variable in `ADAE`. - `pop_data(cols = c("TRTA" = "TRT01A"))` maps `TRT01A` in the population data to `TRTA` in the analysis data (format: `c("analysis_name" = "pop_name")`). - `distinct_by = "USUBJID"` counts each subject once per body system. - Denominators and column Ns come from the full `tplyr_adsl` population. ### Extracting Header N After building a table with population data, you can extract the header N values using `tplyr_header_n()`: ```{r} header_n <- tplyr_header_n(result) kable(header_n) ``` This is useful when you need to programmatically construct column headers or integrate with other reporting tools. ### Population Data with Filters The population data is **not** subject to the spec-level `where` filter. It uses its own `where` clause, specified in the `pop_data()` call: ```{r} spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data( cols = c("TRTA" = "TRT01A"), where = SAFFL == "Y" ), layers = tplyr_layers( group_count( target_var = "AEBODSYS", settings = layer_settings( distinct_by = "USUBJID" ) ) ) ) result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) kable(tplyr_header_n(result)) ``` This separation is intentional. The table-level `where` controls which records are summarized, while `pop_data` `where` controls which subjects contribute to denominators. In practice these often differ -- you might filter AE records to treatment-emergent events while basing denominators on the full safety population. ## Data Completion When building count layers, tplyr2 automatically completes all combinations of factor levels and cross-variables. If a treatment group has zero subjects with a given characteristic, a `0 (0.0%)` row still appears rather than being dropped. ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count(target_var = "RACE") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, c("rowlabel1", grep("^res", names(result), value = TRUE))]) ``` Every race category appears for every treatment group, even when the count is zero. ### Limiting Completion with `limit_data_by` Sometimes completing all combinations is too aggressive. The `limit_data_by` parameter in `layer_settings()` restricts the completion grid to combinations that actually exist in the data. This is essential for AE tables where preferred terms should only appear under their actual body system: ```{r} spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01A")), layers = tplyr_layers( group_count( target_var = "AEDECOD", by = "AEBODSYS", settings = layer_settings( distinct_by = "USUBJID", limit_data_by = c("AEBODSYS", "AEDECOD") ) ) ) ) result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) kable(head(result[, c("rowlabel1", "rowlabel2", grep("^res", names(result), value = TRUE))], 10)) ``` With `limit_data_by = c("AEBODSYS", "AEDECOD")`, tplyr2 only creates rows for body system/preferred term combinations that exist in the data, while still filling in zeros for treatment groups with no events for a given combination. ## Where to Go From Here This vignette covered the table-level properties that control the overall structure of your tplyr2 output. For details on specific layer types and additional features, see: - `vignette("count")` -- `group_count()` for frequency tables, including nested counts, distinct subject counts, population data, and missing value handling. - `vignette("denom")` -- denominator control in depth (this vignette and the denominators vignette both cover `pop_data`; that one goes deeper on `denoms_by`, `denom_where`, and confidence intervals). - `vignette("adverse-events")` -- a full adverse event table built end to end, the canonical use of population data. - `vignette("desc")` -- `group_desc()` for summary statistics. - `vignette("shift")` -- `group_shift()` for baseline-by-post-baseline cross-tabulations. - `vignette("sort")` -- how tplyr2 orders rows. - `vignette("options")` -- package-level options via `tplyr2_options()`.