Approach: Use the standard sorting-based Benjamini–Hochberg procedure: sort p-values ascending, compute BH thresholds (i/m)*q, find largest i with p_(i) <= (i/m)q, mark those as significant; compute adjusted p-values (p_adj) as the minimum of (m/i)*p_(i) enforced to be monotonic non-decreasing when mapped back to original order and capped at 1. Handle NaNs and edge q values.python
import numpy as np
def benjamini_hochberg(p_values, q=0.05):
"""
p_values: array-like of p-values (floats). NaNs allowed (treated as non-significant, p_adj = NaN).
q: target FDR in (0,1]
Returns: is_significant (bool array), p_adj (float array)
"""
p = np.asarray(p_values, dtype=float)
m = p.size
if not (0 < q <= 1):
raise ValueError("q must be in (0, 1].")
# Prepare outputs
p_adj = np.full(m, np.nan)
is_significant = np.zeros(m, dtype=bool)
# Indices of non-NaN p-values
valid = ~np.isnan(p)
if not np.any(valid):
return is_significant, p_adj
pv = p[valid]
m_valid = pv.size
# Sort p-values and keep indices
order = np.argsort(pv)
pv_sorted = pv[order]
# BH adjusted (initial)
ranks = np.arange(1, m_valid + 1) # 1-based ranks
q_factors = (m_valid / ranks) * pv_sorted # (m/i) * p_i
# Enforce monotonicity: p_adj_sorted[i] = min_{j>=i} q_factors[j]
p_adj_sorted = np.minimum.accumulate(q_factors[::-1])[::-1]
p_adj_sorted = np.minimum(p_adj_sorted, 1.0)
# Map adjusted p-values back to original positions among valid entries
p_adj_valid = np.empty(m_valid)
p_adj_valid[order] = p_adj_sorted
p_adj[valid] = p_adj_valid
# Determine significance: find largest i where p_sorted[i] <= (i/m_valid)*q
thresholds = (ranks / m_valid) * q
passed = pv_sorted <= thresholds
if np.any(passed):
max_i = np.max(np.nonzero(passed)[0]) # zero-based index
# All p_sorted[0..max_i] are significant
sig_sorted = np.zeros_like(pv_sorted, dtype=bool)
sig_sorted[: max_i + 1] = True
sig_valid = np.empty(m_valid, dtype=bool)
sig_valid[order] = sig_sorted
is_significant[valid] = sig_valid
return is_significant, p_adj
Key points:- Time complexity O(m log m) due to sorting; space O(m).- Edge cases: handles NaNs, q outside (0,1] raises error, returns no-significance when no valid p-values.- Adjusted p-values are monotonic and capped at 1.0. Alternative: use independent vs. positive dependence assumptions (storey method) if desired.