Builds a publication-ready frequency or cross-tabulation table for one or many categorical variables selected with tidyselect syntax.
With by, produces grouped cross-tabulation summaries (using
cross_tab() internally) with a p-value per variable, from Pearson's
chi-squared test or Fisher's exact test (see test), and optional
association measures.
Without by, produces one-way frequency-style summaries.
Multiple output formats are available via output: a printed ASCII
table ("default"), a numeric data.frame ("data.frame", the
displayed table, or "long", one row per variable, level and group),
or publication-ready tables ("tinytable", "gt", "flextable",
"excel", "clipboard", "word").
Usage
table_categorical(
data,
select = tidyselect::everything(),
by = NULL,
labels = getOption("spicy.var_labels", FALSE),
note_labels = NULL,
levels_keep = NULL,
factor_levels = c("observed", "all"),
include_total = TRUE,
total_row = FALSE,
drop_na = FALSE,
weights = NULL,
rescale = getOption("spicy.rescale", FALSE),
correct = FALSE,
simulate_p = FALSE,
simulate_B = 2000,
test = c("auto", "chisq", "fisher", "midp", "n1"),
p_value = NULL,
p_adjust = "none",
percent = c("column", "row"),
percent_base = c("valid", "total"),
percent_digits = 1,
p_digits = 3,
effect_size_digits = 2,
assoc_measure = "auto",
assoc_ci = FALSE,
smd = FALSE,
decimal_mark = ".",
align = c("decimal", "center", "right"),
note = NULL,
note_add = NULL,
output = c("default", "data.frame", "long", "tinytable", "gt", "flextable", "excel",
"clipboard", "word"),
indent_text = " ",
indent_text_excel_clipboard = strrep(" ", 6),
add_multilevel_header = TRUE,
blank_na_wide = FALSE,
excel_path = NULL,
excel_sheet = NULL,
clipboard_delim = "\t",
word_path = NULL,
user_na = TRUE,
style = NULL,
v_digits
)Arguments
- data
A data frame.
- select
Columns to include as row variables. Supports tidyselect syntax and character vectors of column names. When omitted, defaults to every eligible categorical column in
data: factor, character, logical, and labelled (haven/labelled) columns, excluding thebycolumn – matching the select-less defaults oftable_continuous()andtable_continuous_lm(). An explicitselectis taken verbatim (numeric columns included), so numeric-coded categorical variables can still be tabulated by naming them. Thebyandweightscolumns are not part of the selection:everything()and the other helpers skip them, and naming one inselect, or giving its position, is an error. A name that is not a column ofdatais an error (classspicy_missing_column), and so is a selection with no column to tabulate. The levels of a variable are its rows, in the orderbygives its groups (seeby), a labelled variable's included.- by
Optional grouping column used for columns/groups. Accepts an unquoted column name or a single character column name. Factor levels keep their declared order, which is the way to choose another order; any other
byis sorted, as infreq()andcross_tab(): text in the C locale (capitals before lower case, the same on every machine),FALSEbeforeTRUE, numbers ascending, whatever the order of the rows. The column of missing values comes last. A haven labelledbynames its groups by their value labels, as the rows of a labelled variable name its levels ("[1] Women"), in the order of itslabelsattribute when every observed value carries a label and by ascending code otherwise; a value label no observation holds adds no group, and declared missing values followuser_naas usual. The rule of the whole family.- 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.- levels_keep
Levels to keep, in the order to display them. If
NULL(the default), all the levelsfactor_levelsdisplays are kept. A declared level with no observation that it names is shown with a count of 0, whateverfactor_levelssays. Entries must match the level strings the table displays (for labelled columns these are the"[code] label"strings, not the bare label text). Two forms, as intable_categorical_svy():a character vector applies to every variable: each keeps the levels it has, and a variable with none of them is dropped from the table with a classed warning (
spicy_no_selection) listing its level strings;a named list applies per variable (
levels_keep = list(smoking = "Yes")): each name must be a variable of the table, each level one the variable declares or displays (an error otherwise), and the variables it does not name keep all their levels.
- factor_levels
Character. Controls how factor and labelled values are displayed in the table, as in
freq()."observed"(the default; matches Stata'staband SPSSFREQUENCIES, which both list only values present in the data) shows only levels present in the data."all"keeps every declared level, including unused ones, which appear withn = 0(in every column underby). A character or logical column declares no level. The rows of unused levels are displayed only: they change no percentage of the other rows and no test or association measure.- include_total
Logical. If
TRUE(the default), includes aTotalgroup when available. A level ofbydisplayed with the label of that margin is then an error (classspicy_label_collision): rename the level, or give the margin another label withoptions(spicy.labels = list(header_margin_total = "All")).- total_row
Logical. If
TRUE, closes the block of each variable with aTotalrow under its levels (after the"(Missing)"row when there is one): the count of the variable in each group column and in theTotalcolumn, the"(Missing)"row included, and its percentage, 100 in every column underpercent = "column"and, underpercent = "row", the share of each group in the variable's total, the base rate the row percentages are compared with (on the observed values under the defaultpercent_base). Withoutby, the row holds the count of the variable and 100. A thin rule sets the row apart, in the console and in the rendered outputs. The row never enters the test, the association measure or the SMD. It takes the label of the margin, so a level displayed with that label is then an error (classspicy_label_collision), as beside theTotalcolumn.FALSE(the default) adds no row: the note gives the sample size.- drop_na
Logical. The rule of the family: a table that counts shows the missing values by default, as a frequency table reports how many answers are missing; a table that computes a statistic removes them by default and counts them in the note (see
table_continuous()). This table counts, soFALSE(the default) displays the missing values as a dedicated"(Missing)"level (and, underby, a"(Missing)"group column) – the field convention for descriptive tables (gtsummary's "Unknown" row, janitor'sNArow; see the Epidemiologist R Handbook, Descriptive tables). Under the defaultpercent_base = "valid", the row shows their count and no percentage: the percentages are on the observed values, and the Total row of row percentages too. A level displayed beside that row or column with its label is an error (classspicy_label_collision): rename the level, or give the row another label withoptions(spicy.labels = list(row_missing_level = "(Not stated)")). IfTRUE, rows withNAin the tabulated variable (and inby, when supplied) are removed BEFORE each cross-tabulation, and the removal is disclosed in a table note,bylast ("Missing values removed: smoking (25), sex (40).") rather than silent: a missingbytakes the row out of every variable of the table, a missing value only out of its own variable. Abythat no observation holds is then an error (classspicy_invalid_data).- weights
Optional weights. Either
NULL(the default), a numeric vector of lengthnrow(data), or a single column indatasupplied as an unquoted name or a character string. Rows with anNAweight leave the table, and the note says how many.- rescale
Logical. If
FALSE, weights are used as-is. IfTRUE, rescales weights so total weighted N matches raw N. Passed tospicy::cross_tab(). The default readsoptions(spicy.rescale),FALSEwhen it is unset, matchingcross_tab().- correct
Logical. If
FALSE(the default), no continuity correction is applied. IfTRUE, applies Yates correction in 2x2 chi-squared contexts. Passed tospicy::cross_tab(). A variable tested by Fisher's exact test, its mid-p version, or the N-1 chi-squared (seetest) takes no correction.- simulate_p
Logical. If
FALSE(the default), uses asymptotic p-values. IfTRUE, uses Monte Carlo simulation. Passed tospicy::cross_tab(). Applies to the Pearson chi-squared test only (seetest).- simulate_B
Integer. Number of Monte Carlo replicates when
simulate_p = TRUE, and for the Monte Carlo version of Fisher's exact test (seetest). Defaults to2000.- test
Character. The test of each variable when
byis used. One of"auto"(default),"chisq","fisher","midp", or"n1"."auto": Pearson's chi-squared test when every expected count of the variable's table is at least 5, Fisher's exact test otherwise, the rule of gtsummary'sadd_p(). The choice is made per variable, and the note names the test of each."chisq": Pearson's chi-squared test for every variable, the behavior before 0.14.0."fisher": Fisher's exact test for every variable."midp": the Fisher mid-p test for a 2x2 table."n1": the N-1 chi-squared test for a 2x2 table whose expected counts are all at least 1, Fisher's exact test for one with a smaller count (Campbell's rule).
Under
"midp"and"n1", a variable whose table is not 2x2 follows"auto".correctandsimulate_papply to the Pearson chi-squared test only. When the exact enumeration of a large sparse table fails, Fisher's p-value is its Monte Carlo estimate oversimulate_Btables, and the note says so; callset.seed()first to reproduce it. See Tests. Ignored whenbyis not used or whenp_value = FALSE.- p_value
Logical or
NULL. IfTRUEandbyis used, adds the p-value column of the test chosen bytest. WhenNULL(the default), the p-value is shown automatically wheneverbyis supplied, and hidden otherwise. Passp_value = FALSEto remove the test from every output: thepcolumn, and thetest_type,statistic,dfandp.valuecolumns of"long";test,correctandsimulate_pare then ignored, with a warning. The association measure keeps its own switch, sop_value = FALSE, assoc_measure = "none"gives a purely descriptive table. Ignored whenbyis not used.- p_adjust
Multiple-comparison adjustment of the
pcolumn, applied to the family of every test in the table (one p-value per variable). One of"none"(default),"holm","hochberg","hommel","bonferroni","BH"/"fdr", or"BY", as intable_regression(); delegated tostats::p.adjust(). Needsbyand thepcolumn. See Multiple-comparison adjustment.- percent
The direction of the percentages under
by, as incross_tab():"column"(the default), the distribution of each group over the levels of the variable, summing to 100 down each group; or"row", the share of each level falling in each group, summing to 100 across the groups, so that theTotalcolumn shows 100 on every level row. Unique abbreviations are accepted ("c","r"). Only the displayed shares change: the counts, the test, the association measure and the SMD are the same in both directions. The note says the direction in both cases ("Percentages are column percentages.", "Percentages are row percentages."). Withoutby, each variable has one distribution to show, andpercent = "row"is ignored with a warning (classspicy_ignored_arg).- percent_base
The base of the percentages of a variable whose missing values the
"(Missing)"row shows (drop_na = FALSE)."valid"(the default) computes them on the observed values of the variable, as gtsummary does: the levels sum to 100, and the"(Missing)"row shows its count and no percentage (a blank cell,NAin the data frames)."total"computes them on every row, the"(Missing)"row included, as SPSS does and as this function did before 0.14.0. Underby, the base is the observed values of each column, theTotalcolumn and the column of missingbyvalues included. Row percentages keep each row as its base, the"(Missing)"row included, which keeps its row percentages; the Total row oftotal_row = TRUEgives the share of each group on the observed values under"valid". The test, the association measure and the SMD run on the observed values whatever the base. Without missing values to show (drop_na = TRUE, or a variable with none) the two bases give the same percentages. Unique abbreviations are accepted ("v","t").- 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.- effect_size_digits
Number of digits for the association measure, its confidence interval and the SMD, as for the effect sizes of the other table functions. Defaults to
2.- assoc_measure
Which association measure to report alongside the p-value. Accepts four input shapes:
"none"– drop the column entirely."auto"(the default) – pick a measure per row variable based on the variable type: a 2x2 table (binary row variable vs. binaryby) usesphi, a pair of ordered factors usestau_b, every other case usescramer_v.a single string from
c("cramer_v", "phi", "gamma", "tau_b", "tau_c", "somers_d", "lambda")– applied uniformly to every row variable.a character vector with one entry per row variable. Both named (
c(smoking = "phi", health = "tau_b"), recommended; unnamed variables fall back to"auto") and unnamed positional (c("phi", "tau_b", "auto"), paired up withselect) are accepted. Named is more robust to reordering ofselect.
When a single measure is used for every row, its symbol heads the column and names it in the data frames, as the effect size of
table_continuous_lm()is headed: \(V\) (cramer_v), \(\phi\) (phi), \(\tau_b\) (tau_b) or \(\gamma\) (gamma), the three others keeping their name ("Lambda"). The note defines the symbol as it defines any abbreviation, under theabbreviationsrule ofspicy_style(): "V = Cramér's V" by default, the Greek letters only under"all"and in the"jama"and"lancet"themes. When multiple measures are used (typically with"auto"on a heterogeneousselect), the header collapses to"Effect size"and the table note names the measure used for each variable.phirequires a 2x2 table; if explicitly requested for a non-2x2 variable, an error is raised so the user can choose another measure or fall back to"auto".- assoc_ci
Passed to
cross_tab(). IfTRUE, includes the 95\ and in rendered formats ("gt","tinytable","flextable","word"), the CI is shown inline (e.g.,.14 [.08, .19]) under a header that states its level (V [95% CI]). In the"data.frame","excel"and"clipboard"outputs, two extra columns headed95% CI LL/95% CI ULare added, the level stated as the other tables state it; in the long output ("long") the bounds filles_ci_lower/es_ci_upper. The note definesLLandULin the"excel"and"clipboard"outputs only, whose headers show them. Defaults toFALSE.- smd
Logical. If
TRUE, adds anSMDcolumn holding the standardized mean difference between the two groups ofby, the balance diagnostic of the Table 1 literature. Requires exactly two groups; the value sits on the variable row besidep, never on a level row. Signed for a two-category variable (group 1 minus group 2 on the second category), unsigned for three or more, where it is a distance. No confidence interval and no p-value, by design. Rounded witheffect_size_digits. See the "Standardized mean difference" section below. Defaults toFALSE.- 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 numeric columns in the printed ASCII table and in the
tinytable,gt,flextable,word, andclipboardoutputs. The first column (Variable) is always left-aligned. One of:"decimal"(default): align numeric columns on the decimal mark, the standard scientific-publication convention used by SPSS, SAS, and LaTeXsiunitx. Numeric cells are pre-padded with figure-spaces (U+2007, digit-width) so every string in a column has the same width with the decimal mark at the same internal position; centering those uniform-width strings then stacks the decimal points vertically. The same pad-then-center strategy is applied on every rendering engine (gt,tinytable,flextable,word, ASCII print) for a homogeneous rendering, matchingtable_regression()andtable_continuous_lm(). Theclipboardoutput is delimited text meant to be parsed rather than read at a fixed width, so its cells travel unpadded (a padded number pastes as text next to an unpadded number)."center": center-align all numeric columns."right": right-align all numeric columns.
In the
exceloutput,"center"centers the numeric columns and"right"is the same rendering as the default: cell-string padding does not align decimals under a proportional font, so"decimal"right-aligns instead, which combined with the per-columnnumfmtalready produces dot-aligned columns. Same default and same three values astable_continuous()/table_continuous_lm(), whose workbooks resolve"decimal"differently:table_continuous()right-aligns only the counts and the p-value there, andtable_continuous_lm()applies that convention at everyalign.- note
The note under the table.
NULL(the default) builds it: the general note, one paragraph opened by the lead-in of the style ("Note." by default, none under thejamaandlancetthemes; seenote_labelinspicy_style()), then, in atable_regression()withstars, the legend of the stars on a line of its own. A single string replaces the general note, the sentences the table writes about its data included. It is printed as typed, line breaks ("\n") included, after the lead-in of the style, which a string opening with a lead-in of its own does not repeat (note = "Note. Data weighted."), and the legend of the stars stays below it.""keeps that legend alone, and no note at all without stars.FALSEremoves the note from every output, the legend included.- 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)
- indent_text
Prefix used for modality labels in report table building. Defaults to
" "(two spaces).- indent_text_excel_clipboard
Stronger indentation used in Excel and clipboard exports. Defaults to six non-breaking spaces.
- add_multilevel_header
Logical. If
TRUE(the default), merges top headers in Excel export. Only consulted foroutput = "excel"on a grouped table (bysupplied): typed with anotheroutput, it is ignored with a warning (classspicy_ignored_arg), like the other arguments of one output (excel_path,excel_sheet,clipboard_delim,word_path, ...), and a one-way table, which has a single header row to begin with, does not use it.- blank_na_wide
Logical. If
FALSE(the default),NAvalues are kept as-is inoutput = "data.frame". IfTRUE, the empty cells of its statistic columns become empty strings (the columns become text).- 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"Categorical".- 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", ending in".docx", in a directory that exists. Defaults toNULL. Before 0.13.0, supplying it withoutput = "flextable"also wrote a.docxas a side effect; it is now used exclusively byoutput = "word"(the contract shared with the rest of the table family) and is ignored, with a warning, under any otheroutput.- user_na
Logical. If
TRUE(the default), declared missing values in the row variables and inbyare treated as missing: they join the"(Missing)"level underdrop_na = FALSE, and underdrop_na = TRUEthey are removed with a dedicated disclosure line (Declared missing values removed: ...). IfFALSE, the declared codes stay valid categories. See the "Declared missing values" section offreq().- 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.- v_digits
Defunct. Renamed
effect_size_digitsin spicy 0.14.0, as in the other table functions; supplyingv_digitsis an error (classspicy_defunct).
Value
Depends on output:
"default": adata.frameof class"spicy_categorical_table"carrying the rendering metadata as attributes. Its columns are internal and can change without notice: read the numbers from"data.frame","long",broom::tidy(),broom::glance()oras_structured(). The object is returned visibly, so a baretable_categorical(...)call auto-prints the styled ASCII table at the console whilet <- table_categorical(...)stays silent (printtto display the table)."data.frame": the displayed table with its numbers, what as.data.frame() of the default object returns: one row per printed row – the header row of each variable, which carries its p-value and its association measure, then one row per level. The key columns arevariable(the column name),label(the label the rows display for it, seelabels),leveland.row_role("factor_header","level","missing"for the row of missing values the table adds, or"total"for the Total row oftotal_row = TRUE), then one column per displayed column, the name of a column of a group joining the group printed above it to the header printed under it: one pair<group> n/<group> %per group level (plusTotalwheninclude_total = TRUE),p(absent withp_value = FALSE), the association measure (named by its header: the symbol of the measure,V, its name for a measure without one, orEffect sizewhen the row variables do not share one), its bounds95% CI LL/95% CI ULwithassoc_ci = TRUE, andSMDwithsmd = TRUE. Whenby = NULL, these columns arenand%."long": a longdata.frame, one row per variable, level and group, with columnsvariable,label,level,.row_role(the role oflevel:"level","missing"for the row of missing values, or"total"for the Total row oftotal_row = TRUE), then, whenbyis used,group,.group_role(the role ofgroup:"group","missing"for the column of missingbyvalues, or"total"for the margin) andtotal(TRUEon the rows of the margin and of the Total row), thenn,pct(the percentage the table displays, in the direction ofpercent), then, withby, the test of the variable unlessp_value = FALSE:test_type("chi_squared"for Pearson's,"n1_chi_squared"for the N-1 chi-squared,"fisher_exact","fisher_midp"for the Fisher mid-p test, or"fisher_exact_mc"for the Monte Carlo estimate of Fisher's exact test),statisticanddf(the chi-squared, Pearson's or the N-1,NAfor a variable tested by an exact test),p.value(andp_unadjustedunderp_adjust); and the association measure, whichever measure it is:es_typenames it per row ("cramer_v","phi", ...),es_valueholds it,es_ci_lower/es_ci_upperits interval (NAunlessassoc_ci = TRUE), andsmd_type("binary"or"multinomial", the kernel the value came from) /smd_valuethe standardized mean difference. The measure and SMD columns are there whether or not they were requested,NAwhen not, so the schema does not move with an argument. The statistics of a variable sit on every row of its block."tinytable": atinytableobject."gt": agt_tblobject."flextable": aflextableobject."excel"/"word": writes to disk and returns the file path invisibly."clipboard": copies the table and returns the copied text invisibly.
The note opens with the sample size, "N = 159." or, with by,
"N = 159: Women (n = 80), Men (n = 79).": the rows of the data the table
describes, and those of each group column in column order, the column
of missing by values included, so that the n add up to N. A row
missing by under drop_na = TRUE and a row with a missing weight
leave the table and are not counted; the note says how many. Under
weights the cells hold weighted counts and N counts rows: "N = 60
(unweighted).", a row of zero weight left out, as stats::nobs()
leaves it out of a weighted fit. One N per table, the missing values of
a variable in its "(Missing)" row or in the note. With by, the direction of the
percentages follows, then the weights. Their base takes no sentence:
the table shows it (see percent_base).
The note, the drop_na = TRUE disclosure included, travels with the
table on every route, not just the console: "default" prints it
under the ASCII table, "tinytable" / "gt" / "flextable" /
"word" carry it as a table note, "excel" writes it below the body,
and the default object and the data frames of "data.frame" /
"long" keep the whole note as printed, its lead-in included, in
attr(x, "note") (NULL when there is nothing to say), so a pipeline
that renders the numbers itself can still state what left the table.
The default object also keeps the title the console prints, in
attr(x, "title").
The same three objects record the direction of their percentages in
attr(x, "percent"): "column" or "row" (see percent), and
"column" for a table without by; and their base in
attr(x, "percent_base"), "valid" or "total". The percentage
columns of "data.frame", pct in "long" and the proportion of
broom::tidy() hold the shares the table displays, in that direction
and on that base: NA on the "(Missing)" row under "valid".
On the "tinytable" route the note is set one size down;
options(spicy.note_style) governs that (see table_regression()).
The Excel sheet carries the same title the console prints on its first row; the table itself starts on row 3.
Tests
When by is used, each selected variable is cross-tabulated
against the grouping variable with cross_tab() and tested on the
complete cases of its observed levels, a "(Missing)" level and a
declared level nobody holds left out: one p-value per variable,
in the p column. A table left with a single row or column (a
variable observed at one level, or in one group) is uninformative and
gets no test and no association measure, whatever test and
assoc_measure say: its p and measure cells show an en dash, the
undefined cell, and the note names no measure for it. A blank cell
only means a statistic that was not requested or does not apply.
The chi-squared approximation is unreliable when expected counts are
small. Under test = "auto", the default, a variable takes Pearson's
chi-squared test (stats::chisq.test()) when every expected count of
its table is at least 5, and Fisher's exact test
(stats::fisher.test()) otherwise: the rule gtsummary's add_p()
applies, with the same p-values. Fisher's exact test of a table
larger than 2x2 is the Freeman-Halton extension. When its exact
enumeration fails, which happens on large sparse tables, the
p-value is its Monte Carlo estimate over simulate_B tables drawn
with the observed margins: it is random, so call set.seed() first
to reproduce it. test = "chisq" tests every variable by the
chi-squared, the behavior before 0.14.0, and test = "fisher" every
variable by the exact test.
Fisher's exact test is conservative: its actual level is often well
below the nominal one, at a cost in power. test = "midp" gives a 2x2
table (a two-level variable by two groups) the Fisher mid-p test, as
Fagerland, Lydersen and Laake (2017, equation 1.12, p. 28, and section
4.4.6, p. 97) and Lydersen, Fagerland and Laake (2009, equation 12,
p. 1165) define it: the tables with the observed margins are ordered
by their hypergeometric probability, and the p-value adds the
probabilities of the tables less probable than the observed one to
half the probabilities of the tables as probable, the observed one
included, ties being read within the relative tolerance of
stats::fisher.test(). The mid-p test can exceed the nominal level,
usually by little, and Fagerland, Lydersen and Laake (2017, p. 175)
recommend it for all sample sizes. A variable whose table is not 2x2
follows "auto", and the note names the test of each variable. The
central variant, twice the smaller one-sided mid-p (the exact2x2 and
epitools packages; Agresti, 2019, p. 49), coincides with this one
when the row totals or the column totals are equal, except that it
gives 1 for the most probable table; it differs otherwise and is not
computed.
test = "n1" applies the rule of Campbell (2007, pp. 18-19) to a 2x2
table. When every expected count is at least 1, the table takes the
N-1 chi-squared test: Pearson's statistic, with no continuity
correction, multiplied by (N - 1)/N and referred to a chi-squared
distribution with 1 degree of freedom, which is the Mantel-Haenszel
statistic of a single 2x2 table; statistic and df hold that
statistic in the long output.
Otherwise it takes Fisher's exact test, two-sided by Irwin's rule as
stats::fisher.test() computes it. Campbell found the N-1 test close
to its nominal level under that restriction, not without it. A
variable whose table is not 2x2 follows "auto", and the note names
the test of each variable.
The note names the test behind the p-values, "P-values: Pearson
chi-squared test (sex, education); Fisher exact test (smoking).", or
the one test when it covers them all, "P-value:" when the table shows
a single p-value. test_type in the long output gives the test of
each variable. See @param correct /
simulate_p to switch on Yates' continuity correction or Monte Carlo
p-values for the chi-squared test, and @param assoc_measure for
the per-row dispatch table used by "auto" (2x2 -> Phi, both ordered
-> Kendall's Tau-b, otherwise Cramér's V). Without by, the table
reports the marginal frequency distribution of each variable with no
inferential statistics.
The exact tests condition on counts. Under weights, they run on the
weighted table when its cells are whole numbers (frequency weights);
otherwise a variable that a rule sends to Fisher's exact test keeps
the chi-squared test, Pearson's under test = "auto" and the N-1
under Campbell's rule, with a warning (spicy_fallback), and
test = "fisher", or test = "midp" on a 2x2 table, is an error. For
survey weights, table_categorical_svy() gives the design-based test.
The test and the measure are two decisions with two arguments:
p_value = FALSE removes the test, assoc_measure = "none" the
measure. Together they give a purely descriptive table, which keeps
the group columns and their headers; smd stays available.
For model-based comparisons (cluster-robust SE, weighted contrasts,
fitted means) on continuous outcomes, see table_continuous_lm().
For descriptive (empirical) comparisons on continuous outcomes, see
table_continuous().
Groups with no data
A variable with no observed value in a group of by that holds rows,
typically a question not asked in that group, has no data there, which
is not a missing answer. Its cells in that group show an en dash
(NA in the data frames, in tidy()
and in as_structured()), and its rows there are left out of
everything computed for that variable: the missing values the note
counts, the Total column, the "(Missing)" row, the test, the
association measure and the SMD. A variable left with fewer than two
groups with data is not compared: its p and measure cells show the
dash, and the note names no test and no measure for it. The note
says "No data for X in G.", in one sentence for the variables with no
data in the same groups ("No data for X and Y in G."). Without by, a
variable with no observed value at all keeps its declared levels (its
one row when it declares none) with the dash, and the note says "No
data for X." The column of missing by values is never such a group,
and a declared level of by that holds no row keeps its zero column.
Multiple-comparison adjustment
The p-values of a descriptive table describe: they test no
prespecified hypothesis. STROBE (explanation of item 14) and the
SAMPL guidelines advise against significance tests in descriptive and
baseline tables, hence the default p_adjust = "none".
When a journal or a protocol asks for adjusted p-values anyway,
p_adjust applies stats::p.adjust() to the family of every test in
the table: one p-value per variable (one per block in
table_outcome()), never the rows of one variable, which share one
test. "holm" suits a confirmatory set of comparisons, "BH" a
screening one; table_regression() lists the methods. A test that
returned no p-value stays out of the family.
The adjustment runs before any formatting, so the p column and its
<.001 threshold show the adjusted values, and the table note gives
the number of tests and the method. The raw p-values stay in the
object: a p_unadjusted column next to the p-value column of the
returned data frames, and col_meta$p$p_unadjusted (with the method
in p_adjust and the family size in p_adjust_m) in
as_structured(). Without by, or with p_value = FALSE, the table
shows no p-value to adjust, and a p_adjust other than "none" is an
error.
Standardized mean difference
smd = TRUE adds an SMD column with the balance diagnostic of
the Table 1 literature, on the variable row beside p. For a
two-category variable it is the Bernoulli form,
$$\mathrm{SMD} = \frac{p_1 - p_2}{\sqrt{(p_1(1-p_1) + p_2(1-p_2)) / 2}}$$
with \(p\) the proportion of the SECOND category, signed,
group 1 minus group 2 in the order the table displays them. Note
the denominator: the Bernoulli variance \(p(1-p)\) at n, not
var() at \(n-1\), which would be 19% off on a small table.
"Second category" is the order the table shows, which is worth
knowing for a logical variable: spicy displays TRUE then
FALSE, so the sign is taken on FALSE, where tableone and
cobalt coerce with factor() (FALSE, TRUE) and take it on
TRUE – the same magnitude with the opposite sign. Convert to a
factor with the level order you want if the direction matters.
For three or more categories it is the multivariate form of Yang and Dalton (2012, SAS Global Forum 335-2012),
$$\mathrm{SMD} = \sqrt{T' S^{-} T}$$
with \(T\) the difference of the two profiles of proportions
(first category dropped) and \(S\) the mean of their multinomial
covariance matrices. This is a Mahalanobis distance: it is
unsigned, it is not bounded by 1, and \(S^{-}\) is a
pseudo-inverse, because a declared-but-unobserved category makes
\(S\) singular and solve() would abort where the
pseudo-inverse returns exactly the value that category's absence
implies. Which kernel a row took is published as smd_type in the
"long" output, and the unsigned reading is stated in the table
note whenever a variable has more than two categories. The
MASS package is needed for this arm only.
Two profiles with no category in common have an infinite
standardized distance. The pseudo-inverse would quietly publish a
finite number there, so the cell is an en-dash and a classed
warning says why. The same applies when each group is constant on
a different category, where the naive route publishes 0 –
"perfectly balanced" for the most imbalanced variable possible.
Conventions shared with table_continuous(): exactly two groups
(three or more are refused, not averaged over pairs); complete
cases on the observed groups, so a drop_na = FALSE "(Missing)"
level is displayed and never enters the diagnostic; no confidence
interval and no p-value, by design. Under weights the profiles
are the weighted proportions, which makes this column agree with
both the frequency and the survey-design readings – a profile of
proportions is invariant to a global rescaling of the weights, so
rescale cannot move it. (Only the continuous arm has a
convention to choose there.)
The SMD cell keeps its leading zero where the association cell
drops it: the APA strip belongs to a bounded measure, and this one
is not bounded. The two columns therefore print 0.45 and .45
side by side, on purpose.
A balance table usually shows no test: p_value = FALSE removes the
p column here as it does in table_continuous(), so a table mixing
continuous and categorical variables can drop both. inline()
quotes the cell from the variable row (inline(tbl, smoking, column = "smd")).
Display conventions
Decimal alignment, p-value formatting, and required suggested
packages per output engine are documented under @param align,
@param p_digits, and @param output respectively.
Counts are displayed as integers: weighted counts are rounded
(ties half to even, the R convention) at display time only, in
cells and margins alike – the SPSS Crosstabs convention. Cells
and margins are rounded independently, so small display
discrepancies are possible (e.g. two cells of exactly 0.5 each
display as 0 while their Total of 1.0 displays as 1). The
machine outputs ("data.frame", "long") carry the exact
weighted counts and full-precision percentages.
Declared missing values
Survey files imported with haven often carry declared missing
values: codes such as 8 = Don't know or 9 = Refused that the
source file marks as missing while keeping them distinct from a
plain NA. Two kinds of declaration exist: na_values / na_range
metadata on haven::labelled_spss() vectors, and tagged missing
values created by haven::tagged_na() (the Stata .a, .b, ...
convention). haven::read_sav() keeps the na_values / na_range
declaration only with user_na = TRUE: its default turns the
declared codes into NA on import.
spicy honors the declaration by default (user_na = TRUE):
declared missing values are excluded from every statistic exactly
like NA – valid percentages, means, chi-squared tests,
association measures, row-wise summaries, and group definitions –
but they are not erased from display. freq() lists each observed
declared value as its own row of the Missing block, with its value
label; cross_tab(), table_categorical(), and
table_continuous() disclose the exclusion in the table note
(Declared missing values removed: x (2).); varlist() counts
them as missing in N_valid / NAs / N_distinct while still
listing the declared codes in Values; code_book() counts them in
n_declared_missing and flags them in its values table.
Every function involved offers the same escape hatch: set
user_na = FALSE to ignore the declaration and treat the declared
codes as valid values (the behavior of spicy before 0.13.0).
Tagged missing values are genuine NAs either way; for them,
user_na = FALSE only collapses the per-tag breakdown back into
the regular NA count.
References
Agresti, A. (2019). An Introduction to Categorical Data Analysis (3rd ed.). Wiley.
Campbell, I. (2007). Chi-squared and Fisher-Irwin tests of two-by-two tables with small sample recommendations. Statistics in Medicine, 26(19), 3661-3675. doi:10.1002/sim.2832
Fagerland, M. W., Lydersen, S., & Laake, P. (2017). Statistical Analysis of Contingency Tables. Chapman and Hall/CRC. doi:10.1201/9781315374116
Lydersen, S., Fagerland, M. W., & Laake, P. (2009). Recommended tests for association in 2 x 2 tables. Statistics in Medicine, 28(7), 1159-1175. doi:10.1002/sim.3531
See also
table_continuous() for empirical comparisons on
continuous outcomes; table_continuous_lm() for the model-based
companion (heteroskedasticity-consistent / cluster-robust /
bootstrap / jackknife SE, fitted means, weighted contrasts);
cross_tab() for two-way cross-tabulations; freq() for
one-way frequency tables.
Other spicy tables:
table_continuous(),
table_continuous_lm()
Examples
# --- Basic usage ---------------------------------------------------------
# Default: ASCII console table grouped by sex.
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex
)
#> Categorical table by sex
#>
#> Female Male Total
#> ───────── ───────── ─────────
#> Variable │ n % n % n % p φ
#> ───────────────────┼────────────────────────────────────────────
#> smoking │ .713 .01
#> No │ 475 78.4 451 79.3 926 78.8
#> Yes │ 131 21.6 118 20.7 249 21.2
#> (Missing) │ 14 11 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │ .832 .01
#> No │ 334 53.9 316 54.5 650 54.2
#> Yes │ 286 46.1 264 45.5 550 45.8
#>
#> Note. N = 1200: Female (n = 620), Male (n = 580). Percentages are column percentages. P-values: Pearson chi-squared test.
# One-way frequency-style table (no `by`).
table_categorical(
sochealth,
select = c(smoking, physical_activity)
)
#> Categorical table
#>
#> Variable │ n %
#> ─────────────────────┼───────────────
#> smoking │
#> No │ 926 78.8
#> Yes │ 249 21.2
#> (Missing) │ 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │
#> No │ 650 54.2
#> Yes │ 550 45.8
#>
#> Note. N = 1200.
# Purely descriptive grouped table: no test, no association measure.
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex,
p_value = FALSE,
assoc_measure = "none"
)
#> Categorical table by sex
#>
#> Female Male Total
#> ───────────── ───────────── ─────────────
#> Variable │ n % n % n %
#> ─────────────────────┼─────────────────────────────────────────────
#> smoking │
#> No │ 475 78.4 451 79.3 926 78.8
#> Yes │ 131 21.6 118 20.7 249 21.2
#> (Missing) │ 14 11 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │
#> No │ 334 53.9 316 54.5 650 54.2
#> Yes │ 286 46.1 264 45.5 550 45.8
#>
#> Note. N = 1200: Female (n = 620), Male (n = 580). Percentages are column percentages.
# A small sample: `smoking` has an expected count below 5 and takes
# Fisher's exact test; the note names the test of each variable.
table_categorical(
sochealth[1:40, ],
select = c(smoking, physical_activity),
by = sex
)
#> Categorical table by sex
#>
#> Female Male Total
#> ──────────── ──────────── ────────────
#> Variable │ n % n % n % p φ
#> ─────────────────────┼─────────────────────────────────────────────────────────
#> smoking │ .258 .20
#> No │ 16 72.7 16 88.9 32 80.0
#> Yes │ 6 27.3 2 11.1 8 20.0
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │ .726 .06
#> No │ 11 50.0 8 44.4 19 47.5
#> Yes │ 11 50.0 10 55.6 21 52.5
#>
#> Note. N = 40: Female (n = 22), Male (n = 18). Percentages are column percentages. P-values: Pearson chi-squared test (physical_activity); Fisher exact test (smoking).
# Pretty labels keyed by column name.
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = education,
labels = c(
smoking = "Current smoker",
physical_activity = "Physical activity"
)
)
#> Categorical table by education
#>
#> Lower secondary Upper secondary Tertiary Total
#> ─────────────── ─────────────── ───────── ─────────
#> Variable │ n % n % n % n %
#> ───────────────────┼────────────────────────────────────────────────────────
#> Current smoker │
#> No │ 179 69.6 415 78.7 332 84.9 926 78.8
#> Yes │ 78 30.4 112 21.3 59 15.1 249 21.2
#> (Missing) │ 4 12 9 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Physical activity │
#> No │ 177 67.8 310 57.5 163 40.8 650 54.2
#> Yes │ 84 32.2 229 42.5 237 59.2 550 45.8
#>
#> Variable │ p V
#> ───────────────────┼────────────
#> Current smoker │ <.001 .14
#> No │
#> Yes │
#> (Missing) │
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌
#> Physical activity │ <.001 .21
#> No │
#> Yes │
#>
#> Note. N = 1200: Lower secondary (n = 261), Upper secondary (n = 539), Tertiary (n = 400). Percentages are column percentages. P-values: Pearson chi-squared test. V = Cramér's V.
# Survey weights with rescaling.
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = education,
weights = "weight",
rescale = TRUE
)
#> Categorical table by education
#>
#> Lower secondary Upper secondary Tertiary Total
#> ─────────────── ─────────────── ───────── ─────────
#> Variable │ n % n % n % n %
#> ───────────────────┼────────────────────────────────────────────────────────
#> smoking │
#> No │ 176 69.0 419 78.5 325 84.4 919 78.4
#> Yes │ 79 31.0 114 21.5 60 15.6 254 21.6
#> (Missing) │ 4 13 10 27
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │
#> No │ 174 67.2 315 57.7 166 41.9 655 54.6
#> Yes │ 85 32.8 231 42.3 229 58.1 545 45.4
#>
#> Variable │ p V
#> ───────────────────┼────────────
#> smoking │ <.001 .13
#> No │
#> Yes │
#> (Missing) │
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌
#> physical_activity │ <.001 .19
#> No │
#> Yes │
#>
#> Note. N = 1200 (unweighted): Lower secondary (n = 261), Upper secondary (n = 539), Tertiary (n = 400). Percentages are column percentages. Statistics weighted by weight. P-values: Pearson chi-squared test. V = Cramér's V.
# Confidence interval for the association measure.
table_categorical(
sochealth,
select = smoking,
by = education,
assoc_ci = TRUE
)
#> Categorical table by education
#>
#> Lower secondary Upper secondary Tertiary Total
#> ─────────────── ─────────────── ───────── ─────────
#> Variable │ n % n % n % n % p
#> ─────────────┼───────────────────────────────────────────────────────────────
#> smoking │ <.001
#> No │ 179 69.6 415 78.7 332 84.9 926 78.8
#> Yes │ 78 30.4 112 21.3 59 15.1 249 21.2
#> (Missing) │ 4 12 9 25
#>
#> Variable │ V [95% CI]
#> ─────────────┼────────────────
#> smoking │ .14 [.08, .19]
#> No │
#> Yes │
#> (Missing) │
#>
#> Note. N = 1200: Lower secondary (n = 261), Upper secondary (n = 539), Tertiary (n = 400). Percentages are column percentages. P-value: Pearson chi-squared test. V = Cramér's V; CI = confidence interval.
# --- Per-variable association measure ----------------------------------
# Default (`assoc_measure = "auto"`): one measure per row variable based on
# the variable type (2x2 -> Phi, both ordered factors -> Kendall's Tau-b,
# otherwise Cramer's V). When the chosen measures differ across rows, the
# column header collapses to `"Effect size"` and the note names the
# measure used for each variable.
table_categorical(
sochealth,
select = c(smoking, education),
by = sex
)
#> Categorical table by sex
#>
#> Female Male Total
#> ───────── ───────── ─────────
#> Variable │ n % n % n % p Effect size
#> ───────────────────┼────────────────────────────────────────────────────
#> smoking │ .713 .01
#> No │ 475 78.4 451 79.3 926 78.8
#> Yes │ 131 21.6 118 20.7 249 21.2
#> (Missing) │ 14 11 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> education │ .344 .04
#> Lower secondary │ 134 21.6 127 21.9 261 21.8
#> Upper secondary │ 290 46.8 249 42.9 539 44.9
#> Tertiary │ 196 31.6 204 35.2 400 33.3
#>
#> Note. N = 1200: Female (n = 620), Male (n = 580). Percentages are column percentages. P-values: Pearson chi-squared test. Phi: smoking. Cramér's V: education.
# Force a uniform measure across all row variables: its symbol, V,
# heads the column, and the note defines it.
table_categorical(
sochealth,
select = c(smoking, education),
by = sex,
assoc_measure = "cramer_v"
)
#> Categorical table by sex
#>
#> Female Male Total
#> ───────── ───────── ─────────
#> Variable │ n % n % n % p V
#> ───────────────────┼────────────────────────────────────────────
#> smoking │ .713 .01
#> No │ 475 78.4 451 79.3 926 78.8
#> Yes │ 131 21.6 118 20.7 249 21.2
#> (Missing) │ 14 11 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> education │ .344 .04
#> Lower secondary │ 134 21.6 127 21.9 261 21.8
#> Upper secondary │ 290 46.8 249 42.9 539 44.9
#> Tertiary │ 196 31.6 204 35.2 400 33.3
#>
#> Note. N = 1200: Female (n = 620), Male (n = 580). Percentages are column percentages. P-values: Pearson chi-squared test. V = Cramér's V.
# Per-variable override (recommended named form).
table_categorical(
sochealth,
select = c(smoking, education, self_rated_health),
by = sex,
assoc_measure = c(
smoking = "phi", # binary x binary
education = "cramer_v", # multi-category nominal
self_rated_health = "tau_b" # ordinal x binary, Tau-b
)
)
#> Categorical table by sex
#>
#> Female Male Total
#> ───────── ───────── ─────────
#> Variable │ n % n % n % p Effect size
#> ───────────────────┼────────────────────────────────────────────────────
#> smoking │ .713 .01
#> No │ 475 78.4 451 79.3 926 78.8
#> Yes │ 131 21.6 118 20.7 249 21.2
#> (Missing) │ 14 11 25
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> education │ .344 .04
#> Lower secondary │ 134 21.6 127 21.9 261 21.8
#> Upper secondary │ 290 46.8 249 42.9 539 44.9
#> Tertiary │ 196 31.6 204 35.2 400 33.3
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> self_rated_health │ .849 .01
#> Poor │ 31 5.1 30 5.3 61 5.2
#> Fair │ 143 23.4 123 21.6 266 22.5
#> Good │ 282 46.2 276 48.4 558 47.3
#> Very good │ 154 25.2 141 24.7 295 25.0
#> (Missing) │ 10 10 20
#>
#> Note. N = 1200: Female (n = 620), Male (n = 580). Percentages are column percentages. P-values: Pearson chi-squared test. Phi: smoking. Cramér's V: education. Kendall's Tau-b: self_rated_health.
# --- Output formats -----------------------------------------------------
# The rendered outputs below all wrap the same call:
# table_categorical(sochealth,
# select = c(smoking, physical_activity),
# by = sex)
# only `output` changes. Assign each result to a variable -- some
# engines auto-print as a console-friendly text fallback inside
# the `?` help viewer.
# Wide data.frame (one row per modality).
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex,
output = "data.frame"
)
#> variable label level .row_role Female n Female %
#> 1 smoking smoking <NA> factor_header NA NA
#> 2 smoking smoking No level 475 78.38284
#> 3 smoking smoking Yes level 131 21.61716
#> 4 smoking smoking (Missing) missing 14 NA
#> 5 physical_activity physical_activity <NA> factor_header NA NA
#> 6 physical_activity physical_activity No level 334 53.87097
#> 7 physical_activity physical_activity Yes level 286 46.12903
#> Male n Male % Total n Total % p φ
#> 1 NA NA NA NA 0.7125196 0.010749501
#> 2 451 79.26186 926 78.80851 NA NA
#> 3 118 20.73814 249 21.19149 NA NA
#> 4 11 NA 25 NA NA NA
#> 5 NA NA NA NA 0.8316763 0.006135851
#> 6 316 54.48276 650 54.16667 NA NA
#> 7 264 45.51724 550 45.83333 NA NA
# Long data.frame (one row per (modality x group)).
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex,
output = "long"
)
#> variable label level .row_role group .group_role
#> 1 smoking smoking No level Female group
#> 2 smoking smoking No level Male group
#> 3 smoking smoking No level Total total
#> 4 smoking smoking Yes level Female group
#> 5 smoking smoking Yes level Male group
#> 6 smoking smoking Yes level Total total
#> 7 smoking smoking (Missing) missing Female group
#> 8 smoking smoking (Missing) missing Male group
#> 9 smoking smoking (Missing) missing Total total
#> 10 physical_activity physical_activity No level Female group
#> 11 physical_activity physical_activity No level Male group
#> 12 physical_activity physical_activity No level Total total
#> 13 physical_activity physical_activity Yes level Female group
#> 14 physical_activity physical_activity Yes level Male group
#> 15 physical_activity physical_activity Yes level Total total
#> total n pct test_type statistic df p.value es_type es_value
#> 1 FALSE 475 78.38284 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 2 FALSE 451 79.26186 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 3 TRUE 926 78.80851 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 4 FALSE 131 21.61716 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 5 FALSE 118 20.73814 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 6 TRUE 249 21.19149 chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 7 FALSE 14 NA chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 8 FALSE 11 NA chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 9 TRUE 25 NA chi_squared 0.1357733 1 0.7125196 phi 0.010749501
#> 10 FALSE 334 53.87097 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> 11 FALSE 316 54.48276 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> 12 TRUE 650 54.16667 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> 13 FALSE 286 46.12903 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> 14 FALSE 264 45.51724 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> 15 TRUE 550 45.83333 chi_squared 0.0451784 1 0.8316763 phi 0.006135851
#> es_ci_lower es_ci_upper smd_type smd_value
#> 1 NA NA <NA> NA
#> 2 NA NA <NA> NA
#> 3 NA NA <NA> NA
#> 4 NA NA <NA> NA
#> 5 NA NA <NA> NA
#> 6 NA NA <NA> NA
#> 7 NA NA <NA> NA
#> 8 NA NA <NA> NA
#> 9 NA NA <NA> NA
#> 10 NA NA <NA> NA
#> 11 NA NA <NA> NA
#> 12 NA NA <NA> NA
#> 13 NA NA <NA> NA
#> 14 NA NA <NA> NA
#> 15 NA NA <NA> NA
# \donttest{
# Rendered HTML / docx objects -- best viewed inside a
# Quarto / R Markdown document or a pkgdown article.
if (requireNamespace("tinytable", quietly = TRUE)) {
tt <- table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "tinytable"
)
}
if (requireNamespace("gt", quietly = TRUE)) {
tbl <- table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "gt"
)
}
if (requireNamespace("flextable", quietly = TRUE)) {
ft <- table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "flextable"
)
}
# Excel and Word: write to a temporary file.
if (requireNamespace("openxlsx2", quietly = TRUE)) {
tmp <- tempfile(fileext = ".xlsx")
table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "excel", excel_path = tmp
)
unlink(tmp)
}
if (
requireNamespace("flextable", quietly = TRUE) &&
requireNamespace("officer", quietly = TRUE)
) {
tmp <- tempfile(fileext = ".docx")
table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "word", word_path = tmp
)
unlink(tmp)
}
# }
if (FALSE) { # \dontrun{
# Clipboard: writes to the system clipboard.
table_categorical(
sochealth, select = c(smoking, physical_activity), by = sex,
output = "clipboard"
)
} # }