Step two of the ARD conversion. widen_ard() takes the long table from
normalize_ard() (possibly after you have added rows of your own), builds
one character cell per template, and pivots the column keys across.
Usage
widen_ard(
x,
cols,
rows = NULL,
label = ".label",
cells = "{n} ({p})",
stats = c("cells", "rows"),
value = c("stat", "stat_fmt"),
levels = NULL,
labels = NULL,
sort = FALSE,
sep = "____",
rounding = NULL,
sort_stat = NULL,
na = NA_character_,
notes = TRUE,
stat_groups = NULL,
compare = NULL,
show_keys = NULL,
filter = NULL
)Arguments
- x
A data frame from
normalize_ard().- cols
Column keys, outermost first. Multiple keys are pasted with
sep, producing the"Placebo____Day 1"names thatrtftable()'scol_headeralready reads as a spanning header. May be named, in which case the name is ignored for the column text. Left out (orNULL) when the ARD has no group to spread (one arm, a single cohort): the table is one column,All, its N the ARD's total (cards::ard_total_n()) for a header's{n}. An ARD that has a group needs it named.- rows
Row keys, in output order – any column of the normalized frame, not a fixed one: the ARD's own
variablewhen the row groups are the analysis variables (a demographics table), or a grouping variable's name when they are not ("AEBODSYS"). A named vector renames them, and the name is only the output column's name:rows = c(group = "variable")androws = c(group1 = "AEBODSYS")differ in what they group by, not in kind.Left
NULLon a flat ARD carrying more than one analysis variable, it defaults toc(group = "variable"), since that is the only thing left to group those rows by. One analysis variable, or anyhierarchy, leaves it empty.A formula element is a template here too, which is how a constant row-group heading stops needing a
mutate()of its own:rows = c(param = "PARAM", grp = ~ "Worst Post-Baseline Values"). A bare string is still a column name, so nothing already written changes meaning.- label
Source of the row label, as a single (optionally named) reference. Default
".label", whichnormalize_ard()sets to the deepest hierarchy value, or tovariable_levelwhen there is no hierarchy.NULLdrops the label column, which is what you want when everycellsentry is named.NAbuilds the column, uses it to tell the rows apart, and then drops it: a recipe whose names are a row index —"1"for an estimate line and"2"for the confidence interval under it — needs them to separate two rows of one record, and does not want a column of 1s and 2s in the result.NULLleaves the label out of the row identity altogether, so those two rows collide.An element that is a formula is a template over the record rather than a column name, and the chain works as
cellsdoes: the first element whose guard holds wins, and~ "..."is the unguarded one. Inside a template{column}interpolates, and{.label}is the label this row would otherwise carry. So indenting the severities underAnyis a rule rather than apaste0()on.label:label = c(.label %in% c("Mild", "Severe") ~ " {.label}", ~ "{.label}").- cells
The cell recipes. A character vector is one recipe, used for every variable:
"{n} ({p})"– one row, labelled fromlabel;c("{n} ({p})", "{n}")– one row, the first template that resolves;c(n == 0 ~ "0", "{n} ({p})")– the same chain, its first element guarded: acondition ~ templateelement applies only when the condition holds, so a guard that is false and a template that has no value for one of its tokens fail the same way, and the chain moves on. The condition is ordinary R, evaluated with the record's statistics by name (n,p,mean, ...) plus its own columns (variable,.label,.depth,.kindand the keys), and it may name the caller's variables too;c("Mean (SD)" = "{mean} ({sd})", "Min, Max" = "{min}, {max}")– one row per element, the name being the row label.
A list is instead a map, looked up by analysis variable, then by
context, then by the structural kind, then by"default"; each of its elements is a recipe of the three shapes above. The container decides:c("Mean (SD)" = ..., "Min, Max" = ...)is a two-row recipe, whilelist(continuous = ..., categorical = ...)is a map. A list with no names is a chain, which is whatc()returns once a guard is in it. For a named row whose value is itself a chain, usecell_rows().Statistics no template names are simply not read, which is how an ARD that also carries
method,alternative,conf.levelor a p-value converts without any filtering.See ard-tables for the
{token:spec}grammar.- stats
"cells"(default) builds character cells fromcells."rows"ignorescellsand gives every statistic its own row, labelled withstat_label– the shape a PK concentration table wants.- value
Which of the ARD's two values
stats = "rows"puts in the cell: the raw numeric"stat"(the default, so the table can still be aligned withset_decimal_split()) or"stat_fmt", the string cards already formatted. Ignored whenstats = "cells", where the template says which it wants, token by token.- levels
Named list of level orders, e.g.
list(TRT01P = c("Placebo", "Drug"), AGEGR = c("<65", ">=65")). A name may bea column key – it then fixes the order of the spread columns, which is what keeps a hand-written
col_headerover the arm it names;a row key, by either its source column or its renamed output column – it becomes a factor and drives the row sort;
an analysis variable – it orders that variable's rows in the label column, without your having to know what the label column is called. Variables you leave out keep their
cellstemplates' order.variableitself – a row key like any other:levels = list(variable = c("AGE", "SEX"))orders the analysis variables.
The levels order what the ARD has; they make no row or column. A value they name that the ARD does not have (by its value or as
labelsprints it) is said in a message, whatevernotessays: a level shown with no record has to be in the ARD – a factor's level, as the study's code list applied when the ARD is made gives, which cards counts as 0.- labels
Named character vector recoding key values to display text, e.g.
c(AGE = "Age (years)", SEX = "Sex [n (\%)]"). Text only: the order islevels' or the data's, never the order the labels are written in. The two-parallel-vector spelling works just as well –stats::setNames(group_labels, group_vars)– but note thatsetNames()gives an element anNAname rather than complaining when the two vectors are different lengths, so that entry would never apply; every element must be named, and an unnamed one is an error here rather than a silent omission.One vector is one dictionary for the whole table, and the same value can mean two things on the two axes — a shift table's
"0"is"Grade 0"down the side and"Baseline 0"across the top. Scope it the waylevelsalready is, with a list keyed by column, matched on the output name then the source name, with.defaultcovering the rest:A scope may name an analysis variable too:
labels = list(SEX = c(F = "Female", M = "Male"))recodes that variable's levels in the label column, aslevels = list(SEX = c("M", "F"))orders them (the order is written in the values, the label column prints the text). Its entry under the variable's own name is the variable's label:SEX = c(SEX = "Sex", F = "Female", M = "Male").- sort
FALSE(default) moves a row only where somebody declared an order. A key is a factor exactly whenlevels,labelsor the data itself made it one — and making a column a factor is how a table says what its order is — so a factor key is sorted on, and so is the label column whenlevelsgave it an order. Everything else stays where the data put it, and a declared order nested inside a plain key's block sorts within that block. Nothing is ever alphabetised behind your back. (The cells are gathered by their key before this, so a block is whole either way; what is left is where the blocks sit.)Usually you write nothing here, or you name the keys. A character vector names them, in priority order, each optionally prefixed
-for descending.TRUEis the third, rarer answer: it groups, bringing a plain key's separate blocks together in the order they first appear, which no list of keys says without also choosing an order for them:".overall"the hierarchical-overall rows (an
Any TEAEblock) first.".depth"a level's own summary row before the rows nested under it.
- a column
any row key or the label column, by its output name.
- a statistic
that statistic totalled across the spread columns – what a descending-frequency AE table sorts on.
"<stat>[<col>]"that statistic in one spread column:
"-n[Total]","-n[Placebo]"(a level of the outer column key takes its columns,Placebo____FandPlacebo____M)."<stat>@<key>"the statistic of the group's own row — the row key's summary, its shallowest row — carried to every row of the group:
"-n@soc"orders the SOCs by their own count and keeps each SOC's PTs with it. Both at once:"-n@soc[Total]". An overall row keeps its own number."max(<stat>)","min(<stat>)"the statistic's largest (smallest) value across the spread columns instead of their total:
"-max(n)"puts first the rows most frequent in any arm. With the forms above:"-max(n)@soc","-max(n)[Placebo]"."sum(n)"is"n".- a comparison's statistic
a statistic of the comparison records
plan_compare()places (a difference between two arms,"-estimate"), on the row it lands on: the rows by the arms' difference."-estimate[<comparison>]"picks one whenplan_compare(by = )names them;"-estimate@soc"carries a SOC's own difference to its PTs. A statistic the body has is the body's.
Without
-a key is ascending ("n","max(n)": the fewest first). Sosort = c(".overall", "soc", ".depth", "-n", "term")is the whole of an AE table's row order, and needs neithersort_statnor anarrange()afterwards;c(".overall", "-n@soc", "soc", ".depth", "-n")puts the SOCs in descending order of their own count. Ties go to the keys that follow.- sep
Separator pasted between multiple
colskeys.- rounding
Tie-breaking rule for
{x:.1f}-style tokens:"r"or"sas".NULL, the default, readsgetOption("rtfreporter.rounding")— the package's one rule, base R's half-to-even unless the study set"sas"once. It does not reach{x}or{x:stat_fmt}, which take a string cards already rounded (half away from zero, as it happens). Seeround_num().- sort_stat
Name of a statistic to total across the spread columns and attach as a numeric
.sort_statcolumn – what a descending-frequency AE table sorts on.NULL(default) adds nothing.- na
Value to put in a cell no template could fill.
- notes
Report what was not used. Every stage discards ARD rows – the
attributesandtotal_nrows, the key variables' own tabulations, rows with no value for acolskey, and every statistic no template named – and discarding them in silence is how a mis-typedcellslooks exactly like a correct one.TRUE(default) prints a summary,FALSEsays nothing, and"attr"also attaches the per-variable detail as the"ard_ignored"attribute. It is not attached by default because the result is a plain data frame that you will compare against whatever you built before, and an extra attribute makesall.equal()report a difference that is not in the table."applied"adds the other half: which template produced each cell, with the guard that let it through when it had one. A bare template is its own explanation, but a guarded chain is not — the finished cell cannot tell you which of its three candidates you got — so this is the companion tocellsguards rather than a general-purpose log.- stat_groups
The statistics in named groups, as a named list (
list(n = "N", orr_ci = c("n", "estimate", "conf.low", "conf.high"))): each row gets its group in a key.stat_group, whichcolsorrowsname like any other (cols = c("ARM", ".stat_group"): one record's N in one column, its rate and interval in the next), and acellsmap may be keyed by a group's name (looked up after the analysis variable). A statistic in no group is not read. The options.variablesand.othersscope it (seeplan_stat_groups()).plan_stat_groups()is the plan's spelling.NULL(default): no groups.- compare
Columns read from the ARD's comparison records — the column key with no value, in an analysis the body does not have (a test's p-value, a difference and its CI) — put on the row of the record's variable:
list(cols = list(p = "{p.value:.3f}"), by = NULL, position = "last").plan_compare()is the plan's spelling, and its documentation says where a record goes.NULL(default): none, and the records are left out as rows with no value of the column key.- show_keys
Row keys whose own tabulation stays in the body — their counts as a variable's — instead of only the header's: what
table_plan()'sshow_keyssays.NULL(default): none.- filter
Rows dropped by a condition over their records:
list(conds = list(quote(p >= 0.05)), any = TRUE, parents = "keep").plan_filter()is the plan's spelling, and says the rules.NULL(default): every row.
Details
One ARD record becomes one table row. Layouts that put a single record on two printed lines are deliberately out of scope – do those afterwards, on the returned data frame.
See also
normalize_ard(), plan_template(form = "widen")
Examples
if (requireNamespace("cards", quietly = TRUE)) {
ard <- cards::ard_stack(
cards::ADSL, .by = ARM,
cards::ard_summary(variables = AGE),
cards::ard_tabulate(variables = SEX))
normalize_ard(ard) |>
widen_ard(cols = "ARM", rows = c(group = "variable"),
cells = list(AGE = c("Mean (SD)" = "{mean:.1f} ({sd:.2f})"),
SEX = "{n} ({p:.1f%})"),
notes = FALSE)
}
#> group label Placebo Xanomeline High Dose Xanomeline Low Dose
#> 1 AGE Mean (SD) 75.2 (8.59) 74.4 (7.89) 75.7 (8.29)
#> 2 SEX F 53 (61.6) 40 (47.6) 50 (59.5)
#> 3 SEX M 33 (38.4) 44 (52.4) 34 (40.5)