Skip to contents

spicy 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):

Stabilizing (still maturing; argument names may be tightened before 1.0 with a NEWS.md entry, but no silent behavioral changes):

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

Internal API (not part of the public surface; can change without notice – avoid calling directly from downstream code):

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

ConceptColumn
the variable a row describes, its displayed labelvariable, 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 groupstotal (logical)
a count, a percentage, a proportion between 0 and 1n, 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 intervalin 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 statistictest_type, statistic
the degrees of freedom, which follow the statisticdf 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_adjustp.value, p_unadjusted
the effect size: its measure, value and intervales_type, es_value, es_ci_lower, es_ci_upper
the standardized mean differencesmd_type, smd_value
the observations of a variable or of a model (rows, a row of zero weight left out), and their weighted countnobs, 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 variablen_groups, n_levels
the fit of a modelr.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_error

Catch-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::integer64 columns, degenerate grouping).

  • spicy_wrong_regime – signaled together with spicy_invalid_data when 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 of code_book(); the condition carries its messages in stderr.

  • spicy_missing_column – a referenced column is not in data; the message names it. Signaled together with spicy_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 with spicy_unsupported when the request is well formed but spicy does not compute it (e.g., weighted group tests in table_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 once keep, drop or show_intercept = FALSE have chosen the rows (for a panel of layout = "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 with spicy_invalid_input.

  • spicy_ame_satt_unsupported_formula – signaled together with spicy_unsupported when AME Satterthwaite degrees of freedom are unavailable for the model's formula structure; normally caught internally and surfaced as a spicy_fallback warning.

  • spicy_unsupported_class – the model class has no regression-frame method, so table_regression() cannot render it.

  • spicy_unsupported_vcov – the requested vcov mode is not available for this model class.

  • spicy_unsupported_standardized – the requested standardized mode is not available for this model class.

  • spicy_invalid_frame – an object failed the structural validation of the internal regression-frame contract behind table_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 with spicy_invalid_input so generic input handlers still catch it.

  • spicy_bad_authors – the authors argument of code_book() has none of its accepted shapes. Signaled together with spicy_invalid_input.

  • spicy_label_collision – a label a summary table adds (the row or column of missing values, the margin, a base row, the Overall row) would be displayed beside a level, a value or a label of yours that reads the same. Signaled together with spicy_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_warning

Catch-all parent for every warning. Leaves:

  • spicy_undefined_stat – the requested statistic is undefined for this input; result is NA (e.g., Tau-b on a table with all-zero marginals).

  • spicy_negative_weights_no_test – signaled together with spicy_undefined_stat when table_continuous_svy(), table_categorical_svy() or table_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 with spicy_undefined_stat when table_categorical_svy() withholds the percentage intervals of a domain whose weights change sign, because its ci_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 with spicy_undefined_stat when survey stops on estimates of table_continuous_svy(), table_categorical_svy() or table_battery_svy(), most often on a stratum with a single primary sampling unit under options(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 in survey_message (a character vector) and fn (a list of the same length).

  • spicy_dropped_na – NA observations were silently excluded from the computation (e.g., NA weights).

  • spicy_ignored_arg – an argument the call typed has no effect in its context (e.g., correct = TRUE on a non-2x2 table, excel_path without output = "excel"); the warning names that argument and no other.

  • spicy_unknown_name – an argument named by variable (labels, note_labels, a named digits, the named list of show_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(), a levels_keep of table_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 – a keep or drop pattern of table_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 with spicy_caveat when 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 with spicy_caveat when 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 model table_regression_uv() fits to a binary outcome under the default method).

  • 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 of code_book(), which it still wrote; the condition carries its messages in stderr. Signaled together with spicy_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 by cross_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_info

Parent for informational messages (emitted via rlang::inform(); muffle with withCallingHandlers(spicy_info = ...)). Leaf: spicy_silent_reference – reference levels are displayed nowhere under reference_style = "none" with factor_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 plain lme4::lmer() fit, spicy_lmertest_info.

Author

Maintainer: Amal Tawfik amal.tawfik@hesav.ch (ORCID) (ROR) [copyright holder]

Authors: