pairedTTestFlowchart = {
// Canvas setup
const width = 1300;
const height = 500;
const padding = 60;
// Create SVG with explicit viewBox
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 16px sans-serif;");
// Add title at the bottom
svg.append("text")
.attr("x", width / 2)
.attr("y", height - 20)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.attr("font-weight", "bold")
.text("Paired T-Test Procedure");
// Define nodes with horizontal layout
const nodes = [
// Input data
{id: "A", label: "Measurement 1", x: padding + 80, y: 150},
{id: "B", label: "Measurement 2", x: padding + 80, y: 250},
// Initial calculations
{id: "C", label: "Calculate Difference\nfor Each Pair", x: padding + 280, y: 200},
// Parallel calculations
{id: "D", label: "Calculate Mean\nof Differences", x: padding + 480, y: 150},
{id: "E", label: "Calculate Standard\nError of Differences", x: padding + 480, y: 250},
// Final path
{id: "F", label: "Calculate\nt-statistic", x: padding + 680, y: 200},
{id: "G", label: "Determine\np-value", x: padding + 880, y: 200},
{id: "H", label: "p-value < 0.05?", x: padding + 1080, y: 200, isDecision: true},
{id: "I", label: "Reject null\nhypothesis", x: padding + 990, y: 330},
{id: "J", label: "Retain null\nhypothesis", x: padding + 1170, y: 330}
];
// Define edges with draw order
const edges = [
// Draw these first (order: 1)
{source: "A", target: "C", label: "", order: 1},
{source: "B", target: "C", label: "", order: 1},
{source: "C", target: "D", label: "", order: 1},
{source: "C", target: "E", label: "", order: 1},
{source: "D", target: "F", label: "", order: 1},
{source: "E", target: "F", label: "", order: 1},
{source: "F", target: "G", label: "", order: 1},
{source: "G", target: "H", label: "", order: 1},
{source: "H", target: "I", label: "Yes", order: 1},
{source: "H", target: "J", label: "No", order: 1}
];
// Sort edges by order to control draw sequence
edges.sort((a, b) => a.order - b.order);
// Define arrow marker
svg.append("defs").append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 0 10 10")
.attr("refX", 8)
.attr("refY", 5)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 L 10 5 L 0 10 z")
.attr("fill", "#666");
// Draw edges with improved path calculation
const edgeLines = svg.selectAll("path.edge")
.data(edges)
.join("path")
.attr("class", d => `edge order-${d.order}`)
.attr("d", d => {
const source = nodes.find(n => n.id === d.source);
const target = nodes.find(n => n.id === d.target);
// Calculate connector points
let sourceX, sourceY, targetX, targetY;
let path = "";
// Special case for decision diamond
if (source.isDecision) {
if (d.label === "Yes") {
// Going down-left to I
sourceX = source.x - 25;
sourceY = source.y + 20;
targetX = target.x;
targetY = target.y - 25;
// Return path with offset to avoid overlapping
path = `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
} else if (d.label === "No") {
// Going down-right to J
sourceX = source.x + 25;
sourceY = source.y + 20;
targetX = target.x;
targetY = target.y - 25;
// Return path with offset to avoid overlapping
path = `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
}
}
// Special cases for diagonal flows - A and B to C
else if (source.id === "A" && target.id === "C") {
// A to C (diagonal down)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
else if (source.id === "B" && target.id === "C") {
// B to C (diagonal up)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
// C to D and E
else if (source.id === "C" && target.id === "D") {
// C to D (diagonal up)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
else if (source.id === "C" && target.id === "E") {
// C to E (diagonal down)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
// D and E to F
else if (source.id === "D" && target.id === "F") {
// D to F (diagonal down)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
else if (source.id === "E" && target.id === "F") {
// E to F (diagonal up)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
// Create curved path
const midX = (sourceX + targetX) / 2;
path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;
}
else if (target.y > source.y + 30) {
// General case: Vertical flow down with significant gap
sourceX = source.x;
sourceY = source.y + 25;
targetX = target.x;
targetY = target.y - 25;
path = `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
}
else if (target.y < source.y - 30) {
// General case: Vertical flow up with significant gap
sourceX = source.x;
sourceY = source.y - 25;
targetX = target.x;
targetY = target.y + 25;
path = `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
}
else {
// Horizontal flow (default)
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
path = `M${sourceX},${sourceY} L${targetX},${targetY}`;
}
return path;
})
.attr("stroke", "#666")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("marker-end", "url(#arrowhead)");
// Add edge labels for Yes/No
svg.selectAll(".edgelabel")
.data(edges.filter(d => d.label !== ""))
.join("text")
.attr("class", "edgelabel")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", d => {
const source = nodes.find(n => n.id === d.source);
const target = nodes.find(n => n.id === d.target);
if (d.label === "Yes") {
return (source.x + target.x) / 2 - 30;
} else if (d.label === "No") {
return (source.x + target.x) / 2 + 30;
} else {
return (source.x + target.x) / 2;
}
})
.attr("y", d => {
const source = nodes.find(n => n.id === d.source);
const target = nodes.find(n => n.id === d.target);
if (d.label === "Yes" || d.label === "No") {
return (source.y + target.y) / 2 - 10;
} else {
return source.y - 10;
}
})
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", d => d.label === "Yes" ? "#5a9bd5" : (d.label === "No" ? "#ff9052" : "#333"))
.text(d => d.label);
// Draw nodes with fixed box sizes - after drawing paths to ensure nodes appear on top
const node = svg.selectAll(".node")
.data(nodes)
.join("g")
.attr("class", "node")
.attr("transform", d => `translate(${d.x},${d.y})`);
// Add node shapes (rectangles or diamonds) with consistent sizing
node.each(function(d) {
const elem = d3.select(this);
if (d.isDecision) {
// Diamond for decision node
elem.append("polygon")
.attr("points", "0,-30 60,0 0,30 -60,0")
.attr("fill", "#f8d56f")
.attr("stroke", "#d4a82e")
.attr("stroke-width", 2);
} else {
// Rectangle for regular node with fixed width
const boxWidth = 140;
elem.append("rect")
.attr("x", -boxWidth/2)
.attr("y", -25)
.attr("width", boxWidth)
.attr("height", 50)
.attr("rx", 5)
.attr("ry", 5)
.attr("fill", d => {
if (d.id === "I" || d.id === "J") return "#f0f0f0";
if (d.id === "A" || d.id === "B") return "#e2f0d9"; // Light green for data inputs
return "#b3deff";
})
.attr("stroke", d => {
if (d.id === "I" || d.id === "J") return "#999";
if (d.id === "A" || d.id === "B") return "#70ad47"; // Green border for data inputs
return "#4a98e0";
})
.attr("stroke-width", 2);
}
});
// Add node labels with better text wrapping
node.append("text")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", "14px")
.attr("font-weight", d => (d.id === "H" ? "bold" : "normal"))
.attr("fill", "#333")
.each(function(d) {
const lines = d.label.split('\n');
const elem = d3.select(this);
if (lines.length === 1) {
elem.text(d.label);
} else {
lines.forEach((line, i) => {
const lineHeight = 16;
const yOffset = (i - (lines.length - 1) / 2) * lineHeight;
elem.append("tspan")
.attr("x", 0)
.attr("y", yOffset)
.text(line);
});
}
});
// Add interactivity
node.on("mouseover", function(event, d) {
d3.select(this).select("rect, polygon")
.transition()
.duration(200)
.attr("fill", d => {
if (d.isDecision) return "#ffc107";
if (d.id === "A" || d.id === "B") return "#b8e986"; // Brighter green on hover
if (d.id === "I" || d.id === "J") return "#e6e6e6";
return "#7fc9ff";
});
})
.on("mouseout", function(event, d) {
d3.select(this).select("rect, polygon")
.transition()
.duration(200)
.attr("fill", d => {
if (d.isDecision) return "#f8d56f";
if (d.id === "I" || d.id === "J") return "#f0f0f0";
if (d.id === "A" || d.id === "B") return "#e2f0d9";
return "#b3deff";
});
});
return svg.node();
}
Key Takeaways: Paired Samples t-Test
Tip
- Purpose: Compare the means of two related groups (e.g., before and after measurements)
- When to use: For repeated measures on the same subjects or matched pairs
- Assumptions: Paired observations, normally distributed differences
- Null hypothesis: The mean difference between paired observations is zero (\(H_0: \mu_d = 0\))
- Interpretation: If p < 0.05, there is a significant difference between the paired measurements
- Advantages: Higher statistical power than independent tests when analyzing paired data
- Common applications: Before-after studies, repeated measures, matched pairs designs
What is the Paired Samples t-Test?
The paired samples t-test (also called dependent t-test or repeated measures t-test) is a statistical method used to compare the means of two related groups to determine if there is a significant difference between paired observations. It’s commonly used in before-after studies, repeated measurements on the same subjects, or when analyzing matched pairs.
Tip
When to use the paired samples t-test:
- When comparing measurements taken at two different times (before/after)
- When comparing two different conditions with the same subjects
- When analyzing naturally paired or matched observations
- When you need to account for individual differences between subjects
This online calculator allows you to quickly perform a paired samples t-test, visualize your data, and interpret the results with confidence.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 1300
library(shiny)
library(bslib)
library(ggplot2)
library(bsicons)
library(vroom)
library(shinyjs)
ui <- page_sidebar(
title = "Paired Samples t-Test Calculator",
useShinyjs(), # Enable shinyjs for resetting inputs
sidebar = sidebar(
width = 400,
card(
card_header("Data Input"),
accordion(
accordion_panel(
"Manual Input",
layout_column_wrap(
width = 1/2,
style = css(grid_template_columns = "1fr 1fr"),
textAreaInput("group1_input", "Group 1 [One value per row]", rows = 8,
placeholder = "Paste values here..."),
textAreaInput("group2_input", "Group 2 [One value per row]", rows = 8,
placeholder = "Paste values here...")
),
div(
actionLink("use_example", "Use example data", style = "color:#0275d8;"),
tags$span(bs_icon("file-earmark-text"), style = "margin-left: 5px; color: #0275d8;")
)
),
accordion_panel(
"File Upload",
fileInput("file_upload", "Upload CSV or TXT file:",
accept = c("text/csv", "text/plain", ".csv", ".txt")),
checkboxInput("header", "File has header", TRUE),
conditionalPanel(
condition = "output.file_uploaded",
div(
layout_column_wrap(
width = 1/2,
style = css(grid_template_columns = "1fr 1fr"),
selectInput("group1_var", "Group 1 variable:", choices = NULL),
selectInput("group2_var", "Group 2 variable:", choices = NULL)
),
actionButton("clear_file", "Clear File", class = "btn-danger btn-sm")
)
)
),
id = "input_method",
open = 1
),
# Single "Advanced Options" accordion outside the input method accordion
accordion(
accordion_panel(
"Advanced Options",
radioButtons("alternative", tags$strong("Alternative hypothesis:"),
choices = c("Two-sided" = "two.sided",
"Mean difference < 0" = "less",
"Mean difference > 0" = "greater"),
selected = "two.sided"),
numericInput("conf_level", tags$strong("Confidence level:"),
value = 0.95, min = 0.5, max = 0.999, step = 0.01)
),
open = FALSE
),
actionButton("run_test", "Run Test", class = "btn btn-primary")
),
hr(),
card(
card_header("Interpretation"),
card_body(
div(class = "alert alert-info",
tags$ul(
tags$li("The paired t-test compares the means of two paired groups."),
tags$li(tags$b("Null hypothesis:"), " The mean difference between pairs is zero."),
tags$li(tags$b("Assumption:"), " The differences between pairs are normally distributed."),
tags$li("If p-value < 0.05, there is a significant difference between the paired groups.")
)
)
)
)
),
layout_column_wrap(
width = 1,
card(
card_header("Test Results"),
card_body(
navset_tab(
nav_panel("Results", uiOutput("error_message"), verbatimTextOutput("test_results")),
nav_panel("Assumptions",
h5("Normality of Differences"),
p("The paired t-test assumes that the differences between pairs are normally distributed."),
plotOutput("qqplot"),
verbatimTextOutput("normality_test"),
div(class = "alert alert-info mt-3",
"If p < 0.05 in the Shapiro-Wilk test, your data significantly deviates from normality. Consider using non-parametric tests like the Wilcoxon signed-rank test.")
),
nav_panel("Explanation", div(style = "font-size: 0.9rem;",
p("The paired t-test evaluates whether the mean difference between paired observations equals zero:"),
tags$ul(
tags$li("It analyzes the differences between paired measurements."),
tags$li("The test assumes that the differences are normally distributed."),
tags$li("The confidence interval provides a range of plausible values for the true mean difference.")
)
))
)
)
),
card(
card_header("Visual Assessment"),
card_body(
navset_tab(
nav_panel("Boxplot",
navset_tab(
nav_panel("Plot", plotOutput("boxplot")),
nav_panel("Explanation", div(style = "font-size: 0.9rem;",
p("The boxplot shows the distribution of each group:"),
tags$ul(
tags$li("The box represents the interquartile range (IQR) with the mean shown as a diamond."),
tags$li("The horizontal line in the box represents the median."),
tags$li("Points outside the whiskers are potential outliers.")
)
))
)
),
nav_panel("Difference Plot",
navset_tab(
nav_panel("Plot", plotOutput("diffplot")),
nav_panel("Explanation", div(style = "font-size: 0.9rem;",
p("The difference plot shows the distribution of paired differences:"),
tags$ul(
tags$li("The histogram shows the frequency of difference values."),
tags$li("The curved line shows the theoretical normal distribution."),
tags$li("The dotted vertical line at 0 represents no difference between groups.")
)
))
)
)
)
)
)
)
)
server <- function(input, output, session) {
# Example data for the two groups
example_data1 <- "14.2\n15.8\n12.6\n16.3\n13.9\n15.1\n14.7\n13.5\n16.9\n14.8"
example_data2 <- "12.1\n13.2\n10.6\n14.5\n11.8\n13.0\n12.2\n11.5\n14.9\n12.3"
# Track input method
input_method <- reactiveVal("manual")
# Function to clear file inputs
clear_file_inputs <- function() {
updateSelectInput(session, "group1_var", choices = NULL)
updateSelectInput(session, "group2_var", choices = NULL)
reset("file_upload")
}
# Function to clear text inputs
clear_text_inputs <- function() {
updateTextAreaInput(session, "group1_input", value = "")
updateTextAreaInput(session, "group2_input", value = "")
}
# When example data is used, clear file inputs and set text inputs
observeEvent(input$use_example, {
input_method("manual")
clear_file_inputs()
updateTextAreaInput(session, "group1_input", value = example_data1)
updateTextAreaInput(session, "group2_input", value = example_data2)
})
# When file is uploaded, clear text inputs and set file method
observeEvent(input$file_upload, {
if (!is.null(input$file_upload)) {
input_method("file")
clear_text_inputs()
}
})
# When clear file button is clicked, clear file and set manual method
observeEvent(input$clear_file, {
input_method("manual")
clear_file_inputs()
})
# When text inputs change, clear file inputs if they have content
observeEvent(input$group1_input, {
if (!is.null(input$group1_input) && nchar(input$group1_input) > 0) {
input_method("manual")
clear_file_inputs()
}
}, ignoreInit = TRUE)
observeEvent(input$group2_input, {
if (!is.null(input$group2_input) && nchar(input$group2_input) > 0) {
input_method("manual")
clear_file_inputs()
}
}, ignoreInit = TRUE)
file_data <- reactive({
req(input$file_upload)
tryCatch({
vroom::vroom(input$file_upload$datapath, delim = NULL, col_names = input$header, show_col_types = FALSE)
}, error = function(e) {
showNotification(paste("File read error:", e$message), type = "error")
NULL
})
})
observe({
df <- file_data()
if (!is.null(df)) {
num_vars <- names(df)[sapply(df, is.numeric)]
updateSelectInput(session, "group1_var", choices = num_vars)
updateSelectInput(session, "group2_var", choices = num_vars)
}
})
output$file_uploaded <- reactive({
!is.null(input$file_upload)
})
outputOptions(output, "file_uploaded", suspendWhenHidden = FALSE)
# Function to parse text input
parse_text_input <- function(text) {
if (is.null(text) || text == "") return(NULL)
input_lines <- strsplit(text, "\\r?\\n")[[1]]
input_lines <- input_lines[input_lines != ""]
numeric_values <- suppressWarnings(as.numeric(input_lines))
if (all(is.na(numeric_values))) return(NULL)
return(numeric_values)
}
# Create a data frame with paired observations
paired_data <- reactive({
if (input_method() == "file" && !is.null(file_data()) &&
!is.null(input$group1_var) && !is.null(input$group2_var)) {
df <- file_data()
# Create a paired data frame with both variables
paired_df <- data.frame(
group1 = df[[input$group1_var]],
group2 = df[[input$group2_var]]
)
# Remove any rows with NA in either column
return(na.omit(paired_df))
} else {
g1 <- parse_text_input(input$group1_input)
g2 <- parse_text_input(input$group2_input)
if (is.null(g1) || is.null(g2)) return(NULL)
# Match lengths for pairing (use shorter length if needed)
min_length <- min(length(g1), length(g2))
paired_df <- data.frame(
group1 = g1[1:min_length],
group2 = g2[1:min_length]
)
return(na.omit(paired_df))
}
})
# Extract group values from paired data
group1_values <- reactive({
pd <- paired_data()
if (is.null(pd)) return(NULL)
return(pd$group1)
})
group2_values <- reactive({
pd <- paired_data()
if (is.null(pd)) return(NULL)
return(pd$group2)
})
# Calculate differences for paired test
diff_values <- reactive({
pd <- paired_data()
if (is.null(pd)) return(NULL)
return(pd$group1 - pd$group2)
})
# Validate input data
validate_data <- reactive({
pd <- paired_data()
if (is.null(pd)) {
return("Error: Please check your input. Make sure all values are numeric.")
}
if (nrow(pd) < 2) {
return("Error: At least 2 pairs are required for the paired t-test.")
}
return(NULL)
})
output$error_message <- renderUI({
error <- validate_data()
if (!is.null(error) && input$run_test > 0) {
div(class = "alert alert-danger", error)
}
})
# Run the t-test
test_result <- eventReactive(input$run_test, {
error <- validate_data()
if (!is.null(error)) return(NULL)
result <- t.test(
group1_values(),
group2_values(),
paired = TRUE,
alternative = input$alternative,
conf.level = input$conf_level
)
# Calculate Cohen's d for paired data
diffs <- diff_values()
d <- mean(diffs) / sd(diffs)
# Add effect size to the result
result$cohens_d <- d
return(result)
})
# Run Shapiro-Wilk test for normality on differences
normality_test <- eventReactive(input$run_test, {
diffs <- diff_values()
if (is.null(diffs)) return(NULL)
shapiro.test(diffs)
})
# Display test results
output$test_results <- renderPrint({
if (is.null(test_result())) return(NULL)
result <- test_result()
# Format the main test results
cat("PAIRED SAMPLES T-TEST\n")
cat("==========================\n\n")
# Group statistics
cat("Group Statistics:\n")
cat("-----------------\n")
cat(sprintf("Group 1:\n"))
cat(sprintf(" n = %d, Mean = %.4f, SD = %.4f\n\n",
length(group1_values()),
mean(group1_values()),
sd(group1_values())))
cat(sprintf("Group 2:\n"))
cat(sprintf(" n = %d, Mean = %.4f, SD = %.4f\n\n",
length(group2_values()),
mean(group2_values()),
sd(group2_values())))
# Differences
diffs <- diff_values()
cat(sprintf("Paired Differences (Group 1 - Group 2):\n"))
cat(sprintf(" Mean = %.4f, SD = %.4f\n\n",
mean(diffs),
sd(diffs)))
cat(sprintf("%.1f%% Confidence Interval: [%.4f, %.4f]\n\n",
input$conf_level * 100,
result$conf.int[1],
result$conf.int[2]))
# Test statistics
cat("Test Results:\n")
cat("-------------\n")
cat(sprintf("Test: Paired samples t-test\n"))
cat(sprintf("t = %.4f, df = %.2f, p-value = %.6f\n\n",
result$statistic,
result$parameter,
result$p.value))
# Effect size
cat("Effect Size:\n")
cat("-----------\n")
cat(sprintf("Cohen's d = %.4f\n", result$cohens_d))
effect_size <- if(abs(result$cohens_d) < 0.2) {
"very small"
} else if(abs(result$cohens_d) < 0.5) {
"small"
} else if(abs(result$cohens_d) < 0.8) {
"medium"
} else {
"large"
}
cat(sprintf("Interpretation: %s effect\n\n", effect_size))
# Alternative hypothesis
alt_text <- switch(
input$alternative,
"two.sided" = "different from",
"less" = "less than",
"greater" = "greater than"
)
# Conclusion
cat("Conclusion:\n")
cat("-----------\n")
if(result$p.value < 0.05) {
cat(sprintf("At the 5%% significance level, we reject the null hypothesis.\n"))
cat(sprintf("The mean of Group 1 is significantly %s the mean of Group 2.\n", alt_text))
} else {
cat(sprintf("At the 5%% significance level, we fail to reject the null hypothesis.\n"))
cat(sprintf("We cannot conclude that the mean of Group 1 is %s the mean of Group 2.\n", alt_text))
}
})
# Display normality test results
output$normality_test <- renderPrint({
req(input$run_test > 0, !is.null(normality_test()))
test_result <- normality_test()
cat("Shapiro-Wilk Normality Test for Paired Differences:\n\n")
cat("W statistic:", round(test_result$statistic, 4), "\n")
cat("p-value:", round(test_result$p.value, 6), "\n\n")
if (test_result$p.value < 0.05) {
cat("Interpretation: The paired differences significantly deviate from normality (p < 0.05).\n")
cat("The assumption of normality for the paired t-test may be violated.\n")
cat("Consider using a non-parametric alternative like the Wilcoxon signed-rank test.\n")
} else {
cat("Interpretation: No significant deviation from normality detected (p ≥ 0.05).\n")
cat("The assumption of normality for the paired t-test appears to be satisfied.\n")
}
})
# Generate boxplot
output$boxplot <- renderPlot({
req(input$run_test > 0, !is.null(test_result()))
# Prepare data for ggplot
g1 <- group1_values()
g2 <- group2_values()
res <- test_result()
p_value <- res$p.value
df <- data.frame(
Value = c(g1, g2),
Group = factor(rep(c("Group 1", "Group 2"), c(length(g1), length(g2))))
)
means <- aggregate(Value ~ Group, data = df, FUN = mean)
ggplot(df, aes(x = Group, y = Value, fill = Group)) +
geom_boxplot(alpha = 0.7) +
geom_jitter(width = 0.2, alpha = 0.5) +
geom_point(data = means, aes(y = Value), shape = 23, size = 4, fill = "white") +
scale_fill_manual(values = c("Group 1" = "#5dade2", "Group 2" = "#ff7f0e")) +
theme_minimal(base_size = 14) +
labs(title = "Comparison of Group Values",
subtitle = paste("T-test:", ifelse(p_value < 0.001, "p < 0.001", paste("p =", round(p_value, 3)))),
caption = "Diamond symbols represent group means",
y = "Value") +
theme(
plot.subtitle = element_text(face = "italic"),
legend.position = "none"
)
})
# Generate difference plot
output$diffplot <- renderPlot({
req(input$run_test > 0, !is.null(test_result()))
diffs <- diff_values()
# Calculate mean and standard deviation
mean_diff <- mean(diffs)
sd_diff <- sd(diffs)
# Create a data frame of differences
df <- data.frame(Difference = diffs)
# Calculate normal density curve x-values
x_range <- seq(min(diffs) - sd_diff, max(diffs) + sd_diff, length.out = 100)
# Calculate normal density curve y-values
normal_y <- dnorm(x_range, mean = mean_diff, sd = sd_diff)
# Create normal curve data frame
normal_df <- data.frame(x = x_range, y = normal_y)
ggplot(df, aes(x = Difference)) +
geom_histogram(aes(y = after_stat(density)), bins = min(20, max(5, length(diffs)/2)),
fill = "#5dade2", color = "#1f618d", alpha = 0.7) +
geom_line(data = normal_df, aes(x = x, y = y),
color = "#e74c3c", linewidth = 1) +
geom_vline(xintercept = mean_diff, color = "black",
linewidth = 1, linetype = "dashed") +
geom_vline(xintercept = 0, color = "#7d3c98",
linewidth = 1, linetype = "dotted") +
annotate("text", x = mean_diff, y = max(normal_y) * 0.2,
label = paste("Mean =", round(mean_diff, 2)),
hjust = -0.2, vjust = -0.5,
fontface = "bold", color = "black") +
labs(title = "Distribution of Paired Differences (Group 1 - Group 2)",
subtitle = "With normal curve overlay",
x = "Difference Value",
y = "Density") +
theme_minimal(base_size = 14)
})
# Generate Q-Q plot
output$qqplot <- renderPlot({
req(input$run_test > 0, !is.null(test_result()))
diffs <- diff_values()
qqnorm_data <- qqnorm(diffs, plot.it = FALSE)
qq_df <- data.frame(x = qqnorm_data$x, y = qqnorm_data$y)
# Create line
slope <- sd(diffs)
intercept <- mean(diffs)
ggplot(qq_df, aes(x = x, y = y)) +
geom_point(color = "#5dade2", size = 3, alpha = 0.7) +
geom_abline(slope = slope, intercept = intercept,
color = "#e74c3c", linewidth = 1) +
labs(title = "Normal Q-Q Plot of Paired Differences",
subtitle = "Points should follow the line if differences are normally distributed",
x = "Theoretical Quantiles",
y = "Sample Quantiles") +
theme_minimal(base_size = 14)
})
}
shinyApp(ui = ui, server = server)
Paired vs. Independent Samples t-Test: What’s the Difference?
It’s important to understand when to use a paired t-test versus an independent samples t-test:
Feature | Paired Samples t-Test | Independent Samples t-Test |
---|---|---|
Data structure | Paired observations (before/after, matched pairs) | Unrelated groups |
Key advantage | Controls for individual differences | Works with separate groups |
Degrees of freedom | \(n - 1\) (where \(n\) = number of pairs) | \(n_1 + n_2 - 2\) (with equal variances) |
Statistical power | Higher power for related data | Lower power for same sample size |
What it analyzes | Mean of differences between pairs | Difference between group means |
Typical applications | Before/after studies, repeated measures | Treatment vs control studies |
The paired t-test is generally more powerful than the independent t-test when dealing with related measurements because it accounts for the correlation between paired observations, reducing unexplained variability.
How the Paired Samples t-Test Works
The paired t-test analyzes the differences between paired observations:
Mathematical Procedure
Calculate the differences between each pair of observations:
\(d_i = x_{1i} - x_{2i}\)
where \(x_{1i}\) and \(x_{2i}\) are the paired observations
Calculate the mean difference:
\[\bar{d} = \frac{\sum_{i=1}^{n} d_i}{n}\]
Calculate the standard deviation of the differences:
\[s_d = \sqrt{\frac{\sum_{i=1}^{n} (d_i - \bar{d})^2}{n-1}}\]
Calculate the standard error of the mean difference:
\[SE_{\bar{d}} = \frac{s_d}{\sqrt{n}}\]
Calculate the t-statistic:
\[t = \frac{\bar{d}}{SE_{\bar{d}}}\]
Determine degrees of freedom:
\[df = n - 1\]
Calculate p-value by comparing the t-statistic to the t-distribution with \(n - 1\) degrees of freedom
Effect Size (Cohen’s d for Paired Data)
For paired data, Cohen’s d is calculated as:
\[d = \frac{|\bar{d}|}{s_d}\]
This represents the standardized mean difference in terms of the standard deviation of the differences.
Assumptions of the Paired Samples t-Test
- Paired observations: The data consists of matched pairs or repeated measurements
- Normality of differences: The differences between pairs should follow an approximately normal distribution
- With larger samples (n > 30 pairs), the t-test is robust to normality violations due to the Central Limit Theorem
- Random sampling: The pairs should represent a random sample from the population
Statistical Power Considerations
Important
Statistical Power Note: The paired t-test generally has greater statistical power than an independent t-test when there is a positive correlation between paired measurements.
To achieve 80% power (standard convention) for detecting: - Small effect (d = 0.2): Need approximately 199 pairs - Medium effect (d = 0.5): Need approximately 34 pairs - Large effect (d = 0.8): Need approximately 15 pairs
These calculations assume α = 0.05 for a two-tailed test.
Example 1: Weight Loss Program Effectiveness
A researcher wants to evaluate the effectiveness of a 12-week weight loss program. They measure the weight of 15 participants before and after the program.
Data (weight in kg):
Participant | Before Program | After Program | Difference (Before - After) |
---|---|---|---|
1 | 78.2 | 75.8 | 2.4 |
2 | 91.4 | 87.5 | 3.9 |
3 | 84.6 | 82.1 | 2.5 |
4 | 72.3 | 70.4 | 1.9 |
5 | 86.5 | 82.3 | 4.2 |
6 | 85.2 | 83.6 | 1.6 |
7 | 78.9 | 76.2 | 2.7 |
8 | 92.7 | 88.4 | 4.3 |
9 | 73.1 | 71.8 | 1.3 |
10 | 82.5 | 79.6 | 2.9 |
11 | 90.8 | 87.2 | 3.6 |
12 | 83.7 | 80.1 | 3.6 |
13 | 75.9 | 74.6 | 1.3 |
14 | 88.3 | 85.1 | 3.2 |
15 | 81.4 | 78.7 | 2.7 |
Analysis Steps:
- Check normality assumption:
- Shapiro-Wilk test on differences: p = 0.68
- p > 0.05, so we can assume normality of differences
- Calculate descriptive statistics:
- Mean before program: 83.03 kg
- Mean after program: 80.23 kg
- Mean difference: 2.81 kg
- Standard deviation of differences: 1.01 kg
- Perform paired t-test:
- t-statistic: \(t = \frac{2.81}{1.01/\sqrt{15}} = 10.77\)
- Degrees of freedom: df = 14
- p-value < 0.001
- 95% CI for mean difference: [2.25, 3.36]
- Cohen’s d = 2.78 (very large effect)
Results:
- t(14) = 10.77, p < 0.001, d = 2.78
- Mean weight before: 83.03 kg, Mean weight after: 80.23 kg
- Interpretation: There is a statistically significant reduction in weight after completing the 12-week program (p < 0.05). The mean weight loss was 2.81 kg. The effect size is very large (d > 0.8).
How to Report: “Participants showed a significant reduction in weight after completing the 12-week program (M = 80.23 kg, SD = 6.08) compared to before the program (M = 83.03 kg, SD = 6.28), t(14) = 10.77, p < 0.001, d = 2.78, 95% CI [2.25, 3.36]. The average weight loss was 2.81 kg, representing a clinically meaningful reduction.”
Example 2: Cognitive Training Effect on Memory Scores
A psychologist investigated whether a cognitive training program improves memory performance. They tested 20 participants before and after the training.
Data Summary:
- Mean score before training: 65.4 (SD = 8.7)
- Mean score after training: 72.8 (SD = 7.3)
- Mean difference (After - Before): 7.4 (SD = 5.2)
Results:
- t(19) = 6.36, p < 0.001, d = 1.42
- 95% CI for mean difference: [4.97, 9.83]
- Interpretation: There is a statistically significant improvement in memory scores after the cognitive training program (p < 0.05). The mean improvement was 7.4 points. The effect size is large (d > 0.8).
How to Report: “Memory performance was significantly higher after completing the cognitive training program (M = 72.8, SD = 7.3) compared to before training (M = 65.4, SD = 8.7), t(19) = 6.36, p < 0.001, d = 1.42, 95% CI [4.97, 9.83]. This represents a substantial improvement in memory function following training.”
How to Report Paired Samples t-Test Results
When reporting the results of a paired samples t-test in academic papers or research reports, include the following elements:
[Measurement] was significantly [higher/lower/different] in the [condition 1] condition
"[mean1], SD = [sd1]) compared to the [condition 2] condition (M = [mean2], SD = [sd2]),
(M = [df]) = [t-value], p = [p-value], d = [effect size], 95% CI [lower bound, upper bound]." t(
For example:
"Weight was significantly lower after the program (M = 80.23 kg, SD = 6.08) compared to [2.25, 3.36]." before the program (M = 83.03 kg, SD = 6.28), t(14) = 10.77, p < 0.001, d = 2.78, 95% CI
Additional information to consider including: - Mean difference between conditions - Clinical or practical significance of the difference - Whether assumptions were met (e.g., normality of differences) - Whether the test was one-tailed or two-tailed
APA Style Reporting
For APA style papers (7th edition), report the paired samples t-test results as follows:
[variable] differed between [condition 1]
We conducted a paired samples t-test to examine whether [condition 2]. Results indicated that [variable] was significantly [higher/lower] in the [condition 1]
and [mean1], SD = [sd1]) compared to the [condition 2] condition (M = [mean2], SD = [sd2]),
condition (M = [df]) = [t-value], p = [p-value], d = [effect size], 95% CI [lower, upper]. t(
Reporting in Tables
When reporting multiple paired t-test results in a table, include these columns: - Variable measured - Means and standard deviations for both conditions - Mean difference - t-value - Degrees of freedom - p-value - Effect size (Cohen’s d) - 95% confidence interval for the difference
Test Your Understanding
- What is the key feature that distinguishes a paired t-test from an independent samples t-test?
- Paired t-tests always have larger sample sizes
- Paired t-tests analyze related measurements from the same or matched subjects
- Paired t-tests always use one-tailed hypotheses
- Paired t-tests compare means instead of medians
- What is the formula for the degrees of freedom in a paired samples t-test?
- \(n - 1\), where \(n\) is the number of pairs
- \(n - 2\), where \(n\) is the number of pairs
- \(n_1 + n_2 - 2\), where \(n_1\) and \(n_2\) are the sample sizes
- \(2n - 2\), where \(n\) is the number of pairs
- A researcher finds t(24) = 2.15, p = 0.04 when comparing before and after measurements. What can they conclude?
- There is no significant difference between the measurements
- There is a significant difference between the measurements
- The test is invalid
- More data is needed
- What is the appropriate sample size (number of pairs) to detect a medium effect size (d = 0.5) with 80% power?
- Approximately 15 pairs
- Approximately 34 pairs
- Approximately 64 pairs
- Approximately 200 pairs
- Which assumption is most important for the paired samples t-test with small sample sizes?
- Equal sample sizes in both conditions
- Equal variances in both conditions
- Normal distribution of the differences between pairs
- Independence between all observations
Answers: 1-B, 2-A, 3-B, 4-B, 5-C
Common Questions About the Paired t-Test
When should I use a paired samples t-test versus an independent samples t-test?
Use a paired samples t-test when your data consists of related measurements (like before/after measurements on the same subjects). Use an independent samples t-test when comparing two separate, unrelated groups (like treatment vs. control groups with different participants).
What if my differences aren’t normally distributed?
If your sample size is large (n > 30 pairs), the t-test is generally robust to violations of normality due to the Central Limit Theorem. For smaller samples with non-normal differences, consider using the non-parametric Wilcoxon signed-rank test instead.
How do I report paired t-test results in research papers?
Include: t-value, degrees of freedom, p-value, mean difference, 95% confidence interval, and effect size (Cohen’s d for paired data). For example: “Participants showed significant improvement after treatment (M = 4.2, SD = 1.3) compared to before treatment (M = 2.6, SD = 1.1), t(24) = 7.32, p < .001, mean difference = 1.6, 95% CI [1.15, 2.05].”
What sample size do I need for adequate statistical power?
For a medium effect size (d = 0.5) with 80% power at α = 0.05, you need approximately 34 pairs. For a large effect (d = 0.8), you need about 15 pairs. For a small effect (d = 0.2), you need about 200 pairs. Power analysis tools can provide more precise estimates based on your specific requirements.
Why is the paired t-test more powerful than the independent t-test for related data?
The paired t-test is more powerful because it eliminates the variability between subjects by focusing on within-subject changes. By analyzing differences within pairs, it removes the “noise” from individual differences that would otherwise increase the error variance in an independent t-test. This is especially advantageous when there is high variability between subjects but consistent changes within subjects.
Can I use a paired t-test if some data points are missing?
If you have missing data in some pairs (e.g., missing “after” measurement for a participant), those pairs are typically excluded from the analysis. This reduces your sample size and statistical power. Some statistical software offers methods to handle missing data, such as multiple imputation, but these approaches require careful consideration of the missing data mechanism and pattern.
Examples of When to Use the Paired Samples t-Test
- Medical research: Comparing patients’ health metrics before and after treatment
- Educational interventions: Measuring student performance before and after a teaching method
- Psychology: Evaluating mood scores before and after therapy sessions
- Sports science: Comparing athletic performance before and after training programs
- Product testing: Comparing user ratings of two products by the same participants
- Nutrition studies: Measuring blood markers before and after a dietary intervention
- Pharmaceutical trials: Comparing symptoms before and after medication
- Usability testing: Comparing task completion times between two interface designs
- Environmental monitoring: Comparing pollution levels before and after remediation efforts
- Agriculture: Comparing crop yields before and after implementation of a new technique
Step-by-Step Guide to the Paired Samples t-Test
1. Check Assumptions
Before interpreting the results, verify these assumptions:
- Paired observations: The data consists of matched pairs or repeated measurements
- Normality of differences: The differences between pairs should follow an approximately normal distribution
- Check using the Q-Q plot in the ‘Assumptions’ tab
- With larger samples (n > 30 pairs), the t-test is robust to normality violations
- Random sampling: The pairs should represent a random sample from the population
2. Interpret the Results
- Check the p-value:
- If p < 0.05, there is a statistically significant difference between the paired observations
- If p ≥ 0.05, there is not enough evidence to conclude the pairs differ significantly
- Examine the confidence interval:
- If it doesn’t include zero, the difference is statistically significant
- The width indicates precision of the estimated difference
- Assess the mean difference:
- The direction and magnitude of the difference help interpret practical significance
- Consider whether the difference is meaningful in your context
References
- Student. (1908). The probable error of a mean. Biometrika, 6(1), 1-25.
- Zimmerman, D. W. (1997). A note on interpretation of the paired-samples t test. Journal of Educational and Behavioral Statistics, 22(3), 349-360.
- Dunlap, W. P., Cortina, J. M., Vaslow, J. B., & Burke, M. J. (1996). Meta-analysis of experiments with matched groups or repeated measures designs. Psychological Methods, 1(2), 170-177.
- Lakens, D. (2013). Calculating and reporting effect sizes to facilitate cumulative science: a practical primer for t-tests and ANOVAs. Frontiers in Psychology, 4, 863.
- Morris, S. B., & DeShon, R. P. (2002). Combining effect size estimates in meta-analysis with repeated measures and independent-groups designs. Psychological Methods, 7(1), 105-125.
- Baguley, T. (2012). Serious stats: A guide to advanced statistics for the behavioral sciences. Macmillan International Higher Education.
Reuse
Citation
BibTeX citation:
@online{kassambara2025,
author = {Kassambara, Alboukadel},
title = {Paired {Samples} {t-Test} {Calculator} \textbar{} {Before}
and {After} {Analysis}},
date = {2025-04-07},
url = {https://www.datanovia.com/apps/statfusion/analysis/inferential/mean-comparisons/two-sample/paired-samples-t-test.html},
langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2025. “Paired Samples t-Test Calculator |
Before and After Analysis.” April 7, 2025. https://www.datanovia.com/apps/statfusion/analysis/inferential/mean-comparisons/two-sample/paired-samples-t-test.html.