Skip to contents

pivot_across() is tabular's input-side helper: it consumes a long Analysis Results Data (ARD) data frame (typically produced by cards::ard_stack() or cards::ard_stack_hierarchical()) and returns a wide display data.frame ready to pass to tabular().

Usage

pivot_across(
  data,
  statistic = list(continuous = "{mean} ({sd})", categorical = "{n} ({p}%)"),
  column = NULL,
  row_group = NULL,
  label = NULL,
  overall = "Total",
  decimals = NULL,
  fmt = NULL,
  aux = NULL
)

Arguments

data

Long ARD input data. <data.frame>: required. At minimum needs stat_name and stat. Cards-style group columns (group1, group1_level, ...) and variable / variable_level are auto-detected. Tibbles / card objects / arrow tables are coerced via as.data.frame().

statistic

Format spec for cell composition. <character(1) | named list>: required. Combines one or more ARD stats into one display cell. Three accepted forms — each illustrated below. Inside a format string, {stat_name} substitutes that stat's value from the ARD (for example, "{n} ({p}%)" interpolates the n and p stats into a "53 (62%)" cell). The lookup order when a value is needed for a variable is: per-variable -> per-context -> default -> the literal "{n}".

Form 1: single string

One format string applied to every variable regardless of context. Use when your ARD is homogeneous (e.g. all categorical).

# Every variable rendered as "n (p%)" — categorical-only slice.
cat_only <- cdisc_saf_demo_ard[cdisc_saf_demo_ard$context == "categorical", ]
pivot_across(
  cat_only,
  statistic = "{n} ({p}%)"
)

Form 2: named list by context

Different formats per context. This is the typical clinical-table form because demographics mix continuous and categorical variables.

The list names must match the values in the ARD's context column verbatim. Which strings appear there depends on how the ARD was built:

  • cards::ard_continuous() / ard_categorical() emit "continuous" / "categorical".

  • cards::ard_summary() / ard_tabulate() emit "summary" / "tabulate".

So an ARD assembled with ard_stack(ard_summary(...), ard_tabulate(...)) is keyed summary / tabulate, not continuous / categorical. Inspect unique(ard$context) when unsure.

# AGE (continuous) -> "75.2 (8.59)"; SEX (categorical) -> "53 (62%)"
pivot_across(
  cdisc_saf_demo_ard,
  statistic = list(
    continuous  = "{mean} ({sd})",
    categorical = "{n} ({p}%)"
  )
)

Form 3: named list by variable

Override on a per-variable basis; fall back to default or context. Use when one variable needs a custom format.

# AGE shows just the mean; SEX / RACE keep the categorical default.
pivot_across(
  cdisc_saf_demo_ard,
  statistic = list(
    AGE         = "{mean}",
    categorical = "{n} ({p}%)",
    default     = "{mean} ({sd})"
  )
)

Multi-row continuous spec

Any single entry can itself be a named character vector — each element becomes one display row, with the name as the row label. Use for N / Mean (SD) / Median / Min, Max-style blocks.

pivot_across(
  cdisc_saf_demo_ard,
  statistic = list(
    continuous = c(
      N           = "{N}",
      "Mean (SD)" = "{mean} ({sd})",
      Median      = "{median}",
      "Min, Max"  = "{min}, {max}"
    ),
    categorical = "{n} ({p}%)"
  )
)

column

What runs across the top of the table. <character | NULL>: default NULL. A single grouping variable whose unique values become arm columns (NULL auto-detects from group1, or picks the single non-standard column of renamed input). Two reserved tokens turn the analysis variable into a column band:

  • c(".variable", "<arm>") — each variable becomes a band of arm columns; the statistic entries stack as rows. The cells are combined strings (e.g. "323.9 (106)"). Emitted columns are named "<variable>..<arm>".

  • c(".variable", ".stat") — each variable becomes a band whose statistic entries become their own columns (the landscape "value and change" shell); the arm drops to a leading row stub. Emitted columns are named "<variable>..<stat-entry>".

Anything present in the ARD but not named in column (and not in row_group) stacks as rows. Per-variable statistic / decimals resolve inside each band, so bands may carry different (even different-length) stat lists; ragged bands pad with NA.

Tip: you reference the emitted column names verbatim in a manual headers() call to build the band spanners; pivot_across does not build spanners itself.

row_group

Second, non-column grouping dimension. <character(1) | NULL>: default NULL. Names the non-arm group variable of a two-variable .by (e.g. SEX in ard_stack(.by = c(ARM, SEX))). It widens into a leading row column (not a pivoted arm column), so the result composes with subgroup(by = ...) or group_rows() downstream.

Why it is required. cards encodes a crossing factor and a SOC/PT hierarchy identically (the second group variable appears in variable on its by-marginal rows), so the two cannot be told apart automatically. Naming row_group declares "this is a crossing factor": the by-marginal rows are dropped and the flat path is used. Leave it NULL for a genuine hierarchy.

Restriction: Must name a second grouping variable present in the ARD and must differ from column.

label

Variable-name to display-label map. <character> | NULL: default NULL. Named character vector mapping variable names to display labels (e.g. c(AGE = "Age (years)", SEX = "Sex")). Applies to variable, soc, and label columns of the output. NULL leaves the upstream variable names verbatim.

Renaming the hierarchical "overall" row. A cards::ard_stack_hierarchical(overall = TRUE) ARD carries an internal ..ard_hierarchical_overall.. sentinel for the grand-total ("any event") row. It is relabelled to "Overall" by default; map the sentinel key to override, e.g. label = c("..ard_hierarchical_overall.." = "TOTAL SUBJECTS WITH AN EVENT"). The raw sentinel never reaches the output at any hierarchy depth.

overall

Column name for NA-arm (overall / total) rows. <character(1) | NULL>: default "Total". Pass NULL to drop overall rows entirely (per-arm only output).

Requirement: this relabels pooled rows the ARD already carries — the NA-arm rows cards emits from cards::ard_stack_hierarchical(overall = TRUE) or an ard_*(.overall = TRUE). It does not synthesize a total: cards re-runs the calculation with the by variable removed, so the pooled n / N / p stay internally consistent. With no such rows in the input there is no overall column to label.

Note: if a study arm is literally named the same as overall (default "Total"), that arm and the pooled rows collide under one label and the pivot warns. Pass a distinct overall = or rename the arm upstream.

decimals

Per-stat decimal precision. <named integer | named list>: default c()“. Accepts three forms:

  • named integer vector — global per-stat overrides (c(mean = 1, sd = 2, p = 0)).

  • named list keyed by variable — per-variable plus .default (list(AGE = c(mean = 2), .default = c(p = 1))).

  • named list keyed by row_group value — per-group precision in one call (list(SYSBP = c(mean = 0, sd = 1), WEIGHT = c(mean = 1), .default = c(mean = 1, sd = 2))). Each entry is a per-token spec (a named numeric vector, or a bare scalar applied to every token).

Built-in defaults apply when none sets a stat.

Interaction: a list decimals is read as per-row_group only when row_group is set and every key (apart from .default) is one of its levels; otherwise it stays per-variable. Within the matched group the token falls back group token, then the per-group .default token, then the built-in default. A group present in the data but absent from the list (and NA / ungrouped rows) uses .default. If row_group is NULL but the keys match no variable, pivot_across() errors and asks for a row_group.

fmt

Per-stat custom formatter functions. <named list of function>: default list()“. Each function takes a numeric value and returns a character string; overrides built-ins and decimals for that stat. Useful for p-value styling and other domain-specific formatting.

aux

Auxiliary comparison columns from a second ARD. <named list | NULL>: default NULL. Each entry is a between-arm statistic (difference, hazard ratio, p-value, ...) computed in its own ARD and bound on as a trailing column, aligned 1:1 on the row_group key. The entry name is the column name; reference it in a manual headers() call. Each entry is a list:

  • ard — the auxiliary ARD (required), one row per row_group.

  • statistic — its format string (e.g. "{estimate} ({conf.low}, {conf.high})"); defaults to "{n}".

  • decimals / fmt — optional, as for the main pivot.

Entries append left to right. One entry is one column; for several comparison columns (estimate then p-value) pass several entries.

Requirement: needs a row_group; the auxiliary rows align to the main table on it, which must be unique 1:1 on both sides (a many-to-many key would silently fabricate rows, so it aborts).

# p-value formatter: render below-threshold values as "<0.001".
fmt = list(
  p.value = function(x) {
    ifelse(x < 0.001, "<0.001", sprintf("%.3f", x))
  }
)

Value

A wide data.frame ready for tabular(). Schema:

  • variable — variable name (or label after label = ...).

  • stat_label — display-row label.

  • One column per arm level (named after the group1_level values or the renamed arm column).

  • Total (or whatever overall is set to) when applicable.

  • A leading column named after row_group when set (the second grouping dimension).

  • Hierarchical ARD adds soc, label, row_type instead of variable.

Pass the result straight into tabular() to start the render pipeline.

Details

tabular's package boundary is display-only: pre-summarised data in, rendered file out. pivot_across() is the canonical bridge between the cards aggregation backend and that boundary. It does not aggregate — it pivots arms to columns, interpolates per-cell display strings from the stat values, and applies decimal precision. Filtering, weighting, and aggregation happen upstream in cards or your own data-prep step.

Key statistic by the ARD context

statistic (and fmt) are matched against the ARD's context column verbatim, and that value differs per generating function. Keying by the wrong name silently drops the format. Inspect unique(ard$context) first and key to match (or pass a single format string / default = to cover everything). When an explicitly-supplied statistic matches no context at all, pivot_across() warns rather than silently emitting {n}.

Generating functioncontext to key on
cards::ard_summary()summary
cards::ard_tabulate()tabulate
cards::ard_continuous()continuous
cards::ard_categorical()categorical
cards::ard_stack_hierarchical()tabulate + hierarchical
cardx::ard_categorical_ci()proportion_ci
cardx::ard_continuous_ci()continuous_ci

Zero-suppression (always-on default)

A row whose n value equals zero renders the whole cell as the bare n value instead of fully interpolating the format string. For a categorical level with n = 0, the cell shows "0", not "0 (0.0%)". This is clinical convention — empty cells should read as a single zero, not advertise a meaningless rate.

How the default fires (chain of events). During cell assembly, before format-string interpolation, the engine checks the row's n stat. If it is zero, the engine short-circuits and returns the formatted n value ("0") as the entire cell — {p} is never substituted, so the (0.0%) half of the format string is dropped.

How to opt out: supply a custom fmt$n. Setting any function under fmt$n is the engine's signal that the user owns the n rendering. The short-circuit is disabled for the whole table; for every row the full format string interpolates, so {n} becomes your formatter's output and {p} becomes the standard percentage. For n = 0, that's "0 (0.0%)".

# Force "0 (0.0%)" for n = 0 rows by attaching a custom n formatter.
# The body of fmt$n can be the default integer rendering — its
# presence alone is what disables the zero-suppression branch.
pivot_across(
  cdisc_saf_demo_ard,
  statistic = list(
    continuous  = "{mean} ({sd})",
    categorical = "{n} ({p}%)"
  ),
  fmt = list(n = function(x) sprintf("%d", as.integer(x)))
)

Pharma rounding (always-on default)

A percentage that would otherwise round to 0 (when the value is positive but smaller than the chosen precision) renders as <0.1; one that would round to 100 (positive but smaller than 100) renders as >99.9. The threshold is precision-aware: decimals = c(p = 2) produces <0.01 / >99.99. This matches the pharma convention of never claiming exactly 0% or 100% when at least one subject contributed.

Override per-stat via fmt:

# Show exact rounded percentages even at the extremes
pivot_across(
  data,
  statistic = "{n} ({p}%)",
  decimals  = c(p = 1),
  fmt = list(p = function(x) sprintf("%.1f", x * 100))
)

Your fmt$p receives the raw stat value (a proportion between 0 and 1) and returns the displayed string. The pharma-threshold branch only fires inside the built-in p formatter and the decimals-driven path, so any custom fmt$p bypasses it.

See also

Pipeline entry consumer: tabular() — wraps the wide data frame this helper returns.

Downstream spec-build verbs: cols() / col_spec(), headers(), sort_rows(), style(), paginate(), preset().

Terminal verbs: emit(), as_grid().

Examples

# ---- Example 1: Demographics — long ARD to rendered spec ----
#
# Full pipeline from a `cards::ard_stack()`-style long ARD to a
# sorted `tabular_spec`. The multi-row continuous block (N /
# Mean (SD) / Median / Min, Max) sits above each categorical
# block; decimals are set per-stat (mean 1, sd 2, p 1) to match
# the CDISC convention.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)

cdisc_saf_demo_ard |>
  pivot_across(
    statistic = list(
      continuous = c(
        N           = "{N}",
        "Mean (SD)" = "{mean} ({sd})",
        Median      = "{median}",
        "Min, Max"  = "{min}, {max}"
      ),
      categorical = "{n} ({p}%)"
    ),
    decimals = c(mean = 1, sd = 2, p = 1, median = 1, min = 0, max = 0),
    label    = c(AGE = "Age (years)", SEX = "Sex", RACE = "Race")
  ) |>
  tabular(
    titles = c(
      "Table 14.1.1",
      "Demographics and Baseline Characteristics",
      "Safety Population"
    ),
    footnotes = "Percentages based on N per treatment group."
  ) |>
  cols(
    variable   = col_spec(label = "Parameter"),
    stat_label = col_spec(label = "Statistic"),
    Placebo    = col_spec(
      label = "Placebo\nN={n['placebo']}",
      align = "decimal"
    ),
    `Xanomeline Low Dose` = col_spec(
      label = "Drug 50\nN={n['drug_50']}",
      align = "decimal"
    ),
    `Xanomeline High Dose` = col_spec(
      label = "Drug 100\nN={n['drug_100']}",
      align = "decimal"
    ),
    Total = col_spec(
      label = "Total\nN={n['Total']}",
      align = "decimal"
    )
  )

 

Table 14.1.1

Demographics and Baseline Characteristics

Safety Population

 

ParameterStatisticPlacebo
N=86
Drug 100
N=72
Drug 50
N=96
Total
N=254
Age (years)N 86           72           96          254          
Age (years)Mean (SD) 75.2 (8.59)  73.8 (7.94)  76.0 (8.11)  75.1 (8.25) 
Age (years)Median 76.0         75.5         78.0         77.0        
Age (years)Min, Max 52  , 89     56  , 88     51  , 88     51  , 89    
WEIGHTN 86           72           95          253          
WEIGHTMean (SD) 62.8 (12.77) 69.5 (14.35) 68.0 (14.50) 66.6 (14.13)
WEIGHTMedian 60.6         69.0         66.7         66.7        
WEIGHTMin, Max 34  , 86     44  , 108    42  , 106    34  , 108   
HEIGHTN 86           72           96          254          
HEIGHTMean (SD)162.6 (11.52)165.9 (10.28)163.7 (10.30)163.9 (10.76)
HEIGHTMedian162.6        165.1        162.6        162.8        
HEIGHTMin, Max137  , 185   146  , 190   136  , 196   136  , 196   
BMIN 86           72           95          253          
BMIMean (SD) 23.6 (3.67)  25.2 (3.97)  25.2 (4.40)  24.7 (4.09) 
BMIMedian 23.4         24.8         24.8         24.2        
BMIMin, Max 15  , 33     14  , 35     15  , 40     14  , 40    
AGEGR118-64 14   (16.3%) 11   (15.3%)  8   ( 8.3%) 33   (13.0%)
AGEGR1>64 72   (83.7%) 61   (84.7%) 88   (91.7%)221   (87.0%)
SexF 53   (61.6%) 35   (48.6%) 55   (57.3%)143   (56.3%)
SexM 33   (38.4%) 37   (51.4%) 41   (42.7%)111   (43.7%)
RaceWHITE 78   (90.7%) 62   (86.1%) 90   (93.8%)230   (90.6%)
RaceBLACK OR AFRICAN AMERICAN  8   ( 9.3%)  9   (12.5%)  6   ( 6.2%) 23   ( 9.1%)
RaceASIAN  0            0            0            0          
RaceAMERICAN INDIAN OR ALASKA NATIVE  0            1   ( 1.4%)  0            1   ( 0.4%)
ETHNICHISPANIC OR LATINO  3   ( 3.5%)  3   ( 4.2%)  6   ( 6.2%) 12   ( 4.7%)
ETHNICNOT HISPANIC OR LATINO 83   (96.5%) 69   (95.8%) 90   (93.8%)242   (95.3%)
ETHNICNOT REPORTED  0            0            0            0          
BMI_CATUnderweight (<18.5)  3   ( 3.5%)  1   ( 1.4%)  4   ( 4.2%)  8   ( 3.2%)
BMI_CATNormal (18.5-24.9) 57   (66.3%) 39   (54.2%) 46   (48.4%)142   (56.1%)
BMI_CATOverweight (25-29.9) 20   (23.3%) 23   (31.9%) 32   (33.7%) 75   (29.6%)
BMI_CATObese (>=30)  6   ( 7.0%)  9   (12.5%) 13   (13.7%) 28   (11.1%)

Percentages based on N per treatment group.

# ---- Example 2: Hierarchical SOC/PT AE table ---- # # Hierarchical `cards::ard_stack_hierarchical()` output threaded # through `pivot_across()`. The hierarchical ARD emits a # (soc, label, row_type) triple plus one stat row per (arm, SOC, PT); # `pivot_across()` folds the arm dimension to columns and preserves # the hierarchy markers. Derive `indent_level` from `row_type` so # `col_spec(indent = "indent_level")` drives the SOC -> PT # indent on the `label` column. wide <- cdisc_saf_aesocpt_ard |> pivot_across(statistic = "{n} ({p}%)") wide$indent_level <- as.integer(wide$row_type == "pt") tabular( wide, titles = c( "Table 14.3.1", "Adverse Events by System Organ Class and Preferred Term", "Safety Population" ), footnotes = c( "Subjects are counted once per SOC and once per PT.", "Percentages based on N per treatment group." ) ) |> cols( label = col_spec(label = "SOC / PT", indent = "indent_level"), soc = col_spec(visible = FALSE), row_type = col_spec(visible = FALSE), Placebo = col_spec( label = "Placebo\nN={n['placebo']}", align = "decimal" ), `Xanomeline Low Dose` = col_spec( label = "Drug 50\nN={n['drug_50']}", align = "decimal" ), `Xanomeline High Dose` = col_spec( label = "Drug 100\nN={n['drug_100']}", align = "decimal" ) )

 

Table 14.3.1

Adverse Events by System Organ Class and Preferred Term

Safety Population

 

SOC / PTPlacebo
N=86
Drug 100
N=72
Drug 50
N=96
Overall52 (60%)66 (92%)81 (84%)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS19 (22%)35 (49%)36 (38%)
PRURITUS 8 ( 9%)25 (35%)21 (22%)
ERYTHEMA 8 ( 9%)14 (19%)14 (15%)
RASH 5 ( 6%) 8 (11%)13 (14%)
HYPERHIDROSIS 2 ( 2%) 8 (11%) 4 ( 4%)
SKIN IRRITATION 3 ( 3%) 5 ( 7%) 6 ( 6%)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS15 (17%)30 (42%)36 (38%)
APPLICATION SITE PRURITUS 6 ( 7%)21 (29%)23 (24%)
APPLICATION SITE ERYTHEMA 3 ( 3%)14 (19%)13 (14%)
APPLICATION SITE DERMATITIS 5 ( 6%) 7 (10%) 9 ( 9%)
APPLICATION SITE IRRITATION 3 ( 3%) 9 (12%) 9 ( 9%)
APPLICATION SITE VESICLES 1 ( 1%) 5 ( 7%) 5 ( 5%)
GASTROINTESTINAL DISORDERS13 (15%)17 (24%)12 (12%)
DIARRHOEA 9 (10%) 3 ( 4%) 5 ( 5%)
VOMITING 3 ( 3%) 6 ( 8%) 4 ( 4%)
NAUSEA 3 ( 3%) 6 ( 8%) 3 ( 3%)
ABDOMINAL PAIN 1 ( 1%) 1 ( 1%) 3 ( 3%)
SALIVARY HYPERSECRETION 0       4 ( 6%) 0      
NERVOUS SYSTEM DISORDERS 6 ( 7%)17 (24%)18 (19%)
DIZZINESS 2 ( 2%)10 (14%) 9 ( 9%)
HEADACHE 3 ( 3%) 5 ( 7%) 3 ( 3%)
SYNCOPE 0       2 ( 3%) 5 ( 5%)
SOMNOLENCE 2 ( 2%) 1 ( 1%) 3 ( 3%)
TRANSIENT ISCHAEMIC ATTACK 0       1 ( 1%) 2 ( 2%)
CARDIAC DISORDERS 7 ( 8%)14 (19%)12 (12%)
SINUS BRADYCARDIA 2 ( 2%) 8 (11%) 7 ( 7%)
MYOCARDIAL INFARCTION 4 ( 5%) 4 ( 6%) 2 ( 2%)
ATRIAL FIBRILLATION 1 ( 1%) 2 ( 3%) 2 ( 2%)
SUPRAVENTRICULAR EXTRASYSTOLES 1 ( 1%) 1 ( 1%) 1 ( 1%)
VENTRICULAR EXTRASYSTOLES 0       1 ( 1%) 2 ( 2%)
INFECTIONS AND INFESTATIONS12 (14%)11 (15%) 6 ( 6%)
NASOPHARYNGITIS 2 ( 2%) 6 ( 8%) 4 ( 4%)
UPPER RESPIRATORY TRACT INFECTION 6 ( 7%) 3 ( 4%) 1 ( 1%)
INFLUENZA 1 ( 1%) 1 ( 1%) 1 ( 1%)
URINARY TRACT INFECTION 2 ( 2%) 1 ( 1%) 0      
CYSTITIS 1 ( 1%) 1 ( 1%) 0      
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 5 ( 6%) 9 (12%) 8 ( 8%)
COUGH 1 ( 1%) 5 ( 7%) 5 ( 5%)
NASAL CONGESTION 3 ( 3%) 3 ( 4%) 1 ( 1%)
DYSPNOEA 1 ( 1%) 1 ( 1%) 1 ( 1%)
EPISTAXIS 0       2 ( 3%) 1 ( 1%)
PHARYNGOLARYNGEAL PAIN 0       1 ( 1%) 1 ( 1%)
PSYCHIATRIC DISORDERS 7 ( 8%) 3 ( 4%) 9 ( 9%)
CONFUSIONAL STATE 2 ( 2%) 1 ( 1%) 3 ( 3%)
AGITATION 2 ( 2%) 0       3 ( 3%)
INSOMNIA 2 ( 2%) 2 ( 3%) 0      
ANXIETY 0       0       3 ( 3%)
DELUSION 1 ( 1%) 1 ( 1%) 0      
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS 3 ( 3%) 5 ( 7%) 6 ( 6%)
BACK PAIN 1 ( 1%) 3 ( 4%) 1 ( 1%)
ARTHRALGIA 1 ( 1%) 1 ( 1%) 2 ( 2%)
SHOULDER PAIN 1 ( 1%) 0       2 ( 2%)
MUSCLE SPASMS 0       1 ( 1%) 1 ( 1%)
ARTHRITIS 0       1 ( 1%) 0      
INVESTIGATIONS 5 ( 6%) 3 ( 4%) 4 ( 4%)
ELECTROCARDIOGRAM ST SEGMENT DEPRESSION 4 ( 5%) 0       1 ( 1%)
ELECTROCARDIOGRAM T WAVE INVERSION 2 ( 2%) 1 ( 1%) 1 ( 1%)
BLOOD GLUCOSE INCREASED 0       1 ( 1%) 1 ( 1%)
ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED 1 ( 1%) 0       1 ( 1%)
BIOPSY 0       1 ( 1%) 0      

Subjects are counted once per SOC and once per PT.

Percentages based on N per treatment group.

# ---- Example 3: Hierarchical ARD (SOC / PT) ---- # # `cdisc_saf_aesocpt_ard` carries an `ard_stack_hierarchical` shape with # two grouping variables (AEBODSYS / AEDECOD). `pivot_across()` # recognises the hierarchical structure and emits dedicated `soc`, # `label`, and `row_type` columns so the SOC -> PT nesting survives # the pivot. The result is ready for `tabular()` plus `sort_rows()`. head(cdisc_saf_aesocpt_ard, 3) #> # A tibble: 3 × 10 #> group1 group1_level group2 group2_level variable variable_level #> <chr> <list> <chr> <list> <chr> <list> #> 1 NA <NULL> NA <NULL> TRT01A <chr [1]> #> 2 NA <NULL> NA <NULL> TRT01A <chr [1]> #> 3 NA <NULL> NA <NULL> TRT01A <chr [1]> #> # ℹ 4 more variables: context <chr>, stat_name <chr>, #> # stat_label <chr>, stat <list> wide <- cdisc_saf_aesocpt_ard |> pivot_across(statistic = "{n} ({p}%)") head(wide, 3) #> soc #> 1 Overall #> 2 SKIN AND SUBCUTANEOUS TISSUE DISORDERS #> 3 SKIN AND SUBCUTANEOUS TISSUE DISORDERS #> label row_type Placebo #> 1 Overall overall 52 (60%) #> 2 SKIN AND SUBCUTANEOUS TISSUE DISORDERS soc 19 (22%) #> 3 PRURITUS pt 8 (9%) #> Xanomeline High Dose Xanomeline Low Dose #> 1 66 (92%) 81 (84%) #> 2 35 (49%) 36 (38%) #> 3 25 (35%) 21 (22%) # ---- Example 4: Multi-row continuous spec + label re-labelling ---- # # `statistic = c(<label> = <template>, ...)` produces one display # row per named entry — the canonical "N / Mean (SD) / Median / # Min, Max" block for continuous variables. `label = c(...)` # renames the variable headings emitted into the wide output. cdisc_saf_demo_ard |> pivot_across( statistic = list( continuous = c( N = "{N}", "Mean (SD)" = "{mean} ({sd})", Median = "{median}", "Q1, Q3" = "{p25}, {p75}", "Min, Max" = "{min}, {max}" ), categorical = "{n} ({p}%)" ), label = c( AGE = "Age (years)", WEIGHT = "Weight (kg)", HEIGHT = "Height (cm)", BMI = "BMI (kg/m^2)" ) ) #> variable stat_label Placebo #> 1 Age (years) N 86 #> 2 Age (years) Mean (SD) 75.2 (8.59) #> 3 Age (years) Median 76.0 #> 4 Age (years) Q1, Q3 69.0, 82.0 #> 5 Age (years) Min, Max 52.0, 89.0 #> 6 Weight (kg) N 86 #> 7 Weight (kg) Mean (SD) 62.8 (12.77) #> 8 Weight (kg) Median 60.6 #> 9 Weight (kg) Q1, Q3 53.5, 74.4 #> 10 Weight (kg) Min, Max 34.0, 86.2 #> 11 Height (cm) N 86 #> 12 Height (cm) Mean (SD) 162.6 (11.52) #> 13 Height (cm) Median 162.6 #> 14 Height (cm) Q1, Q3 153.7, 171.4 #> 15 Height (cm) Min, Max 137.2, 185.4 #> 16 BMI (kg/m^2) N 86 #> 17 BMI (kg/m^2) Mean (SD) 23.6 (3.67) #> 18 BMI (kg/m^2) Median 23.4 #> 19 BMI (kg/m^2) Q1, Q3 21.2, 25.7 #> 20 BMI (kg/m^2) Min, Max 15.1, 33.3 #> 21 AGEGR1 18-64 14 (16%) #> 22 AGEGR1 >64 72 (84%) #> 23 SEX F 53 (62%) #> 24 SEX M 33 (38%) #> 25 RACE WHITE 78 (91%) #> 26 RACE BLACK OR AFRICAN AMERICAN 8 (9%) #> 27 RACE ASIAN 0 #> 28 RACE AMERICAN INDIAN OR ALASKA NATIVE 0 #> 29 ETHNIC HISPANIC OR LATINO 3 (3%) #> 30 ETHNIC NOT HISPANIC OR LATINO 83 (97%) #> 31 ETHNIC NOT REPORTED 0 #> 32 BMI_CAT Underweight (<18.5) 3 (3%) #> 33 BMI_CAT Normal (18.5-24.9) 57 (66%) #> 34 BMI_CAT Overweight (25-29.9) 20 (23%) #> 35 BMI_CAT Obese (>=30) 6 (7%) #> Xanomeline High Dose Xanomeline Low Dose Total #> 1 72 96 254 #> 2 73.8 (7.94) 76.0 (8.11) 75.1 (8.25) #> 3 75.5 78.0 77.0 #> 4 70.0, 79.0 71.0, 82.0 70.0, 81.0 #> 5 56.0, 88.0 51.0, 88.0 51.0, 89.0 #> 6 72 95 253 #> 7 69.5 (14.35) 68.0 (14.50) 66.6 (14.13) #> 8 69.0 66.7 66.7 #> 9 56.7, 80.3 55.8, 78.5 55.3, 77.1 #> 10 44.5, 108.0 41.7, 106.1 34.0, 108.0 #> 11 72 96 254 #> 12 165.9 (10.28) 163.7 (10.30) 163.9 (10.76) #> 13 165.1 162.6 162.8 #> 14 157.5, 172.9 157.5, 170.2 156.2, 171.4 #> 15 146.1, 190.5 135.9, 195.6 135.9, 195.6 #> 16 72 95 253 #> 17 25.2 (3.97) 25.2 (4.40) 24.7 (4.09) #> 18 24.8 24.8 24.2 #> 19 22.7, 27.6 22.2, 28.3 21.9, 27.3 #> 20 13.7, 34.6 15.3, 40.2 13.7, 40.2 #> 21 11 (15%) 8 (8%) 33 (13%) #> 22 61 (85%) 88 (92%) 221 (87%) #> 23 35 (49%) 55 (57%) 143 (56%) #> 24 37 (51%) 41 (43%) 111 (44%) #> 25 62 (86%) 90 (94%) 230 (91%) #> 26 9 (12%) 6 (6%) 23 (9%) #> 27 0 0 0 #> 28 1 (1%) 0 1 (0%) #> 29 3 (4%) 6 (6%) 12 (5%) #> 30 69 (96%) 90 (94%) 242 (95%) #> 31 0 0 0 #> 32 1 (1%) 4 (4%) 8 (3%) #> 33 39 (54%) 46 (48%) 142 (56%) #> 34 23 (32%) 32 (34%) 75 (30%) #> 35 9 (12%) 13 (14%) 28 (11%) # ---- Example 5: ARD keyed by summary / tabulate contexts ---- # # The `statistic` list names must match the ARD's `context` column # verbatim. `cards::ard_summary()` / `ard_tabulate()` emit `"summary"` / # `"tabulate"` (not the `"continuous"` / `"categorical"` of # `ard_continuous()` / `ard_categorical()`), so a list keyed # `continuous`/`categorical` would silently match nothing. Always check # `unique(ard$context)` first. Here the bundled `cdisc_saf_demo_ard` is # relabelled to mimic `ard_summary()` + `ard_tabulate()` output; the # by-variable's own row drops automatically and both the summary and # the tabulate variables survive. card_st <- cdisc_saf_demo_ard card_st$context[card_st$context == "continuous"] <- "summary" card_st$context[card_st$context == "categorical"] <- "tabulate" pivot_across( card_st, statistic = list( summary = "{mean} ({sd})", tabulate = "{n} ({p}%)" ) ) #> variable stat_label Placebo #> 1 AGE AGE 75.2 (8.59) #> 2 WEIGHT WEIGHT 62.8 (12.77) #> 3 HEIGHT HEIGHT 162.6 (11.52) #> 4 BMI BMI 23.6 (3.67) #> 5 AGEGR1 18-64 14 (16%) #> 6 AGEGR1 >64 72 (84%) #> 7 SEX F 53 (62%) #> 8 SEX M 33 (38%) #> 9 RACE WHITE 78 (91%) #> 10 RACE BLACK OR AFRICAN AMERICAN 8 (9%) #> 11 RACE ASIAN 0 #> 12 RACE AMERICAN INDIAN OR ALASKA NATIVE 0 #> 13 ETHNIC HISPANIC OR LATINO 3 (3%) #> 14 ETHNIC NOT HISPANIC OR LATINO 83 (97%) #> 15 ETHNIC NOT REPORTED 0 #> 16 BMI_CAT Underweight (<18.5) 3 (3%) #> 17 BMI_CAT Normal (18.5-24.9) 57 (66%) #> 18 BMI_CAT Overweight (25-29.9) 20 (23%) #> 19 BMI_CAT Obese (>=30) 6 (7%) #> Xanomeline High Dose Xanomeline Low Dose Total #> 1 73.8 (7.94) 76.0 (8.11) 75.1 (8.25) #> 2 69.5 (14.35) 68.0 (14.50) 66.6 (14.13) #> 3 165.9 (10.28) 163.7 (10.30) 163.9 (10.76) #> 4 25.2 (3.97) 25.2 (4.40) 24.7 (4.09) #> 5 11 (15%) 8 (8%) 33 (13%) #> 6 61 (85%) 88 (92%) 221 (87%) #> 7 35 (49%) 55 (57%) 143 (56%) #> 8 37 (51%) 41 (43%) 111 (44%) #> 9 62 (86%) 90 (94%) 230 (91%) #> 10 9 (12%) 6 (6%) 23 (9%) #> 11 0 0 0 #> 12 1 (1%) 0 1 (0%) #> 13 3 (4%) 6 (6%) 12 (5%) #> 14 69 (96%) 90 (94%) 242 (95%) #> 15 0 0 0 #> 16 1 (1%) 4 (4%) 8 (3%) #> 17 39 (54%) 46 (48%) 142 (56%) #> 18 23 (32%) 32 (34%) 75 (30%) #> 19 9 (12%) 13 (14%) 28 (11%) # ---- Example 6: Analysis variables as side-by-side column bands ---- # # `column = c(".variable", "<arm>")` turns each analysis variable into # its own band of arm columns (the "value and change side by side" # shape), with statistics stacked as rows. Per-variable `statistic` / # `decimals` resolve inside each band. Emitted columns are named # "<variable>..<arm>"; reference them verbatim in a manual `headers()` # call to draw the band spanners (pivot_across never builds spanners). vitals <- cdisc_saf_demo_ard[ cdisc_saf_demo_ard$variable %in% c("AGE", "WEIGHT"), ] vitals |> pivot_across( column = c(".variable", "TRT01A"), overall = NULL, statistic = list( AGE = c(N = "{N}", "Mean (SD)" = "{mean} ({sd})", Median = "{median}"), WEIGHT = c(N = "{N}", "Mean (SD)" = "{mean} ({sd})", Median = "{median}") ), decimals = list( AGE = c(mean = 1, sd = 2, median = 1), WEIGHT = c(mean = 1, sd = 2, median = 1) ) ) |> tabular( titles = c( "Table 14.2.1", "Summary of Continuous Parameters by Treatment", "Safety Population" ) ) |> cols(stat_label = col_spec(label = "Statistic")) |> headers( Age = c( "AGE..Placebo", "AGE..Xanomeline Low Dose", "AGE..Xanomeline High Dose" ), Weight = c( "WEIGHT..Placebo", "WEIGHT..Xanomeline Low Dose", "WEIGHT..Xanomeline High Dose" ) )

 

Table 14.2.1

Summary of Continuous Parameters by Treatment

Safety Population

 

AgeWeight
StatisticAGE..PlaceboAGE..Xanomeline High DoseAGE..Xanomeline Low DoseWEIGHT..PlaceboWEIGHT..Xanomeline High DoseWEIGHT..Xanomeline Low Dose
N867296867295
Mean (SD)75.2 (8.59)73.8 (7.94)76.0 (8.11)62.8 (12.77)69.5 (14.35)68.0 (14.50)
Median76.075.578.060.669.066.7
# ---- Example 7: Statistics as columns within each band ---- # # `column = c(".variable", ".stat")` spreads each statistic entry into # its own column (the landscape shell); the arm drops to a leading row # stub. Emitted columns are named "<variable>..<stat-entry>". Bands may # carry different stat sets, with no row-alignment needed. vitals |> pivot_across( column = c(".variable", ".stat"), overall = NULL, statistic = list( AGE = c(N = "{N}", Mean = "{mean}", SD = "{sd}"), WEIGHT = c(N = "{N}", Mean = "{mean}", SD = "{sd}", Median = "{median}") ), decimals = c(mean = 1, sd = 2, median = 1) ) #> TRT01A AGE..N AGE..Mean AGE..SD WEIGHT..N WEIGHT..Mean #> 1 Placebo 86 75.2 8.59 86 62.8 #> 2 Xanomeline High Dose 72 73.8 7.94 72 69.5 #> 3 Xanomeline Low Dose 96 76.0 8.11 95 68.0 #> WEIGHT..SD WEIGHT..Median #> 1 12.77 60.6 #> 2 14.35 69.0 #> 3 14.50 66.7 # ---- Example 8: Per-row-group decimal precision ---- # # A by-parameter vitals table where each parameter carries its own # value precision: systolic BP to 0 dp, weight to 1 dp, in ONE call. # `decimals` is keyed by the `row_group` (PARAM) value; the engine # selects each row's token precision by its parameter. No bundled ARD # carries a second grouping dimension, so build a tiny one inline. vital_ard <- do.call(rbind, lapply( list( c("SYSBP", "Placebo", "mean", "133.27"), c("SYSBP", "Placebo", "sd", "15.81"), c("SYSBP", "Drug", "mean", "128.94"), c("SYSBP", "Drug", "sd", "14.02"), c("WEIGHT", "Placebo", "mean", "71.43"), c("WEIGHT", "Placebo", "sd", "12.77"), c("WEIGHT", "Drug", "mean", "73.06"), c("WEIGHT", "Drug", "sd", "13.19") ), function(r) { data.frame( group1 = "PARAM", group1_level = r[[1]], group2 = "TRTA", group2_level = r[[2]], variable = "AVAL", variable_level = NA_character_, context = "continuous", stat_name = r[[3]], stat_label = r[[3]], stat = I(list(as.numeric(r[[4]]))), stringsAsFactors = FALSE ) } )) vital_ard |> pivot_across( column = "TRTA", row_group = "PARAM", overall = NULL, statistic = list(continuous = "{mean} ({sd})"), decimals = list( SYSBP = c(mean = 0, sd = 1), WEIGHT = c(mean = 1, sd = 2) ) ) #> PARAM variable stat_label Placebo Drug #> 1 SYSBP AVAL AVAL 133 (15.8) 129 (14.0) #> 2 WEIGHT AVAL AVAL 71.4 (12.77) 73.1 (13.19)