tplyr2 provides a set of session-level options that control how tables are built. These options let you configure behaviors that apply across all tables in a session – rounding rules, quantile algorithms, precision limits, custom summary functions, and scientific notation handling – without repeating yourself in every spec.
All tplyr2 options are managed through a single function:
tplyr2_options(). When called with no arguments, it returns
the current values of all options. When called with named arguments, it
sets those options. Under the hood, each option is stored as a standard
R option with the tplyr2. prefix, so they can also be set
with options() directly if you prefer.
To see what every option is currently set to, call
tplyr2_options() with no arguments:
tplyr2_options()
#> $tplyr2.IBMRounding
#> [1] FALSE
#>
#> $tplyr2.quantile_type
#> [1] 7
#>
#> $tplyr2.precision_cap
#> NULL
#>
#> $tplyr2.custom_summaries
#> NULL
#>
#> $tplyr2.scipen
#> [1] 9999The output is a named list. Options you have not changed show their
defaults: IBMRounding is FALSE,
quantile_type is 7, precision_cap
and custom_summaries are NULL, and
scipen is 9999.
R uses “banker’s rounding” (round half to even) by default. This means that 0.5 rounds to 0, 1.5 rounds to 2, 2.5 rounds to 2, and 3.5 rounds to 4. The rule minimizes cumulative bias in large datasets, but it does not match how SAS and many other statistical software packages round.
In clinical trial reporting, you often need to reproduce SAS output
exactly. SAS uses round-half-away-from-zero (sometimes called “IBM
rounding”), where 2.5 always rounds up to 3 and -2.5 always rounds down
to -3. tplyr2 supports this through the IBMRounding
option.
The formula used when IBM rounding is enabled is:
\[\text{round}(x) = \text{sign}(x) \cdot \left\lfloor |x| \cdot 10^d + 0.5 \right\rfloor / 10^d\]
Let’s see the difference in practice. We will build a simple descriptive statistics table and compare the mean under both rounding methods.
The difference only shows when a value lands exactly on a rounding boundary at the displayed precision, so the data below is chosen to do that: the two group means are exactly 2.5 and 4.5, displayed with no decimals.
demo_data <- data.frame(
TRT = rep(c("Drug", "Placebo"), each = 4),
VAL = c(1, 2, 3, 4, 3, 4, 5, 6) # means of exactly 2.5 and 4.5
)
spec <- tplyr_spec(
cols = "TRT",
layers = tplyr_layers(
group_desc("VAL",
settings = layer_settings(
format_strings = list(
"Mean" = f_str("xx", "mean")
)
)
)
)
)
# Default: banker's rounding
result_bankers <- tplyr_build(spec, demo_data)
kable(result_bankers[, !grepl("^ord", names(result_bankers))],
caption = "Banker's rounding (default)")| rowlabel1 | res1 | res2 |
|---|---|---|
| Mean | 2 | 4 |
# IBM rounding
tplyr2_options(IBMRounding = TRUE)
result_ibm <- tplyr_build(spec, demo_data)
kable(result_ibm[, !grepl("^ord", names(result_ibm))],
caption = "IBM rounding (round-half-away-from-zero)")| rowlabel1 | res1 | res2 |
|---|---|---|
| Mean | 3 | 5 |
Both cells change. Banker’s rounding sends 2.5 down to the even 2 and 4.5 down to the even 4; IBM rounding sends both up, to 3 and 5. A value that is not on a boundary rounds identically under either rule, which is why the difference is easy to miss until a reviewer reconciles your output against SAS.
The setting applies to every rounded value in a build — percentages,
means, quartiles, risk differences — so a table is internally consistent
either way. Negative values round away from zero as well, so
-2.5 becomes -3. See
vignette("format_strings") for how rounding interacts with
format string widths.
R’s quantile() function supports nine different
algorithms, numbered Type 1 through Type 9. The default in R is Type 7,
which uses linear interpolation and is widely used in statistics.
However, SAS’s PROC UNIVARIATE uses an algorithm closest to
R’s Type 3, which selects the nearest even order statistic.
In tplyr2, the quantile_type option controls which
algorithm is used for q1, q3, and
iqr in descriptive statistics layers.
spec <- tplyr_spec(
cols = "TRT01P",
layers = tplyr_layers(
group_desc("AGE",
settings = layer_settings(
format_strings = list(
"Q1, Q3" = f_str("xx.xx, xx.xx", "q1", "q3"),
"IQR" = f_str("xx.xx", "iqr")
)
)
)
)
)
# Type 7 (R default)
result_t7 <- tplyr_build(spec, tplyr_adsl)
kable(result_t7[, !grepl("^ord", names(result_t7))],
caption = "Quantile Type 7 (R default)")| rowlabel1 | res1 | res2 | res3 |
|---|---|---|---|
| Q1, Q3 | 69.25, 81.75 | 70.75, 80.00 | 71.00, 82.00 |
| IQR | 12.50 | 9.25 | 11.00 |
# Type 3 (SAS-compatible)
tplyr2_options(quantile_type = 3)
result_t3 <- tplyr_build(spec, tplyr_adsl)
kable(result_t3[, !grepl("^ord", names(result_t3))],
caption = "Quantile Type 3 (SAS-compatible)")| rowlabel1 | res1 | res2 | res3 |
|---|---|---|---|
| Q1, Q3 | 69.00, 81.00 | 70.00, 80.00 | 71.00, 82.00 |
| IQR | 12.00 | 10.00 | 11.00 |
The differences between Type 7 and Type 3 tend to be small for large
datasets, but they can be meaningful for small groups or when exact SAS
reconciliation is required. If your statistical analysis plan (SAP)
specifies SAS-compatible quantiles, set quantile_type = 3
at the start of your session.
When using tplyr2’s auto-precision system (format strings with
a or A characters), the integer and decimal
widths are determined dynamically from the data. This is useful when the
same format must accommodate variables with different scales, but
sometimes the data can drive widths to impractical extremes – a variable
recorded to six decimal places would produce very wide columns.
The precision_cap option lets you set a global ceiling
on auto-precision widths. It takes a named numeric vector with
int and/or dec components.
# Without a cap, precision is driven entirely by the data
spec <- tplyr_spec(
cols = "TRT01P",
layers = tplyr_layers(
group_desc("WEIGHTBL",
by = "Weight (kg)",
settings = layer_settings(
format_strings = list(
"Mean (SD)" = f_str("a+1.a+1 (a+2.a+2)", "mean", "sd")
)
)
)
)
)
result_nocap <- tplyr_build(spec, tplyr_adsl)
kable(result_nocap[, !grepl("^ord", names(result_nocap))],
caption = "Auto-precision, no cap")| rowlabel1 | rowlabel2 | res1 | res2 | res3 |
|---|---|---|---|---|
| Weight (kg) | Mean (SD) | 62.76 ( 12.772) | 70.00 ( 14.653) | 67.28 ( 14.124) |
# Apply a global cap: max 3 integer digits, max 1 decimal digit
tplyr2_options(precision_cap = c(int = 3, dec = 1))
result_capped <- tplyr_build(spec, tplyr_adsl)
kable(result_capped[, !grepl("^ord", names(result_capped))],
caption = "Auto-precision, capped at int=3, dec=1")| rowlabel1 | rowlabel2 | res1 | res2 | res3 |
|---|---|---|---|---|
| Weight (kg) | Mean (SD) | 62.76 ( 12.772) | 70.00 ( 14.653) | 67.28 ( 14.124) |
Note that the precision_cap option serves as a global
default. If a specific layer provides its own precision_cap
in layer_settings(), the layer-level cap takes priority.
This gives you a safety net at the session level while allowing
individual layers to override when needed.
The cap applies to the width measured from the data, before
any +N offset in the format string is added – so
dec = 1 does not guarantee one decimal in the output.
vignette("precision_alignment") covers the auto-precision
system in full, including precision_by,
precision_on, and precision_data.
Descriptive statistics layers in tplyr2 come with a set of built-in
summaries (n, mean, sd,
median, var, min,
max, iqr, q1, q3,
missing). When your study requires additional statistics
across many tables – geometric means, coefficients of variation, or any
other derived measure – you can register them at the session level so
they are available everywhere.
# Register session-level custom summaries
tplyr2_options(
custom_summaries = list(
geo_mean = quote(exp(mean(log(.var[.var > 0]), na.rm = TRUE))),
cv = quote(sd(.var, na.rm = TRUE) / mean(.var, na.rm = TRUE) * 100)
)
)
spec <- 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"),
"Geometric Mean" = f_str("xx.xx", "geo_mean"),
"CV (%)" = f_str("xx.x", "cv")
)
)
)
)
)
result <- tplyr_build(spec, tplyr_adsl)
kable(result[, !grepl("^ord", names(result))])| rowlabel1 | res1 | res2 | res3 |
|---|---|---|---|
| n | 86 | 84 | 84 |
| Mean (SD) | 75.2 ( 8.59) | 74.4 ( 7.89) | 75.7 ( 8.29) |
| Geometric Mean | 74.70 | 73.94 | 75.18 |
| CV (%) | 11.4 | 10.6 | 11.0 |
A few things to keep in mind about custom summaries:
.var as a placeholder for the
target variable’s values within each group. At build time,
.var is replaced with the actual numeric vector.quote() to delay
evaluation.log() on non-positive values), tplyr2 catches the
error and returns NA_real_ rather than stopping the
build.custom_summaries in layer_settings(). This
means you can set study-wide defaults and still customize individual
layers.mean), in which case it replaces the built-in
calculation.R may format very large or very small numbers in scientific notation
(e.g., 1e+05 instead of 100000). This is
controlled by R’s scipen option, which sets a penalty
against scientific notation – higher values make it less likely. During
tplyr_build(), tplyr2 temporarily overrides
scipen to prevent scientific notation from appearing in
formatted output.
The default value of 9999 effectively guarantees that
scientific notation will never appear in your tables. In most cases, you
will not need to change this. However, if you have a specific reason to
allow scientific notation (or if you are debugging formatting), you can
adjust it:
The scipen override is applied only for the duration of
tplyr_build() and is automatically restored to its previous
value when the build completes. This means it will not affect other code
in your session.
Clinical datasets usually carry a SAS-style label
attribute on each column (the descriptive text like “Age” or “Planned
Treatment”). The helper get_data_labels() pulls those
labels off a data frame as a named character vector, returning
NA for any column that has no label. It is handy for
building column headers or annotating output.
To return all options to their defaults, set each one explicitly:
tplyr2_options(
IBMRounding = FALSE,
quantile_type = 7L,
precision_cap = NULL,
custom_summaries = NULL,
scipen = 9999L
)
# Verify
tplyr2_options()
#> $tplyr2.IBMRounding
#> [1] FALSE
#>
#> $tplyr2.quantile_type
#> [1] 7
#>
#> $tplyr2.precision_cap
#> NULL
#>
#> $tplyr2.custom_summaries
#> NULL
#>
#> $tplyr2.scipen
#> [1] 9999Because tplyr2 options are stored as standard R options (with the
tplyr2. prefix), they reset automatically when you start a
new R session. If you want your options to persist, place the
tplyr2_options() call in your project’s
.Rprofile or at the top of your analysis script.
vignette("desc") – descriptive statistics, where
quantile_type and custom_summaries apply.vignette("precision_alignment") – auto-precision, which
precision_cap bounds.vignette("format_strings") – the f_str()
system that IBM rounding feeds into.