A small family that turns an ARD (Analysis Results Data, as produced by
cards and cardx) into the table data.frame that
as_rtftables() consumes. The target is one ARD record, one table row.
Layouts that print one record over two lines, or that need derived rows such
as marginal totals, stay a human job – do them on the returned data frame,
or on the long frame from normalize_ard().
The functions
list_ard_keys()What keys, variables, contexts and statistics an ARD actually holds.
normalize_ard()ARD to a flat, explicitly keyed long table.
widen_ard()Long table to the wide table data.frame.
plan_template(form = "widen")Emit runnable conversion code for a given ARD.
overall_row()Where the table's overall row comes from.
pull_ard()A statistic keyed like the spread columns, for a column header or an overall row.
- tflspec
The Excel definition of a table (
tflspec::tfl_table_spec()) andtflspec::tfl_table_plan(), which builds a plan from it.
Nothing is read from the object's attributes
A cards ARD carries attributes as well as rows, but they are not a contract:
attr(ard, "args") lists by and variables in a different order per
generator and cannot tell them apart, it keeps only the first operand's
value after dplyr::bind_rows(), and it is not updated when the ARD is
filtered. The ARD's class survives bind_rows() with a differently shaped
ARD, so dispatching on it is no safer. Every structural decision here comes
from an explicit argument instead; only the tibble's rows are inspected.
The cell template grammar
A template is a string with {...} tokens naming statistics:
{mean} | the value cards itself formatted (fmt_fun) |
{mean:.1f} | 1 decimal place, rounded per rounding |
{p:.1f\%} | multiplied by 100 first, then 1 decimal |
{mean:.3s} | 3 significant digits |
{n:d} | integer |
{n:stat} | the stat column, as.character(), untouched |
{mean:stat_fmt} | the stat_fmt column, demanded |
An ARD carries two values per statistic and both are reachable: a token
the two specs that reach them are spelled as the ARD spells them, so there
is no mapping to learn. :stat_fmt takes the string cards formatted
and errors when this ARD has none, because that is a demand; :stat
takes the value untouched, character statistics such as method included;
any other spec formats stat here. A bare token is the forgiving one
— it prefers stat_fmt and falls back to stat — since stat_fmt is
optional and an ARD built without fmt_fun would otherwise produce
nothing. The two differ for a proportion: cards writes 61.6 into
stat_fmt while stat holds 0.616, so {p} and {p:.1f\%} agree and
{p:.1f} does not. For stats = "rows", where a value goes into the cell without
a template, widen_ard(value = ) makes the same choice.
A template whose statistics are not all present yields NA, which is what
lets c("{n} ({p})", "{n}") act as a fallback chain. A named vector of
templates produces one table row per element, the name being the row label:
that is how Min and Max become a single Min, Max line.
How a cells entry is chosen (and why it survives a cards upgrade)
context is a cards implementation detail and it moves.
ard_continuous() stamps "continuous", but its rename (cards 0.7.0)
ard_summary() stamps "summary"; ard_categorical() stamps
"categorical", but ard_tabulate() stamps "tabulate". Keying cells
on the context alone would tie your script to one cards generation and
produce no cells at all against another.
So every summary – a variable under one context – is also classified from what its rows actually contain, its kind:
"categorical"the summary has levels to enumerate (some
variable_levelis present): a factor, a dichotomous value of interest, a hierarchy term."continuous"it does not – one row per statistic of one numeric variable.
A variable both summarised and tabulated in one ARD is two summaries, and
gets one kind for each.
That reading is structural, so it is the same on every cards version,
past and future. A cells entry is then matched in this order:
the analysis variable's own name;
context, with the known spellings treated as equivalent;the kind, likewise;
"default".
Step 2 lets a context-specific entry win where you wrote one – cardx's
"proportion_ci", "survival", "stats_t_test" and friends. Step 3 is
what keeps continuous / categorical (or summary / tabulate, either
spelling) working when cards renames a verb again.
list_ard_keys() prints both: the contexts, which are version-specific, and the
kinds, which are not.
Where a value lives depends on how the ARD was built
Two things a table needs have no fixed home in an ARD, because cards offers more than one way to produce them. Neither is guessed:
- the overall row
ard_stack_hierarchical(over_variables = TRUE)writes an "any event" row as the sentinel variable..ard_hierarchical_overall..; summarise each level separately and bind the results and there is no sentinel – that block counts the treatment itself, so the arm sits invariable/variable_level.overall_row()says which.- the denominator
in one ARD
stat_name == "N"is the per-arm denominator on the summary rows and the study total on the by variable's own rows, where the per-arm count isninstead; and an author may compute their own.pull_ard()names the statistic and lists the candidates when the choice is ambiguous.
Column headers are rtfreporter's own job – col_header takes a plain
character vector – so widen_ard() builds the body only, and
pull_ard() is there when the header needs a number that must agree with
the percentages.