Skip to content

E-Value Methods

Procedures

online_fdr.e_values.batch.EBH

Bases: AbstractBatchingTest

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(AbstractBatchingTest):
    """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
    """

    def __init__(self, alpha: float):
        super().__init__(alpha)
        self._last_num_rejections = 0

    @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."""
        raw_values = list(e_values)
        if not raw_values:
            return []
        decisions = e_bh(raw_values, self.target_level)
        self._last_num_rejections = sum(decisions)
        threshold = (
            len(raw_values) / (self.target_level * self._last_num_rejections)
            if self._last_num_rejections
            else math.inf
        )
        self._set_test_level(self.target_level, rejection_threshold=threshold)
        self._advance_batch(len(raw_values))
        return decisions

    def test_batch_detail(self, e_values: Sequence[float]) -> BatchDecision:
        """Return details using the same validation and state as ``test_batch``."""
        return super().test_batch_detail(e_values)

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."""
    raw_values = list(e_values)
    if not raw_values:
        return []
    decisions = e_bh(raw_values, self.target_level)
    self._last_num_rejections = sum(decisions)
    threshold = (
        len(raw_values) / (self.target_level * self._last_num_rejections)
        if self._last_num_rejections
        else math.inf
    )
    self._set_test_level(self.target_level, rejection_threshold=threshold)
    self._advance_batch(len(raw_values))
    return decisions

test_batch_detail(e_values)

Return details using the same validation and state as test_batch.

Source code in online_fdr/e_values/batch.py
def test_batch_detail(self, e_values: Sequence[float]) -> BatchDecision:
    """Return details using the same validation and state as ``test_batch``."""
    return super().test_batch_detail(e_values)

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: AbstractSequentialTest

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(AbstractSequentialTest):
    """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
    """

    def __init__(
        self,
        alpha: float,
        gamma_seq: AbstractGammaSequence | None = None,
    ):
        super().__init__(alpha)
        self.num_reject = 0
        self.seq = gamma_seq or DefaultLondGammaSequence(c=0.07720838)

    @classmethod
    def _restore_snapshot_state(cls, state: dict[str, Any]) -> dict[str, Any]:
        # Schema-1 snapshots before the shared state used this private name.
        if "_current_level" in state:
            state["_last_test_level"] = state.pop("_current_level")
        return state

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

    def test_one(self, e_value: float) -> bool:
        """Test a single e-value and return whether it is rejected."""
        check_e_value(e_value)
        gamma_t = float(self.seq.calc_gamma(self.num_hypotheses + 1, alpha=1.0))
        level = self.target_level * gamma_t * (self.num_reject + 1)
        threshold = math.inf if level <= 0 else 1.0 / level
        self._set_test_level(level, rejection_threshold=threshold)
        self._advance_hypotheses()

        rejected = float(e_value) >= threshold
        if rejected:
            self.num_reject += 1
        return rejected

    def test_one_detail(self, e_value: float) -> TestDecision:
        """Return details using the same validation and state as ``test_one``."""
        return super().test_one_detail(e_value)

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."""
    check_e_value(e_value)
    gamma_t = float(self.seq.calc_gamma(self.num_hypotheses + 1, alpha=1.0))
    level = self.target_level * gamma_t * (self.num_reject + 1)
    threshold = math.inf if level <= 0 else 1.0 / level
    self._set_test_level(level, rejection_threshold=threshold)
    self._advance_hypotheses()

    rejected = float(e_value) >= threshold
    if rejected:
        self.num_reject += 1
    return rejected

test_one_detail(e_value)

Return details using the same validation and state as test_one.

Source code in online_fdr/e_values/sequential.py
def test_one_detail(self, e_value: float) -> TestDecision:
    """Return details using the same validation and state as ``test_one``."""
    return super().test_one_detail(e_value)

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
    try:
        return float(exponent * math.pow(float(p_value), exponent - 1.0))
    except OverflowError:
        # The power may overflow even when multiplication by k brings the
        # complete calibrated value back into the representable range.
        log_value = math.log(exponent) + (exponent - 1.0) * math.log(float(p_value))
        try:
            return float(math.exp(log_value))
        except OverflowError:
            return math.inf

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, log_weights = _prepare_weights(len(e_values), weights)
    if any(
        math.isinf(float(e)) and log_weight != -math.inf
        for e, log_weight in zip(e_values, log_weights)
    ):
        return math.inf
    return float(
        sum(
            float(e) * weight
            if weight > 0
            else math.exp(math.log(float(e)) + log_weight)
            for e, weight, log_weight in zip(e_values, normalized, log_weights)
            if log_weight != -math.inf and e > 0
        )
    )

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. A product containing both zero and infinity is undefined and raises ValueError, independently of input order.

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.
    A product containing both zero and infinity is undefined and raises
    ``ValueError``, independently of input order.
    """
    check_e_values(e_values)
    values = [float(value) for value in e_values]
    has_zero = 0.0 in values
    has_infinity = math.inf in values
    if has_zero and has_infinity:
        raise ValueError("a product containing both zero and infinity is undefined.")
    if has_zero:
        return -math.inf
    if has_infinity:
        return math.inf
    total = sum(math.log(value) for value in values)
    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 _is_real(e_value):
        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 _is_real(value) 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 _is_real(weight) 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))
        self.log_current = _advance_log_capital(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)
        self._weights, self._log_weights = _prepare_weights(
            len(self.log_likelihood_ratios),
            weights,
            mismatch_message="weights must match likelihood-ratio components.",
        )
        self.component_log_currents = [0.0] * len(self.log_likelihood_ratios)

    @property
    def weights(self) -> list[float]:
        """Return the normalized fixed prior without exposing mutable state."""
        return list(self._weights)

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

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

    def update(self, observation: Any) -> float:
        updated = self.component_log_currents.copy()
        for idx, (log_weight, log_lr) in enumerate(
            zip(self._log_weights, self.log_likelihood_ratios)
        ):
            if log_weight == -math.inf:
                continue
            value = float(log_lr(observation))
            updated[idx] = _advance_log_capital(updated[idx], value)
        self.component_log_currents = updated
        return _exp_or_inf(self.log_current)

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

Attributes

weights property

Return the normalized fixed prior without exposing mutable state.

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,
    ):
        check_positive_integer(n, "n")
        if batch_size is not None:
            check_positive_integer(batch_size, "batch_size")
        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 = None if batch_size is None else int(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 if self.batch_size is not None else self.n
        check_positive_integer(size, "size")
        size = int(size)
        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 if self.batch_size is not None else self.n
    check_positive_integer(size, "size")
    size = int(size)
    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])