Source code for ClearMap.Analysis.Statistics.MultipleComparisonCorrection

# -*- coding: utf-8 -*-
"""
MultipleComparisonCorrection
============================

Correction methods for multiple comparison tests.
"""
__author__    = 'Christoph Kirst <christoph.kirst.ck@gmail.com>, Charly Rousseau <charly.rousseau@icm-institute.org>'
__license__   = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__   = 'https://idisco.info'
__download__  = 'https://www.github.com/ChristophKirst/ClearMap2'


import numpy as np
import scipy
import scipy.interpolate


###############################################################################
# ## Benjamini Hochberg correction
############################################################################### 

[docs] def correct_p_values(p_values, method='BH'): """Corrects p-values for multiple testing using various methods. Arguments --------- p_values : array List of p values to be corrected. method : str Optional method to use: 'BH' = 'FDR' = 'Benjamini-Hochberg', 'B' = 'FWER' = 'Bonferoni'. Returns ------- qvalues : array Corrected p values. References ---------- - `Benjamini Hochberg, 1995 <https://www.jstor.org/stable/2346101?seq=1#page_scan_tab_contents>`_ - `Bonferoni correction <https://www.tandfonline.com/doi/abs/10.1080/01621459.1961.10482090#.VmHWUHbH6KE>`_ - `R statistics package <https://www.r-project.org/>`_ Notes ----- Modified from http://statsmodels.sourceforge.net/ipdirective/generated/scikits.statsmodels.sandbox.stats.multicomp.multipletests.html. """ p_vals = np.asarray(p_values) n = len(p_vals) if method.lower() in ['bh', 'fdr']: pvals_sorted_ids = np.argsort(p_vals) pvals_sorted = p_vals[pvals_sorted_ids] sorted_ids_inv = pvals_sorted_ids.argsort() bhfactor = np.arange(1, n+1) / float(n) pvals_corrected_raw = pvals_sorted / bhfactor pvals_corrected = np.minimum.accumulate(pvals_corrected_raw[::-1])[::-1] pvals_corrected[pvals_corrected > 1] = 1 return pvals_corrected[sorted_ids_inv] elif method.lower() in ['b', 'fwer']: pvals_corrected = n * p_vals pvals_corrected[pvals_corrected > 1] = 1 return pvals_corrected else: raise ValueError(f"Unknown method '{method}'. Use 'BH' or 'B' " f"for Benjamini Hochberg and Bonferoni respectively.")
#return reject[pvals_sortind.argsort()]
[docs] def estimate_q_values(p_values, m=None, pi0=None, verbose=False, low_memory=False): """Estimates q-values from p-values. Arguments --------- p_values : array List of p-values. m : int or None Number of tests. If None, m = p_values.size pi0 : float or None Estimate of m_0 / m which is the (true null / total tests) ratio. If None estimation via cubic spline. verbose : bool Print info during execution low_memory : bool If true, use low memory version. Returns ------- q_values : array The q values. Notes ----- - The q-value of a particular feature can be described as the expected proportion of false positives among all features as or more extreme than the observed one. - The estimated q-values are increasing in the same order as the p-values. References ---------- - `Storey and Tibshirani, 2003 <https://www.pnas.org/content/100/16/9440.full>`_ - modified from https://github.com/nfusi/qvalue """ if not (p_values.min() >= 0 and p_values.max() <= 1): raise RuntimeError('estimateQValues: p-values should be between 0 and 1') original_shape = p_values.shape p_values = p_values.ravel() # flattens the array in place, more efficient than flatten() if m is None: m = float(len(p_values)) else: # the user has supplied an m m *= 1.0 # if the number of hypotheses is small, just set pi0 to 1 if len(p_values) < 100 and pi0 is None: pi0 = 1.0 elif pi0 is not None: pi0 = pi0 else: # evaluate pi0 for different lambdas pi0 = [] lam = np.arange(0, 0.90, 0.01) counts = np.array([(p_values > i).sum() for i in lam]) for l in range(len(lam)): pi0.append(counts[l]/(m*(1-lam[l]))) pi0 = np.array(pi0) # fit natural cubic scipy line tck = scipy.interpolate.splrep(lam, pi0, k = 3) pi0 = scipy.interpolate.splev(lam[-1], tck) if pi0 > 1: if verbose: raise Warning(f'estimateQValues: got pi0 > 1 ({pi0:.3f}) while estimating qvalues, setting it to 1') pi0 = 1.0 if not (0 <= pi0 <= 1): raise RuntimeError(f'estimateQValues: pi0 is not between 0 and 1: {pi0:f}') if low_memory: # low memory version, only uses 1 p_values and 1 qv matrices qv = scipy.zeros((len(p_values),)) last_p_values = p_values.argmax() qv[last_p_values] = (pi0 * p_values[last_p_values] * m) / float(m) p_values[last_p_values] = -scipy.inf prev_qv = last_p_values for i in range(int(len(p_values)) - 2, -1, -1): cur_max = p_values.argmax() qv_i = (pi0 * m * p_values[cur_max] / float(i + 1)) p_values[cur_max] = -scipy.inf qv_i1 = prev_qv qv[cur_max] = min(qv_i, qv_i1) prev_qv = qv[cur_max] else: p_ordered = p_values.argsort() p_values = p_values[p_ordered] qv = pi0 * m / len(p_values) * p_values qv[-1] = min(qv[-1],1.0) for i in range(len(p_values) - 2, -1, -1): qv[i] = min(pi0 * m * p_values[i] / (i + 1.0), qv[i + 1]) # reorder q_values qv_temp = qv.copy() qv = np.zeros_like(qv) qv[p_ordered] = qv_temp # reshape q_values qv = qv.reshape(original_shape) return qv