Standard Group Comparison
Executive Summary

Executive Summary

Does math score differ by parental level of education?

Observations
1000
Groups Compared
6
Primary Test
Kruskal-Wallis
Primary p-value
0.0001
Effect Size
0.032
Significant Pairs
6
math score genuinely differs between the parental level of education groups (Kruskal-Wallis, p = 7.12e-05). The clearest gap is master's degree-high school: a difference of 7.608 in math score (95% CI 1.266 to 13.95, adjusted p = 0.00837), with 6 of 15 pairwise comparison(s) significant overall. Highest group: master's degree (mean 69.746); lowest: high school (mean 62.138). Magnitude: eta-squared = 0.032 (small effect — share of variation in math score explained by parental level of education). Because the outcome is not normally distributed, the rank-based result is the safer headline here.
What this means

Math scores genuinely differ by parental level of education. The Kruskal-Wallis rank-based test yielded p = 0.0001, confirming the pattern is stronger than chance. The largest gap is 7.608 points between master's degree (mean 69.746) and high school (mean 62.138), with a 95% confidence interval of 1.266 to 13.95 and adjusted p = 0.00837. Overall, 6 of 15 pairwise comparisons were significant. The effect size (eta-squared = 0.032) is small, meaning parental education explains about 3% of the variation in math scores, though the difference is real and detectable.

Overview

Analysis Overview

Comparison of math score across 6 parental level of education groups (1,000 observations).

N Observations1000
N Groups6
N Tests4
N Pairwise15
What this means

This analysis compares math scores across 6 parental education levels using a full statistical toolkit: parametric tests (one-way ANOVA and Welch ANOVA) test whether group means differ, while the Kruskal-Wallis rank-based test makes no normality assumption. Eta-squared quantifies the practical size of any difference. Because the Shapiro-Wilk normality check rejected normality (p = 0.000432), the rank-based result is the safer headline. All 1,000 observations were complete and all 6 groups met minimum size requirements, so no rows or groups were dropped.

Data Preparation

Data Quality

Row and group cleaning applied before testing.

Initial Rows1000
Final Rows1000
Rows Removed0
Groups Dropped0
Levels Lumped0
What this means

All 1,000 rows loaded were retained; no missing math score values required removal. All 6 parental education groups (associate's degree, bachelor's degree, high school, master's degree, some college, some high school) had at least 3 observations and none were lumped into an "Other" category. The final comparison uses the full dataset with no data quality exclusions, ensuring the results reflect the complete sample.

Visualization

Outcome by Group

math score distribution within each parental level of education group.

What this means

Master's degree shows the highest median (73) and high school the lowest (63). The boxes overlap substantially, reflecting within-group variation that is large relative to the between-group differences. Group spreads are broadly comparable across all six parental education levels, indicating no severe heterogeneity. The visual separation between boxes is modest but consistent with the significant pairwise differences detected in the formal tests—the differences exist but are modest relative to the scatter within each group.

Data Table

Group Statistics

n, mean, spread, median and 95% CI of math score per parental level of education group.

GroupNMeanSDMedianCI LowCI High
associate's degree22267.8815.116765.8869.88
bachelor's degree11869.3914.946866.6772.11
high school19662.1414.546360.0964.19
master's degree5969.7515.157365.873.69
some college22667.1314.3167.565.2569
some high school17963.515.936561.1565.85
What this means

Master's degree has the highest mean (69.746, 95% CI 65.797 to 73.695) among 59 students, while high school has the lowest (62.138, 95% CI 60.09 to 64.186) among 196 students. Bachelor's degree (n=118, mean 69.39) and some college (n=226, mean 67.128) occupy the middle ground. Some high school (n=179, mean 63.497) and associate's degree (n=222, mean 67.883) round out the comparison. Means and medians align closely within each group, suggesting symmetric distributions. The non-overlapping confidence intervals between master's degree and high school signal a genuine difference.

Data Table

Statistical Tests

The full test battery: parametric, nonparametric, and the normality check.

TestStatisticP ValueEffect SizeInterpretation
One-way ANOVA6.5225.59e-060.032Mean math score differs across parental level of education groups: highly significant (p < 0.001); eta-squared = 0.032 (small effect).
Welch ANOVA (unequal variances)6.4948.79e-060.032Variance-robust cross-check: highly significant (p < 0.001).
Kruskal-Wallis26.510.0001Rank-based (no normality assumption): highly significant (p < 0.001).
Shapiro-Wilk normality (residuals)0.0004Normality clearly violated — favor the rank-based result.
What this means

All tests agree the difference is significant. The one-way ANOVA (F = 6.522, p = 5.59e-06) and Welch ANOVA (F = 6.494, p = 8.79e-06) both show highly significant results with eta-squared = 0.032. The rank-based Kruskal-Wallis test (H = 26.506, p = 0.0001) confirms the pattern without assuming normality. The Shapiro-Wilk normality test on residuals (p = 0.0004) clearly rejects the normality assumption, making the Kruskal-Wallis result the safer headline. The agreement across three independent tests strengthens confidence in the finding.

Data Table

Which Groups Differ

Pairwise differences in mean math score between parental level of education groups.

ComparisonDifferenceCI LowCI HighAdj PSignificant
master's degree-high school7.6081.26613.950.0084yes
high school-bachelor's degree-7.252-12.23-2.2760.0005yes
some high school-master's degree-6.249-12.660.1630.0611no
some high school-bachelor's degree-5.893-10.96-0.8280.0119yes
high school-associate's degree-5.745-9.931-1.5590.0013yes
some college-high school4.9910.8229.1590.0086yes
some high school-associate's degree-4.386-8.676-0.0950.0418yes
some high school-some college-3.631-7.9040.6420.148no
some college-master's degree-2.617-8.8613.6270.838no
some college-bachelor's degree-2.262-7.1122.5890.768no
master's degree-associate's degree1.863-4.3938.1180.958no
bachelor's degree-associate's degree1.507-3.3596.3730.95no
some high school-high school1.359-3.0565.7750.951no
some college-associate's degree-0.755-4.793.2810.995no
master's degree-bachelor's degree0.356-6.4547.1661no
What this means

Six pairs differ significantly after adjusting for multiple comparisons. The master's degree–high school gap (7.608, 95% CI 1.266 to 13.95, adj. p = 0.0084) is the largest. High school also differs significantly from bachelor's degree (−7.252, 95% CI −12.228 to −2.276, adj. p = 0.0005) and associate's degree (−5.745, 95% CI −9.931 to −1.559, adj. p = 0.0013). Some college exceeds high school by 4.991 (95% CI 0.822 to 9.159, adj. p = 0.0086). Some high school differs from bachelor's degree (−5.893, adj. p = 0.0119) and associate's degree (−4.386, adj. p = 0.0418). The remaining 9 pairs show no significant differences, with confidence intervals spanning zero.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Group Comparison — t-test, ANOVA & Nonparametric

Does a numeric outcome differ between groups? One deck runs the whole standard toolkit: Welch and Student t-tests (2 groups) or one-way and Welch ANOVA (3+ groups), Mann-Whitney / Kruskal-Wallis nonparametric cross-checks, effect sizes (Cohen's d, eta-squared), and pairwise differences with confidence intervals.

Why This Method?

Real questions rarely announce which test they need. Running the parametric and nonparametric versions together, with a normality check on the residuals, lets the report itself say which result is the safest headline — instead of leaving the user to guess between four calculators.

What This Analysis Covers

  • Group distributions side by side (boxplot) and per-group statistics
  • The full test battery with statistics, p-values, and effect sizes
  • Exactly which group pairs differ (Tukey HSD / Welch CI)

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {outcome, group}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Core Analysis Pipeline

compute_shared <- function(df, params, col_map = list()) {
  # === SHARED EXPORTS ===
  #   initial_rows/final_rows/rows_removed  $ row accounting
  #   outcome_h / group_h  $ humanized user names for the two mapped columns
  #   k / group_levels     $ number of groups after cleaning + their names
  #   n_na_outcome         $ rows dropped for missing/non-numeric outcome
  #   dropped_groups_df    $ data.frame(group, n) — groups dropped (n < 3)
  #   lumped_levels        $ character — levels folded into "Other"
  #   group_summary_df     $ group, n, mean, sd, median, ci_low, ci_high
  #   test_results_df      $ test, statistic, p_value, effect_size, interpretation
  #   pairwise_df          $ comparison, difference, ci_low, ci_high, adj_p, significant
  #   boxplot_df           $ group_name, outcome_value (<= 2000 sampled rows)
  #   shapiro_p / out_frac / nonparam_preferred  $ assumption diagnostics
  #   primary_test / primary_p / primary_sig     $ the headline result
  #   effect_value / effect_text                 $ headline effect size
  #   top_sig_pair         $ 1-row slice of pairwise_df or NULL
  #   metrics / json_output
  # === /SHARED EXPORTS ===

Step 1: Resolve mapped columns (humanized for all prose)

initial_rows <- nrow(df)
  outcome_h <- humanize_semantic("outcome", col_map)
  group_h   <- humanize_semantic("group", col_map)
  if (!("outcome" %in% names(df)) || !("group" %in% names(df))) {
    stop(sprintf("Group comparison needs both &#x27;%s' (the numeric outcome) and '%s' (the groups) mapped.",
                 outcome_h, group_h))
  }

Step 2: Coerce the outcome to numeric (95% rule); drop NA-outcome rows

v <- df$outcome
  if (!is.numeric(v)) {
    ch <- as.character(v)
    non_blank <- !is.na(ch) & trimws(ch) != ""
    conv <- suppressWarnings(as.numeric(ch))
    if (sum(non_blank) == 0 ||
        sum(!is.na(conv[non_blank])) < 0.95 * sum(non_blank)) {
      stop(sprintf("The outcome column &#x27;%s' does not look numeric — fewer than 95%% of its values parse as numbers. Pick a numeric column to compare.",
                   outcome_h))
    }
    v <- conv
  }
  df$outcome <- v

  g <- as.character(df$group)
  g[is.na(g) | trimws(g) == ""] <- "Missing"

  keep <- !is.na(df$outcome)
  n_na_outcome <- sum(!keep)
  df <- df[keep, , drop = FALSE]
  g  <- g[keep]
  if (nrow(df) == 0) {
    stop(sprintf("No rows with a usable numeric value in &#x27;%s' remained after cleaning.", outcome_h))
  }

Step 3: Clean the groups — drop n<3 (reported), lump beyond 8 levels

tab <- table(g)
  small <- names(tab)[tab < 3]
  dropped_groups_df <- data.frame(group = character(0), n = integer(0),
                                  stringsAsFactors = FALSE)
  if (length(small) > 0) {
    dropped_groups_df <- data.frame(group = small, n = as.integer(tab[small]),
                                    stringsAsFactors = FALSE)
    sel <- !(g %in% small)
    df <- df[sel, , drop = FALSE]
    g  <- g[sel]
  }

  lumped_levels <- character(0)
  tab <- sort(table(g), decreasing = TRUE)
  if (length(tab) > 8) {
    keep_lv <- names(tab)[1:8]
    lumped_levels <- setdiff(names(tab), keep_lv)
    g[g %in% lumped_levels] <- "Other"
  }

Re-check after lumping ("Other" itself could be tiny)

tab <- table(g)
  small2 <- names(tab)[tab < 3]
  if (length(small2) > 0) {
    dropped_groups_df <- rbind(dropped_groups_df,
                               data.frame(group = small2, n = as.integer(tab[small2]),
                                          stringsAsFactors = FALSE))
    sel <- !(g %in% small2)
    df <- df[sel, , drop = FALSE]
    g  <- g[sel]
  }

  gf <- factor(g)
  k  <- nlevels(gf)
  if (k < 2) {
    stop(sprintf("Group comparison needs at least 2 groups in &#x27;%s' with 3 or more rows each; only %d usable group(s) remained after cleaning. Check that '%s' really splits the data into groups.",
                 group_h, k, group_h))
  }
  y <- df$outcome
  final_rows <- length(y)
  rows_removed <- initial_rows - final_rows
  if (final_rows < 10) {
    stop(sprintf("Only %d usable rows remained — at least 10 are needed to compare groups.", final_rows))
  }
  if (isTRUE(stats::var(y) == 0)) {
    stop(sprintf("The outcome &#x27;%s' has no variation at all (every value is identical) — there is nothing to compare.", outcome_h))
  }
  group_levels <- levels(gf)

Step 4: Per-group summary statistics with 95% CIs

group_summary_df <- do.call(rbind, lapply(group_levels, function(l) {
    x <- y[gf == l]
    n <- length(x)
    m <- mean(x)
    s <- stats::sd(x)
    half <- if (!is.na(s) && s > 0 && n > 1) stats::qt(0.975, n - 1) * s / sqrt(n) else 0
    data.frame(group = l, n = n,
               mean = round(m, 3),
               sd = round(if (is.na(s)) 0 else s, 3),
               median = round(stats::median(x), 3),
               ci_low = round(m - half, 3),
               ci_high = round(m + half, 3),
               stringsAsFactors = FALSE)
  }))
  rownames(group_summary_df) <- NULL

Step 5: Assumption diagnostics — normality of residuals + outliers

dat <- data.frame(y = y, gf = gf)
  fit <- stats::aov(y ~ gf, data = dat)
  res <- stats::residuals(fit)
  shapiro_p <- NA_real_
  if (final_rows >= 3 && final_rows <= 5000) {
    shapiro_p <- tryCatch(stats::shapiro.test(res)$p.value,
                          error = function(e) NA_real_)
  }
  iqr <- stats::IQR(res)
  qs  <- stats::quantile(res, c(0.25, 0.75), names = FALSE)
  out_frac <- if (is.na(iqr) || iqr == 0) 0 else
    mean(res < qs[1] - 3 * iqr | res > qs[2] + 3 * iqr)
  heavy_outliers <- out_frac > 0.01
  nonparam_preferred <- (!is.na(shapiro_p) && shapiro_p < 0.01) || heavy_outliers

  sig_phrase <- function(p) {
    if (is.na(p)) "could not be computed"
    else if (p < 0.001) "highly significant(p < 0.001)"
    else if (p < 0.05) sprintf("significant(p = %.3g)", p)
    else sprintf("not significant(p = %.3g)", p)
  }
  d_word <- function(d) {
    a <- abs(d)
    if (is.na(a)) "unknown"
    else if (a < 0.2) "negligible" else if (a < 0.5) "small"
    else if (a < 0.8) "medium" else "large"
  }
  eta_word <- function(e) {
    if (is.na(e)) "unknown"
    else if (e < 0.01) "negligible" else if (e < 0.06) "small"
    else if (e < 0.14) "medium" else "large"
  }

Step 6: The test battery

test_rows <- list()
  add_test <- function(test, statistic, p_value, effect_size, interpretation) {
    test_rows[[length(test_rows) + 1]] <<- data.frame(
      test = test,
      statistic = round(statistic, 3),
      p_value = signif(p_value, 3),
      effect_size = if (is.na(effect_size)) NA_real_ else round(effect_size, 3),
      interpretation = interpretation,
      stringsAsFactors = FALSE
    )
  }

  pairwise_df <- NULL
  effect_value <- NA_real_
  effect_text <- ""
  primary_test <- NA_character_
  primary_p <- NA_real_

  if (k == 2) {

Two groups: Welch (primary), Student, Mann-Whitney, Cohen's d

l1 <- group_levels[1]; l2 <- group_levels[2]
    x1 <- y[gf == l1]; x2 <- y[gf == l2]
    n1 <- length(x1); n2 <- length(x2)
    s1 <- stats::sd(x1); s2 <- stats::sd(x2)
    sp <- sqrt(((n1 - 1) * s1^2 + (n2 - 1) * s2^2) / (n1 + n2 - 2))
    cohens_d <- if (!is.na(sp) && sp > 0) (mean(x1) - mean(x2)) / sp else NA_real_
    higher <- if (mean(x1) >= mean(x2)) l1 else l2

    welch <- tryCatch(stats::t.test(x1, x2), error = function(e) NULL)
    student <- tryCatch(stats::t.test(x1, x2, var.equal = TRUE),
                        error = function(e) NULL)
    mw <- tryCatch(suppressWarnings(stats::wilcox.test(x1, x2)),
                   error = function(e) NULL)
    rank_biserial <- if (!is.null(mw)) 2 * unname(mw$statistic) / (n1 * n2) - 1 else NA_real_

    if (!is.null(welch)) {
      add_test("Welch t-test(unequal variances)", unname(welch$statistic),
               welch$p.value, cohens_d,
               sprintf("Difference in mean %s is %s; %s effect(Cohen&#x27;s d), %s mean is higher.",
                       outcome_h, sig_phrase(welch$p.value), d_word(cohens_d), higher))
    }
    if (!is.null(student)) {
      add_test("Student t-test(equal variances)", unname(student$statistic),
               student$p.value, cohens_d,
               sprintf("Classic t-test cross-check: %s.", sig_phrase(student$p.value)))
    }
    if (!is.null(mw)) {
      add_test("Mann-Whitney U", unname(mw$statistic), mw$p.value, rank_biserial,
               sprintf("Rank-based(no normality assumption): %s; rank-biserial r = %s.",
                       sig_phrase(mw$p.value),
                       ifelse(is.na(rank_biserial), "NA", round(rank_biserial, 3))))
    }
    if (!is.na(shapiro_p)) {
      add_test("Shapiro-Wilk normality(residuals)", NA_real_, shapiro_p, NA_real_,
               if (shapiro_p < 0.01)
                 "Normality clearly violated — favor the rank-based result."
               else "No strong evidence against normality.")
    }

Pairwise table = the single Welch comparison row (ALWAYS has data)

dmean <- mean(x1) - mean(x2)
    if (!is.null(welch)) {
      ci <- as.numeric(welch$conf.int)
      pw_p <- welch$p.value
    } else if (!is.null(student)) {
      ci <- as.numeric(student$conf.int)
      pw_p <- student$p.value
    } else {
      ci <- c(dmean, dmean)
      pw_p <- NA_real_
    }
    pairwise_df <- data.frame(
      comparison = paste0(l1, " - ", l2),
      difference = round(dmean, 3),
      ci_low = round(ci[1], 3),
      ci_high = round(ci[2], 3),
      adj_p = signif(pw_p, 3),
      significant = ifelse(is.na(pw_p), "", ifelse(pw_p < 0.05, "yes", "no")),
      stringsAsFactors = FALSE
    )

    effect_value <- cohens_d
    effect_text <- sprintf("Cohen&#x27;s d = %s (%s effect)",
                           ifelse(is.na(cohens_d), "NA", round(cohens_d, 2)),
                           d_word(cohens_d))
    if (nonparam_preferred && !is.null(mw)) {
      primary_test <- "Mann-Whitney U"; primary_p <- mw$p.value
    } else if (!is.null(welch)) {
      primary_test <- "Welch t-test"; primary_p <- welch$p.value
    } else if (!is.null(student)) {
      primary_test <- "Student t-test"; primary_p <- student$p.value
    } else if (!is.null(mw)) {
      primary_test <- "Mann-Whitney U"; primary_p <- mw$p.value
    } else {
      stop(sprintf("No comparison test could be computed for &#x27;%s' between the two %s groups (the data may be essentially constant).",
                   outcome_h, group_h))
    }
  } else {

3+ groups: ANOVA, Welch ANOVA, Kruskal-Wallis, eta², Tukey HSD

a_tab <- summary(fit)[[1]]
    ss_b <- a_tab[["Sum Sq"]][1]
    ss_w <- a_tab[["Sum Sq"]][2]
    eta2 <- if (!is.na(ss_b) && !is.na(ss_w) && (ss_b + ss_w) > 0)
      ss_b / (ss_b + ss_w) else NA_real_
    f_stat <- a_tab[["F value"]][1]
    anova_p <- a_tab[["Pr(>F)"]][1]

    welch_a <- tryCatch(stats::oneway.test(y ~ gf, data = dat),
                        error = function(e) NULL)
    kw <- tryCatch(stats::kruskal.test(y ~ gf, data = dat),
                   error = function(e) NULL)

    if (!is.na(anova_p)) {
      add_test("One-way ANOVA", f_stat, anova_p, eta2,
               sprintf("Mean %s differs across %s groups: %s; eta-squared = %s(%s effect).",
                       outcome_h, group_h, sig_phrase(anova_p),
                       ifelse(is.na(eta2), "NA", round(eta2, 3)), eta_word(eta2)))
    }
    if (!is.null(welch_a)) {
      add_test("Welch ANOVA(unequal variances)", unname(welch_a$statistic),
               welch_a$p.value, eta2,
               sprintf("Variance-robust cross-check: %s.", sig_phrase(welch_a$p.value)))
    }
    if (!is.null(kw)) {
      add_test("Kruskal-Wallis", unname(kw$statistic), kw$p.value, NA_real_,
               sprintf("Rank-based(no normality assumption): %s.", sig_phrase(kw$p.value)))
    }
    if (!is.na(shapiro_p)) {
      add_test("Shapiro-Wilk normality(residuals)", NA_real_, shapiro_p, NA_real_,
               if (shapiro_p < 0.01)
                 "Normality clearly violated — favor the rank-based result."
               else "No strong evidence against normality.")
    }

    tk <- tryCatch(stats::TukeyHSD(fit)$gf, error = function(e) NULL)
    if (!is.null(tk) && nrow(tk) > 0) {
      pairwise_df <- data.frame(
        comparison = rownames(tk),
        difference = round(as.numeric(tk[, "diff"]), 3),
        ci_low = round(as.numeric(tk[, "lwr"]), 3),
        ci_high = round(as.numeric(tk[, "upr"]), 3),
        adj_p = signif(as.numeric(tk[, "p adj"]), 3),
        stringsAsFactors = FALSE
      )
      pairwise_df$significant <- ifelse(is.na(pairwise_df$adj_p), "",
                                        ifelse(pairwise_df$adj_p < 0.05, "yes", "no"))
      ok <- !is.na(pairwise_df$difference)
      pairwise_df <- pairwise_df[ok, , drop = FALSE]
      pairwise_df <- pairwise_df[order(-abs(pairwise_df$difference)), , drop = FALSE]
      pairwise_df <- head(pairwise_df, 15)
      rownames(pairwise_df) <- NULL
    } else {
      pairwise_df <- data.frame(comparison = character(0), difference = numeric(0),
                                ci_low = numeric(0), ci_high = numeric(0),
                                adj_p = numeric(0), significant = character(0),
                                stringsAsFactors = FALSE)
    }

    effect_value <- eta2
    effect_text <- sprintf("eta-squared = %s(%s effect — share of variation in %s explained by %s)",
                           ifelse(is.na(eta2), "NA", round(eta2, 3)),
                           eta_word(eta2), outcome_h, group_h)
    if (nonparam_preferred && !is.null(kw)) {
      primary_test <- "Kruskal-Wallis"; primary_p <- kw$p.value
    } else if (!is.null(welch_a) && !is.na(welch_a$p.value)) {
      primary_test <- "Welch ANOVA"; primary_p <- welch_a$p.value
    } else if (!is.na(anova_p)) {
      primary_test <- "One-way ANOVA"; primary_p <- anova_p
    } else if (!is.null(kw)) {
      primary_test <- "Kruskal-Wallis"; primary_p <- kw$p.value
    } else {
      stop(sprintf("No comparison test could be computed for &#x27;%s' across the %s groups (the data may be essentially constant).",
                   outcome_h, group_h))
    }
  }

  test_results_df <- do.call(rbind, test_rows)
  rownames(test_results_df) <- NULL
  primary_sig <- !is.na(primary_p) && primary_p < 0.05

Top significant pairwise difference (NA-safe — never which.max over NAs)

top_sig_pair <- NULL
  if (nrow(pairwise_df) > 0) {
    sig_rows <- pairwise_df[!is.na(pairwise_df$difference) &
                              pairwise_df$significant == "yes", , drop = FALSE]
    if (nrow(sig_rows) > 0) top_sig_pair <- sig_rows[1, ]  # already |diff|-sorted
  }
  n_sig_pairs <- sum(pairwise_df$significant == "yes", na.rm = TRUE)

Step 7: Boxplot dataset — <= 2000 sampled rows

set.seed(42)
  sidx <- if (final_rows > 2000) sample(final_rows, 2000) else seq_len(final_rows)
  boxplot_df <- data.frame(
    group_name = as.character(gf[sidx]),
    outcome_value = y[sidx],
    stringsAsFactors = FALSE
  )

  metrics <- list(
    `Observations`      = final_rows,
    `Groups Compared`   = k,
    `Primary Test`      = primary_test,
    `Primary p-value`   = signif(primary_p, 3),
    `Effect Size`       = if (is.na(effect_value)) NA_real_ else round(effect_value, 3),
    `Significant Pairs` = as.integer(n_sig_pairs)
  )

  json_output <- list(
    answer = paste0(
      "Compared ", outcome_h, " across ", k, " ", group_h, " groups(",
      format(final_rows, big.mark = ","), " rows): the ", primary_test,
      " finds the difference ", sig_phrase(primary_p), "; ", effect_text, ". ",
      if (!is.null(top_sig_pair)) paste0(
        "Largest significant gap: ", top_sig_pair$comparison, " (difference = ",
        top_sig_pair$difference, ", adjusted p = ", top_sig_pair$adj_p, "); ")
      else "No individual pair reaches significance; ",
      n_sig_pairs, " of ", nrow(pairwise_df), " pairwise comparison(s) significant at p<0.05."
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "group_boxplot",
        "group_summary", "test_results", "pairwise_differences"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows,
    rows_removed = rows_removed, n_na_outcome = n_na_outcome,
    outcome_h = outcome_h, group_h = group_h,
    k = k, group_levels = group_levels,
    dropped_groups_df = dropped_groups_df, lumped_levels = lumped_levels,
    group_summary_df = group_summary_df,
    test_results_df = test_results_df,
    pairwise_df = pairwise_df,
    boxplot_df = boxplot_df,
    shapiro_p = shapiro_p, out_frac = out_frac,
    nonparam_preferred = nonparam_preferred,
    primary_test = primary_test, primary_p = primary_p,
    primary_sig = primary_sig,
    effect_value = effect_value, effect_text = effect_text,
    top_sig_pair = top_sig_pair, n_sig_pairs = n_sig_pairs,
    metrics = metrics, json_output = json_output
  )
}
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing