Skip to contents

spicy (development version)

Breaking changes

  • Declared missing values are now honored package-wide. Codes that survey files declare as missing (na_values / na_range metadata on haven-imported columns, and haven::tagged_na() values) are treated as missing by default in freq(), cross_tab(), table_categorical(), table_continuous(), table_continuous_lm(), mean_n(), sum_n(), count_n(), varlist(), vl(), and code_book(): they are excluded from statistics exactly like NA (valid percents, means, chi-squared tests and association measures, min_valid gates, group definitions), so numbers change for labelled survey data. Nothing disappears silently: freq() shows each declared value as its own labelled row in its Missing block (tagged NAs get per-tag rows with their labels), and the tabulation helpers disclose the exclusion in the table note (“Declared missing values removed: …”). Migration: every function involved gains the same escape hatch, user_na = FALSE, which restores the previous behavior of treating declared codes as valid values. See the new “Declared missing values” section in ?freq.

  • varlist(), vl(), and code_book() count columns are now internally coherent for labelled data: N_distinct uses the same missing definition as N_valid / NAs (declared missing values and NA elements of list and POSIXlt columns no longer count as distinct valid values), na_range codes observed in the data are listed in Values like na_values codes instead of vanishing, and value labels attached to tagged NAs (e.g. Refused = tagged_na("a")) now appear in Values.

  • cross_tab() now tabulates observations at an explicit NA factor level (addNA(), factor(exclude = NULL), forcats::fct_na_value_to_level()) as a regular NA category row or column instead of silently dropping them: an explicit level is the analyst’s choice to show missing as a category, so totals, percentages, and the chi-squared statistic now include it, matching freq() and base table() on the same input. Numbers change for tables built from such factors.

  • freq() on a factor with an explicit NA level now excludes those observations from the valid-percent denominator (and the n_valid attribute), matching the Missing classification its own printed table gives them and the SPSS convention; the Valid Percent column previously summed to less than the 100.0 printed in its Total row.

  • build_ascii_table() is no longer exported. It has always been documented as internal plumbing; use spicy_print_table() for console rendering from code.

  • The column_total_line argument of build_ascii_table() and spicy_print_table() is removed. It was accepted and documented but never had any effect.

  • Association measures: detail = TRUE vectors now always include the standard error as an se element, and the internal-only .include_se argument is gone from every public signature.

  • gamma_gk(), kendall_tau_b(), and kendall_tau_c() report an NA p-value when the asymptotic standard error is zero (e.g. a perfect association), instead of a spurious 0 or NaN – the same zero-SE gate as the other association measures.

  • uncertainty_coef() returns NA with a spicy_undefined_stat warning when the marginal entropy in the denominator is zero (a constant variable, e.g. an unused factor level), instead of silently returning 0 – the same degenerate-table behavior as the rest of the association family.

  • kendall_tau_c() returns NA with a spicy_undefined_stat warning when one variable is constant (all observations in a single row or column), instead of a silent definite 0 with SE 0 and a zero-width confidence interval – the same degenerate-table contract as the rest of the ordinal family, and what SPSS / PSPP report (no value).

  • The association measures and assoc_measures() validate conf_level: anything other than a single number strictly between 0 and 1 (or NULL to omit the confidence interval) raises a classed error (spicy_invalid_input), instead of silently producing Inf or NaN confidence bounds. conf_level = 95 gets a hint suggesting conf_level = 0.95.

  • freq() and cross_tab() replace the logical styled argument with output, the same rendering argument the table_*() family uses. Migration: styled = TRUE is now output = "default" (the default) and styled = FALSE is now output = "data.frame". Passing styled raises a classed error (spicy_defunct) that spells out the replacement. The rendered engines of the table_*() family ("tinytable", "gt", "flextable", …) are not accepted by the two tabulators.

  • cross_tab(output = "data.frame") (previously styled = FALSE) now returns a genuinely plain data.frame (and a list of plain data frames with by): the metadata attributes (title, note, n_total, chi2, p_value, assoc_*, …) are stripped, as documented. For programmatic access to the statistics, read the attributes of the default object instead, e.g. attr(cross_tab(...), "p_value").

  • freq() now defaults to rescale = FALSE (raw weighted counts), matching cross_tab(), and reads options(spicy.rescale) the same way cross_tab() does. Call freq(..., rescale = TRUE) to restore the previous behavior.

  • freq() no longer prints as a side effect: it returns its spicy_freq_table visibly and regular auto-printing displays it, so a bare freq(...) call still shows the table while f <- freq(...) is now silent (print f to display it). The unused ... argument is removed from the signature; passing unknown arguments now errors.

  • copy_clipboard() arguments use snake_case like every other spicy function: row_names_as_col, row_names, and col_names (were row.names.as.col, row.names, col.names). The old dot.case names raise an error that names the exact replacement.

  • table_regression(exponentiate = TRUE) now errors on links whose exponentiated coefficient is not a ratio (probit, cauchit, inverse, sqrt, …). Ratio links (logit, log, binomial / ordinal cloglog) are unchanged; identity links keep the warn-and-skip.

  • table_categorical() defaults to drop_na = FALSE: missing values display as a "(Missing)" level instead of being silently removed. With drop_na = TRUE, a table note now reports what was removed.

  • table_categorical() rejects p_digits below 1 with a classed error, as its documentation always promised; such values were silently rendered with 3 decimals. Matches table_continuous(), table_continuous_lm(), and cross_tab().

  • table_categorical() uses the same labels contract as table_continuous() and table_continuous_lm(): a named character vector (labels = c(smoking = "Current smoker")), with the other columns falling back to the variable’s label attribute (e.g. from haven) and then to the column name. Unnamed positional label vectors, accepted since before 0.11.0, now raise an error with a migration hint.

  • table_categorical(output = "flextable") no longer writes a .docx as a side effect when word_path is supplied: word_path is consulted only by output = "word", as everywhere else in the table family. The old combination now warns (class spicy_ignored_arg); save the returned object with flextable::save_as_docx() if you relied on it.

  • standardized = "smart" scales continuous inputs by 2 SD and leaves binary inputs (0/1 and factor dummies) unscaled, as Gelman (2008) defines it. The rule was applied inverted since 0.12.0, halving every continuous “smart” beta.

  • align = "auto" is removed from all table_*() functions; use "decimal" (default), "center", or "right".

  • table_regression(show_fit_stats = character(0)) errors; use FALSE to suppress the block.

  • Multi-model show_columns = "all_b" / "all_ame" auto-compact (CIs dropped); request atomic tokens to keep them.

  • tidy() labels AME rows estimate_type = "ame" (was "AME").

  • The show_fit_stats information criteria are lowercase tokens like everything else: "aic", "aicc", "bic" (was "AIC" / "AICc" / "BIC"). Uppercase errors with the replacement; rendered row labels are unchanged.

  • The SE footer reads "classical (Fisher information)" (was "classical (MLE inverse Hessian)").

  • count_n() now warns (class spicy_no_selection) and returns NA for all rows when the selection resolves to zero usable columns, matching mean_n() and sum_n() (it silently returned all zeros). A valid selection where the value is simply absent still counts 0.

  • mean_n() and sum_n() with min_valid = 0 now return NA for rows with no valid values; mean_n() returned NaN and sum_n() a silent 0 (the raw rowMeans() / rowSums() identities).

New supported models

table_regression() gains support for some 30 model classes beyond lm / glm. See ?table_regression_models for the full registry and per-family behaviour, and the new vignettes for walk-throughs.

  • Mixed effects (lme4::lmer / glmer, glmmTMB, nlme::lme): random effects as a block of rows (SD, correlations, residual, with SE and CI), ICC, per-group N and marginal / conditional R² as fit statistics, and a boundary-corrected LR test against the model without random effects. The test follows the fit’s own estimator (REML or ML) and its full specification (prior weights, variance and correlation structures, zero-inflation).
  • Bayesian (rstanarm, brms): posterior median, MAD SD, and equal-tailed credible intervals (header 95% CrI; ci_method = "hdi" opts into the highest-density interval); no p-values — the probability of direction is opt-in (show_columns = "pd"). R² (Bayes) in the default fit statistics; "elpd_loo" / "looic" / "waic" opt-in with their standard errors in the footer and reliability caveats when the diagnostics flag them. Every table runs a sampler-diagnostics guard (R-hat, ESS, divergences, E-BFMI): problems add a footer line and a spicy_bayes_diagnostics warning, and per-coefficient "rhat" / "ess_bulk" / "ess_tail" columns are available – plus "mcse", the Monte Carlo standard error of the displayed posterior median (the criterion for how many digits the table can honestly show). Under exponentiate = TRUE all quantities come from the exponentiated draws (no delta method). The AME columns are draws-native too: avg_slopes() runs per posterior draw and the table reports the posterior median, MAD SD and credible interval of those draws (no "ame_p" – same policy as the p column). Standardized betas work for the algebraic flavors ("posthoc" / "basic" / "smart", exact affine rescales of the draws) on fixed-effects Bayesian fits: stan_glm-style models and standard-formula brm() models (design matrix recovered through insight; the scale factors are engine-invariant). Multilevel fits, non-GLM stanreg subclasses (stan_polr, stan_betareg), brms formulas with distributional or special terms, and "refit" / "pseudo" are refused with a pre-standardization hint. Variational / optimizing fits are refused with a refit hint.
  • Survival (survival::coxph / survreg, rms::cph, flexsurv::flexsurvreg): Cox tables report n and N events as fit statistics and the concordance as a footer note.
  • Categorical and ordinal (nnet::multinom, mlogit::mlogit, MASS::polr, ordinal::clm): a single multinom renders outcome categories as column groups (outcome_labels relabels them); mlogit uses a two-segment alternative-specific layout; ordinal thresholds render as a labelled block (show_thresholds = FALSE to opt out), and partial-proportional-odds clm fits render their non-proportional terms as their own block.
  • Survey (survey::svyglm); robust / IV / panel (estimatr, AER::ivreg, fixest); beta, Tobit, and two-part counts (betareg, AER::tobit, pscl::zeroinfl / hurdle); plus MASS::rlm / glm.nb, quantreg::rq, mgcv::gam / bam, stats::nls, rms::ols / lrm / Glm, sampleSelection::selection.

Requests a class cannot honour are refused with a classed error (spicy_unsupported_vcov, spicy_unsupported_standardized) instead of rendering an empty column.

New functions

  • table_regression_uv(): univariable screening tables. One fit per candidate predictor, rendered as one table with a row block per predictor and merged side by side with the multivariable model. Supports lm (the default linear screen), glm (selected automatically when a family is supplied), and coxph (outcome = Surv(time, status)). Under the linear default, a binary-looking outcome proceeds as a linear probability model and says so in a classed warning pointing to vcov = "HC3" and to method = "glm" for the logistic screen (see “Why the default screen is linear” in ?table_regression_uv). A per-predictor N column is shown by default and a note discloses when Ns differ across fits; complete_cases = TRUE forces the common sample. Intercepts are hidden by default; show_intercept = TRUE displays each univariable fit’s own intercept at the top of its block along with the multivariable one. p_adjust treats the whole screen as one family. exponentiate, vcov / cluster (the footer names the cluster column), labels, the output engines, and tidy() work as in table_regression().
  • table_regression_models(): the machine-readable registry of supported model classes (family, engine, AME, exponentiate semantics); its help page is the per-family reference.

New features

  • table_regression() supports GEE fits from geepack::geeglm() (population-averaged models). The fit’s own sandwich standard errors – clustered on its id =, or the jackknife variants chosen via geeglm’s std.err = – are the displayed inference (Wald z), and the footer discloses the working correlation structure with its estimated alpha. Default fit statistics report the cluster structure (n, N (<id>), largest cluster); "qic", "qicu", and "scale" are opt-in show_fit_stats tokens. AME columns and the usual exponentiate gates (OR / IRR / RR by link) work as for glm. Requests GEE cannot honour are refused with actionable messages: spicy’s vcov / cluster (inference is already robust by construction; clustering belongs to id =), standardized, nested = TRUE, and likelihood-based fit statistics (compare working models with QIC).
  • New show_columns token families "rmst" and "risk_diff" for coxph fits: covariate-adjusted differences in restricted mean survival time over [0, tau] and in cumulative incidence at at_time, computed by g-computation from the fitted model, with bootstrap SEs, CIs, and p-values. Factor predictors get one row per level (vs the reference); continuous predictors report the +1-unit contrast. The horizon is explicit and required – tau = "minmax" picks the smallest per-group maximum follow-up – and the table note states the estimand and the replicate count.
  • New show_columns token "n_events": outcome event counts displayed as events/N next to the estimates – per factor level (reference row included) with model totals on continuous rows, computed on each model’s own estimation sample. Available for binomial outcomes – glm, glmer, and glmmTMB fits alike – and right-censored coxph fits; opt-in everywhere.
  • re_ci = "profile": profile-likelihood CIs for the variance components of lmer / glmer fits (asymmetric, boundary-respecting; no SE column, the footer discloses the method).
  • re_test = "lrt" / "rlrt": opt-in per-term tests for the variance components – a boundary-corrected LR test or the exact restricted LRT – filling the otherwise empty test columns of the Random effects rows.
  • ci_method = "profile" gives profile-likelihood CIs for glm, polr, and clm; new ci_method = "boot_percentile" (with vcov = "bootstrap") reports percentile bootstrap CIs from the same replicates as the bootstrap SEs. The footer names the method.
  • Heteroskedasticity- and cluster-robust vcov for the supported classes. Resampling footers report the valid replicate count, and a bootstrap / jackknife whose replicates nearly all fail raises spicy_resampling_failed instead of silently reporting classical SEs.
  • Cluster-robust vcov ("CR0"-"CR3") for nnet::multinom, including its AME columns (needs sandwich >= 3.1-2, which added the estfun() method). HC* stays refused: a multi-equation model has no working residuals.
  • Under a cluster-robust vcov, the ordinal Thresholds block (polr / clm) now takes its SEs, z, p and CIs from the same cluster sandwich as the slopes – previously the cut-point rows kept model-based SEs under a footer describing the table as cluster-robust.
  • AME columns are available for betareg, mgcv::gam, svyglm, survreg, fixest, estimatr, quantreg::rq, AER::ivreg, and rms fits, and per outcome category for polr / clm / multinom. AME SEs, CIs, and p-values honour a robust vcov.
  • fixest tables disclose their absorbed fixed effects by default: a Fixed effects: block at the top of the fit statistics with one Yes / No row per factor (blank for non-fixest models in mixed tables; varying-slope-only factors absorb no intercept and read No where another model absorbs that factor), plus the within R-squared ("within_r2") for feols and McFadden’s pseudo-R² for feglm / fepois in the default statistics. The "n_groups" token now renders one N (<factor>) row per grouping factor – for absorbed fixed effects and crossed or nested random effects alike – instead of cramming every factor into one cell.
  • Two-part models show their full model: the zero component of pscl::zeroinfl / hurdle and the ziformula / dispformula components of glmmTMB render as labelled row blocks with full inference (show_components = FALSE to opt out). A component is exponentiated only when its link yields a ratio.
  • Class-aware fit-statistics defaults: pseudo-R² for ordinal and multinomial fits; every other class falls back to nobs + AIC instead of a blank block. The N (groups) row upgrades to plain counts (e.g. N (Subject)) when models share a single grouping factor.
  • select is optional in table_categorical(): when omitted, every eligible categorical column is tabulated – factor, character, logical, and labelled (haven) columns, excluding by – matching the select-less defaults of table_continuous() and table_continuous_lm(). An explicit select is still taken verbatim, so numeric-coded categorical variables can be tabulated by naming them.
  • Seven new vignettes: Mixed-effects, GEE (population-averaged), Multinomial, Count and two-part, Survival, and Ordinal regression tables, plus Categorical predictors — a cross-cutting guide to dummy coding and reference levels, joint tests of a factor, ordinal predictors (scores vs dummies), successive-difference contrasts, and why continuous predictors should not be categorized.
  • table_categorical() reads options(spicy.rescale) like cross_tab(); an explicitly supplied rescale argument still wins.
  • table_continuous() gains drop_na, mirroring table_categorical(): the default TRUE keeps the historical behavior (rows with a missing by value are removed), while FALSE displays them as a "(Missing)" group with the group-comparison test and effect size still computed on the observed groups only. In both modes the default output now discloses removed missing values in a table note (“Missing values removed: …”) instead of dropping them silently.

Row-wise summaries

  • count_n() resolves select and exclude through the same tidyselect path as mean_n() and sum_n(): unknown names in a character select raise a classed error (instead of a base subscript error), and exclude accepts positions as well as names.
  • count_n() validates every special entry before expanding "all", so a typo supplied alongside "all" (e.g. special = c("all", "banana")) errors instead of being silently discarded.
  • count_n(special = character(0)) errors with a clear classed message (an empty special selects nothing to count) instead of crashing downstream with an obscure base error.

Minor improvements

  • cross_tab() now discloses excluded missing values in the table note (Missing values removed: x (2), y (1); 2 rows in total.; with by, also Rows with missing g removed: 2.) instead of silently dropping the NA rows from the tabulation. The deduplicated row total appears when both variables lost values, so overlapping per-variable counts cannot be misread as a sum.

  • cross_tab() accepts logical weights, coerced to 1/0 like freq() already did; they previously raised an error.

  • cross_tab() warns (class spicy_ignored_arg) when a third positional argument is supplied in vector mode, e.g. cross_tab(df$x, df$y, df$z); the extra argument used to be silently ignored.

  • cross_tab() validates digits with the same classed error as freq(); invalid values (negative, fractional, non-numeric) used to degrade the display silently or fail with obscure base-R errors.

  • freq()’s error for an invalid sort value now lists "" (no sorting) among the valid choices.

  • copy_clipboard() re-emits clipboard backend messages and warnings as real R conditions instead of colored console text, and signals the “row_names_as_col has no effect” notice as a classed warning (spicy_ignored_arg), so suppressMessages() / suppressWarnings() and condition handlers work; quiet = TRUE still silences everything at once. Its invisible return value is now documented as the object actually sent to the clipboard (reflecting a requested row-name promotion).

  • Invalid values for the enum arguments of the user-facing surface (output, align, percent, assoc_measure, direction, method, …) now raise a classed spicy_invalid_input error naming the argument and its valid values, instead of the locale-dependent base match.arg() error that escaped the tryCatch(spicy_error = ...) catch-all.

  • print() on cross_tab() tables validates its digits argument (single non-negative integer) with a classed error.

  • spicy_print_table() raises classed errors (spicy_invalid_data / spicy_invalid_input) when x is not a data frame or display_labels does not have one label per column, replacing bare stopifnot() failures.

  • table_categorical(), table_continuous(), and table_continuous_lm() announce their clipboard export with a classed message (spicy_info), so it can be muffled with withCallingHandlers(spicy_info = ...) like every other spicy signal.

  • table_regression()’s internal invariant check on the structured body warns with a classed condition (spicy_internal_invariant) instead of a bare warning().

  • Wide multi-model tables split into stacked panels more cleanly: continuation panels carry no empty stub rows, and over-wide column spanners truncate with a visible ellipsis.

  • Under exponentiate = TRUE with a visible SE column, the footer states the SE scale (delta method) and that the CI bounds are asymmetric.

  • show_fit_stats = FALSE suppresses the fit-statistics block.

  • Placeholder cells decimal-align in the gt / flextable / tinytable / Word / Excel outputs; "deviance" prints at 1 decimal; the descriptive tables use a single font in Word outputs.

  • table_continuous()’s “test is ignored” warning states the full trigger condition (p_value, statistic, effect_size, and effect_size_ci all turned off) instead of naming only the first two toggles.

  • varlist(), vl(), and code_book() annotate difftime values with their units in Values (e.g. 1.5, 2.5 (hours)); the bare numbers were ambiguous between hours and days.

  • varlist() and vl() return tibble columns without stray names attributes (the variable names leaked onto 5 of the 7 columns), so element-wise identical() and snapshot comparisons behave the same for every column.

Bug fixes

  • The flextable outputs of table_categorical(), table_continuous() and table_continuous_lm() carry the same spicy_flextable wrapper as table_regression(), so notes and knit-time rendering behave identically across the four functions.

  • The asymptotic standard error of kendall_tau_b() mis-scaled one margin term of its gradient, so every SE, confidence interval, and Wald p-value it reported was wrong – typically by under 1% on near-balanced tables, but by up to 12% on tables with skewed margins (the CI could be off at the second displayed decimal). Every CRAN release from 0.6.0 through 0.12.0 shipped the wrong formula; point estimates were always correct. The corrected ASE matches SPSS / PSPP CROSSTABS to 7 decimals and is pinned to PSPP 2.0 on six tables. It deliberately diverges from DescTools::KendallTauB(), which computes the same wrong SE. Also affects the tau-b rows of assoc_measures() and the default ordered-by-ordered association line of cross_tab().

  • somers_d(direction = "symmetric") returns the correct 0 instead of a silent NA when concordant and discordant pairs are exactly equal (e.g. an independence-pattern table), matching SPSS / PSPP; the association line requested from cross_tab() no longer silently disappears on such tables.

  • assoc_measures() no longer swallows the classed warnings its measures raise on degenerate tables: each distinct warning (e.g. spicy_undefined_stat for an undefined statistic) is re-emitted once after the table is assembled, so -- rows come with their signal and condition handlers / suppressWarnings() keep working.

  • cramer_v(), phi(), and contingency_coef() return NA with a spicy_undefined_stat warning on tables with a zero row or column margin (the chi-squared statistic is NaN there), instead of a silent NaN (detail = FALSE) or a crash (detail = TRUE); their assoc_measures() rows now carry that signal too.

  • cross_tab() no longer blanket-suppresses warnings while computing the association measure for its note: classed warnings from the measures reach the caller, as in assoc_measures().

  • cross_tab(), table_categorical(), and assoc_measures() label the tau_c association measure "Stuart's Tau-c" (the SPSS / PSPP name) everywhere: the cross_tab() note, its assoc_measure attribute, and the assoc_measures() summary row used to say "Kendall's Tau-c" while the table_categorical() column header said "Stuart's Tau-c".

  • cross_tab() and table_categorical() no longer swallow the classed error an association measure raises when it does not apply to the tabulated table (e.g. phi on a labelled column whose declared missing codes are kept as valid values by user_na = FALSE, making the table 3x2): the documented hard error surfaces instead of a silent all-NA association column. assoc_measure = "auto" now counts levels under the same user_na regime as the table, so it picks a measure that matches the table actually tested.

  • table_categorical(), table_continuous(), and table_continuous_lm() resolve by data-first, like tidyselect: a column always wins over a same-named variable in the calling environment. An environment variable holding another column’s name used to silently redirect the grouping to that other column.

  • table_categorical() keeps a by factor level that is declared but never observed as an explicit zero column (0 n, 0.0 %) in the wide and long outputs; the wide output used to carry blank NA cells under that group’s columns while the long output omitted the group entirely.

  • table_categorical() computes the ordinal association measures (tau_b, tau_c, gamma, somers_d, including the automatic tau-b for a pair of ordered factors) on the table in the declared level order under the default drop_na = FALSE. An internal character round-trip re-sorted the levels alphabetically before tabulation, so those measures were silently wrong whenever the alphabetical order differed from the ordinal order (education by self-rated health reported tau-b 0.02 instead of the correct 0.20); chi-squared, p, Cramer’s V, and Phi were unaffected, and drop_na = TRUE was always correct.

  • table_categorical() displays the value labels of labelled (haven) columns as "[code] label" levels in every path, as freq() does; the raw codes (1, 2, 3) used to be displayed under drop_na = FALSE and in grouped tables, so the rendering depended on drop_na and on by.

  • table_categorical() keeps both the group and the margin when a by level is literally named "Total": the true margin is always present (auto-renamed to "Total_1" with a single spicy_renamed_column warning, mirroring cross_tab()), and the user’s "Total" group keeps its own column. The margin used to disappear silently, with the user’s group displayed under the Total header. tidy() and glance() on such tables now drop the real margin instead of the user’s group.

  • table_categorical() machine outputs (output = "data.frame", "long") carry full-precision values in grouped tables: weighted counts are exact and percentages are no longer pre-rounded to 1 decimal, matching the ungrouped path. percent_digits = 2 therefore renders true decimals (33.33, not the fabricated 33.30), and tidy() keeps exact fractional weighted counts instead of truncating them.

  • table_categorical() displays counts as integers everywhere: weighted cells and their Total margin are rounded by the same display rule (round half to even, at display time only – the SPSS Crosstabs convention). A weighted row used to mix integer cells with a fractional margin. Cells and margins are rounded independently, so small display discrepancies remain possible; the machine outputs carry the exact weighted counts.

  • table_categorical(correct = TRUE) on a non-2x2 table warns once that Yates’ correction is ignored, with the dimensions of the table actually tested; the warning used to fire three times per variable, twice with the dimensions of an internal table that included the "(Missing)" column.

  • table_categorical(output = "data.frame") includes the documented Chi2 and df columns in grouped tables, consistent with the long output and glance().

  • print() on freq() tables invisibly returns the table object itself (as documented), not the internally rebuilt display frame.

  • freq() keeps the variable label footer when observations with NA weights are dropped; base subsetting used to strip the label attribute from plain vectors that carry a variable label without value labels (the haven pattern), silently losing the footer.

  • freq(sort = "name+") / "name-" on labelled variables sorts by the underlying code whenever the code is displayed (labelled_levels "prefixed" or "values"), matching the SPSS by-value convention; string collation used to rank [10] ahead of [2]. With labelled_levels = "labels" the alphabetical label sort is unchanged.

  • table_categorical() warns (class spicy_no_selection) and lists the available level strings when levels_keep matches nothing for a selected variable, instead of silently dropping the variable from the table. For labelled columns the matching strings are the "[code] label" level strings, never the bare label text.

  • varlist(), vl(), and code_book() render POSIXlt columns as datetime values under values = TRUE, as the compact summary already did, instead of a list-column summary (List(3): list).

  • varlist(), vl(), and code_book() show an explicit NA factor level (e.g. from addNA()) as <NA> in Values instead of silently dropping it, so Values and N_distinct agree on the declared levels.

  • label_from_names() no longer blames the split for duplicate column names that already existed in the input (check.names = FALSE data): pre-existing duplicates pass through untouched, and only collisions created by the renaming error.

  • table_regression_uv() no longer silently ignores family with method = "lm": a non-gaussian family is refused with the same actionable error as the coxph refusal, and a supplied gaussian() is ignored with a classed warning.

  • gt and flextable outputs now render in Quarto / R Markdown Word, PowerPoint, and PDF documents (they silently disappeared from non-HTML targets). A new as_flextable() method returns the underlying flextable for manual composition.

  • table_continuous_lm() now discloses robust and resampling SEs in its table note, carries its notes into every rich output, and accepts cluster = ~region.

  • table_continuous_lm() reports correct estimated means, SEs, CIs, and the displayed binary difference when by is an ordered factor. The model was fitted with polynomial contrasts while the prediction grid assumed treatment coding, so every M column was wrong (and the 2-level difference was shrunk by exactly 1/sqrt(2)) while the F statistic and p-value stayed correct, masking the error. A categorical by is now fitted with explicit treatment contrasts, matching the documented convention whatever the session sets: an ordered by no longer picks up polynomial coding, and a session-wide options(contrasts = c("contr.sum", "contr.poly")) (common in ANOVA workflows) no longer alters the results or aborts a binary by.

  • table_continuous_lm(adjustment = "balanced") computes correct adjusted means when a covariate is an ordered factor: the synthetic grid keeps the covariate’s ordered coding (contr.poly) instead of silently rebuilding it with treatment columns against polynomial coefficients. adjustment = "proportional" was not affected.

  • table_continuous() forms groups from a non-factor by (character, numeric, haven labelled) in order of first appearance, matching table_categorical(); they used to be re-sorted, so the two sibling tables could show the same groups in different orders. The group-comparison test and effect size follow the displayed order, so the sign of a two-group statistic matches the first-minus-second convention of the table rows.

  • table_continuous() degrades per variable when a group-comparison test or effect size fails or is undefined on degenerate data (e.g. t.test()‘s “data are essentially constant”, or a zero pooled SD making Hedges’ g non-finite): the affected cells become NA with a classed warning naming the variable, and the other selected variables keep their results. The raw error used to kill the whole multi-variable table.

  • table_continuous_lm() treats a haven labelled by with value labels as a categorical predictor – groups over the raw codes, the same dispatch as table_continuous() and table_categorical() – instead of silently fitting a continuous slope on the codes. A labelled by without value labels is still treated as continuous.

  • table_continuous_lm() fits cleanly when a factor covariate declares a level that never occurs in the data: empty covariate levels are dropped at fit entry, so the table matches the same model on droplevels()-ed data instead of crashing.

  • table_continuous_lm() excludes rows with NA in weights from the analytic sample, as its documentation always promised; such rows used to raise a hard “must contain only finite values” error. Rows dropped for a missing by value or a missing weight are now disclosed in the table note, like the rest of the family.

  • table_continuous_lm() accepts covariates with non-syntactic names (e.g. "co var"); they used to raise a raw formula parse error.

  • table_continuous_lm() errors with a classed, actionable message when by has a single observed level (nothing to compare; an all-NA row used to print silently), and reports NA SEs, CIs, tests, and p-values with a classed warning when the fit is saturated (one observation per group); NaN cells with a misleading "z" test label used to be displayed. When missing values leave a single outcome with fewer than two observed groups (or too few observations for a slope), that outcome degrades with a classed warning naming it, and its NA row keeps the regular group columns instead of growing a spurious M (NA) column; the other outcomes are unaffected.

  • table_continuous_lm(adjustment = "proportional") averages the G-computation predictions with the case weights when weights is supplied (the Stata margins convention, equivalent to marginaleffects::avg_predictions(wts = )); the aggregation used to ignore the weights, contradicting the documented Stata equivalence.

  • table_continuous_lm(effect_size = "omega2", effect_size_ci = TRUE) reports the correct confidence interval for the covariate-adjusted partial omega-squared, matching effectsize::omega_squared(partial = TRUE); the bounds used to be those of the partial eta-squared.

  • table_regression() partial effect sizes are now true Type-II tests in models with interactions. The focal F behind partial_f2 / partial_eta2 / partial_omega2 (and their CIs), and the partial_chi2 likelihood-ratio chi-square for glm, test each term by comparing the two nested models that both exclude every higher-order interaction containing it (the car::Anova(type = 2) convention), so main-effect values respect marginality and no longer change with the factor coding (treatment, sum, Helmert). Values change only for models with interaction terms; additive models and the interaction terms themselves are unchanged. The same convention now governs the term-level Wald chi-square that partial_chi2 reports for mixed-effects models (lmer / glmer / glmmTMB / nlme::lme).

  • The RMST and risk-difference columns extend to parametric survival models (survival::survreg): the AFT curves are closed-form, so the standardized survival is exact up to numerical integration. Cross-validated against flexsurv::standsurv() (exact) and the closed-form exponential RMST (machine precision). Stratified survreg fits (per-stratum scale) are refused.

  • The univariable screen (table_regression_uv(method = "coxph")) accepts the RMST and risk-difference columns: unadjusted per-predictor estimands next to the covariate-adjusted ones of the multivariable column, all at one shared horizon (tau = "minmax" is refused there – per-fit horizons would not be comparable).

  • The RMST and risk-difference columns now support stratified Cox models (strata()): g-computation keeps each subject’s own stratum baseline, and the table note says so. The strata variable itself gets no contrast row. Cross-validated against adjustedCurves::adjusted_rmst() (exact).

  • MASS::glm.nb fits gain opt-in dispersion fit-stat rows: show_fit_stats tokens "theta" (the NB2 dispersion) and "alpha" (its reciprocal, the Stata convention). Refused with a clear error for other families.

  • betareg fits gain the matching opt-in precision row: the show_fit_stats token "phi", back-transformed from the precision link (y ~ x | 1 reports the same phi as y ~ x). Refused for other families and when the precision has covariates (y ~ x | z), so phi is not a single number.

  • nnet::multinom fits now report McFadden’s and Nagelkerke’s pseudo-R² (as the other categorical families do). The tokens were silently dropped from show_fit_stats before, and a weighted fit’s null log-likelihood ignored the weights.

  • Bayesian tables get their fit statistics: "r2_bayes" (the posterior-median Bayesian R², now in the all-Bayesian default block) and the opt-in "elpd_loo" / "looic" / "waic" tokens (PSIS-LOO / Watanabe-Akaike; the footer discloses the elpd standard error). Refused for frequentist fits; Bayes factors stay out by design.

  • All-Bayesian tables drop the p column from the defaults (a dash column carries no information), refuse an explicit "p" / "t" request, expand the "all_b*" presets without them, and label the interval header 95% CrI. Mixed frequentist + Bayesian tables keep the shared 95% CI label and dash the Bayesian p cells.

  • Bayesian tables gain the "pd" column (show_columns): the posterior probability of direction, the reporting-guideline-aligned reading of “is there an effect” – computed at extraction since 0.12 but never exposed. Refused for frequentist fits.

  • Bayesian tables tighten up. stan_glmer / multilevel brm fits report their random effects as a proper block (posterior median SD, credible interval from the draws) instead of one flat row per group deviation; the footer reads Random effects (MCMC) and no likelihood-ratio line is printed. p_adjust and likelihood-based fit-statistic tokens (AIC, pseudo-R², …) are refused with clear errors for all-Bayesian tables – both were silent no-ops – and the class-aware defaults report n only. Mixed frequentist + Bayesian tables keep the frequentist columns filled.

  • Factors under non-default contrast codings (successive differences, sum-to-zero, Helmert, custom matrices) now group under their parent variable like treatment and polynomial codings, labelled by the contrast-matrix column names. No reference row is shown – none exists under those codings.

  • ordinal::clm(scale = ~) fits now render their scale (dispersion) coefficients as a Scale effects block. They were silently absent from the table: an estimated component of the model went unreported. The rows stay on the log scale under exponentiate = TRUE (their exponential is a ratio of latent standard deviations, not an odds ratio) and the footer says so.

  • broom::tidy() gains an outcome_level column naming the response category of per-category rows (ordinal and multinomial average marginal effects). Those rows were previously indistinguishable – four age AME rows sharing one term, with nothing to identify the category.

  • The random-effects LR test no longer prints a negative statistic (-0.00) on singular fits: the statistic is clamped at zero and the boundary p-value is now 1 – half the chi-bar-squared null distribution is a point mass at zero, so a zero statistic carries no evidence (it printed 0.500, the ceiling of the halved-chi-squared formula).

  • ci_level now reaches the random-effect variance-component CIs; all three mixed engines hardcoded 95% for those rows.

  • Large mixed fits no longer spend minutes on variance-component SEs: above options("spicy.re_se_max_n") (default 1000) those cells are omitted, with a note and a warning giving the override.

  • nested = TRUE now works for multinom (LR chi-square rows) and defaults to LRT rows for Cox comparisons (lm’s R² / F-change rows are undefined for a partial likelihood).

  • Quantile regression (quantreg::rq) gets its own vcov estimator family: the default is now the heteroskedasticity-robust "nid" sandwich (quantreg’s own large-sample default; the previous iid default understated SEs under heteroskedasticity and disagreed with the AME columns, which already used "nid"), with "iid", "ker", "rank" (rank-inversion CIs, no SE / t / p) and a native "bootstrap" as opt-ins. cluster works through the wild gradient cluster bootstrap (vcov = "bootstrap"); HC*, CR* and "jackknife" are refused with the reason. The footer names the estimator; AME columns share the coefficient rows’ matrix. nested = TRUE compares nested rq fits through anova.rq’s Wald-type F (all fits at one tau; mixing taus or model classes is refused with the reason).

  • AME columns that silently rendered empty are now populated (fixest, estimatr, quantreg, AER::ivreg, rms, pscl) or refused with a pointer to ?table_regression_models (classes with no AME backend).

  • The statistic column header follows each model’s actual reference distribution (z or t); it was hardcoded to t.

  • Factor coefficient and AME rows follow levels() order (was alphabetical); ordered factors with AME columns show a reference row; ame_ci / ame_p / ame_se populate without the bare "ame" token; stars anchor on B (and AME), never on beta.

  • Bootstrap / jackknife and standardized = "refit" refits no longer leak the caller’s environment and now work on factor() / log() / poly() formulas; a failed refit falls back with a warning instead of silently changing method.

  • Standardized beta rows on mixed fits inherit B’s reference distribution (they showed a second p-value for the same test); the standardized-coefficient table note states the interaction convention and is fallback-aware.

  • Titles: binomial mixed / survey fits are link-aware (a probit fit is no longer titled “Logistic”); Tobit titles name the response; ordinal titles name the shared-slopes assumption by its link; proper nouns keep their capitals in multi-model titles.

  • Mixed fits with cbind() responses get their LR test against the no-random-effects model; labels accepts coefficient-level keys on mixed fits; polr / clm detect non-uniform prior weights.

  • flexsurv: probit-scale splines and ancillary-parameter covariates refuse exponentiate = TRUE; factor predictors group under their parent variable with a reference row.

  • as_structured() and the rich output engines match the console body exactly (blank vs en-dash reference cells, the multi-outcome Outcome row); the structured schema gains reference_models_by_row and outcome_labels_by_col.

  • table_regression(m1, m2) without list() errors with a helpful message; colliding model labels no longer break output = "gt"; the p_adjust footer’s family size matches the adjustment performed; ci_method = "profile" with a robust vcov defers to the vcov and warns; the singular-fit note states the fact and leaves the advice to a build-time warning.

  • Average marginal effects now use the fit’s prior weights, as the Weights section always promised: for a weighted lm / glm / geeglm fit the AME (and its SE / CI / p) is the weighted average of the unit-level slopes, matching marginaleffects::avg_slopes(fit, wts = weights(fit)). AME values change for weighted fits (they matched the unweighted average before); svyglm is unaffected – its design weights were already applied natively.

  • nlme::gls fits populate the AME columns (show_columns = c("b", "ame")); the column rendered silently empty even though the model registry advertises AME support for gls.

  • Logical predictors get the grouped factor layout the documentation promises: an is_smoker: header with indented FALSE (ref.) / TRUE rows (and aligned AME cells), instead of a flat is_smokerTRUE row with no reference level.

  • In mixed-class tables, a fit statistic not defined for a model’s class renders an en-dash in that model’s cell (console and rich outputs), as documented; the cell was blank, indistinguishable from “not requested”. The first-column dash of the nested change statistics is unchanged.

  • show_fit_stats = "pseudo_r2_tjur" is refused with a classed error (spicy_invalid_input) when no model in the set is a binomial-family glm – Tjur’s R² needs a binary outcome; the requested row was silently dropped before. With at least one binomial model in the set, the non-binomial cells render the per-cell en-dash.

  • table_regression() returns carry the documented provenance attributes outcome and model_ids (one entry per model, in table order). output = "data.frame" carries the same pair, and as.data.frame() no longer drops col_spec, so the two documented equivalents now return identical objects (same cells, classes, and attributes).

  • table_regression(output = "long") returns the long-format tibble (tbl_df) its documentation always promised; it used to return a plain data.frame.

  • count_n() raises a classed error (spicy_invalid_input) when count is zero-length or contains only missing values, instead of silently returning a plausible-looking all-zero count; and rejecting count = NaN now points to special = "NaN" (the exact counterpart) instead of describing the input as count = NA and hinting at special = "NA", which counts NA and NaN together.

  • cross_tab() weighted count tables (percent = "none") compute the Total row and grand total from the unrounded weighted table, rounded once for display, instead of summing the already-rounded cells: with fractional weights the printed margins could contradict both the true weighted totals and the N row the percent tables derive from the same data, and under rescale = TRUE the Total row did not even sum to its own printed grand total.

  • cross_tab() titles and the weight footer no longer present a data value plucked out of an inline expression as a variable name (e.g. a title ending in x "g2", or Weight: 1 for a literal weight vector): when no name can be derived structurally, a neutral x / y / weights placeholder is used.

  • freq() warns (class spicy_caveat) when labelled_levels = "labels" merges distinct codes that share the same label text, naming the merged codes: the pooling is forced by factor semantics, but SPSS keeps one row per value, so the silently changed partition is now disclosed.

  • freq(valid = FALSE) with missing values present no longer prints a Valid Percent column of NA values whose Total row asserts 100.0; the column (and Cum. Valid Percent with cum = TRUE) is dropped whenever valid percentages were not computed.

  • freq(), cross_tab(), mean_n(), sum_n(), table_categorical(), table_continuous(), and table_continuous_lm() reject bit64::integer64 input (tabulated variables, weights, selected columns, by groups, and covariates) with a classed error (spicy_invalid_data) naming the fix: integer64 passes is.numeric() but stores raw 64-bit integer bit patterns that base R numeric code silently misreads as garbage counts near 1e-323 – a summary table showed M = 0.00 for every integer64 column, and group labels printed as denormal doubles unless the bit64 namespace happened to be loaded – a realistic hazard for BIGINT columns imported via DBI or data.table::fread(). Convert with as.integer() / as.numeric() (or as.character() for codes wider than 2^53) first; count_n() compares values without numeric aggregation and continues to work.

  • table_regression_uv() accepts family in the three forms stats::glm() accepts – a family object (binomial()), its name ("binomial"), or the bare constructor function (binomial) – and refuses anything else up front with a classed error (spicy_invalid_input); a name or bare constructor previously failed with a raw $ operator is invalid for atomic vectors error before any model was fitted.

spicy 0.12.0

CRAN release: 2026-05-19

New features

  • New table_regression(): publication-ready coefficient summary for one or more fitted lm or glm models, side by side. APA Manual 7 formatting is the default. Highlights:

    • Robust variance: classical, HC, cluster-robust (CR) with Satterthwaite df, bootstrap, jackknife. Per-model vcov accepted for SE-comparison tables.
    • Standardisation: refit, posthoc, basic, smart, pseudo (the last glm only).
    • Average marginal effects (AME) as separate columns; AME inference shares the coefficient’s variance estimator so B and AME are reported on the same inferential footing.
    • Partial effect sizes: f², η², ω² for lm (noncentral-F CIs); partial χ² for glm.
    • GLM response-scale reporting via exponentiate = TRUE, with family-appropriate labels (OR, IRR, HR, RR, MR, exp(B)) and optional profile-likelihood CIs (ci_method = "profile").
    • Multiplicity correction via p_adjust (any stats::p.adjust() method).
    • Hierarchical comparison via nested = TRUE (ΔR² / F-change for lm; LRT for glm).
    • Display controls: variable filtering, intercept and factor placement, reference-row styles, multi-model labels, stars, decimal mark, per-column digits.
    • Outputs: console, data.frame, long tibble, gt, flextable, tinytable, Excel, Word, clipboard. broom::tidy() and broom::glance() methods supported.

    See ?table_regression and vignette("table-regression").

  • table_continuous_lm() gains additive covariate adjustment via the new covariates argument. Two estimands for the per-group adjusted means: "proportional" (G-computation, default) and "balanced" (equal-weight synthetic grid). Under adjustment, and ω² become partial effect sizes; d and g raise an explanatory error. The auto-built footer documents the covariates and the estimand. See vignette("table-continuous-lm").

  • New exported as_structured() accessor returns a typed view of a table_regression() result for programmatic use: raw numerics, CI split into LL / UL columns, and a column-level format specification.

Breaking changes

  • code_book() no longer silently truncates the export filename to 120 characters. Very long titles now surface a clear OS-level error. Migration: shorten the title or pass an explicit filename = argument.

Bug fixes

  • table_categorical() no longer over-truncates a p-value in the interval (10^-p_digits, 0.001) when p_digits >= 4. Example: p = 0.000108 now correctly prints as ".0001" at p_digits = 4 (was "<.0001").
  • count_n(special = ...) returns a length-nrow(data) zero vector when no usable column survives the list-column filter, matching the documented contract and the count = ... branch (was numeric(0), which broke dplyr::mutate() pipelines).
  • lambda_gk() and goodman_kruskal_tau() emit spicy_undefined_stat and return a fully-NA result on rank-1 contingency tables (constant predicted variable), matching the existing pattern in gamma_gk(), kendall_tau_b(), somers_d(), and yule_q().
  • cross_tab() no longer silently overwrites a user’s y-variable level named "N", "Total" or "Values". The conflicting reserved column is auto-renamed with a numbered suffix and a single spicy_renamed_column warning is emitted.
  • broom::glance() on a spicy_continuous_lm_table keeps df.residual numeric, so Satterthwaite degrees of freedom from vcov = "CR2" / "CR3" are preserved verbatim instead of being truncated through as.integer().

Minor improvements

  • Console en-dash alignment: non-numeric placeholders (en-dash, “NA”) sit at the decimal-mark column instead of the integer- part column (APA Manual 7 §7.13). Integer cells in mixed- precision columns (n row alongside ) keep their right- aligned placement.
  • R/ source is byte-pure ASCII (tools::showNonASCIIfile() reports zero hits package-wide).
  • openxlsx2::wb_add_border() calls now pass NULL on unused sides, preventing the default "thin" from being applied to all four sides of a cell when only one rule is intended.

spicy 0.11.0

CRAN release: 2026-05-04

New features

table_continuous_lm()

  • Cluster-robust SEs via cluster and four vcov choices ("CR0""CR3"), dispatched to clubSandwich with Satterthwaite df (clubSandwich in Suggests).
  • vcov = "bootstrap" (nonparametric or cluster) and vcov = "jackknife" (leave-one-out / leave-one-cluster-out) variance estimators in pure base R, controlled by boot_n.
  • Three new effect_size choices alongside "f2": Cohen’s "d", Hedges’ "g" (two-group only), Hays’ "omega2". New effect_size_ci adds noncentral t / F CIs rendered inline as 0.18 [0.07, 0.30].
  • HC* estimators delegate to sandwich::vcovHC(); rank-deficient fits return a clean rank-by-rank covariance.

Harmonisation across the table family

  • Shared reporting vocabulary (decimal_mark, p_digits, align, named-labels) now spans cross_tab(), freq() and the three table_*() helpers, including APA-style p-value notation (<.001 / .045, no leading zero).
  • table_categorical()’s assoc_measure accepts a per-variable spec. When measures differ across rows the column collapses to "Effect size" and an APA-style Note. line documents the per-variable measure; phi on a non-2x2 errors.
  • All three table_*() functions gain as.data.frame(), tibble::as_tibble(), broom::tidy() and broom::glance() methods (broom in Suggests).

Quality and robustness

  • Classed conditions. Errors and warnings now carry stable classes (spicy_error / spicy_warning plus 11 leaf classes documented in ?spicy), so downstream code can dispatch via tryCatch() / withCallingHandlers() instead of matching message strings. rlang (>= 1.1.0) required.
  • Structured cli messages. Multi-line errors and warnings (vcov fallbacks, bootstrap/jackknife failures, padding migration, labels length mismatch) render as cli bullets.
  • Locale-deterministic ordering. Sorts in varlist(), freq(), cross_tab() and table_*() use method = "radix". Output is byte-stable across locales and platforms, matching Stata / SPSS guarantees.
  • Edge-case hardening. A new length-guarded sort helper makes varlist() / code_book() / cross_tab() / freq() survive zero-length or all-NA Date / POSIXct / character columns and factors with no observed levels.
  • Snapshot-locked rendering. tests/testthat/test-snapshots.R pins the exact console output of every spicy print method, so any unintended formatting drift surfaces as a PR diff.
  • API stability contract. ?spicy documents which exports are stable, stabilising or internal. pkgdown reference groups exports via four @family tags.
  • Cross-software validation. All 13 association measures agree with PSPP 2.0 (CROSSTABS /STATISTICS=ALL, 65 / 65 statistics on four datasets); Cohen’s d and Hedges’ g noncentral CIs are tested numerically against effectsize::cohens_d() / effectsize::hedges_g() (tolerance = 1e-6); point-estimate formulas and asymptotic standard errors follow DescTools (Signorell et al.).

Improvements

  • cross_tab() warns when correct = TRUE is ignored on a non-2x2 sub-table, when weights contains NA, and notes statistics computed on a sub-table after empty rows / columns are pruned.
  • cross_tab() validates decimal_mark, p_digits and simulate_B up front; freq() validates decimal_mark and tightens digits to a non-negative integer.
  • A user category literally named "N" or "Total" is no longer mis-rendered as the totals row in cross_tab().
  • table_continuous_lm(output = "long") returns n, df1, df2 as integer columns; predictor_label preserved on the degenerate-model fallback path.
  • cramer_v() / phi() doc states the CI uses the Fisher z-transformation (point estimate and p-value identical to DescTools / SPSS).
  • uncertainty_coef() doc states entropy uses 0 log 0 = 0 (matching SPSS, PSPP, Stata, Cover & Thomas).

Bug fixes

  • label_from_names() raises actionable errors on duplicate or empty new column names; trims whitespace and preserves the input class.
  • table_continuous_lm(output = "data.frame") names contrast CI columns from ci_level (was hardcoded to 95 %).
  • The categorical-predictor global Wald F degrades to NA on a singular coefficient covariance submatrix.
  • The degenerate-table branch of cramer_v(), yule_q(), gamma_gk(), kendall_tau_b() and somers_d() respects detail: scalar NA_real_ by default, fully shaped spicy_assoc_detail when detail = TRUE.
  • uncertainty_coef() returns a finite estimate (was NaN) when a marginal is zero.
  • somers_d(direction = "symmetric") returns the harmonic mean of the two asymmetric values, matching SPSS / PSPP CROSSTABS.
  • print.spicy_assoc_detail() / print.spicy_assoc_table() use APA-strict <.001 / .045 notation, matching the rest of the package.
  • varlist() / code_book() honour factor_levels = "all" for haven_labelled columns: declared-but-unobserved labels appear in the Values summary.
  • copy_clipboard() rejects row.names.as.col vectors of length ≠ 1 and empty strings; accumulates all messages from clipr::write_clip() instead of overwriting.
  • mean_n() / sum_n() reject non-integer min_valid >= 1 and min_valid > ncol; their digits requires a non-negative integer.

Breaking changes

  • table_continuous_lm() and table_categorical() default to decimal-point alignment for numeric columns (align = "decimal"). Pass align = "auto" for the previous behaviour.
  • build_ascii_table() / spicy_print_table(): padding switches from a string enum to a non-negative integer. Default 2L (was +5L); printed tables are roughly 40 % narrower. Migration: "compact" -> 0L, "normal" -> 2L, "wide" -> 4L.
  • table_categorical(assoc_measure = "auto") on a 2x2 table picks phi instead of cramer_v. Numeric value unchanged (|phi| = V on 2x2); only the column label changes.
  • freq() drops observations with NA weights (with a warning) instead of recoding them to zero. Aligns with cross_tab().
  • table_continuous_lm(output = "long") returns NA in es_type / es_value when effect_size = "none" (was "f2"), and renames sum_w to weighted_n.

spicy 0.10.0

CRAN release: 2026-04-27

New features

  • code_book() now accepts tidyselect-style variable selectors through ..., matching varlist() and vl().

  • code_book() gains a filename argument for the base name of CSV, Excel, and PDF exports. When NULL (the default), the filename is derived from title and falls back to "Codebook" when needed. Filenames are sanitized to portable ASCII consistently across platforms.

  • varlist() now summarizes matrix and array columns by their dimensions, and counts valid, missing, and distinct observations by rows.

  • freq() gains a factor_levels argument that mirrors varlist() and code_book(). With factor_levels = "all", declared-but-unobserved factor and labelled levels appear in the output with n = 0, matching SPSS FREQUENCIES; the default "observed" preserves the previous Stata tab-style behavior.

Improvements

  • varlist() now displays missing values as <NA> and <NaN> in the Values summary when include_na = TRUE, and quotes literal "NA", "NaN", and empty-string values so they cannot be confused with the missing markers.

  • varlist() now emits a column-named warning and marks the failing cell as <error: ...> when a column cannot be summarized, instead of silently writing "Invalid or unsupported format". Remaining columns are unaffected.

  • varlist() produces more precise Viewer titles for extraction, pipe, and literal get("name") expressions, while keeping ambiguous dynamic calls anonymous (vl: <data>).

  • code_book() now rejects partial-match names in ... (e.g. val = TRUE, tit = "x") that would otherwise be silently treated as tidyselect expressions, and surfaces varlist() selection errors directly.

  • freq() now resolves the weights argument via tidy-eval, so column references nested in compound expressions (e.g. weights = if (use_w) col else NULL) work as expected. Qualified expressions like weights = df2$w continue to take precedence over column lookup.

  • freq() validates digits, sort, weights, and the logical scalar arguments (valid, cum, rescale, styled) more strictly at the public boundary, with clearer error messages for non-finite values, NA, multi-element inputs, and non-numeric weight vectors.

  • freq() now documents the interaction of weights containing NA with rescale = TRUE (Stata pweight semantics) and the dropping of unused factor / labelled levels (Stata tab semantics, with code_book(factor_levels = "all") as the schema-style alternative).

Bug fixes

  • varlist() now displays labelled values in the same prefixed-label order for compact and values = TRUE summaries; previously the compact summary used data order.

  • varlist(values = TRUE) now deduplicates element types when summarizing list-columns. Previously list(1L, 2L, "a") produced "List(3): character, integer, integer"; now produces "List(3): character, integer".

  • include_na = TRUE now correctly appends <NA> markers for list-columns in both varlist() modes; previously it had no effect on this column type.

  • varlist() now validates column names up front and gives clearer errors for missing, empty, NA, or duplicate names.

  • varlist() now errors clearly when tidyselect expressions try to rename columns; ... is for selecting variables, not renaming.

  • freq(data, x, weights = NULL) now correctly treats the explicit NULL as “no weighting” instead of emitting a misleading "variable 'NULL' not found" error. Parameterized patterns like weights = if (use_w) wts else NULL are now supported.

  • print() for spicy_freq_table no longer crashes when the var_label attribute is NA_character_, numeric, or multi-element; the Label: line is silently skipped for any value that is not a single non-empty string.

  • freq() no longer surfaces the name of the ignored data vector in the printed footer when both data and x are passed as vectors. The footer now consistently shows the analyzed vector’s name.

spicy 0.9.0

CRAN release: 2026-04-20

Breaking changes

  • table_continuous() now enables inferential output by default when by is supplied. With a grouping variable, the p column from test is shown automatically (previous default hid it). This aligns the two table helpers: table_continuous() stays descriptive when by is absent, and reports the test p-value when by is supplied, matching table_continuous_lm()’s inferential default. To preserve the previous behavior, pass p_value = FALSE explicitly. statistic and effect_size remain FALSE by default and must still be enabled consciously.

  • varlist() now displays observed factor levels by default in Values, matching its role as a quick inspection of the current data. Use factor_levels = "all" to display unused factor levels as well, which was the previous default behavior and remains the default in code_book().

Minor improvements

  • code_book() gains a factor_levels argument. It defaults to "all" so exported codebooks continue to document all declared factor levels, including unused levels; use "observed" to mirror varlist() output.

  • freq() now prints the Freq. column as integers regardless of digits, which continues to control percentage precision. This matches the convention of SPSS, Stata, and SAS PROC FREQ for weighted counts and keeps the two numeric concepts (discrete counts vs. continuous percentages) visually distinct.

  • freq(..., styled = FALSE) now returns a genuinely plain data.frame with no spicy_freq_table rendering metadata clinging to it, so str(), dput(), and downstream programmatic use see only the tabulation columns. The metadata attributes (digits, data_name, var_name, var_label, class_name, n_total, n_valid, weighted, rescaled, weight_var) are now documented in @return and remain available on the invisibly returned spicy_freq_table object when styled = TRUE (the default).

  • table_continuous_lm() documentation now clarifies why p_value = TRUE and r2 = "r2" are the defaults, and robust-variance fallback warnings are now more explicit when a model matrix is singular.

Bug fixes

  • freq() now correctly resolves qualified weight expressions such as weights = other$w or weights = other[["w"]] even when the referenced column name also exists in data. Previously the bare-name fallback could silently pull the weight vector from the wrong data frame when column names collided.

  • freq() with sort and missing values now keeps the NA row at the end of the tabulation so the printed Cum. Percent and Cum. Valid Percent columns stay monotonic and match the Valid → Missing → Total display layout. Sorting previously could push the NA row between valid rows and make cumulative percentages appear to jump.

  • varlist() now preserves literal "NA" and empty-string values in the Values summary instead of removing them as if they were missing values.

  • varlist() now distinguishes actual NA values from NaN in the Values summary when include_na = TRUE.

  • varlist(values = TRUE) now preserves factor level order in the Values summary, matching the default compact factor display.

  • varlist() now validates values, tbl, and include_na up front and gives a clear error when one of them is not TRUE or FALSE.

spicy 0.8.0

CRAN release: 2026-04-10

New features

  • table_continuous_lm() adds APA-style bivariate linear-model tables for continuous outcomes. It acts as the model-based companion to table_continuous() for reporting fitted mean comparisons or slopes in an lm framework, with one predictor per model, model-based means for categorical predictors, optional case weights, classical or HC0-HC5 variance estimators, multiple output formats (ASCII, tinytable, gt, flextable, Excel, clipboard, and Word), output = "data.frame" for the wide raw table, output = "long" for the analytic long table, and configurable display of tests, confidence intervals, fit statistics, and effect sizes.

Minor improvements

  • Installed package vignettes now avoid embedding heavy HTML table and codebook widgets during CRAN builds, reducing package size while preserving rich pkgdown article rendering.

  • Website and vignette coverage now includes table_continuous_lm(), using the bundled sochealth data throughout and adding a dedicated article for model-based continuous summary tables.

  • table_continuous() and table_continuous_lm() now support dedicated display precision for effect-size columns, and table_continuous_lm() also supports separate precision for columns, so model fit and effect sizes can be formatted independently from descriptive values and test statistics.

  • table_continuous_lm() now keeps n as the unweighted analytic sample size in wide and rendered outputs, and can optionally add a separate Weighted n column reporting the sum of case weights.

spicy 0.7.0

CRAN release: 2026-03-30

New features

  • table_continuous() is a new helper for continuous summary tables. It computes descriptive statistics (mean, SD, min, max, confidence interval of the mean, and n) for numeric variables, with tidyselect column selection, optional grouping via by, and multiple output formats (ASCII, tinytable, gt, flextable, Excel, clipboard, and Word).

  • table_continuous() gains effect_size and effect_size_ci arguments. When by is used, effect_size = TRUE adds an “ES” column with the appropriate measure (Hedges’ g, eta-squared, rank-biserial r_rb, or epsilon-squared) chosen automatically based on the test method and number of groups, and effect_size_ci = TRUE appends the confidence interval in brackets.

  • table_continuous() gains a test argument ("welch", "student", or "nonparametric") to choose the group-comparison method, along with independent p_value and statistic display toggles so users can request either or both outputs when by is used.

  • ASCII console tables now split oversized outputs into stacked horizontal panels, repeating the left-most identifier columns so wide freq(), cross_tab(), table_categorical(), and table_continuous() prints stay readable in narrow consoles.

Breaking changes

  • table_categorical() replaces table_apa() as the public helper for categorical summary tables. It uses select and by, supports grouped cross-tabulation or one-way frequency-style tables when by = NULL, and consolidates output formats under a single output argument. Migrate existing table_apa() calls to table_categorical(), use output = "default" for ASCII tables and output = "data.frame" for plain data frames, and replace former output = "wide" / style = "report" paths with the formatted output engines.

  • Excel export now uses openxlsx2 instead of openxlsx for a lighter dependency footprint (no Rcpp compilation required).

Minor improvements

  • Package citation metadata now uses the current package title and CRAN DOI, so citation("spicy") matches DESCRIPTION and points to the package DOI.

  • table_categorical() and table_continuous() now print shorter ASCII titles without appending the input data frame name, and no longer require officer for output = "flextable" alone; officer is now required only for Word export paths that actually write .docx files.

  • table_continuous() now accepts tidyselect syntax in exclude in addition to character vectors, and no longer warns that test is ignored when it is still needed to compute effect sizes.

spicy 0.6.0

CRAN release: 2026-03-23

New features

  • New family of association measure functions for contingency tables: assoc_measures(), contingency_coef(), gamma_gk(), goodman_kruskal_tau(), kendall_tau_b(), kendall_tau_c(), lambda_gk(), phi(), somers_d(), uncertainty_coef(), and yule_q(). Each returns a numeric scalar by default; pass detail = TRUE for a named vector with estimate, confidence interval, and p-value.

  • cross_tab() gains assoc_measure and assoc_ci arguments. When both variables are ordered factors, it automatically selects Kendall’s Tau-b instead of Cramer’s V. The note format changes from Chi-2: 18.0 (df = 4) to Chi-2(4) = 18.0. Numeric attributes (chi2, df, p_value, assoc_measure, assoc_value, assoc_result) are now attached to the output data frame.

  • table_apa() now dynamically labels the association measure column based on the measure used, instead of always showing “Cramer’s V”. New assoc_measure and assoc_ci arguments are passed through to cross_tab().

  • table_apa() gains output = "gt" to produce a gt_tbl object with APA-style formatting, column spanners, and alignment.

  • table_apa() now correctly centers spanner labels over their column pairs in tinytable and flextable output.

  • All association measure functions and assoc_measures() gain a digits argument (default 3) that controls the number of decimal places when printed. The p-value always uses 3 decimal places or < 0.001.

  • detail = TRUE results now print with formatted output (aligned columns, fixed decimal places) via a new print.spicy_assoc_detail() method. assoc_measures() output uses a new print.spicy_assoc_table() method with the same formatting.

  • New bundled dataset sochealth: a simulated social-health survey (n = 1200, 24 variables) with variable labels, ordered factors, survey weights, and missing values. Includes four Likert-scaled life satisfaction items (life_sat_health, life_sat_work, life_sat_relationships, life_sat_standard) for demonstrating mean_n(), sum_n(), and count_n().

Bug fixes

  • count_n() now correctly counts NA values when count = NA and strict = TRUE are both used. List columns are now reported in verbose mode instead of causing silent errors.

  • cross_tab() rescale logic now operates on complete cases only, so the weighted total N matches the unweighted N when missing values are present (consistent with Stata behavior).

  • freq() now uses true NA consistently (instead of the "<NA>" string) in both weighted and unweighted paths. cum_valid_prop is now correctly NA for missing rows. Invalid digits and sort values are rejected with clear error messages.

  • mean_n() and sum_n() now validate min_valid and digits arguments, rejecting non-numeric, negative, or multi-element values.

  • mean_n(), sum_n(), and count_n() no longer trigger a tidyselect deprecation warning when select receives a character vector. Character vectors are now automatically wrapped with all_of().

  • table_apa() now preserves the original factor level order in row variables instead of sorting alphabetically. When drop_na = FALSE, the (Missing) category is placed at the bottom of each variable’s levels. percent_digits, p_digits, and v_digits are now validated.

  • table_apa() p-values no longer wrap across lines in tinytable HTML output.

Breaking changes

  • cramer_v() now accepts a detail argument. By default it returns a numeric scalar (as before). Pass detail = TRUE to get a 4-element named vector (estimate, ci_lower, ci_upper, p_value), or detail = TRUE, conf_level = NULL for a 2-element vector (estimate, p_value) without CI.

spicy 0.5.0

CRAN release: 2026-03-14

New features

  • New table_apa() helper to build APA-ready cross-tab reports with multiple output formats (wide, long, tinytable, flextable, excel, clipboard, word).
  • table_apa() exposes key cross_tab() controls for weighting and inference (weights, rescale, correct, simulate_p, simulate_B) and now handles missing values explicitly when drop_na = FALSE.

Bug fixes

  • count_n() no longer crashes when special = "NaN" is used with non-numeric columns. Passing count = NA now errors with a message directing to special = "NA".
  • cross_tab() fixes a spurious rescale warning for explicit all-ones weights and aligns the Cramer’s V formula with cramer_v().
  • table_apa() no longer leaks global options on error. The simulate_p default is aligned to FALSE.
  • varlist() title generation no longer crashes on unrecognizable expressions.

Minor improvements

  • copy_clipboard() parameter message renamed to show_message.
  • freq() now dispatches printing correctly via S3.
  • Removed unused collapse and stringi from Imports.

spicy 0.4.2

CRAN release: 2026-03-06

  • cross_tab() hardening: improved vector-mode detection (including labelled vectors), stricter weight validation, safer rescaling, and clearer early errors (e.g., explicit y = NULL).
  • cross_tab() statistics are now computed on non-empty margins in grouped tables, avoiding spurious NA results; internal core path refactored to remove dplyr/tibble from computation while preserving user-facing behavior.
  • freq() now errors clearly when x is missing for data.frame input and validates rescaling when weight sums are zero/non-finite.
  • count_n(), mean_n(), and sum_n() regex mode is hardened (regex = TRUE now validates/defaults select safely).
  • mean_n() and sum_n() now return NA (with warning) when no numeric columns are selected.
  • label_from_names() now validates input type (data.frame/tibble required).
  • cramer_v() now returns NA with warning for degenerate tables.
  • Dependency optimization: DT and clipr moved to Suggests; optional runtime checks added in code_book() and copy_clipboard().
  • Tests expanded with regression coverage for all the above edge cases.

spicy 0.4.1

CRAN release: 2025-12-21

  • Fixed CRAN incoming check notes by removing non-standard top-level files.

spicy 0.4.0

  • Print methods have been fully redesigned to produce clean, aligned ASCII tables inspired by Stata’s layout. The new implementation improves formatting, adds optional color support, and provides more consistent handling of totals and column spacing.

  • Output from freq() and cross_tab() now benefits from the enhanced print.spicy() formatting, offering clearer, more readable summary tables.

  • Documentation and internal tests were updated for clarity and consistency.

  • cross_tab() gains an explicit correct argument to control the use of Yates’ continuity correction for Chi-squared tests in 2x2 tables. The default behavior remains unchanged.

  • The documentation of cross_tab() was refined and harmonized, with a clearer high-level description, improved parameter wording, and expanded examples.

  • Minor cosmetic improvements were made to varlist() output: the title prefix now uses vl: instead of VARLIST, and the column name Ndist_val was renamed to N_distinct for improved readability and consistency.

  • Minor cosmetic improvement: ASCII table output no longer includes a closing bottom rule by default.

spicy 0.3.0

CRAN release: 2025-10-22

  • New function code_book(), which generates a comprehensive variable codebook that can be viewed interactively and exported to multiple formats (copy, print, CSV, Excel, PDF).

spicy 0.2.1

CRAN release: 2025-10-04

  • label_from_names() now correctly handles edge cases when the separator appears in the label or is missing.

spicy 0.2.0

CRAN release: 2025-09-25

  • New function label_from_names() to derive and assign variable labels from headers of the form "name<sep>label" (e.g. "name. label"). Especially useful for LimeSurvey CSV exports (Export results -> CSV -> Headings: Question code & question text), where the default separator is ". ".

spicy 0.1.0

CRAN release: 2025-05-05

Initial release

  • Introduces a collection of tools for variable inspection, descriptive summaries, and data exploration.
  • Provides functions to:
    • Extract variable metadata and display compact summaries (varlist()).
    • Compute frequency tables (freq()), cross-tabulations (cross_tab()), and Cramer’s V for categorical associations (cramer_v()).
    • Generate descriptive statistics such as means (mean_n()), sums (sum_n()), and counts (count_n()) with automatic handling of missing data.
    • Copy data (copy_clipboard()) directly to the clipboard for quick export.