Given a list of p-values, adjust the p-values for multiple comparisons. This is done in order to control the false positive, or Type 1 error rate.

This is also known as the "false discovery rate" (FDR). After adjustment, the p-values will be higher but still inside [0,1].

The adjusted p-values are sometimes called "q-values".



Task

Given one list of p-values, return the p-values correcting for multiple comparisons

   p = {4.533744e-01, 7.296024e-01, 9.936026e-02, 9.079658e-02, 1.801962e-01,
        8.752257e-01, 2.922222e-01, 9.115421e-01, 4.355806e-01, 5.324867e-01,
        4.926798e-01, 5.802978e-01, 3.485442e-01, 7.883130e-01, 2.729308e-01,
        8.502518e-01, 4.268138e-01, 6.442008e-01, 3.030266e-01, 5.001555e-02,
        3.194810e-01, 7.892933e-01, 9.991834e-01, 1.745691e-01, 9.037516e-01,
        1.198578e-01, 3.966083e-01, 1.403837e-02, 7.328671e-01, 6.793476e-02,
        4.040730e-03, 3.033349e-04, 1.125147e-02, 2.375072e-02, 5.818542e-04,
        3.075482e-04, 8.251272e-03, 1.356534e-03, 1.360696e-02, 3.764588e-04,
        1.801145e-05, 2.504456e-07, 3.310253e-02, 9.427839e-03, 8.791153e-04,
        2.177831e-04, 9.693054e-04, 6.610250e-05, 2.900813e-02, 5.735490e-03}



There are several methods to do this, see:

 Yoav Benjamini, Yosef Hochberg "Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing", Journal of the Royal Statistical Society. Series B, Vol. 57, No. 1 (1995), pp. 289-300, JSTOR:2346101
 Yoav Benjamini, Daniel Yekutieli, "The control of the false discovery rate in multiple testing under dependency", Ann. Statist., Vol. 29, No. 4 (2001), pp. 1165-1188, DOI:10.1214/aos/1013699998 JSTOR:2674075
 Sture Holm, "A Simple Sequentially Rejective Multiple Test Procedure", Scandinavian Journal of Statistics, Vol. 6, No. 2 (1979), pp. 65-70, JSTOR:4615733
 Yosef Hochberg, "A sharper Bonferroni procedure for multiple tests of significance", Biometrika, Vol. 75, No. 4 (1988), pp 800–802, DOI:10.1093/biomet/75.4.800 JSTOR:2336325
 Gerhard Hommel, "A stagewise rejective multiple test procedure based on a modified Bonferroni test", Biometrika, Vol. 75, No. 2 (1988), pp 383–386, DOI:10.1093/biomet/75.2.383 JSTOR:2336190


Each method has its own advantages and disadvantages.




using System;
using System.Collections.Generic;
using System.Linq;
 
namespace PValueCorrection {
    class Program {
        static List<int> SeqLen(int start, int end) {
            var result = new List<int>();
            if (start == end) {
                for (int i = 0; i < end + 1; ++i) {
                    result.Add(i + 1);
                }
            } else if (start < end) {
                for (int i = 0; i < end - start + 1; ++i) {
                    result.Add(start + i);
                }
            } else {
                for (int i = 0; i < start - end + 1; ++i) {
                    result.Add(start - i);
                }
            }
            return result;
        }
 
        static List<int> Order(List<double> array, bool decreasing) {
            List<int> idx = new List<int>();
            for (int i = 0; i < array.Count; ++i) {
                idx.Add(i);
            }
 
            IComparer<int> cmp;
            if (decreasing) {
                cmp = Comparer<int>.Create((a, b) => array[a] < array[b] ? 1 : array[b] < array[a] ? -1 : 0);
            } else {
                cmp = Comparer<int>.Create((a, b) => array[b] < array[a] ? 1 : array[a] < array[b] ? -1 : 0);
            }
 
            idx.Sort(cmp);
            return idx;
        }
 
        static List<double> Cummin(List<double> array) {
            if (array.Count < 1) throw new ArgumentOutOfRangeException("cummin requires at least one element");
            var output = new List<double>();
            double cumulativeMin = array[0];
            for (int i = 0; i < array.Count; ++i) {
                if (array[i] < cumulativeMin) cumulativeMin = array[i];
                output.Add(cumulativeMin);
            }
            return output;
        }
 
        static List<double> Cummax(List<double> array) {
            if (array.Count < 1) throw new ArgumentOutOfRangeException("cummax requires at least one element");
            var output = new List<double>();
            double cumulativeMax = array[0];
            for (int i = 0; i < array.Count; ++i) {
                if (array[i] > cumulativeMax) cumulativeMax = array[i];
                output.Add(cumulativeMax);
            }
            return output;
        }
 
        static List<double> Pminx(List<double> array, double x) {
            if (array.Count < 1) throw new ArgumentOutOfRangeException("pmin requires at least one element");
            var result = new List<double>();
            for (int i = 0; i < array.Count; ++i) {
                if (array[i] < x) {
                    result.Add(array[i]);
                } else {
                    result.Add(x);
                }
            }
            return result;
        }
 
        static void Say(List<double> array) {
            Console.Write("[ 1] {0:E}", array[0]);
            for (int i = 1; i < array.Count; ++i) {
                Console.Write(" {0:E}", array[i]);
                if ((i + 1) % 5 == 0) Console.Write("\n[{0,2}]", i + 1);
            }
            Console.WriteLine();
        }
 
        static List<double> PAdjust(List<double> pvalues, string str) {
            var size = pvalues.Count;
            if (size < 1) throw new ArgumentOutOfRangeException("pAdjust requires at least one element");
 
            int type;
            switch (str.ToLower()) {
                case "bh":
                case "fdr":
                    type = 0;
                    break;
                case "by":
                    type = 1;
                    break;
                case "bonferroni":
                    type = 2;
                    break;
                case "hochberg":
                    type = 3;
                    break;
                case "holm":
                    type = 4;
                    break;
                case "hommel":
                    type = 5;
                    break;
                default:
                    throw new ArgumentException(str + " doesn't match any accepted FDR types");
            }
 
            if (2 == type) { // Bonferroni method
                var result2 = new List<double>();
                for (int i = 0; i < size; ++i) {
                    double b = pvalues[i] * size;
                    if (b >= 1) {
                        result2.Add(1);
                    } else if (0 <= b && b < 1) {
                        result2.Add(b);
                    } else {
                        throw new Exception(b + " is outside [0, 1)");
                    }
                }
                return result2;
            } else if (4 == type) { // Holm method
                var o4 = Order(pvalues, false);
                var o4d = o4.ConvertAll(x => (double)x);
                var cummaxInput = new List<double>();
                for (int i = 0; i < size; ++i) {
                    cummaxInput.Add((size - i) * pvalues[o4[i]]);
                }
                var ro4 = Order(o4d, false);
                var cummaxOutput = Cummax(cummaxInput);
                var pmin4 = Pminx(cummaxOutput, 1.0);
                var hr = new List<double>();
                for (int i = 0; i < size; ++i) {
                    hr.Add(pmin4[ro4[i]]);
                }
                return hr;
            } else if (5 == type) { // Hommel method
                var indices = SeqLen(size, size);
                var o5 = Order(pvalues, false);
                var p = new List<double>();
                for (int i = 0; i < size; ++i) {
                    p.Add(pvalues[o5[i]]);
                }
                var o5d = o5.ConvertAll(x => (double)x);
                var ro5 = Order(o5d, false);
                var q = new List<double>();
                var pa = new List<double>();
                var npi = new List<double>();
                for (int i = 0; i < size; ++i) {
                    npi.Add(p[i] * size / indices[i]);
                }
                double min = npi.Min();
                q.AddRange(Enumerable.Repeat(min, size));
                pa.AddRange(Enumerable.Repeat(min, size));
                for (int j = size; j >= 2; --j) {
                    var ij = SeqLen(1, size - j + 1);
                    for (int i = 0; i < size - j + 1; ++i) {
                        ij[i]--;
                    }
                    int i2Length = j - 1;
                    var i2 = new List<int>();
                    for (int i = 0; i < i2Length; ++i) {
                        i2.Add(size - j + 2 + i - 1);
                    }
                    double q1 = j * p[i2[0]] / 2.0;
                    for (int i = 1; i < i2Length; ++i) {
                        double temp_q1 = p[i2[i]] * j / (2.0 + i);
                        if (temp_q1 < q1) q1 = temp_q1;
                    }
                    for (int i = 0; i < size - j + 1; ++i) {
                        q[ij[i]] = Math.Min(p[ij[i]] * j, q1);
                    }
                    for (int i = 0; i < i2Length; ++i) {
                        q[i2[i]] = q[size - j];
                    }
                    for (int i = 0; i < size; ++i) {
                        if (pa[i] < q[i]) {
                            pa[i] = q[i];
                        }
                    }
                }
                for (int i = 0; i < size; ++i) {
                    q[i] = pa[ro5[i]];
                }
                return q;
            }
 
            var ni = new List<double>();
            var o = Order(pvalues, true);
            var od = o.ConvertAll(x => (double)x);
            for (int i = 0; i < size; ++i) {
                if (pvalues[i] < 0 || pvalues[i] > 1) {
                    throw new Exception("array[" + i + "] = " + pvalues[i] + " is outside [0, 1]");
                }
                ni.Add((double)size / (size - i));
            }
            var ro = Order(od, false);
            var cumminInput = new List<double>();
            if (0 == type) {       // BH method
                for (int i = 0; i < size; ++i) {
                    cumminInput.Add(ni[i] * pvalues[o[i]]);
                }
            } else if (1 == type) { // BY method
                double q = 0;
                for (int i = 1; i < size + 1; ++i) {
                    q += 1.0 / i;
                }
                for (int i = 0; i < size; ++i) {
                    cumminInput.Add(q * ni[i] * pvalues[o[i]]);
                }
            } else if (3 == type) { // Hochberg method
                for (int i = 0; i < size; ++i) {
                    cumminInput.Add((i + 1) * pvalues[o[i]]);
                }
            }
            var cumminArray = Cummin(cumminInput);
            var pmin = Pminx(cumminArray, 1.0);
            var result = new List<double>();
            for (int i = 0; i < size; ++i) {
                result.Add(pmin[ro[i]]);
            }
            return result;
        }
 
        static void Main(string[] args) {
            var pvalues = new List<double> {
                4.533744e-01, 7.296024e-01, 9.936026e-02, 9.079658e-02, 1.801962e-01,
                8.752257e-01, 2.922222e-01, 9.115421e-01, 4.355806e-01, 5.324867e-01,
                4.926798e-01, 5.802978e-01, 3.485442e-01, 7.883130e-01, 2.729308e-01,
                8.502518e-01, 4.268138e-01, 6.442008e-01, 3.030266e-01, 5.001555e-02,
                3.194810e-01, 7.892933e-01, 9.991834e-01, 1.745691e-01, 9.037516e-01,
                1.198578e-01, 3.966083e-01, 1.403837e-02, 7.328671e-01, 6.793476e-02,
                4.040730e-03, 3.033349e-04, 1.125147e-02, 2.375072e-02, 5.818542e-04,
                3.075482e-04, 8.251272e-03, 1.356534e-03, 1.360696e-02, 3.764588e-04,
                1.801145e-05, 2.504456e-07, 3.310253e-02, 9.427839e-03, 8.791153e-04,
                2.177831e-04, 9.693054e-04, 6.610250e-05, 2.900813e-02, 5.735490e-03
            };
            var correctAnswers = new List<List<double>> {
                new List<double> { // Benjamini-Hochberg
                    6.126681e-01, 8.521710e-01, 1.987205e-01, 1.891595e-01, 3.217789e-01,
                    9.301450e-01, 4.870370e-01, 9.301450e-01, 6.049731e-01, 6.826753e-01,
                    6.482629e-01, 7.253722e-01, 5.280973e-01, 8.769926e-01, 4.705703e-01,
                    9.241867e-01, 6.049731e-01, 7.856107e-01, 4.887526e-01, 1.136717e-01,
                    4.991891e-01, 8.769926e-01, 9.991834e-01, 3.217789e-01, 9.301450e-01,
                    2.304958e-01, 5.832475e-01, 3.899547e-02, 8.521710e-01, 1.476843e-01,
                    1.683638e-02, 2.562902e-03, 3.516084e-02, 6.250189e-02, 3.636589e-03,
                    2.562902e-03, 2.946883e-02, 6.166064e-03, 3.899547e-02, 2.688991e-03,
                    4.502862e-04, 1.252228e-05, 7.881555e-02, 3.142613e-02, 4.846527e-03,
                    2.562902e-03, 4.846527e-03, 1.101708e-03, 7.252032e-02, 2.205958e-02
                },
                new List<double> { // Benjamini & Yekutieli
                    1.000000e+00, 1.000000e+00, 8.940844e-01, 8.510676e-01, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 5.114323e-01,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.754486e-01, 1.000000e+00, 6.644618e-01,
                    7.575031e-02, 1.153102e-02, 1.581959e-01, 2.812089e-01, 1.636176e-02,
                    1.153102e-02, 1.325863e-01, 2.774239e-02, 1.754486e-01, 1.209832e-02,
                    2.025930e-03, 5.634031e-05, 3.546073e-01, 1.413926e-01, 2.180552e-02,
                    1.153102e-02, 2.180552e-02, 4.956812e-03, 3.262838e-01, 9.925057e-02
                },
                new List<double> { // Bonferroni
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 7.019185e-01, 1.000000e+00, 1.000000e+00,
                    2.020365e-01, 1.516674e-02, 5.625735e-01, 1.000000e+00, 2.909271e-02,
                    1.537741e-02, 4.125636e-01, 6.782670e-02, 6.803480e-01, 1.882294e-02,
                    9.005725e-04, 1.252228e-05, 1.000000e+00, 4.713920e-01, 4.395577e-02,
                    1.088915e-02, 4.846527e-02, 3.305125e-03, 1.000000e+00, 2.867745e-01
                },
                new List<double> { // Hochberg
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 4.632662e-01, 9.991834e-01, 9.991834e-01,
                    1.575885e-01, 1.383967e-02, 3.938014e-01, 7.600230e-01, 2.501973e-02,
                    1.383967e-02, 3.052971e-01, 5.426136e-02, 4.626366e-01, 1.656419e-02,
                    8.825610e-04, 1.252228e-05, 9.930759e-01, 3.394022e-01, 3.692284e-02,
                    1.023581e-02, 3.974152e-02, 3.172920e-03, 8.992520e-01, 2.179486e-01
                },
                new List<double> { // Holm
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00, 1.000000e+00,
                    1.000000e+00, 1.000000e+00, 4.632662e-01, 1.000000e+00, 1.000000e+00,
                    1.575885e-01, 1.395341e-02, 3.938014e-01, 7.600230e-01, 2.501973e-02,
                    1.395341e-02, 3.052971e-01, 5.426136e-02, 4.626366e-01, 1.656419e-02,
                    8.825610e-04, 1.252228e-05, 9.930759e-01, 3.394022e-01, 3.692284e-02,
                    1.023581e-02, 3.974152e-02, 3.172920e-03, 8.992520e-01, 2.179486e-01
                },
                new List<double> { // Hommel
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.987624e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.595180e-01,
                    9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01, 9.991834e-01,
                    9.991834e-01, 9.991834e-01, 4.351895e-01, 9.991834e-01, 9.766522e-01,
                    1.414256e-01, 1.304340e-02, 3.530937e-01, 6.887709e-01, 2.385602e-02,
                    1.322457e-02, 2.722920e-01, 5.426136e-02, 4.218158e-01, 1.581127e-02,
                    8.825610e-04, 1.252228e-05, 8.743649e-01, 3.016908e-01, 3.516461e-02,
                    9.582456e-03, 3.877222e-02, 3.172920e-03, 8.122276e-01, 1.950067e-01
                }
            };
 
            string[] types = { "bh", "by", "bonferroni", "hochberg", "holm", "hommel" };
            for (int type = 0; type < types.Length; ++type) {
                var q = PAdjust(pvalues, types[type]);
                double error = 0.0;
                for (int i = 0; i < pvalues.Count; ++i) {
                    error += Math.Abs(q[i] - correctAnswers[type][i]);
                }
                Say(q);
                Console.WriteLine("type {0} = '{1}' has a cumulative error of {2:E}", type, types[type], error);
                Console.WriteLine();
            }
        }
    }
}