Hypothesis Testing

t-tests & the Truth About p-values

What’s On For Today? 🔬

Tip

Some Helpful Stuff …

  • User input - how to get user input?
  • Functions - creating, using and lovin’ functions

Formal Statistical Inference

  • The Logic - null hypotheses and p-values
  • t-tests - one-sample, two-sample, paired
  • correlation - testing for relationships between two continuous variables

A p-value answers one narrow question. Know exactly which one.

Some Helpful Stuff

User Input

# Getting user input
myVar = readline(prompt = "Enter your name :")
Enter your name :
cat("Your name is :",myVar,"\n") #output
Your name is :  

Getting user input in R

Note

Note: The readline() function is used to get input from the user. It will pause the execution of the code and wait for the user to enter a value. Generally not used in R Markdown documents or on back-end servers, but useful in interactive R scripts.

A Simple Blast Off Countdown Thingy 🚀

# uncomment to get user input
# myVar <- readline(prompt = "Enter blastoff count :")

myVar <- 10 # Testing ...

for (i in 0:myVar) {
  cat("countDown :", i, "\n")
  if (i == myVar) {
    cat("Blast Off!")
  }
}
countDown : 0 
countDown : 1 
countDown : 2 
countDown : 3 
countDown : 4 
countDown : 5 
countDown : 6 
countDown : 7 
countDown : 8 
countDown : 9 
countDown : 10 
Blast Off!

Functions: Creatin’ and Lovin’ ’em

How we write functions in R

The General Syntax of a Function

# The general structure of a function in R
my_function <- function(arg_1, arg_2, ...) {
  # Function body
  # code to be executed
  return(value)  # optional return statement
}

Important

  • Functions are reusable blocks of code that can take inputs (arguments) and return outputs (values).
  • Easier to debug and maintain code when it is organized into functions.
  • The function() keyword is used to define a function, and the function body contains the code that will be executed when the function is called.
  • The return() statement is optional; if omitted, the function will return the last evaluated expression.

Another WONDERFUL Example

add_numbers <- function(x, y) {
  result <- x + y
  return(result)
}

#call and print
cat(add_numbers(3,4))
7

Important

  • The return() statement is used to specify the output of the function.
  • If no return() statement is provided, the function will return the last evaluated expression.

A MIGHTY WONDERFUL Example

# calculating sine of an angle in degrees
sine_degrees <- function(angle_degrees) {
    # Convert degrees to radians
    angle_radians <- angle_degrees * (pi / 180)  
    # Calculate sine
    sine_value <- sin(angle_radians)
    # Return the sine value
    return(sine_value)
}
# call the function
cat(sine_degrees(30)) # Output: 0.5
0.5

A SUPER Plotting Function

make_plot <-  function(x_data, y_data, myCol) {
  plot(x_data, y_data, col = myCol)
}

make_plot(1:10, 11:20, "BLUE")

Another SUPER-DUPER Plotting Function

# calculating sine of an angle in degrees
sine_degrees <- function(angle_degrees) {
    # Convert degrees to radians
    angle_radians <- angle_degrees * (pi / 180)  
    # Calculate sine
    sine_value <- sin(angle_radians)
    # Return the sine value
    return(sine_value)
}

plot_sine <- function(start, end, step) {
  # Generate a sequence of angles in degrees
  angles <- seq(start, end, by = step)
  # Calculate sine values for each angle
  sine_values <- sapply(angles, sine_degrees)
  
  # Create the plot
  plot(angles, sine_values, type = "l", col = "blue", lwd = 2,
       xlab = "Angle (degrees)", ylab = "Sine Value",
       main = "Sine Function Plot")
  grid()  # Add grid lines for better visualization
}

plot_sine(1,400,1)

A Quick Word About Quantiles

Tip

  • Quartiles: Splits the data into four equal parts (Q₁ at 25%, Q₂ at 50%, Q₃ at 75%).
  • Common quantiles include quartiles (4 parts), quintiles (5 parts), and percentiles (100 parts).
  • Useful when summarizing data spread, central tendency, and range

Let’s See That in Code

# Generate some sample data
set.seed(301)
data <- rnorm(100)

#cat(data) # Uncomment to see the data

# Calculate quantiles: How much of the data is below a certain value?
quantile(data, probs = c(0.25, 0.5, 0.75))
        25%         50%         75% 
-0.56422487 -0.01148396  0.50636497 
summary(data)
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
-2.85719 -0.56422 -0.01148 -0.07634  0.50636  2.00706 

A Function to Check Normality of Data

If your data is normally distributed, the points in the Q-Q plot will fall approximately along the reference line. Deviations from this line indicate departures from normality.

library(tidyverse)

# Quartiles
myQuartilePlot <- function(data) {
  # Create a data frame for ggplot
  df <- data.frame(value = data)
  
  # Create the quantile plot
  ggplot(df, aes(sample = value)) +
    stat_qq() +
    stat_qq_line() +
    labs(title = "Quantile-Quantile Plot",
         x = "Theoretical Quantiles",
         y = "Sample Quantiles") +
    theme_minimal()
}

# Generate some sample data
set.seed(301)
data <- rnorm(100)

myQuartilePlot(data)

A Function to Check Normality of Data Plot

In normal distributions, the points will align with the reference line.

Correlation: Testing for Relationships Between Two Continuous Variables

  • What does a correlation coefficient tell us?
  • How we test for correlation in R?

Correlation: A Relatedness Measure

Important

  • A correlation coefficient measures the strength and direction of the linear relationship between two continuous variables.

  • Correlation coefficients range from -1 (negative) to 1 (positive), with values closer to -1 or 1 indicating stronger relationships.

Positive Correlation

Positive correlation: as one variable increases, the other also increases.

Negative Correlation

Negative correlation: as one variable increases, the other decreases.

Neutral Correlation

Neutral correlation: the increase in one variable is not related to the increase in the other.

All Correlations

All correlations: positive, negative, and neutral correlations.

Correlations with Numerical Values

Different types of correlation coefficients exist, such as Pearson’s correlation coefficient (for linear relationships) and Spearman’s rank correlation coefficient (for monotonic relationships).

Correlations with Numerical Values

The correlation coefficient is a unitless measure; it does not depend on the units of the variables being measured.

Correlation Coefficients

Tip

Correlation measures the strength and direction of the linear relationship between two continuous variables. The correlation coefficient ranges from -1 to 1, where:

  • 1 indicates a perfect positive linear relationship
  • 0.5 indicates a moderate positive linear relationship
  • 0 indicates no linear relationship
  • -0.5 indicates a moderate negative linear relationship
  • -1 indicates a perfect negative linear relationship

Code for Correlations in R Part 1

# Online help? Use ?cor or help(cor)
# to get help on the cor() function

# Two simple vectors
cor(1:10, 2:11) # == 1
[1] 1
cor(1:10, -2:-11) # == -1
[1] -1
# matrix
cor(longley)
             GNP.deflator       GNP Unemployed Armed.Forces Population
GNP.deflator    1.0000000 0.9915892  0.6206334    0.4647442  0.9791634
GNP             0.9915892 1.0000000  0.6042609    0.4464368  0.9910901
Unemployed      0.6206334 0.6042609  1.0000000   -0.1774206  0.6865515
Armed.Forces    0.4647442 0.4464368 -0.1774206    1.0000000  0.3644163
Population      0.9791634 0.9910901  0.6865515    0.3644163  1.0000000
Year            0.9911492 0.9952735  0.6682566    0.4172451  0.9939528
Employed        0.9708985 0.9835516  0.5024981    0.4573074  0.9603906
                  Year  Employed
GNP.deflator 0.9911492 0.9708985
GNP          0.9952735 0.9835516
Unemployed   0.6682566 0.5024981
Armed.Forces 0.4172451 0.4573074
Population   0.9939528 0.9603906
Year         1.0000000 0.9713295
Employed     0.9713295 1.0000000
# Correlation Matrix of Multivariate sample:
(Cl <- cor(longley))
             GNP.deflator       GNP Unemployed Armed.Forces Population
GNP.deflator    1.0000000 0.9915892  0.6206334    0.4647442  0.9791634
GNP             0.9915892 1.0000000  0.6042609    0.4464368  0.9910901
Unemployed      0.6206334 0.6042609  1.0000000   -0.1774206  0.6865515
Armed.Forces    0.4647442 0.4464368 -0.1774206    1.0000000  0.3644163
Population      0.9791634 0.9910901  0.6865515    0.3644163  1.0000000
Year            0.9911492 0.9952735  0.6682566    0.4172451  0.9939528
Employed        0.9708985 0.9835516  0.5024981    0.4573074  0.9603906
                  Year  Employed
GNP.deflator 0.9911492 0.9708985
GNP          0.9952735 0.9835516
Unemployed   0.6682566 0.5024981
Armed.Forces 0.4172451 0.4573074
Population   0.9939528 0.9603906
Year         1.0000000 0.9713295
Employed     0.9713295 1.0000000
# Graphical Correlation Matrix:
symnum(Cl) # highly correlated
             GNP. GNP U A P Y E
GNP.deflator 1                 
GNP          B    1            
Unemployed   ,    ,   1        
Armed.Forces .    .     1      
Population   B    B   , . 1    
Year         B    B   , . B 1  
Employed     B    B   . . B B 1
attr(,"legend")
[1] 0 ' ' 0.3 '.' 0.6 ',' 0.8 '+' 0.9 '*' 0.95 'B' 1
# Pearson’s r
symnum(clP <- cor(longley, method = "pearson")) #default
             GNP. GNP U A P Y E
GNP.deflator 1                 
GNP          B    1            
Unemployed   ,    ,   1        
Armed.Forces .    .     1      
Population   B    B   , . 1    
Year         B    B   , . B 1  
Employed     B    B   . . B B 1
attr(,"legend")
[1] 0 ' ' 0.3 '.' 0.6 ',' 0.8 '+' 0.9 '*' 0.95 'B' 1

Code for Correlations in R Part 2

# Spearman's rho, Kendall's tau and
# Data is of non-bivariate normal distribution
symnum(clS <- cor(longley, method = "spearman"))
             GNP. GNP U A P Y E
GNP.deflator 1                 
GNP          B    1            
Unemployed   ,    ,   1        
Armed.Forces          . 1      
Population   B    B   ,   1    
Year         B    B   ,   1 1  
Employed     B    B   .   B B 1
attr(,"legend")
[1] 0 ' ' 0.3 '.' 0.6 ',' 0.8 '+' 0.9 '*' 0.95 'B' 1
symnum(clK <- cor(longley, method = "kendall"))
             GNP. GNP U A P Y E
GNP.deflator 1                 
GNP          B    1            
Unemployed   .    .   1        
Armed.Forces            1      
Population   B    B   .   1    
Year         B    B   .   1 1  
Employed     *    *   .   + + 1
attr(,"legend")
[1] 0 ' ' 0.3 '.' 0.6 ',' 0.8 '+' 0.9 '*' 0.95 'B' 1
# How much do they differ?
i <- lower.tri(Cl)
cor(cbind(P = Cl[i], S = clS[i], K = clK[i]))
          P         S         K
P 1.0000000 0.9802390 0.9572562
S 0.9802390 1.0000000 0.9742171
K 0.9572562 0.9742171 1.0000000

Variance and Standard Deviation

  • How spread out is the data?
  • Does the data have a lot of variability about the mean?

Whats the Spread of the Data?

How spread out is the data? Does the data have a lot of variability about the mean?

Variance is a measure of how spread out the data is from the mean. It is calculated by taking the average of the squared differences from the mean.

Variance

Tip

Variance is expressed in the squared units of the data, which can make it less interpretable than standard deviation.

The formula for variance (sigma squared) is: \(\sigma^2 = \frac{\sum_{i=1}^{N} (x_i - \mu)^2 }{N}\)

Where,

  • \(\sigma\) is the standard deviation
  • \(N\) is the number of observations
  • \(x_i\) is the \(i\)-th observation
  • \(\mu\) is the mean.

Code to Calculate Variance

myData <- c( 9, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4)
var(myData) # Variance is 9.368421
[1] 9.368421
# Standard Deviation
sqrt(var(myData)) # Standard deviation is 3.060788
[1] 3.060788
myData <- c( NA, 2, 5, 4, 12, 7, 8, 11, 9, 3, 7, 4, 12, 5, 4, 10, 9, 6, 9, 4)

# The variance function cannot handle missing
# values by default, so it returns NA.
var(myData) # NA's in datasets create trouble. 
[1] NA
# Alternative way to calculate variance
var(myData, na.rm = TRUE) # Variance is 9.368421
[1] 9.654971

Standard Deviation

Standard deviation is the square root of the variance. It is a measure of the amount of variation or dispersion of a set of values.

Tip

If variance is a measure of how spread out the data is, standard deviation is a measure of how much the data deviates from the mean. It is expressed in the same units as the data, making it more interpretable than variance.

The formula for standard deviation (sigma) is: \(\sigma = \sqrt{\frac{\sum_{i=1}^{N} (x_i - \mu)^2 }{N}}\)

Where,

  • \(\sigma\) is the standard deviation
  • \(N\) is the number of observations
  • \(x_i\) is the \(i\)-th observation
  • \(\mu\) is the mean.

Standard Deviation Visually Part 1

Standard Deviation Visually Part 2

Tip

  • Large standard deviation → data is spread out over a wider range of values.
    • Large and Lazy
  • Small standard deviation → data is clustered closely around the mean.
    • Small and Snug

How to Judge Standard Deviations

Tip

Instead of looking for a universal “good” or “bad” number, evaluate your SD using these three methods:

  • Compare SD to the mean (Coefficient of Variation): Divide the standard deviation by the mean to see relative spread. A lower percentage means your data is tightly clustered around the average.

  • Compare SD to your requirements or limits.

    • Check if the expected spread fits inside your acceptable range. For a normally distributed bell curve, roughly 68% of data falls within 1 standard deviation of the mean, and 95% falls within 2 standard deviations. If 95% of your results fit inside your customer or project specifications, your variation is under control.
  • Compare it over time: Track the number across repeated runs. A rising standard deviation means your process is becoming less stable or predictable.

Above Details stolen indirectly from Wikipedia: https://en.wikipedia.org/wiki/Standard_deviation

Standard Deviation Visually Part 3

Tip

  • How does the SD compare to your requirements or limits?
  • How does the SD change over time?

Standard Deviation and Means

library(ggplot2)

# Define the distributions
params <- data.frame(
  group = c("Low", "Moderate", "High", "Very high"),
  mean = c(10, 20, 30, 40),
  sd = c(2, 4, 6, 8)
)

# Generate density values for each distribution
set.seed(123)

data <- do.call(rbind, lapply(1:nrow(params), function(i) {
  x <- seq(
    params$mean[i] - 4 * params$sd[i],
    params$mean[i] + 4 * params$sd[i],
    length.out = 500
  )

  data.frame(
    x = x,
    density = dnorm(
      x,
      mean = params$mean[i],
      sd = params$sd[i]
    ),
    group = params$group[i]
  )
}))

# Plot the distributions
ggplot(data, aes(x = x, y = density, color = group)) +
  geom_line(linewidth = 1.2) +
  geom_vline(
    data = params,
    aes(xintercept = mean, color = group),
    linetype = "dashed",
    linewidth = 0.7,
    show.legend = FALSE
  ) +
  labs(
    title = "Increasing Means and Standard Deviations",
    subtitle = "Dashed lines indicate the means",
    x = "Value",
    y = "Probability density",
    color = "Distribution"
  ) +
  theme_minimal(base_size = 13)

Standard Deviation and Means (Plot)

The Logic of Testing

Important

  • What is a null hypothesis?
  • What is an alternative hypothesis?
  • What is a p-value?
  • How do we interpret a p-value?

The Courtroom Analogy

In court

  • Presumption: innocent
  • Evidence is gathered
  • “Beyond reasonable doubt” → convict
  • Otherwise → not guilty (not “proven innocent”)

In statistics

  • Presumption: H₀, no effect
  • Data is gathered
  • p < α → reject H₀
  • Otherwise → fail to reject (not “H₀ is true”)

“Fail to Reject” Is Not “Accept”

Absence of evidence is not evidence of absence. A tiny study can fail to detect an enormous effect.

The Vocabulary

Tip

Term Meaning
\(H_0\) (null) The boring claim: no difference, no effect, no relationship
\(H_1\) (alternative) What you suspect is happening
Test statistic A number summarizing how far your data is from H₀
p-value P(data this extreme or more | H₀ is true)
α (alpha) Your threshold, chosen before looking (usually 0.05)
Type I error Rejecting a true H₀ — a false alarm (rate = α)
Type II error Failing to reject a false H₀ — a miss (rate = β)
Power 1 − β: your chance of detecting a real effect

What a p-value Is NOT 🚫

Four Very Common Errors

❌ “p = 0.03 means there is a 3% chance H₀ is true”
❌ “p = 0.03 means a 97% chance the effect is real”
❌ “p = 0.06 means there is no effect”
❌ “p = 0.001 means the effect is large and important”

What It Actually Says

“If there were truly no effect, data at least this extreme would appear 3% of the time.” That is all. It says nothing about the size or importance of the effect. 📏

A SUPER Fast View of P-Values!

Tip

  • If the p-value is small (< 0.05), we reject H₀. If the p-value is large, we fail to reject H₀.
  • If the p-value is very small (< 0.01), we have stronger evidence for Ha.

A SUPER Fast Example with \(H_0\) and \(H_1\)! 🍹

Tip

Let’s say we’re testing whether a new drink increases alertness. We have 30 participants, and we measure their alertness before and after drinking the new drink.

  • \(H_0\): The new drink has no effect on alertness.
  • \(H_1\): The new drink increases alertness.

We collect the data and find a p-value of 0.03, which is less than our significance level of 0.05. So we reject H₀ and conclude that the new drink increases alertness.

Seeing the Null Distribution 👀

library(tidyverse)
set.seed(301)

# Simulate H0 being TRUE: two groups from the same population
null_ts <- replicate(5000, {
  a <- rnorm(20, 100, 15)
  b <- rnorm(20, 100, 15)      # identical population!
  t.test(a, b)$statistic
})

observed <- 2.3   # pretend this is our real study's t statistic

ggplot(tibble(t = null_ts), aes(t)) +
  geom_histogram(bins = 60, fill = "#BDC3C7", color = "white") +
  geom_vline(xintercept = c(-observed, observed), color = "#E74C3C", linewidth = 1) +
  labs(title = "5,000 studies where H0 was true",
       subtitle = paste0("p-value = fraction beyond the red lines = ",
                         round(mean(abs(null_ts) >= observed), 4)),
       x = "t statistic", y = "Count") +
  theme_minimal(base_size = 12)

Errors and Power 🎯

# Power: if the true difference is 5 points (sd = 15), how often
# will a study with 30 per group detect it at alpha = 0.05?
power.t.test(n = 30, delta = 5, sd = 15, sig.level = 0.05)$power
[1] 0.2450048
# How many per group would we need for 80% power?
power.t.test(delta = 5, sd = 15, sig.level = 0.05, power = 0.80)$n
[1] 142.2466

Underpowered Studies Are Doubly Dangerous

They usually miss real effects — and when they do hit significance, the estimated effect is exaggerated, because only large flukes clear the bar. 📈

One-Tailed or Two? 🔀

set.seed(301)
scores <- rnorm(30, mean = 104, sd = 15)

# Two-sided (default): "is it different from 100?"
t.test(scores, mu = 100)$p.value
[1] 0.2734048
# One-sided: "is it GREATER than 100?"
t.test(scores, mu = 100, alternative = "greater")$p.value
[1] 0.1367024

Important

Choose the direction before you see the data. Switching to a one-tailed test after a two-tailed test disappoints you is a textbook form of p-hacking. 🚨

Part 2: t-tests 🧪

Important

  • What is a t-test?
  • How do we perform a t-test in R?
  • How do we interpret the results of a t-test?

A Green Drink, a Purple Drink, and a p-value! 🍹

Important

Imagine your company bottles two drinks: green and purple. You suspect that the Green drink bottles are filled to higher levels by the green machine than the Purple machine fills its purple bottles.

Green and Purple: Form an Experiment and Collect Data! 🍹

Important

  • Suppose: Each drink comes in a bottle and we would like to know whether the green and the purple drinks are filled to the same levels.
    • Implying that that the green and purple machines are calibrated to fill the bottles to the same levels.
    • Is this true?
  • We randomly select 9 random bottles from our entire set of 100000 bottles. Note: this is actually a very small sample size, but we are just doing this for demonstration purposes!
  • Statistics is used to extrapolate our findings from the small set to the larger set of bottles.

Note

We formulate our hypotheses to test the claim that the green and purple drinks are filled to the same levels.

  • \(H_0\): The green and purple drinks are filled to the same levels (no difference).
  • \(H_1\): The green and purple drinks are filled to different levels (there is a difference).

Green and Purple: Our Selection

Important

We randomly select 9 bottles of each drink and measure the fill levels. These nine bottles are used to represent the total population of bottles, and to test our hypotheses.

The data is as follows:

  • Green drink: 102, 107, 111, 110, 112, 115, 118, 120, 122
  • Purple drink: 98, 100, 102, 102, 107, 110, 113, 104, 111

Green and Purple: The Test To Determine a Difference

t-tests to Compare Means Between Samples

  • Non-parametric “Two Sample” t-tests are used to compare the means of two independent groups.
    • Here, we have two independent groups: the green drink and the purple drink.
    • The t-test determines whether there is difference in fill levels between the two drinks is statistically significant, or if any difference could have occurred by random chance.
  • The t-test provides a p-value to inform us which hypothesis to accept or reject.

Important

  • If the p-value is less than 0.05, we reject the null hypothesis
    • Conclude that there is a statistically significant difference in fill levels between the green and purple drinks.
  • If the p-value is greater than 0.05, we fail to reject the null hypothesis
    • Conclude that there is not enough evidence to suggest a statistically significant difference in fill levels between the green and purple drinks.

Green and Purple: Running the t-test:

# Create vectors for the fill levels of each drink
green <- c(102, 107, 111, 110, 112, 115, 118, 120, 122)
purple <- c(98, 100, 102, 102, 107, 110, 113, 104, 111)

# Perform the t-test
t.test(green, purple)

    Welch Two Sample t-test

data:  green and purple
t = 2.8101, df = 15.404, p-value = 0.01293
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
  1.891901 13.663655
sample estimates:
mean of x mean of y 
 113.0000  105.2222 

A SUPER Fast View of P-Values (Again!)

Tip

  • If the p-value is small (< 0.05), we reject H₀. If the p-value is large, we fail to reject H₀.
  • If the p-value is very small (< 0.01), we have stronger evidence for Ha.

Interpretation of the p-value for Green and Purple Drinks: The p-value = 0.01293 and so we reject the null hypothesis and conclude that there is a statistically significant difference in fill levels between the green and purple drinks.

One-Sample t-test

Question: is this sample’s mean different from a specific value?

library(tidyverse)
# Is the average mpg in mtcars different from 20?
t.test(mtcars$mpg, mu = 20)

    One Sample t-test

data:  mtcars$mpg
t = 0.08506, df = 31, p-value = 0.9328
alternative hypothesis: true mean is not equal to 20
95 percent confidence interval:
 17.91768 22.26357
sample estimates:
mean of x 
 20.09062 
# mean of mtcars$mpg
mean(mtcars$mpg)
[1] 20.09062

One-Sample t-test: Visualizing the Null Distribution

library(tidyverse)
# Create a histogram of the sample data
ggplot(mtcars, aes(x = mpg)) +
  geom_histogram(bins = 10, fill = "lightblue", color = "black", alpha = 0.7) +
  geom_vline(xintercept = 20, color = "red", linetype = "dashed", size = 1) +
  geom_vline(xintercept = mean(mtcars$mpg), color = "blue", linetype = "dashed", size = 1) +
  labs(title = "Null Distribution for One-Sample t-test",
       x = "MPG",
       y = "Frequency") +
  theme_minimal()

Yet ANOTHER Two-Sample t-test (mtcars)

# Do manual cars get better mileage?
t.test(mpg ~ am, data = mtcars)

    Welch Two Sample t-test

data:  mpg by am
t = -3.7671, df = 18.332, p-value = 0.001374
alternative hypothesis: true difference in means between group 0 and group 1 is not equal to 0
95 percent confidence interval:
 -11.280194  -3.209684
sample estimates:
mean in group 0 mean in group 1 
       17.14737        24.39231 

R defaults to Welch’s t-test, which does not assume equal variances. That is the safer default — keep it. ✅

As a plot, the difference is clear

library(tidyverse)
ggplot(mtcars,
# Note, factor(am) places transmission type
# on the x-axis as a category.
       aes(factor(am), mpg, 
           fill = factor(am))) +
  geom_boxplot(alpha = 0.7) +
# geo_jitter() adds the points to box plots
  geom_jitter(width = 0.12, size = 2) +
  scale_x_discrete(
    labels = c("Automatic", "Manual")) +
  scale_fill_manual(
    values = c("#3498DB", "#E67E22")) +
  labs(x = NULL, y = "MPG") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

Paired t-test 👯

Important

A PAIRED t-TEST checks whether the mean difference between two matched or related measurements is equal to zero. Good for “before and after” studies, or when the same subjects are measured under two different conditions.

data(sleep) # 10 sleep patients, two different drugs each
#help(sleep) # for details
# WRONG type of test: treats the 20 measurements as independent
t.test(extra ~ group, data = sleep)$p.value
[1] 0.07939414
# RIGHT type of test: the same 10 patients took both drugs
# (modern R wants the two vectors, not a formula, when paired = TRUE)
pvalue <- with(sleep, t.test(extra[group == 1], extra[group == 2], paired = TRUE))$p.value
cat("The p-value for the paired t-test is: ", pvalue, "\n")
The p-value for the paired t-test is:  0.00283289 

Note

Pairing removes person-to-person variation, so it is far more sensitive. Same data, p drops from 0.079 to 0.003 — the design matters more than the test. 🎯

Cohen’s d: Effect Size for t-tests

Important

  • Calculated as the difference in means divided by the pooled standard deviation.

  • Used to interpret the practical significance of a t-test result. It helps us understand whether the observed difference between groups is meaningful in a real-world context.

See more about Cohen’s d here: https://en.wikipedia.org/wiki/Effect_size#Cohen’s_d or https://statisticsbyjim.com/basics/cohens-d/.

Chohen’s d is calculated as:

\(d\) = \(\frac{\bar{x}_1 - \bar{x}_2}{s_p}\)

Effect Size: The Missing Number 📏

# Cohen's d: the difference in standard deviation units
cohens_d <- function(x, y) {
  nx <- length(x); ny <- length(y)
  pooled_sd <- sqrt(((nx - 1) * var(x) + (ny - 1) * var(y)) / (nx + ny - 2))
  (mean(x) - mean(y)) / pooled_sd
}

manual    <- mtcars$mpg[mtcars$am == 1]
automatic <- mtcars$mpg[mtcars$am == 0]

round(cohens_d(manual, automatic), 2)
[1] 1.48
Cohen’s d Conventional label
0.2 small
0.5 medium
0.8 large

Important

Always report an effect size and a confidence interval alongside p. With n = 10,000, a meaningless difference gets p < 0.001. 📊

Checking Assumptions 🩺

#| echo: true
#| fig-width: 11
#| fig-height: 3.4

# 1. Normality of each group (matters most for small n)
shapiro.test(mtcars$mpg[mtcars$am == 1])$p.value

# 2. Equal variances (only if you use var.equal = TRUE)
var.test(mpg ~ am, data = mtcars)$p.value

# 3. Independence - a design question, not a test!

library(patchwork)
p1 <- ggplot(mtcars, aes(sample = mpg)) +
  stat_qq(color = "#2E86C1") + stat_qq_line(color = "#E74C3C") +
  labs(title = "Q-Q plot: points on the line = normal") +
  theme_minimal(base_size = 11)

p2 <- ggplot(mtcars, aes(mpg)) +
  geom_histogram(bins = 12, fill = "#3498DB", color = "white") +
  labs(title = "Histogram") + theme_minimal(base_size = 11)

p1 + p2

Warning

Normality tests are themselves underpowered at small n and hypersensitive at large n. Look at the Q-Q plot. 👀

Checking Assumptions Plot

Interactive: t-Test Builder 🎮

🧪 t-Test Builder

Describe your design and click "Generate Code"!

Knowledge Check 🧠

A study reports p = 0.04. Which statement is correct?
There is a 4% chance the null hypothesis is true
The effect is real with 96% certainty
If there were no real effect, data this extreme would occur about 4% of the time
The effect is small but statistically meaningful