Skip to content

E-Value Methods

Procedures

online_fdr.e_values.batch.EBH

Bases: StatefulMethodMixin

Batch FDR control with e-values via e-Benjamini-Hochberg.

References

Wang, R. and Ramdas, A. (2022). False discovery rate control with e-values. Journal of the Royal Statistical Society: Series B. Author code: https://github.com/ruoduwang/e-BH

Source code in online_fdr/e_values/batch.py
class EBH(StatefulMethodMixin):
    """Batch FDR control with e-values via e-Benjamini-Hochberg.

    References:
        Wang, R. and Ramdas, A. (2022). False discovery rate control with
        e-values. Journal of the Royal Statistical Society: Series B.
        Author code: https://github.com/ruoduwang/e-BH
    """

    error_rate = "FDR"

    def __init__(self, alpha: float):
        check_alpha(alpha)
        self.target_level = float(alpha)
        self._num_hypotheses = 0
        self._num_batches = 0
        self._last_test_level: float | None = None
        self._last_rejection_threshold: float | None = None
        self._last_num_rejections = 0

    @property
    def num_hypotheses(self) -> int:
        return self._num_hypotheses

    @property
    def num_tests(self) -> int:
        return self.num_hypotheses

    @property
    def num_batches(self) -> int:
        return self._num_batches

    @property
    def last_test_level(self) -> float | None:
        return self._last_test_level

    @property
    def last_rejection_threshold(self) -> float | None:
        return self._last_rejection_threshold

    @property
    def current_threshold(self) -> float | None:
        return self.last_rejection_threshold

    @property
    def current_k(self) -> int:
        return self._last_num_rejections

    def test_batch(self, e_values: Sequence[float]) -> list[bool]:
        """Test one batch of e-values and return rejection decisions."""
        return list(self.test_batch_detail(e_values).rejected)

    def test_batch_detail(self, e_values: Sequence[float]) -> BatchDecision:
        """Test one batch of e-values and return immutable decision details."""
        raw_values = list(e_values)
        if not raw_values:
            return BatchDecision(
                rejected=(),
                values=(),
                rejection_threshold=self.last_rejection_threshold,
                batch_index=self.num_batches,
                test_level=self.last_test_level,
                error_rate=self.error_rate,
                metadata={"num_rejections": 0},
            )
        check_e_values(raw_values)
        values = [float(value) for value in raw_values]
        decisions = e_bh(values, self.target_level)
        self._last_num_rejections = sum(decisions)
        self._last_test_level = self.target_level
        self._last_rejection_threshold = (
            len(values) / (self.target_level * self._last_num_rejections)
            if self._last_num_rejections
            else math.inf
        )
        self._num_batches += 1
        self._num_hypotheses += len(values)
        return BatchDecision(
            rejected=tuple(decisions),
            values=tuple(values),
            rejection_threshold=self.last_rejection_threshold,
            batch_index=self.num_batches,
            test_level=self.last_test_level,
            error_rate=self.error_rate,
            metadata={"num_rejections": self._last_num_rejections},
        )

Functions

test_batch(e_values)

Test one batch of e-values and return rejection decisions.

Source code in online_fdr/e_values/batch.py
def test_batch(self, e_values: Sequence[float]) -> list[bool]:
    """Test one batch of e-values and return rejection decisions."""
    return list(self.test_batch_detail(e_values).rejected)

test_batch_detail(e_values)

Test one batch of e-values and return immutable decision details.

Source code in online_fdr/e_values/batch.py
def test_batch_detail(self, e_values: Sequence[float]) -> BatchDecision:
    """Test one batch of e-values and return immutable decision details."""
    raw_values = list(e_values)
    if not raw_values:
        return BatchDecision(
            rejected=(),
            values=(),
            rejection_threshold=self.last_rejection_threshold,
            batch_index=self.num_batches,
            test_level=self.last_test_level,
            error_rate=self.error_rate,
            metadata={"num_rejections": 0},
        )
    check_e_values(raw_values)
    values = [float(value) for value in raw_values]
    decisions = e_bh(values, self.target_level)
    self._last_num_rejections = sum(decisions)
    self._last_test_level = self.target_level
    self._last_rejection_threshold = (
        len(values) / (self.target_level * self._last_num_rejections)
        if self._last_num_rejections
        else math.inf
    )
    self._num_batches += 1
    self._num_hypotheses += len(values)
    return BatchDecision(
        rejected=tuple(decisions),
        values=tuple(values),
        rejection_threshold=self.last_rejection_threshold,
        batch_index=self.num_batches,
        test_level=self.last_test_level,
        error_rate=self.error_rate,
        metadata={"num_rejections": self._last_num_rejections},
    )

online_fdr.e_values.batch.e_bh(e_values, alpha)

Apply the base e-BH procedure to a fixed batch of e-values.

The procedure rejects the largest k e-values, where k is the largest index satisfying e_(k) >= m / (alpha * k) after sorting descending. Ties are broken stably by original input order.

This is the base e-BH procedure of Wang and Ramdas (2022), which controls FDR under arbitrary dependence for valid e-values.

Source code in online_fdr/e_values/batch.py
def e_bh(e_values: Sequence[float], alpha: float) -> list[bool]:
    """Apply the base e-BH procedure to a fixed batch of e-values.

    The procedure rejects the largest ``k`` e-values, where ``k`` is the largest
    index satisfying ``e_(k) >= m / (alpha * k)`` after sorting descending.
    Ties are broken stably by original input order.

    This is the base e-BH procedure of Wang and Ramdas (2022), which controls
    FDR under arbitrary dependence for valid e-values.
    """
    check_alpha(alpha)
    raw_values = list(e_values)
    if not raw_values:
        return []
    check_e_values(raw_values)
    values = [float(value) for value in raw_values]

    m = len(values)
    ordered = sorted(enumerate(values), key=lambda item: (-item[1], item[0]))
    k_star = 0
    for rank, (_, value) in enumerate(ordered, start=1):
        threshold = m / (alpha * rank)
        if value >= threshold:
            k_star = rank

    rejected = [False] * m
    for original_idx, _ in ordered[:k_star]:
        rejected[original_idx] = True
    return rejected

online_fdr.e_values.sequential.ELond

Bases: StatefulMethodMixin

Online FDR control for e-values with e-LOND.

e-LOND uses the same test levels as p-value LOND but rejects when the incoming e-value exceeds the reciprocal test level. Valid e-values give FDR control under arbitrary dependence.

References

Xu, Z. and Ramdas, A. (2024). Online multiple testing with e-values. Proceedings of AISTATS 2024. Author code: https://github.com/neilzxu/evalue-omt

Source code in online_fdr/e_values/sequential.py
class ELond(StatefulMethodMixin):
    """Online FDR control for e-values with e-LOND.

    e-LOND uses the same test levels as p-value LOND but rejects when the
    incoming e-value exceeds the reciprocal test level. Valid e-values give FDR
    control under arbitrary dependence.

    References:
        Xu, Z. and Ramdas, A. (2024). Online multiple testing with e-values.
        Proceedings of AISTATS 2024.
        Author code: https://github.com/neilzxu/evalue-omt
    """

    error_rate = "FDR"

    def __init__(
        self,
        alpha: float,
        gamma_seq: AbstractGammaSequence | None = None,
    ):
        check_alpha(alpha)
        self.target_level = float(alpha)
        self._num_hypotheses = 0
        self.num_reject = 0
        self._current_level: float | None = None
        self._last_rejection_threshold: float | None = None
        self.seq = gamma_seq or DefaultLondGammaSequence(c=0.07720838)

    @property
    def num_hypotheses(self) -> int:
        return self._num_hypotheses

    @property
    def num_tests(self) -> int:
        return self.num_hypotheses

    @property
    def last_test_level(self) -> float | None:
        return self._current_level

    @property
    def current_level(self) -> float | None:
        return self.last_test_level

    @property
    def last_rejection_threshold(self) -> float | None:
        return self._last_rejection_threshold

    @property
    def current_threshold(self) -> float | None:
        return self.last_rejection_threshold

    def test_one(self, e_value: float) -> bool:
        """Test a single e-value and return whether it is rejected."""
        return self.test_one_detail(e_value).rejected

    def test_one_detail(self, e_value: float) -> TestDecision:
        """Test a single e-value and return immutable decision details."""
        check_e_value(e_value)
        self._num_hypotheses += 1

        gamma_t = self._calc_gamma(self.num_hypotheses)
        self._current_level = self.target_level * gamma_t * (self.num_reject + 1)
        self._last_rejection_threshold = (
            math.inf if self._current_level <= 0 else 1.0 / self._current_level
        )

        rejected = float(e_value) >= self._last_rejection_threshold
        if rejected:
            self.num_reject += 1
        return TestDecision(
            rejected=bool(rejected),
            value=float(e_value),
            rejection_threshold=self.last_rejection_threshold,
            index=self.num_hypotheses,
            test_level=self.last_test_level,
            error_rate=self.error_rate,
        )

    def _calc_gamma(self, index: int) -> float:
        try:
            return float(self.seq.calc_gamma(index, alpha=1.0))
        except TypeError:
            return float(self.seq.calc_gamma(index))

Functions

test_one(e_value)

Test a single e-value and return whether it is rejected.

Source code in online_fdr/e_values/sequential.py
def test_one(self, e_value: float) -> bool:
    """Test a single e-value and return whether it is rejected."""
    return self.test_one_detail(e_value).rejected

test_one_detail(e_value)

Test a single e-value and return immutable decision details.

Source code in online_fdr/e_values/sequential.py
def test_one_detail(self, e_value: float) -> TestDecision:
    """Test a single e-value and return immutable decision details."""
    check_e_value(e_value)
    self._num_hypotheses += 1

    gamma_t = self._calc_gamma(self.num_hypotheses)
    self._current_level = self.target_level * gamma_t * (self.num_reject + 1)
    self._last_rejection_threshold = (
        math.inf if self._current_level <= 0 else 1.0 / self._current_level
    )

    rejected = float(e_value) >= self._last_rejection_threshold
    if rejected:
        self.num_reject += 1
    return TestDecision(
        rejected=bool(rejected),
        value=float(e_value),
        rejection_threshold=self.last_rejection_threshold,
        index=self.num_hypotheses,
        test_level=self.last_test_level,
        error_rate=self.error_rate,
    )

Toolbox

online_fdr.e_values.toolbox.e_to_p(e_value)

Convert an e-value to the conservative p-value min(1, 1 / e).

Source code in online_fdr/e_values/toolbox.py
def e_to_p(e_value: float) -> float:
    """Convert an e-value to the conservative p-value ``min(1, 1 / e)``."""
    check_e_value(e_value)
    value = float(e_value)
    if value == 0:
        return 1.0
    if math.isinf(value):
        return 0.0
    return min(1.0, 1.0 / value)

online_fdr.e_values.toolbox.p_to_e_power(p_value, exponent)

Calibrate a p-value into an e-value with k * p ** (k - 1).

The exponent must be in (0, 1). The calibrator integrates to one on [0, 1] and is decreasing, so applying it to a valid p-value yields an e-value. This is the power calibrator from Vovk and Wang (2021).

Source code in online_fdr/e_values/toolbox.py
def p_to_e_power(p_value: float, exponent: float) -> float:
    """Calibrate a p-value into an e-value with ``k * p ** (k - 1)``.

    The exponent must be in ``(0, 1)``. The calibrator integrates to one on
    ``[0, 1]`` and is decreasing, so applying it to a valid p-value yields an
    e-value. This is the power calibrator from Vovk and Wang (2021).
    """
    check_p_val(p_value)
    if not 0 < exponent < 1:
        raise ValueError("exponent must be in (0, 1).")
    if p_value == 0:
        return math.inf
    return float(exponent * math.pow(float(p_value), exponent - 1.0))

online_fdr.e_values.toolbox.make_power_calibrator(exponent)

Return the power p-to-e calibrator for a fixed exponent.

Source code in online_fdr/e_values/toolbox.py
def make_power_calibrator(exponent: float) -> Callable[[float], float]:
    """Return the power p-to-e calibrator for a fixed exponent."""
    if not 0 < exponent < 1:
        raise ValueError("exponent must be in (0, 1).")
    return lambda p_value: p_to_e_power(p_value, exponent)

online_fdr.e_values.toolbox.weighted_arithmetic_mean(e_values, weights=None)

Merge e-values by weighted arithmetic mean.

This is valid for arbitrary dependence when all inputs are valid e-values and weights are fixed independently of the null evidence, as in the averaging rule of Vovk and Wang (2021).

Source code in online_fdr/e_values/toolbox.py
def weighted_arithmetic_mean(
    e_values: Sequence[float],
    weights: Sequence[float] | None = None,
) -> float:
    """Merge e-values by weighted arithmetic mean.

    This is valid for arbitrary dependence when all inputs are valid e-values
    and weights are fixed independently of the null evidence, as in the
    averaging rule of Vovk and Wang (2021).
    """
    check_e_values(e_values)
    normalized = _normalized_weights(len(e_values), weights)
    if any(
        math.isinf(float(e)) and weight > 0 for e, weight in zip(e_values, normalized)
    ):
        return math.inf
    return float(sum(float(e) * weight for e, weight in zip(e_values, normalized)))

online_fdr.e_values.toolbox.mixture_e_value(e_values, weights=None)

Alias for weighted arithmetic e-value merging.

Source code in online_fdr/e_values/toolbox.py
def mixture_e_value(
    e_values: Sequence[float],
    weights: Sequence[float] | None = None,
) -> float:
    """Alias for weighted arithmetic e-value merging."""
    return weighted_arithmetic_mean(e_values, weights)

online_fdr.e_values.toolbox.log_product_e_values(e_values)

Return the log product of e-values.

Products are valid under independence or suitable conditional/sequential validity assumptions supplied by the caller.

Source code in online_fdr/e_values/toolbox.py
def log_product_e_values(e_values: Sequence[float]) -> float:
    """Return the log product of e-values.

    Products are valid under independence or suitable conditional/sequential
    validity assumptions supplied by the caller.
    """
    check_e_values(e_values)
    total = 0.0
    for e_value in e_values:
        value = float(e_value)
        if value == 0:
            return -math.inf
        if math.isinf(value):
            return math.inf
        total += math.log(value)
    return float(total)

online_fdr.e_values.toolbox.product_e_values(e_values)

Return the product of e-values in ordinary scale.

Source code in online_fdr/e_values/toolbox.py
def product_e_values(e_values: Sequence[float]) -> float:
    """Return the product of e-values in ordinary scale."""
    log_value = log_product_e_values(e_values)
    if log_value == -math.inf:
        return 0.0
    if log_value == math.inf:
        return math.inf
    try:
        return float(math.exp(log_value))
    except OverflowError:
        return math.inf

online_fdr.e_values.toolbox.max_e_value(e_values)

Conservative max merge, max(e_values) / len(e_values).

The scaling is deliberate: the raw maximum is not generally an e-value under arbitrary dependence.

Source code in online_fdr/e_values/toolbox.py
def max_e_value(e_values: Sequence[float]) -> float:
    """Conservative max merge, ``max(e_values) / len(e_values)``.

    The scaling is deliberate: the raw maximum is not generally an e-value under
    arbitrary dependence.
    """
    check_e_values(e_values)
    if not e_values:
        raise ValueError("at least one e-value is required.")
    max_value = max(float(value) for value in e_values)
    return math.inf if math.isinf(max_value) else float(max_value / len(e_values))

online_fdr.e_values.toolbox.check_e_value(e_value)

Validate that an e-value is numeric and nonnegative.

Infinite e-values are allowed because they can arise from exact-zero calibrated p-values or likelihood ratios with zero null density.

Source code in online_fdr/core/utils/validity.py
def check_e_value(e_value: float) -> None:
    """Validate that an e-value is numeric and nonnegative.

    Infinite e-values are allowed because they can arise from exact-zero
    calibrated p-values or likelihood ratios with zero null density.
    """
    if not isinstance(e_value, int | float):
        raise ValueError("e-value must be a numeric value.")
    value = float(e_value)
    if math.isnan(value) or value < 0:
        raise ValueError("e-value must be nonnegative and not NaN.")

online_fdr.e_values.toolbox.check_e_values(e_values)

Validate a sequence of e-values.

Source code in online_fdr/core/utils/validity.py
def check_e_values(e_values: Sequence[float]) -> None:
    """Validate a sequence of e-values."""
    for idx, e_value in enumerate(e_values):
        try:
            check_e_value(e_value)
        except ValueError as exc:
            raise ValueError(f"e-value at index {idx} is invalid: {exc}") from exc

online_fdr.e_values.toolbox.check_calibrator(calibrator, *, grid_size=10000, tolerance=0.001)

Numerically check basic p-to-e calibrator conditions.

This helper cannot prove validity for arbitrary functions. It checks the practical conditions used by this package: nonnegative, nonincreasing values on a midpoint grid and approximate integral at most one.

Source code in online_fdr/core/utils/validity.py
def check_calibrator(
    calibrator: Callable[[float], float],
    *,
    grid_size: int = 10_000,
    tolerance: float = 1e-3,
) -> None:
    """Numerically check basic p-to-e calibrator conditions.

    This helper cannot prove validity for arbitrary functions. It checks the
    practical conditions used by this package: nonnegative, nonincreasing values
    on a midpoint grid and approximate integral at most one.
    """
    if grid_size < 10:
        raise ValueError("grid_size must be at least 10.")
    if tolerance < 0:
        raise ValueError("tolerance must be nonnegative.")

    prev: float | None = None
    total = 0.0
    for idx in range(grid_size):
        p_value = (idx + 0.5) / grid_size
        value = calibrator(p_value)
        if not isinstance(value, int | float) or not math.isfinite(float(value)):
            raise ValueError("calibrator must return finite numeric values on (0, 1).")
        value = float(value)
        if value < 0:
            raise ValueError("calibrator must be nonnegative.")
        if prev is not None and value > prev + tolerance:
            raise ValueError("calibrator must be nonincreasing.")
        prev = value
        total += value

    integral = total / grid_size
    if integral > 1.0 + tolerance:
        raise ValueError("calibrator midpoint integral exceeds one.")

online_fdr.e_values.toolbox.check_nonnegative_weights(weights)

Validate that weights are finite, nonnegative, and not all zero.

Source code in online_fdr/core/utils/validity.py
def check_nonnegative_weights(weights: Sequence[float]) -> None:
    """Validate that weights are finite, nonnegative, and not all zero."""
    if not weights:
        raise ValueError("weights must not be empty.")

    total = 0.0
    for idx, weight in enumerate(weights):
        if not isinstance(weight, int | float) or not math.isfinite(float(weight)):
            raise ValueError(f"weight at index {idx} must be finite.")
        value = float(weight)
        if value < 0:
            raise ValueError(f"weight at index {idx} must be nonnegative.")
        total += value

    if total <= 0:
        raise ValueError("at least one weight must be positive.")

E-Processes

online_fdr.e_values.processes.EProcess

Bases: Protocol

Protocol for anytime-valid e-processes.

Source code in online_fdr/e_values/processes.py
class EProcess(Protocol):
    """Protocol for anytime-valid e-processes."""

    @property
    def current(self) -> float:
        """Current e-value."""
        ...

    def update(self, observation: Any) -> float:
        """Update with one observation and return the current e-value."""
        ...

    def reset(self) -> None:
        """Reset the process to its initial e-value."""
        ...

Attributes

current property

Current e-value.

Functions

update(observation)

Update with one observation and return the current e-value.

Source code in online_fdr/e_values/processes.py
def update(self, observation: Any) -> float:
    """Update with one observation and return the current e-value."""
    ...

reset()

Reset the process to its initial e-value.

Source code in online_fdr/e_values/processes.py
def reset(self) -> None:
    """Reset the process to its initial e-value."""
    ...

online_fdr.e_values.processes.LikelihoodRatioEProcess

Log-space likelihood-ratio e-process.

log_likelihood_ratio must return log f_alt(x) / f_null(x) for each observation. The caller is responsible for the model assumptions that make this an e-process under the null.

Source code in online_fdr/e_values/processes.py
class LikelihoodRatioEProcess:
    """Log-space likelihood-ratio e-process.

    ``log_likelihood_ratio`` must return log ``f_alt(x) / f_null(x)`` for each
    observation. The caller is responsible for the model assumptions that make
    this an e-process under the null.
    """

    def __init__(self, log_likelihood_ratio: Callable[[Any], float]):
        self.log_likelihood_ratio = log_likelihood_ratio
        self.log_current = 0.0

    @property
    def current(self) -> float:
        return _exp_or_inf(self.log_current)

    def update(self, observation: Any) -> float:
        increment = float(self.log_likelihood_ratio(observation))
        if math.isnan(increment):
            raise ValueError("log likelihood ratio must not be NaN.")
        self.log_current += increment
        return self.current

    def reset(self) -> None:
        self.log_current = 0.0

online_fdr.e_values.processes.MixtureLikelihoodRatioEProcess

Fixed-prior mixture of likelihood-ratio e-processes.

Each component accumulates its own likelihood-ratio process over time. The reported value is the weighted arithmetic mixture of those cumulative component processes, not a product of per-observation mixture likelihoods. The caller is responsible for the null and alternative model assumptions.

Source code in online_fdr/e_values/processes.py
class MixtureLikelihoodRatioEProcess:
    """Fixed-prior mixture of likelihood-ratio e-processes.

    Each component accumulates its own likelihood-ratio process over time. The
    reported value is the weighted arithmetic mixture of those cumulative
    component processes, not a product of per-observation mixture likelihoods.
    The caller is responsible for the null and alternative model assumptions.
    """

    def __init__(
        self,
        log_likelihood_ratios: Sequence[Callable[[Any], float]],
        weights: Sequence[float] | None = None,
    ):
        if not log_likelihood_ratios:
            raise ValueError("at least one likelihood-ratio component is required.")
        self.log_likelihood_ratios = list(log_likelihood_ratios)
        if weights is None:
            self.weights = [1.0 / len(self.log_likelihood_ratios)] * len(
                self.log_likelihood_ratios
            )
        else:
            if len(weights) != len(self.log_likelihood_ratios):
                raise ValueError("weights must match likelihood-ratio components.")
            check_nonnegative_weights(weights)
            total = float(sum(weights))
            self.weights = [float(weight) / total for weight in weights]
        self.component_log_currents = [0.0] * len(self.log_likelihood_ratios)

    @property
    def current(self) -> float:
        return _exp_or_inf(self.log_current)

    @property
    def log_current(self) -> float:
        terms = []
        for weight, component_log_current in zip(
            self.weights, self.component_log_currents
        ):
            if weight == 0:
                terms.append(-math.inf)
            else:
                terms.append(math.log(weight) + component_log_current)
        return _logsumexp(terms)

    def update(self, observation: Any) -> float:
        for idx, (weight, log_lr) in enumerate(
            zip(self.weights, self.log_likelihood_ratios)
        ):
            if weight == 0:
                continue
            value = float(log_lr(observation))
            if math.isnan(value):
                raise ValueError("log likelihood ratio must not be NaN.")
            self.component_log_currents[idx] += value
            if math.isnan(self.component_log_currents[idx]):
                raise ValueError("cumulative log likelihood ratio must not be NaN.")
        return _exp_or_inf(self.log_current)

    def reset(self) -> None:
        self.component_log_currents = [0.0] * len(self.log_likelihood_ratios)

online_fdr.e_values.processes.BettingEProcess

Simple betting e-process from multiplicative betting factors.

For each score, the factor is 1 + stake * score. The caller is responsible for using scores and predictable stakes that make the factor conditionally expectation-bounded by one under the null.

Source code in online_fdr/e_values/processes.py
class BettingEProcess:
    """Simple betting e-process from multiplicative betting factors.

    For each score, the factor is ``1 + stake * score``. The caller is
    responsible for using scores and predictable stakes that make the factor
    conditionally expectation-bounded by one under the null.
    """

    def __init__(self, stake: float | Callable[[float], float]):
        self.stake = stake
        self.log_current = 0.0

    @property
    def current(self) -> float:
        return _exp_or_inf(self.log_current)

    def update(self, score: float) -> float:
        stake = self.stake(score) if callable(self.stake) else self.stake
        stake = float(stake)
        score = float(score)
        factor = 1.0 + stake * score
        if not math.isfinite(factor) or factor < 0:
            raise ValueError("betting factor must be finite and nonnegative.")
        if factor == 0:
            self.log_current = -math.inf
        elif self.log_current != -math.inf:
            self.log_current += math.log(factor)
        return self.current

    def reset(self) -> None:
        self.log_current = 0.0

online_fdr.e_values.processes.value_at_stop(process)

Return the stopped value of an e-process.

Source code in online_fdr/e_values/processes.py
def value_at_stop(process: EProcess) -> float:
    """Return the stopped value of an e-process."""
    return float(process.current)

online_fdr.e_values.processes.stop(process)

Alias for value_at_stop.

Source code in online_fdr/e_values/processes.py
def stop(process: EProcess) -> float:
    """Alias for ``value_at_stop``."""
    return value_at_stop(process)

Generation

online_fdr.e_values.generation.GaussianEValueGenerator

Generate labeled Gaussian likelihood-ratio e-values.

Source code in online_fdr/e_values/generation.py
class GaussianEValueGenerator:
    """Generate labeled Gaussian likelihood-ratio e-values."""

    def __init__(
        self,
        n: int,
        pi0: float,
        alt_mean: float = 3.0,
        null_mean: float = 0.0,
        std: float = 1.0,
        batch_size: Optional[int] = None,
        seed: int = 1,
    ):
        if n <= 0:
            raise ValueError("n must be positive.")
        if not 0 <= pi0 <= 1:
            raise ValueError("pi0 must be in [0, 1].")
        if std <= 0:
            raise ValueError("std must be positive.")
        self.n = int(n)
        self.pi0 = float(pi0)
        self.alt_mean = float(alt_mean)
        self.null_mean = float(null_mean)
        self.std = float(std)
        self.batch_size = batch_size
        self.rng = np.random.RandomState(seed)
        self.current_idx = 0

        n_null = int(self.n * self.pi0)
        n_alt = self.n - n_null
        null_samples = self.rng.normal(self.null_mean, self.std, n_null)
        alt_samples = self.rng.normal(self.alt_mean, self.std, n_alt)
        samples = np.concatenate([null_samples, alt_samples])
        labels = np.concatenate([np.zeros(n_null), np.ones(n_alt)]).astype(bool)
        order = self.rng.permutation(self.n)

        self.samples = samples[order]
        self.labels = labels[order]
        self.e_values = gaussian_likelihood_ratio_e_values(
            self.samples,
            self.alt_mean,
            null_mean=self.null_mean,
            std=self.std,
        )

    def sample_one(self) -> Tuple[float, bool]:
        """Sample one ``(e_value, is_alternative)`` pair."""
        if self.current_idx >= self.n:
            raise StopIteration("All samples have been generated.")
        e_value = self.e_values[self.current_idx]
        label = self.labels[self.current_idx]
        self.current_idx += 1
        return float(e_value), bool(label)

    def sample_batch(self, size: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray]:
        """Sample a batch of e-values and labels."""
        if size is None:
            size = self.batch_size or self.n
        if self.current_idx >= self.n:
            raise StopIteration("All samples have been generated.")
        end_idx = min(self.current_idx + size, self.n)
        e_values = self.e_values[self.current_idx : end_idx]
        labels = self.labels[self.current_idx : end_idx]
        self.current_idx = end_idx
        return e_values, labels

    def reset(self) -> None:
        """Reset the generator to the beginning."""
        self.current_idx = 0

    @property
    def remaining(self) -> int:
        """Number of samples remaining."""
        return self.n - self.current_idx

Attributes

remaining property

Number of samples remaining.

Functions

sample_one()

Sample one (e_value, is_alternative) pair.

Source code in online_fdr/e_values/generation.py
def sample_one(self) -> Tuple[float, bool]:
    """Sample one ``(e_value, is_alternative)`` pair."""
    if self.current_idx >= self.n:
        raise StopIteration("All samples have been generated.")
    e_value = self.e_values[self.current_idx]
    label = self.labels[self.current_idx]
    self.current_idx += 1
    return float(e_value), bool(label)

sample_batch(size=None)

Sample a batch of e-values and labels.

Source code in online_fdr/e_values/generation.py
def sample_batch(self, size: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray]:
    """Sample a batch of e-values and labels."""
    if size is None:
        size = self.batch_size or self.n
    if self.current_idx >= self.n:
        raise StopIteration("All samples have been generated.")
    end_idx = min(self.current_idx + size, self.n)
    e_values = self.e_values[self.current_idx : end_idx]
    labels = self.labels[self.current_idx : end_idx]
    self.current_idx = end_idx
    return e_values, labels

reset()

Reset the generator to the beginning.

Source code in online_fdr/e_values/generation.py
def reset(self) -> None:
    """Reset the generator to the beginning."""
    self.current_idx = 0

online_fdr.e_values.generation.gaussian_likelihood_ratio_e_value(sample, alt_mean, *, null_mean=0.0, std=1.0)

Likelihood-ratio e-value for normal means with known common variance.

Source code in online_fdr/e_values/generation.py
def gaussian_likelihood_ratio_e_value(
    sample: float,
    alt_mean: float,
    *,
    null_mean: float = 0.0,
    std: float = 1.0,
) -> float:
    """Likelihood-ratio e-value for normal means with known common variance."""
    if std <= 0:
        raise ValueError("std must be positive.")
    sample = float(sample)
    variance = std * std
    log_lr = ((sample - null_mean) ** 2 - (sample - alt_mean) ** 2) / (2 * variance)
    return float(np.exp(log_lr))

online_fdr.e_values.generation.gaussian_likelihood_ratio_e_values(samples, alt_mean, *, null_mean=0.0, std=1.0)

Vectorized Gaussian likelihood-ratio e-values.

Source code in online_fdr/e_values/generation.py
def gaussian_likelihood_ratio_e_values(
    samples: np.ndarray | list[float],
    alt_mean: float,
    *,
    null_mean: float = 0.0,
    std: float = 1.0,
) -> np.ndarray:
    """Vectorized Gaussian likelihood-ratio e-values."""
    if std <= 0:
        raise ValueError("std must be positive.")
    values = np.asarray(samples, dtype=float)
    variance = std * std
    log_lr = ((values - null_mean) ** 2 - (values - alt_mean) ** 2) / (2 * variance)
    return np.exp(log_lr)

online_fdr.e_values.generation.calibrated_p_value_stream(p_values, exponent)

Convert p-values to e-values with the power calibrator.

Source code in online_fdr/e_values/generation.py
def calibrated_p_value_stream(
    p_values: np.ndarray | list[float],
    exponent: float,
) -> np.ndarray:
    """Convert p-values to e-values with the power calibrator."""
    return np.asarray([p_to_e_power(float(p_value), exponent) for p_value in p_values])