--- title: "Getting Started with tplyr2" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Getting Started with tplyr2} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) options(rmarkdown.html_vignette.check_title = FALSE) library(tplyr2) library(knitr) ``` # How tplyr2 Works If you have worked with clinical data long enough, you know that summary tables -- demographics tables, adverse event tables, lab shift tables -- all share a common structural pattern. Each section of the table represents some kind of summary: a set of counts, a block of descriptive statistics, or a cross-tabulation. tplyr2 is built around this idea. Rather than writing bespoke data manipulation code for every table, you describe _what_ the table should contain using a declarative specification, and tplyr2 handles the computing, formatting, and assembly. The key concepts are: - **Spec**: A `tplyr_spec()` object is pure configuration. It describes the column structure, global filters, and a list of layers. No data processing happens when you create a spec. - **Layers**: Each layer is one summary block. Count layers (`group_count()`) produce frequencies. Descriptive layers (`group_desc()`) compute means, medians, and other summaries. Shift layers (`group_shift()`) create cross-tabulations. Custom layers (`group_analyze()`) accept user-defined functions. - **Build**: `tplyr_build(spec, data)` executes the spec against a dataset and returns a formatted data frame. This separation of configuration from execution is intentional. A spec can be saved to disk, reviewed, reused across studies, or applied to different datasets at build time. If you are familiar with the PHUSE white paper *Analyses & Displays Associated with Demographics, Disposition, and Medications in Phase 2-4 Clinical Trials and Integrated Summary Documents*, the layer model maps naturally to the summary blocks described there. # The tplyr_spec() Object Every table starts with a `tplyr_spec()`. At minimum, you need the column variable (typically the treatment arm) and at least one layer. ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("SEX"), group_desc("AGE") ) ) spec ``` The `cols` parameter determines which variable defines the output columns. Each unique value becomes a result column. The function also accepts optional parameters: - `where`: A global filter expression (e.g., `where = SAFFL == "Y"`). - `pop_data`: A `pop_data()` configuration for population-based denominators. - `total_groups`: A list of `total_group()` objects for "Total" columns. - `custom_groups`: A list of `custom_group()` objects for combined treatment arms. - `layers`: Layer objects wrapped in `tplyr_layers()`. The spec contains no data. Data is supplied at build time, so the same spec can be reused across datasets. # The tplyr_layer() Object Layers are the building blocks of a tplyr2 table. You create them with the `group_*()` family: `group_count()`, `group_desc()`, `group_shift()`, and `group_analyze()`. Every layer constructor accepts: - `target_var`: The variable(s) being summarized. - `by`: Optional row grouping. Strings matching column names become grouping variables; other strings become text labels in the output. Use `label()` for explicit disambiguation. - `where`: An optional layer-level filter expression. - `settings`: A `layer_settings()` object for detailed configuration. Multiple layers are collected with `tplyr_layers()`: ```{r} layers <- tplyr_layers( group_count("SEX", by = "Sex n (%)"), group_count("RACE", by = "Race n (%)"), group_desc("AGE", by = "Age (Years)") ) ``` Layers are processed in order and stacked vertically in the output. # Building a Table Once you have a spec and a dataset, building is a single function call. Let's look at a demographics-style table with both count and descriptive layers: ```{r} spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers( group_count("SEX", by = "Sex n (%)"), group_desc( "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[, !grepl("^ord", names(result))]) ``` The output is a standard data frame. Here is what the columns mean: - **rowlabel1, rowlabel2, ...**: Row label columns. A `by` label adds one column; a `by` data variable adds another; and the target variable adds one more. - **res1, res2, ...**: Result columns, one per unique level of `cols`. Each carries a `label` attribute with the column name and, when population data is available, an `(N=n)` suffix. - **ord_layer_index, ord_layer_1, ...**: Ordering columns for sorting. These can be dropped for display but are useful for programmatic reordering. # String Formatting in tplyr2 One of the most important features of tplyr2 is the format string system. Clinical tables have specific alignment and precision requirements. The `f_str()` function lets you declare exactly how numbers should appear. An `f_str()` has two parts: a template string that defines the layout, and variable names that map into the template's format groups. A format group is a sequence of `x` characters (with optional decimal point) that determines width and precision. ```{r} # Two decimal places for mean, two for SD, with parentheses f_str("xx.xx (xx.xx)", "mean", "sd") # Integer count only f_str("xxx", "n") # Count with percentage f_str("xx (xx.x%)", "n", "pct") ``` The number of `x` characters in the integer part sets the total width (including leading spaces for alignment). Characters after the decimal point set decimal precision. Everything between format groups is literal text. For **descriptive statistics layers**, format strings are a named list and each name becomes a row label. For **count and shift layers**, a single format string goes under the reserved key `n_counts`: ```{r eval=FALSE} # count layer: n (pct%) -- the default list(n_counts = f_str("xx (xx.x%)", "n", "pct")) # count layer: distinct subjects rather than records list(n_counts = f_str("xx (xx.x%)", "distinct_n", "distinct_pct")) ``` Each layer type computes its own set of statistics, and `vignette("format_strings")` carries the complete reference. Two more characters extend the template: `a` lets the *data* determine the decimal width, and uppercase `X` closes the gap between a number and its delimiter -- both covered in `vignette("precision_alignment")`. Because the format and source variables are declared together, the package knows exactly which numbers produced each cell -- this is the foundation of the metadata and traceability system. # Layer Types ## Descriptive Statistics Layers Descriptive statistics layers summarize continuous variables. Without custom format strings, the default produces six rows: n, Mean (SD), Median, Q1/Q3, Min/Max, and Missing. ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, !grepl("^ord", names(result))]) ``` ### Multiple Target Variables When you need to summarize more than one variable in the same layer -- for example, Age and Average Daily Dose -- pass a vector of names to `target_var`. Each variable gets its own block of rows, with the variable name as an additional row label. ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc( c("AGE", "AVGDD"), settings = layer_settings( format_strings = list( "n" = f_str("xxx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd"), "Min, Max" = f_str("xx, xx", "min", "max") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, !grepl("^ord", names(result))]) ``` ## Count Layers Count layers tabulate the frequency of categorical variable levels. By default, they produce counts and percentages formatted as `"xx (xx.x%)"`: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("RACE") ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, !grepl("^ord", names(result))]) ``` ### Total Rows A total row sums the counts across all levels. Enable it with `total_row = TRUE` in `layer_settings()`: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count( "RACE", settings = layer_settings(total_row = TRUE) ) ) ) result <- tplyr_build(spec, tplyr_adsl) kable(result[, !grepl("^ord", names(result))]) ``` ### Distinct Counting In adverse event tables, you often need to count the number of _subjects_ who experienced an event, not the number of event records. Use `distinct_by` to specify the subject identifier. Because `ADAE` contains only subjects with events, the denominator must come from the full population (`ADSL`), supplied through `pop_data()` -- otherwise the percentages are computed against subjects-with-events and come out too large (see `vignette("count")` and `vignette("denom")`): ```{r} spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01A")), layers = tplyr_layers( group_count( "AEBODSYS", settings = layer_settings( distinct_by = "USUBJID", format_strings = list( n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) kable(result[, !grepl("^ord", names(result))]) ``` ### Nested Counts Many clinical tables require hierarchical counts -- for example, adverse events by body system and preferred term. Pass a vector of variable names to `target_var`: ```{r} spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01A")), layers = tplyr_layers( group_count( c("AEBODSYS", "AEDECOD"), settings = layer_settings( distinct_by = "USUBJID", format_strings = list( n_counts = f_str("xxx (xx.x%)", "distinct_n", "distinct_pct") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) kable(head(result[, !grepl("^ord", names(result))], 15)) ``` The outer level (body system) appears in `rowlabel1`, and the inner level (preferred term) appears in `rowlabel2`. Outer rows contain aggregate counts for the body system; inner rows contain per-term counts. Use `collapse_row_labels(result, nest = TRUE)` to merge these into a single indented column (see the Count Layer vignette for details). ## Shift Layers Shift tables display a cross-tabulation of a baseline value against a post-baseline value within each treatment arm. The `group_shift()` function requires a named character vector with `row` and `column` elements: ```{r} set.seed(12345) shift_data <- data.frame( USUBJID = paste0("SUBJ", 1:30), TRTA = rep(c("Placebo", "Active"), each = 15), BNRIND = factor( sample(c("LOW", "NORMAL", "HIGH"), 30, replace = TRUE, prob = c(0.2, 0.6, 0.2)), levels = c("LOW", "NORMAL", "HIGH") ), ANRIND = factor( sample(c("LOW", "NORMAL", "HIGH"), 30, replace = TRUE, prob = c(0.15, 0.5, 0.35)), levels = c("LOW", "NORMAL", "HIGH") ) ) spec <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_shift( c(row = "BNRIND", column = "ANRIND"), settings = layer_settings( format_strings = list(n_counts = f_str("xxx", "n")) ) ) ) ) result <- tplyr_build(spec, shift_data) kable(result[, !grepl("^ord", names(result))]) ``` In the output, `rowlabel1` contains the baseline (row) variable values, and the result columns represent each combination of treatment arm and post-baseline (column) level. Factor ordering is respected, so setting levels appropriately (e.g., `c("LOW", "NORMAL", "HIGH")`) ensures the intended display order. # Numeric Data Formatted cells look good in a final report, but sometimes you need the raw numbers -- for validation, further computation, or archiving. Every tplyr2 build retains unformatted numeric data as an attribute. Use `tplyr_numeric_data()` to retrieve it: ```{r} spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("SEX"), group_desc( "AGE", settings = layer_settings( format_strings = list( "n" = f_str("xxx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd") ) ) ) ) ) result <- tplyr_build(spec, tplyr_adsl) # Raw numeric data for the count layer (layer 1) kable(tplyr_numeric_data(result, layer = 1)) ``` ```{r} # Raw numeric data for the desc layer (layer 2) kable(tplyr_numeric_data(result, layer = 2)) ``` This gives you the exact counts, percentages, means, and every other computed statistic in raw numeric form, keyed by the same grouping variables used during the build. # Where to Go from Here This vignette covers the fundamentals. Here are the topics covered in other vignettes: - `vignette("table")` -- Spec-level structure: column variables, table filters, total/custom groups, and population data. - `vignette("count")` -- Count layers in depth: population-based denominators, distinct counts, nested summaries, stat columns, missing values. - `vignette("desc")` -- Descriptive layers: custom summaries, auto-precision, stats-as-columns. - `vignette("shift")` -- Shift layers, shift denominators, and the denominator row. - `vignette("denom")` -- Denominator control: population data, header N, `denoms_by`, `denom_where`, single-proportion confidence intervals. - `vignette("adverse-events")` -- A full adverse event table built end to end. - `vignette("riskdiff")` and `vignette("binding-statistics")` -- Risk differences and association-test p-value columns. - `vignette("sort")` -- Row ordering by frequency, factor levels, or a VARN companion. - `vignette("format_strings")` -- The `f_str()` format-string system: the grammar, the complete statistic keyword reference per layer type, rounding, and missing-value handling. - `vignette("precision_alignment")` -- Data-driven decimal precision (`a`/`A`) and parenthesis hugging (`X`/`A`). - `vignette("display_conventions")` -- `<1%` and `>99%` percents, zero-count suppression, statistics as columns, missing rows. - `vignette("post_processing")` -- Row masks, row label collapsing, conditional formatting, `as_display()`, and text wrapping. - `vignette("metadata")` -- Cell-level metadata and traceability. - `vignette("serialization")`, `vignette("ard")` -- Saving/loading specs and Analysis Results Data conversion. - `vignette("analyze")` -- Custom analysis layers with user-defined functions. - `vignette("options")` -- Package options (rounding, quantiles, precision). - `vignette("migration")` -- Moving from Tplyr v1 to tplyr2. # References - PHUSE, *Analyses & Displays Associated with Demographics, Disposition, and Medications in Phase 2-4 Clinical Trials and Integrated Summary Documents*. Available from the PHUSE deliverables catalogue. - [CDISC Analysis Data Model (ADaM)](https://www.cdisc.org/standards/foundational/adam)