spicy (development version)
Breaking changes
Declared missing values are now honored package-wide. Codes that survey files declare as missing (
na_values/na_rangemetadata on haven-imported columns, andhaven::tagged_na()values) are treated as missing by default infreq(),cross_tab(),table_categorical(),table_continuous(),table_continuous_lm(),mean_n(),sum_n(),count_n(),varlist(),vl(), andcode_book(): they are excluded from statistics exactly likeNA(valid percents, means, chi-squared tests and association measures,min_validgates, 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(), andcode_book()count columns are now internally coherent for labelled data:N_distinctuses the same missing definition asN_valid/NAs(declared missing values andNAelements of list andPOSIXltcolumns no longer count as distinct valid values),na_rangecodes observed in the data are listed inValueslikena_valuescodes instead of vanishing, and value labels attached to tagged NAs (e.g.Refused = tagged_na("a")) now appear inValues.cross_tab()now tabulates observations at an explicitNAfactor level (addNA(),factor(exclude = NULL),forcats::fct_na_value_to_level()) as a regularNAcategory 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, matchingfreq()and basetable()on the same input. Numbers change for tables built from such factors.freq()on a factor with an explicitNAlevel now excludes those observations from the valid-percent denominator (and then_validattribute), matching the Missing classification its own printed table gives them and the SPSS convention; the Valid Percent column previously summed to less than the100.0printed in its Total row.build_ascii_table()is no longer exported. It has always been documented as internal plumbing; usespicy_print_table()for console rendering from code.The
column_total_lineargument ofbuild_ascii_table()andspicy_print_table()is removed. It was accepted and documented but never had any effect.Association measures:
detail = TRUEvectors now always include the standard error as anseelement, and the internal-only.include_seargument is gone from every public signature.gamma_gk(),kendall_tau_b(), andkendall_tau_c()report anNAp-value when the asymptotic standard error is zero (e.g. a perfect association), instead of a spurious0orNaN– the same zero-SE gate as the other association measures.uncertainty_coef()returnsNAwith aspicy_undefined_statwarning 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()returnsNAwith aspicy_undefined_statwarning 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()validateconf_level: anything other than a single number strictly between 0 and 1 (orNULLto omit the confidence interval) raises a classed error (spicy_invalid_input), instead of silently producingInforNaNconfidence bounds.conf_level = 95gets a hint suggestingconf_level = 0.95.freq()andcross_tab()replace the logicalstyledargument withoutput, the same rendering argument thetable_*()family uses. Migration:styled = TRUEis nowoutput = "default"(the default) andstyled = FALSEis nowoutput = "data.frame". Passingstyledraises a classed error (spicy_defunct) that spells out the replacement. The rendered engines of thetable_*()family ("tinytable","gt","flextable", …) are not accepted by the two tabulators.cross_tab(output = "data.frame")(previouslystyled = FALSE) now returns a genuinely plaindata.frame(and a list of plain data frames withby): 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 torescale = FALSE(raw weighted counts), matchingcross_tab(), and readsoptions(spicy.rescale)the same waycross_tab()does. Callfreq(..., rescale = TRUE)to restore the previous behavior.freq()no longer prints as a side effect: it returns itsspicy_freq_tablevisibly and regular auto-printing displays it, so a barefreq(...)call still shows the table whilef <- freq(...)is now silent (printfto 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, andcol_names(wererow.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 todrop_na = FALSE: missing values display as a"(Missing)"level instead of being silently removed. Withdrop_na = TRUE, a table note now reports what was removed.table_categorical()rejectsp_digitsbelow 1 with a classed error, as its documentation always promised; such values were silently rendered with 3 decimals. Matchestable_continuous(),table_continuous_lm(), andcross_tab().table_categorical()uses the samelabelscontract astable_continuous()andtable_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.docxas a side effect whenword_pathis supplied:word_pathis consulted only byoutput = "word", as everywhere else in the table family. The old combination now warns (classspicy_ignored_arg); save the returned object withflextable::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 alltable_*()functions; use"decimal"(default),"center", or"right".table_regression(show_fit_stats = character(0))errors; useFALSEto suppress the block.Multi-model
show_columns = "all_b"/"all_ame"auto-compact (CIs dropped); request atomic tokens to keep them.tidy()labels AME rowsestimate_type = "ame"(was"AME").The
show_fit_statsinformation 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 (classspicy_no_selection) and returnsNAfor all rows when the selection resolves to zero usable columns, matchingmean_n()andsum_n()(it silently returned all zeros). A valid selection where the value is simply absent still counts0.mean_n()andsum_n()withmin_valid = 0now returnNAfor rows with no valid values;mean_n()returnedNaNandsum_n()a silent0(the rawrowMeans()/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 (header95% 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 aspicy_bayes_diagnosticswarning, 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). Underexponentiate = TRUEall 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-formulabrm()models (design matrix recovered through insight; the scale factors are engine-invariant). Multilevel fits, non-GLMstanregsubclasses (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 reportnandN eventsas fit statistics and the concordance as a footer note. - Categorical and ordinal (
nnet::multinom,mlogit::mlogit,MASS::polr,ordinal::clm): a singlemultinomrenders outcome categories as column groups (outcome_labelsrelabels them);mlogituses a two-segment alternative-specific layout; ordinal thresholds render as a labelled block (show_thresholds = FALSEto opt out), and partial-proportional-oddsclmfits 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); plusMASS::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. Supportslm(the default linear screen),glm(selected automatically when afamilyis supplied), andcoxph(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 tovcov = "HC3"and tomethod = "glm"for the logistic screen (see “Why the default screen is linear” in?table_regression_uv). A per-predictorNcolumn is shown by default and a note discloses when Ns differ across fits;complete_cases = TRUEforces the common sample. Intercepts are hidden by default;show_intercept = TRUEdisplays each univariable fit’s own intercept at the top of its block along with the multivariable one.p_adjusttreats the whole screen as one family.exponentiate,vcov/cluster(the footer names the cluster column),labels, the output engines, andtidy()work as intable_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 fromgeepack::geeglm()(population-averaged models). The fit’s own sandwich standard errors – clustered on itsid =, or the jackknife variants chosen via geeglm’sstd.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-inshow_fit_statstokens. AME columns and the usualexponentiategates (OR / IRR / RR by link) work as forglm. Requests GEE cannot honour are refused with actionable messages: spicy’svcov/cluster(inference is already robust by construction; clustering belongs toid =),standardized,nested = TRUE, and likelihood-based fit statistics (compare working models with QIC). - New
show_columnstoken families"rmst"and"risk_diff"forcoxphfits: covariate-adjusted differences in restricted mean survival time over[0, tau]and in cumulative incidence atat_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_columnstoken"n_events": outcome event counts displayed asevents/Nnext 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, andglmmTMBfits alike – and right-censoredcoxphfits; opt-in everywhere. -
re_ci = "profile": profile-likelihood CIs for the variance components oflmer/glmerfits (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 theRandom effectsrows. -
ci_method = "profile"gives profile-likelihood CIs forglm,polr, andclm; newci_method = "boot_percentile"(withvcov = "bootstrap") reports percentile bootstrap CIs from the same replicates as the bootstrap SEs. The footer names the method. - Heteroskedasticity- and cluster-robust
vcovfor the supported classes. Resampling footers report the valid replicate count, and a bootstrap / jackknife whose replicates nearly all fail raisesspicy_resampling_failedinstead of silently reporting classical SEs. - Cluster-robust
vcov("CR0"-"CR3") fornnet::multinom, including its AME columns (needs sandwich >= 3.1-2, which added theestfun()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, andrmsfits, and per outcome category forpolr/clm/multinom. AME SEs, CIs, and p-values honour a robustvcov. -
fixesttables disclose their absorbed fixed effects by default: aFixed 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") forfeolsand McFadden’s pseudo-R² forfeglm/fepoisin the default statistics. The"n_groups"token now renders oneN (<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/hurdleand theziformula/dispformulacomponents ofglmmTMBrender as labelled row blocks with full inference (show_components = FALSEto 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+AICinstead of a blank block. TheN (groups)row upgrades to plain counts (e.g.N (Subject)) when models share a single grouping factor. -
selectis optional intable_categorical(): when omitted, every eligible categorical column is tabulated – factor, character, logical, and labelled (haven) columns, excludingby– matching the select-less defaults oftable_continuous()andtable_continuous_lm(). An explicitselectis 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()readsoptions(spicy.rescale)likecross_tab(); an explicitly suppliedrescaleargument still wins. -
table_continuous()gainsdrop_na, mirroringtable_categorical(): the defaultTRUEkeeps the historical behavior (rows with a missingbyvalue are removed), whileFALSEdisplays 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()resolvesselectandexcludethrough the same tidyselect path asmean_n()andsum_n(): unknown names in a characterselectraise a classed error (instead of a base subscript error), andexcludeaccepts positions as well as names. -
count_n()validates everyspecialentry 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 emptyspecialselects 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.; withby, alsoRows 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 likefreq()already did; they previously raised an error.cross_tab()warns (classspicy_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()validatesdigitswith the same classed error asfreq(); invalid values (negative, fractional, non-numeric) used to degrade the display silently or fail with obscure base-R errors.freq()’s error for an invalidsortvalue 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_colhas no effect” notice as a classed warning (spicy_ignored_arg), sosuppressMessages()/suppressWarnings()and condition handlers work;quiet = TRUEstill 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 classedspicy_invalid_inputerror naming the argument and its valid values, instead of the locale-dependent basematch.arg()error that escaped thetryCatch(spicy_error = ...)catch-all.print()oncross_tab()tables validates itsdigitsargument (single non-negative integer) with a classed error.spicy_print_table()raises classed errors (spicy_invalid_data/spicy_invalid_input) whenxis not a data frame ordisplay_labelsdoes not have one label per column, replacing barestopifnot()failures.table_categorical(),table_continuous(), andtable_continuous_lm()announce their clipboard export with a classed message (spicy_info), so it can be muffled withwithCallingHandlers(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 barewarning().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 = TRUEwith a visible SE column, the footer states the SE scale (delta method) and that the CI bounds are asymmetric.show_fit_stats = FALSEsuppresses 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 “testis ignored” warning states the full trigger condition (p_value,statistic,effect_size, andeffect_size_ciall turned off) instead of naming only the first two toggles.varlist(),vl(), andcode_book()annotatedifftimevalues with their units inValues(e.g.1.5, 2.5 (hours)); the bare numbers were ambiguous between hours and days.varlist()andvl()return tibble columns without stray names attributes (the variable names leaked onto 5 of the 7 columns), so element-wiseidentical()and snapshot comparisons behave the same for every column.
Bug fixes
The flextable outputs of
table_categorical(),table_continuous()andtable_continuous_lm()carry the samespicy_flextablewrapper astable_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 / PSPPCROSSTABSto 7 decimals and is pinned to PSPP 2.0 on six tables. It deliberately diverges fromDescTools::KendallTauB(), which computes the same wrong SE. Also affects the tau-b rows ofassoc_measures()and the default ordered-by-ordered association line ofcross_tab().somers_d(direction = "symmetric")returns the correct 0 instead of a silentNAwhen concordant and discordant pairs are exactly equal (e.g. an independence-pattern table), matching SPSS / PSPP; the association line requested fromcross_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_statfor 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(), andcontingency_coef()returnNAwith aspicy_undefined_statwarning on tables with a zero row or column margin (the chi-squared statistic is NaN there), instead of a silentNaN(detail = FALSE) or a crash (detail = TRUE); theirassoc_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 inassoc_measures().cross_tab(),table_categorical(), andassoc_measures()label thetau_cassociation measure"Stuart's Tau-c"(the SPSS / PSPP name) everywhere: thecross_tab()note, itsassoc_measureattribute, and theassoc_measures()summary row used to say"Kendall's Tau-c"while thetable_categorical()column header said"Stuart's Tau-c".cross_tab()andtable_categorical()no longer swallow the classed error an association measure raises when it does not apply to the tabulated table (e.g.phion a labelled column whose declared missing codes are kept as valid values byuser_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 sameuser_naregime as the table, so it picks a measure that matches the table actually tested.table_categorical(),table_continuous(), andtable_continuous_lm()resolvebydata-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 abyfactor level that is declared but never observed as an explicit zero column (0n,0.0%) in the wide and long outputs; the wide output used to carry blankNAcells 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 defaultdrop_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, anddrop_na = TRUEwas always correct.table_categorical()displays the value labels of labelled (haven) columns as"[code] label"levels in every path, asfreq()does; the raw codes (1,2,3) used to be displayed underdrop_na = FALSEand in grouped tables, so the rendering depended ondrop_naand onby.table_categorical()keeps both the group and the margin when abylevel is literally named"Total": the true margin is always present (auto-renamed to"Total_1"with a singlespicy_renamed_columnwarning, mirroringcross_tab()), and the user’s"Total"group keeps its own column. The margin used to disappear silently, with the user’s group displayed under theTotalheader.tidy()andglance()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 = 2therefore renders true decimals (33.33, not the fabricated33.30), andtidy()keeps exact fractional weighted counts instead of truncating them.table_categorical()displays counts as integers everywhere: weighted cells and theirTotalmargin 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 documentedChi2anddfcolumns in grouped tables, consistent with the long output andglance().print()onfreq()tables invisibly returns the table object itself (as documented), not the internally rebuilt display frame.freq()keeps the variable label footer when observations withNAweights are dropped; base subsetting used to strip thelabelattribute 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]. Withlabelled_levels = "labels"the alphabetical label sort is unchanged.table_categorical()warns (classspicy_no_selection) and lists the available level strings whenlevels_keepmatches 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(), andcode_book()renderPOSIXltcolumns as datetime values undervalues = TRUE, as the compact summary already did, instead of a list-column summary (List(3): list).varlist(),vl(), andcode_book()show an explicitNAfactor level (e.g. fromaddNA()) as<NA>inValuesinstead of silently dropping it, soValuesandN_distinctagree on the declared levels.label_from_names()no longer blames the split for duplicate column names that already existed in the input (check.names = FALSEdata): pre-existing duplicates pass through untouched, and only collisions created by the renaming error.table_regression_uv()no longer silently ignoresfamilywithmethod = "lm": a non-gaussian family is refused with the same actionable error as thecoxphrefusal, and a suppliedgaussian()is ignored with a classed warning.gtandflextableoutputs now render in Quarto / R Markdown Word, PowerPoint, and PDF documents (they silently disappeared from non-HTML targets). A newas_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 acceptscluster = ~region.table_continuous_lm()reports correct estimated means, SEs, CIs, and the displayed binary difference whenbyis an ordered factor. The model was fitted with polynomial contrasts while the prediction grid assumed treatment coding, so everyMcolumn 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 categoricalbyis now fitted with explicit treatment contrasts, matching the documented convention whatever the session sets: an orderedbyno longer picks up polynomial coding, and a session-wideoptions(contrasts = c("contr.sum", "contr.poly"))(common in ANOVA workflows) no longer alters the results or aborts a binaryby.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-factorby(character, numeric, haven labelled) in order of first appearance, matchingtable_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 becomeNAwith 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 labelledbywith value labels as a categorical predictor – groups over the raw codes, the same dispatch astable_continuous()andtable_categorical()– instead of silently fitting a continuous slope on the codes. A labelledbywithout 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 ondroplevels()-ed data instead of crashing.table_continuous_lm()excludes rows withNAinweightsfrom 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 missingbyvalue 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 whenbyhas a single observed level (nothing to compare; an all-NA row used to print silently), and reportsNASEs, CIs, tests, and p-values with a classed warning when the fit is saturated (one observation per group);NaNcells 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 itsNArow keeps the regular group columns instead of growing a spuriousM (NA)column; the other outcomes are unaffected.table_continuous_lm(adjustment = "proportional")averages the G-computation predictions with the case weights whenweightsis supplied (the Statamarginsconvention, equivalent tomarginaleffects::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, matchingeffectsize::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 behindpartial_f2/partial_eta2/partial_omega2(and their CIs), and thepartial_chi2likelihood-ratio chi-square forglm, test each term by comparing the two nested models that both exclude every higher-order interaction containing it (thecar::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 thatpartial_chi2reports 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 againstflexsurv::standsurv()(exact) and the closed-form exponential RMST (machine precision). Stratifiedsurvregfits (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 againstadjustedCurves::adjusted_rmst()(exact).MASS::glm.nbfits gain opt-in dispersion fit-stat rows:show_fit_statstokens"theta"(the NB2 dispersion) and"alpha"(its reciprocal, the Stata convention). Refused with a clear error for other families.betaregfits gain the matching opt-in precision row: theshow_fit_statstoken"phi", back-transformed from the precision link (y ~ x | 1reports the same phi asy ~ x). Refused for other families and when the precision has covariates (y ~ x | z), so phi is not a single number.nnet::multinomfits now report McFadden’s and Nagelkerke’s pseudo-R² (as the other categorical families do). The tokens were silently dropped fromshow_fit_statsbefore, 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 header95% CrI. Mixed frequentist + Bayesian tables keep the shared95% CIlabel 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/ multilevelbrmfits 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 readsRandom effects (MCMC)and no likelihood-ratio line is printed.p_adjustand 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 reportnonly. 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 aScale effectsblock. They were silently absent from the table: an estimated component of the model went unreported. The rows stay on the log scale underexponentiate = TRUE(their exponential is a ratio of latent standard deviations, not an odds ratio) and the footer says so.broom::tidy()gains anoutcome_levelcolumn naming the response category of per-category rows (ordinal and multinomial average marginal effects). Those rows were previously indistinguishable – fourageAME 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_levelnow 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 = TRUEnow works formultinom(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 ownvcovestimator 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.clusterworks 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 = TRUEcompares nestedrqfits throughanova.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 (
zort); it was hardcoded tot.Factor coefficient and AME rows follow
levels()order (was alphabetical); ordered factors with AME columns show a reference row;ame_ci/ame_p/ame_sepopulate 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 onfactor()/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;labelsaccepts coefficient-level keys on mixed fits;polr/clmdetect non-uniform prior weights.flexsurv: probit-scale splines and ancillary-parameter covariates refuseexponentiate = 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-outcomeOutcomerow); the structured schema gainsreference_models_by_rowandoutcome_labels_by_col.table_regression(m1, m2)withoutlist()errors with a helpful message; colliding model labels no longer breakoutput = "gt"; thep_adjustfooter’s family size matches the adjustment performed;ci_method = "profile"with a robustvcovdefers to thevcovand 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/geeglmfit the AME (and its SE / CI / p) is the weighted average of the unit-level slopes, matchingmarginaleffects::avg_slopes(fit, wts = weights(fit)). AME values change for weighted fits (they matched the unweighted average before);svyglmis unaffected – its design weights were already applied natively.nlme::glsfits populate the AME columns (show_columns = c("b", "ame")); the column rendered silently empty even though the model registry advertises AME support forgls.Logical predictors get the grouped factor layout the documentation promises: an
is_smoker:header with indentedFALSE (ref.)/TRUErows (and aligned AME cells), instead of a flatis_smokerTRUErow 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-familyglm– 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 attributesoutcomeandmodel_ids(one entry per model, in table order).output = "data.frame"carries the same pair, andas.data.frame()no longer dropscol_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 plaindata.frame.count_n()raises a classed error (spicy_invalid_input) whencountis zero-length or contains only missing values, instead of silently returning a plausible-looking all-zero count; and rejectingcount = NaNnow points tospecial = "NaN"(the exact counterpart) instead of describing the input ascount = NAand hinting atspecial = "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 underrescale = TRUEthe 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 inx "g2", orWeight: 1for a literal weight vector): when no name can be derived structurally, a neutralx/y/weightsplaceholder is used.freq()warns (classspicy_caveat) whenlabelled_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 ofNAvalues whose Total row asserts100.0; the column (andCum. Valid Percentwithcum = TRUE) is dropped whenever valid percentages were not computed.freq(),cross_tab(),mean_n(),sum_n(),table_categorical(),table_continuous(), andtable_continuous_lm()rejectbit64::integer64input (tabulated variables, weights, selected columns,bygroups, and covariates) with a classed error (spicy_invalid_data) naming the fix: integer64 passesis.numeric()but stores raw 64-bit integer bit patterns that base R numeric code silently misreads as garbage counts near1e-323– a summary table showedM = 0.00for 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 ordata.table::fread(). Convert withas.integer()/as.numeric()(oras.character()for codes wider than 2^53) first;count_n()compares values without numeric aggregation and continues to work.table_regression_uv()acceptsfamilyin the three formsstats::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 vectorserror 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 fittedlmorglmmodels, 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
vcovaccepted for SE-comparison tables. - Standardisation:
refit,posthoc,basic,smart,pseudo(the lastglmonly). - 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 χ² forglm. - 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(anystats::p.adjust()method). - Hierarchical comparison via
nested = TRUE(ΔR² / F-change forlm; LRT forglm). - 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()andbroom::glance()methods supported.
- Robust variance: classical, HC, cluster-robust (CR) with Satterthwaite df, bootstrap, jackknife. Per-model
table_continuous_lm()gains additive covariate adjustment via the newcovariatesargument. Two estimands for the per-group adjusted means:"proportional"(G-computation, default) and"balanced"(equal-weight synthetic grid). Under adjustment,f²andω²become partial effect sizes;dandgraise an explanatory error. The auto-built footer documents the covariates and the estimand. Seevignette("table-continuous-lm").New exported
as_structured()accessor returns a typed view of atable_regression()result for programmatic use: raw numerics, CI split intoLL/ULcolumns, 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 explicitfilename =argument.
Bug fixes
-
table_categorical()no longer over-truncates a p-value in the interval(10^-p_digits, 0.001)whenp_digits >= 4. Example:p = 0.000108now correctly prints as".0001"atp_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 thecount = ...branch (wasnumeric(0), which brokedplyr::mutate()pipelines). -
lambda_gk()andgoodman_kruskal_tau()emitspicy_undefined_statand return a fully-NAresult on rank-1 contingency tables (constant predicted variable), matching the existing pattern ingamma_gk(),kendall_tau_b(),somers_d(), andyule_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 singlespicy_renamed_columnwarning is emitted. -
broom::glance()on aspicy_continuous_lm_tablekeepsdf.residualnumeric, so Satterthwaite degrees of freedom fromvcov = "CR2"/"CR3"are preserved verbatim instead of being truncated throughas.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 (
nrow alongsideR²) keep their right- aligned placement. -
R/source is byte-pure ASCII (tools::showNonASCIIfile()reports zero hits package-wide). -
openxlsx2::wb_add_border()calls now passNULLon 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
clusterand fourvcovchoices ("CR0"–"CR3"), dispatched toclubSandwichwith Satterthwaite df (clubSandwichinSuggests). -
vcov = "bootstrap"(nonparametric or cluster) andvcov = "jackknife"(leave-one-out / leave-one-cluster-out) variance estimators in pure base R, controlled byboot_n. - Three new
effect_sizechoices alongside"f2": Cohen’s"d", Hedges’"g"(two-group only), Hays’"omega2". Neweffect_size_ciadds noncentral t / F CIs rendered inline as0.18 [0.07, 0.30]. -
HC*estimators delegate tosandwich::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 spanscross_tab(),freq()and the threetable_*()helpers, including APA-style p-value notation (<.001/.045, no leading zero). -
table_categorical()’sassoc_measureaccepts a per-variable spec. When measures differ across rows the column collapses to"Effect size"and an APA-styleNote.line documents the per-variable measure;phion a non-2x2 errors. - All three
table_*()functions gainas.data.frame(),tibble::as_tibble(),broom::tidy()andbroom::glance()methods (broominSuggests).
Quality and robustness
-
Classed conditions. Errors and warnings now carry stable classes (
spicy_error/spicy_warningplus 11 leaf classes documented in?spicy), so downstream code can dispatch viatryCatch()/withCallingHandlers()instead of matching message strings.rlang (>= 1.1.0)required. -
Structured cli messages. Multi-line errors and warnings (vcov fallbacks, bootstrap/jackknife failures,
paddingmigration,labelslength mismatch) render as cli bullets. -
Locale-deterministic ordering. Sorts in
varlist(),freq(),cross_tab()andtable_*()usemethod = "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-NADate/POSIXct/charactercolumns and factors with no observed levels. -
Snapshot-locked rendering.
tests/testthat/test-snapshots.Rpins the exact console output of every spicy print method, so any unintended formatting drift surfaces as a PR diff. -
API stability contract.
?spicydocuments which exports are stable, stabilising or internal. pkgdown reference groups exports via four@familytags. -
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 againsteffectsize::cohens_d()/effectsize::hedges_g()(tolerance = 1e-6); point-estimate formulas and asymptotic standard errors followDescTools(Signorell et al.).
Improvements
-
cross_tab()warns whencorrect = TRUEis ignored on a non-2x2 sub-table, whenweightscontainsNA, and notes statistics computed on a sub-table after empty rows / columns are pruned. -
cross_tab()validatesdecimal_mark,p_digitsandsimulate_Bup front;freq()validatesdecimal_markand tightensdigitsto a non-negative integer. - A user category literally named
"N"or"Total"is no longer mis-rendered as the totals row incross_tab(). -
table_continuous_lm(output = "long")returnsn,df1,df2as integer columns;predictor_labelpreserved on the degenerate-model fallback path. -
cramer_v()/phi()doc states the CI uses the Fisher z-transformation (point estimate and p-value identical toDescTools/ SPSS). -
uncertainty_coef()doc states entropy uses0 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 fromci_level(was hardcoded to 95 %). - The categorical-predictor global Wald F degrades to
NAon a singular coefficient covariance submatrix. - The degenerate-table branch of
cramer_v(),yule_q(),gamma_gk(),kendall_tau_b()andsomers_d()respectsdetail: scalarNA_real_by default, fully shapedspicy_assoc_detailwhendetail = TRUE. -
uncertainty_coef()returns a finite estimate (wasNaN) when a marginal is zero. -
somers_d(direction = "symmetric")returns the harmonic mean of the two asymmetric values, matching SPSS / PSPPCROSSTABS. -
print.spicy_assoc_detail()/print.spicy_assoc_table()use APA-strict<.001/.045notation, matching the rest of the package. -
varlist()/code_book()honourfactor_levels = "all"forhaven_labelledcolumns: declared-but-unobserved labels appear in theValuessummary. -
copy_clipboard()rejectsrow.names.as.colvectors of length ≠ 1 and empty strings; accumulates all messages fromclipr::write_clip()instead of overwriting. -
mean_n()/sum_n()reject non-integermin_valid >= 1andmin_valid > ncol; theirdigitsrequires a non-negative integer.
Breaking changes
-
table_continuous_lm()andtable_categorical()default to decimal-point alignment for numeric columns (align = "decimal"). Passalign = "auto"for the previous behaviour. -
build_ascii_table()/spicy_print_table():paddingswitches from a string enum to a non-negative integer. Default2L(was+5L); printed tables are roughly 40 % narrower. Migration:"compact" -> 0L,"normal" -> 2L,"wide" -> 4L. -
table_categorical(assoc_measure = "auto")on a 2x2 table picksphiinstead ofcramer_v. Numeric value unchanged (|phi| = V on 2x2); only the column label changes. -
freq()drops observations withNAweights (with a warning) instead of recoding them to zero. Aligns withcross_tab(). -
table_continuous_lm(output = "long")returnsNAines_type/es_valuewheneffect_size = "none"(was"f2"), and renamessum_wtoweighted_n.
spicy 0.10.0
CRAN release: 2026-04-27
New features
code_book()now accepts tidyselect-style variable selectors through..., matchingvarlist()andvl().code_book()gains afilenameargument for the base name of CSV, Excel, and PDF exports. WhenNULL(the default), the filename is derived fromtitleand 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 afactor_levelsargument that mirrorsvarlist()andcode_book(). Withfactor_levels = "all", declared-but-unobserved factor and labelled levels appear in the output withn = 0, matching SPSSFREQUENCIES; the default"observed"preserves the previous Statatab-style behavior.
Improvements
varlist()now displays missing values as<NA>and<NaN>in theValuessummary wheninclude_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 literalget("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 surfacesvarlist()selection errors directly.freq()now resolves theweightsargument via tidy-eval, so column references nested in compound expressions (e.g.weights = if (use_w) col else NULL) work as expected. Qualified expressions likeweights = df2$wcontinue to take precedence over column lookup.freq()validatesdigits,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 ofweightscontainingNAwithrescale = TRUE(Statapweightsemantics) and the dropping of unused factor / labelled levels (Statatabsemantics, withcode_book(factor_levels = "all")as the schema-style alternative).
Bug fixes
varlist()now displays labelled values in the same prefixed-label order for compact andvalues = TRUEsummaries; previously the compact summary used data order.varlist(values = TRUE)now deduplicates element types when summarizing list-columns. Previouslylist(1L, 2L, "a")produced"List(3): character, integer, integer"; now produces"List(3): character, integer".include_na = TRUEnow correctly appends<NA>markers for list-columns in bothvarlist()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 explicitNULLas “no weighting” instead of emitting a misleading"variable 'NULL' not found"error. Parameterized patterns likeweights = if (use_w) wts else NULLare now supported.print()forspicy_freq_tableno longer crashes when thevar_labelattribute isNA_character_, numeric, or multi-element; theLabel:line is silently skipped for any value that is not a single non-empty string.freq()no longer surfaces the name of the ignoreddatavector in the printed footer when bothdataandxare 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 whenbyis supplied. With a grouping variable, thepcolumn fromtestis shown automatically (previous default hid it). This aligns the two table helpers:table_continuous()stays descriptive whenbyis absent, and reports the test p-value whenbyis supplied, matchingtable_continuous_lm()’s inferential default. To preserve the previous behavior, passp_value = FALSEexplicitly.statisticandeffect_sizeremainFALSEby default and must still be enabled consciously.varlist()now displays observed factor levels by default inValues, matching its role as a quick inspection of the current data. Usefactor_levels = "all"to display unused factor levels as well, which was the previous default behavior and remains the default incode_book().
Minor improvements
code_book()gains afactor_levelsargument. It defaults to"all"so exported codebooks continue to document all declared factor levels, including unused levels; use"observed"to mirrorvarlist()output.freq()now prints theFreq.column as integers regardless ofdigits, which continues to control percentage precision. This matches the convention of SPSS, Stata, and SASPROC FREQfor weighted counts and keeps the two numeric concepts (discrete counts vs. continuous percentages) visually distinct.freq(..., styled = FALSE)now returns a genuinely plaindata.framewith nospicy_freq_tablerendering metadata clinging to it, sostr(),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@returnand remain available on the invisibly returnedspicy_freq_tableobject whenstyled = TRUE(the default).table_continuous_lm()documentation now clarifies whyp_value = TRUEandr2 = "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 asweights = other$worweights = other[["w"]]even when the referenced column name also exists indata. Previously the bare-name fallback could silently pull the weight vector from the wrong data frame when column names collided.freq()withsortand missing values now keeps theNArow at the end of the tabulation so the printedCum. PercentandCum. Valid Percentcolumns stay monotonic and match the Valid → Missing → Total display layout. Sorting previously could push theNArow between valid rows and make cumulative percentages appear to jump.varlist()now preserves literal"NA"and empty-string values in theValuessummary instead of removing them as if they were missing values.varlist()now distinguishes actualNAvalues fromNaNin theValuessummary wheninclude_na = TRUE.varlist(values = TRUE)now preserves factor level order in theValuessummary, matching the default compact factor display.varlist()now validatesvalues,tbl, andinclude_naup front and gives a clear error when one of them is notTRUEorFALSE.
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 totable_continuous()for reporting fitted mean comparisons or slopes in anlmframework, 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 bundledsochealthdata throughout and adding a dedicated article for model-based continuous summary tables.table_continuous()andtable_continuous_lm()now support dedicated display precision for effect-size columns, andtable_continuous_lm()also supports separate precision forR²columns, so model fit and effect sizes can be formatted independently from descriptive values and test statistics.table_continuous_lm()now keepsnas the unweighted analytic sample size in wide and rendered outputs, and can optionally add a separateWeighted ncolumn 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 viaby, and multiple output formats (ASCII, tinytable, gt, flextable, Excel, clipboard, and Word).table_continuous()gainseffect_sizeandeffect_size_ciarguments. Whenbyis used,effect_size = TRUEadds an “ES” column with the appropriate measure (Hedges’ g, eta-squared, rank-biserialr_rb, or epsilon-squared) chosen automatically based on the test method and number of groups, andeffect_size_ci = TRUEappends the confidence interval in brackets.table_continuous()gains atestargument ("welch","student", or"nonparametric") to choose the group-comparison method, along with independentp_valueandstatisticdisplay toggles so users can request either or both outputs whenbyis 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(), andtable_continuous()prints stay readable in narrow consoles.
Breaking changes
table_categorical()replacestable_apa()as the public helper for categorical summary tables. It usesselectandby, supports grouped cross-tabulation or one-way frequency-style tables whenby = NULL, and consolidates output formats under a singleoutputargument. Migrate existingtable_apa()calls totable_categorical(), useoutput = "default"for ASCII tables andoutput = "data.frame"for plain data frames, and replace formeroutput = "wide"/style = "report"paths with the formatted output engines.Excel export now uses
openxlsx2instead ofopenxlsxfor 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")matchesDESCRIPTIONand points to the package DOI.table_categorical()andtable_continuous()now print shorter ASCII titles without appending the input data frame name, and no longer requireofficerforoutput = "flextable"alone;officeris now required only for Word export paths that actually write.docxfiles.table_continuous()now accepts tidyselect syntax inexcludein addition to character vectors, and no longer warns thattestis 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(), andyule_q(). Each returns a numeric scalar by default; passdetail = TRUEfor a named vector with estimate, confidence interval, and p-value.cross_tab()gainsassoc_measureandassoc_ciarguments. When both variables are ordered factors, it automatically selects Kendall’s Tau-b instead of Cramer’s V. The note format changes fromChi-2: 18.0 (df = 4)toChi-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”. Newassoc_measureandassoc_ciarguments are passed through tocross_tab().table_apa()gainsoutput = "gt"to produce agt_tblobject with APA-style formatting, column spanners, and alignment.table_apa()now correctly centers spanner labels over their column pairs intinytableandflextableoutput.All association measure functions and
assoc_measures()gain adigitsargument (default 3) that controls the number of decimal places when printed. The p-value always uses 3 decimal places or< 0.001.detail = TRUEresults now print with formatted output (aligned columns, fixed decimal places) via a newprint.spicy_assoc_detail()method.assoc_measures()output uses a newprint.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 demonstratingmean_n(),sum_n(), andcount_n().
Bug fixes
count_n()now correctly countsNAvalues whencount = NAandstrict = TRUEare 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 trueNAconsistently (instead of the"<NA>"string) in both weighted and unweighted paths.cum_valid_propis now correctlyNAfor missing rows. Invaliddigitsandsortvalues are rejected with clear error messages.mean_n()andsum_n()now validatemin_validanddigitsarguments, rejecting non-numeric, negative, or multi-element values.mean_n(),sum_n(), andcount_n()no longer trigger a tidyselect deprecation warning whenselectreceives a character vector. Character vectors are now automatically wrapped withall_of().table_apa()now preserves the original factor level order in row variables instead of sorting alphabetically. Whendrop_na = FALSE, the(Missing)category is placed at the bottom of each variable’s levels.percent_digits,p_digits, andv_digitsare now validated.table_apa()p-values no longer wrap across lines intinytableHTML output.
Breaking changes
-
cramer_v()now accepts adetailargument. By default it returns a numeric scalar (as before). Passdetail = TRUEto get a 4-element named vector (estimate,ci_lower,ci_upper,p_value), ordetail = TRUE, conf_level = NULLfor 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 keycross_tab()controls for weighting and inference (weights,rescale,correct,simulate_p,simulate_B) and now handles missing values explicitly whendrop_na = FALSE.
Bug fixes
-
count_n()no longer crashes whenspecial = "NaN"is used with non-numeric columns. Passingcount = NAnow errors with a message directing tospecial = "NA". -
cross_tab()fixes a spurious rescale warning for explicit all-ones weights and aligns the Cramer’s V formula withcramer_v(). -
table_apa()no longer leaks global options on error. Thesimulate_pdefault is aligned toFALSE. -
varlist()title generation no longer crashes on unrecognizable expressions.
Minor improvements
-
copy_clipboard()parametermessagerenamed toshow_message. -
freq()now dispatches printing correctly via S3. - Removed unused
collapseandstringifromImports.
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., explicity = NULL). -
cross_tab()statistics are now computed on non-empty margins in grouped tables, avoiding spuriousNAresults; internal core path refactored to removedplyr/tibblefrom computation while preserving user-facing behavior. -
freq()now errors clearly whenxis missing for data.frame input and validates rescaling when weight sums are zero/non-finite. -
count_n(),mean_n(), andsum_n()regex mode is hardened (regex = TRUEnow validates/defaultsselectsafely). -
mean_n()andsum_n()now returnNA(with warning) when no numeric columns are selected. -
label_from_names()now validates input type (data.frame/tibble required). -
cramer_v()now returnsNAwith warning for degenerate tables. - Dependency optimization:
DTandcliprmoved toSuggests; optional runtime checks added incode_book()andcopy_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()andcross_tab()now benefits from the enhancedprint.spicy()formatting, offering clearer, more readable summary tables.Documentation and internal tests were updated for clarity and consistency.
cross_tab()gains an explicitcorrectargument 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 usesvl:instead ofVARLIST, and the column nameNdist_valwas renamed toN_distinctfor 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.
- Extract variable metadata and display compact summaries (