Skip to contents

Add col_spec() entries to a tabular_spec. Each named argument is one column: the name is the input column in .spec@data and the value is the col_spec carrying that column's display attributes (usage, label, format, alignment, width, visibility, NA text). Columns not mentioned get a default col_spec() (usage = display) at engine-validate time.

Usage

cols(.spec, ..., .default = NULL)

Arguments

.spec

The tabular_spec to extend. <tabular_spec>: required. Dot-prefixed so R's partial argument matching cannot accidentally bind a short user-supplied name (e.g. s, sp) in ... to the spec slot. Pipe input (tabular(...) |> cols(...)) works the normal way — the spec is supplied positionally.

...

Named col_spec objects, one per column. Each name is the input column name in .spec@data. Names must match an existing column — pre-compute derived columns upstream with dplyr::mutate() (or equivalent) before tabular().

Restriction: Names must be unique within a single cols() call (duplicates warn; "last value wins"). Tip: To override an attribute already declared, use a second cols() call downstream and let the merge rule apply.

.default

Fallback col_spec for unmentioned columns. <col_spec | NULL>: default NULL. When a col_spec, it is field-merged onto every data column that is NOT named in ... and does not already carry a spec from an earlier cols() call. NULL (default) leaves unmentioned columns to the engine-time default. Use it to set one alignment / format across a variable number of arm columns in a single call.

Interaction: Explicit ... specs always win — .default only fills the gaps. A column carried over from a prior cols() call is treated as already specified and is left untouched.

# Decimal-align every arm column without listing each by name.
tabular(cdisc_saf_demo) |>
  cols(
    variable   = col_spec(usage = "group", label = "Parameter"),
    stat_label = col_spec(label = "Statistic"),
    .default   = col_spec(align = "decimal")
  )

Value

The updated tabular_spec. Continue chaining with headers(), sort_rows(), style().

Details

Sparse declaration. Declare only the columns whose attributes differ from the default — a typical pipeline uses one cols() call with one entry per non-default column.

Layout order follows the data frame, not cols(). The left-to-right column order in the rendered table is the column order of .spec@data; the order of the named ... arguments here is irrelevant (they are a lookup keyed by name). To move a column, reorder the data frame upstream — a column derived with df$new <- ... is appended last and will render last unless you reorder.

Within-call duplicates warn. A duplicate name inside one cols() call warns and "last value wins". To intentionally override an attribute, use a second cols() call downstream and let the merge rule below apply.

Repeat-call merge semantics

When cols() is called more than once for the same column, the engine merges the new col_spec into the existing one field-by- field. A field set to a non-default value on the new spec overrides; a field left at its "unset" sentinel leaves the existing value intact. Every mergeable field has a genuine unset sentinel, so a later call can also restore a default (e.g. visible = TRUE re-shows a hidden column, group_display = "header_row" resets a prior "column"). This lets you build a column's spec in stages — declare the label-and-alignment block up front, add the width once you know it fits, then attach a sort key, all without re-stating earlier attributes. Essential when generating specs programmatically (looping over arms, layering a house-style helper).

Unset sentinels — a field left at this value does NOT override the existing field (every other value, including a default like visible = TRUE, overrides):

fieldunset sentinel
usageNA
labelNA_character_
formatNULL
visibleNA
width"auto"
group_displayNA
group_skipNA
alignNA_character_
valignNA_character_
na_textNA_character_ (inherit preset)
indentNA

# Three-stage build: label/usage first, alignment second, width
# third. Each stage leaves earlier fields intact.
tabular(cdisc_saf_demo) |>
  cols(variable = col_spec(usage = "group", label = "Parameter")) |>
  cols(variable = col_spec(align = "left")) |>
  cols(variable = col_spec(width = 2.0))
# Result: variable has usage="group", label="Parameter",
#         align="left", width=2.0 — all four fields set.

See also

Companion constructor: col_spec() builds the per-column DSL object that cols() attaches.

Sibling build verbs: headers(), sort_rows(), style(), paginate(), preset().

Entry / terminal verbs: tabular(), emit(), as_grid().

Examples

# ---- Example 1: Demographics with arm BigN inline in headers ----
#
# Demographics table where the row-label columns sit on the left
# and the four treatment-arm columns embed BigN in the header
# label (drawn inline from the bundled `cdisc_saf_n` data frame). Every
# arm column is decimal-aligned so mixed-format cells like
# "5 (3.2%)" line up on the decimal mark.
n <- stats::setNames(cdisc_saf_n$n, cdisc_saf_n$arm_short)

tabular(
  cdisc_saf_demo,
  titles = c(
    "Table 14.1.1",
    "Demographics and Baseline Characteristics",
    "Safety Population"
  ),
  footnotes = "Percentages based on N per treatment group."
) |>
  cols(
    variable   = col_spec(usage = "group", label = "Parameter"),
    stat_label = col_spec(label = "Statistic"),
    placebo    = col_spec(label = "Placebo\nN={n['placebo']}",  align = "decimal"),
    drug_50    = col_spec(label = "Drug 50\nN={n['drug_50']}",  align = "decimal"),
    drug_100   = col_spec(label = "Drug 100\nN={n['drug_100']}", align = "decimal"),
    Total      = col_spec(label = "Total\nN={n['Total']}",    align = "decimal")
  ) |>
  sort_rows(by = c("variable", "stat_label"))

 

Table 14.1.1

Demographics and Baseline Characteristics

Safety Population

 

StatisticPlacebo
N=86
Drug 50
N=96
Drug 100
N=72
Total
N=254
Age (years)
Mean (SD)75.2 (8.59)76.0 (8.11)73.8 (7.94) 75.1 (8.25)
Median76.0       78.0       75.5        77.0       
Min, Max52  , 89   51  , 88   56  , 88    51  , 89   
Q1, Q369.2, 81.8 71.0, 82.0 70.5, 79.0  70.0, 81.0 
n86         96         72         254         
 
Race, n (%)
AMERICAN INDIAN OR ALASKA NATIVE 0          0          1 ( 1.4)    1 ( 0.4)  
ASIAN 0          0          0           0         
BLACK OR AFRICAN AMERICAN 8 ( 9.3)   6 ( 6.2)   9 (12.5)   23 ( 9.1)  
WHITE78 (90.7)  90 (93.8)  62 (86.1)  230 (90.6)  
 
Sex, n (%)
F53 (61.6)  55 (57.3)  35 (48.6)  143 (56.3)  
M33 (38.4)  41 (42.7)  37 (51.4)  111 (43.7)  

Percentages based on N per treatment group.

# ---- Example 2: BOR table with CDISC factor ordering and hidden helper ---- # # Best Overall Response table where `stat_label` carries the # canonical CDISC factor levels (driving the sort) and `row_type` # is hidden — present in the data for the sort, absent from the # rendered output via `col_spec(visible = FALSE)`. bor_levels <- c( "CR", "PR", "SD", "NON-CR/NON-PD", "PD", "NE", "MISSING", "ORR (CR + PR)", "CBR (CR + PR + SD)", "DCR (CR + PR + SD + NON-CR/NON-PD)", "95% CI (Clopper-Pearson)" ) eff <- cdisc_eff_resp eff$stat_label <- factor(eff$stat_label, levels = bor_levels) ne <- stats::setNames(cdisc_eff_n$n, cdisc_eff_n$arm_short) tabular( eff, titles = c( "Table 14.2.1", "Best Overall Response and Response Rates", "Efficacy Evaluable Population" ), footnotes = "Response per RECIST 1.1, investigator assessment." ) |> cols( stat_label = col_spec(label = "Response"), row_type = col_spec(visible = FALSE), groupid = col_spec(visible = FALSE), group_label = col_spec(visible = FALSE), placebo = col_spec(label = "Placebo\nN={ne['placebo']}", align = "decimal"), drug_50 = col_spec(label = "Drug 50\nN={ne['drug_50']}", align = "decimal"), drug_100 = col_spec(label = "Drug 100\nN={ne['drug_100']}", align = "decimal") ) |> sort_rows(by = c("groupid", "stat_label"))

 

Table 14.2.1

Best Overall Response and Response Rates

Efficacy Evaluable Population

 

ResponsePlacebo
N=86
Drug 50
N=84
Drug 100
N=84
CR 1   ( 1.2) 1   ( 1.2) 1   ( 1.2)
PR 1   ( 1.2) 0          0         
SD 1   ( 1.2) 0          0         
NON-CR/NON-PD 0          0          1   ( 1.2)
PD 0          0          1   ( 1.2)
NE 0          1   ( 1.2) 0         
MISSING83   (96.5)82   (97.6)81   (96.4)
ORR (CR + PR) 2   ( 2.3) 1   ( 1.2) 1   ( 1.2)
95% CI (Clopper-Pearson)( 0.3, 8.1)( 0.0, 6.5)( 0.0, 6.5)
CBR (CR + PR + SD) 3   ( 3.5) 1   ( 1.2) 1   ( 1.2)
95% CI (Clopper-Pearson)( 0.7, 9.9)( 0.0, 6.5)( 0.0, 6.5)
DCR (CR + PR + SD + NON-CR/NON-PD) 3   ( 3.5) 1   ( 1.2) 2   ( 2.4)
95% CI (Clopper-Pearson)( 0.7, 9.9)( 0.0, 6.5)( 0.3, 8.3)

Response per RECIST 1.1, investigator assessment.

# ---- Example 3: AE-by-SOC/PT with indented label + repeat-call merge ---- # # `label` carries SOC text on SOC rows and PT text on PT rows; # `indent = "indent_level"` indents the PT rows one level under # their SOC. `soc`, `row_type`, and `n_total` ride along as hidden # sort keys. A second `cols()` call later in the chain adds widths # once the user knows the page geometry; the repeat-call merge # preserves prior attributes (label, indent, align, visible) # without restating them. tabular( cdisc_saf_aesocpt, titles = c("Table 14.3.1", "Adverse Events by SOC and Preferred Term") ) |> cols( label = col_spec(label = "SOC / PT", indent = "indent_level"), soc = col_spec(visible = FALSE), row_type = col_spec(visible = FALSE), soc_n = col_spec(visible = FALSE), n_total = col_spec(visible = FALSE), placebo = col_spec(label = "Placebo", align = "decimal"), drug_50 = col_spec(label = "Drug 50", align = "decimal"), drug_100 = col_spec(label = "Drug 100", align = "decimal"), Total = col_spec(label = "Total", align = "decimal") ) |> sort_rows(by = c("soc_n", "n_total"), descending = c(TRUE, TRUE)) |> # Second `cols()` call: add widths after the rest of the spec # is built. Repeat-call merge preserves prior attributes. cols( label = col_spec(width = "2.5in"), placebo = col_spec(width = "0.9in"), drug_50 = col_spec(width = "0.9in"), drug_100 = col_spec(width = "0.9in"), Total = col_spec(width = "0.9in") )

 

Table 14.3.1

Adverse Events by SOC and Preferred Term

 

SOC / PTPlaceboDrug 50Drug 100Total
TOTAL SUBJECTS WITH AN EVENT52 (60.5)81 (84.4)66 (91.7)199 (78.3)
SKIN AND SUBCUTANEOUS TISSUE DISORDERS19 (22.1)36 (37.5)35 (48.6) 90 (35.4)
PRURITUS 8 ( 9.3)21 (21.9)25 (34.7) 54 (21.3)
ERYTHEMA 8 ( 9.3)14 (14.6)14 (19.4) 36 (14.2)
RASH 5 ( 5.8)13 (13.5) 8 (11.1) 26 (10.2)
HYPERHIDROSIS 2 ( 2.3) 4 ( 4.2) 8 (11.1) 14 ( 5.5)
SKIN IRRITATION 3 ( 3.5) 6 ( 6.2) 5 ( 6.9) 14 ( 5.5)
GENERAL DISORDERS AND ADMINISTRATION SITE CONDITIONS15 (17.4)36 (37.5)30 (41.7) 81 (31.9)
APPLICATION SITE PRURITUS 6 ( 7.0)23 (24.0)21 (29.2) 50 (19.7)
APPLICATION SITE ERYTHEMA 3 ( 3.5)13 (13.5)14 (19.4) 30 (11.8)
APPLICATION SITE DERMATITIS 5 ( 5.8) 9 ( 9.4) 7 ( 9.7) 21 ( 8.3)
APPLICATION SITE IRRITATION 3 ( 3.5) 9 ( 9.4) 9 (12.5) 21 ( 8.3)
APPLICATION SITE VESICLES 1 ( 1.2) 5 ( 5.2) 5 ( 6.9) 11 ( 4.3)
GASTROINTESTINAL DISORDERS13 (15.1)12 (12.5)17 (23.6) 42 (16.5)
DIARRHOEA 9 (10.5) 5 ( 5.2) 3 ( 4.2) 17 ( 6.7)
VOMITING 3 ( 3.5) 4 ( 4.2) 6 ( 8.3) 13 ( 5.1)
NAUSEA 3 ( 3.5) 3 ( 3.1) 6 ( 8.3) 12 ( 4.7)
ABDOMINAL PAIN 1 ( 1.2) 3 ( 3.1) 1 ( 1.4)  5 ( 2.0)
SALIVARY HYPERSECRETION 0        0        4 ( 5.6)  4 ( 1.6)
NERVOUS SYSTEM DISORDERS 6 ( 7.0)18 (18.8)17 (23.6) 41 (16.1)
DIZZINESS 2 ( 2.3) 9 ( 9.4)10 (13.9) 21 ( 8.3)
HEADACHE 3 ( 3.5) 3 ( 3.1) 5 ( 6.9) 11 ( 4.3)
SYNCOPE 0        5 ( 5.2) 2 ( 2.8)  7 ( 2.8)
SOMNOLENCE 2 ( 2.3) 3 ( 3.1) 1 ( 1.4)  6 ( 2.4)
TRANSIENT ISCHAEMIC ATTACK 0        2 ( 2.1) 1 ( 1.4)  3 ( 1.2)
CARDIAC DISORDERS 7 ( 8.1)12 (12.5)14 (19.4) 33 (13.0)
SINUS BRADYCARDIA 2 ( 2.3) 7 ( 7.3) 8 (11.1) 17 ( 6.7)
MYOCARDIAL INFARCTION 4 ( 4.7) 2 ( 2.1) 4 ( 5.6) 10 ( 3.9)
ATRIAL FIBRILLATION 1 ( 1.2) 2 ( 2.1) 2 ( 2.8)  5 ( 2.0)
SUPRAVENTRICULAR EXTRASYSTOLES 1 ( 1.2) 1 ( 1.0) 1 ( 1.4)  3 ( 1.2)
VENTRICULAR EXTRASYSTOLES 0        2 ( 2.1) 1 ( 1.4)  3 ( 1.2)
INFECTIONS AND INFESTATIONS12 (14.0) 6 ( 6.2)11 (15.3) 29 (11.4)
NASOPHARYNGITIS 2 ( 2.3) 4 ( 4.2) 6 ( 8.3) 12 ( 4.7)
UPPER RESPIRATORY TRACT INFECTION 6 ( 7.0) 1 ( 1.0) 3 ( 4.2) 10 ( 3.9)
INFLUENZA 1 ( 1.2) 1 ( 1.0) 1 ( 1.4)  3 ( 1.2)
URINARY TRACT INFECTION 2 ( 2.3) 0        1 ( 1.4)  3 ( 1.2)
CYSTITIS 1 ( 1.2) 0        1 ( 1.4)  2 ( 0.8)
RESPIRATORY, THORACIC AND MEDIASTINAL DISORDERS 5 ( 5.8) 8 ( 8.3) 9 (12.5) 22 ( 8.7)
COUGH 1 ( 1.2) 5 ( 5.2) 5 ( 6.9) 11 ( 4.3)
NASAL CONGESTION 3 ( 3.5) 1 ( 1.0) 3 ( 4.2)  7 ( 2.8)
DYSPNOEA 1 ( 1.2) 1 ( 1.0) 1 ( 1.4)  3 ( 1.2)
EPISTAXIS 0        1 ( 1.0) 2 ( 2.8)  3 ( 1.2)
PHARYNGOLARYNGEAL PAIN 0        1 ( 1.0) 1 ( 1.4)  2 ( 0.8)
PSYCHIATRIC DISORDERS 7 ( 8.1) 9 ( 9.4) 3 ( 4.2) 19 ( 7.5)
CONFUSIONAL STATE 2 ( 2.3) 3 ( 3.1) 1 ( 1.4)  6 ( 2.4)
AGITATION 2 ( 2.3) 3 ( 3.1) 0         5 ( 2.0)
INSOMNIA 2 ( 2.3) 0        2 ( 2.8)  4 ( 1.6)
ANXIETY 0        3 ( 3.1) 0         3 ( 1.2)
DELUSION 1 ( 1.2) 0        1 ( 1.4)  2 ( 0.8)
MUSCULOSKELETAL AND CONNECTIVE TISSUE DISORDERS 3 ( 3.5) 6 ( 6.2) 5 ( 6.9) 14 ( 5.5)
BACK PAIN 1 ( 1.2) 1 ( 1.0) 3 ( 4.2)  5 ( 2.0)
ARTHRALGIA 1 ( 1.2) 2 ( 2.1) 1 ( 1.4)  4 ( 1.6)
SHOULDER PAIN 1 ( 1.2) 2 ( 2.1) 0         3 ( 1.2)
MUSCLE SPASMS 0        1 ( 1.0) 1 ( 1.4)  2 ( 0.8)
ARTHRITIS 0        0        1 ( 1.4)  1 ( 0.4)
INVESTIGATIONS 5 ( 5.8) 4 ( 4.2) 3 ( 4.2) 12 ( 4.7)
ELECTROCARDIOGRAM ST SEGMENT DEPRESSION 4 ( 4.7) 1 ( 1.0) 0         5 ( 2.0)
ELECTROCARDIOGRAM T WAVE INVERSION 2 ( 2.3) 1 ( 1.0) 1 ( 1.4)  4 ( 1.6)
BLOOD GLUCOSE INCREASED 0        1 ( 1.0) 1 ( 1.4)  2 ( 0.8)
ELECTROCARDIOGRAM T WAVE AMPLITUDE DECREASED 1 ( 1.2) 1 ( 1.0) 0         2 ( 0.8)
BIOPSY 0        0        1 ( 1.4)  1 ( 0.4)
# ---- Example 4: Compact AE-overall with pre-derived Active column ---- # # Drop the per-arm columns and surface only the Total. Pre-compute # the pooled "Active" column upstream (here `paste0(drug_50, " / ", # drug_100)`) before piping into `tabular()`; `cols()` then just # declares each column's display role. The same pattern handles # any post-pivot derivation (`pivot_across() |> mutate(...) |> # tabular()`). # # Column LAYOUT order follows the data frame, not the `cols()` # argument order. A derived column appended with `ae$active <- ...` # would render last, so reorder the frame to place it where it # should display (here between Placebo and Total). ae <- cdisc_saf_ae ae$active <- paste0(ae$drug_50, " / ", ae$drug_100) ae <- ae[c("stat_label", "placebo", "active", "Total", "drug_50", "drug_100")] tabular( ae, titles = c("Table 14.3.0", "Adverse Event Overview"), footnotes = "Active = pooled Drug 50 + Drug 100 columns." ) |> cols( stat_label = col_spec(label = ""), placebo = col_spec(label = "Placebo", align = "decimal"), active = col_spec(label = "Active arms"), drug_50 = col_spec(visible = FALSE), drug_100 = col_spec(visible = FALSE), Total = col_spec(label = "Total", align = "decimal") )

 

Table 14.3.0

Adverse Event Overview

 

PlaceboActive armsTotal
Any TEAE65 (75.6)84 (87.5) / 68 (94.4)217 (85.4)
Any Serious AE (SAE) 0       2 (2.1) / 1 (1.4)  3 ( 1.2)
Any AE Related to Study Drug43 (50.0)77 (80.2) / 64 (88.9)184 (72.4)
Any AE Leading to Death 2 ( 2.3)1 (1.0) / 0 (0.0)  3 ( 1.2)
Any AE Recovered / Resolved47 (54.7)61 (63.5) / 49 (68.1)157 (61.8)
  Maximum severity: Mild36 (41.9)21 (21.9) / 20 (27.8) 77 (30.3)
  Maximum severity: Moderate24 (27.9)47 (49.0) / 40 (55.6)111 (43.7)
  Maximum severity: Severe 5 ( 5.8)16 (16.7) / 8 (11.1) 29 (11.4)

Active = pooled Drug 50 + Drug 100 columns.