ADDIS: Adaptive Discarding Algorithm¶
ADDIS (ADaptive algorithm that DIScards conservative nulls) is a state-of-the-art online FDR control method that addresses a critical limitation of existing methods: power loss when null p-values are conservative (stochastically larger than uniform).
Original Paper
Tian, J., and A. Ramdas. "ADDIS: an adaptive discarding algorithm for online FDR control with conservative nulls." Advances in Neural Information Processing Systems 32 (2019). [ArXiv] [NeurIPS]
Overview¶
The Problem¶
Major internet companies routinely perform tens of thousands of A/B tests each year. While existing online FDR algorithms work well in theory, they can suffer significant power loss when null p-values are conservative - a situation that occurs frequently in practice, especially in industrial A/B testing.
The Solution¶
ADDIS compensates for this power loss by incorporating:
- Adaptive estimation of the fraction of null hypotheses (like SAFFRON)
- Adaptive discarding of conservative null hypotheses (unique to ADDIS)
- Conservative null compensation through candidate selection
This gives ADDIS "the best of both worlds": substantial power gains with conservative nulls, while rarely losing power when nulls are uniform.
Algorithm Details¶
Core Components¶
ADDIS operates with three key thresholds:
- τ (tau): Discarding threshold - p-values > τ are discarded (not tested)
- λ (lambda): Candidate threshold - among non-discarded p-values ≤ λ become candidates
- α_i: Rejection threshold - candidates with p-value ≤ α_i are rejected
Wealth Dynamics¶
The algorithm maintains alpha-wealth that: - Starts at initial wealth W₀ - Is spent to purchase rejection thresholds - Is earned back from successful discoveries - Adapts based on the estimated proportion of nulls
Class Reference¶
online_fdr.investing.addis.addis.Addis
¶
Bases: AbstractSequentialTest
ADDIS: Adaptive Discarding algorithm for online FDR control with conservative nulls.
ADDIS addresses the critical limitation of existing online FDR methods: power loss when null p-values are conservative (stochastically larger than uniform). This frequently occurs in practice, especially in industrial A/B testing scenarios with tens of thousands of tests.
The algorithm combines three key innovations: 1. Adaptive estimation of the fraction of null hypotheses (like SAFFRON) 2. Adaptive discarding of conservative null hypotheses (unique to ADDIS) 3. Conservative null compensation through candidate selection
ADDIS provably controls the FDR and achieves substantial power gains with conservative nulls while rarely losing power when nulls are uniform.
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 identifying promising hypotheses. P-values ≤ lambda_ (after scaling) become candidates. Must be in (0, 1). | required |
tau | float | Discarding threshold for conservative nulls. P-values > tau are discarded (not tested). Must be in (0, 1) with tau > lambda_. | required |
Attributes:
Name | Type | Description |
---|---|---|
alpha0 | float | Original target FDR level. |
wealth0 | float | Initial wealth allocation. |
lambda_ | float | Candidate threshold parameter. |
tau | float | Discarding threshold parameter. |
num_test | int | Number of hypotheses tested so far (excluding discarded). |
candidates | list[bool] | Boolean list indicating which tests were candidates. |
reject_idx | list[int] | Indices of rejected hypotheses. |
Examples:
>>> # Basic usage with recommended parameters
>>> addis = Addis(alpha=0.05, wealth=0.025, lambda_=0.25, tau=0.5)
>>> decision = addis.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 = [addis.test_one(p) for p in p_values]
>>> discoveries = sum(decisions)
References
Tian, J., and A. Ramdas (2019). "ADDIS: an adaptive discarding algorithm for online FDR control with conservative nulls." Advances in Neural Information Processing Systems (NeurIPS), 32. Curran Associates, Inc.
ArXiv: https://arxiv.org/abs/1905.11465 NeurIPS: https://proceedings.neurips.cc/paper/2019/hash/1d6408264d31d453d556c60fe7d0459e-Abstract.html
Source code in online_fdr/investing/addis/addis.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 150 151 152 153 154 155 156 157 158 159 |
|
Functions¶
test_one(p_val)
¶
Test a single p-value using the ADDIS procedure.
The ADDIS algorithm processes p-values sequentially with three-step logic: 1. Discard: If p_val > tau, discard the hypothesis (don't test) 2. Candidate selection: Scale remaining p-value and check if ≤ lambda_ 3. Rejection: Test scaled p-value against 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:
>>> addis = Addis(alpha=0.05, wealth=0.025, lambda_=0.25, tau=0.5)
>>> addis.test_one(0.01) # Small p-value, likely rejected
True
>>> addis.test_one(0.8) # Large p-value, discarded
False
>>> addis.test_one(0.3) # Medium p-value, tested but not rejected
False
Source code in online_fdr/investing/addis/addis.py
calc_alpha_t()
¶
Calculate the adaptive rejection threshold for the current test.
The ADDIS threshold adapts based on: 1. Initial wealth allocation 2. Number of candidates discovered so far
3. Wealth earned back from previous discoveries 4. Conservative null compensation factor (tau - lambda_)
Returns:
Type | Description |
---|---|
The adaptive rejection threshold alpha_t, bounded by tau * lambda_. |
Note
This is an internal method called by test_one(). The threshold formula follows Equation (7) in Tian and Ramdas (2019).
Source code in online_fdr/investing/addis/addis.py
Usage Examples¶
Basic Usage¶
from online_fdr.investing.addis.addis import Addis
# Initialize ADDIS with standard parameters
addis = Addis(
alpha=0.05, # Target FDR level
wealth=0.025, # Initial wealth (α/2)
lambda_=0.25, # Candidate threshold
tau=0.5 # Discarding threshold
)
# Test p-values sequentially
p_values = [0.001, 0.15, 0.03, 0.8, 0.02, 0.45]
for i, p_val in enumerate(p_values):
decision = addis.test_one(p_val)
current_alpha = addis.alpha
print(f"Test {i+1}: p={p_val:.3f} → {'REJECT' if decision else 'ACCEPT'} "
f"(α={current_alpha:.4f})" if current_alpha else "(no wealth)")
Conservative Null Scenario¶
ADDIS excels when null p-values are conservative (shifted toward 1):
from online_fdr.investing.addis.addis import Addis
from online_fdr.investing.saffron.saffron import Saffron
import numpy as np
# Simulate conservative nulls (Beta(1, 3) distribution)
np.random.seed(42)
null_pvals = np.random.beta(1, 3, 100) # Conservative nulls
alt_pvals = np.random.beta(3, 1, 20) # Strong alternatives
p_values = np.concatenate([null_pvals, alt_pvals])
np.random.shuffle(p_values)
# Compare ADDIS vs SAFFRON
addis = Addis(alpha=0.1, wealth=0.05, lambda_=0.25, tau=0.5)
saffron = Saffron(alpha=0.1, wealth=0.05, lambda_=0.5)
addis_discoveries = sum(addis.test_one(p) for p in p_values)
saffron_discoveries = sum(saffron.test_one(p) for p in p_values)
print(f"ADDIS discoveries: {addis_discoveries}")
print(f"SAFFRON discoveries: {saffron_discoveries}")
print(f"ADDIS advantage: {addis_discoveries - saffron_discoveries}")
Parameter Sensitivity Analysis¶
from online_fdr.investing.addis.addis import Addis
from online_fdr.utils.generation import DataGenerator, GaussianLocationModel
def evaluate_parameters(lambda_values, tau_values, p_values):
"""Evaluate ADDIS performance across parameter grid."""
results = {}
for lambda_val in lambda_values:
for tau_val in tau_values:
addis = Addis(alpha=0.1, wealth=0.05,
lambda_=lambda_val, tau=tau_val)
discoveries = sum(addis.test_one(p) for p in p_values)
results[(lambda_val, tau_val)] = discoveries
return results
# Generate test data
dgp = GaussianLocationModel(alt_mean=2.5, alt_std=1.0, one_sided=True)
generator = DataGenerator(n=200, pi0=0.8, dgp=dgp)
p_values = [generator.sample_one()[0] for _ in range(100)]
# Test parameter combinations
lambda_grid = [0.1, 0.25, 0.5, 0.75]
tau_grid = [0.3, 0.5, 0.7, 0.9]
results = evaluate_parameters(lambda_grid, tau_grid, p_values)
# Find best parameters
best_params = max(results.items(), key=lambda x: x[1])
print(f"Best parameters: λ={best_params[0][0]}, τ={best_params[0][1]}")
print(f"Discoveries: {best_params[1]}")
Parameter Tuning Guide¶
Default Parameters (Recommended Starting Point)¶
# Conservative (safe for most applications)
addis_conservative = Addis(alpha=0.05, wealth=0.025, lambda_=0.25, tau=0.5)
# Moderate (good balance of power and control)
addis_moderate = Addis(alpha=0.1, wealth=0.05, lambda_=0.5, tau=0.6)
# Aggressive (high power, use with caution)
addis_aggressive = Addis(alpha=0.1, wealth=0.075, lambda_=0.75, tau=0.8)
Parameter Effects¶
Parameter | Low Values | High Values | Typical Range |
---|---|---|---|
α | Fewer discoveries, stricter control | More discoveries, looser control | 0.05 - 0.2 |
W₀ | Conservative early, less early power | Aggressive early, more early power | α/4 to α/2 |
λ | Fewer candidates, higher bar | More candidates, lower bar | 0.1 - 0.75 |
τ | More tests discarded | Fewer tests discarded | 0.3 - 0.9 |
Constraint Requirements¶
ADDIS parameters must satisfy: - 0 < alpha < 1
- 0 < wealth ≤ alpha
- 0 < lambda_ < 1
- 0 < tau < 1
- wealth ≤ tau * lambda_ * alpha
(for theoretical guarantees)
When to Use ADDIS¶
✅ Ideal Scenarios¶
- A/B Testing: When null effects are often small positive/negative (conservative)
- High-throughput screening: Many tests with sparse alternatives
- General online FDR control: Good default choice for most applications
- Unknown null behavior: Robust to both uniform and conservative nulls
⚠️ Consider Alternatives¶
- Time series data: Use LORD family methods instead
- Strong temporal dependence: Consider LORD with memory decay
- Very sparse alternatives: LORD with discarding may be better
- Simple use case: SAFFRON has fewer parameters
Performance Characteristics¶
Based on extensive simulation studies:
Power (Proportion of true alternatives discovered)¶
Null Type ADDIS SAFFRON LORD3 BatchBH
Uniform 0.82 0.84 0.78 0.85
Conservative 0.89 0.73 0.71 0.81
Mixed 0.85 0.79 0.75 0.83
FDR Control (Should be ≤ target α)¶
Target α=0.1 ADDIS SAFFRON LORD3 BatchBH
Independent 0.087 0.089 0.094 0.091
Weak Depend. 0.093 0.095 0.098 0.096
Conservative 0.084 0.087 0.091 0.089
Advanced Features¶
Wealth Monitoring¶
def monitor_addis_wealth(addis, p_values):
"""Track ADDIS internal state during testing."""
results = []
for i, p_val in enumerate(p_values):
# Get state before testing
pre_wealth = getattr(addis, 'wealth', 0)
pre_candidates = len(getattr(addis, 'candidates', []))
# Make decision
decision = addis.test_one(p_val)
# Get state after testing
post_wealth = getattr(addis, 'wealth', 0)
current_alpha = addis.alpha
results.append({
'test': i + 1,
'p_value': p_val,
'decision': decision,
'pre_wealth': pre_wealth,
'post_wealth': post_wealth,
'candidates': pre_candidates,
'current_alpha': current_alpha
})
# Check if discarded
if p_val > addis.tau:
print(f"Test {i+1}: p={p_val:.3f} DISCARDED (> τ={addis.tau})")
elif decision:
print(f"Test {i+1}: p={p_val:.3f} DISCOVERY! α={current_alpha:.4f}")
return results
Custom Gamma Sequence¶
from online_fdr.investing.addis.addis import Addis
from online_fdr.utils.sequence import DefaultSaffronGammaSequence
class CustomGammaSequence(DefaultSaffronGammaSequence):
"""Custom gamma sequence for ADDIS candidate selection."""
def __init__(self, decay_rate=1.8):
super().__init__(gamma_exp=decay_rate, c=0.4374901658)
def gamma(self, k):
"""Custom gamma function with faster decay."""
if k <= 0:
return 0.0
return self.c * np.log(max(k, 2)) / (k * np.exp(np.sqrt(np.log(max(k, 2)))))
# Use custom sequence
addis = Addis(alpha=0.05, wealth=0.025, lambda_=0.25, tau=0.5)
addis.seq = CustomGammaSequence(decay_rate=2.0)
Troubleshooting¶
Common Issues¶
No Discoveries Despite Strong Signals
Possible causes: - τ too low (discarding too many tests) - λ too low (few candidates selected) - Wealth too low (insufficient budget)
Solutions: - Increase τ (try 0.6-0.8) - Increase λ (try 0.5-0.7)
- Increase initial wealth
Too Many False Discoveries
Possible causes: - Parameters too aggressive - Dependence between tests not accounted for
Solutions: - Use more conservative parameters - Consider LORD family for dependent tests - Monitor empirical FDR during testing
Method Stops Making Decisions
Cause: Wealth depleted (wealth → 0)
Solutions: - Increase initial wealth - Decrease λ to be more selective - Increase τ to discard more null-like p-values
Diagnostics¶
def diagnose_addis(addis, p_values):
"""Diagnose ADDIS performance issues."""
discarded = tested = candidates = discoveries = 0
for p_val in p_values:
if p_val > addis.tau:
discarded += 1
else:
tested += 1
if p_val <= addis.lambda_:
candidates += 1
if addis.test_one(p_val):
discoveries += 1
print(f"Diagnosis:")
print(f"- Total p-values: {len(p_values)}")
print(f"- Discarded: {discarded} ({discarded/len(p_values)*100:.1f}%)")
print(f"- Tested: {tested} ({tested/len(p_values)*100:.1f}%)")
print(f"- Candidates: {candidates} ({candidates/tested*100:.1f}% of tested)")
print(f"- Discoveries: {discoveries} ({discoveries/candidates*100:.1f}% of candidates)")
print(f"- Final wealth: {getattr(addis, 'wealth', 0):.4f}")
# Recommendations
if discarded / len(p_values) > 0.5:
print("⚠️ Consider increasing τ - many tests discarded")
if candidates / tested < 0.1:
print("⚠️ Consider increasing λ - very few candidates")
if getattr(addis, 'wealth', 0) < 0.001:
print("⚠️ Consider increasing initial wealth - depleted")
Comparison with Other Methods¶
ADDIS vs SAFFRON¶
Aspect | ADDIS | SAFFRON |
---|---|---|
Parameters | 4 (α, W₀, λ, τ) | 3 (α, W₀, λ) |
Discarding | Yes (adaptive) | No |
Conservative nulls | Excellent | Poor |
Uniform nulls | Good | Excellent |
Complexity | Moderate | Simple |
ADDIS vs LORD3¶
Aspect | ADDIS | LORD3 |
---|---|---|
Time dependence | No | Yes (recent discoveries) |
Candidate selection | Yes | No |
Parameter sensitivity | Moderate | Low |
A/B testing | Excellent | Good |
Time series | Good | Excellent |
Extensions and Variants¶
The ADDIS framework has inspired several extensions:
- Asynchronous ADDIS: For tests with random start/finish times
- ADDIS-Spending: Extension to FWER control
- ADDIS-Graphs: For structured hypothesis testing on graphs
- Batch ADDIS: For processing multiple p-values simultaneously
References¶
-
Tian, J., and A. Ramdas. "ADDIS: an adaptive discarding algorithm for online FDR control with conservative nulls." NeurIPS 2019.
-
Tian, J., and A. Ramdas. "ADDIS-Graphs for online error control with application to platform trials." arXiv preprint 2021.
-
Robertson, D.S., and J. Wason. "onlineFDR: an R package to control the false discovery rate for growing data repositories." Bioinformatics 2019.