flowchartDiagram = {
// Canvas setup
const width = 1100;
const height = 550;
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
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.attr("font-weight", "bold")
.text("Wilcoxon Signed-Rank Test Procedure");
// Define nodes with adjusted positions to match flowchart
const nodes = [
{id: "A", label: "Sample Data", x: padding + 100, y: 100},
{id: "B", label: "Calculate Differences\nfrom Hypothesized Value", x: padding + 300, y: 100},
{id: "C", label: "Ignore Zero\nDifferences", x: padding + 500, y: 100},
{id: "D", label: "Rank Absolute\nDifferences", x: padding + 700, y: 100},
{id: "E", label: "Assign Original Signs\nto Ranks", x: padding + 900, y: 100},
{id: "F", label: "Calculate Sum of\nPositive & Negative\nRanks", x: padding + 900, y: 200},
{id: "G", label: "Determine Test\nStatistic V", x: padding + 700, y: 200},
{id: "H", label: "Calculate p-value", x: padding + 500, y: 200},
{id: "I", label: "p-value < 0.05?", x: padding + 300, y: 200, isDecision: true},
{id: "J", label: "Reject null\nhypothesis", x: padding + 150, y: 300},
{id: "K", label: "Retain null\nhypothesis", x: padding + 450, y: 300}
];
// Define edges
const edges = [
{source: "A", target: "B", label: ""},
{source: "B", target: "C", label: ""},
{source: "C", target: "D", label: ""},
{source: "D", target: "E", label: ""},
{source: "E", target: "F", label: ""},
{source: "F", target: "G", label: ""},
{source: "G", target: "H", label: ""},
{source: "H", target: "I", label: ""},
{source: "I", target: "J", label: "Yes"},
{source: "I", target: "K", label: "No"}
];
// 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", "edge")
.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;
// Decision diamond special case
if (source.isDecision) {
if (d.label === "Yes") {
// Going down and left to J
sourceX = source.x - 30;
sourceY = source.y + 20;
targetX = target.x;
targetY = target.y - 25;
// Return path with offset to avoid overlapping
return `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
} else if (d.label === "No") {
// Going down and right to K
sourceX = source.x + 30;
sourceY = source.y + 20;
targetX = target.x;
targetY = target.y - 25;
// Return path with offset to avoid overlapping
return `M${sourceX},${sourceY} L${sourceX},${(sourceY + targetY)/2} L${targetX},${(sourceY + targetY)/2} L${targetX},${targetY}`;
}
} else if (target.y > source.y) {
// Vertical flow down
sourceX = source.x;
sourceY = source.y + 25;
targetX = target.x;
targetY = target.y - 25;
return `M${sourceX},${sourceY} L${targetX},${targetY}`;
} else {
// Horizontal flow
sourceX = source.x + 70;
sourceY = source.y;
targetX = target.x - 70;
targetY = target.y;
return `M${sourceX},${sourceY} L${targetX},${targetY}`;
}
})
.attr("stroke", "#666")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr("marker-end", "url(#arrowhead)");
// Add edge labels with better positioning
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 - 50;
} else if (d.label === "No") {
return source.x + 50;
} else {
// Horizontal edge
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 {
// Horizontal edge
return source.y - 10;
}
})
.attr("font-size", "14px")
.attr("fill", "#333")
.text(d => d.label);
// Draw nodes with fixed box sizes
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 === "J" || d.id === "K") return "#f0f0f0";
return "#b3deff";
})
.attr("stroke", d => {
if (d.id === "J" || d.id === "K") return "#999";
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 === "I" ? "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 => d.isDecision ? "#ffc107" : "#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 === "J" || d.id === "K") return "#f0f0f0";
return "#b3deff";
});
});
return svg.node();
}
Key Takeaways: One-Sample Wilcoxon Signed-Rank Test
Tip
- Purpose: Non-parametric alternative to the one-sample t-test for comparing a sample median to a hypothesized value
- When to use: When data doesn’t meet normality assumptions or contains outliers
- Data requirements: At least 5 observations recommended; works with ordinal data
- Null hypothesis: The sample median equals the hypothesized value
- Interpretation: If p < 0.05, there is a significant difference between the sample median and the hypothesized value
- Advantages: More robust with non-normal data; resistant to outliers; tests the median (more appropriate for skewed distributions)
What is the One-Sample Wilcoxon Signed-Rank Test?
The one-sample Wilcoxon signed-rank test is a non-parametric statistical method used to determine whether a sample median differs from a hypothesized value. It serves as a robust alternative to the one-sample t-test when your data doesn’t follow a normal distribution or when working with ordinal data.
Tip
When to use the one-sample Wilcoxon signed-rank test:
- When testing if a sample’s median differs from a specified value
- When your data doesn’t follow a normal distribution
- When working with ordinal data or ranked measurements
- When your sample contains outliers that would skew a t-test
- When you have a small sample size and cannot verify normality
This online calculator allows you to quickly perform a one-sample Wilcoxon signed-rank 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: 1400
library(shiny)
library(bslib)
library(ggplot2)
library(bsicons)
library(vroom)
library(shinyjs)
ui <- page_sidebar(
title = "One-Sample Wilcoxon Test",
useShinyjs(), # Enable shinyjs for resetting inputs
sidebar = sidebar(
width = 400,
card(
card_header("Data Input"),
accordion(
accordion_panel(
"Manual Input",
textAreaInput("sample_input", "Sample Data [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(
selectInput("sample_var", "Sample variable:", choices = NULL),
actionButton("clear_file", "Clear File", class = "btn-danger btn-sm")
)
)
),
id = "input_method",
open = 1
),
# Advanced Options accordion
accordion(
accordion_panel(
"Advanced Options",
card(
card_header("Null Hypothesis:"),
card_body(
tags$div(style = "margin-bottom: 10px;",
numericInput("null_hypothesis", "mu =", 0.0, width = "100%")
)
)
),
card(
card_header("Alternative Hypothesis:"),
card_body(
radioButtons("alternative", NULL,
choices = c("Two-sided" = "two.sided",
"mu < specified value" = "less",
"mu > specified value" = "greater"),
selected = "two.sided")
)
),
card(
card_header("Type of Test:"),
card_body(
radioButtons("method", NULL,
choices = c("Default" = "default",
"Exact" = "exact",
"Normal approximation" = "normal",
"Normal approximation with continuity correction" = "correct"),
selected = "default")
)
)
),
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 one-sample Wilcoxon test compares the median of a sample to a hypothesized value."),
tags$li(tags$b("Null hypothesis:"), " The sample median equals the specified value."),
tags$li(tags$b("Alternative:"), " The sample median differs from (or is less/greater than) the specified value."),
tags$li("If p-value < 0.05, there is a significant difference between the sample median and the hypothesized value.")
)
)
)
)
),
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("Explanation", div(style = "font-size: 0.9rem;",
p("The Wilcoxon Signed-Rank Test is a non-parametric alternative to the one-sample t-test:"),
tags$ul(
tags$li("It does not assume normality, making it more robust than the t-test."),
tags$li("The test ranks the absolute differences between each observation and the hypothesized median."),
tags$li("Signs of the original differences are assigned to the ranks, and the test statistic is calculated."),
tags$li("A small p-value indicates the sample median likely differs from the hypothesized value.")
)
))
)
)
),
card(
card_header("Visual Assessment"),
card_body(
navset_tab(
nav_panel("Histogram",
navset_tab(
nav_panel("Plot", plotOutput("histogram")),
nav_panel("Explanation", div(style = "font-size: 0.9rem;",
p("The histogram shows the distribution of your sample:"),
tags$ul(
tags$li("The vertical dashed line shows the sample median."),
tags$li("The vertical dotted line shows the hypothesized median value."),
tags$li("Comparing these lines helps you visualize the difference being tested.")
)
))
)
),
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 your sample:"),
tags$ul(
tags$li("The box represents the interquartile range (IQR), with the median shown as a horizontal line."),
tags$li("Whiskers extend to the smallest and largest values within 1.5 times the IQR."),
tags$li("The horizontal dotted line shows the hypothesized median value."),
tags$li("Points outside the whiskers are potential outliers.")
)
))
)
),
nav_panel("Differences",
navset_tab(
nav_panel("Plot", plotOutput("differences")),
nav_panel("Explanation", div(style = "font-size: 0.9rem;",
p("The difference plot shows:"),
tags$ul(
tags$li("Each observation's difference from the hypothesized value."),
tags$li("Positive values are above the hypothesized median, negative values are below."),
tags$li("The horizontal dotted line at 0 represents no difference from the hypothesized value."),
tags$li("This plot helps visualize the direction and magnitude of differences.")
)
))
)
)
)
)
)
)
)
server <- function(input, output, session) {
# Example data
example_data <- "8.5\n7.2\n12.4\n10.8\n9.3\n6.7\n11.5\n8.9\n10.2\n7.8"
# Track input method
input_method <- reactiveVal("manual")
# Function to clear file inputs
clear_file_inputs <- function() {
updateSelectInput(session, "sample_var", choices = NULL)
reset("file_upload")
}
# Function to clear text inputs
clear_text_inputs <- function() {
updateTextAreaInput(session, "sample_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, "sample_input", value = example_data)
})
# 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 input changes, clear file inputs if it has content
observeEvent(input$sample_input, {
if (!is.null(input$sample_input) && nchar(input$sample_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, "sample_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(na.omit(numeric_values))
}
# Get sample values
sample_values <- reactive({
if (input_method() == "file" && !is.null(file_data()) && !is.null(input$sample_var)) {
df <- file_data()
return(na.omit(df[[input$sample_var]]))
} else {
return(parse_text_input(input$sample_input))
}
})
# Get mu value
mu_value <- reactive({
return(input$null_hypothesis)
})
# Validate input data
validate_data <- reactive({
sample <- sample_values()
if (is.null(sample)) {
return("Error: Please check your input. Make sure all values are numeric.")
}
if (length(sample) < 5) {
return("Error: At least 5 observations are recommended for the Wilcoxon test.")
}
# Check if all values are equal to mu (test isn't appropriate)
if (all(sample == mu_value())) {
return("Error: All values equal the null hypothesis value. The Wilcoxon test is not applicable.")
}
# Check if values after subtracting mu contain zeros (warn about ties)
zeroes <- sum(sample == mu_value())
if (zeroes > 0) {
return(paste0("Warning: ", zeroes, " value(s) equal the null hypothesis value and will be excluded from the test."))
}
return(NULL)
})
output$error_message <- renderUI({
error <- validate_data()
if (!is.null(error) && input$run_test > 0) {
if (startsWith(error, "Warning")) {
div(class = "alert alert-warning", error)
} else {
div(class = "alert alert-danger", error)
}
}
})
# Run the Wilcoxon test
test_result <- eventReactive(input$run_test, {
error <- validate_data()
if (!is.null(error) && startsWith(error, "Error")) return(NULL)
# Fix the issue with exact and correct parameters
# Get the method selected by the user
method_choice <- input$method
# Set parameters based on method
if (method_choice == "default") {
# For default method, let R decide based on sample size
wilcox.test(
sample_values(),
mu = mu_value(),
alternative = input$alternative
)
} else if (method_choice == "exact") {
# For exact method
wilcox.test(
sample_values(),
mu = mu_value(),
alternative = input$alternative,
exact = TRUE,
correct = FALSE
)
} else if (method_choice == "normal") {
# For normal approximation without correction
wilcox.test(
sample_values(),
mu = mu_value(),
alternative = input$alternative,
exact = FALSE,
correct = FALSE
)
} else if (method_choice == "correct") {
# For normal approximation with correction
wilcox.test(
sample_values(),
mu = mu_value(),
alternative = input$alternative,
exact = FALSE,
correct = TRUE
)
}
})
# Display test results
output$test_results <- renderPrint({
if (is.null(test_result())) return(NULL)
result <- test_result()
sample <- sample_values()
# Use alternative input for interpretation
alt_text <- switch(
input$alternative,
"two.sided" = "different from",
"less" = "less than",
"greater" = "greater than"
)
cat("One-Sample Wilcoxon Signed Rank Test Results:\n")
cat("============================================\n\n")
cat("V statistic:", result$statistic, "\n")
# For normal approximation, show Z score
if (input$method %in% c("normal", "correct") || (input$method == "default" && length(sample) >= 50)) {
# Calculate Z score manually (approximate)
n_effective <- sum(sample != mu_value())
V <- as.numeric(result$statistic)
# Expected value and standard deviation under H0
expected_V <- n_effective * (n_effective + 1) / 4
sd_V <- sqrt(n_effective * (n_effective + 1) * (2 * n_effective + 1) / 24)
# Z score calculation
correction <- if(input$method == "correct" || (input$method == "default" && length(sample) >= 50)) 0.5 else 0
Z <- (V - expected_V - correction) / sd_V
cat("Z score (approximation):", round(Z, 4), "\n")
}
cat("p-value:", round(result$p.value, 6), "\n\n")
cat("Sample Summary:\n")
cat("---------------\n")
cat("Sample size:", length(sample), "\n")
cat("Sample median:", round(median(sample), 4), "\n")
cat("Sample mean:", round(mean(sample), 4), "\n")
cat("First quartile (Q1):", round(quantile(sample, 0.25), 4), "\n")
cat("Third quartile (Q3):", round(quantile(sample, 0.75), 4), "\n\n")
cat("Test Information:\n")
cat("-----------------\n")
cat("Hypothesized median:", mu_value(), "\n")
cat("Alternative hypothesis: true location is", alt_text, mu_value(), "\n")
cat("Method:", switch(input$method,
"default" = "automatic (exact for small samples, normal approximation for large)",
"exact" = "exact",
"normal" = "normal approximation",
"correct" = "normal approximation with continuity correction"), "\n\n")
cat("Conclusion:\n")
cat("-----------\n")
if (result$p.value < 0.05) {
cat("There is a significant difference (p < 0.05).\n")
cat("The sample median is significantly", alt_text, "the hypothesized value of", mu_value(), ".\n")
} else {
cat("Interpretation: No significant difference detected (p ≥ 0.05).\n")
cat("We cannot conclude that the sample median is", alt_text, "the hypothesized value of", mu_value(), ".\n")
}
})
# Generate histogram
output$histogram <- renderPlot({
req(input$run_test > 0, !is.null(sample_values()))
sample <- sample_values()
ggplot(data.frame(value = sample), aes(x = value)) +
geom_histogram(bins = min(30, max(10, length(sample)/3)),
fill = "#5dade2", color = "#2874a6", alpha = 0.7) +
geom_vline(xintercept = median(sample), linetype = "dashed",
color = "#c0392b", size = 1.5) +
geom_vline(xintercept = mu_value(), linetype = "dotted",
color = "#8e44ad", size = 1.5) +
annotate("text", x = median(sample), y = 0,
label = paste("Median =", round(median(sample), 2)),
vjust = -1, hjust = ifelse(median(sample) > mu_value(), 1.1, -0.1),
color = "#c0392b", fontface = "bold") +
annotate("text", x = mu_value(), y = 0,
label = paste("H₀ =", mu_value()),
vjust = -3, hjust = ifelse(mu_value() > median(sample), 1.1, -0.1),
color = "#8e44ad", fontface = "bold") +
labs(title = "Distribution of Sample Values",
subtitle = "With sample median and hypothesized value",
x = "Value",
y = "Frequency") +
theme_minimal(base_size = 14)
})
# Generate boxplot
output$boxplot <- renderPlot({
req(input$run_test > 0, !is.null(sample_values()))
sample <- sample_values()
# Create data frame for boxplot
df <- data.frame(value = sample)
# Calculate summary statistics for annotation
sample_median <- median(sample)
ggplot(df, aes(y = value, x = factor(1))) +
geom_boxplot(fill = "#5dade2", width = 0.5, alpha = 0.7, outlier.color = "#e74c3c") +
geom_jitter(width = 0.1, alpha = 0.5, color = "#34495e") +
geom_hline(yintercept = mu_value(), linetype = "dotted",
color = "#8e44ad", size = 1.5) +
annotate("text", y = mu_value(), x = 1.5,
label = paste("H₀ =", mu_value()),
hjust = 0, vjust = ifelse(mu_value() > sample_median, -0.5, 1.5),
color = "#8e44ad", fontface = "bold") +
labs(title = "Boxplot of Sample Values",
subtitle = "With hypothesized value",
y = "Value") +
theme_minimal(base_size = 14) +
theme(axis.title.x = element_blank(),
axis.text.x = element_blank(),
axis.ticks.x = element_blank())
})
# Generate differences plot
output$differences <- renderPlot({
req(input$run_test > 0, !is.null(sample_values()))
sample <- sample_values()
mu <- mu_value()
# Calculate differences from hypothesized value
diffs <- sample - mu
# Create data frame for difference plot
df <- data.frame(
index = 1:length(diffs),
difference = diffs
)
# Determine color based on difference
df$color <- ifelse(df$difference > 0, "Positive", "Negative")
ggplot(df, aes(x = index, y = difference, fill = color)) +
geom_col(width = 0.7, alpha = 0.8) +
geom_hline(yintercept = 0, linetype = "dotted",
color = "black", size = 1) +
scale_fill_manual(values = c("Positive" = "#2ecc71", "Negative" = "#e74c3c")) +
labs(title = "Differences from Hypothesized Value",
subtitle = paste("Value - H₀ (", mu, ")", sep = ""),
x = "Observation Index",
y = "Difference") +
theme_minimal(base_size = 14) +
theme(legend.title = element_blank(),
legend.position = "top")
})
}
shinyApp(ui = ui, server = server)
One-Sample Wilcoxon Test vs. One-Sample t-Test
When should you choose the Wilcoxon signed-rank test over the traditional t-test?
Feature | Wilcoxon Signed-Rank Test | One-Sample t-Test |
---|---|---|
Distribution assumptions | No normality assumption | Assumes normal distribution |
Central tendency measured | Tests median | Tests mean |
Resistance to outliers | Highly resistant | Sensitive to outliers |
Efficiency | Slightly less powerful when data is normal | More powerful with normal data |
Data types | Works with ordinal data | Requires interval/ratio data |
Small samples | Appropriate for small non-normal samples | Less reliable with small non-normal samples |
The one-sample Wilcoxon signed-rank test is generally recommended when:
- Your data shows clear deviation from normality (asymmetry, outliers, etc.)
- You’re working with small samples where normality is uncertain
- Your research question specifically concerns the median rather than the mean
- You’re analyzing ranked or ordinal data
How the One-Sample Wilcoxon Signed-Rank Test Works
The one-sample Wilcoxon signed-rank test works by analyzing the ranks of the absolute differences between each observation and the hypothesized value:
Mathematical Procedure
- Calculate differences: For each observation, calculate the difference between the observed value and the hypothesized value (\(d_i = x_i - \mu_0\))
- Exclude zeros: Remove any observations where the difference is exactly zero
- Rank absolute differences: Rank the absolute values of the differences from smallest to largest
- Assign signs: Attach the original sign of each difference to its rank
- Calculate rank sums: Calculate the sum of positive ranks (\(V^+\)) and the sum of negative ranks (\(V^-\))
- Determine test statistic: The test statistic \(V\) is typically the sum of positive ranks
- Calculate p-value:
- For small samples (\(n < 10\)): Use exact probability distribution
- For larger samples: Use normal approximation with
- Mean = \(n(n+1)/4\)
- Standard Deviation = \(\sqrt{n(n+1)(2n+1)/24}\)
Assumptions of the One-Sample Wilcoxon Signed-Rank Test
The test has fewer assumptions than parametric tests:
- Random sample: The data should be a random sample from the population
- Independence: The observations should be independent of each other
- Continuous variable: The variable should be continuous (or at least ordinal)
- Symmetry: The distribution of differences should be approximately symmetric around the median (not required for large samples)
Statistical Power Considerations
Important
Statistical Power Note: The one-sample Wilcoxon signed-rank test has approximately 95% of the power of the t-test when the t-test’s assumptions are perfectly met. However, when those assumptions are violated (non-normality, outliers), the Wilcoxon test often has substantially greater power.
For optimal statistical power:
- Use at least 10-15 observations when possible
- Consider the exact test for small samples
- With highly skewed data, the Wilcoxon test is almost always more powerful than the t-test
Example 1: Product Weight Quality Control
A manufacturer needs to verify if their product meets the specified weight of 100 grams. They randomly select 12 products from the production line and weigh them.
Data (weights in grams): 96.5, 102.8, 97.3, 101.5, 99.8, 103.2, 98.7, 104.1, 97.9, 100.2, 103.8, 99.5
Analysis Steps:
State hypotheses:
- \(H_0\): The median product weight equals 100 grams
- \(H_1\): The median product weight differs from 100 grams
Calculate differences from hypothesized value (100): -3.5, 2.8, -2.7, 1.5, -0.2, 3.2, -1.3, 4.1, -2.1, 0.2, 3.8, -0.5
Rank absolute differences:
Value Difference Absolute Difference Rank Signed Rank 96.5 -3.5 3.5 10 -10 102.8 2.8 2.8 8 8 97.3 -2.7 2.7 7 -7 101.5 1.5 1.5 5 5 99.8 -0.2 0.2 1 -1 103.2 3.2 3.2 9 9 98.7 -1.3 1.3 4 -4 104.1 4.1 4.1 12 12 97.9 -2.1 2.1 6 -6 100.2 0.2 0.2 1 1 103.8 3.8 3.8 11 11 99.5 -0.5 0.5 3 -3 Calculate sum of signed ranks:
- Sum of positive ranks: \(V^+ = 8 + 5 + 9 + 12 + 1 + 11 = 46\)
- Sum of negative ranks: \(V^- = -10 + (-7) + (-1) + (-4) + (-6) + (-3) = -31\)
- Test statistic \(V = 46\) (sum of positive ranks)
Calculate p-value:
- For \(n = 12\), using the normal approximation
- Expected \(V\) under \(H_0 = 12(12+1)/4 = 39\)
- Standard deviation = \(\sqrt{\frac{12(12+1)(2 \times 12+1)}{24}} = 13.23\)
- \(Z = \frac{46 - 39}{13.23} = 0.53\)
- Two-tailed p-value = 0.596
Results:
- \(V = 46\), \(p = 0.596\)
- Sample median = 99.65
- Interpretation: There is no significant evidence that the median product weight differs from the specified 100 grams (\(p > 0.05\)).
How to Report: “A one-sample Wilcoxon signed-rank test indicated that the median product weight (\(\text{Mdn} = 99.65\) g) did not significantly differ from the specified 100 g (\(V = 46\), \(p = 0.596\)), suggesting the production process is meeting the weight specifications.”
Example 2: Patient Recovery Time
A hospital wants to determine if a new treatment protocol has reduced patient recovery time below the historical median of 14 days.
Data (recovery time in days): 10, 12, 9, 15, 8, 11, 7, 13, 10, 9, 12, 8, 11, 10, 9
Results:
- \(V = 18\), \(p = 0.006\)
- Sample median = 10
- Interpretation: There is significant evidence that the median recovery time is less than the historical value of 14 days (\(p < 0.05\)).
How to Report: “A one-sample Wilcoxon signed-rank test showed that the median recovery time under the new protocol (\(\text{Mdn} = 10\) days) was significantly less than the historical median of 14 days (\(V = 18\), \(p = 0.006\)), indicating the new treatment protocol effectively reduces patient recovery time.”
How to Report One-Sample Wilcoxon Test Results
When reporting the results of a one-sample Wilcoxon signed-rank test in academic papers or research reports, include the following elements:
[value]$) was significantly [higher/lower/different from]
"The sample median ($\text{Mdn} = [test statistic]$, $p = [p-value]$)." the hypothesized value of $\mu_0$ ($V =
For example:
"The sample median ($\text{Mdn} = 25.5$) was significantly higher than the hypothesized value of 20 ($V = 45$, $n = 15$, $p = 0.008$)."
Additional information to consider including: - Sample size after excluding ties (\(n\)) - Effect size (\(r = Z/\sqrt{N}\) where \(Z\) is the standardized test statistic) - Confidence interval for the median (if available) - Whether the test was one-tailed or two-tailed - Whether exact or approximate methods were used
APA Style Reporting
For APA style papers (7th edition), report the one-sample Wilcoxon signed-rank test results as follows:
[variable]
We conducted a one-sample Wilcoxon signed-rank test to examine whether the median [hypothesized value]. Results indicated that the median [variable] ($\text{Mdn} = [value]$)
differed from [higher/lower than/different from] the hypothesized value of $\mu_0$
was significantly [test statistic]$, $z = [z-value if available]$, $p = [exact p-value]$, $r = [effect size]$). ($V =
Reporting in Tables
When reporting multiple one-sample Wilcoxon test results in a table, include these columns: - Variable tested - Hypothesized value (\(\mu_0\)) - Sample median - \(V\) statistic - \(Z\) statistic (if using normal approximation) - p-value - Effect size - Sample size
Test Your Understanding
- When is the one-sample Wilcoxon signed-rank test most appropriate?
- When comparing paired measurements from the same subjects
- When comparing a sample median to a hypothesized value with non-normal data
- When comparing two independent groups
- When testing for correlation between variables
- What is the main advantage of the one-sample Wilcoxon test over the one-sample t-test?
- It always provides higher statistical power
- It can be used with categorical data
- It doesn’t require the assumption of normality
- It can test multiple hypotheses simultaneously
- What does the one-sample Wilcoxon signed-rank test primarily compare?
- The sample mean to a hypothesized value
- The sample median to a hypothesized value
- The sample variance to a hypothesized value
- The sample distribution shape to a reference distribution
- A researcher finds \(V = 32\), \(p = 0.02\) when testing if a sample differs from a hypothesized value. What can they conclude?
- There is no significant difference from the hypothesized value
- There is a significant difference from the hypothesized value
- The test is invalid
- More data is needed
- What happens to observations that exactly equal the hypothesized value in the Wilcoxon test?
- They are assigned a rank of zero
- They are excluded from the analysis
- They are averaged with other values
- They are treated as positive differences
Answers: 1-B, 2-C, 3-B, 4-B, 5-B
Common Questions About the One-Sample Wilcoxon Test
What’s the difference between the one-sample Wilcoxon test and the paired Wilcoxon test?
The one-sample Wilcoxon test compares a single sample’s median to a hypothesized value, while the paired Wilcoxon test compares two related measurements from the same subjects. They use the same underlying statistical method (signed-rank test), but in the paired test, the differences are calculated between matched pairs rather than between each observation and a fixed value.
How does the test handle observations that exactly equal the hypothesized value?
Observations that exactly equal the hypothesized value result in a difference of zero and are excluded from the analysis. The effective sample size becomes the number of non-zero differences. If you have many such values, this could reduce the power of your test.
What’s the exact vs. normal approximation method for the Wilcoxon test?
For small samples (typically \(n < 50\)), an exact method calculates the precise probability distribution of the test statistic. For larger samples, a normal approximation is used, which estimates the p-value based on the assumption that the test statistic approximately follows a normal distribution. The continuity correction improves this approximation.
How do I report one-sample Wilcoxon test results in research papers?
Include: the \(V\) statistic, sample size, p-value, and sample median. For example: “The sample median (\(\text{Mdn} = 12.5\)) was significantly higher than the hypothesized value of 10 (\(V = 45\), \(n = 20\), \(p = .008\)).” If using normal approximation, you may also report the \(Z\) statistic.
Can I use the one-sample Wilcoxon test with very small samples?
Yes, the test can be used with small samples, though it becomes more powerful with larger sample sizes. For very small samples (\(n < 5\)), the ability to detect significant differences is limited. With very small samples, it’s advisable to use the exact version of the test rather than the normal approximation.
What is the relationship between the one-sample Wilcoxon test and the sign test?
The sign test is an even simpler non-parametric alternative that only considers the direction of differences (positive or negative) but ignores their magnitude. The Wilcoxon test is generally more powerful because it also takes into account the magnitude of the differences. Use the sign test when only the direction of differences is reliable or when the distribution of differences is highly asymmetric.
Examples of When to Use the One-Sample Wilcoxon Signed-Rank Test
- Medical research: Testing if patient pain scores differ from a clinically relevant threshold when pain is measured on an ordinal scale
- Quality control: Determining if the median lifetime of components differs from the manufacturer’s specified value
- Psychology: Testing if median response times differ from a theoretical standard
- Environmental science: Checking if a sample of pollution measurements differs from a regulatory threshold
- Market research: Analyzing if customer satisfaction ratings differ from a benchmark value
- Education: Evaluating if student performance metrics differ from a national standard
- Clinical trials: Assessing if a treatment produces a change from a standardized baseline
- Finance: Testing if investment returns differ from a market benchmark
- Manufacturing: Verifying if product attributes meet design specifications
- Sports science: Comparing athlete performance metrics to established performance standards
References
- Wilcoxon, F. (1945). Individual comparisons by ranking methods. Biometrics Bulletin, 1(6), 80-83.
- Hollander, M., & Wolfe, D. A. (1999). Nonparametric statistical methods (2nd ed.). Wiley.
- Siegel, S., & Castellan, N. J. (1988). Nonparametric statistics for the behavioral sciences (2nd ed.). McGraw-Hill.
- Conover, W. J. (1999). Practical nonparametric statistics (3rd ed.). John Wiley & Sons.
- Corder, G. W., & Foreman, D. I. (2014). Nonparametric statistics: A step-by-step approach (2nd ed.). Wiley.
- Mundry, R., & Fischer, J. (1998). Use of statistical programs for nonparametric tests of small samples often leads to incorrect P values: examples from Animal Behaviour. Animal Behaviour, 56(1), 256-259.
Reuse
Citation
BibTeX citation:
@online{kassambara2025,
author = {Kassambara, Alboukadel},
title = {One-Sample {Wilcoxon} {Signed-Rank} {Test} {Calculator}
\textbar{} {Single} {Sample} {Analysis}},
date = {2025-04-07},
url = {https://www.datanovia.com/apps/statfusion/analysis/inferential/non-parametric/one-sample/wilcoxon-signed-rank-test-single.html},
langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2025. “One-Sample Wilcoxon Signed-Rank
Test Calculator | Single Sample Analysis.” April 7, 2025. https://www.datanovia.com/apps/statfusion/analysis/inferential/non-parametric/one-sample/wilcoxon-signed-rank-test-single.html.