Executive Summary
Multiple-testing correction across 66 tests
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.
Analysis Overview
Holm-Bonferroni and Benjamini-Hochberg corrections applied to 66 p-values from test label.
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 Quality
P-value validation and exclusions.
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).
Adjusted Results
Every test with raw, Holm, and BH p-values and significance verdicts.
| Test | Raw P | Holm P | Bh P | Sig Raw | Sig Holm | Sig Bh |
|---|---|---|---|---|---|---|
| fixed acidity ~ volatile acidity | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| fixed acidity ~ citric acid | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| fixed acidity ~ density | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| fixed acidity ~ pH | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| volatile acidity ~ citric acid | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| volatile acidity ~ pH | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| volatile acidity ~ sulphates | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| volatile acidity ~ quality | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| citric acid ~ chlorides | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| citric acid ~ density | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| citric acid ~ pH | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| citric acid ~ sulphates | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| citric acid ~ quality | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| residual sugar ~ total sulfur dioxide | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| residual sugar ~ density | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| chlorides ~ pH | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| chlorides ~ sulphates | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| chlorides ~ alcohol | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| free sulfur dioxide ~ total sulfur dioxide | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| total sulfur dioxide ~ alcohol | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| density ~ pH | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| density ~ alcohol | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| pH ~ alcohol | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| sulphates ~ quality | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| alcohol ~ quality | 1e-300 | 6.6e-299 | 2.64e-300 | Yes | Yes | Yes |
| volatile acidity ~ alcohol | 2.22e-16 | 9.104e-15 | 5.428e-16 | Yes | Yes | Yes |
| chlorides ~ density | 2.22e-16 | 9.104e-15 | 5.428e-16 | Yes | Yes | Yes |
| pH ~ sulphates | 1.11e-15 | 4.33e-14 | 2.617e-15 | Yes | Yes | Yes |
| residual sugar ~ free sulfur dioxide | 2.753e-14 | 1.046e-12 | 6.266e-14 | Yes | Yes | Yes |
| total sulfur dioxide ~ quality | 5.196e-14 | 1.922e-12 | 1.143e-13 | Yes | Yes | Yes |
| fixed acidity ~ sulphates | 1.015e-13 | 3.653e-12 | 2.16e-13 | Yes | Yes | Yes |
| density ~ quality | 1.251e-12 | 4.377e-11 | 2.579e-12 | Yes | Yes | Yes |
| fixed acidity ~ free sulfur dioxide | 4.972e-10 | 1.69e-08 | 9.943e-10 | Yes | Yes | Yes |
| density ~ sulphates | 1.958e-09 | 6.462e-08 | 3.801e-09 | Yes | Yes | Yes |
| citric acid ~ residual sugar | 6.72e-09 | 2.15e-07 | 1.267e-08 | Yes | Yes | Yes |
| chlorides ~ quality | 2.05e-07 | 6.354e-06 | 3.758e-07 | Yes | Yes | Yes |
| fixed acidity ~ quality | 5.853e-07 | 0 | 1.044e-06 | Yes | Yes | Yes |
| fixed acidity ~ residual sugar | 3.888e-06 | 0.0001 | 6.753e-06 | Yes | Yes | Yes |
| fixed acidity ~ total sulfur dioxide | 5.307e-06 | 0.0001 | 8.981e-06 | Yes | Yes | Yes |
| citric acid ~ alcohol | 9.926e-06 | 0.0003 | 0 | Yes | Yes | Yes |
| fixed acidity ~ chlorides | 0.0002 | 0.0044 | 0.0003 | Yes | Yes | Yes |
| sulphates ~ alcohol | 0.0002 | 0.0044 | 0.0003 | Yes | Yes | Yes |
| residual sugar ~ pH | 0.0006 | 0.0142 | 0.0009 | Yes | Yes | Yes |
| volatile acidity ~ total sulfur dioxide | 0.0022 | 0.0501 | 0.0033 | Yes | No | Yes |
| total sulfur dioxide ~ density | 0.0043 | 0.0946 | 0.0063 | Yes | No | Yes |
| free sulfur dioxide ~ pH | 0.0048 | 0.101 | 0.0069 | Yes | No | Yes |
| free sulfur dioxide ~ alcohol | 0.0054 | 0.1086 | 0.0076 | Yes | No | Yes |
| total sulfur dioxide ~ pH | 0.0077 | 0.1471 | 0.0106 | Yes | No | Yes |
| fixed acidity ~ alcohol | 0.0135 | 0.2438 | 0.0182 | Yes | No | Yes |
| volatile acidity ~ chlorides | 0.0141 | 0.2438 | 0.0186 | Yes | No | Yes |
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.
What Survives Correction
Significant test counts: uncorrected vs Holm vs BH.
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.
P-Value Distribution
10-bin histogram of raw p-values — the real-effects-vs-noise diagnostic.
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.
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('%s' not found).", p_name))
}
if (!("test_label" %in% names(df))) {
stop(sprintf("column_mapping must map test_label to your test-name column('%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('%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 '%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.05Step 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 '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
)
}