| Title: | A Grammar of Clinical Summary Tables |
|---|---|
| Description: | Implements a grammar of summary data for clinical reports. Clinical summary tables are decomposed into modular layers, each representing an independent summary block. Supports count, descriptive statistics, and shift layer types with a declarative spec-based API built on data.table for performance. |
| Authors: | Mike Stackhouse [aut, cre] |
| Maintainer: | Mike Stackhouse <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.0 |
| Built: | 2026-10-11 10:02:05 UTC |
| Source: | https://github.com/atorus-research/tplyr2 |
This function allows you to conditionally re-format a string of numbers based on a numeric value within the string itself. By selecting a "format group", which is targeting a specific number within the string, a user can establish a condition upon which a provided replacement string can be used. Either the entire replacement can be used to replace the entire string, or the replacement text can refill the "format group" while preserving the original width and alignment of the target string.
apply_conditional_format( string, format_group, condition, replacement, full_string = FALSE )apply_conditional_format( string, format_group, condition, replacement, full_string = FALSE )
string |
Target character vector where text may be replaced |
format_group |
An integer representing the targeted numeric field within the string, numbered from left to right |
condition |
An expression, using the variable name |
replacement |
A string to use as the replacement value |
full_string |
TRUE if the full string should be replaced, FALSE if the replacement should be done within the format group |
A character vector
string <- c(" 0 (0.0%)", " 8 (9.3%)", "78 (90.7%)") apply_conditional_format(string, 2, x == 0, " 0 ", full_string = TRUE) apply_conditional_format(string, 2, x < 1, "(<1%)")string <- c(" 0 (0.0%)", " 8 (9.3%)", "78 (90.7%)") apply_conditional_format(string, 2, x == 0, " 0 ", full_string = TRUE) apply_conditional_format(string, 2, x < 1, "(<1%)")
Vectorized formatting function. Takes an f_str object and numeric vectors, returns a character vector of formatted strings.
apply_formats( fmt, ..., precision = NULL, lt = NULL, gt = NULL, lt_gt_group = NULL, na = NULL, width = NULL, pad = c("right", "left") )apply_formats( fmt, ..., precision = NULL, lt = NULL, gt = NULL, lt_gt_group = NULL, na = NULL, width = NULL, pad = c("right", "left") )
fmt |
An |
... |
Numeric vectors, one per variable in the f_str (positional matching) |
precision |
Optional list of resolved precision per group (for auto-precision) |
lt |
Optional numeric less-than threshold applied to the group named by
|
gt |
Optional numeric greater-than threshold applied to the group named by
|
lt_gt_group |
Optional integer index of the format group to which |
na |
Optional string substituted for cells whose format-group inputs are
all NA, used instead of the default blank-width fill. |
width |
Optional integer total width to pad each formatted token to,
using |
pad |
Side to pad on when |
Character vector of formatted values
f_str() for the format-string grammar.
# Vectorized: one formatted string per element apply_formats(f_str("xx (xx.x%)", "n", "pct"), n = c(5, 12, 103), pct = c(4.5, 33.333, 99.9)) # `na` replaces the default blank-width fill apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA)) apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA), na = "NE") apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA), na = "") # lt/gt thresholds, targeting the percent group (index 2) apply_formats(f_str("xx (xx.x%)", "n", "pct"), n = c(1, 199), pct = c(0.4, 99.7), lt = 1, gt = 99, lt_gt_group = 2) # Pad to a fixed width for row-binding against other output apply_formats(f_str("xx.x", "mean"), mean = c(1.2, 10.75), width = 10) apply_formats(f_str("xx.x", "mean"), mean = c(1.2, 10.75), width = 10, pad = "left")# Vectorized: one formatted string per element apply_formats(f_str("xx (xx.x%)", "n", "pct"), n = c(5, 12, 103), pct = c(4.5, 33.333, 99.9)) # `na` replaces the default blank-width fill apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA)) apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA), na = "NE") apply_formats(f_str("xx.x", "mean"), mean = c(1.2, NA), na = "") # lt/gt thresholds, targeting the percent group (index 2) apply_formats(f_str("xx (xx.x%)", "n", "pct"), n = c(1, 199), pct = c(0.4, 99.7), lt = 1, gt = 99, lt_gt_group = 2) # Pad to a fixed width for row-binding against other output apply_formats(f_str("xx.x", "mean"), mean = c(1.2, 10.75), width = 10) apply_formats(f_str("xx.x", "mean"), mean = c(1.2, 10.75), width = 10, pad = "left")
Walks each rowlabel* column top-to-bottom and blanks values that
are identical to the previous row, respecting layer boundaries
(ord_layer_index).
apply_row_masks(result, row_breaks = FALSE)apply_row_masks(result, row_breaks = FALSE)
result |
A data.frame produced by |
row_breaks |
Logical. If TRUE, insert a blank row between layers. |
A data.frame with repeated labels blanked
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1", by = "SEX")) ) built <- tplyr_build(spec, tplyr_adsl) # SEX repeats on every age-group row built[, c("rowlabel1", "rowlabel2")] # Masked: each value prints once, at the top of its block apply_row_masks(built)[, c("rowlabel1", "rowlabel2")] # row_breaks inserts a blank row between layers two <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"), group_count("AGEGR1"))), tplyr_adsl ) apply_row_masks(two, row_breaks = TRUE)[, c("rowlabel1", "res1")]spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1", by = "SEX")) ) built <- tplyr_build(spec, tplyr_adsl) # SEX repeats on every age-group row built[, c("rowlabel1", "rowlabel2")] # Masked: each value prints once, at the top of its block apply_row_masks(built)[, c("rowlabel1", "rowlabel2")] # row_breaks inserts a blank row between layers two <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"), group_count("AGEGR1"))), tplyr_adsl ) apply_row_masks(two, row_breaks = TRUE)[, c("rowlabel1", "res1")]
Returns just the display content of a tplyr_build result,
dropping the internal ordering helpers (ord_layer_index,
ord_layer_*) and the row_id metadata column, and giving a frame
ready to hand to a table-rendering package (clinify, flextable, gt, ...). The
build output is already ordered, so no re-sorting is performed.
as_display(x, labels = FALSE)as_display(x, labels = FALSE)
x |
A data.frame produced by |
labels |
Logical. When |
Everything that is not an internal helper is kept. That is normally the
rowlabel*, res*, rdiff*, and pval* columns, but a
stats_as_columns desc layer with no by variable names its result
columns after the statistics themselves, and those are retained too.
A data.frame of display columns.
spec <- tplyr_spec(cols = "TRT", layers = tplyr_layers(group_count("SEX"))) b <- tplyr_build(spec, data.frame(TRT = rep(c("A", "B"), 3), SEX = rep(c("F", "M"), 3))) as_display(b)spec <- tplyr_spec(cols = "TRT", layers = tplyr_layers(group_count("SEX"))) b <- tplyr_build(spec, data.frame(TRT = rep(c("A", "B"), 3), SEX = rep(c("F", "M"), 3))) as_display(b)
Configures an association test and lands its formatted result beside the
n(\
comparisons is supplied:
assoc_test( fn, format = f_str("x.xxx", "p"), label = NULL, reference = NULL, comparisons = NULL, total_row = TRUE )assoc_test( fn, format = f_str("x.xxx", "p"), label = NULL, reference = NULL, comparisons = NULL, total_row = TRUE )
fn |
A function of one argument. In omnibus mode it is called with the
source-data subset (a data.frame) for a single |
format |
An |
label |
Character string used as the result column's header label. In
pairwise mode it may be a vector with one entry per comparison (or a single
value recycled across comparisons); |
reference |
Pairwise mode only. Character(1) naming the reference arm
level of the first |
comparisons |
Pairwise mode only. A character vector (or list of single
levels) of arm levels, each compared to |
total_row |
Pairwise mode only. Logical(1); when the layer also emits a
total row ( |
Omnibus mode (comparisons = NULL, the default). Runs
fn once per by group, across the treatment columns, and
lands its result as a single trailing column. The supplied function receives
the raw source-data subset for the by group (all cols levels
and all target/row levels), so a caller can tabulate and test naturally
(e.g. fisher.test(table(.data$TRT, .data$RESP)) or
coin::cmh_test(...)). When the layer has no by variable the
test runs once over the whole layer; otherwise once per by group, with
the value placed on that group's first output row. This mode works on
count, shift, and desc layers – on a desc layer
it is the natural home for a continuous-variable comparison across arms
(ANOVA / Kruskal-Wallis / t-test), e.g.
fn = function(.data) anova(lm(AGE ~ TRT, .data))[["Pr(>F)"]][1], with
one p-value per by group on that group's first statistic row.
Pairwise / per-level mode (comparisons non-NULL).
Count layers only. Emits one pval column per comparison, each
comparing an arm level of the first cols variable to reference,
with a value on every target-level row (like risk_diff's
rdiff columns). On a nested count layer it emits a value on
every row of every level – each inner (e.g. preferred-term) row and each
outer (e.g. system-organ-class subtotal) row, each row's 2x2 built from that
row's own counts – and, when the layer has a total row, on the grand-total
row too (see total_row). Here fn receives, for one (row,
comparison) pair, a 2x2 contingency matrix
matrix(c(n_ref, n_cmp, N_ref - n_ref, N_cmp - n_cmp), nrow = 2) –
rows are (reference, comparison) arm, columns are (event, no event) – where
n is the cell count and N the population denominator for that
arm. When the layer sets distinct_by, the distinct counts/denominators
are used. fn returns either a numeric value (a scalar, or a vector of
several statistics matching a multi-variable format) or a verbatim
character display string (NA renders a blank).
Attach it to a layer via
layer_settings(assoc_test = assoc_test(...)).
A tplyr_assoc_test object.
# Omnibus at <- assoc_test( fn = function(.data) fisher.test(table(.data$TRT, .data$RESP))$p.value, format = f_str("x.xxx", "p"), label = "p-value [1]" ) # Pairwise per-level (count layer): Fisher on an incidence 2x2 at2 <- assoc_test( fn = function(m) fisher.test(m)$p.value, reference = "Placebo", comparisons = c("Low", "High"), format = f_str("x.xxx", "p") ) # Omnibus on a desc layer: continuous comparison across arms (ANOVA) at3 <- assoc_test( fn = function(.data) anova(lm(AGE ~ TRT, .data))[["Pr(>F)"]][1], format = f_str("x.xxx", "p") ) # Multiple statistics in one cell: odds ratio with a confidence interval at4 <- assoc_test( fn = function(m) { ft <- fisher.test(m) c(ft$estimate, ft$conf.int[1], ft$conf.int[2]) }, reference = "Placebo", comparisons = c("Low", "High"), format = f_str("xx.xx (xx.xx, xx.xx)", "or", "lo", "hi"), label = "OR (95% CI)" )# Omnibus at <- assoc_test( fn = function(.data) fisher.test(table(.data$TRT, .data$RESP))$p.value, format = f_str("x.xxx", "p"), label = "p-value [1]" ) # Pairwise per-level (count layer): Fisher on an incidence 2x2 at2 <- assoc_test( fn = function(m) fisher.test(m)$p.value, reference = "Placebo", comparisons = c("Low", "High"), format = f_str("x.xxx", "p") ) # Omnibus on a desc layer: continuous comparison across arms (ANOVA) at3 <- assoc_test( fn = function(.data) anova(lm(AGE ~ TRT, .data))[["Pr(>F)"]][1], format = f_str("x.xxx", "p") ) # Multiple statistics in one cell: odds ratio with a confidence interval at4 <- assoc_test( fn = function(m) { ft <- fisher.test(m) c(ft$estimate, ft$conf.int[1], ft$conf.int[2]) }, reference = "Placebo", comparisons = c("Low", "High"), format = f_str("xx.xx (xx.xx, xx.xx)", "or", "lo", "hi"), label = "OR (95% CI)" )
This is a generalized post processing function that allows you to take groups of by variables and collapse them into a single column. Repeating values are split into separate rows, and for each level of nesting, a specified indentation level can be applied.
collapse_row_labels( x, ..., indent = " ", target_col = "row_label", nest = FALSE )collapse_row_labels( x, ..., indent = " ", target_col = "row_label", nest = FALSE )
x |
Input data frame |
... |
Column names (as character strings) to be collapsed, must be 2 or more |
indent |
Indentation string to be used, which is multiplied at each indentation level |
target_col |
Character string naming the output column containing collapsed row labels |
nest |
Logical. If TRUE, collapse row labels in-place without inserting stub rows for repeating values. Allows a single column to be passed. Default is FALSE. |
data.frame with row labels collapsed into a single column
x <- data.frame( row_label1 = c("A", "A", "A", "B", "B"), row_label2 = c("C", "C", "D", "E", "F"), var1 = 1:5, stringsAsFactors = FALSE ) collapse_row_labels(x, "row_label1", "row_label2") collapse_row_labels(x, "row_label1", "row_label2", indent = " ", target_col = "rl")x <- data.frame( row_label1 = c("A", "A", "A", "B", "B"), row_label2 = c("C", "C", "D", "E", "F"), var1 = 1:5, stringsAsFactors = FALSE ) collapse_row_labels(x, "row_label1", "row_label2") collapse_row_labels(x, "row_label1", "row_label2", indent = " ", target_col = "rl")
Combines existing column levels into a custom group. Rows matching any of the source levels are duplicated with the column variable set to the group name.
custom_group(col_var, ...)custom_group(col_var, ...)
col_var |
Character string naming the column variable |
... |
Named arguments where names are group labels and values are
character vectors of source levels to combine.
Example: |
A tplyr_custom_group object
# Pool the two dose arms into one "Xanomeline (All)" column, kept alongside # the arms it is built from spec <- tplyr_spec( cols = "TRT01P", custom_groups = list(custom_group( "TRT01P", "Xanomeline (All)" = c("Xanomeline High Dose", "Xanomeline Low Dose") )), layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Several groups at once custom_group( "TRT01P", "Active" = c("Xanomeline High Dose", "Xanomeline Low Dose"), "Control" = "Placebo" )# Pool the two dose arms into one "Xanomeline (All)" column, kept alongside # the arms it is built from spec <- tplyr_spec( cols = "TRT01P", custom_groups = list(custom_group( "TRT01P", "Xanomeline (All)" = c("Xanomeline High Dose", "Xanomeline Low Dose") )), layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Several groups at once custom_group( "TRT01P", "Active" = c("Xanomeline High Dose", "Xanomeline Low Dose"), "Control" = "Placebo" )
Create a format string object
f_str(format_string, ..., empty = NULL)f_str(format_string, ..., empty = NULL)
format_string |
Character string defining the display template |
... |
Character strings naming the variables that populate the template |
empty |
Value to display when data is NA/missing. Supplied as
|
Each run of x characters is one format group, and each group is filled by
the correspondingly-positioned variable in .... The count of xs sets the
field width, so "xx.x" renders two integer digits and one decimal. Literal
text between groups is preserved verbatim. a (and A) request
auto-precision, where the decimal count comes from the data.
A tplyr_f_str object
apply_formats() to render values outside a build.
# Two format groups filled by n and pct fmt <- f_str("xx (xx.x%)", "n", "pct") fmt apply_formats(fmt, n = c(5, 12), pct = c(4.5, 33.33)) # Width is set by the number of x's apply_formats(f_str("xxx", "n"), n = 7) apply_formats(f_str("x", "n"), n = 7) # `empty` fills each NA group in place, preserving alignment apply_formats(f_str("xx (xxx)", "n", "pct", empty = "NA"), n = NA, pct = NA) # `.overall` replaces the whole cell, but only when every group is NA both_na <- f_str("xx (xxx)", "n", "pct", empty = c(.overall = "Not est.")) apply_formats(both_na, n = NA, pct = NA) # Used in a layer spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", settings = layer_settings( format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")))) ) ) tplyr_build(spec, tplyr_adsl)# Two format groups filled by n and pct fmt <- f_str("xx (xx.x%)", "n", "pct") fmt apply_formats(fmt, n = c(5, 12), pct = c(4.5, 33.33)) # Width is set by the number of x's apply_formats(f_str("xxx", "n"), n = 7) apply_formats(f_str("x", "n"), n = 7) # `empty` fills each NA group in place, preserving alignment apply_formats(f_str("xx (xxx)", "n", "pct", empty = "NA"), n = NA, pct = NA) # `.overall` replaces the whole cell, but only when every group is NA both_na <- f_str("xx (xxx)", "n", "pct", empty = c(.overall = "Not est.")) apply_formats(both_na, n = NA, pct = NA) # Used in a layer spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", settings = layer_settings( format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")))) ) ) tplyr_build(spec, tplyr_adsl)
Creates a character ID for each row by combining the layer index and
row label values. These IDs can be used with tplyr_meta_result()
and tplyr_meta_subset() to look up cell metadata.
generate_row_ids(result)generate_row_ids(result)
result |
A data.frame produced by |
Character vector of row IDs (same length as nrow(result))
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl) generate_row_ids(built) # IDs are derived from the row labels, so generate them from an unmodified # build. tplyr_build(metadata = TRUE) attaches a row_id column that survives # post-processing, which is the safer route. with_meta <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) with_meta$row_idspec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl) generate_row_ids(built) # IDs are derived from the row labels, so generate them from an unmodified # build. tplyr_build(metadata = TRUE) attaches a row_id column that survives # post-processing, which is the safer route. with_meta <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) with_meta$row_id
Returns a named character vector of variable labels. Labels are extracted
from the "label" attribute of each column (standard for Haven-imported
CDISC data).
get_data_labels(data)get_data_labels(data)
data |
A data.frame |
Named character vector where names are column names and values are
labels. Columns without labels return NA_character_.
# CDISC data imported via haven carries a "label" attribute per column labs <- get_data_labels(tplyr_adsl) head(labs) # Columns with no label attribute come back NA get_data_labels(data.frame(a = 1, b = 2))# CDISC data imported via haven carries a "label" attribute per column labs <- get_data_labels(tplyr_adsl) head(labs) # Columns with no label attribute come back NA get_data_labels(data.frame(a = 1, b = 2))
Allows a user-defined function to compute summary statistics. The function receives a data subset and the target variable name for each group combination, and returns a data.frame of results.
group_analyze( target_var, by = NULL, where = NULL, analyze_fn, settings = layer_settings() )group_analyze( target_var, by = NULL, where = NULL, analyze_fn, settings = layer_settings() )
target_var |
Character string naming the target variable(s) |
by |
Character string or vector for row grouping |
where |
Expression for filtering data for this layer |
analyze_fn |
A function with signature |
settings |
A layer_settings object |
The analyze_fn is called once per group combination (defined by
cols and by data variables). It receives:
.data: A data.frame subset for the current group
.target_var: Character string with the target variable name(s)
If format_strings are provided in settings, analyze_fn should
return a single-row data.frame of named numeric values. Each format string
entry becomes one output row, with its name used as the row label.
If no format_strings are provided, analyze_fn must return a
data.frame with row_label and formatted columns.
Note that analyze_fn is called once per cols x by
combination, so it only ever sees a single treatment column at a time — it
cannot compute a statistic across the treatment columns. For an
omnibus association test that spans the columns (e.g. Fisher's exact or CMH
on a count/shift layer), see assoc_test.
A tplyr_analyze_layer object
assoc_test for cross-column association tests.
# format_strings mode: the function returns one row of named numbers, and # each format string becomes an output row. spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_analyze("AGE", analyze_fn = function(.data, .target_var) { v <- .data[[.target_var]] data.frame(gmean = exp(mean(log(v))), rng = diff(range(v))) }, settings = layer_settings(format_strings = list( "Geometric mean" = f_str("xx.xx", "gmean"), "Range" = f_str("xx", "rng") ))) ) ) tplyr_build(spec, tplyr_adsl) # Pre-formatted mode: the function supplies row_label and formatted itself. pre <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_analyze("AGE", analyze_fn = function(.data, .target_var) { v <- .data[[.target_var]] data.frame( row_label = "Median [IQR]", formatted = sprintf("%.1f [%.1f]", median(v), IQR(v)) ) }) ) ) tplyr_build(pre, tplyr_adsl)# format_strings mode: the function returns one row of named numbers, and # each format string becomes an output row. spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_analyze("AGE", analyze_fn = function(.data, .target_var) { v <- .data[[.target_var]] data.frame(gmean = exp(mean(log(v))), rng = diff(range(v))) }, settings = layer_settings(format_strings = list( "Geometric mean" = f_str("xx.xx", "gmean"), "Range" = f_str("xx", "rng") ))) ) ) tplyr_build(spec, tplyr_adsl) # Pre-formatted mode: the function supplies row_label and formatted itself. pre <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_analyze("AGE", analyze_fn = function(.data, .target_var) { v <- .data[[.target_var]] data.frame( row_label = "Median [IQR]", formatted = sprintf("%.1f [%.1f]", median(v), IQR(v)) ) }) ) ) tplyr_build(pre, tplyr_adsl)
Create a count layer
group_count(target_var, by = NULL, where = NULL, settings = layer_settings())group_count(target_var, by = NULL, where = NULL, settings = layer_settings())
target_var |
Character string or vector naming the target variable(s). Multiple variables create nested/hierarchical counts. |
by |
Character string or vector for row grouping. Strings that don't
match column names are treated as text labels. Use |
where |
Expression for filtering data for this layer |
settings |
A layer_settings object |
A tplyr_count_layer object
layer_settings() for denominators, sorting, and special rows.
# Counts of a categorical variable within each column group spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Distinct subject counts with a total row, filtered to serious events ae <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_count("AEBODSYS", where = AESER == "Y", settings = layer_settings(distinct_by = "USUBJID", total_row = TRUE)) ) ) head(tplyr_build(ae, tplyr_adae)) # Two target variables nest: preferred term within body system nested <- tplyr_spec( cols = "TRTA", layers = tplyr_layers(group_count(c("AEBODSYS", "AEDECOD"))) ) head(tplyr_build(nested, tplyr_adae))# Counts of a categorical variable within each column group spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Distinct subject counts with a total row, filtered to serious events ae <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_count("AEBODSYS", where = AESER == "Y", settings = layer_settings(distinct_by = "USUBJID", total_row = TRUE)) ) ) head(tplyr_build(ae, tplyr_adae)) # Two target variables nest: preferred term within body system nested <- tplyr_spec( cols = "TRTA", layers = tplyr_layers(group_count(c("AEBODSYS", "AEDECOD"))) ) head(tplyr_build(nested, tplyr_adae))
Create a descriptive statistics layer
group_desc(target_var, by = NULL, where = NULL, settings = layer_settings())group_desc(target_var, by = NULL, where = NULL, settings = layer_settings())
target_var |
Character string or vector naming the target variable(s) |
by |
Character string or vector for row grouping |
where |
Expression for filtering data for this layer |
settings |
A layer_settings object |
A tplyr_desc_layer object
layer_settings() for auto-precision and custom summaries.
# Default summary: n, Mean (SD), Median, Q1/Q3, Min/Max, Missing spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) tplyr_build(spec, tplyr_adsl) # Choose the statistics and their formats custom <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( format_strings = list( "n" = f_str("xx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd") ))) ) ) tplyr_build(custom, tplyr_adsl) # Several target variables in one layer, grouped by visit multi <- tplyr_spec( cols = "TRTA", layers = tplyr_layers(group_desc(c("AVAL", "CHG"), by = "AVISIT")) ) head(tplyr_build(multi, tplyr_adlb))# Default summary: n, Mean (SD), Median, Q1/Q3, Min/Max, Missing spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) tplyr_build(spec, tplyr_adsl) # Choose the statistics and their formats custom <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( format_strings = list( "n" = f_str("xx", "n"), "Mean (SD)" = f_str("xx.x (xx.xx)", "mean", "sd") ))) ) ) tplyr_build(custom, tplyr_adsl) # Several target variables in one layer, grouped by visit multi <- tplyr_spec( cols = "TRTA", layers = tplyr_layers(group_desc(c("AVAL", "CHG"), by = "AVISIT")) ) head(tplyr_build(multi, tplyr_adlb))
Create a shift layer
group_shift(target_var, by = NULL, where = NULL, settings = layer_settings())group_shift(target_var, by = NULL, where = NULL, settings = layer_settings())
target_var |
Named character vector with |
by |
Character string or vector for row grouping |
where |
Expression for filtering data for this layer |
settings |
A layer_settings object |
A tplyr_shift_layer object
# Baseline (rows) against post-baseline (columns) reference ranges spec <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_shift(c(row = "BNRIND", column = "ANRIND")) ) ) head(tplyr_build(spec, tplyr_adlb)) # Percentages relative to each baseline (column) group rather than the arm by_col <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_shift(c(row = "BNRIND", column = "ANRIND"), settings = layer_settings(shift_denom = "column")) ) ) head(tplyr_build(by_col, tplyr_adlb))# Baseline (rows) against post-baseline (columns) reference ranges spec <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_shift(c(row = "BNRIND", column = "ANRIND")) ) ) head(tplyr_build(spec, tplyr_adlb)) # Percentages relative to each baseline (column) group rather than the arm by_col <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_shift(c(row = "BNRIND", column = "ANRIND"), settings = layer_settings(shift_denom = "column")) ) ) head(tplyr_build(by_col, tplyr_adlb))
Check if an object is a tplyr_pop_data
is_pop_data(x)is_pop_data(x)
x |
An object to check |
Logical
is_pop_data(pop_data(cols = "TRT01P")) is_pop_data("TRT01P")is_pop_data(pop_data(cols = "TRT01P")) is_pop_data("TRT01P")
Check if an object is a tplyr_layer
is_tplyr_layer(x)is_tplyr_layer(x)
x |
Object to check |
Logical
is_tplyr_layer(group_count("SEX")) is_tplyr_layer(group_desc("AGE")) is_tplyr_layer("SEX")is_tplyr_layer(group_count("SEX")) is_tplyr_layer(group_desc("AGE")) is_tplyr_layer("SEX")
Check if an object is a tplyr_spec
is_tplyr_spec(x)is_tplyr_spec(x)
x |
An object to check |
Logical
spec <- tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"))) is_tplyr_spec(spec) is_tplyr_spec(mtcars)spec <- tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"))) is_tplyr_spec(spec) is_tplyr_spec(mtcars)
Explicitly marks a string as a text label (not a data variable name). Useful when a label string might coincidentally match a column name.
label(x)label(x)
x |
Character string to use as a label |
A tplyr_label object
# A `by` string that matches no column is already treated as a label, but # label() is explicit -- and necessary when the text matches a column name. spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", by = label("Age Group (y)")) ) ) head(tplyr_build(spec, tplyr_adsl))# A `by` string that matches no column is already treated as a label, but # label() is explicit -- and necessary when the text matches a column name. spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", by = label("Age Group (y)")) ) ) head(tplyr_build(spec, tplyr_adsl))
Configuration object for all layer options. Unused parameters default to NULL and are ignored during build. Type-specific validation happens at build time.
layer_settings( format_strings = NULL, stat_columns = NULL, denoms_by = NULL, shift_denom = "total", denom_row = FALSE, denom_row_label = "n", denom_row_format = NULL, denom_where = NULL, denom_ignore = NULL, distinct_by = NULL, total_row = FALSE, total_row_label = "Total", total_row_count_missings = TRUE, missing_count = NULL, missing_subjects = FALSE, missing_subjects_label = "Missing", keep_levels = NULL, limit_data_by = NULL, custom_summaries = NULL, stats_as_columns = FALSE, precision_by = NULL, precision_on = NULL, precision_data = NULL, precision_cap = NULL, order_count_method = NULL, ordering_cols = NULL, result_order_var = NULL, outer_sort_position = NULL, risk_diff = NULL, ci_method = c("clopper_pearson", "wilson", "wald", "agresti_coull", "jeffreys"), ci_level = 0.95, assoc_test = NULL, pct_lt = NULL, pct_gt = NULL, zero_count_display = "full", name = NULL )layer_settings( format_strings = NULL, stat_columns = NULL, denoms_by = NULL, shift_denom = "total", denom_row = FALSE, denom_row_label = "n", denom_row_format = NULL, denom_where = NULL, denom_ignore = NULL, distinct_by = NULL, total_row = FALSE, total_row_label = "Total", total_row_count_missings = TRUE, missing_count = NULL, missing_subjects = FALSE, missing_subjects_label = "Missing", keep_levels = NULL, limit_data_by = NULL, custom_summaries = NULL, stats_as_columns = FALSE, precision_by = NULL, precision_on = NULL, precision_data = NULL, precision_cap = NULL, order_count_method = NULL, ordering_cols = NULL, result_order_var = NULL, outer_sort_position = NULL, risk_diff = NULL, ci_method = c("clopper_pearson", "wilson", "wald", "agresti_coull", "jeffreys"), ci_level = 0.95, assoc_test = NULL, pct_lt = NULL, pct_gt = NULL, zero_count_display = "full", name = NULL )
format_strings |
Named list of f_str objects |
stat_columns |
Named list of f_str objects for count layers. Each
entry produces its own result column per column group (e.g. one
"n (\
name used as the column sub-label. Column label attributes follow the
pattern |
denoms_by |
Character vector of variable names for denominator grouping.
This replaces (does not augment) the default denominator grouping,
which is the column ( |
shift_denom |
Denominator basis for shift layers. |
denom_row |
Logical, shift layers only. When |
denom_row_label |
Character string, the row label for the |
denom_row_format |
An |
denom_where |
Expression for separate denominator filter |
denom_ignore |
Character vector of values to exclude from denominators |
distinct_by |
Character string naming the variable for distinct counting |
total_row |
Logical, whether to add a total row |
total_row_label |
Character string for total row label |
total_row_count_missings |
Logical, include missing in total |
missing_count |
List configuring the Missing row. Recognized keys:
Any other key is an error. |
missing_subjects |
Logical, add missing subjects row |
missing_subjects_label |
Character string for missing subjects label |
keep_levels |
Character vector of levels to keep |
limit_data_by |
Character vector for data limiting |
custom_summaries |
Named list of expressions for custom summaries |
stats_as_columns |
Logical, transpose stats to columns |
precision_by |
Character vector for precision grouping |
precision_on |
Character string for precision variable |
precision_data |
Data frame with external precision values |
precision_cap |
Named numeric vector c(int=, dec=) |
order_count_method |
Character, ordering method |
ordering_cols |
Character, which column drives ordering |
result_order_var |
Character, which result variable for ordering |
outer_sort_position |
Character, outer sort direction |
risk_diff |
List with risk difference configuration |
ci_method |
Method for the single-proportion confidence interval exposed
through the |
ci_level |
Numeric coverage probability for the single-proportion
confidence interval keywords. Defaults to |
assoc_test |
A |
pct_lt |
Numeric less-than threshold for count-layer percents. A cell
with a nonzero count whose percent would display below this value renders
the percent as |
pct_gt |
Numeric greater-than threshold for count-layer percents. A cell
whose percent is below 100 but would display above this value renders the
percent as |
zero_count_display |
How to display count-layer cells whose count is
zero: |
name |
Character string, layer name for identification |
A tplyr_layer_settings object
Not all settings apply to every layer type. The table below shows which settings are applicable for each of the four layer types:
| Setting | Count | Desc | Shift | Analyze |
format_strings |
X | X | X | X |
stat_columns |
X | |||
denoms_by |
X | X | X | |
shift_denom |
X | |||
denom_row |
X | |||
denom_row_label |
X | |||
denom_row_format |
X | |||
denom_where |
X | X | X | |
denom_ignore |
X | X | ||
distinct_by |
X | X | ||
total_row |
X | |||
total_row_label |
X | |||
total_row_count_missings |
X | |||
missing_count |
X | |||
missing_subjects |
X | |||
missing_subjects_label |
X | |||
keep_levels |
X | |||
limit_data_by |
X | |||
custom_summaries |
X | |||
stats_as_columns |
X | |||
precision_by |
X | |||
precision_on |
X | |||
precision_data |
X | |||
precision_cap |
X | |||
order_count_method |
X | |||
ordering_cols |
X | |||
result_order_var |
X | |||
outer_sort_position |
X | |||
risk_diff |
X | |||
ci_method |
X | |||
ci_level |
X | |||
assoc_test |
X | X | X | |
pct_lt |
X | |||
pct_gt |
X | |||
zero_count_display |
X | |||
name |
X | X | X | X |
Settings provided for an inapplicable layer type are silently ignored.
# Formats and special rows on a count layer spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", settings = layer_settings( format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")), total_row = TRUE, total_row_label = "Total subjects" )) ) ) tplyr_build(spec, tplyr_adsl) # Denominators: percentages within each arm-by-sex cell rather than the arm denom <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", by = "SEX", settings = layer_settings(denoms_by = c("TRT01P", "SEX"))) ) ) tplyr_build(denom, tplyr_adsl) # Auto-precision on a desc layer: 'a' takes decimals from the data, and # precision_cap bounds them. prec <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_desc("AVAL", by = "PARAMCD", settings = layer_settings( format_strings = list("Mean (SD)" = f_str("a.a+1 (a.a+2)", "mean", "sd")), precision_by = "PARAMCD", precision_cap = c(int = 3, dec = 2) )) ) ) head(tplyr_build(prec, tplyr_adlb)) # A custom summary adds a statistic the built-ins do not provide cv <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( custom_summaries = list(cv = quote(sd(.var) / mean(.var) * 100)), format_strings = list("CV%" = f_str("xx.x", "cv")) )) ) ) tplyr_build(cv, tplyr_adsl)# Formats and special rows on a count layer spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", settings = layer_settings( format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")), total_row = TRUE, total_row_label = "Total subjects" )) ) ) tplyr_build(spec, tplyr_adsl) # Denominators: percentages within each arm-by-sex cell rather than the arm denom <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1", by = "SEX", settings = layer_settings(denoms_by = c("TRT01P", "SEX"))) ) ) tplyr_build(denom, tplyr_adsl) # Auto-precision on a desc layer: 'a' takes decimals from the data, and # precision_cap bounds them. prec <- tplyr_spec( cols = "TRTA", layers = tplyr_layers( group_desc("AVAL", by = "PARAMCD", settings = layer_settings( format_strings = list("Mean (SD)" = f_str("a.a+1 (a.a+2)", "mean", "sd")), precision_by = "PARAMCD", precision_cap = c(int = 3, dec = 2) )) ) ) head(tplyr_build(prec, tplyr_adlb)) # A custom summary adds a statistic the built-ins do not provide cv <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( custom_summaries = list(cv = quote(sd(.var) / mean(.var) * 100)), format_strings = list("CV%" = f_str("xx.x", "cv")) )) ) ) tplyr_build(cv, tplyr_adsl)
Configuration object specifying how population data maps to the spec.
The actual population data.frame is provided at build time via
tplyr_build(spec, data, pop_data = ...).
pop_data(cols, where = NULL)pop_data(cols, where = NULL)
cols |
Character vector of column variable names in the population data.
If named, names are the spec column names and values are the pop_data column
names (e.g., |
where |
Expression for filtering the population data (optional) |
A tplyr_pop_data object
# The AE data's TRTA maps to the subject-level TRT01P. Denominators and the # header N then come from the population, not from the AE records. spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers( group_count("AEBODSYS", settings = layer_settings(distinct_by = "USUBJID")) ) ) built <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) head(built) # Header N reflects the 254 enrolled subjects, not the 200 AE records tplyr_header_n(built) # Restrict the population to the safety set saf <- pop_data(cols = c("TRTA" = "TRT01P"), where = SAFFL == "Y") saf# The AE data's TRTA maps to the subject-level TRT01P. Denominators and the # header N then come from the population, not from the AE records. spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers( group_count("AEBODSYS", settings = layer_settings(distinct_by = "USUBJID")) ) ) built <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) head(built) # Header N reflects the 254 enrolled subjects, not the 200 AE records tplyr_header_n(built) # Restrict the population to the safety set saf <- pop_data(cols = c("TRTA" = "TRT01P"), where = SAFFL == "Y") saf
Vectorized computation of a two-sided confidence interval for a single
proportion x / n, using one of five standard methods. All methods are
computed in closed form or via qbeta (no per-row
binom.test loop), so the function scales to a full count table's worth
of cells at once.
proportion_ci( x, n, method = c("clopper_pearson", "wilson", "wald", "agresti_coull", "jeffreys"), level = 0.95 )proportion_ci( x, n, method = c("clopper_pearson", "wilson", "wald", "agresti_coull", "jeffreys"), level = 0.95 )
x |
Numeric vector of event counts (numerators). |
n |
Numeric vector of trial counts (denominators). Recycled against
|
method |
One of |
level |
Numeric coverage probability (default |
The methods and their references:
clopper_pearsonExact interval based on the beta
distribution. Matches stats::binom.test() and SAS
PROC FREQ ... EXACT (the clinical convention). This is the
default.
wilsonWilson score interval without continuity correction.
Matches stats::prop.test(correct = FALSE).
waldNormal-approximation ("simple asymptotic") interval,
clamped to [0, 1].
agresti_coullAdds z^2/2 pseudo-successes and
pseudo-failures, then applies the Wald interval to the adjusted counts.
jeffreysBayesian interval using the Jeffreys
Beta(0.5, 0.5) prior, with the standard boundary adjustments at
x == 0 and x == n.
Edge cases: when n == 0 (or either input is NA) both bounds are
NA; when x == 0 the lower bound is exactly 0; when
x == n the upper bound is exactly 1.
A data.table with two columns, lower and upper,
giving the interval bounds as proportions in [0, 1] (multiply by 100
for the percentage scale).
proportion_ci(c(0, 12, 40), c(40, 40, 40), method = "clopper_pearson")proportion_ci(c(0, 12, 40), c(40, 40, 40), method = "clopper_pearson")
Useful for HTML rendering where leading spaces are collapsed.
replace_leading_whitespace(x, replace_with = " ")replace_leading_whitespace(x, replace_with = " ")
x |
Character vector |
replace_with |
Replacement string for each leading space |
Character vector with leading spaces replaced
# Indentation survives an HTML renderer that would collapse plain spaces terms <- c("CARDIAC DISORDERS", " ATRIAL FIBRILLATION") out <- replace_leading_whitespace(terms) out # One replacement per leading space; interior spacing is untouched replace_leading_whitespace(" A B", replace_with = "-")# Indentation survives an HTML renderer that would collapse plain spaces terms <- c("CARDIAC DISORDERS", " ATRIAL FIBRILLATION") out <- replace_leading_whitespace(terms) out # One replacement per leading space; interior spacing is untouched replace_leading_whitespace(" A B", replace_with = "-")
Extracts the Nth numeric value from a formatted tplyr2 string.
str_extract_num(x, index = 1L)str_extract_num(x, index = 1L)
x |
Character vector of formatted strings |
index |
Integer, which numeric value to extract (1-based) |
Numeric vector
cells <- c(" 22 (25.6%)", " 9 (10.5%)", " 55 (64.0%)") # Default pulls the count; index = 2 pulls the percent str_extract_num(cells) str_extract_num(cells, index = 2) # Asking past the available numbers gives NA, as does an NA input str_extract_num(c(" 5", NA), index = 2) # Recover a sort key from an already-formatted table built <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1"))), tplyr_adsl ) built[order(-str_extract_num(built$res1)), c("rowlabel1", "res1")]cells <- c(" 22 (25.6%)", " 9 (10.5%)", " 55 (64.0%)") # Default pulls the count; index = 2 pulls the percent str_extract_num(cells) str_extract_num(cells, index = 2) # Asking past the available numbers gives NA, as does an NA input str_extract_num(c(" 5", NA), index = 2) # Recover a sort key from an already-formatted table built <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1"))), tplyr_adsl ) built[order(-str_extract_num(built$res1)), c("rowlabel1", "res1")]
Leverages stringr::str_wrap() under the hood, but takes extra steps
to preserve any indentation that has been applied to a character element,
and use hyphenated wrapping of single words that run longer than the
allotted wrapping width.
str_indent_wrap(x, width = 10, tab_width = 5)str_indent_wrap(x, width = 10, tab_width = 5)
x |
An input character vector |
width |
The desired width of elements within the output character vector |
tab_width |
The number of spaces to which tabs should be converted |
stringr::str_wrap() is highly efficient, but in the context of table
creation there are two features missing — hyphenation for long running
strings that overflow width, and respect for pre-indentation of a character
element. For example, in an adverse event table, you may have body system
rows as an un-indented column, and preferred terms as indented columns. These
strings may run long and require wrapping to not surpass the column width.
Furthermore, for crowded tables a single word may be longer than the column
width itself.
This function resolves these two issues, while minimizing additional overhead required to apply the wrapping of strings.
Note: This function automatically converts tabs to spaces. Tab width varies
depending on font, so width cannot automatically be determined within a data
frame. Users can specify the width via tab_width.
A character vector with string wrapping applied
ex_text1 <- c("RENAL AND URINARY DISORDERS", " NEPHROLITHIASIS") ex_text2 <- c("RENAL AND URINARY DISORDERS", "\tNEPHROLITHIASIS") cat(paste(str_indent_wrap(ex_text1, width = 8), collapse = "\n\n"), "\n") cat(paste(str_indent_wrap(ex_text2, tab_width = 4), collapse = "\n\n"), "\n")ex_text1 <- c("RENAL AND URINARY DISORDERS", " NEPHROLITHIASIS") ex_text2 <- c("RENAL AND URINARY DISORDERS", "\tNEPHROLITHIASIS") cat(paste(str_indent_wrap(ex_text1, width = 8), collapse = "\n\n"), "\n") cat(paste(str_indent_wrap(ex_text2, tab_width = 4), collapse = "\n\n"), "\n")
Specifies that a synthetic "Total" column level should be added by duplicating all rows with the specified column variable set to the label.
total_group(col_var, label = "Total")total_group(col_var, label = "Total")
col_var |
Character string naming the column variable to totalize |
label |
Character string for the total group label (default: "Total") |
A tplyr_total_group object
# Adds a "Total" column spanning every arm, alongside the individual arms spec <- tplyr_spec( cols = "TRT01P", total_groups = list(total_group("TRT01P")), layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Rename the total column all_pts <- tplyr_spec( cols = "TRT01P", total_groups = list(total_group("TRT01P", label = "All Patients")), layers = tplyr_layers(group_count("SEX")) ) tplyr_build(all_pts, tplyr_adsl)# Adds a "Total" column spanning every arm, alongside the individual arms spec <- tplyr_spec( cols = "TRT01P", total_groups = list(total_group("TRT01P")), layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Rename the total column all_pts <- tplyr_spec( cols = "TRT01P", total_groups = list(total_group("TRT01P", label = "All Patients")), layers = tplyr_layers(group_count("SEX")) ) tplyr_build(all_pts, tplyr_adsl)
A sample CDISC ADaM ADAE dataset from the PHUSE Test Data Factory. Contains adverse event records.
tplyr_adaetplyr_adae
A data.frame
A sample CDISC ADaM ADLB dataset from the PHUSE Test Data Factory. Contains laboratory test results.
tplyr_adlbtplyr_adlb
A data.frame
A sample CDISC ADaM ADSL dataset from the PHUSE Test Data Factory. Contains subject-level demographics and baseline characteristics.
tplyr_adsltplyr_adsl
A data.frame
Executes the table specification against the provided data, producing a formatted output data frame.
tplyr_build(spec, data, pop_data = NULL, metadata = FALSE, ...)tplyr_build(spec, data, pop_data = NULL, metadata = FALSE, ...)
spec |
A tplyr_spec object (or path to a JSON/YAML spec file) |
data |
A data.frame to process |
pop_data |
Optional population data.frame (overrides spec pop_data) |
metadata |
If TRUE, attach cell-level metadata enabling traceability
back to source data rows via |
... |
Additional named arguments overriding spec-level parameters. Names
must match a field of |
A data.frame with rowlabel, res, and ord columns
tplyr_spec() to build the specification, and
tplyr_numeric_data() for the unformatted values behind the cells.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Override spec fields at build time without editing the spec. Overrides go # through `...`, which evaluates eagerly, so a `where` must be a string or # quoted -- not the bare expression tplyr_spec() accepts. tplyr_build(spec, tplyr_adsl, where = "SEX == 'F'") tplyr_build(spec, tplyr_adsl, where = quote(SEX == "F")) # Population data supplies the denominators and the header N pop_spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers( group_count("AEBODSYS", settings = layer_settings(distinct_by = "USUBJID")) ) ) head(tplyr_build(pop_spec, tplyr_adae, pop_data = tplyr_adsl))spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) tplyr_build(spec, tplyr_adsl) # Override spec fields at build time without editing the spec. Overrides go # through `...`, which evaluates eagerly, so a `where` must be a string or # quoted -- not the bare expression tplyr_spec() accepts. tplyr_build(spec, tplyr_adsl, where = "SEX == 'F'") tplyr_build(spec, tplyr_adsl, where = quote(SEX == "F")) # Population data supplies the denominators and the header N pop_spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers( group_count("AEBODSYS", settings = layer_settings(distinct_by = "USUBJID")) ) ) head(tplyr_build(pop_spec, tplyr_adae, pop_data = tplyr_adsl))
Takes Analysis Results Data (long format) and a tplyr_spec, then
applies the spec's formatting rules to produce a formatted output table.
tplyr_from_ard(ard, spec)tplyr_from_ard(ard, spec)
ard |
A data.frame in ARD format (as produced by |
spec |
A |
A data.frame with the same structure as tplyr_build() output
tplyr_to_ard() to produce the ARD.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) built <- tplyr_build(spec, tplyr_adsl) # Round-tripping through the ARD reproduces the formatted cells, so a table # can be rebuilt from stored results without the subject-level data ard <- tplyr_to_ard(built) recon <- tplyr_from_ard(ard, spec) recon[, c("rowlabel1", "res1")] identical(as.vector(recon$res1), as.vector(built$res1)) # Changing only the spec's formats re-renders the same numbers differently respec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( format_strings = list("Mean (SD)" = f_str("xx.xx (xx.xxx)", "mean", "sd")))) ) ) tplyr_from_ard(ard, respec)[, c("rowlabel1", "res1")]spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) built <- tplyr_build(spec, tplyr_adsl) # Round-tripping through the ARD reproduces the formatted cells, so a table # can be rebuilt from stored results without the subject-level data ard <- tplyr_to_ard(built) recon <- tplyr_from_ard(ard, spec) recon[, c("rowlabel1", "res1")] identical(as.vector(recon$res1), as.vector(built$res1)) # Changing only the spec's formats re-renders the same numbers differently respec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_desc("AGE", settings = layer_settings( format_strings = list("Mean (SD)" = f_str("xx.xx (xx.xxx)", "mean", "sd")))) ) ) tplyr_from_ard(ard, respec)[, c("rowlabel1", "res1")]
Returns the population-based header N values that were computed during
tplyr_build(). Only available when population data was provided.
tplyr_header_n(result)tplyr_header_n(result)
result |
A data.frame produced by |
A data.frame with column variable levels and their N values, or NULL if no population data was used.
spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers(group_count("AEBODSYS")) ) built <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) tplyr_header_n(built) # NULL when the build had no population data to draw an N from no_pop <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"))), tplyr_adsl ) tplyr_header_n(no_pop)spec <- tplyr_spec( cols = "TRTA", pop_data = pop_data(cols = c("TRTA" = "TRT01P")), layers = tplyr_layers(group_count("AEBODSYS")) ) built <- tplyr_build(spec, tplyr_adae, pop_data = tplyr_adsl) tplyr_header_n(built) # NULL when the build had no population data to draw an N from no_pop <- tplyr_build( tplyr_spec(cols = "TRT01P", layers = tplyr_layers(group_count("SEX"))), tplyr_adsl ) tplyr_header_n(no_pop)
Wraps one or more layer objects into a validated list for use in tplyr_spec().
tplyr_layers(...)tplyr_layers(...)
... |
Layer objects created by group_count(), group_desc(), group_shift(), or group_analyze() |
A list of tplyr_layer objects
# Layers stack in the order given, and may mix types freely layers <- tplyr_layers( group_desc("AGE"), group_count("SEX"), group_count("AGEGR1") ) length(layers) spec <- tplyr_spec(cols = "TRT01P", layers = layers) tplyr_build(spec, tplyr_adsl)# Layers stack in the order given, and may mix types freely layers <- tplyr_layers( group_desc("AGE"), group_count("SEX"), group_count("AGEGR1") ) length(layers) spec <- tplyr_spec(cols = "TRT01P", layers = layers) tplyr_build(spec, tplyr_adsl)
Contains filter expressions that, when evaluated against the original data, reproduce the subset of rows that contributed to a specific cell in the output table.
tplyr_meta( names = character(0), filters = list(), layer_index = integer(0), anti_join = NULL, statistic = NULL )tplyr_meta( names = character(0), filters = list(), layer_index = integer(0), anti_join = NULL, statistic = NULL )
names |
Character vector of variable names relevant to this cell |
filters |
List of R language objects (call expressions) representing filter conditions |
layer_index |
Integer layer index (1-based) |
anti_join |
NULL or a |
statistic |
NULL or a character string naming which statistic the
cell displays (set for |
A tplyr_meta object
tplyr_meta_result() to retrieve one from a build.
# Usually obtained from a build rather than constructed by hand m <- tplyr_meta( names = c("TRT01P", "AGEGR1"), filters = list(quote(TRT01P == "Placebo"), quote(AGEGR1 == "65-80")), layer_index = 1L ) m # The filters are ordinary language objects, so they can be applied directly subset(tplyr_adsl, TRT01P == "Placebo" & AGEGR1 == "65-80")[1:3, c("USUBJID", "AGEGR1")]# Usually obtained from a build rather than constructed by hand m <- tplyr_meta( names = c("TRT01P", "AGEGR1"), filters = list(quote(TRT01P == "Placebo"), quote(AGEGR1 == "65-80")), layer_index = 1L ) m # The filters are ordinary language objects, so they can be applied directly subset(tplyr_adsl, TRT01P == "Placebo" & AGEGR1 == "65-80")[1:3, c("USUBJID", "AGEGR1")]
Returns a tplyr_meta object containing the filter expressions
that describe the source data for the specified cell.
tplyr_meta_result(result, row_id, column)tplyr_meta_result(result, row_id, column)
result |
A data.frame from |
row_id |
Character row ID (from |
column |
Character column name (e.g., |
A tplyr_meta object, or NULL if no metadata for that cell
tplyr_meta_subset() to fetch the source rows themselves.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) built[, c("row_id", "rowlabel1", "res1")] # The filters behind the Placebo / 65-80 cell tplyr_meta_result(built, built$row_id[1], "res1")spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) built[, c("row_id", "rowlabel1", "res1")] # The filters behind the Placebo / 65-80 cell tplyr_meta_result(built, built$row_id[1], "res1")
Evaluates the stored filter expressions against the original data to return the rows that contributed to the specified output cell.
tplyr_meta_subset(result, row_id, column, data, pop_data = NULL)tplyr_meta_subset(result, row_id, column, data, pop_data = NULL)
result |
A data.frame from |
row_id |
Character row ID |
column |
Character column name (e.g., |
data |
The original data.frame that was passed to |
pop_data |
Optional population data.frame, required when the cell represents a missing subjects row (anti-join) |
A data.frame subset of the original data, or NULL if no metadata
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) built[, c("row_id", "rowlabel1", "res1")] # Trace the first cell back to the subjects it counted src <- tplyr_meta_subset(built, built$row_id[1], "res1", tplyr_adsl) nrow(src) head(src[, c("USUBJID", "TRT01P", "AGEGR1")]) # The row count matches the number displayed in the cell built$res1[1]spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_count("AGEGR1")) ) built <- tplyr_build(spec, tplyr_adsl, metadata = TRUE) built[, c("row_id", "rowlabel1", "res1")] # Trace the first cell back to the subjects it counted src <- tplyr_meta_subset(built, built$row_id[1], "res1", tplyr_adsl) nrow(src) head(src[, c("USUBJID", "TRT01P", "AGEGR1")]) # The row count matches the number displayed in the cell built$res1[1]
Returns the unformatted numeric data that was computed during the build process, before formatting and pivoting to wide format.
tplyr_numeric_data(result, layer = NULL)tplyr_numeric_data(result, layer = NULL)
result |
A data.frame produced by |
layer |
Integer layer index (1-based), or NULL for all layers |
If layer is specified, a data.frame of raw statistics for that
layer. If layer is NULL, a named list of data.frames keyed by layer
index. Returns NULL if numeric data was not retained.
tplyr_stats_data() for a single statistic with its grouping
columns.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) built <- tplyr_build(spec, tplyr_adsl) # One data.frame per layer, keyed by layer index names(tplyr_numeric_data(built)) # The counts behind the formatted cells, before rounding and padding head(tplyr_numeric_data(built, 1)) # Every statistic the desc layer computed, including unused ones names(tplyr_numeric_data(built, 2))spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) built <- tplyr_build(spec, tplyr_adsl) # One data.frame per layer, keyed by layer index names(tplyr_numeric_data(built)) # The counts behind the formatted cells, before rounding and padding head(tplyr_numeric_data(built, 1)) # Every statistic the desc layer computed, including unused ones names(tplyr_numeric_data(built, 2))
Deserializes a spec from a file. Expressions are reconstructed from their string representations.
tplyr_read_spec(path)tplyr_read_spec(path)
path |
File path to a JSON or YAML spec file |
A tplyr_spec object
tplyr_write_spec() to write one.
spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers(group_count("AGEGR1", by = "SEX")) ) path <- file.path(tempdir(), "spec.json") tplyr_write_spec(spec, path) # The round trip reproduces the spec, and the same table spec2 <- tplyr_read_spec(path) spec2 identical(tplyr_build(spec, tplyr_adsl), tplyr_build(spec2, tplyr_adsl)) # tplyr_build() also accepts a spec file path directly head(tplyr_build(path, tplyr_adsl)) unlink(path)spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers(group_count("AGEGR1", by = "SEX")) ) path <- file.path(tempdir(), "spec.json") tplyr_write_spec(spec, path) # The round trip reproduces the spec, and the same table spec2 <- tplyr_read_spec(path) spec2 identical(tplyr_build(spec, tplyr_adsl), tplyr_build(spec2, tplyr_adsl)) # tplyr_build() also accepts a spec file path directly head(tplyr_build(path, tplyr_adsl)) unlink(path)
The spec is a pure configuration object describing what to compute.
No data processing occurs until tplyr_build() is called.
tplyr_spec( cols, where = NULL, pop_data = NULL, total_groups = NULL, custom_groups = NULL, layers = tplyr_layers(), settings = NULL )tplyr_spec( cols, where = NULL, pop_data = NULL, total_groups = NULL, custom_groups = NULL, layers = tplyr_layers(), settings = NULL )
cols |
Character vector of column variable names |
where |
Expression for global data filter (optional) |
pop_data |
A pop_data() object for population-based features (optional) |
total_groups |
List of total_group() objects (optional) |
custom_groups |
List of custom_group() objects (optional) |
layers |
A list of layer objects from tplyr_layers() |
settings |
Additional spec-level settings (optional) |
A tplyr_spec object
tplyr_build() to execute a spec, tplyr_layers() to assemble
layers, and layer_settings() for per-layer configuration.
# A spec is inert configuration -- nothing is computed until tplyr_build() spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) spec tplyr_build(spec, tplyr_adsl) # A global `where` filter applies to every layer safety <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers(group_count("SEX")) ) tplyr_build(safety, tplyr_adsl)# A spec is inert configuration -- nothing is computed until tplyr_build() spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) spec tplyr_build(spec, tplyr_adsl) # A global `where` filter applies to every layer safety <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers(group_count("SEX")) ) tplyr_build(safety, tplyr_adsl)
Filters the raw numeric data for a specific layer and statistic. Use
tplyr_numeric_data() to get every statistic for the layer.
tplyr_stats_data(result, layer, statistic)tplyr_stats_data(result, layer, statistic)
result |
A data.frame produced by |
layer |
Integer layer index (1-based) |
statistic |
Character string naming the statistic column to extract (e.g., "n", "pct", "mean", "sd") |
A data.frame with the layer's grouping columns and the requested statistic. Returns NULL if the layer has no numeric data or does not compute the statistic.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) built <- tplyr_build(spec, tplyr_adsl) tplyr_stats_data(built, 1, "mean")spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers(group_desc("AGE")) ) built <- tplyr_build(spec, tplyr_adsl) tplyr_stats_data(built, 1, "mean")
Transforms the numeric data attached to a tplyr_build() result into
a long-format data frame with one row per statistic per group combination.
This is compatible with the CDISC Analysis Results Data standard.
tplyr_to_ard(result)tplyr_to_ard(result)
result |
A data.frame produced by |
A data.frame in long format with columns:
Integer layer index
Character name of the statistic
Numeric value of the statistic
Grouping columns from the original data
tplyr_from_ard() to rebuild a formatted table from an ARD.
spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) built <- tplyr_build(spec, tplyr_adsl) ard <- tplyr_to_ard(built) head(ard) # One row per statistic per group; analysis_id identifies the layer table(ard$analysis_id) unique(ard$stat_name[ard$analysis_id == 2])spec <- tplyr_spec( cols = "TRT01P", layers = tplyr_layers( group_count("AGEGR1"), group_desc("AGE") ) ) built <- tplyr_build(spec, tplyr_adsl) ard <- tplyr_to_ard(built) head(ard) # One row per statistic per group; analysis_id identifies the layer table(ard$analysis_id) unique(ard$stat_name[ard$analysis_id == 2])
Serializes a spec object to a file. The format is determined by the file
extension: .json for JSON, .yaml or .yml for YAML.
tplyr_write_spec(spec, path)tplyr_write_spec(spec, path)
spec |
A tplyr_spec object |
path |
File path. Extension determines format. |
Expressions (e.g., where clauses) are deparsed to strings and
reconstructed on read. Format string objects (f_str) are stored as
their component parts and regenerated on read.
Invisible file path
tplyr_read_spec() to read one back.
spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers( group_count("AGEGR1", by = "SEX", settings = layer_settings( denoms_by = c("TRT01P", "SEX"), format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")))) ) ) path <- file.path(tempdir(), "spec.json") tplyr_write_spec(spec, path) cat(readLines(path), sep = "\n") # YAML is chosen by the file extension if (requireNamespace("yaml", quietly = TRUE)) { ypath <- file.path(tempdir(), "spec.yaml") tplyr_write_spec(spec, ypath) cat(readLines(ypath), sep = "\n") unlink(ypath) } unlink(path)spec <- tplyr_spec( cols = "TRT01P", where = SAFFL == "Y", layers = tplyr_layers( group_count("AGEGR1", by = "SEX", settings = layer_settings( denoms_by = c("TRT01P", "SEX"), format_strings = list(n_counts = f_str("xx (xx.x%)", "n", "pct")))) ) ) path <- file.path(tempdir(), "spec.json") tplyr_write_spec(spec, path) cat(readLines(path), sep = "\n") # YAML is chosen by the file extension if (requireNamespace("yaml", quietly = TRUE)) { ypath <- file.path(tempdir(), "spec.yaml") tplyr_write_spec(spec, ypath) cat(readLines(ypath), sep = "\n") unlink(ypath) } unlink(path)
View and modify tplyr2-specific options. When called with no arguments, returns all current tplyr2 options with their defaults. When called with named arguments, sets those options.
tplyr2_options(...)tplyr2_options(...)
... |
Named arguments to set (e.g., |
Available options:
Logical. Use round-half-away-from-zero instead
of R's default banker's rounding. Default: FALSE.
Integer. Quantile algorithm type passed to
quantile(). Default: 7.
Named numeric vector c(int=, dec=).
Maximum int/dec widths for auto-precision. Default: NULL.
Named list of expressions for global custom
summary functions. Default: NULL.
Integer. scipen value used during tplyr_build()
to prevent scientific notation. Default: 9999.
An unrecognized option name is an error rather than a silent no-op, so a misspelling cannot quietly leave the build on the default behavior.
When called with no arguments, a named list of current option values. When called with arguments, invisibly returns the previous values.
# Inspect the current values tplyr2_options() # Setting returns the previous values, so the change can be undone old <- tplyr2_options(IBMRounding = TRUE) getOption("tplyr2.IBMRounding") do.call(options, old) getOption("tplyr2.IBMRounding") # IBM (half-away-from-zero) rounding vs R's banker's rounding fmt <- f_str("xx", "n") apply_formats(fmt, n = 2.5) old <- tplyr2_options(IBMRounding = TRUE) apply_formats(fmt, n = 2.5) do.call(options, old) # A misspelled name errors instead of setting a dead option try(tplyr2_options(IBMrounding = TRUE))# Inspect the current values tplyr2_options() # Setting returns the previous values, so the change can be undone old <- tplyr2_options(IBMRounding = TRUE) getOption("tplyr2.IBMRounding") do.call(options, old) getOption("tplyr2.IBMRounding") # IBM (half-away-from-zero) rounding vs R's banker's rounding fmt <- f_str("xx", "n") apply_formats(fmt, n = 2.5) old <- tplyr2_options(IBMRounding = TRUE) apply_formats(fmt, n = 2.5) do.call(options, old) # A misspelled name errors instead of setting a dead option try(tplyr2_options(IBMrounding = TRUE))