Nothing
#' ANOVA or Welch's ANOVA with appropriate post-hoc tests
#'
#' Internal function that performs ANOVA or Welch's one-way test and
#' corresponding post-hoc comparisons. Uses TukeyHSD for equal variances
#' (Fisher's ANOVA) and Games-Howell for unequal variances (Welch's ANOVA).
#'
#' @param samples Numeric vector; the dependent variable.
#' @param fact Factor; the grouping variable.
#' @param conf.level Numeric; confidence level for tests and intervals (default: 0.95).
#' @param samplename Character; label for y-axis (default: "").
#' @param factorname Character; label for x-axis (default: "").
#' @param cex Numeric; character expansion factor for plot elements (default: 1).
#'
#' @return A list with components:
#' \describe{
#' \item{summary statistics of ANOVA}{Summary of Fisher's ANOVA or Welch's oneway test}
#' \item{post-hoc analysis}{TukeyHSD object or Games-Howell results in compatible format}
#' \item{conf.level}{The confidence level used}
#' }
#'
#' @details
#' The function first tests for homogeneity of variance using Levene's test.
#' If variances are equal (p > alpha), Fisher's one-way ANOVA with Tukey's HSD
#' post-hoc is performed. If variances are unequal (p <= alpha), Welch's
#' one-way ANOVA with Games-Howell post-hoc is performed.
#'
#' The function produces a box plot with jittered points and group means
#' (red diamonds for the parametric branches), annotated with a compact
#' letter display showing which groups differ significantly.
#' @examples
#' # Example with equal variances (uses Fisher's ANOVA + TukeyHSD)
#' data(PlantGrowth)
#' result1 <- vis_anova(PlantGrowth$weight, PlantGrowth$group,
#' samplename = "Weight", factorname = "Group")
#'
#' # Example with unequal variances (uses Welch's ANOVA + Games-Howell)
#' # Create data with heterogeneous variances
#' set.seed(123)
#' group_a <- rnorm(20, mean = 10, sd = 1)
#' group_b <- rnorm(20, mean = 15, sd = 5) # Much larger variance
#' group_c <- rnorm(20, mean = 12, sd = 2)
#' values <- c(group_a, group_b, group_c)
#' groups <- factor(rep(c("A", "B", "C"), each = 20))
#' result2 <- vis_anova(values, groups,
#' samplename = "Value", factorname = "Group")
#'
#' @export
vis_anova <- function(samples,
fact,
conf.level = conf.level,
samplename = "",
factorname = "",
cex = 1) {
if (missing(conf.level)) {
conf.level <- 0.95
}
oldparanova <- par(no.readonly = TRUE)
on.exit(par(oldparanova))
alpha <- 1 - conf.level
samples3 <- na.omit(samples)
fact <- subset(fact, !is.na(samples))
samples <- samples3
n_classes <- length(unique(fact))
meanna <- function(x) {
mean(x, na.rm = T)
}
m <- tapply(samples, fact, meanna)
# tests
an <- aov(samples ~ fact)
summaryAnova <- summary(an)
oneway <- oneway.test(samples ~ fact)
# check for homogeneity
bartlett_test <- bartlett.test(samples ~ fact)
p_bart <- bartlett_test$p.value
levene_test <- levene.test(samples,fact)
p_levene <- levene_test$p.value
if (p_levene > 1 - conf.level)
{
p_aov <- summaryAnova[[1]][["Pr(>F)"]][1]
F_value <- sprintf("%.2f", summaryAnova[[1]]$`F value`[1])
label_aov <- "Fisher's one-way ANOVA"
summarystat <- summaryAnova
post_hoc_anova <- TukeyHSD(an, conf.level = conf.level)
} else {
# Unequal variances - use Welch's ANOVA with Games-Howell post-hoc
p_aov <- oneway$p.value
F_value <- sprintf("%.2f", oneway$statistic)
label_aov <- "Welch's one-way ANOVA"
summarystat <- oneway
# Use Games-Howell for post-hoc (correct for unequal variances)
gh_result <- games.howell(samples, fact, conf.level = conf.level)
# Convert to format needed by multcompLetters (line 846)
comparison_names <- paste0(gh_result$group2, "-", gh_result$group1)
result_matrix <- cbind(
diff = gh_result$mean_diff,
lwr = gh_result$ci_lower,
upr = gh_result$ci_upper,
`p adj` = gh_result$p_adj
)
rownames(result_matrix) <- comparison_names
post_hoc_anova <- list(fact = result_matrix)
}
# Y-axis limits (with extra lower margin for legend space)
lower_margin <- 0.45
upper_margin <- 0.2
margins <- calc_min_max_of_y_axis(samples, lower_margin, upper_margin)
mi <- margins[[1]]
ma <- margins[[2]]
# For legend positioning (spread from actual plot range)
spread <- ma - mi
box_cols <- rep_len(c(colorscheme(1), colorscheme(3)), n_classes)
show_jitter <- max(tapply(samples, fact, length)) <= jitter_max_n
b <- boxplot(
samples ~ fact,
xlim = c(0, n_classes + 1),
ylim = c(mi, ma),
col = box_cols,
ylab = samplename,
xlab = factorname,
las = 2,
outline = !show_jitter # outliers via boxplot only when jitter is suppressed
)
if (show_jitter) {
stripchart(
samples ~ fact,
vertical = TRUE,
method = "jitter",
col = adjustcolor(box_cols, red.f = 0.55, green.f = 0.55, blue.f = 0.55),
pch = 1,
cex = 0.7,
add = TRUE
)
}
# Group means -- parametric branch tests means, so mark them explicitly
points(seq_len(n_classes), m,
pch = 16, col = "red", cex = 1.3)
if (n_classes > 6) {
n_labels <- c(paste("N =", b$n[1]), as.character(b$n[-1]))
} else {
n_labels <- paste("N =", b$n)
}
text(seq_len(n_classes), ma, n_labels)
s <- multcompLetters(post_hoc_anova[[1]][, 4], threshold = alpha)
ord <- c()
v <- attributes(s$Letters)$names
f_levels <- sort(unique(fact))
for (i in 1:n_classes) {
ord[i] <- which(v == f_levels[i])
}
text(seq(1:n_classes + 1),
mi,
s$Letters[ord],
col = colors()[81],
lwd = 2)
mtext(
paste0(label_aov, "\nF = ", F_value, ", p = ", signif(p_aov, 2)))
# Legend: mean marker (top) + significance letters with post-hoc method
# and alpha (split on two lines for compactness in narrow windows).
# All post-hoc / alpha info lives here, so the title can stay compact.
posthoc_name <- ifelse(p_levene > alpha, "Tukey's HSD", "Games-Howell")
legend("bottomleft",
legend = c("group mean",
"a, b, ...: significance letters",
paste0("(", posthoc_name, ", alpha = ", signif(alpha, 2), ")")),
pch = c(16, NA, NA),
col = c("red", NA, NA),
text.col = c("red", colors()[81], colors()[81]),
bty = "n",
cex = 0.9,
inset = 0.08)
my_list <-
list(
# "summary statistics of Fisher's one-way ANOVA" = summaryAnova,
# "summary statistics of Welch's one-way ANOVA (not assuming equal variances)" = oneway,
"summary statistics of ANOVA" = summarystat,
"post-hoc analysis " = post_hoc_anova,
"conf.level" = conf.level
)
return(my_list)
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.