Changelog
Source:NEWS.md
anovakit 0.1.0
First release. The package was previously distributed as loose scripts under the name ANOVAtoolbox; it is now an installable package called anovakit, and the repository has been renamed to match.
Eight analysis-of-variance workflows behind one argument convention and one return class: anova_welch(), anova_kw(), anova_ancova(), anova_rm(), anova_manova(), anova_bin(), anova_count() and anova_glm(). Each validates its input, checks the assumptions that matter for its method, reports effect sizes, computes estimated marginal means and pairwise comparisons, and returns ggplot2 objects rather than drawing them. See vignette("anovakit") for a tour and the README for the design decisions behind it.
The package supersedes a collection of loose scripts distributed from the same repository. If you used those, note the following before comparing results.
Changes made after an adversarial review, before release
A multi-agent adversarial review reproduced 167 problems; all were fixed, each with a regression test against an independent reference, and a second, independent pass re-verified every fix and closed the gaps it found. The changes a user will notice:
-
MANOVA reports genuine Type II / Type III multivariate tests from
car::Manova(); it previously reported sequential (Type I) tests whatevertypesaid, so results depended on the order ofgroups.$modelis now the multivariatemlm. With covariates, covariates are centred and a multivariate homogeneity-of-slopes test is reported in$slopes_test. -
ANCOVA gains
interaction, defaulting toTRUE: with several grouping factors their interaction is fitted, and the slopes test and simple slopes follow the cells.vcov_typenow reaches the F tests (robust Wald F) and the simple slopes. A 0/1 covariate is held at its mean in$emmeans. A design with more coefficients than rows (say, the full factorial of three 22-level factors on 300 rows) is refused at once, naming the remedy. -
Marginal means and comparisons. For an additive multi-factor model (
interaction = FALSE) they are reported per factor, with atermcolumn, instead of for every cell of the grid. Empty cells are left out or flagged. Comparisons above 5000 are skipped with a note.$posthochasp_value(unadjusted),p_adjustedandadjustmentin every function. Non-log, non-logit links compare on the response scale instead of mislabelled link-scale “ratios”. Quasi families use t. Globalemm_options()no longer change results. When the marginal means cannot be computed, every function says that the comparisons and the plot were skipped. A grouping column named like a result column (se,estimate, …) no longer overwrites a statistic. -
Effect sizes.
anova_welch()standardises by the average of the two group variances, with the noncentral-t interval of Welch’s statistic (aseffectsize::cohens_d(pooled_sd = FALSE)reports), so the interval is valid under unequal variances (astandardisercolumn says so). Odds ratios (anova_bin()) and rate ratios (anova_count()) are each a level against its reference level, whatever the contrasts, withfactorandcomparisoncolumns. McFadden’s R squared is reported only for binomial, Poisson and negative binomial families, and is invariant to aggregation. -
Weights. Zero-weight rows are dropped and counted in
$n_removed.anova_bin()’s null model, model-versus-null test and proportion table use the weights. Whole-number weights above 1 on a count or 0/1 response are frequency weights: the dispersion (anova_count(),anova_glm()) and the robust covariance (anova_bin(),anova_count(),anova_glm()) are those of the data expanded to one row per observation. Before, robust standard errors of a frequency table were an order of magnitude too large. - Intervals. Profile intervals use t cut-offs when the dispersion is estimated. Under separation or a zero-count cell an interval is open on the side the estimate diverged in, and its finite end is profiled directly, in every GLM family; the other coefficients keep their profile intervals.
- Robust standard errors fall back to model-based ones, with a note, at the boundary (separation, an all-zero group) and at leverage 1 (a single-observation cell, where HC0 and HC1 give a zero standard error).
- Separation is detected whatever the contrasts, and zero-count cells in count models are flagged.
-
Repeated measures. Sphericity corrections are computed directly when afex cannot supply them, so an uncorrected p-value is never labelled “GG”;
observedreaches generalised eta squared; normality is tested per within-subject cell;$residualsfollow$data_used; column names of any kind work;...accepts onlyfun_aggregate,observed,typeandanova_table = list(p_adjust_method = ). -
Count models. An explicit
model = "negbin"that cannot be fitted is an error rather than a silent switch to quasi-Poisson; the theta and overdispersion notes state the size of the problem they describe. -
Kruskal-Wallis keeps infinite values (ranks can use them), counts ties on the ranks, and
anova_welch()/anova_kw()label a combined grouping “A x B cells”. -
Input. Non-ASCII group labels no longer crash in UTF-8 sessions; an explicit
NAfactor level counts as missing; distinct values that print alike stay distinct groups; colliding cell labels, a column in two roles, and the namesResidualsand(Intercept)are refused. -
Printing.
summary()returns asummary.anovakit_fitobject and shows the method’s own tables; counts are not rounded; p-values below machine precision print as such, and Tukey, Dunnett and Sidak p-values below the precision of their computation print as a bound; the omnibus heading names the test. -
The stored model can be refitted directly:
update(fit$model, . ~ 1),step()andlmtest::lrtest()work onfit$modelfrom any frame. -
Packaging.
ggplot2 (>= 3.4.0)andtestthat (>= 3.1.8)are declared.
The functions were renamed and now share one convention
ancova_analysis() is anova_ancova() and manova_analysis() is anova_manova(). The grouping argument, previously iv, group_var, group_vars or group_vars_vec depending on the file, is groups everywhere; the response, previously dv or response_var, is response. anova_manova() takes column names rather than a formula. Ten helpers that were exported under generic names — validate_vars(), check_normality(), compute_emm(), calculate_effect_sizes() and others — are now internal.
Every function returns an anovakit_fit object with print(), summary() and plot() methods, in place of eight differently shaped lists.
Results that changed
Numbers the old scripts produced for these analyses should not be relied on.
- Count models. Exposure offsets never reached the model, marginal means came back without standard errors, and the negative binomial refit failed silently on every call and fell back to Poisson — reporting p = 1.9e-07 on overdispersed data where the negative binomial gives p = 0.21.
- Type III sums of squares. The contrasts were set after the model was fitted, which does nothing, so the table was simple effects at the reference level under a Type III label. On an unbalanced factorial that reported p = 0.455 where the genuine Type III p-value was 4.2e-09.
- ANCOVA. The group effect was reported at covariate = 0 whenever the interaction was retained. Covariates are now mean-centred by default.
- MANCOVA. Marginal means fell back to raw group means with standard errors 2.4 times too large.
- Repeated measures. Sphericity was never computed, no diagnostic plots were returned, and a factor response was converted through its level indices.
-
Welch. Normality was tested on pooled residuals — which fails for perfectly normal groups whenever their variances differ, the exact case the method exists for — and the column labelled
cohen_dheld Hedges’ g. -
Effect sizes. Omega squared used the non-partial formula on Type II and Type III tables, where it is not a proportion of variance; it is now partial omega squared, matching
effectsize::omega_squared(partial = TRUE). Every effect-size table also carried a spuriousResidualsrow at exactly 0.5.
What the package now guarantees
-
Nothing prints. No analysis function writes to the console and no plot is drawn as a side effect. Warnings and messages raised by
car,glm(),emmeansandafexare captured into$notes. -
conf_levelsets every interval in the tables a function returns. -
type = "III"fits under sum-to-zero contrasts, so the label matches the table. -
Row accounting closes.
nrow($data_used) + $n_removedis always the number of rows supplied, including when rows leave for an incomplete repeated-measures design or afex aggregation. -
$notesrecords every decision made on the caller’s behalf: the model chosen from a dispersion statistic or a slopes test, a centred covariate, a dropped subject, an unidentified odds ratio. -
No escape hatches. Only
anova_rm()takes..., and it documents that the arguments go toafex::aov_ez(). Prior weights are named as a column, so they are subsetted with the data rather than misaligning when a row is dropped.
New capabilities
- Mardia’s tests of multivariate skewness and kurtosis, Box’s M, and a canonical discriminant analysis with its plot, in
anova_manova(). The previous README advertised these; the code contained none of them. - Dunn’s test with the standard tie correction, implemented directly.
-
interactiononanova_bin(),anova_count(),anova_glm()andanova_manova();modelonanova_count()to choose the count family explicitly;force_interactiononanova_ancova()to fix the model in advance rather than select it with a test on the same data; per-group covariate slopes fromemmeans::emtrends(). -
posthoc = FALSEon every function that computes comparisons, and heteroskedasticity-consistent standard errors throughvcov_typethat flow into the marginal means and the comparisons.
Dependencies
Imports car, emmeans, ggplot2 and base R. afex, MASS and sandwich are Suggests behind requireNamespace() guards. The scripts previously needed eighteen packages, including dplyr, magrittr, rlang, multcomp, nortest, lmtest, effectsize, effsize and dunn.test.
Tests
A testthat (edition 3) suite that checks results against stats::oneway.test, stats::kruskal.test, stats::manova, car::Anova, MASS::glm.nb, afex::aov_ez and hand-computed effect sizes, with a regression test for each behaviour described above. The previous suite consisted of cat()-based harnesses whose runners were commented out, so R CMD check ran no assertions.