The Challenge with Base R Summary Operations

When analyzing differences across two groups in a dataset, developers often need to compute group-wise summary statistics (like mean and standard deviation) and perform hypothesis testing (such as a two-sample t-test) across multiple continuous variables. Doing this with base R functions like aggregate() or manual matrix conversions can lead to verbose, brittle, and unreadable code.

The Modern Solution: Tidyverse and Broom

By leveraging modern R tools—specifically dplyr, tidyr, and broom—you can compute all summary statistics and column-wise t-tests cleanly in just a few lines of code.

Step 1: Compute Group Summary Statistics Cleanly

Instead of running multiple aggregate() calls and stitching vectors together with cbind(), use summarise(across(...)) in dplyr:

library(dplyr)

# Filter dataset for groups of interest (4 and 8 cylinders)
data_sub <- mtcars %>% 
  filter(cyl %in% c(4, 8))

# Calculate Mean and SD across all numeric variables grouped by cyl
summary_stats <- data_sub %>%
  group_by(cyl) %>%
  summarise(across(everything(), list(mean = mean, sd = sd)))

print(summary_stats)

Step 2: Run Column-Wise T-Tests with pivot_longer() and broom::tidy()

To run t-tests across every numeric variable efficiently without manual loop indexing, reshape the dataset into long format, group by variable name, and extract clean test metrics with broom::tidy():

library(tidyr)
library(purrr)
library(broom)

# Reshape and perform t-test for each variable
t_test_results <- data_sub %>%
  pivot_longer(cols = -cyl, names_to = "variable", values_to = "value") %>%
  group_by(variable) %>%
  summarise(
    test = list(t.test(value ~ cyl)),
    .groups = "drop"
  ) %>%
  mutate(tidied = map(test, tidy)) %>%
  unnest(tidied) %>%
  select(variable, p.value, statistic, conf.low, conf.high)

print(t_test_results)

Step 3: Combine Everything into a Publication-Ready Table

If your goal is to produce a single aggregated data frame containing group means, standard deviations, and the calculated p-value for each feature, combine the two workflows:

# Convert summary statistics to long layout
stats_reshaped <- summary_stats %>%
  pivot_longer(-cyl, names_to = c("variable", "stat"), names_sep = "_") %>%
  pivot_wider(names_from = c(cyl, stat), values_from = value, names_prefix = "cyl_")

# Join p-values from the t-tests
final_summary <- stats_reshaped %>%
  left_join(t_test_results %>% select(variable, p.value), by = "variable")

print(final_summary)

Alternative Approach: Using rstatix

For an even more streamlined experience, the rstatix package provides pipe-friendly statistical wrappers designed for tidy data frames:

library(rstatix)

t_tests_rstatix <- data_sub %>%
  pivot_longer(cols = -cyl, names_to = "variable", values_to = "value") %>%
  group_by(variable) %>%
  t_test(value ~ cyl)

print(t_tests_rstatix)

Summary

Using dplyr::across() together with tidyr::pivot_longer() and broom::tidy() replaces long base R scripts with a readable data pipeline. This pattern scales easily to any number of numerical columns and complex grouped data workflows in R.