# SPRT — Sequential Probability Ratio Test

**Types:** `SprtBernoulli`, `SprtGaussian`
**Module path:** `nexus_stats_detection::estimation`
**Feature flags:** `std` or `libm`.

Wald's sequential probability ratio test. Given two hypotheses `H0` and `H1` and pre-specified type I and type II error bounds, SPRT minimizes the expected number of samples to reach a decision by stopping as soon as the evidence is strong enough.

## When to use it

- **Online A/B testing.** Compare two variants without fixing the sample count in advance. Stop early when the evidence is clear.
- **Quality control.** "Is this batch of samples from the good distribution or the bad one?"
- **Early-stopped experiments.** Minimize samples-to-decision while guaranteeing error rates.

## SprtBernoulli — success/failure rates

Test whether an underlying success probability is `p0` (H0) or `p1` (H1).

### API

```rust
use nexus_stats_detection::estimation::SprtBernoulli;

pub enum Decision { Continue, Accept, Reject }

impl SprtBernoulli {
    pub fn builder() -> SprtBernoulliBuilder;
    pub fn update(&mut self, success: bool) -> Decision;
    pub fn is_decided(&self) -> bool;
    pub fn reset(&mut self);
}

impl SprtBernoulliBuilder {
    pub fn p0(self, p0: f64) -> Self;
    pub fn p1(self, p1: f64) -> Self;
    pub fn alpha(self, alpha: f64) -> Self;  // P(reject H0 | H0 true)
    pub fn beta(self, beta: f64) -> Self;    // P(accept H0 | H1 true)
    pub fn build(self) -> Result<SprtBernoulli, ConfigError>;
}
```

### Example — fill rate A/B test

```rust
use nexus_stats_detection::estimation::{SprtBernoulli, Decision};

// H0: fill rate is 0.75 (current)
// H1: fill rate is 0.85 (new quoter)
// type I error 5%, type II error 10%
let mut sprt = SprtBernoulli::builder()
    .p0(0.75)
    .p1(0.85)
    .alpha(0.05)
    .beta(0.10)
    .build()
    .unwrap();

let mut count = 0;
for filled in observed_fills {
    count += 1;
    match sprt.update(filled) {
        Decision::Continue => {}
        Decision::Accept => { println!("after {count} samples: accept H0 (old is fine)"); break; }
        Decision::Reject => { println!("after {count} samples: reject H0 (new is better)"); break; }
    }
}
```

SPRT stops at the smallest sample count that distinguishes 75% vs 85% at the specified error bounds — typically 20-100 samples, compared to 300+ for a fixed-sample test.

## SprtGaussian — continuous means

Test whether the mean of a Gaussian stream is `mu_0` (H0) or `mu_1` (H1), at known variance.

### API

```rust
use nexus_stats_detection::estimation::SprtGaussian;

impl SprtGaussian {
    pub fn builder() -> SprtGaussianBuilder;
    pub fn update(&mut self, value: f64) -> Result<Decision, DataError>;
    pub fn is_decided(&self) -> bool;
}
```

### Example — latency regression

```rust
use nexus_stats_detection::estimation::{SprtGaussian, Decision};

// H0: deploy didn't regress (mean 120us)
// H1: deploy regressed by 10us (mean 130us)
// sigma known from historical data
let mut sprt = SprtGaussian::builder()
    .mu0(120.0)
    .mu1(130.0)
    .sigma(5.0)
    .alpha(0.01)
    .beta(0.05)
    .build()
    .unwrap();

for latency in latencies {
    match sprt.update(latency).unwrap() {
        Decision::Continue => {}
        Decision::Accept => { println!("deploy safe"); break; }
        Decision::Reject => { rollback(); break; }
    }
}
```

## Caveats

- **You must specify H1 in advance.** SPRT is a two-hypothesis test, not a general-purpose anomaly detector.
- **Errors are rates, not guarantees per test.** If you run 100 SPRTs with alpha = 0.05, expect ~5 false rejects.
- **Gaussian variant needs known variance.** Estimate from calibration data; feeding the wrong sigma skews decisions.
- **Reset between experiments.** A decided test stays decided until `reset()`.

## Cross-references

- `ErrorRateF64` — rolling error fraction without sequential-test machinery.
- `HitRateF64` — rolling success rate.
- For multiple comparisons or continuous thresholds, use `AdaptiveThresholdF64` or `MultiGateF64` instead.
