How to Perform a One-Sample T-Test in R

The one-sample t-test answers a simple question: does your sample come from a population with a specific mean? You have data, you have a hypothesized value, and you want to know if the difference...

Key Insights

  • The one-sample t-test compares your sample mean against a hypothesized population value, making it essential for quality control, process validation, and claims testing—but only when your data meets normality and independence assumptions.
  • Always check assumptions before running the test: use Shapiro-Wilk for normality and boxplots for outliers, as violating these assumptions can lead to misleading p-values and incorrect conclusions.
  • R’s built-in t.test() function handles everything you need, but understanding how to extract and interpret individual components (t-statistic, confidence intervals, p-value) separates competent analysts from those who just copy-paste code.

Introduction to One-Sample T-Tests

The one-sample t-test answers a simple question: does your sample come from a population with a specific mean? You have data, you have a hypothesized value, and you want to know if the difference between them is statistically significant or just random noise.

This test appears constantly in practical work. Quality control teams use it to verify that manufacturing processes hit target specifications. Researchers test whether observed measurements differ from established benchmarks. Business analysts check if actual performance metrics deviate from stated goals. If you’ve ever needed to validate a claim like “our average response time is under 200 milliseconds” or “this batch meets the 500mg specification,” you need a one-sample t-test.

The test works by calculating how many standard errors your sample mean sits away from the hypothesized value. If that distance is large enough, you conclude the true population mean likely differs from your hypothesis.

Assumptions and Prerequisites

Before running any t-test, verify these four assumptions:

Continuous data: Your measurements must be on a continuous scale (time, weight, temperature, revenue). The t-test doesn’t work for categorical or ordinal data.

Random sampling: Observations must be independent and randomly selected from the population. Convenience samples or time-series data with autocorrelation violate this assumption.

Approximate normality: The sampling distribution of the mean should be approximately normal. For large samples (n > 30), the Central Limit Theorem usually handles this. For smaller samples, your data itself should be roughly normally distributed.

No significant outliers: Extreme values disproportionately affect the mean and can invalidate your results.

For this tutorial, you need only base R. The t.test() function comes built-in. We’ll use ggplot2 for visualization, but it’s optional.

# Load optional visualization package
library(ggplot2)

# Create sample dataset: delivery times in minutes
# A company claims average delivery is 30 minutes
set.seed(42)
delivery_times <- c(32, 28, 35, 31, 29, 33, 27, 34, 30, 36,
                    31, 28, 33, 29, 32, 35, 30, 34, 31, 28)

# Quick summary
summary(delivery_times)

Checking Assumptions

Never skip assumption checking. Running a t-test on data that violates assumptions produces meaningless results.

Testing Normality

The Shapiro-Wilk test provides a formal normality check. A p-value above 0.05 suggests your data doesn’t significantly deviate from normality.

# Shapiro-Wilk normality test
shapiro_result <- shapiro.test(delivery_times)
print(shapiro_result)

# Output interpretation:
# p-value > 0.05: Data is approximately normal (proceed with t-test)
# p-value < 0.05: Data significantly deviates from normal (consider alternatives)

Visual inspection with Q-Q plots often reveals more than formal tests, especially for small samples where Shapiro-Wilk has low power.

# Q-Q plot for visual normality assessment
qqnorm(delivery_times, main = "Q-Q Plot: Delivery Times")
qqline(delivery_times, col = "red", lwd = 2)

# Points should follow the red line closely
# Systematic deviations indicate non-normality

Identifying Outliers

Boxplots quickly reveal outliers. The IQR method flags values beyond 1.5 times the interquartile range.

# Boxplot for outlier detection
boxplot(delivery_times, 
        main = "Delivery Times Distribution",
        ylab = "Minutes",
        col = "lightblue")

# Programmatic outlier detection using IQR method
identify_outliers <- function(x) {
  q1 <- quantile(x, 0.25)
  q3 <- quantile(x, 0.75)
  iqr <- q3 - q1
  lower_bound <- q1 - 1.5 * iqr
  upper_bound <- q3 + 1.5 * iqr
  
  outliers <- x[x < lower_bound | x > upper_bound]
  return(outliers)
}

outliers <- identify_outliers(delivery_times)
cat("Outliers detected:", ifelse(length(outliers) == 0, "None", outliers))

Performing the T-Test

R’s t.test() function handles one-sample t-tests with minimal syntax. The key parameters are:

  • x: Your numeric vector of observations
  • mu: The hypothesized population mean you’re testing against
  • alternative: Direction of the test (“two.sided”, “less”, or “greater”)
  • conf.level: Confidence level for the interval (default 0.95)
# Two-sided test: Is the true mean different from 30?
result_two_sided <- t.test(delivery_times, mu = 30, alternative = "two.sided")
print(result_two_sided)

# One-sided test: Is the true mean greater than 30?
result_greater <- t.test(delivery_times, mu = 30, alternative = "greater")
print(result_greater)

# One-sided test: Is the true mean less than 30?
result_less <- t.test(delivery_times, mu = 30, alternative = "less")
print(result_less)

Choose your alternative hypothesis before looking at the data. Picking the direction after seeing results is p-hacking and invalidates your analysis.

Interpreting the Output

The t-test output contains several components. Understanding each one matters.

# Run the test
result <- t.test(delivery_times, mu = 30, alternative = "two.sided")

# The full output
print(result)

# Extract individual components
cat("T-statistic:", result$statistic, "\n")
cat("Degrees of freedom:", result$parameter, "\n")
cat("P-value:", result$p.value, "\n")
cat("95% Confidence Interval:", result$conf.int[1], "to", result$conf.int[2], "\n")
cat("Sample Mean:", result$estimate, "\n")

T-statistic: Measures how many standard errors the sample mean is from the hypothesized value. Larger absolute values indicate stronger evidence against the null hypothesis.

Degrees of freedom: Equal to n - 1 for a one-sample t-test. This determines which t-distribution to use for calculating the p-value.

P-value: The probability of observing a t-statistic this extreme (or more extreme) if the null hypothesis were true. Compare this to your significance level (typically 0.05).

Confidence interval: The range likely containing the true population mean. If this interval excludes your hypothesized value, you’ll reject the null hypothesis.

Sample mean: Your observed average, which you’re comparing against the hypothesized value.

# Decision logic
alpha <- 0.05

if (result$p.value < alpha) {
  cat("Reject null hypothesis: Evidence suggests true mean differs from", 
      result$null.value, "\n")
} else {
  cat("Fail to reject null hypothesis: Insufficient evidence that true mean differs from",
      result$null.value, "\n")
}

Visualizing Results

Good visualizations communicate findings more effectively than tables of numbers.

# Histogram with hypothesized mean and sample mean
ggplot(data.frame(time = delivery_times), aes(x = time)) +
  geom_histogram(binwidth = 2, fill = "steelblue", color = "white", alpha = 0.7) +
  geom_vline(xintercept = 30, color = "red", linetype = "dashed", size = 1.2) +
  geom_vline(xintercept = mean(delivery_times), color = "darkgreen", size = 1.2) +
  annotate("text", x = 30, y = 4.5, label = "Claimed: 30 min", color = "red", hjust = -0.1) +
  annotate("text", x = mean(delivery_times), y = 4, 
           label = paste("Observed:", round(mean(delivery_times), 1), "min"), 
           color = "darkgreen", hjust = -0.1) +
  labs(title = "Distribution of Delivery Times",
       x = "Delivery Time (minutes)",
       y = "Frequency") +
  theme_minimal()

A confidence interval plot provides another effective visualization:

# Confidence interval visualization
ci_data <- data.frame(
  mean = result$estimate,
  lower = result$conf.int[1],
  upper = result$conf.int[2]
)

ggplot(ci_data, aes(x = 1, y = mean)) +
  geom_point(size = 4, color = "steelblue") +
  geom_errorbar(aes(ymin = lower, ymax = upper), width = 0.1, size = 1, color = "steelblue") +
  geom_hline(yintercept = 30, color = "red", linetype = "dashed", size = 1) +
  annotate("text", x = 1.15, y = 30, label = "Hypothesized mean: 30", color = "red") +
  coord_flip() +
  labs(title = "95% Confidence Interval for Mean Delivery Time",
       y = "Delivery Time (minutes)",
       x = "") +
  theme_minimal() +
  theme(axis.text.y = element_blank(), axis.ticks.y = element_blank())

Complete Worked Example

Let’s work through a realistic scenario from start to finish. A food delivery company claims their average delivery time is 30 minutes. You’ve collected data from 25 random deliveries and want to test this claim.

# Complete reproducible workflow
set.seed(123)

# Scenario: Testing if delivery times differ from claimed 30 minutes
delivery_data <- c(32, 28, 35, 31, 29, 33, 27, 34, 30, 36,
                   31, 28, 33, 29, 32, 35, 30, 34, 31, 28,
                   33, 30, 32, 29, 34)

cat("=== STEP 1: Exploratory Analysis ===\n")
cat("Sample size:", length(delivery_data), "\n")
cat("Sample mean:", round(mean(delivery_data), 2), "minutes\n")
cat("Sample SD:", round(sd(delivery_data), 2), "minutes\n")
cat("Hypothesized mean: 30 minutes\n\n")

cat("=== STEP 2: Check Assumptions ===\n")

# Normality test
shapiro <- shapiro.test(delivery_data)
cat("Shapiro-Wilk p-value:", round(shapiro$p.value, 4), "\n")
cat("Normality assumption:", ifelse(shapiro$p.value > 0.05, "SATISFIED", "VIOLATED"), "\n")

# Outlier check
outliers <- identify_outliers(delivery_data)
cat("Outliers:", ifelse(length(outliers) == 0, "None detected", paste(outliers, collapse = ", ")), "\n\n")

cat("=== STEP 3: Perform T-Test ===\n")
test_result <- t.test(delivery_data, mu = 30, alternative = "two.sided", conf.level = 0.95)
print(test_result)

cat("\n=== STEP 4: Conclusion ===\n")
alpha <- 0.05
if (test_result$p.value < alpha) {
  cat("At alpha =", alpha, ", we REJECT the null hypothesis.\n")
  cat("Conclusion: The true mean delivery time significantly differs from 30 minutes.\n")
  cat("The sample mean of", round(test_result$estimate, 2), 
      "minutes suggests deliveries take longer than claimed.\n")
} else {
  cat("At alpha =", alpha, ", we FAIL TO REJECT the null hypothesis.\n")
  cat("Conclusion: Insufficient evidence that delivery time differs from 30 minutes.\n")
}

The one-sample t-test is a foundational tool. Master it, understand its assumptions, and you’ll have a reliable method for testing claims against observed data. When assumptions fail, consider non-parametric alternatives like the Wilcoxon signed-rank test, but that’s a topic for another article.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.