Skip to contents

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 Chi-squared p-values 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 wide or long numeric data.frame ("data.frame", "long"), or publication-ready tables ("tinytable", "gt", "flextable", "excel", "clipboard", "word").

Usage

table_categorical(
  data,
  select = tidyselect::everything(),
  by = NULL,
  labels = NULL,
  levels_keep = NULL,
  include_total = TRUE,
  drop_na = FALSE,
  weights = NULL,
  rescale = FALSE,
  correct = FALSE,
  simulate_p = FALSE,
  simulate_B = 2000,
  percent_digits = 1,
  p_digits = 3,
  v_digits = 2,
  assoc_measure = "auto",
  assoc_ci = FALSE,
  smd = FALSE,
  decimal_mark = ".",
  align = c("decimal", "center", "right"),
  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
)

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 the by column – matching the select-less defaults of table_continuous() and table_continuous_lm(). An explicit select is taken verbatim (numeric columns included), so numeric-coded categorical variables can still be tabulated by naming them.

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; any other by (character, numeric, haven labelled) forms group columns in order of first appearance in the data – the same convention as table_continuous(). For a haven labelled by, the group headers are the raw codes (value labels are not used for group headers – the family convention shared with table_continuous() and table_continuous_lm()); declared missing values follow user_na as usual.

labels

An optional named character vector of variable labels whose names match column names in data (e.g. c(smoking = "Current smoker")) – the same contract as table_continuous() and table_continuous_lm(). Only listed columns are relabelled. For the remaining columns (and when labels = NULL, the default), labels are auto-detected from the variable's label attribute (e.g. from haven); if none is found, the column name is used. Unnamed (positional) label vectors, accepted before 0.13.0, now raise an error.

levels_keep

Optional character vector of levels to keep/order for row modalities. If NULL, all observed levels are kept. Entries must match the level strings the table displays (for labelled columns these are the "[code] label" strings, not the bare label text). When nothing matches for a selected variable, that variable is dropped from the table with a classed warning (spicy_no_selection) listing the available level strings.

include_total

Logical. If TRUE (the default), includes a Total group when available.

drop_na

Logical. If FALSE (the default), missing values are displayed as a dedicated "(Missing)" level (and, under by, a "(Missing)" group column) – the field convention for descriptive tables (gtsummary's "Unknown" row, janitor's NA row; see the Epidemiologist R Handbook, Descriptive tables). If TRUE, rows with NA in the tabulated variable (and in by, when supplied) are removed BEFORE each cross-tabulation, and the removal is disclosed in a table note ("Missing values removed: ...") rather than silent. Before 0.13.0 the default was TRUE with no disclosure.

weights

Optional weights. Either NULL (the default), a numeric vector of length nrow(data), or a single column in data supplied as an unquoted name or a character string.

rescale

Logical. If FALSE (the default), weights are used as-is. If TRUE, rescales weights so total weighted N matches raw N. Passed to spicy::cross_tab(). When the argument is not supplied, the default is read from options(spicy.rescale) (falling back to FALSE), matching cross_tab().

correct

Logical. If FALSE (the default), no continuity correction is applied. If TRUE, applies Yates correction in 2x2 chi-squared contexts. Passed to spicy::cross_tab().

simulate_p

Logical. If FALSE (the default), uses asymptotic p-values. If TRUE, uses Monte Carlo simulation. Passed to spicy::cross_tab().

simulate_B

Integer. Number of Monte Carlo replicates when simulate_p = TRUE. Defaults to 2000.

percent_digits

Number of digits for percentages in report outputs. Defaults to 1.

p_digits

Integer >= 1. Number of decimal places used to render p-values in the p column (default: 3, the APA Publication Manual standard). Both the displayed precision and the small-p threshold derive from this argument: p_digits = 3 prints .045 and <.001; p_digits = 4 prints .0451 and <.0001. Leading zeros are always stripped, following APA convention.

v_digits

Number of digits for the association measure. Defaults to 2.

assoc_measure

Which association measure to report alongside the chi-squared 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. binary by) uses phi, a pair of ordered factors uses tau_b, every other case uses cramer_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 with select) are accepted. Named is more robust to reordering of select.

When a single measure is used for every row, the column header is that measure's name (e.g. "Cramer's V"). When multiple measures are used (typically with "auto" on a heterogeneous select), the header collapses to "Effect size" and an APA-style Note. line is appended documenting which measure was used for which variable.

phi requires 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(). If TRUE, includes the confidence interval of the association measure. In wide raw outputs ("data.frame", "excel", "clipboard"), two extra columns CI lower / CI upper are added; in the long raw output ("long") the bounds appear as ci_lower / ci_upper. In rendered formats ("gt", "tinytable", "flextable", "word"), the CI is shown inline (e.g., .14 [.08, .19]). Defaults to FALSE.

smd

Logical. If TRUE, adds an SMD column holding the standardized mean difference between the two groups of by, the balance diagnostic of the Table 1 literature. Requires exactly two groups; the value sits on the variable row beside p, 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 with v_digits. See the "Standardized mean difference" section below. Defaults to FALSE.

decimal_mark

Decimal separator ("." or ","). Defaults to ".".

align

Horizontal alignment of numeric columns in the printed ASCII table and in the tinytable, gt, flextable, word, and clipboard outputs. 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 LaTeX siunitx. 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; centring those uniform-width strings then stacks the decimal points vertically. The same pad-then-centre strategy is applied on every rendering engine (gt, tinytable, flextable, word, ASCII print) for a homogeneous rendering, matching table_regression() and table_continuous_lm(). The clipboard output 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 excel output, "center" centres 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-column numfmt already produces dot-aligned columns. Same default and same three values as table_continuous() / table_continuous_lm(), whose workbooks resolve "decimal" differently: table_continuous() right-aligns only the counts and the p-value there, and table_continuous_lm() applies that convention at every align.

output

Output format. One of:

  • "default" (an ASCII table object, printed when the call is bare)

  • "data.frame" (a wide numeric data.frame)

  • "long" (a long numeric data.frame)

  • "tinytable" (requires tinytable)

  • "gt" (requires gt)

  • "flextable" (requires flextable)

  • "excel" (requires openxlsx2)

  • "clipboard" (requires clipr)

  • "word" (requires flextable and officer)

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 for output = "excel" on a grouped table (by supplied); like the other output-scoped presentation arguments (excel_sheet, clipboard_delim, ...), it is silently unused in every other output and in one-way tables, which have a single header row to begin with.

blank_na_wide

Logical. If FALSE (the default), NA values are kept as-is in wide raw output. If TRUE, replaces them with empty strings.

excel_path

Path for output = "excel". Defaults to NULL.

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". Defaults to NULL. Before 0.13.0, supplying it with output = "flextable" also wrote a .docx as a side effect; it is now used exclusively by output = "word" (the contract shared with the rest of the table family) and is ignored, with a warning, under output = "flextable".

user_na

Logical. If TRUE (the default), declared missing values in the row variables and in by are treated as missing: they join the "(Missing)" level under drop_na = FALSE, and under drop_na = TRUE they are removed with a dedicated disclosure line (Declared missing values removed: ...). If FALSE, the declared codes stay valid categories. See the "Declared missing values" section of freq().

style

A journal style: a theme name ("jama", "nejm", "lancet", "annals", "apa", "aer"), a spicy_style() object, or NULL (the default). A style only changes DEFAULTS – any argument you pass explicitly wins over it. Set options(spicy.style = ) for document-wide scope. A theme covers numeric formatting conformity only, not full editorial conformity; ?spicy_style lists the exact rules each one encodes and the official document they come from. An unknown name is an error.

Value

Depends on output:

  • "default": the underlying data.frame carrying the rendering metadata as attributes (S3 class "spicy_categorical_table"). The object is returned visibly, so a bare table_categorical(...) call auto-prints the styled ASCII table at the console while t <- table_categorical(...) stays silent (print t to display the table).

  • "data.frame": a wide data.frame with one row per variable–level combination. When by is used, the columns are Variable, Level, and one pair of n / \% columns per group level (plus Total when include_total = TRUE), followed by Chi2, df, p, and the association measure column. When by = NULL, the columns are Variable, Level, n, \%.

  • "long": a long data.frame with columns variable, level, n, pct (plus group, chi2, df, p when by is used). The association measure is always called effect_size, whichever measure it is, and effect_size_type names that measure per row ("cramer_v", "phi", ...), or is NA on the rows of a variable given assoc_measure = "none". The wide outputs instead name the column after the measure, or Effect size when the row variables do not share one. With smd = TRUE this output also carries smd and smd_type ("binary" or "multinomial", the kernel the value came from); the wide outputs name that column SMD. Like the association columns, both are ABSENT when the statistic is not requested.

  • "tinytable": a tinytable object.

  • "gt": a gt_tbl object.

  • "flextable": a flextable object.

  • "excel" / "word": writes to disk and returns the file path invisibly.

  • "clipboard": copies the table and returns the display data.frame invisibly.

The drop_na = TRUE disclosure 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 "data.frame" keeps the sentence verbatim in the missing_note attribute (attr(x, "missing_note"), NULL when nothing was removed) so a pipeline that renders the numbers itself can still state what left the table. 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 the omnibus chi-squared p-value is reported in the p column. See @param correct / simulate_p to switch on Yates' continuity correction or Monte Carlo p-values, and @param assoc_measure for the per-row dispatch table used by "auto" (2x2 -> Phi, both ordered -> Kendall's Tau-b, otherwise Cramer's V). Without by, the table reports the marginal frequency distribution of each variable with no inferential statistics.

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().

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.

Two limits of the current grammar. This function has no p_value argument, so the p column cannot be switched off here as it can in table_continuous(); a complete balance table mixing continuous and categorical variables will show a categorical p beside a continuous column you removed. And inline() cannot quote this SMD cell: like p and the association measure, it lives on the variable row, which inline() cannot address on a variable that has levels. The continuous SMD cell is quotable (inline(tbl, x, "A", column = "smd")); for the categorical one, read output = "long".

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).

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() and code_book() count them as missing in N_valid / NAs / N_distinct while still listing the declared codes in Values.

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.

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
#> 
#>  Variable                  │ Female n  Female %  Male n  Male %  Total n 
#> ───────────────────────────┼─────────────────────────────────────────────
#>  Current smoker            │                                             
#>    No                      │   475       76.6     451     77.8     926   
#>    Yes                     │   131       21.1     118     20.3     249   
#>    (Missing)               │    14        2.3      11      1.9      25   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity │                                             
#>    No                      │   334       53.9     316     54.5     650   
#>    Yes                     │   286       46.1     264     45.5     550   
#> 
#>  Variable                  │ Total %   p    Phi 
#> ───────────────────────────┼────────────────────
#>  Current smoker            │          .713  .01 
#>    No                      │  77.2              
#>    Yes                     │  20.8              
#>    (Missing)               │   2.1              
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity │          .832  .01 
#>    No                      │  54.2              
#>    Yes                     │  45.8              

# One-way frequency-style table (no `by`).
table_categorical(
  sochealth,
  select = c(smoking, physical_activity)
)
#> Categorical table
#> 
#>  Variable                    │   n      %    
#> ─────────────────────────────┼───────────────
#>  Current smoker              │               
#>    No                        │  926    77.2  
#>    Yes                       │  249    20.8  
#>    (Missing)                 │   25     2.1  
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity   │               
#>    No                        │  650    54.2  
#>    Yes                       │  550    45.8  

# 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
#> 
#>  Variable          │ Lower secondary n  Lower secondary %  Upper secondary n 
#> ───────────────────┼─────────────────────────────────────────────────────────
#>  Current smoker    │                                                         
#>    No              │        179               68.6                415        
#>    Yes             │         78               29.9                112        
#>    (Missing)       │          4                1.5                 12        
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Physical activity │                                                         
#>    No              │        177               67.8                310        
#>    Yes             │         84               32.2                229        
#> 
#>  Variable          │ Upper secondary %  Tertiary n  Tertiary %  Total n 
#> ───────────────────┼────────────────────────────────────────────────────
#>  Current smoker    │                                                    
#>    No              │       77.0            332         83.0       926   
#>    Yes             │       20.8             59         14.8       249   
#>    (Missing)       │        2.2              9          2.2        25   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Physical activity │                                                    
#>    No              │       57.5            163         40.8       650   
#>    Yes             │       42.5            237         59.2       550   
#> 
#>  Variable          │ Total %    p    Cramer's V 
#> ───────────────────┼────────────────────────────
#>  Current smoker    │          <.001     .14     
#>    No              │  77.2                      
#>    Yes             │  20.8                      
#>    (Missing)       │   2.1                      
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Physical activity │          <.001     .21     
#>    No              │  54.2                      
#>    Yes             │  45.8                      

# Survey weights with rescaling.
table_categorical(
  sochealth,
  select = c(smoking, physical_activity),
  by = education,
  weights = "weight",
  rescale = TRUE
)
#> Categorical table by education
#> 
#>  Variable                  │ Lower secondary n  Lower secondary % 
#> ───────────────────────────┼──────────────────────────────────────
#>  Current smoker            │                                      
#>    No                      │        176               68.1        
#>    Yes                     │         79               30.6        
#>    (Missing)               │          4                1.4        
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity │                                      
#>    No                      │        174               67.2        
#>    Yes                     │         85               32.8        
#> 
#>  Variable                  │ Upper secondary n  Upper secondary %  Tertiary n 
#> ───────────────────────────┼──────────────────────────────────────────────────
#>  Current smoker            │                                                  
#>    No                      │        419               76.6            325     
#>    Yes                     │        114               21.0             60     
#>    (Missing)               │         13                2.4             10     
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity │                                                  
#>    No                      │        315               57.7            166     
#>    Yes                     │        231               42.3            229     
#> 
#>  Variable                  │ Tertiary %  Total n  Total %    p    Cramer's V 
#> ───────────────────────────┼─────────────────────────────────────────────────
#>  Current smoker            │                               <.001     .13     
#>    No                      │    82.2       919     76.6                      
#>    Yes                     │    15.2       254     21.1                      
#>    (Missing)               │     2.6        27      2.2                      
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Regular physical activity │                               <.001     .19     
#>    No                      │    41.9       655     54.6                      
#>    Yes                     │    58.1       545     45.4                      

# Confidence interval for the association measure.
table_categorical(
  sochealth,
  select = smoking,
  by = education,
  assoc_ci = TRUE
)
#> Categorical table by education
#> 
#>  Variable       │ Lower secondary n  Lower secondary %  Upper secondary n 
#> ────────────────┼─────────────────────────────────────────────────────────
#>  Current smoker │                                                         
#>    No           │        179               68.6                415        
#>    Yes          │         78               29.9                112        
#>    (Missing)    │          4                1.5                 12        
#> 
#>  Variable       │ Upper secondary %  Tertiary n  Tertiary %  Total n  Total % 
#> ────────────────┼─────────────────────────────────────────────────────────────
#>  Current smoker │                                                             
#>    No           │       77.0            332         83.0       926     77.2   
#>    Yes          │       20.8             59         14.8       249     20.8   
#>    (Missing)    │        2.2              9          2.2        25      2.1   
#> 
#>  Variable       │   p    Cramer's V  CI lower  CI upper 
#> ────────────────┼───────────────────────────────────────
#>  Current smoker │ <.001     .14        .08       .19    
#>    No           │                                       
#>    Yes          │                                       
#>    (Missing)    │                                       

# --- 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 an APA-style `Note.` line
# documents which measure was used for which variable.
table_categorical(
  sochealth,
  select = c(smoking, education),
  by = sex
)
#> Categorical table by sex
#> 
#>  Variable                │ Female n  Female %  Male n  Male %  Total n  Total % 
#> ─────────────────────────┼──────────────────────────────────────────────────────
#>  Current smoker          │                                                      
#>    No                    │   475       76.6     451     77.8     926     77.2   
#>    Yes                   │   131       21.1     118     20.3     249     20.8   
#>    (Missing)             │    14        2.3      11      1.9      25      2.1   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │                                                      
#>    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   
#> 
#>  Variable                │  p    Effect size 
#> ─────────────────────────┼───────────────────
#>  Current smoker          │ .713      .01     
#>    No                    │                   
#>    Yes                   │                   
#>    (Missing)             │                   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │ .344      .04     
#>    Lower secondary       │                   
#>    Upper secondary       │                   
#>    Tertiary              │                   
#> 
#> Note. Phi: Current smoker; Cramer's V: Highest education level.

# Force a uniform measure across all row variables.
table_categorical(
  sochealth,
  select = c(smoking, education),
  by = sex,
  assoc_measure = "cramer_v"
)
#> Categorical table by sex
#> 
#>  Variable                │ Female n  Female %  Male n  Male %  Total n  Total % 
#> ─────────────────────────┼──────────────────────────────────────────────────────
#>  Current smoker          │                                                      
#>    No                    │   475       76.6     451     77.8     926     77.2   
#>    Yes                   │   131       21.1     118     20.3     249     20.8   
#>    (Missing)             │    14        2.3      11      1.9      25      2.1   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │                                                      
#>    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   
#> 
#>  Variable                │  p    Cramer's V 
#> ─────────────────────────┼──────────────────
#>  Current smoker          │ .713     .01     
#>    No                    │                  
#>    Yes                   │                  
#>    (Missing)             │                  
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │ .344     .04     
#>    Lower secondary       │                  
#>    Upper secondary       │                  
#>    Tertiary              │                  

# 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
#> 
#>  Variable                │ Female n  Female %  Male n  Male %  Total n  Total % 
#> ─────────────────────────┼──────────────────────────────────────────────────────
#>  Current smoker          │                                                      
#>    No                    │   475       76.6     451     77.8     926     77.2   
#>    Yes                   │   131       21.1     118     20.3     249     20.8   
#>    (Missing)             │    14        2.3      11      1.9      25      2.1   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │                                                      
#>    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       │                                                      
#>    Poor                  │    31        5.0      30      5.2      61      5.1   
#>    Fair                  │   143       23.1     123     21.2     266     22.2   
#>    Good                  │   282       45.5     276     47.6     558     46.5   
#>    Very good             │   154       24.8     141     24.3     295     24.6   
#>    (Missing)             │    10        1.6      10      1.7      20      1.7   
#> 
#>  Variable                │  p    Effect size 
#> ─────────────────────────┼───────────────────
#>  Current smoker          │ .713      .01     
#>    No                    │                   
#>    Yes                   │                   
#>    (Missing)             │                   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Highest education level │ .344      .04     
#>    Lower secondary       │                   
#>    Upper secondary       │                   
#>    Tertiary              │                   
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#>  Self-rated health       │ .849      .01     
#>    Poor                  │                   
#>    Fair                  │                   
#>    Good                  │                   
#>    Very good             │                   
#>    (Missing)             │                   
#> 
#> Note. Phi: Current smoker; Cramer's V: Highest education level; 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     Level Female n  Female % Male n    Male %
#> 1            Current smoker        No      475 76.612903    451 77.758621
#> 2            Current smoker       Yes      131 21.129032    118 20.344828
#> 3            Current smoker (Missing)       14  2.258065     11  1.896552
#> 4 Regular physical activity        No      334 53.870968    316 54.482759
#> 5 Regular physical activity       Yes      286 46.129032    264 45.517241
#>   Total n   Total %      Chi2 df         p         Phi
#> 1     926 77.166667 0.1357733  1 0.7125196 0.010749501
#> 2     249 20.750000 0.1357733  1 0.7125196 0.010749501
#> 3      25  2.083333 0.1357733  1 0.7125196 0.010749501
#> 4     650 54.166667 0.0451784  1 0.8316763 0.006135851
#> 5     550 45.833333 0.0451784  1 0.8316763 0.006135851

# Long data.frame (one row per (modality x group)).
table_categorical(
  sochealth,
  select = c(smoking, physical_activity),
  by = sex,
  output = "long"
)
#>                     variable     level  group   n       pct      chi2 df
#> 1             Current smoker        No Female 475 76.612903 0.1357733  1
#> 2             Current smoker        No   Male 451 77.758621 0.1357733  1
#> 3             Current smoker        No  Total 926 77.166667 0.1357733  1
#> 4             Current smoker       Yes Female 131 21.129032 0.1357733  1
#> 5             Current smoker       Yes   Male 118 20.344828 0.1357733  1
#> 6             Current smoker       Yes  Total 249 20.750000 0.1357733  1
#> 7             Current smoker (Missing) Female  14  2.258065 0.1357733  1
#> 8             Current smoker (Missing)   Male  11  1.896552 0.1357733  1
#> 9             Current smoker (Missing)  Total  25  2.083333 0.1357733  1
#> 10 Regular physical activity        No Female 334 53.870968 0.0451784  1
#> 11 Regular physical activity        No   Male 316 54.482759 0.0451784  1
#> 12 Regular physical activity        No  Total 650 54.166667 0.0451784  1
#> 13 Regular physical activity       Yes Female 286 46.129032 0.0451784  1
#> 14 Regular physical activity       Yes   Male 264 45.517241 0.0451784  1
#> 15 Regular physical activity       Yes  Total 550 45.833333 0.0451784  1
#>            p effect_size effect_size_type
#> 1  0.7125196 0.010749501              phi
#> 2  0.7125196 0.010749501              phi
#> 3  0.7125196 0.010749501              phi
#> 4  0.7125196 0.010749501              phi
#> 5  0.7125196 0.010749501              phi
#> 6  0.7125196 0.010749501              phi
#> 7  0.7125196 0.010749501              phi
#> 8  0.7125196 0.010749501              phi
#> 9  0.7125196 0.010749501              phi
#> 10 0.8316763 0.006135851              phi
#> 11 0.8316763 0.006135851              phi
#> 12 0.8316763 0.006135851              phi
#> 13 0.8316763 0.006135851              phi
#> 14 0.8316763 0.006135851              phi
#> 15 0.8316763 0.006135851              phi

# \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"
)
} # }