Skip to contents

Computes a two-way cross-tabulation with optional weights, grouping (including combinations of multiple variables via interaction()), row / column percentages, and inferential statistics (Chi-squared test with an APA-style association measure).

Both x and y are required; for one-way frequency tables, use freq().

Usage

cross_tab(
  data,
  x,
  y = NULL,
  by = NULL,
  weights = NULL,
  rescale = FALSE,
  percent = c("none", "column", "row"),
  include_stats = TRUE,
  assoc_measure = c("auto", "cramer_v", "phi", "gamma", "tau_b", "tau_c", "somers_d",
    "lambda", "none"),
  assoc_ci = FALSE,
  correct = FALSE,
  simulate_p = FALSE,
  simulate_B = 2000,
  digits = NULL,
  output = c("default", "data.frame"),
  show_n = TRUE,
  decimal_mark = ".",
  p_digits = 3L,
  user_na = TRUE,
  styled
)

Arguments

data

A data frame. Alternatively, a vector when using the vector-based interface.

x

Row variable (unquoted).

y

Column variable (unquoted). Required; the NULL default in the signature is a placeholder and triggers an error if left unset (use freq() for one-way tables).

by

Optional grouping variable or expression. Can be a single variable or a combination of multiple variables (e.g. interaction(vs, am)).

weights

Optional numeric weights. A logical vector is also accepted and coerced to 1/0 (include / exclude).

rescale

Logical. If FALSE (the default), weights are used as-is. If TRUE, rescales weights so total weighted N matches raw N.

percent

One of "none" (the default), "column", or "row". Unique abbreviations are accepted (e.g. "n", "c", "r").

include_stats

Logical. If TRUE (the default), computes Chi-squared and an association measure (see assoc_measure).

assoc_measure

Character. Which association measure to report. "auto" (default) selects Kendall's Tau-b when both variables are ordered factors and Cramer's V otherwise. Other choices: "cramer_v", "phi", "gamma", "tau_b", "tau_c", "somers_d", "lambda", "none".

assoc_ci

Logical. If TRUE, includes the 95 percent confidence interval of the association measure in the note. Defaults to FALSE.

correct

Logical. If FALSE (the default), no continuity correction is applied. If TRUE, applies Yates correction (only for 2x2 tables).

simulate_p

Logical. If FALSE (the default), uses asymptotic p-values. If TRUE, uses Monte Carlo simulation.

simulate_B

Integer. Number of replicates for Monte Carlo simulation. Defaults to 2000.

digits

Number of decimals for cell values: a single non-negative integer. Defaults to NULL, which is resolved to 1 when percent != "none" and 0 when percent = "none" (counts are integers unless fractional weights are used; raise digits to display fractional weighted counts exactly). Same role as digits in freq(), which formats percentages only and therefore uses a fixed default of 1. Displayed values round ties half to even (the R / IEC 60559 convention, shared with Stata), so an exact tie like 6.25 prints as 6.2 where SPSS would print 6.3.

output

Output format. "default" (the default) returns a spicy_cross_table object (for formatted printing); "data.frame" returns a plain data.frame. The values match the output argument of the table_*() family; the rendered engines that family also accepts ("tinytable", "gt", "flextable", ...) are not available in cross_tab().

show_n

Logical. If TRUE (the default), adds marginal N totals when percent != "none".

decimal_mark

Character used as the decimal mark in printed numeric values (cells, chi-squared, association estimate, CI bounds, p-value). Defaults to ".". Set to "," for European formatting; matches the decimal_mark argument of the table_*() family.

p_digits

Integer number of decimals used to format the p-value (and to determine the small-p threshold below which < .001 notation is used). Defaults to 3 (the APA standard); matches the p_digits argument of the table_*() family.

user_na

Logical. If TRUE (the default), declared missing values in x, y, or by are treated as missing: they are excluded from the table and its statistics like NA, and the exclusion is disclosed in the table note (Declared missing values removed: ...). If FALSE, the declared codes tabulate as categories (and, in by, define groups). See the "Declared missing values" section of freq().

styled

Defunct. styled = TRUE is now output = "default" (the default) and styled = FALSE is now output = "data.frame"; supplying styled is an error.

Value

Depends on output and by:

  • output = "default", no by: a spicy_cross_table object (a data.frame carrying rendering metadata as attributes: title, digits, decimal_mark, n_row_idx, n_col_name, and the inferential block when include_stats = TRUE). Printing dispatches to print.spicy_cross_table().

  • output = "default", by supplied: a spicy_cross_table_list, i.e. a named list of spicy_cross_table objects (one element per group level, named by that level). Printing dispatches to print.spicy_cross_table_list() which renders each table in turn separated by a blank line.

  • output = "data.frame": the same payload returned as a plain data.frame (or named list of data.frames with by), stripped of the spicy_* classes and of every metadata attribute (title, note, n_total, chi2, p_value, assoc_*, ...). For programmatic access to the statistics, read the attributes of the default object, e.g. attr(cross_tab(...), "p_value").

Cell columns are the levels of y; rows are the levels of x. When percent != "none", the N column (or N row) is added according to show_n. When include_stats = TRUE, the result carries a Chi-squared row (statistic, df, p) and an association-measure row (estimate, optional CI via assoc_ci).

Global Options

The function recognizes the following global options that modify its default behavior:

  • options(spicy.percent = "column") Sets the default percentage mode for all calls to cross_tab(). Valid values are "none", "row", and "column". Equivalent to setting percent = "column" (or another choice) in each call.

  • options(spicy.simulate_p = TRUE) Enables Monte Carlo simulation for all Chi-squared tests by default. Equivalent to setting simulate_p = TRUE in every call.

  • options(spicy.rescale = TRUE) Automatically rescales weights so that total weighted N equals the raw N. Equivalent to setting rescale = TRUE in each call. Also read by freq(), so one option governs both tabulators.

These options are convenient for users who wish to enforce consistent behavior across multiple calls: spicy.percent and spicy.simulate_p apply to cross_tab(), and spicy.rescale applies to both cross_tab() and freq(). They can be disabled or reset by setting them to NULL: options(spicy.percent = NULL, spicy.simulate_p = NULL, spicy.rescale = NULL).

Example:

options(spicy.simulate_p = TRUE, spicy.rescale = TRUE)
cross_tab(sochealth, smoking, education, weights = weight)

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.

Examples

# Basic crosstab
cross_tab(sochealth, smoking, education)
#> Crosstable: smoking x education (N)
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                      179                415         332      926 
#>  Yes                      78                112          59      249 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                   257                527         391     1175 
#> 
#> Chi-2(2) = 21.6, p <.001
#> Cramer's V = 0.14
#> Missing values removed: smoking (25).

# Column percentages
cross_tab(sochealth, smoking, education, percent = "column")
#> Crosstable: smoking x education (Column %)
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                     69.6               78.7        84.9     78.8 
#>  Yes                    30.4               21.3        15.1     21.2 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                 100.0              100.0       100.0    100.0 
#>  N                       257                527         391     1175 
#> 
#> Chi-2(2) = 21.6, p <.001
#> Cramer's V = 0.14
#> Missing values removed: smoking (25).

# Weighted (rescaled)
cross_tab(sochealth, smoking, education, weights = weight, rescale = TRUE)
#> Crosstable: smoking x education (N)
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                      176                419         325      921 
#>  Yes                      79                115          60      254 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                   256                534         386     1175 
#> 
#> Chi-2(2) = 21.4, p <.001
#> Cramer's V = 0.13
#> Weight: weight (rescaled)
#> Missing values removed: smoking (25).

# Grouped by sex
cross_tab(sochealth, smoking, education, by = sex)
#> Crosstable: smoking x education (N) | sex = Female
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       95                220         160      475 
#>  Yes                      38                 62          31      131 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                   133                282         191      606 
#> 
#> Chi-2(2) = 7.1, p = .029
#> Cramer's V = 0.11
#> Missing values removed: smoking (14).
#> 
#> Crosstable: smoking x education (N) | sex = Male
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       84                195         172      451 
#>  Yes                      40                 50          28      118 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                   124                245         200      569 
#> 
#> Chi-2(2) = 15.6, p <.001
#> Cramer's V = 0.17
#> Missing values removed: smoking (11).

# Grouped by combination of variables
cross_tab(sochealth, smoking, education, by = interaction(sex, age_group))
#> Crosstable: smoking x education (N) | sex x age_group = Female.25-34
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       23                 49          29      101 
#>  Yes                       9                  9           7       25 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    32                 58          36      126 
#> 
#> Chi-2(2) = 2.1, p = .356
#> Cramer's V = 0.13
#> Missing values removed: smoking (4).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Male.25-34
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                        9                 42          32       83 
#>  Yes                      11                 11           4       26 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    20                 53          36      109 
#> 
#> Chi-2(2) = 14.2, p <.001
#> Cramer's V = 0.36
#> Missing values removed: smoking (3).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Female.35-49
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       24                 73          48      145 
#>  Yes                      10                 20           8       38 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    34                 93          56      183 
#> 
#> Chi-2(2) = 3.0, p = .223
#> Cramer's V = 0.13
#> Missing values removed: smoking (7).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Male.35-49
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       33                 59          60      152 
#>  Yes                      14                 17           7       38 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    47                 76          67      190 
#> 
#> Chi-2(2) = 6.9, p = .032
#> Cramer's V = 0.19
#> Missing values removed: smoking (3).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Female.50-64
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       28                 63          45      136 
#>  Yes                       8                 16           6       30 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    36                 79          51      166 
#> 
#> Chi-2(2) = 2.0, p = .360
#> Cramer's V = 0.11
#> Missing values removed: smoking (3).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Male.50-64
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       28                 58          42      128 
#>  Yes                       8                 13           5       26 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    36                 71          47      154 
#> 
#> Chi-2(2) = 2.1, p = .343
#> Cramer's V = 0.12
#> Missing values removed: smoking (4).
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Female.65-75
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       20                 35          38       93 
#>  Yes                      11                 17          10       38 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    31                 52          48      131 
#> 
#> Chi-2(2) = 2.5, p = .282
#> Cramer's V = 0.14
#> 
#> Crosstable: smoking x education (N) | sex x age_group = Male.65-75
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                       14                 36          38       88 
#>  Yes                       7                  9          12       28 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                    21                 45          50      116 
#> 
#> Chi-2(2) = 1.4, p = .499
#> Cramer's V = 0.11
#> Missing values removed: smoking (1).

# Ordinal variables: auto-selects Kendall's Tau-b
cross_tab(sochealth, education, self_rated_health)
#> Crosstable: education x self_rated_health (N)
#> 
#>  Values               Poor    Fair    Good    Very good    Total 
#> ───────────────────┼─────────────────────────────────────┼─────────
#>  Lower secondary        28      86     102           44      260 
#>  Upper secondary        28     118     263          118      527 
#>  Tertiary                5      62     193          133      393 
#> ───────────────────┼─────────────────────────────────────┼─────────
#>  Total                  61     266     558          295     1180 
#> 
#> Chi-2(6) = 73.2, p <.001
#> Kendall's Tau-b = 0.20
#> Missing values removed: self_rated_health (20).

# 2x2 table with Yates correction
cross_tab(sochealth, smoking, physical_activity, correct = TRUE)
#> Crosstable: smoking x physical_activity (N)
#> 
#>  Values       No    Yes    Total 
#> ──────────┼──────────────┼─────────
#>  No          505    421      926 
#>  Yes         134    115      249 
#> ──────────┼──────────────┼─────────
#>  Total       639    536     1175 
#> 
#> Chi-2(1) = 0.0, p = .896
#> Cramer's V = 0.01
#> Yates continuity correction applied.
#> Missing values removed: smoking (25).

# APA-style p-value precision and European decimal mark
cross_tab(sochealth, smoking, education, decimal_mark = ",", p_digits = 4)
#> Crosstable: smoking x education (N)
#> 
#>  Values      Lower secondary    Upper secondary    Tertiary    Total 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  No                      179                415         332      926 
#>  Yes                      78                112          59      249 
#> ──────────┼──────────────────────────────────────────────────┼─────────
#>  Total                   257                527         391     1175 
#> 
#> Chi-2(2) = 21,6, p <,0001
#> Cramer's V = 0,14
#> Missing values removed: smoking (25).