SAFFRON: Adaptive Online FDR Control¶
SAFFRON (Serial estimate of the Alpha Fraction that is Futilely Rationed On true Nulls) is an adaptive algorithm for online FDR control that estimates the proportion of true null hypotheses to set more powerful rejection thresholds.
Original Paper
Ramdas, A., T. Zrnic, M. J. Wainwright, and M. I. Jordan. "SAFFRON: an adaptive algorithm for online control of the false discovery rate." Proceedings of the 35th International Conference on Machine Learning (ICML), Proceedings of Machine Learning Research, vol. 80, pp. 4286-4294, PMLR, 2018. [ArXiv] [ICML]
Overview¶
The Innovation¶
SAFFRON can be seen as an online analogue of the famous offline Storey-BH adaptive procedure. Just as Storey-BH typically achieves higher power than Benjamini-Hochberg by estimating the proportion of true nulls, SAFFRON typically achieves higher power than non-adaptive online methods like LORD.
Key Features¶
- Adaptive null proportion estimation - Uses candidate fraction to estimate alpha-wealth allocated to true nulls
- Alpha-wealth dynamics - Intelligently allocates wealth to different tests over time
- Proven FDR control - Provably controls FDR for independent p-values
- Superior power - Typically more powerful than non-adaptive counterparts
Algorithm Details¶
Core Mechanism¶
SAFFRON starts with alpha-wealth that it allocates to tests over time, earning back wealth on discoveries. Unlike older alpha-investing methods, SAFFRON's thresholds are based on estimating the alpha fraction allocated to true null hypotheses using the candidate threshold λ.
Wealth Dynamics¶
The algorithm maintains wealth that: - Starts at initial value W₀
- Is spent to purchase rejection thresholds - Is earned back from discoveries - Adapts based on estimated proportion of true nulls via candidates
Class Reference¶
online_fdr.investing.saffron.saffron.Saffron
¶
Bases: AbstractSequentialTest
SAFFRON: Serial estimate of the Alpha Fraction that is Futilely Rationed On true Nulls.
SAFFRON is an adaptive algorithm for online FDR control that estimates the proportion of true null hypotheses and uses this estimate to set more powerful rejection thresholds. It can be seen as an online analogue of the famous offline Storey-BH adaptive procedure.
Like alpha-investing algorithms, SAFFRON starts with alpha-wealth that it intelligently allocates to different tests over time, earning back wealth on discoveries. However, unlike older methods, SAFFRON's threshold sequence is based on a novel estimate of the alpha fraction allocated to true null hypotheses.
SAFFRON typically achieves higher power than non-adaptive methods like LORD under independence, similar to how Storey-BH typically outperforms Benjamini-Hochberg.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
alpha | float | Target FDR level (e.g., 0.05 for 5% FDR). Must be in (0, 1). | required |
wealth | float | Initial alpha-wealth for purchasing rejection thresholds. Must satisfy 0 ≤ wealth ≤ alpha. | required |
lambda_ | float | Candidate threshold for estimating the proportion of nulls. P-values ≤ lambda_ are considered "candidates". Must be in (0, 1). | required |
Attributes:
Name | Type | Description |
---|---|---|
alpha0 | float | Original target FDR level. |
wealth0 | float | Initial wealth allocation. |
lambda_ | float | Candidate threshold parameter. |
num_test | int | Number of hypotheses tested so far. |
candidates | list[bool] | Boolean list indicating which tests were candidates. |
reject_idx | list[int] | Indices of rejected hypotheses. |
Examples:
>>> # Basic usage with recommended parameters
>>> saffron = Saffron(alpha=0.05, wealth=0.025, lambda_=0.5)
>>> decision = saffron.test_one(0.01) # Test a small p-value
>>> print(f"Rejected: {decision}")
>>> # Sequential testing
>>> p_values = [0.001, 0.3, 0.02, 0.8, 0.005]
>>> decisions = [saffron.test_one(p) for p in p_values]
>>> discoveries = sum(decisions)
References
Ramdas, A., T. Zrnic, M. J. Wainwright, and M. I. Jordan (2018). "SAFFRON: an adaptive algorithm for online control of the FDR." Proceedings of the 35th International Conference on Machine Learning (ICML), Proceedings of Machine Learning Research, vol. 80, pp. 4286-4294, PMLR.
ArXiv: https://arxiv.org/abs/1802.09098 ICML: https://proceedings.mlr.press/v80/ramdas18a.html
Source code in online_fdr/investing/saffron/saffron.py
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
|
Functions¶
test_one(p_val)
¶
Test a single p-value using the SAFFRON procedure.
The SAFFRON algorithm processes p-values sequentially: 1. Calculate adaptive rejection threshold based on candidate estimate 2. Determine if p-value is a candidate (≤ lambda_) 3. Reject if p-value ≤ adaptive threshold
Parameters:
Name | Type | Description | Default |
---|---|---|---|
p_val | float | P-value to test. Must be in [0, 1]. | required |
Returns:
Type | Description |
---|---|
bool | True if the null hypothesis is rejected (discovery), False otherwise. |
Raises:
Type | Description |
---|---|
ValueError | If p_val is not in [0, 1]. |
Examples:
>>> saffron = Saffron(alpha=0.05, wealth=0.025, lambda_=0.5)
>>> saffron.test_one(0.01) # Small p-value, likely rejected
True
>>> saffron.test_one(0.8) # Large p-value, not rejected
False
Source code in online_fdr/investing/saffron/saffron.py
calc_alpha_t()
¶
Calculate the adaptive rejection threshold for the current test.
The SAFFRON threshold is based on estimating the alpha-wealth allocated to true null hypotheses using the candidate fraction. The threshold adapts based on: 1. Initial wealth allocation and candidate estimate (first test) 2. Updated wealth based on discoveries and candidate history (subsequent tests) 3. Compensation factor (1 - lambda_) for candidate selection
Returns:
Type | Description |
---|---|
The adaptive rejection threshold alpha_t, bounded by lambda_. |
Note
This is an internal method called by test_one(). The threshold formula follows the SAFFRON procedure in Ramdas et al. (2018).
Source code in online_fdr/investing/saffron/saffron.py
Usage Examples¶
Basic Usage¶
from online_fdr.investing.saffron.saffron import Saffron
# Create SAFFRON instance with recommended parameters
saffron = Saffron(alpha=0.05, wealth=0.025, lambda_=0.5)
# Test individual p-values
p_values = [0.001, 0.15, 0.03, 0.8, 0.02, 0.45, 0.006]
print("SAFFRON Online Testing:")
discoveries = []
for i, p_value in enumerate(p_values):
decision = saffron.test_one(p_value)
if decision:
discoveries.append(i + 1)
print(f"✓ Test {i+1}: p={p_value:.3f} → DISCOVERY!")
else:
print(f" Test {i+1}: p={p_value:.3f} → no rejection")
print(f"\nTotal discoveries: {len(discoveries)}")
print(f"Discovery indices: {discoveries}")
Parameter Selection¶
# Conservative: Lower lambda for more selective candidate identification
conservative = Saffron(alpha=0.05, wealth=0.01, lambda_=0.25)
# Moderate: Balanced parameters (recommended starting point)
moderate = Saffron(alpha=0.05, wealth=0.025, lambda_=0.5)
# Aggressive: Higher lambda for more candidates
aggressive = Saffron(alpha=0.05, wealth=0.05, lambda_=0.75)
# Test on same data for comparison
test_p_values = [0.001, 0.02, 0.15, 0.03, 0.8, 0.01]
for name, method in [("Conservative", conservative),
("Moderate", moderate),
("Aggressive", aggressive)]:
decisions = [method.test_one(p) for p in test_p_values]
print(f"{name}: {sum(decisions)} discoveries")
Working with Real Data Patterns¶
from online_fdr.utils.generation import DataGenerator, BetaMixtureModel
# Simulate realistic genomics-style data with conservative nulls
dgp = BetaMixtureModel(alt_alpha=0.3, alt_beta=5.0) # Alternatives skewed toward 0
generator = DataGenerator(n=200, pi0=0.9, dgp=dgp) # 90% true nulls
# Create SAFFRON instance
saffron = Saffron(alpha=0.1, wealth=0.05, lambda_=0.5)
print("SAFFRON on Realistic Data:")
print("=" * 35)
true_discoveries = 0
false_discoveries = 0
# Test first 50 hypotheses
for i in range(50):
p_value, is_alternative = generator.sample_one()
decision = saffron.test_one(p_value)
if decision:
if is_alternative:
true_discoveries += 1
result = "TRUE discovery ✓"
else:
false_discoveries += 1
result = "FALSE discovery ✗"
truth = "ALT" if is_alternative else "NULL"
print(f"Test {i+1:2d}: p={p_value:.3f} ({truth}) → REJECT ({result})")
total_discoveries = true_discoveries + false_discoveries
empirical_fdr = false_discoveries / max(total_discoveries, 1)
print(f"\nSummary:")
print(f"True discoveries: {true_discoveries}")
print(f"False discoveries: {false_discoveries}")
print(f"Empirical FDR: {empirical_fdr:.3f}")
print(f"Target FDR: {saffron.alpha0}")
Comparison with Non-Adaptive Methods¶
from online_fdr.investing.lord.three import LordThree
def compare_adaptive_vs_nonadaptive(p_values):
"""Compare SAFFRON (adaptive) with LORD 3 (non-adaptive)."""
print("Adaptive vs Non-Adaptive Comparison:")
print("=" * 40)
# Create methods
saffron = Saffron(alpha=0.05, wealth=0.025, lambda_=0.5)
lord3 = LordThree(alpha=0.05, wealth=0.025, reward=0.025)
# Test both methods
saffron_decisions = [saffron.test_one(p) for p in p_values]
lord3_decisions = [lord3.test_one(p) for p in p_values]
saffron_discoveries = sum(saffron_decisions)
lord3_discoveries = sum(lord3_decisions)
print(f"SAFFRON (adaptive): {saffron_discoveries} discoveries")
print(f"LORD 3 (non-adaptive): {lord3_discoveries} discoveries")
print(f"Power advantage: {saffron_discoveries - lord3_discoveries}")
# Show which additional discoveries SAFFRON made
additional = []
for i, (s_dec, l_dec) in enumerate(zip(saffron_decisions, lord3_decisions)):
if s_dec and not l_dec:
additional.append(f"p={p_values[i]:.3f}")
if additional:
print(f"Additional SAFFRON discoveries: {additional}")
return saffron_discoveries, lord3_discoveries
# Test with mixed p-value scenario
mixed_p_values = [0.001, 0.25, 0.02, 0.7, 0.005, 0.9, 0.04, 0.3, 0.008, 0.6]
compare_adaptive_vs_nonadaptive(mixed_p_values)
Mathematical Foundation¶
Candidate-Based Estimation¶
SAFFRON estimates the proportion of alpha-wealth allocated to true nulls using:
where candidates are p-values ≤ λ.
Threshold Formula¶
The adaptive rejection threshold at time t is:
The wealth-based component adapts based on: - Initial wealth allocation - Discoveries and candidate history
- Gamma sequence for proper spending
FDR Guarantee¶
Theorem (SAFFRON FDR Control): Under independence of p-values, SAFFRON controls FDR at level α.
Best Practices¶
Parameter Selection Guidelines¶
Lambda (λ) Selection
- λ = 0.25: Conservative, fewer candidates, more selective
- λ = 0.5: Moderate, balanced (recommended default)
- λ = 0.75: Aggressive, more candidates, higher power potential
Wealth (W₀) Selection
- Start with W₀ = α/2 (e.g., 0.025 for α = 0.05)
- Increase for more initial power, decrease for more conservative start
- Must satisfy 0 ≤ W₀ ≤ α
When to Use SAFFRON¶
- Recommended for: Independent p-values with unknown π₀
- Advantages: Higher power than non-adaptive methods, robust performance
- Considerations: Requires tuning λ parameter, assumes independence
Troubleshooting¶
Common Issues
- Low power: Try increasing λ or W₀
- Too aggressive: Decrease λ or W₀
- No early discoveries: SAFFRON needs some candidates to build momentum
References¶
-
Ramdas, A., T. Zrnic, M. J. Wainwright, and M. I. Jordan (2018). "SAFFRON: an adaptive algorithm for online control of the false discovery rate." Proceedings of the 35th International Conference on Machine Learning (ICML), PMLR, 80:4286-4294.
-
Storey, J. D. (2002). "A direct approach to false discovery rates." Journal of the Royal Statistical Society: Series B, 64(3):479-498.
-
Foster, D. P., and R. A. Stine (2008). "α-investing: a procedure for sequential control of expected false discoveries." Journal of the Royal Statistical Society: Series B, 70(2):429-444.
See Also¶
- ADDIS: Handles conservative nulls better than SAFFRON
- LORD variants: Non-adaptive alternatives
- Theory: Mathematical foundations of online FDR control