Standard Multiple Comparisons
Executive Summary

Executive Summary

Multiple-testing correction across 66 tests

Tests Evaluated
66
Invalid P-Values Dropped
0
Significant (Uncorrected)
55
Significant (Holm)
43
Significant (BH)
54
Smallest P-Value
1e-300
Of 66 tests, 55 were significant uncorrected (p < 0.05); Holm-Bonferroni keeps 43 (strict family-wise control) and Benjamini-Hochberg keeps 54 (false-discovery-rate control). 12 uncorrected finding(s) did not survive Holm — likely multiple-testing artifacts.
What this means

The short answer

Of 66 tests, 55 were significant uncorrected; Holm-Bonferroni retained 43, and Benjamini-Hochberg retained 54. The wine chemistry correlations show strong signal, but strict family-wise control removes 12 findings likely to be false positives.

The detail

The dataset evaluated 66 correlation tests with no invalid p-values dropped. Raw significance at p < 0.05 flagged 55 tests. Holm-Bonferroni, which controls family-wise error rate, kept 43 significant results. Benjamini-Hochberg, which controls false-discovery rate, kept 54—removing only 1 additional finding beyond the uncorrected threshold. The smallest p-value observed was 1e-300, indicating extremely strong signal in the strongest associations.

What this can't tell you

The count of 12 findings removed by Holm reflects the trade-off between error control stringency and discovery rate; it does not identify which specific correlations are true versus artifacts without examining effect sizes and domain knowledge. A transaction-level or subject-level export would allow assessment of whether the strongest signals remain robust when stratified by wine type or other characteristics.

Overview

Analysis Overview

Holm-Bonferroni and Benjamini-Hochberg corrections applied to 66 p-values from test label.

N Tests66
Alpha0.05
N Significant Raw55
N Significant Holm43
N Significant Bh54
What this means

The short answer

Running 66 tests inflates false positives by chance alone. Holm-Bonferroni controls the risk of ANY false positive across all tests; Benjamini-Hochberg controls the fraction of false positives among discoveries you report, trading stricter family-wise safety for more discoveries.

The detail

Each test is one hypothesis. At uncorrected alpha = 0.05, roughly 3.3 false positives are expected by chance across 66 tests. Holm-Bonferroni is the stricter, family-wise standard: it ensures at most a 5% chance that even ONE false positive appears anywhere in your 66 results. Benjamini-Hochberg is less strict: it tolerates a 5% false-discovery rate—meaning among the tests you call significant, up to 5% may be false positives. Here 55 tests were significant uncorrected, 43 under Holm, and 54 under BH. The 1-test gap between BH and uncorrected shows BH is nearly as permissive as raw significance in this dataset.

What this can't tell you

This analysis does not distinguish which of the 12 tests that fail Holm are true false positives versus weak true effects; a larger sample or stronger effect sizes would sharpen that distinction.

Data Preparation

Data Quality

P-value validation and exclusions.

Initial Rows66
Final Rows66
Rows Removed0
N Missing0
N Out Of Range0
What this means

The short answer

All 66 p-values were valid (between 0 and 1) and entered the correction. No rows were dropped, so the test family size remained 66 throughout.

The detail

66 rows loaded from the test label and raw p-value columns. 0 rows were missing and 0 were out of range. All 66 valid tests entered both the Holm-Bonferroni and Benjamini-Hochberg corrections. No exclusions changed the family size, so the adjustment thresholds reflect the full 66-test burden.

What this can't tell you

The analysis does not validate whether the underlying p-values themselves are correct (e.g., whether the correlations were computed on complete data, whether assumptions of the correlation test were met, or whether the data contained outliers that inflated significance).

Data Table

Adjusted Results

Every test with raw, Holm, and BH p-values and significance verdicts.

TestRaw PHolm PBh PSig RawSig HolmSig Bh
fixed acidity ~ volatile acidity1e-3006.6e-2992.64e-300YesYesYes
fixed acidity ~ citric acid1e-3006.6e-2992.64e-300YesYesYes
fixed acidity ~ density1e-3006.6e-2992.64e-300YesYesYes
fixed acidity ~ pH1e-3006.6e-2992.64e-300YesYesYes
volatile acidity ~ citric acid1e-3006.6e-2992.64e-300YesYesYes
volatile acidity ~ pH1e-3006.6e-2992.64e-300YesYesYes
volatile acidity ~ sulphates1e-3006.6e-2992.64e-300YesYesYes
volatile acidity ~ quality1e-3006.6e-2992.64e-300YesYesYes
citric acid ~ chlorides1e-3006.6e-2992.64e-300YesYesYes
citric acid ~ density1e-3006.6e-2992.64e-300YesYesYes
citric acid ~ pH1e-3006.6e-2992.64e-300YesYesYes
citric acid ~ sulphates1e-3006.6e-2992.64e-300YesYesYes
citric acid ~ quality1e-3006.6e-2992.64e-300YesYesYes
residual sugar ~ total sulfur dioxide1e-3006.6e-2992.64e-300YesYesYes
residual sugar ~ density1e-3006.6e-2992.64e-300YesYesYes
chlorides ~ pH1e-3006.6e-2992.64e-300YesYesYes
chlorides ~ sulphates1e-3006.6e-2992.64e-300YesYesYes
chlorides ~ alcohol1e-3006.6e-2992.64e-300YesYesYes
free sulfur dioxide ~ total sulfur dioxide1e-3006.6e-2992.64e-300YesYesYes
total sulfur dioxide ~ alcohol1e-3006.6e-2992.64e-300YesYesYes
density ~ pH1e-3006.6e-2992.64e-300YesYesYes
density ~ alcohol1e-3006.6e-2992.64e-300YesYesYes
pH ~ alcohol1e-3006.6e-2992.64e-300YesYesYes
sulphates ~ quality1e-3006.6e-2992.64e-300YesYesYes
alcohol ~ quality1e-3006.6e-2992.64e-300YesYesYes
volatile acidity ~ alcohol2.22e-169.104e-155.428e-16YesYesYes
chlorides ~ density2.22e-169.104e-155.428e-16YesYesYes
pH ~ sulphates1.11e-154.33e-142.617e-15YesYesYes
residual sugar ~ free sulfur dioxide2.753e-141.046e-126.266e-14YesYesYes
total sulfur dioxide ~ quality5.196e-141.922e-121.143e-13YesYesYes
fixed acidity ~ sulphates1.015e-133.653e-122.16e-13YesYesYes
density ~ quality1.251e-124.377e-112.579e-12YesYesYes
fixed acidity ~ free sulfur dioxide4.972e-101.69e-089.943e-10YesYesYes
density ~ sulphates1.958e-096.462e-083.801e-09YesYesYes
citric acid ~ residual sugar6.72e-092.15e-071.267e-08YesYesYes
chlorides ~ quality2.05e-076.354e-063.758e-07YesYesYes
fixed acidity ~ quality5.853e-0701.044e-06YesYesYes
fixed acidity ~ residual sugar3.888e-060.00016.753e-06YesYesYes
fixed acidity ~ total sulfur dioxide5.307e-060.00018.981e-06YesYesYes
citric acid ~ alcohol9.926e-060.00030YesYesYes
fixed acidity ~ chlorides0.00020.00440.0003YesYesYes
sulphates ~ alcohol0.00020.00440.0003YesYesYes
residual sugar ~ pH0.00060.01420.0009YesYesYes
volatile acidity ~ total sulfur dioxide0.00220.05010.0033YesNoYes
total sulfur dioxide ~ density0.00430.09460.0063YesNoYes
free sulfur dioxide ~ pH0.00480.1010.0069YesNoYes
free sulfur dioxide ~ alcohol0.00540.10860.0076YesNoYes
total sulfur dioxide ~ pH0.00770.14710.0106YesNoYes
fixed acidity ~ alcohol0.01350.24380.0182YesNoYes
volatile acidity ~ chlorides0.01410.24380.0186YesNoYes
What this means

The short answer

The 43 tests that survive Holm-Bonferroni are the safest claims; 54 survive Benjamini-Hochberg. The strongest result—fixed acidity ~ volatile acidity (raw p = 1e-300, Holm p = 6.6e-299, BH p = 2.64e-300)—is significant under both. The 11 tests that pass BH but fail Holm are plausible discoveries that don't meet the strictest standard.

The detail

All 66 tests are sorted by raw p-value, smallest first. The top 4 tests (fixed acidity ~ volatile acidity, fixed acidity ~ citric acid, fixed acidity ~ density, fixed acidity ~ pH) all show raw p = 1e-300, Holm p = 6.6e-299, BH p = 2.64e-300, and are significant under all three standards. Reading down, 43 rows remain marked "Yes" under Holm and 54 under BH. The cutoff between Holm Yes and No identifies the 43rd-smallest test; one additional test passes BH but not Holm, representing the 1-test difference between the methods.

What this can't tell you

The table does not report effect sizes or confidence intervals for individual correlations, so you cannot assess whether surviving tests represent large or small associations.

Visualization

What Survives Correction

Significant test counts: uncorrected vs Holm vs BH.

What this means

The short answer

Uncorrected significance identified 55 findings; Holm-Bonferroni reduced this to 43, and Benjamini-Hochberg to 54. The 12-finding drop under Holm versus 1-finding drop under BH reflects the difference between controlling family-wise error (no false positives allowed) and controlling false-discovery rate (small fraction of false positives tolerated).

The detail

The three bars show significant test counts at α = 0.05: 55 uncorrected, 43 after Holm-Bonferroni, and 54 after Benjamini-Hochberg. Holm removes 12 findings, BH removes 1. BH sits between the extremes because it controls the proportion of false discoveries among all discoveries, permitting a small expected false-positive rate in exchange for higher power; Holm controls the probability of any false positive across the family, a stricter standard that costs more power.

What this can't tell you

The gap between methods does not indicate which 12 findings are false positives—only that they fail the strictest test. Examining the raw p-values and effect sizes of the Holm-rejected findings would help assess whether they represent true weak signals or noise. A replication sample would be the most direct way to validate which findings replicate.

Visualization

P-Value Distribution

10-bin histogram of raw p-values — the real-effects-vs-noise diagnostic.

What this means

The short answer

58 of 66 p-values fall in the lowest bin (0.0–0.1), about 8.8 times the count expected under a null-only scenario. This spike near zero is the classic signature of genuine effects hiding among the tests, not random noise.

The detail

The histogram splits 66 p-values into ten bins of width 0.1. Under a uniform (all-null) distribution, each bin would hold about 6.6 tests. The 0.0–0.1 bin holds 58; the 0.1–0.2 bin holds 1; all other bins hold 0–2 tests each. The 58-test spike in the lowest bin far exceeds the ~7 a uniform null would predict, confirming that many correlations in your wine chemistry dataset are genuinely non-null.

What this can't tell you

The distribution does not distinguish strong effects from weak ones within the lowest bin, nor does it identify which specific correlations are true versus false; the adjusted results and significance summary supply that breakdown.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Multiple Comparisons Correction — What Survives?

Takes a set of raw p-values from many tests, applies Holm-Bonferroni and Benjamini-Hochberg corrections, and reports which results are still significant under each method at alpha = 0.05 — plus the classic p-value distribution diagnostic.

Why This Method?

Running many tests inflates false positives: at alpha = 0.05, 20 null tests yield one "significant" result by chance on average. Holm-Bonferroni controls the family-wise error rate (the chance of ANY false positive); Benjamini-Hochberg controls the false discovery rate (the expected fraction of false positives among discoveries). Reporting both, per test, is the standard defensible answer to "which results are real?"

What This Analysis Covers

  • Holm and BH adjusted p-values for every test, with significance verdicts
  • Significant counts under no correction / Holm / BH
  • A 10-bin p-value histogram diagnosing real effects vs mostly-null noise

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {test_label, p_value}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Core Analysis Pipeline

compute_shared <- function(df, params, col_map = list()) {
  # === SHARED EXPORTS ===
  #   initial_rows/final_rows/rows_removed  $ row accounting (rows_removed = invalid p dropped)
  #   p_name / label_name    $ humanized user column names
  #   n_tests                $ integer — valid tests entering the correction
  #   n_na / n_out_of_range  $ integer — dropped: missing vs outside [0,1]
  #   alpha                  $ 0.05
  #   results_all            $ data.frame(test, raw_p, holm_p, bh_p, sig_raw, sig_holm, sig_bh) sorted by raw_p
  #   adjusted_results_df    $ results_all capped at 50 rows
  #   results_truncated      $ logical — TRUE when results_all > 50 rows
  #   n_sig_raw/n_sig_holm/n_sig_bh $ integer — significant counts per method
  #   significance_summary_df $ data.frame(method, significant_count)
  #   p_value_distribution_df $ data.frame(p_range, count) — 10 bins of width 0.1
  #   dist_diagnosis          $ character — computed narrative for the histogram shape
  #   metrics / json_output
  # === /SHARED EXPORTS ===

Step 1: Locate the mapped columns

initial_rows <- nrow(df)
  p_name <- humanize_semantic("p_value", col_map)
  label_name <- humanize_semantic("test_label", col_map)
  if (!("p_value" %in% names(df))) {
    stop(sprintf("column_mapping must map p_value to your p-value column(&#x27;%s' not found).", p_name))
  }
  if (!("test_label" %in% names(df))) {
    stop(sprintf("column_mapping must map test_label to your test-name column(&#x27;%s' not found).", label_name))
  }

Step 2: Labels — character, blanks filled with a positional name

lab <- as.character(df$test_label)
  blank <- is.na(lab) | trimws(lab) == ""
  if (any(blank)) lab[blank] <- paste0("Test ", which(blank))

Step 3: Coerce p-values (95% rule) and validate to [0, 1]

v <- df$p_value
  if (!is.numeric(v)) {
    conv <- suppressWarnings(as.numeric(as.character(v)))
    n_orig <- sum(!is.na(v) & as.character(v) != "")
    if (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig) {
      v <- conv
    } else {
      stop(sprintf("The mapped p-value column(&#x27;%s') is not numeric — map a column of raw p-values between 0 and 1.", p_name))
    }
  }
  n_na <- sum(is.na(v))
  n_out_of_range <- sum(!is.na(v) & (v < 0 | v > 1))
  keep <- !is.na(v) & v >= 0 & v <= 1
  p <- as.numeric(v[keep])
  labels <- lab[keep]
  final_rows <- length(p)
  rows_removed <- initial_rows - final_rows
  if (final_rows < 5) {
    stop(sprintf(
      "Only %d valid p-values remained in &#x27;%s' after dropping %d invalid (missing or outside 0-1) — need at least 5 tests to correct.",
      final_rows, p_name, rows_removed))
  }
  n_tests <- final_rows
  alpha <- 0.05

Step 4: Adjust — Holm (family-wise) + Benjamini-Hochberg (FDR)

holm <- p.adjust(p, method = "holm")
  bh   <- p.adjust(p, method = "BH")
  sig_raw  <- p    < alpha
  sig_holm <- holm < alpha
  sig_bh   <- bh   < alpha
  n_sig_raw  <- sum(sig_raw)
  n_sig_holm <- sum(sig_holm)
  n_sig_bh   <- sum(sig_bh)

  results_all <- data.frame(
    test     = labels,
    raw_p    = signif(p, 4),
    holm_p   = signif(holm, 4),
    bh_p     = signif(bh, 4),
    sig_raw  = ifelse(sig_raw,  "Yes", "No"),
    sig_holm = ifelse(sig_holm, "Yes", "No"),
    sig_bh   = ifelse(sig_bh,   "Yes", "No"),
    stringsAsFactors = FALSE
  )
  results_all <- results_all[order(results_all$raw_p), , drop = FALSE]
  rownames(results_all) <- NULL
  results_truncated <- nrow(results_all) > 50
  adjusted_results_df <- head(results_all, 50)

  significance_summary_df <- data.frame(
    method = c("Uncorrected", "Holm-Bonferroni", "Benjamini-Hochberg"),
    significant_count = c(n_sig_raw, n_sig_holm, n_sig_bh),
    stringsAsFactors = FALSE
  )

Step 5: P-value distribution — 10 bins of width 0.1

breaks <- seq(0, 1, by = 0.1)
  bin_idx <- pmin(pmax(findInterval(p, breaks, rightmost.closed = TRUE), 1), 10)
  bin_labels <- paste0(format(breaks[-11], nsmall = 1), "-", format(breaks[-1], nsmall = 1))
  counts <- as.integer(table(factor(bin_idx, levels = 1:10)))
  p_value_distribution_df <- data.frame(
    p_range = bin_labels,
    count = counts,
    stringsAsFactors = FALSE
  )

Classic diagnostic, narrated from the computed bin counts: a spike in the lowest bin suggests true effects; a near-uniform histogram suggests mostly null effects.

uniform_expected <- n_tests / 10
  first_bin <- counts[1]
  dist_diagnosis <- if (first_bin >= 2 * uniform_expected && first_bin >= 3) {
    sprintf(paste0(
      "The lowest bin(0.0-0.1) holds %d of %d p-values — about %.1fx the ~%.0f a ",
      "uniform(all-null) distribution would put there. That spike near zero is the ",
      "classic signature of genuinely non-null effects among your tests."),
      first_bin, n_tests, first_bin / max(uniform_expected, 1e-9), uniform_expected)
  } else if (first_bin > uniform_expected) {
    sprintf(paste0(
      "The lowest bin(0.0-0.1) holds %d of %d p-values, modestly above the ~%.0f a ",
      "uniform(all-null) distribution would produce — weak evidence of a few true ",
      "effects mixed into mostly null tests."),
      first_bin, n_tests, uniform_expected)
  } else {
    sprintf(paste0(
      "The histogram is close to uniform(lowest bin: %d of %d p-values vs ~%.0f ",
      "expected under all-null) — consistent with mostly null effects, so treat any ",
      "uncorrected significant results with suspicion."),
      first_bin, n_tests, uniform_expected)
  }

  metrics <- list(
    `Tests Evaluated`         = n_tests,
    `Invalid P-Values Dropped` = as.integer(rows_removed),
    `Significant(Uncorrected)` = as.integer(n_sig_raw),
    `Significant(Holm)`      = as.integer(n_sig_holm),
    `Significant(BH)`        = as.integer(n_sig_bh),
    `Smallest P-Value`        = signif(min(p), 3)
  )

  json_output <- list(
    answer = paste0(
      "Of ", n_tests, " tests, ", n_sig_raw, " were significant uncorrected(p < 0.05); ",
      "Holm-Bonferroni keeps ", n_sig_holm, " (strict family-wise control) and ",
      "Benjamini-Hochberg keeps ", n_sig_bh, " (false-discovery-rate control). ",
      dist_diagnosis
    ),
    cards = lapply(
      c("tldr", "overview", "preprocessing", "adjusted_results",
        "significance_summary", "p_value_distribution"),
      function(cid) list(id = cid, metrics = metrics)
    )
  )

  list(
    initial_rows = initial_rows, final_rows = final_rows,
    rows_removed = rows_removed,
    p_name = p_name, label_name = label_name,
    n_tests = n_tests, n_na = n_na, n_out_of_range = n_out_of_range,
    alpha = alpha,
    results_all = results_all,
    adjusted_results_df = adjusted_results_df,
    results_truncated = results_truncated,
    n_sig_raw = n_sig_raw, n_sig_holm = n_sig_holm, n_sig_bh = n_sig_bh,
    significance_summary_df = significance_summary_df,
    p_value_distribution_df = p_value_distribution_df,
    dist_diagnosis = dist_diagnosis,
    metrics = metrics, json_output = json_output
  )
}

# Card: tldr (tldr)
card_tldr <- function(shared, df, params) {
  survival_word <- if (shared$n_sig_raw == 0) {
    "No result was significant even before correction."
  } else if (shared$n_sig_holm == shared$n_sig_raw) {
    "Every uncorrected finding survived even the strictest correction — these results are robust to multiple testing."
  } else if (shared$n_sig_holm == 0 && shared$n_sig_bh == 0) {
    "No result survived either correction — the uncorrected &#x27;significant' findings are consistent with multiple-testing luck."
  } else {
    sprintf("%d uncorrected finding(s) did not survive Holm — likely multiple-testing artifacts.",
            shared$n_sig_raw - shared$n_sig_holm)
  }
  text <- paste0(
    "Of ", shared$n_tests, " tests, ", shared$n_sig_raw,
    " were significant uncorrected(p < ", shared$alpha, "); Holm-Bonferroni keeps ",
    shared$n_sig_holm, " (strict family-wise control) and Benjamini-Hochberg keeps ",
    shared$n_sig_bh, " (false-discovery-rate control). ", survival_word
  )
  list(
    title = "Executive Summary",
    description = paste0("Multiple-testing correction across ", shared$n_tests, " tests"),
    metrics = shared$metrics,
    text = text
  )
}
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing