table_categorical() builds publication-ready categorical
tables suitable for APA-style reporting in social science and data
science research. With by, it produces grouped
cross-tabulation tables with chi-squared \(p\)-values, effect sizes, confidence
intervals, and multi-level headers. Without by, it produces
one-way frequency-style tables for the selected variables. Export to gt,
tinytable, flextable, Excel, or Word. This vignette walks through the
main features.
Basic usage
For grouped tables, provide a data frame, one or more selected variables, and a grouping variable:
table_categorical(
sochealth,
select = c(smoking, physical_activity, dentist_12m),
by = education
)
#> Categorical table by education
#>
#> Variable │ Lower secondary n Lower secondary %
#> ─────────────────────────────────┼──────────────────────────────────────
#> Current smoker │
#> No │ 179 68.6
#> Yes │ 78 29.9
#> (Missing) │ 4 1.5
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │
#> No │ 177 67.8
#> Yes │ 84 32.2
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Dentist visit in last 12 months │
#> No │ 113 43.3
#> Yes │ 148 56.7
#>
#> Variable │ Upper secondary n Upper secondary %
#> ─────────────────────────────────┼──────────────────────────────────────
#> Current smoker │
#> No │ 415 77.0
#> Yes │ 112 20.8
#> (Missing) │ 12 2.2
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │
#> No │ 310 57.5
#> Yes │ 229 42.5
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Dentist visit in last 12 months │
#> No │ 174 32.3
#> Yes │ 365 67.7
#>
#> Variable │ Tertiary n Tertiary % Total n Total %
#> ─────────────────────────────────┼──────────────────────────────────────────
#> Current smoker │
#> No │ 332 83.0 926 77.2
#> Yes │ 59 14.8 249 20.8
#> (Missing) │ 9 2.2 25 2.1
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │
#> No │ 163 40.8 650 54.2
#> Yes │ 237 59.2 550 45.8
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Dentist visit in last 12 months │
#> No │ 67 16.8 354 29.5
#> Yes │ 333 83.2 846 70.5
#>
#> Variable │ p Cramer's V
#> ─────────────────────────────────┼───────────────────
#> Current smoker │ <.001 .14
#> No │
#> Yes │
#> (Missing) │
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │ <.001 .21
#> No │
#> Yes │
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Dentist visit in last 12 months │ <.001 .22
#> No │
#> Yes │The default output is "default", which prints a styled
ASCII table to the console, folded into successive panels when it is
wider than the console. Reading the table: each level of by
gets a pair of columns, the count n and the column
percentage computed within that group – percentages sum to 100
down each education group (for smoking within “Lower secondary”: 68.6 +
29.9 + 1.5 = 100.0) – and the Total pair is the margin over
all groups combined. In the last panel, p is the
p-value of the chi-squared test of association between the row
variable and by, computed once per variable on its full
cross-table, and the final column is the association measure – here
Cramer’s V (covered below). Use output = "data.frame" for a
plain data frame suitable for further processing.
One-way tables
Omit by to build a frequency-style table for the
selected variables:
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.8select itself is optional: when omitted, the table
includes every eligible categorical column in the data – factor,
character, logical, and labelled (haven) columns – excluding the
by column, mirroring the select-less defaults of
table_continuous() and table_continuous_lm().
An explicit select is taken verbatim, so numeric-coded
categorical variables can still be tabulated by naming them.
Output formats
table_categorical() supports several output formats. The
table below summarizes the options:
| Format | Description |
|---|---|
"default" |
Styled ASCII table in the console (default) |
"data.frame" |
Wide data frame, one row per level |
"long" |
Long data frame, one row per level x group |
"gt" |
Formatted gt table |
"tinytable" |
Formatted tinytable |
"flextable" |
Formatted flextable |
"excel" |
Excel file (requires excel_path) |
"clipboard" |
Copy to clipboard |
"word" |
Word document (requires word_path) |
The three rendered formats ("gt",
"tinytable", "flextable") are demonstrated in
the final section, together with the file exports.
Data frame output
Use output = "data.frame" for a wide data frame with one
row per level: two character identifier columns (Variable,
Level), numeric columns carrying the counts and
full-precision percentages, and the chi-squared statistic,
df, p-value, and association measure repeated on
every row of a variable’s block. Use output = "long" for a
long format with one row per level x group, the Total
margin included as a group:
table_categorical(
sochealth,
select = smoking,
by = education,
output = "data.frame"
)
#> Variable Level Lower secondary n Lower secondary %
#> 1 Current smoker No 179 68.582375
#> 2 Current smoker Yes 78 29.885057
#> 3 Current smoker (Missing) 4 1.532567
#> Upper secondary n Upper secondary % Tertiary n Tertiary % Total n Total %
#> 1 415 76.994434 332 83.00 926 77.166667
#> 2 112 20.779221 59 14.75 249 20.750000
#> 3 12 2.226345 9 2.25 25 2.083333
#> Chi2 df p Cramer's V
#> 1 21.62672 2 2.012877e-05 0.1356677
#> 2 21.62672 2 2.012877e-05 0.1356677
#> 3 21.62672 2 2.012877e-05 0.1356677Custom labels
By default, table_categorical() labels each row block
with the variable’s label attribute when one is present (e.g. data
imported with haven), and with the column name otherwise –
the same auto-detection as table_continuous() and
table_continuous_lm(). Use the labels argument
to override either: a named character vector keyed by
column name in data. Only listed columns are relabelled;
the others keep their attribute label or column name. (Unnamed
positional label vectors, accepted before 0.13.0, now raise an
error.)
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = education,
labels = c(
smoking = "Smoking status",
physical_activity = "Regular physical activity"
)
)
#> Categorical table by education
#>
#> Variable │ Lower secondary n Lower secondary %
#> ───────────────────────────┼──────────────────────────────────────
#> Smoking status │
#> No │ 179 68.6
#> Yes │ 78 29.9
#> (Missing) │ 4 1.5
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │
#> No │ 177 67.8
#> Yes │ 84 32.2
#>
#> Variable │ Upper secondary n Upper secondary % Tertiary n
#> ───────────────────────────┼──────────────────────────────────────────────────
#> Smoking status │
#> No │ 415 77.0 332
#> Yes │ 112 20.8 59
#> (Missing) │ 12 2.2 9
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │
#> No │ 310 57.5 163
#> Yes │ 229 42.5 237
#>
#> Variable │ Tertiary % Total n Total % p Cramer's V
#> ───────────────────────────┼─────────────────────────────────────────────────
#> Smoking status │ <.001 .14
#> No │ 83.0 926 77.2
#> Yes │ 14.8 249 20.8
#> (Missing) │ 2.2 25 2.1
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Regular physical activity │ <.001 .21
#> No │ 40.8 650 54.2
#> Yes │ 59.2 550 45.8Association measures and confidence intervals
table_categorical() picks the association measure per
row variable based on the variable type
(assoc_measure = "auto", the default):
-
2x2 (binary row variable vs. binary
by) ->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 each
variable.
Seven measures are available: "cramer_v",
"phi", "gamma", "tau_b",
"tau_c", "somers_d", and
"lambda"; assoc_measure = "none" drops the
column entirely. See ?table_categorical for the dispatch
details and
vignette("association-measures", package = "spicy") for
definitions and guidance on choosing among them. Override the automatic
choice with a single string for uniform application, or with a named
vector to mix measures per row:
# Uniform: same measure for every row variable
table_categorical(
sochealth,
select = smoking,
by = education,
assoc_measure = "lambda"
)
#> 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 Lambda
#> ────────────────┼───────────────
#> Current smoker │ <.001 .00
#> No │
#> Yes │
#> (Missing) │A significant chi-squared p-value next to a lambda of exactly .00 is not a contradiction. Goodman-Kruskal lambda measures the proportional reduction in the error of predicting the row variable once the group is known, and it is exactly 0 whenever the modal category is the same in every group: here “No” is the most frequent answer at all three education levels (68.6%, 77.0%, 83.0%), so knowing education never changes the best single guess, even though the distributions clearly differ (hence the significant chi-squared test).
In a named vector, variables you do not name keep the
"auto" choice, so only the overrides need to be listed.
Here "auto" would pick Cramer’s V for smoking
(binary x ordered, not 2x2) and Kendall’s Tau-b for
self_rated_health (ordered x ordered); the named vector
keeps the former and replaces the latter with Goodman-Kruskal Gamma:
# Named vector: override "auto" for one variable only
table_categorical(
sochealth,
select = c(smoking, self_rated_health),
by = education,
assoc_measure = c(self_rated_health = "gamma")
)
#> 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
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Self-rated health │
#> (Missing) │ 1 0.4 12
#> Poor │ 28 10.7 28
#> Fair │ 86 33.0 118
#> Good │ 102 39.1 263
#> Very good │ 44 16.9 118
#>
#> 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
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Self-rated health │
#> (Missing) │ 2.2 7 1.8 20
#> Poor │ 5.2 5 1.2 61
#> Fair │ 21.9 62 15.5 266
#> Good │ 48.8 193 48.2 558
#> Very good │ 21.9 133 33.2 295
#>
#> Variable │ Total % p Effect size
#> ───────────────────┼─────────────────────────────
#> Current smoker │ <.001 .14
#> No │ 77.2
#> Yes │ 20.8
#> (Missing) │ 2.1
#> ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
#> Self-rated health │ <.001 .31
#> (Missing) │ 1.7
#> Poor │ 5.1
#> Fair │ 22.2
#> Good │ 46.5
#> Very good │ 24.6
#>
#> Note. Cramer's V: Current smoker; Goodman-Kruskal Gamma: Self-rated health.smoking keeps its automatic Cramer’s V (.14) while
self_rated_health switches from the automatic Tau-b (.20)
to Gamma (.31) – Gamma is larger on the same table because it ignores
tied pairs. Since the two measures differ, the header collapses to
"Effect size" and the Note. line documents the
per-row choice.
Add confidence intervals with assoc_ci = TRUE. In the
rendered formats (gt, tinytable,
flextable, word) the CI is shown inline after
the measure, e.g. .14 [.08, .19] (demonstrated in the final
section). In the default console table and in the wide data formats
("data.frame", "excel",
"clipboard"), separate CI lower and
CI upper columns are added; in the long format
("long") the bounds appear as ci_lower /
ci_upper:
table_categorical(
sochealth,
select = smoking,
by = education,
assoc_ci = TRUE,
output = "data.frame"
)
#> Variable Level Lower secondary n Lower secondary %
#> 1 Current smoker No 179 68.582375
#> 2 Current smoker Yes 78 29.885057
#> 3 Current smoker (Missing) 4 1.532567
#> Upper secondary n Upper secondary % Tertiary n Tertiary % Total n Total %
#> 1 415 76.994434 332 83.00 926 77.166667
#> 2 112 20.779221 59 14.75 249 20.750000
#> 3 12 2.226345 9 2.25 25 2.083333
#> Chi2 df p Cramer's V CI lower CI upper
#> 1 21.62672 2 2.012877e-05 0.1356677 0.07909264 0.1913716
#> 2 21.62672 2 2.012877e-05 0.1356677 0.07909264 0.1913716
#> 3 21.62672 2 2.012877e-05 0.1356677 0.07909264 0.1913716Weighted tables
Pass survey weights with the weights argument. By
default (rescale = FALSE) the weights are used as-is;
rescale = TRUE rescales them so the total weighted N equals
the number of observations (here 1200, against a raw weight sum of
1196.474). Displayed counts are weighted counts rounded to integers at
display time – the SPSS Crosstabs convention – while the machine formats
("data.frame", "long") carry the exact
fractional weighted counts:
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.4Handling missing values
By default, missing values are displayed as a “(Missing)” category
(drop_na = FALSE), so the percentages sum over every
observation. Set drop_na = TRUE to remove them. The removal
happens before each cross-tabulation, variable by variable: each row
variable keeps its own complete cases (on itself and on
by), so the total N can differ across variables within the
same table – this is available-case analysis per cross-table, not
listwise deletion over the whole selection. The removal is disclosed in
a table note rather than silent:
table_categorical(
sochealth,
select = income_group,
by = education,
drop_na = TRUE
)
#> Categorical table by education
#>
#> Variable │ Lower secondary n Lower secondary %
#> ────────────────────────┼──────────────────────────────────────
#> Household income group │
#> Low │ 87 33.7
#> Lower middle │ 92 35.7
#> Upper middle │ 58 22.5
#> High │ 21 8.1
#>
#> Variable │ Upper secondary n Upper secondary % Tertiary n
#> ────────────────────────┼──────────────────────────────────────────────────
#> Household income group │
#> Low │ 115 21.7 45
#> Lower middle │ 186 35.1 110
#> Upper middle │ 135 25.5 135
#> High │ 94 17.7 104
#>
#> Variable │ Tertiary % Total n Total % p Kendall's Tau-b
#> ────────────────────────┼──────────────────────────────────────────────────────
#> Household income group │ <.001 .22
#> Low │ 11.4 247 20.9
#> Lower middle │ 27.9 388 32.8
#> Upper middle │ 34.3 328 27.7
#> High │ 26.4 219 18.5
#>
#> Missing values removed: income_group (18).With several selected variables the note lists each variable’s
removals separately: selecting smoking and
physical_activity together under
drop_na = TRUE keeps 1175 and 1200 complete cases
respectively, in the same table.
Filtering and reordering levels
Use levels_keep to display only specific levels. The
order you specify controls the display order, which is useful for
placing “(Missing)” first to highlight missingness:
table_categorical(
sochealth,
select = income_group,
by = education,
drop_na = FALSE,
levels_keep = c("(Missing)", "Low", "High")
)
#> Categorical table by education
#>
#> Variable │ Lower secondary n Lower secondary %
#> ────────────────────────┼──────────────────────────────────────
#> Household income group │
#> (Missing) │ 3 1.1
#> Low │ 87 33.3
#> High │ 21 8.0
#>
#> Variable │ Upper secondary n Upper secondary % Tertiary n
#> ────────────────────────┼──────────────────────────────────────────────────
#> Household income group │
#> (Missing) │ 9 1.7 6
#> Low │ 115 21.3 45
#> High │ 94 17.4 104
#>
#> Variable │ Tertiary % Total n Total % p Kendall's Tau-b
#> ────────────────────────┼──────────────────────────────────────────────────────
#> Household income group │ <.001 .22
#> (Missing) │ 1.5 18 1.5
#> Low │ 11.2 247 20.6
#> High │ 26.0 219 18.2levels_keep filters the display only: counts,
percentages, the chi-squared test, and the association measure are all
still computed on the full cross-table. That is why the displayed
percentages no longer sum to 100 (within “Lower secondary”: 1.1 + 33.3 +
8.0 = 42.4 – the hidden “Lower middle” and “Upper middle” levels still
count in the denominator), and why p and Tau-b are identical to
the unfiltered table. To recompute the statistics on a subset of levels,
filter the data before calling table_categorical().
Formatting options
Control the number of digits for percentages, p-values, and the association measure:
table_categorical(
sochealth,
select = smoking,
by = education,
percent_digits = 2,
p_digits = 4,
v_digits = 3
)
#> Categorical table by education
#>
#> Variable │ Lower secondary n Lower secondary % Upper secondary n
#> ────────────────┼─────────────────────────────────────────────────────────
#> Current smoker │
#> No │ 179 68.58 415
#> Yes │ 78 29.89 112
#> (Missing) │ 4 1.53 12
#>
#> Variable │ Upper secondary % Tertiary n Tertiary % Total n Total %
#> ────────────────┼─────────────────────────────────────────────────────────────
#> Current smoker │
#> No │ 76.99 332 83.00 926 77.17
#> Yes │ 20.78 59 14.75 249 20.75
#> (Missing) │ 2.23 9 2.25 25 2.08
#>
#> Variable │ p Cramer's V
#> ────────────────┼────────────────────
#> Current smoker │ <.0001 .136
#> No │
#> Yes │
#> (Missing) │p_digits drives both the displayed precision of the
p column and the small-p threshold
(p_digits = 3 -> <.001,
p_digits = 4 -> <.0001), matching
table_continuous() and
table_continuous_lm().
Decimal alignment
By default (align = "decimal") numeric columns are
aligned on the decimal mark, the standard scientific-publication
convention (SPSS, SAS, LaTeX siunitx). Numeric cells are
pre-padded with figure-spaces (U+2007, spaces exactly one digit wide) so
that 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 engine (gt, tinytable,
flextable, word, clipboard, ASCII
print) for a homogeneous rendering – same single-font policy as
table_regression(). The native
gt::cols_align_decimal() and
tinytable::style_tt(align = "d") primitives are
deliberately not used: the former renders visually right-aligned and the
latter centres each cell on its own value rather than on the decimal
mark, which would be inconsistent with the other engines.
"center" and "right" apply literal
alignment:
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex,
align = "right"
)
#> 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.8Tidying for downstream pipelines
table_categorical() returns an object that can be
coerced to a plain data.frame / tbl_df
(stripping the spicy formatting attributes) or piped into
broom::tidy() / broom::glance() for any
downstream tidyverse-stats workflow:
out <- 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 row per (variable x level x group) with broom-style columns
# (outcome, level, group, n, proportion). The synthetic Total
# margin is excluded so each observation is counted once.
broom::tidy(out)
#> # A tibble: 10 × 5
#> outcome level group n proportion
#> <chr> <chr> <chr> <int> <dbl>
#> 1 Current smoker No Female 475 0.766
#> 2 Current smoker No Male 451 0.778
#> 3 Current smoker Yes Female 131 0.211
#> 4 Current smoker Yes Male 118 0.203
#> 5 Current smoker (Missing) Female 14 0.0226
#> 6 Current smoker (Missing) Male 11 0.0190
#> 7 Regular physical activity No Female 334 0.539
#> 8 Regular physical activity No Male 316 0.545
#> 9 Regular physical activity Yes Female 286 0.461
#> 10 Regular physical activity Yes Male 264 0.455
# One row per outcome with the omnibus chi-squared test and the
# chosen association measure (test_type, statistic, df, p.value,
# assoc_type, assoc_value, assoc_ci_lower / assoc_ci_upper, n_total).
broom::glance(out)
#> # A tibble: 2 × 10
#> outcome test_type statistic df p.value assoc_type assoc_value
#> <chr> <chr> <dbl> <int> <dbl> <chr> <dbl>
#> 1 Current smoker chi_squa… 0.136 1 0.713 Phi 0.0107
#> 2 Regular physical act… chi_squa… 0.0452 1 0.832 Phi 0.00614
#> # ℹ 3 more variables: assoc_ci_lower <dbl>, assoc_ci_upper <dbl>, n_total <int>Rendered outputs and export
The rendered formats produce publication-ready tables for HTML and
Word workflows. The "gt" format produces a table with
APA-style borders, column spanners, and decimal alignment:
pkgdown_dark_gt(
table_categorical(
sochealth,
select = c(smoking, physical_activity, dentist_12m),
by = education,
output = "gt"
)
)|
Variable
|
Lower secondary
|
Upper secondary
|
Tertiary
|
Total
|
p
|
Cramer's V
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
| n | % | n | % | n | % | n | % | |||
| Current smoker | <.001 | .14 | ||||||||
| No | 179 | 68.6 | 415 | 77.0 | 332 | 83.0 | 926 | 77.2 | ||
| Yes | 78 | 29.9 | 112 | 20.8 | 59 | 14.8 | 249 | 20.8 | ||
| (Missing) | 4 | 1.5 | 12 | 2.2 | 9 | 2.2 | 25 | 2.1 | ||
| Regular physical activity | <.001 | .21 | ||||||||
| 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 | ||
| Dentist visit in last 12 months | <.001 | .22 | ||||||||
| No | 113 | 43.3 | 174 | 32.3 | 67 | 16.8 | 354 | 29.5 | ||
| Yes | 148 | 56.7 | 365 | 67.7 | 333 | 83.2 | 846 | 70.5 | ||
The "tinytable" format applies the same layout
conventions through the lightweight tinytable engine (here a two-group
table by sex):
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = sex,
output = "tinytable"
)| Variable | Female | Male | Total | p | Phi | |||
|---|---|---|---|---|---|---|---|---|
| n | % | n | % | n | % | |||
| Current smoker | .713 | .01 | ||||||
| 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 | ||
| Regular physical activity | .832 | .01 | ||||||
| No | 334 | 53.9 | 316 | 54.5 | 650 | 54.2 | ||
| Yes | 286 | 46.1 | 264 | 45.5 | 550 | 45.8 | ||
With assoc_ci = TRUE, the rendered formats show the
confidence interval inline after the association measure:
pkgdown_dark_gt(
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = education,
assoc_ci = TRUE,
output = "gt"
)
)|
Variable
|
Lower secondary
|
Upper secondary
|
Tertiary
|
Total
|
p
|
Cramer's V
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
| n | % | n | % | n | % | n | % | |||
| Current smoker | <.001 | .14 [.08, .19] | ||||||||
| No | 179 | 68.6 | 415 | 77.0 | 332 | 83.0 | 926 | 77.2 | ||
| Yes | 78 | 29.9 | 112 | 20.8 | 59 | 14.8 | 249 | 20.8 | ||
| (Missing) | 4 | 1.5 | 12 | 2.2 | 9 | 2.2 | 25 | 2.1 | ||
| Regular physical activity | <.001 | .21 [.15, .26] | ||||||||
| 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 | ||
For Excel export, provide a file path:
table_categorical(
sochealth,
select = c(smoking, physical_activity, dentist_12m),
by = education,
output = "excel",
excel_path = "my_table.xlsx"
)For Word, use output = "word":
table_categorical(
sochealth,
select = c(smoking, physical_activity, dentist_12m),
by = education,
output = "word",
word_path = "my_table.docx"
)You can also copy directly to the clipboard for pasting into a spreadsheet or a text editor:
table_categorical(
sochealth,
select = c(smoking, physical_activity),
by = education,
output = "clipboard"
)See also
- See
vignette("table-continuous", package = "spicy")for the continuous counterpart with classical group-comparison tests. - See
vignette("table-continuous-lm", package = "spicy")for model-based continuous summary tables. - See
vignette("table-regression", package = "spicy")for the full coefficient table from one or several fittedlm/glmmodels (APA Table 3). - See
vignette("summary-tables-reporting", package = "spicy")for a cross-function reporting workflow that ties the four summary-table helpers together along the APA Table 1 / 2 / 3 sequence.