• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

int-brain-lab / ibllib / 29037613829

09 Jul 2026 05:36PM UTC coverage: 61.575% (-3.3%) from 64.888%
29037613829

push

github

web-flow
Merge 88653ad51 into 087557586

1822 of 3048 new or added lines in 81 files covered. (59.78%)

3091 existing lines in 75 files now uncovered.

12243 of 19883 relevant lines covered (61.58%)

0.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.3
/brainbox/task/_statsmodels.py
1
"""
2
Multiple Testing and P-Value Correction
3
Author: Josef Perktold
4
License: BSD-3
5
Statsmodels
6
https://www.statsmodels.org/
7

8

9
Copyright (C) 2006, Jonathan E. Taylor
10
All rights reserved.
11

12
Copyright (c) 2006-2008 Scipy Developers.
13
All rights reserved.
14

15
Copyright (c) 2009-2018 statsmodels Developers.
16
All rights reserved.
17

18

19
Redistribution and use in source and binary forms, with or without
20
modification, are permitted provided that the following conditions are met:
21

22
  a. Redistributions of source code must retain the above copyright notice,
23
     this list of conditions and the following disclaimer.
24
  b. Redistributions in binary form must reproduce the above copyright
25
     notice, this list of conditions and the following disclaimer in the
26
     documentation and/or other materials provided with the distribution.
27
  c. Neither the name of statsmodels nor the names of its contributors
28
     may be used to endorse or promote products derived from this software
29
     without specific prior written permission.
30

31

32
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
33
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
34
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
35
ARE DISCLAIMED. IN NO EVENT SHALL STATSMODELS OR CONTRIBUTORS BE LIABLE FOR
36
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
37
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
38
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
40
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
41
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
42
DAMAGE.
43
"""
44

45
from collections import OrderedDict
1✔
46

47
import numpy as np
1✔
48

49
from ._knockoff import RegressionFDR
1✔
50

51
__all__ = ['fdrcorrection', 'fdrcorrection_twostage', 'local_fdr', 'multipletests', 'NullDistribution', 'RegressionFDR']
1✔
52

53
# ==============================================
54
#
55
# Part 1: Multiple Tests and P-Value Correction
56
#
57
# ==============================================
58

59

60
def _ecdf(x):
1✔
61
    """no frills empirical cdf used in fdrcorrection"""
UNCOV
62
    nobs = len(x)
×
UNCOV
63
    return np.arange(1, nobs + 1) / float(nobs)
×
64

65

66
multitest_methods_names = {
1✔
67
    'b': 'Bonferroni',
68
    's': 'Sidak',
69
    'h': 'Holm',
70
    'hs': 'Holm-Sidak',
71
    'sh': 'Simes-Hochberg',
72
    'ho': 'Hommel',
73
    'fdr_bh': 'FDR Benjamini-Hochberg',
74
    'fdr_by': 'FDR Benjamini-Yekutieli',
75
    'fdr_tsbh': 'FDR 2-stage Benjamini-Hochberg',
76
    'fdr_tsbky': 'FDR 2-stage Benjamini-Krieger-Yekutieli',
77
    'fdr_gbs': 'FDR adaptive Gavrilov-Benjamini-Sarkar',
78
}
79

80
_alias_list = [
1✔
81
    ['b', 'bonf', 'bonferroni'],
82
    ['s', 'sidak'],
83
    ['h', 'holm'],
84
    ['hs', 'holm-sidak'],
85
    ['sh', 'simes-hochberg'],
86
    ['ho', 'hommel'],
87
    ['fdr_bh', 'fdr_i', 'fdr_p', 'fdri', 'fdrp'],
88
    ['fdr_by', 'fdr_n', 'fdr_c', 'fdrn', 'fdrcorr'],
89
    ['fdr_tsbh', 'fdr_2sbh'],
90
    ['fdr_tsbky', 'fdr_2sbky', 'fdr_twostage'],
91
    ['fdr_gbs'],
92
]
93

94

95
multitest_alias = OrderedDict()
1✔
96
for m in _alias_list:
1✔
97
    multitest_alias[m[0]] = m[0]
1✔
98
    for a in m[1:]:
1✔
99
        multitest_alias[a] = m[0]
1✔
100

101

102
def multipletests(pvals, alpha=0.05, method='hs', is_sorted=False, returnsorted=False):
1✔
103
    """
104
    Test results and p-value correction for multiple tests
105
    Parameters
106
    ----------
107
    pvals : array_like, 1-d
108
        uncorrected p-values.   Must be 1-dimensional.
109
    alpha : float
110
        FWER, family-wise error rate, e.g. 0.1
111
    method : str
112
        Method used for testing and adjustment of pvalues. Can be either the
113
        full name or initial letters. Available methods are:
114
        - `bonferroni` : one-step correction
115
        - `sidak` : one-step correction
116
        - `holm-sidak` : step down method using Sidak adjustments
117
        - `holm` : step-down method using Bonferroni adjustments
118
        - `simes-hochberg` : step-up method  (independent)
119
        - `hommel` : closed method based on Simes tests (non-negative)
120
        - `fdr_bh` : Benjamini/Hochberg  (non-negative)
121
        - `fdr_by` : Benjamini/Yekutieli (negative)
122
        - `fdr_tsbh` : two stage fdr correction (non-negative)
123
        - `fdr_tsbky` : two stage fdr correction (non-negative)
124
    is_sorted : bool
125
        If False (default), the p_values will be sorted, but the corrected
126
        pvalues are in the original order. If True, then it assumed that the
127
        pvalues are already sorted in ascending order.
128
    returnsorted : bool
129
         not tested, return sorted p-values instead of original sequence
130
    Returns
131
    -------
132
    reject : ndarray, boolean
133
        true for hypothesis that can be rejected for given alpha
134
    pvals_corrected : ndarray
135
        p-values corrected for multiple tests
136
    alphacSidak: float
137
        corrected alpha for Sidak method
138
    alphacBonf: float
139
        corrected alpha for Bonferroni method
140
    Notes
141
    -----
142
    There may be API changes for this function in the future.
143
    Except for 'fdr_twostage', the p-value correction is independent of the
144
    alpha specified as argument. In these cases the corrected p-values
145
    can also be compared with a different alpha. In the case of 'fdr_twostage',
146
    the corrected p-values are specific to the given alpha, see
147
    ``fdrcorrection_twostage``.
148
    The 'fdr_gbs' procedure is not verified against another package, p-values
149
    are derived from scratch and are not derived in the reference. In Monte
150
    Carlo experiments the method worked correctly and maintained the false
151
    discovery rate.
152
    All procedures that are included, control FWER or FDR in the independent
153
    case, and most are robust in the positively correlated case.
154
    `fdr_gbs`: high power, fdr control for independent case and only small
155
    violation in positively correlated case
156
    **Timing**:
157
    Most of the time with large arrays is spent in `argsort`. When
158
    we want to calculate the p-value for several methods, then it is more
159
    efficient to presort the pvalues, and put the results back into the
160
    original order outside of the function.
161
    Method='hommel' is very slow for large arrays, since it requires the
162
    evaluation of n partitions, where n is the number of p-values.
163
    """
164
    import gc
×
165

UNCOV
166
    pvals = np.asarray(pvals)
×
UNCOV
167
    alphaf = alpha  # Notation ?
×
168

169
    if not is_sorted:
×
170
        sortind = np.argsort(pvals)
×
UNCOV
171
        pvals = np.take(pvals, sortind)
×
172

173
    ntests = len(pvals)
×
NEW
174
    alphacSidak = 1 - np.power((1.0 - alphaf), 1.0 / ntests)
×
175
    alphacBonf = alphaf / float(ntests)
×
176
    if method.lower() in ['b', 'bonf', 'bonferroni']:
×
177
        reject = pvals <= alphacBonf
×
UNCOV
178
        pvals_corrected = pvals * float(ntests)
×
179

180
    elif method.lower() in ['s', 'sidak']:
×
181
        reject = pvals <= alphacSidak
×
NEW
182
        pvals_corrected = 1 - np.power((1.0 - pvals), ntests)
×
183

184
    elif method.lower() in ['hs', 'holm-sidak']:
×
NEW
185
        alphacSidak_all = 1 - np.power((1.0 - alphaf), 1.0 / np.arange(ntests, 0, -1))
×
186
        notreject = pvals > alphacSidak_all
×
187
        del alphacSidak_all
×
188

UNCOV
189
        nr_index = np.nonzero(notreject)[0]
×
190
        if nr_index.size == 0:
×
191
            # nonreject is empty, all rejected
192
            notrejectmin = len(pvals)
×
193
        else:
194
            notrejectmin = np.min(nr_index)
×
195
        notreject[notrejectmin:] = True
×
UNCOV
196
        reject = ~notreject
×
197
        del notreject
×
198

NEW
199
        pvals_corrected_raw = 1 - np.power((1.0 - pvals), np.arange(ntests, 0, -1))
×
UNCOV
200
        pvals_corrected = np.maximum.accumulate(pvals_corrected_raw)
×
201
        del pvals_corrected_raw
×
202

203
    elif method.lower() in ['h', 'holm']:
×
UNCOV
204
        notreject = pvals > alphaf / np.arange(ntests, 0, -1)
×
205
        nr_index = np.nonzero(notreject)[0]
×
UNCOV
206
        if nr_index.size == 0:
×
207
            # nonreject is empty, all rejected
208
            notrejectmin = len(pvals)
×
209
        else:
210
            notrejectmin = np.min(nr_index)
×
211
        notreject[notrejectmin:] = True
×
212
        reject = ~notreject
×
213
        pvals_corrected_raw = pvals * np.arange(ntests, 0, -1)
×
UNCOV
214
        pvals_corrected = np.maximum.accumulate(pvals_corrected_raw)
×
215
        del pvals_corrected_raw
×
216
        gc.collect()
×
217

218
    elif method.lower() in ['sh', 'simes-hochberg']:
×
219
        alphash = alphaf / np.arange(ntests, 0, -1)
×
220
        reject = pvals <= alphash
×
221
        rejind = np.nonzero(reject)
×
222
        if rejind[0].size > 0:
×
223
            rejectmax = np.max(np.nonzero(reject))
×
224
            reject[:rejectmax] = True
×
UNCOV
225
        pvals_corrected_raw = np.arange(ntests, 0, -1) * pvals
×
226
        pvals_corrected = np.minimum.accumulate(pvals_corrected_raw[::-1])[::-1]
×
UNCOV
227
        del pvals_corrected_raw
×
228

229
    elif method.lower() in ['ho', 'hommel']:
×
230
        # we need a copy because we overwrite it in a loop
231
        a = pvals.copy()
×
232
        for m in range(ntests, 1, -1):
×
NEW
233
            cim = np.min(m * pvals[-m:] / np.arange(1, m + 1.0))
×
234
            a[-m:] = np.maximum(a[-m:], cim)
×
UNCOV
235
            a[:-m] = np.maximum(a[:-m], np.minimum(m * pvals[:-m], cim))
×
236
        pvals_corrected = a
×
UNCOV
237
        reject = a <= alphaf
×
238

239
    elif method.lower() in ['fdr_bh', 'fdr_i', 'fdr_p', 'fdri', 'fdrp']:
×
240
        # delegate, call with sorted pvals
NEW
241
        reject, pvals_corrected = fdrcorrection(pvals, alpha=alpha, method='indep', is_sorted=True)
×
242
    elif method.lower() in ['fdr_by', 'fdr_n', 'fdr_c', 'fdrn', 'fdrcorr']:
×
243
        # delegate, call with sorted pvals
NEW
244
        reject, pvals_corrected = fdrcorrection(pvals, alpha=alpha, method='n', is_sorted=True)
×
245
    elif method.lower() in ['fdr_tsbky', 'fdr_2sbky', 'fdr_twostage']:
×
246
        # delegate, call with sorted pvals
NEW
247
        reject, pvals_corrected = fdrcorrection_twostage(pvals, alpha=alpha, method='bky', is_sorted=True)[:2]
×
248
    elif method.lower() in ['fdr_tsbh', 'fdr_2sbh']:
×
249
        # delegate, call with sorted pvals
NEW
250
        reject, pvals_corrected = fdrcorrection_twostage(pvals, alpha=alpha, method='bh', is_sorted=True)[:2]
×
251

UNCOV
252
    elif method.lower() in ['fdr_gbs']:
×
253
        # adaptive stepdown in Gavrilov, Benjamini, Sarkar, Annals of Statistics 2009
254

255
        ii = np.arange(1, ntests + 1)
×
NEW
256
        q = (ntests + 1.0 - ii) / ii * pvals / (1.0 - pvals)
×
257
        pvals_corrected_raw = np.maximum.accumulate(q)  # up requirementd
×
258

UNCOV
259
        pvals_corrected = np.minimum.accumulate(pvals_corrected_raw[::-1])[::-1]
×
260
        del pvals_corrected_raw
×
261
        reject = pvals_corrected <= alpha
×
262

263
    else:
264
        raise ValueError('method not recognized')
×
265

UNCOV
266
    if pvals_corrected is not None:  # not necessary anymore
×
UNCOV
267
        pvals_corrected[pvals_corrected > 1] = 1
×
UNCOV
268
    if is_sorted or returnsorted:
×
UNCOV
269
        return reject, pvals_corrected, alphacSidak, alphacBonf
×
270
    else:
UNCOV
271
        pvals_corrected_ = np.empty_like(pvals_corrected)
×
UNCOV
272
        pvals_corrected_[sortind] = pvals_corrected
×
UNCOV
273
        del pvals_corrected
×
UNCOV
274
        reject_ = np.empty_like(reject)
×
UNCOV
275
        reject_[sortind] = reject
×
UNCOV
276
        return reject_, pvals_corrected_, alphacSidak, alphacBonf
×
277

278

279
def fdrcorrection(pvals, alpha=0.05, method='indep', is_sorted=False):
1✔
280
    """pvalue correction for false discovery rate
281
    This covers Benjamini/Hochberg for independent or positively correlated and
282
    Benjamini/Yekutieli for general or negatively correlated tests. Both are
283
    available in the function multipletests, as method=`fdr_bh`, resp. `fdr_by`.
284
    Parameters
285
    ----------
286
    pvals : array_like
287
        set of p-values of the individual tests.
288
    alpha : float
289
        error rate
290
    method : {'indep', 'negcorr')
291
    Returns
292
    -------
293
    rejected : ndarray, bool
294
        True if a hypothesis is rejected, False if not
295
    pvalue-corrected : ndarray
296
        pvalues adjusted for multiple hypothesis testing to limit FDR
297
    Notes
298
    -----
299
    If there is prior information on the fraction of true hypothesis, then alpha
300
    should be set to alpha * m/m_0 where m is the number of tests,
301
    given by the p-values, and m_0 is an estimate of the true hypothesis.
302
    (see Benjamini, Krieger and Yekuteli)
303
    The two-step method of Benjamini, Krieger and Yekutiel that estimates the number
304
    of false hypotheses will be available (soon).
305
    Method names can be abbreviated to first letter, 'i' or 'p' for fdr_bh and 'n' for
306
    fdr_by.
307
    """
308
    pvals = np.asarray(pvals)
×
309

UNCOV
310
    if not is_sorted:
×
311
        pvals_sortind = np.argsort(pvals)
×
312
        pvals_sorted = np.take(pvals, pvals_sortind)
×
313
    else:
314
        pvals_sorted = pvals  # alias
×
315

UNCOV
316
    if method in ['i', 'indep', 'p', 'poscorr']:
×
317
        ecdffactor = _ecdf(pvals_sorted)
×
318
    elif method in ['n', 'negcorr']:
×
NEW
319
        cm = np.sum(1.0 / np.arange(1, len(pvals_sorted) + 1))  # corrected this
×
320
        ecdffactor = _ecdf(pvals_sorted) / cm
×
321
    else:
322
        raise ValueError('only indep and negcorr implemented')
×
323
    reject = pvals_sorted <= ecdffactor * alpha
×
324
    if reject.any():
×
325
        rejectmax = max(np.nonzero(reject)[0])
×
326
        reject[:rejectmax] = True
×
327

UNCOV
328
    pvals_corrected_raw = pvals_sorted / ecdffactor
×
329
    pvals_corrected = np.minimum.accumulate(pvals_corrected_raw[::-1])[::-1]
×
UNCOV
330
    del pvals_corrected_raw
×
UNCOV
331
    pvals_corrected[pvals_corrected > 1] = 1
×
UNCOV
332
    if not is_sorted:
×
UNCOV
333
        pvals_corrected_ = np.empty_like(pvals_corrected)
×
UNCOV
334
        pvals_corrected_[pvals_sortind] = pvals_corrected
×
UNCOV
335
        del pvals_corrected
×
UNCOV
336
        reject_ = np.empty_like(reject)
×
UNCOV
337
        reject_[pvals_sortind] = reject
×
UNCOV
338
        return reject_, pvals_corrected_
×
339
    else:
UNCOV
340
        return reject, pvals_corrected
×
341

342

343
def fdrcorrection_twostage(pvals, alpha=0.05, method='bky', iter=False, is_sorted=False):
1✔
344
    """(iterated) two stage linear step-up procedure with estimation of number of true
345
    hypotheses
346
    Benjamini, Krieger and Yekuteli, procedure in Definition 6
347
    Parameters
348
    ----------
349
    pvals : array_like
350
        set of p-values of the individual tests.
351
    alpha : float
352
        error rate
353
    method : {'bky', 'bh')
354
        see Notes for details
355
        * 'bky' - implements the procedure in Definition 6 of Benjamini, Krieger
356
           and Yekuteli 2006
357
        * 'bh' - the two stage method of Benjamini and Hochberg
358
    iter : bool
359
    Returns
360
    -------
361
    rejected : ndarray, bool
362
        True if a hypothesis is rejected, False if not
363
    pvalue-corrected : ndarray
364
        pvalues adjusted for multiple hypotheses testing to limit FDR
365
    m0 : int
366
        ntest - rej, estimated number of true hypotheses
367
    alpha_stages : list of floats
368
        A list of alphas that have been used at each stage
369
    Notes
370
    -----
371
    The returned corrected p-values are specific to the given alpha, they
372
    cannot be used for a different alpha.
373
    The returned corrected p-values are from the last stage of the fdr_bh
374
    linear step-up procedure (fdrcorrection0 with method='indep') corrected
375
    for the estimated fraction of true hypotheses.
376
    This means that the rejection decision can be obtained with
377
    ``pval_corrected <= alpha``, where ``alpha`` is the original significance
378
    level.
379
    (Note: This has changed from earlier versions (<0.5.0) of statsmodels.)
380
    BKY described several other multi-stage methods, which would be easy to implement.
381
    However, in their simulation the simple two-stage method (with iter=False) was the
382
    most robust to the presence of positive correlation
383
    TODO: What should be returned?
384
    """
385
    pvals = np.asarray(pvals)
×
386

387
    if not is_sorted:
×
UNCOV
388
        pvals_sortind = np.argsort(pvals)
×
389
        pvals = np.take(pvals, pvals_sortind)
×
390

391
    ntests = len(pvals)
×
392
    if method == 'bky':
×
NEW
393
        fact = 1.0 + alpha
×
394
        alpha_prime = alpha / fact
×
UNCOV
395
    elif method == 'bh':
×
NEW
396
        fact = 1.0
×
397
        alpha_prime = alpha
×
398
    else:
399
        raise ValueError("only 'bky' and 'bh' are available as method")
×
400

401
    alpha_stages = [alpha_prime]
×
NEW
402
    rej, pvalscorr = fdrcorrection(pvals, alpha=alpha_prime, method='indep', is_sorted=True)
×
403
    r1 = rej.sum()
×
404
    if (r1 == 0) or (r1 == ntests):
×
UNCOV
405
        return rej, pvalscorr * fact, ntests - r1, alpha_stages
×
406
    ri_old = r1
×
407

UNCOV
408
    while True:
×
UNCOV
409
        ntests0 = 1.0 * ntests - ri_old
×
UNCOV
410
        alpha_star = alpha_prime * ntests / ntests0
×
411
        alpha_stages.append(alpha_star)
×
412
        # print ntests0, alpha_star
NEW
413
        rej, pvalscorr = fdrcorrection(pvals, alpha=alpha_star, method='indep', is_sorted=True)
×
UNCOV
414
        ri = rej.sum()
×
415
        if (not iter) or ri == ri_old:
×
416
            break
×
417
        elif ri < ri_old:
×
418
            # prevent cycles and endless loops
NEW
419
            raise RuntimeError(' oops - should not be here')
×
420
        ri_old = ri
×
421

422
    # make adjustment to pvalscorr to reflect estimated number of Non-Null cases
423
    # decision is then pvalscorr < alpha  (or <=)
UNCOV
424
    pvalscorr *= ntests0 * 1.0 / ntests
×
UNCOV
425
    if method == 'bky':
×
NEW
426
        pvalscorr *= 1.0 + alpha
×
427

UNCOV
428
    if not is_sorted:
×
UNCOV
429
        pvalscorr_ = np.empty_like(pvalscorr)
×
UNCOV
430
        pvalscorr_[pvals_sortind] = pvalscorr
×
UNCOV
431
        del pvalscorr
×
UNCOV
432
        reject = np.empty_like(rej)
×
UNCOV
433
        reject[pvals_sortind] = rej
×
UNCOV
434
        return reject, pvalscorr_, ntests - ri, alpha_stages
×
435
    else:
UNCOV
436
        return rej, pvalscorr, ntests - ri, alpha_stages
×
437

438

439
def local_fdr(zscores, null_proportion=1.0, null_pdf=None, deg=7, nbins=30):
1✔
440
    """
441
    Calculate local FDR values for a list of Z-scores.
442
    Parameters
443
    ----------
444
    zscores : array_like
445
        A vector of Z-scores
446
    null_proportion : float
447
        The assumed proportion of true null hypotheses
448
    null_pdf : function mapping reals to positive reals
449
        The density of null Z-scores; if None, use standard normal
450
    deg : int
451
        The maximum exponent in the polynomial expansion of the
452
        density of non-null Z-scores
453
    nbins : int
454
        The number of bins for estimating the marginal density
455
        of Z-scores.
456
    Returns
457
    -------
458
    fdr : array_like
459
        A vector of FDR values
460
    References
461
    ----------
462
    B Efron (2008).  Microarrays, Empirical Bayes, and the Two-Groups
463
    Model.  Statistical Science 23:1, 1-22.
464
    Examples
465
    --------
466
    Basic use (the null Z-scores are taken to be standard normal):
467
    >>> from statsmodels.stats.multitest import local_fdr
468
    >>> import numpy as np
469
    >>> zscores = np.random.randn(30)
470
    >>> fdr = local_fdr(zscores)
471
    Use a Gaussian null distribution estimated from the data:
472
    >>> null = EmpiricalNull(zscores)
473
    >>> fdr = local_fdr(zscores, null_pdf=null.pdf)
474
    """
475

UNCOV
476
    from statsmodels.genmod.generalized_linear_model import GLM
×
477
    from statsmodels.genmod.generalized_linear_model import families
×
UNCOV
478
    from statsmodels.regression.linear_model import OLS
×
479

480
    # Bins for Poisson modeling of the marginal Z-score density
UNCOV
481
    minz = min(zscores)
×
UNCOV
482
    maxz = max(zscores)
×
483
    bins = np.linspace(minz, maxz, nbins)
×
484

485
    # Bin counts
486
    zhist = np.histogram(zscores, bins)[0]
×
487

488
    # Bin centers
UNCOV
489
    zbins = (bins[:-1] + bins[1:]) / 2
×
490

491
    # The design matrix at bin centers
UNCOV
492
    dmat = np.vander(zbins, deg + 1)
×
493

494
    # Use this to get starting values for Poisson regression
UNCOV
495
    md = OLS(np.log(1 + zhist), dmat).fit()
×
496

497
    # Poisson regression
UNCOV
498
    md = GLM(zhist, dmat, family=families.Poisson()).fit(start_params=md.params)
×
499

500
    # The design matrix for all Z-scores
501
    dmat_full = np.vander(zscores, deg + 1)
×
502

503
    # The height of the estimated marginal density of Z-scores,
504
    # evaluated at every observed Z-score.
UNCOV
505
    fz = md.predict(dmat_full) / (len(zscores) * (bins[1] - bins[0]))
×
506

507
    # The null density.
UNCOV
508
    if null_pdf is None:
×
UNCOV
509
        f0 = np.exp(-0.5 * zscores**2) / np.sqrt(2 * np.pi)
×
510
    else:
UNCOV
511
        f0 = null_pdf(zscores)
×
512

513
    # The local FDR values
UNCOV
514
    fdr = null_proportion * f0 / fz
×
515

UNCOV
516
    fdr = np.clip(fdr, 0, 1)
×
517

UNCOV
518
    return fdr
×
519

520

521
class NullDistribution(object):
1✔
522
    """
523
    Estimate a Gaussian distribution for the null Z-scores.
524
    The observed Z-scores consist of both null and non-null values.
525
    The fitted distribution of null Z-scores is Gaussian, but may have
526
    non-zero mean and/or non-unit scale.
527
    Parameters
528
    ----------
529
    zscores : array_like
530
        The observed Z-scores.
531
    null_lb : float
532
        Z-scores between `null_lb` and `null_ub` are all considered to be
533
        true null hypotheses.
534
    null_ub : float
535
        See `null_lb`.
536
    estimate_mean : bool
537
        If True, estimate the mean of the distribution.  If False, the
538
        mean is fixed at zero.
539
    estimate_scale : bool
540
        If True, estimate the scale of the distribution.  If False, the
541
        scale parameter is fixed at 1.
542
    estimate_null_proportion : bool
543
        If True, estimate the proportion of true null hypotheses (i.e.
544
        the proportion of z-scores with expected value zero).  If False,
545
        this parameter is fixed at 1.
546
    Attributes
547
    ----------
548
    mean : float
549
        The estimated mean of the empirical null distribution
550
    sd : float
551
        The estimated standard deviation of the empirical null distribution
552
    null_proportion : float
553
        The estimated proportion of true null hypotheses among all hypotheses
554
    References
555
    ----------
556
    B Efron (2008).  Microarrays, Empirical Bayes, and the Two-Groups
557
    Model.  Statistical Science 23:1, 1-22.
558
    Notes
559
    -----
560
    See also:
561
    http://nipy.org/nipy/labs/enn.html#nipy.algorithms.statistics.empirical_pvalue.NormalEmpiricalNull.fdr
562
    """
563

564
    def __init__(self, zscores, null_lb=-1, null_ub=1, estimate_mean=True, estimate_scale=True, estimate_null_proportion=False):
1✔
565

566
        # Extract the null z-scores
567
        ii = np.flatnonzero((zscores >= null_lb) & (zscores <= null_ub))
×
568
        if len(ii) == 0:
×
NEW
569
            raise RuntimeError('No Z-scores fall between null_lb and null_ub')
×
570
        zscores0 = zscores[ii]
×
571

572
        # Number of Z-scores, and null Z-scores
573
        n_zs, n_zs0 = len(zscores), len(zscores0)
×
574

575
        # Unpack and transform the parameters to the natural scale, hold
576
        # parameters fixed as specified.
577
        def xform(params):
×
578

NEW
579
            mean = 0.0
×
NEW
580
            sd = 1.0
×
NEW
581
            prob = 1.0
×
582

UNCOV
583
            ii = 0
×
UNCOV
584
            if estimate_mean:
×
UNCOV
585
                mean = params[ii]
×
UNCOV
586
                ii += 1
×
UNCOV
587
            if estimate_scale:
×
UNCOV
588
                sd = np.exp(params[ii])
×
UNCOV
589
                ii += 1
×
UNCOV
590
            if estimate_null_proportion:
×
591
                prob = 1 / (1 + np.exp(-params[ii]))
×
592

UNCOV
593
            return mean, sd, prob
×
594

UNCOV
595
        from scipy.stats.distributions import norm
×
596

597
        def fun(params):
×
598
            """
599
            Negative log-likelihood of z-scores.
600
            The function has three arguments, packed into a vector:
601
            mean : location parameter
602
            logscale : log of the scale parameter
603
            logitprop : logit of the proportion of true nulls
604
            The implementation follows section 4 from Efron 2008.
605
            """
606

607
            d, s, p = xform(params)
×
608

609
            # Mass within the central region
NEW
610
            central_mass = norm.cdf((null_ub - d) / s) - norm.cdf((null_lb - d) / s)
×
611

612
            # Probability that a Z-score is null and is in the central region
613
            cp = p * central_mass
×
614

615
            # Binomial term
616
            rval = n_zs0 * np.log(cp) + (n_zs - n_zs0) * np.log(1 - cp)
×
617

618
            # Truncated Gaussian term for null Z-scores
UNCOV
619
            zv = (zscores0 - d) / s
×
NEW
620
            rval += np.sum(-(zv**2) / 2) - n_zs0 * np.log(s)
×
UNCOV
621
            rval -= n_zs0 * np.log(central_mass)
×
622

UNCOV
623
            return -rval
×
624

625
        # Estimate the parameters
UNCOV
626
        from scipy.optimize import minimize
×
627

628
        # starting values are mean = 0, scale = 1, p0 ~ 1
NEW
629
        mz = minimize(fun, np.r_[0.0, 0, 3], method='Nelder-Mead')
×
UNCOV
630
        mean, sd, prob = xform(mz['x'])
×
631

UNCOV
632
        self.mean = mean
×
UNCOV
633
        self.sd = sd
×
UNCOV
634
        self.null_proportion = prob
×
635

636
    # The fitted null density function
637
    def pdf(self, zscores):
1✔
638
        """
639
        Evaluates the fitted empirical null Z-score density.
640
        Parameters
641
        ----------
642
        zscores : scalar or array_like
643
            The point or points at which the density is to be
644
            evaluated.
645
        Returns
646
        -------
647
        The empirical null Z-score density evaluated at the given
648
        points.
649
        """
650

UNCOV
651
        zval = (zscores - self.mean) / self.sd
×
UNCOV
652
        return np.exp(-0.5 * zval**2 - np.log(self.sd) - 0.5 * np.log(2 * np.pi))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc