Skip to content

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:

  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

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:

  1. τ (tau): Discarding threshold - p-values > τ are discarded (not tested)
  2. λ (lambda): Candidate threshold - among non-discarded p-values ≤ λ become candidates
  3. α_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
class Addis(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.

    Args:
        alpha: Target FDR level (e.g., 0.05 for 5% FDR). Must be in (0, 1).
        wealth: Initial alpha-wealth for purchasing rejection thresholds. 
                Must satisfy 0 ≤ wealth ≤ alpha.
        lambda_: Candidate threshold for identifying promising hypotheses. 
                 P-values ≤ lambda_ (after scaling) become candidates. Must be in (0, 1).
        tau: Discarding threshold for conservative nulls. P-values > tau are discarded 
             (not tested). Must be in (0, 1) with tau > lambda_.

    Attributes:
        alpha0: Original target FDR level.
        wealth0: Initial wealth allocation.
        lambda_: Candidate threshold parameter.
        tau: Discarding threshold parameter.
        num_test: Number of hypotheses tested so far (excluding discarded).
        candidates: Boolean list indicating which tests were candidates.
        reject_idx: 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
    """

    def __init__(
        self,
        alpha: float,
        wealth: float,
        lambda_: float,
        tau: float,
    ):  # fmt: skip
        super().__init__(alpha)
        self.alpha0: float = alpha
        self.wealth0: float = wealth
        self.lambda_: float = lambda_
        self.tau: float = tau

        validity.check_initial_wealth(wealth, alpha)
        validity.check_candidate_threshold(lambda_)

        self.num_test: int = 0
        self.candidates: list[bool] = []
        self.reject_idx: list[int] = []

        self.seq = DefaultSaffronGammaSequence(gamma_exp=1.6, c=0.4374901658)

    def test_one(self, p_val: float) -> bool:
        """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

        Args:
            p_val: P-value to test. Must be in [0, 1].

        Returns:
            True if the null hypothesis is rejected (discovery), False otherwise.

        Raises:
            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
        """
        validity.check_p_val(p_val)

        if p_val > self.tau:  # discard
            self.alpha = None
            return False

        self.num_test += 1
        self.alpha = self.calc_alpha_t()

        p_val *= 1 / self.tau
        is_candidate = p_val <= self.lambda_  # candidate
        self.candidates.append(is_candidate)

        is_rejected = p_val <= self.alpha  # rejection
        self.reject_idx.append(self.num_test) if is_rejected else None
        return is_rejected

    def calc_alpha_t(self):
        """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:
            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).
        """
        alpha_t = self.wealth0 * self.seq.calc_gamma(
            self.num_test - sum(self.candidates), None
        )
        if len(self.reject_idx) >= 1:
            tau_1 = self.reject_idx[0]
            c_1_plus = sum(self.candidates[tau_1:])
            alpha_t += (self.alpha0 - self.wealth0) * self.seq.calc_gamma(
                (self.num_test - tau_1 - c_1_plus), None
            )
        if len(self.reject_idx) >= 2:
            alpha_t += self.alpha0 * sum(
                self.seq.calc_gamma(
                    (self.num_test - idx - sum(self.candidates[idx:])),
                    None,
                )
                for idx in self.reject_idx[1:]
            )
        alpha_t *= self.tau - self.lambda_
        return min(self.tau * self.lambda_, alpha_t)

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
def test_one(self, p_val: float) -> bool:
    """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

    Args:
        p_val: P-value to test. Must be in [0, 1].

    Returns:
        True if the null hypothesis is rejected (discovery), False otherwise.

    Raises:
        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
    """
    validity.check_p_val(p_val)

    if p_val > self.tau:  # discard
        self.alpha = None
        return False

    self.num_test += 1
    self.alpha = self.calc_alpha_t()

    p_val *= 1 / self.tau
    is_candidate = p_val <= self.lambda_  # candidate
    self.candidates.append(is_candidate)

    is_rejected = p_val <= self.alpha  # rejection
    self.reject_idx.append(self.num_test) if is_rejected else None
    return is_rejected

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
def calc_alpha_t(self):
    """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:
        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).
    """
    alpha_t = self.wealth0 * self.seq.calc_gamma(
        self.num_test - sum(self.candidates), None
    )
    if len(self.reject_idx) >= 1:
        tau_1 = self.reject_idx[0]
        c_1_plus = sum(self.candidates[tau_1:])
        alpha_t += (self.alpha0 - self.wealth0) * self.seq.calc_gamma(
            (self.num_test - tau_1 - c_1_plus), None
        )
    if len(self.reject_idx) >= 2:
        alpha_t += self.alpha0 * sum(
            self.seq.calc_gamma(
                (self.num_test - idx - sum(self.candidates[idx:])),
                None,
            )
            for idx in self.reject_idx[1:]
        )
    alpha_t *= self.tau - self.lambda_
    return min(self.tau * self.lambda_, alpha_t)

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

# 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

  1. Tian, J., and A. Ramdas. "ADDIS: an adaptive discarding algorithm for online FDR control with conservative nulls." NeurIPS 2019.

  2. Tian, J., and A. Ramdas. "ADDIS-Graphs for online error control with application to platform trials." arXiv preprint 2021.

  3. Robertson, D.S., and J. Wason. "onlineFDR: an R package to control the false discovery rate for growing data repositories." Bioinformatics 2019.