Tabulates a battery of items answered on one common scale, such as a
Likert grid or the "array" question of a survey tool: one row per item
and one column per level of the scale, with the row percentages of each
item, an optional column adding up some levels (top), and the base of
each row (n). The items are selected with tidyselect syntax.
With by, each group gets a block of columns, and each item gets a
test of independence of the item and the group (the chi-squared, or
Fisher's exact test when an expected count is below 5), adjusted for
the family of the items by Holm's method.
Multiple output formats are available via output: a printed ASCII
table ("default"), a wide or long numeric data.frame
("data.frame", "long"), or publication-ready tables
("tinytable", "gt", "flextable", "excel", "clipboard",
"word").
Usage
table_battery(
data,
select,
by = NULL,
levels = NULL,
top = NULL,
sort = "",
drop_na = TRUE,
show_n = TRUE,
weights = NULL,
test = c("auto", "chisq", "fisher", "midp", "n1"),
p_value = NULL,
p_adjust = "holm",
labels = getOption("spicy.var_labels", FALSE),
note_labels = NULL,
percent_digits = 1,
p_digits = 3,
decimal_mark = ".",
align = c("decimal", "center", "right"),
title = NULL,
note = NULL,
note_add = NULL,
output = c("default", "data.frame", "long", "tinytable", "gt", "flextable", "excel",
"clipboard", "word"),
excel_path = NULL,
excel_sheet = NULL,
clipboard_delim = "\t",
word_path = NULL,
style = NULL
)Arguments
- data
A data frame, one row per respondent.
- select
The items of the battery, one column per item, in tidyselect syntax or as a character vector of column names. Their order is the order of the rows, unless
sortsorts them. Thebyandweightscolumns are not part of the selection: a selection helper skips them, and naming one, or giving its position, is an error. A name that is not a column ofdatais an error (classspicy_missing_column), and so is an empty selection. The items share one scale; see Items and levels.- by
Optional grouping column, unquoted or as a string. Factor levels keep their declared order; any other column is sorted, as in
table_categorical(). Rows with a missingbyare removed from the whole table, and the note gives their count ("Missing values removed: sex (2)."); abythat no observation holds is an error (classspicy_invalid_data).- levels
The scale: the levels to show, in the order of the columns.
NULL(the default) reads the scale from the items (see Items and levels). Given,levelsis the scale whatever the items hold: a level that no item holds shows at 0, and any other answer, observed or declared, is left out of the percentages and the base, and the note names it.levelsis the way to keep a "Don't know" answer out of the base, and to give numeric or text items a level nobody chose (levels = 1:5). The entries are written as the table shows the answers: for a labelled column, the"[code] label"string. An entry that no item declares or holds may be a typo: it keeps its column, at 0, with a warning (classspicy_unknown_name). A level that a factor declares and nobody chose does not warn, nor does a number within the values the numeric items hold (3 on a 1-5 scale); a number outside them does.- top
The levels the summary column adds up, for example
top = c("Strongly agree", "Agree"). The column follows the levels; its header is the list of the levels joined by" + ", in the order oftop, and its value is the sum of their percentages. Each entry must be one of the levels shown (seelevels); another value is an error. Withoutby, atopof one level would repeat the column of that level, and is an error.NULL(the default) adds no column.- sort
The order of the rows, in the suffix grammar of
freq()andtable_multiresponse(),+for increasing and-for decreasing order:""(the default): the order ofselect;"+"/"-": by the summary column oftop, or, withouttop, by the first level shown;"top+"/"top-": by the summary column (needstop);"name+"/"name-": by the label of the item, in the C locale, the same on every platform (capitals before lower case);"<level>+"/"<level>-": by the percentage of a level shown, for examplesort = "Strongly agree-".
With
by, the percentages are those of all the groups together. Items with the same value keep the order ofselect, and an item without data sorts last. The keywords are read first:"name+"sorts on the labels even when a level is called "name", and"top+"on the summary column even when a level is called "top". A level or a keyword without its direction ("Agree","top") has two readings, and is an error that gives both. Any other value is an error.- drop_na
Logical. If
TRUE(the default), the missing answers to an item leave its base: its percentages are on its valid answers, and itsncounts them. IfFALSE, a last column of missing answers ("(Missing)") follows the levels, and the base of each item is every row, its missing answers included.- show_n
Logical. If
TRUE(the default), each row (each group block underby) ends withn, the base of its percentages.- weights
Optional frequency weights, as in
table_categorical():NULL(the default), a numeric vector of lengthnrow(data), or a single column ofdata, unquoted or as a string. The counts, the percentages,nand the tests are weighted. Rows with anNAweight leave the table, and the note says how many. Fisher's exact test needs whole numbers: see Tests. For survey weights, the percentages are right but the tests are not, which need the design: seetable_battery_svy().- test
The test of each item's p-value, as in
table_categorical():"auto"(the default) takes Pearson's chi-squared test when every expected count of the item's table is at least 5 and Fisher's exact test otherwise;"chisq"and"fisher"take one test for every item;"midp"takes the Fisher mid-p test for an item whose table is 2x2;"n1"takes the N-1 chi-squared test for a 2x2 table whose expected counts are all at least 1, and Fisher's exact test otherwise. A table that is not 2x2 follows"auto"under"midp"and"n1". The note names the test of each item. Ignored, with a warning, when the table runs no test.- p_value
Logical or
NULL. Withby,NULL(the default) andTRUEshow thepcolumn of the item tests;FALSEremoves it from every output, the test columns of the long output included. Ignored withoutby, with a warning whenTRUE: the table runs no test without groups to compare.- p_adjust
Multiple-comparison adjustment of the
pcolumn, applied to the family of the items tested:"holm"(the default),"none", or another method ofstats::p.adjust()("hochberg","hommel","bonferroni","BH"/"fdr","BY"), as intable_multiresponse(). See Multiple-comparison adjustment. Typing it when the table has no test to adjust (noby, orp_value = FALSE) is an error.- labels
How each variable is named in the table: in the rows, in the title, and in the notes. Four forms:
labels = FALSEshows the column name.labels = TRUEshows the column'slabelattribute (as set byhaven,labelled, orlabel_from_names()) when it is present, notNA, and not empty, and the column name otherwise.labels = c(bmi = "Body mass index"), a named character vector, gives the columns it names these labels. Every other column followsoptions(spicy.var_labels): its name when the option is unset orFALSE, itslabelattribute when it isTRUE.labels = var_labels(bmi = "BMI (kg/m2)")gives the columns it names these labels, and every other column itslabelattribute, whatever the option says.var_labels()alone islabels = TRUE.
A name that matches no column of the table warns (class
spicy_unknown_name) and is left out, so one vector can still serve every table of a project. The default readsoptions(spicy.var_labels), and aTRUEorFALSEtyped in the call wins over it. Any other value,NULLincluded, is an error. A variable label documents the data file; a published table carries the wording you set here.note_labelsgives a variable a shorter label in the title and the notes.- note_labels
The label a variable takes where the table names it in a sentence, in the title and in the notes, in place of the one
labelsgives it: a named character vector,note_labels = c(sex = "sex at birth"), or avar_labels()object, which reads as one.labelscan then name a row with a whole question while the note says "Missing values removed: sex at birth (2).". The rows, the column headers and the spanners keep the labels oflabels, and so do the title and the notes for a variablenote_labelsdoes not name. A name that matches no column warns and is left out, as inlabels, andNULL(the default) renames nothing. A note label that another variable of the table shows is an error (classspicy_label_collision): choose another one, or relabel the other variable.- percent_digits
Number of digits for percentages in report outputs. Defaults to
1. A whole number: a fraction is an error, as for every*_digitsargument.- p_digits
Integer >= 1. Number of decimal places used to render p-values in the
pcolumn (default:3, the APA Publication Manual standard). Both the displayed precision and the small-p threshold derive from this argument:p_digits = 3prints.045and<.001;p_digits = 4prints.0451and<.0001. Leading zeros are always stripped, following APA convention.- decimal_mark
The decimal mark, a single character:
"."(the default) or","in most styles; the Lancet style ofspicy_style()uses the middle dot.- align
Horizontal alignment of the numeric columns in the console and in the rendered outputs:
"decimal"(the default),"center", or"right", as intable_categorical().- title
The table title.
NULL(the default) gives "Item battery", followed by "by" and thebyvariable under thelabelscontract (or its label innote_labels); a single string replaces it, andFALSEremoves it, as intable_multiresponse().- note
The table note.
NULL(the default) builds it, opening with "Note.": the direction of the percentages, the base, the levels left out, the weights, the rows removed and the items without data, then the tests, and the sentences ofnote_add. A single string replaces it, printed as typed after the lead-in ("Note.", unless the string opens with one),note_addafter it, andFALSEremoves it, as in every table function.- note_add
Sentences to add to the note: a character vector of one or more strings, each printed as typed (no period is added) and joined to the note by a space. They close its paragraph, after the definitions of the abbreviations and before the legend of the stars, in every output and in
attr(x, "note"), and the sentences the table writes stay. With a string innote, they follow it, so a wrapper can add a house sentence to every table while one table replaces its note. AnNAor an empty string is an error, and so isnote_addwithnote = FALSE, which leaves no note to add to.- output
Output format. One of:
"default"(an ASCII table object, printed when the call is bare)"data.frame"(the displayed table, numeric)"long"(a long numericdata.frame)"tinytable"(requirestinytable)"gt"(requiresgt)"flextable"(requiresflextable)"excel"(requiresopenxlsx2)"clipboard"(requiresclipr)"word"(requiresflextableandofficer)
- excel_path
Path for
output = "excel", ending in".xlsx", in a directory that exists. Defaults toNULL.- excel_sheet
Sheet name for Excel export.
NULL(the default) uses"Item battery".- clipboard_delim
Delimiter for clipboard text export. Defaults to
"\t". A cell holding the delimiter itself, a double quote or a line break is quoted RFC 4180-style, so the grid survives whatever delimiter you choose.- word_path
File path for
output = "word". Defaults toNULL.- style
A journal style: a theme name (
"jama","nejm","lancet","annals","apa","aer"), aspicy_style()object, orNULL(the default). A style only changes DEFAULTS – any argument you pass explicitly wins over it. Setoptions(spicy.style = )for document-wide scope. A theme covers numeric formatting, the word that opens the note and the abbreviations the note defines, not full editorial conformity;?spicy_stylelists the exact rules each one encodes and the official document they come from. An unknown name is an error.
Value
Depends on output:
"default": aspicy_battery_table(adata.framesubclass), with the rendering metadata as attributes, returned visibly, so a bare call prints the ASCII table."data.frame": the displayed table with its numbers, what as.data.frame() of the default object returns: one row per item, with columnsvariable(the item's column),label(the label the row displays), then the printed columns: one percentage per level shown,(Missing)underdrop_na = FALSE, the summary column oftop(named by its header,"Strongly agree + Agree") andn, each prefixed with the group underby("Female Agree","Female n"), andpwithp_value."long": one row per item, group and level, with columnsvariable,label,group(withby),level(the level, or the label of the column of missing answers),.row_role("level", or"missing"for the missing answers),n(the count, weighted underweights) andpct(its percentage of the base), and, when the tests ran,test_type("chi_squared","n1_chi_squared","fisher_exact","fisher_midp","fisher_exact_mc"),statisticanddf(NAfor an exact test),p.value, andp_unadjustedunder an adjustment. The summary column is not part of it: it is the sum of the levels oftop."tinytable","gt","flextable": the rendered table object."excel"/"word": writes to disk and returns the file path invisibly."clipboard": copies the table and returns the displaydata.frameinvisibly.
The default object and the data frames carry the note as printed in
attr(x, "note") (its lead-in included, NULL under note = FALSE).
as_structured() reads the default object: one "summary" row per
item, token "pct" for each percentage column (with scale_level, the
level it shows, missing on the column of missing answers, and top
with top_levels on the summary column), "n" and "p". inline() cites
a cell: inline(x, item, "Agree", "pct"), with model = "Female"
under by.
Items and levels
The items of a battery share one scale. Without levels, the scale is
read from the items:
items that declare their levels, factors and labelled columns with value labels (as
"[code] label", the conversion offreq()andtable_categorical()), must declare the same set. An item whose levels differ is an error (classspicy_invalid_input) that names it with its levels. The same set in another order is accepted, and the columns follow the order of the first item;items that declare none, numeric, character and logical columns, take the union of the values they hold: numbers sorted, text in the order of first appearance across the items, so a level that one item alone holds is a column of every item;
a mix of the two kinds is an error that names the first item of the other kind.
Given, levels is the scale, and nothing is refused: a level that no
item holds shows at 0, and every other answer, observed or declared, is
left out of the percentages and the base and named in the note. An item
without any answer shows a dash. Declared missing values of a labelled
column (na_values, na_range) are missing.
A level that no respondent chose keeps its column, at 0, as a level of the scale that was offered and not chosen. A dash marks a cell with no base: an item without a valid answer, in the table or in a group.
The percentages are row percentages: those of an item sum to 100 over
the levels shown (the column of missing answers included under
drop_na = FALSE). Its base, n, is its valid answers on the levels
shown, or every row under drop_na = FALSE; an answer that levels
leaves out is out of the base either way. A percentage carries no more
digits than its denominator, so a base of fewer than 100 respondents
takes whole percentages: percent_digits = 0 prints them.
The table computes no mean or score of the scale: the levels of a rating scale are ordered, not spaced on an interval.
Tests
With by, each item gets a test of independence of the item and the
group, on its table of the levels shown by the groups, its missing
answers left out whatever drop_na. The test is the one test
chooses, with the rule of table_categorical(): under "auto", the
Pearson chi-squared test, without continuity correction, when every
expected count is at least 5, and Fisher's exact test otherwise; when
the exact enumeration fails, the p-value is its Monte Carlo estimate
over 2000 tables, which draws from the session's RNG. A level or a
group that holds no answer to the item leaves its table. An item left
with one level has no test: its p cell shows a dash and the note
names it. An item with answers in fewer than two groups has no test
either, and its p cell is blank. Without by, the table runs no
test.
Fisher's exact test and its mid-p version condition on counts. Under
weights that are not whole numbers, test = "fisher" and
test = "midp" are an error (class spicy_unsupported), and an item
that "auto" or "n1" would send to Fisher's exact test keeps the
chi-squared test, with a warning (class spicy_fallback).
Multiple-comparison adjustment
The items of a battery are read together, as a family of tests, so the
p column is adjusted by Holm's method by default, as for the items of
table_multiresponse(). Holm's method controls the family-wise error
rate whatever the dependence between the items. The raw p-values stay
in the long output as p_unadjusted, and in col_meta$p$p_unadjusted
of as_structured(). The note names the method and the number of
items adjusted together.
See also
table_categorical() for one block of rows per variable,
table_multiresponse() for a "check all that apply" question,
table_battery_svy() for a survey design; the
article Item batteries.
Examples
# The four life_sat_* items of sochealth rate satisfaction with four
# domains of life on the same scale, from 1 to 5.
sat <- sochealth[c("sex", "age_group")]
scale_labels <- c(
"Very dissatisfied", "Dissatisfied", "Neutral",
"Satisfied", "Very satisfied"
)
for (v in grep("^life_sat_", names(sochealth), value = TRUE)) {
sat[[v]] <- factor(sochealth[[v]], levels = 1:5, labels = scale_labels)
attr(sat[[v]], "label") <- attr(sochealth[[v]], "label")
}
# One row per item, one column per level, row percentages and the base.
table_battery(sat, select = starts_with("life_sat_"), labels = TRUE)
#> Item battery
#>
#> Variable │ Very dissatisfied Dissatisfied
#> ────────────────────────────────────────────┼─────────────────────────────────
#> Satisfaction with health (1-5) │ 7.4 14.2
#> Satisfaction with work (1-5) │ 7.6 16.3
#> Satisfaction with relationships (1-5) │ 3.1 10.9
#> Satisfaction with standard of living (1-5) │ 6.4 17.1
#>
#> Variable │ Neutral Satisfied
#> ────────────────────────────────────────────┼────────────────────
#> Satisfaction with health (1-5) │ 24.0 25.1
#> Satisfaction with work (1-5) │ 26.4 30.3
#> Satisfaction with relationships (1-5) │ 26.3 29.7
#> Satisfaction with standard of living (1-5) │ 26.0 31.5
#>
#> Variable │ Very satisfied n
#> ────────────────────────────────────────────┼──────────────────────
#> Satisfaction with health (1-5) │ 29.4 1192
#> Satisfaction with work (1-5) │ 19.5 1192
#> Satisfaction with relationships (1-5) │ 29.9 1192
#> Satisfaction with standard of living (1-5) │ 19.0 1192
#>
#> Note. Percentages are row percentages. Base: respondents who answered the item.
# The top of the scale, the items sorted on it.
table_battery(
sat,
select = starts_with("life_sat_"),
top = c("Very satisfied", "Satisfied"),
sort = "top-",
percent_digits = 0
)
#> Item battery
#>
#> Variable │ Very dissatisfied Dissatisfied Neutral Satisfied
#> ────────────────────────┼─────────────────────────────────────────────────────
#> life_sat_relationships │ 3 11 26 30
#> life_sat_health │ 7 14 24 25
#> life_sat_standard │ 6 17 26 31
#> life_sat_work │ 8 16 26 30
#>
#> Variable │ Very satisfied Very satisfied + Satisfied n
#> ────────────────────────┼──────────────────────────────────────────────────
#> life_sat_relationships │ 30 60 1192
#> life_sat_health │ 29 54 1192
#> life_sat_standard │ 19 51 1192
#> life_sat_work │ 19 50 1192
#>
#> Note. Percentages are row percentages. Base: respondents who answered the item.
# By sex: the top of the scale in each group, and a test per item.
table_battery(
sat,
select = starts_with("life_sat_"),
by = sex,
top = c("Very satisfied", "Satisfied")
)
#> Item battery by sex
#>
#> Variable │ Female Very satisfied + Satisfied Female n
#> ────────────────────────┼─────────────────────────────────────────────
#> life_sat_health │ 52.9 616
#> life_sat_work │ 49.1 615
#> life_sat_relationships │ 59.8 615
#> life_sat_standard │ 50.9 615
#>
#> Variable │ Male Very satisfied + Satisfied Male n p
#> ────────────────────────┼───────────────────────────────────────────────
#> life_sat_health │ 56.1 576 .798
#> life_sat_work │ 50.4 577 .052
#> life_sat_relationships │ 59.4 577 .798
#> life_sat_standard │ 50.1 577 .397
#>
#> Note. Percentages are row percentages. Base: respondents who answered the item. Item p-values: Pearson chi-squared test of each item by group. Adjusted for 4 tests (Holm method).
# The neutral level left out of the base, missing answers shown.
table_battery(
sat,
select = starts_with("life_sat_"),
levels = scale_labels[-3],
drop_na = FALSE
)
#> Item battery
#>
#> Variable │ Very dissatisfied Dissatisfied Satisfied
#> ────────────────────────┼────────────────────────────────────────────
#> life_sat_health │ 9.6 18.5 32.7
#> life_sat_work │ 10.2 21.9 40.8
#> life_sat_relationships │ 4.2 14.7 40.0
#> life_sat_standard │ 8.5 22.9 42.1
#>
#> Variable │ Very satisfied (Missing) n
#> ────────────────────────┼────────────────────────────────
#> life_sat_health │ 38.3 0.9 914
#> life_sat_work │ 26.2 0.9 885
#> life_sat_relationships │ 40.3 0.9 886
#> life_sat_standard │ 25.5 0.9 890
#>
#> Note. Percentages are row percentages. Base: all respondents but those whose answer is left out, including those who did not answer the item. Answers left out of the percentages and the base: Neutral.
# The integer items as they are: `levels` gives the scale.
table_battery(sochealth, select = starts_with("life_sat_"), levels = 1:5)
#> Item battery
#>
#> Variable │ 1 2 3 4 5 n
#> ──────────────────────────┼───────────────────────────────────────────────
#> life_sat_health │ 7.4 14.2 24.0 25.1 29.4 1192
#> life_sat_work │ 7.6 16.3 26.4 30.3 19.5 1192
#> life_sat_relationships │ 3.1 10.9 26.3 29.7 29.9 1192
#> life_sat_standard │ 6.4 17.1 26.0 31.5 19.0 1192
#>
#> Note. Percentages are row percentages. Base: respondents who answered the item.
# Long data frame.
head(table_battery(sat, select = starts_with("life_sat_"), output = "long"))
#> variable label level .row_role n pct
#> 1 life_sat_health life_sat_health Very dissatisfied level 88 7.382550
#> 2 life_sat_health life_sat_health Dissatisfied level 169 14.177852
#> 3 life_sat_health life_sat_health Neutral level 286 23.993289
#> 4 life_sat_health life_sat_health Satisfied level 299 25.083893
#> 5 life_sat_health life_sat_health Very satisfied level 350 29.362416
#> 6 life_sat_work life_sat_work Very dissatisfied level 90 7.550336
# \donttest{
if (requireNamespace("tinytable", quietly = TRUE)) {
tt <- table_battery(
sat,
select = starts_with("life_sat_"),
by = sex,
labels = TRUE,
output = "tinytable"
)
}
# }