spicy: publication-ready tables for descriptive statistics and regression models
Source:R/spicy-package.R
spicy-package.Rdspicy provides publication-ready tables for descriptive statistics and regression models: frequency tables and cross-tabulations with association measures, categorical and continuous summary tables (by group or from a complex survey design), and regression tables for one or more models side by side, across more than thirty model classes, with robust standard errors, average marginal effects, and univariable screening. Tables follow APA conventions by default, can switch to named journal styles, and render identically in the console and the rich outputs. Declared missing values in labelled data are honored and disclosed throughout the descriptive tables; helpers cover codebooks, variable inspection, and row-wise summaries.
API stability
spicy is in active pre-1.0 development. Breaking changes are
made deliberately at minor-version bumps and are always
announced in NEWS.md. The API surface is partitioned as
follows; users planning to embed spicy in production pipelines
or downstream packages should rely on the stable surface.
Stable (signature and behavior preserved across 0.y.z and into 1.0.0; documented changes only):
Frequency / cross-tabs:
freq(),cross_tab()Variable inspection:
varlist()/vl(),label_from_names()Clipboard export:
copy_clipboard()Association measures (point estimates and documented CIs):
cramer_v(),phi(),contingency_coef(),yule_q(),gamma_gk(),kendall_tau_b(),kendall_tau_c(),somers_d(),lambda_gk(),goodman_kruskal_tau(),uncertainty_coef()
Stabilizing (still maturing; argument names may be tightened
before 1.0 with a NEWS.md entry, but no silent behavioral
changes):
Summary table builders:
table_categorical(),table_continuous(),table_continuous_lm(),table_outcome(), andvar_labels()for theirlabelsSurvey-design summary tables:
table_continuous_svy(),table_categorical_svy()Regression tables:
table_regression(),table_regression_uv(),table_regression_models(),as_structured()Inline citation:
inline()Table styles:
spicy_style(),spicy_style_names()Omnibus association overview:
assoc_measures()
Experimental (new since 0.13.0; the shape of the output and
the argument names may still move, with a NEWS.md entry, on their
OWN clock rather than the parent family's):
Display language and label overrides:
spicy_labels()(withoptions(spicy.language)/options(spicy.labels)), since 0.13.0Codebooks:
code_book()(thespicy_codebookobject, its Excel workbook, and its PDF), since 0.14.0Multiple-response questions:
table_multiresponse(), since 0.14.0Item batteries:
table_battery()and its design twintable_battery_svy(), since 0.15.0Stacked tables:
table_stack(), withtable_regression(layout = "stacked"), since 0.15.0
Internal API (not part of the public surface; can change without notice – avoid calling directly from downstream code):
ASCII rendering primitive:
spicy_print_table()(build_ascii_table()is no longer exported)
broom output shape
The broom::tidy() and broom::glance() methods of every table class,
spicy_categorical_table, spicy_categorical_svy_table,
spicy_continuous_table, spicy_continuous_svy_table,
spicy_continuous_lm_table, spicy_outcome_table,
spicy_multiresponse_table, spicy_battery_table,
spicy_battery_svy_table, spicy_regression_table and
spicy_stack_table, and the output = "long" frame of every table
function, speak one vocabulary: broom's name wherever broom defines
one, and one snake_case name per concept otherwise, the same in every
function. The outputs of a stacked table (table_stack()) open with
a panel column, the label of the panel a row belongs to.
One exception, the estimates: tidy() gives them broom's names, and
the long output of a descriptive table names each statistic it holds
(see the table below).
| Concept | Column |
| the variable a row describes, its displayed label | variable, label |
the level of a categorical variable, the group of by (in the tables that take a by) | level, group |
the role of the row's level (or item): a level, the row of missing values, ... | .row_role ("level", "missing", "summary", ...) |
the role of group: a group, the column of missing by values, the margin | .group_role ("group", "missing", "total") |
the Total margin of the groups | total (logical) |
| a count, a percentage, a proportion between 0 and 1 | n, pct, proportion, named by their base where a table has two (pct_cases, pct_responses, proportion_cases, proportion_responses) |
| the estimate of a row, its standard error and interval | in tidy(), and in the long output of table_regression(): estimate, std.error, conf.low, conf.high; in the long output of the descriptive tables, the name of the statistic: mean, se, ci_lower, ci_upper (median, med_ci_lower, ...; emmean, emmean_se, estimate_ci_lower, ... in table_continuous_lm()) |
| the test: its name and statistic | test_type, statistic |
| the degrees of freedom, which follow the statistic | df for a t or a chi-squared (Inf for a z); df and df.residual for an F, the Rao-Scott F and the F of a linear model included |
the p-value, and the raw one under p_adjust | p.value, p_unadjusted |
| the effect size: its measure, value and interval | es_type, es_value, es_ci_lower, es_ci_upper |
| the standardized mean difference | smd_type, smd_value |
| the observations of a variable or of a model (rows, a row of zero weight left out), and their weighted count | nobs, weighted_nobs |
the weighted count of a group (beside its n) | weighted_n |
the counts of the groups of by, of the categories of a variable | n_groups, n_levels |
| the fit of a model | r.squared, adj.r.squared as columns; as a row, its show_fit_stats token in variable (r2, adj_r2, aic, ...) |
variable holds the name of the column a row describes and label
its displayed label. outcome names the response of a regression
model and the single outcome of table_outcome(). group is a key of
the tables that take a by (table_categorical(),
table_categorical_svy(), table_continuous(),
table_continuous_svy(), table_multiresponse(), table_battery()
and table_battery_svy()): their tidy() carries it with or without
by, NA without. table_outcome() has
no group: its groupings are the level of each block. Nor has
table_continuous_lm(), whose by sets the columns of the table: its
levels are the level of the long output and the term of the
"emmean" rows of tidy().
A fit statistic is a column of glance() and of the long output,
under broom's name, and a row of output = "data.frame" (.row_role
"fit_stat"), keyed in variable by its show_fit_stats token, the
token inline() takes: r2 is the row of the column r.squared.
The proportions of
tidy() are between 0 and 1, and the Total rows stay, flagged by
total. Each role column describes the key column it follows,
whatever the orientation of the printed table: .row_role the row's
level, .group_role its group. A level or a group added for
missing values is told apart by its role, "missing", never by its
label, which follows the display language. With by, the long
output of table_categorical() and table_continuous() carries the
effect-size and SMD columns whether or not they were requested (NA
then), and that of table_continuous_lm() and table_outcome() the
effect-size columns; the survey tables, table_multiresponse() and
table_battery() compute neither. glance() has a fixed set of
columns.
p_value = FALSE removes from the long output the test columns that
neither the statistic nor an effect size needs; glance() keeps them,
NA, as does the tidy() of table_continuous_lm().
Model-level statistics (nobs, r.squared, ...) are in glance().
The long output carries them as well where its rows are models of
their own: in table_continuous_lm(), one model per outcome (the fit
on the first row of the outcome, its size on every row), and in
table_regression_uv(), one univariable model per predictor. The long
output of table_regression() has the same columns, NA: one shape
for the class. The printed objects carry the by column and its
label in the attributes group_var and group_label, the label the
title gives it (its label in note_labels, when there is one).
output = "data.frame" and as.data.frame() return the table as it
is displayed, one row per printed row, with numbers: the key columns
variable, label (and the level or group of the row, and
.row_role, the role of the printed row, where the table adds rows),
then the displayed statistics under their keys, which join the header
of a spanner (a group of by, the coverage of an interval) to the
header printed under it: "Female n", "95% CI LL". The two are
identical for every class. A stacked table (table_stack()) is the exception to one
row per printed row: its data frame binds the data frames of its
panels by rows, under a leading panel column, without the rows that
open the panels or repeat their headers. output = "excel" and
"word" return the path of the
file, invisibly. The columns of the object output = "default"
returns are internal and can change without notice: the contracts are
output = "data.frame", output = "long", tidy(), glance() and
as_structured().
The set of columns produced by each method is considered
stabilizing (experimental for table_multiresponse(), as the
function itself): existing columns will not be
silently renamed or have their semantics changed within 0.y.z,
and any breaking change is announced in NEWS.md. Adding optional new
columns (e.g. covariate-adjustment metadata) is not a breaking
change. Numeric columns keep the types downstream
broom-consumers expect: test degrees of freedom that are integer
by construction (chi-squared, factor-comparison F tests) stay
integer, while every degrees-of-freedom column that can be
fractional – df.residual, the regression method's
per-coefficient df, and Welch-corrected test df – is
numeric double, so Satterthwaite-corrected degrees of freedom
from cluster-robust variance modes are preserved verbatim
(matching lmerTest::glance() and the afex output
convention).
Classed conditions
All errors and warnings emitted by the stable / stabilizing
surfaces carry classed conditions so downstream code can
dispatch on class via tryCatch() / withCallingHandlers()
instead of matching message strings. Each condition has a
package-wide parent class plus a leaf class describing the
specific cause:
spicy_errorCatch-all parent for every error raised by spicy. Leaves:
spicy_invalid_input– bad argument value or type.spicy_invalid_data– bad data shape or content (not a data.frame, data or a design with no rows, length mismatch,bit64::integer64columns, degenerate grouping).spicy_wrong_regime– signaled together withspicy_invalid_datawhen a table receives the wrong kind of data for its regime: a survey design handed to a plain table, or a data frame handed to a survey twin. The message names the function to use instead.spicy_missing_pkg– a Suggests dependency is required by the requested operation but not installed.spicy_missing_quarto–code_book()was asked for a PDF, and Quarto 1.7 or later, which compiles it, was not found.spicy_typst_failed– Typst could not compile the PDF ofcode_book(); the condition carries its messages instderr.spicy_missing_column– a referenced column is not indata; the message names it. Signaled together withspicy_invalid_input.spicy_unsupported– the operation is not applicable to this input (e.g., Phi requested on a non-2x2 table).spicy_not_implemented– signaled together withspicy_unsupportedwhen the request is well formed but spicy does not compute it (e.g., weighted group tests intable_continuous()); the message names an alternative.spicy_empty_table–table_regression()has no row to show for a model: it has no coefficient, every requested row block is switched off, or no coefficient row is left oncekeep,droporshow_intercept = FALSEhave chosen the rows (for a panel oflayout = "stacked", or for every model of the table);table_stack()received a table without rows.spicy_stack_mismatch–table_stack()received tables built in different languages, with different decimal marks or alignments, or under styles that format a cell differently. Signaled together withspicy_invalid_input.spicy_ame_satt_unsupported_formula– signaled together withspicy_unsupportedwhen AME Satterthwaite degrees of freedom are unavailable for the model's formula structure; normally caught internally and surfaced as aspicy_fallbackwarning.spicy_unsupported_class– the model class has no regression-frame method, sotable_regression()cannot render it.spicy_unsupported_vcov– the requestedvcovmode is not available for this model class.spicy_unsupported_standardized– the requestedstandardizedmode is not available for this model class.spicy_invalid_frame– an object failed the structural validation of the internal regression-frame contract behindtable_regression().spicy_structured_version–as_structured()received a table built under an older version of the typed contract; rebuild the table with the current spicy.spicy_resampling_failed– bootstrap / jackknife resampling produced too few valid replicates to estimate the requested statistic.spicy_defunct– an argument, or a value of an argument, removed or renamed in a pre-1.0 hard break (e.g.,v_digits,effect_size = "eta_sq"); the message names the replacement. Signaled together withspicy_invalid_inputso generic input handlers still catch it.spicy_bad_authors– theauthorsargument ofcode_book()has none of its accepted shapes. Signaled together withspicy_invalid_input.spicy_label_collision– a label a summary table adds (the row or column of missing values, the margin, a base row, theOverallrow) would be displayed beside a level, a value or a label of yours that reads the same. Signaled together withspicy_invalid_input; the message gives the two ways out.spicy_internal– an internal precondition failed; this is a bug in spicy, please report it.spicy_internal_invariant– an internal consistency check on a spicy-built object failed and the result cannot be trusted (see the warning leaf of the same name for the renderable case).
spicy_warningCatch-all parent for every warning. Leaves:
spicy_undefined_stat– the requested statistic is undefined for this input; result isNA(e.g., Tau-b on a table with all-zero marginals).spicy_negative_weights_no_test– signaled together withspicy_undefined_statwhentable_continuous_svy(),table_categorical_svy()ortable_battery_svy()withholds a group comparison because the analytic sample carries negatively weighted rows; the estimates are still reported and the table note says so.spicy_negative_weights_no_ci– signaled together withspicy_undefined_statwhentable_categorical_svy()withholds the percentage intervals of a domain whose weights change sign, because itsci_method("logit"or"likelihood") fits a logistic regression, which does not allow negative weights. The percentages are still reported, the table note says so, and the warning names the methods that accept negative weights.spicy_svy_estimation_failed– signaled together withspicy_undefined_statwhen survey stops on estimates oftable_continuous_svy(),table_categorical_svy()ortable_battery_svy(), most often on a stratum with a single primary sampling unit underoptions(survey.lonely.psu = "fail"). The cells that could not be estimated show a dash and the table note says what the dash means. One warning per table, which carries the table call, quotes each distinct message of survey with the calls that stopped on it; the condition carries them insurvey_message(a character vector) andfn(a list of the same length).spicy_dropped_na–NAobservations were silently excluded from the computation (e.g.,NAweights).spicy_ignored_arg– an argument the call typed has no effect in its context (e.g.,correct = TRUEon a non-2x2 table,excel_pathwithoutoutput = "excel"); the warning names that argument and no other.spicy_unknown_name– an argument named by variable (labels,note_labels, a nameddigits, the named list ofshow_columns) holds a name that matches nothing in the table; the warning names the argument and the name, and the table is built without it.spicy_no_selection– a selection or a filter matched nothing, and an empty result is returned rather than an error (the row-wise summaries,varlist(), alevels_keepoftable_categorical()that matches no level of a variable, which is left out). In the table functions an empty column selection is an error (spicy_invalid_input).spicy_no_match– akeepordroppattern oftable_regression()matched no coefficient term.spicy_fallback– the requested computation failed; a simpler estimator was used instead.spicy_caveat– the computation succeeded but its interpretation carries a non-trivial methodological caveat (e.g., standardized coefficients on non-additive terms).spicy_bayes_diagnostics– signaled together withspicy_caveatwhen a Bayesian fit's sampler or predictive-accuracy diagnostics miss their targets (R-hat, ESS, divergences, E-BFMI, Pareto k, p_waic).spicy_nonconvergence– signaled together withspicy_caveatwhen the fitting engine reports that its optimizer did not converge. The table still shows the numbers the model object holds, and a footer note says what they are worth.spicy_model_choice– a defaulted modeling choice was made on the user's behalf and is disclosed (e.g., the linear probability modeltable_regression_uv()fits to a binary outcome under the defaultmethod).spicy_passthrough– a third-party warning captured during an operation (e.g., the clipboard copy) and re-emitted under the spicy taxonomy.spicy_typst_warning– Typst warned while compiling the PDF ofcode_book(), which it still wrote; the condition carries its messages instderr. Signaled together withspicy_passthrough.spicy_summary_failed–varlist()could not summarize one column; the rest of the table is fine.spicy_renamed_column– a user data column or factor level collided with a spicy-internal name and was auto-renamed to preserve the data (emitted bycross_tab()).spicy_internal_invariant– an internal consistency check on a spicy-built object failed but the output still renders, so the user sees both the table and the diagnostic.
spicy_infoParent for informational messages (emitted via
rlang::inform(); muffle withwithCallingHandlers(spicy_info = ...)). Leaf:spicy_silent_reference– reference levels are displayed nowhere underreference_style = "none"withfactor_layout = "flat". The once-per-session hint on ordered-factor polynomial contrasts carries its own class,spicy_polynomial_contrasts_info, and so does the one that suggests lmerTest for a plainlme4::lmer()fit,spicy_lmertest_info.
Author
Maintainer: Amal Tawfik amal.tawfik@hesav.ch (ORCID) (ROR) [copyright holder]
Authors:
Amal Tawfik amal.tawfik@hesav.ch (ORCID) (ROR) [copyright holder]