"Every alarm I raise is a bet. A false alarm costs me credibility; a missed detection costs me everything. My whole job is choosing the exchange rate between those two before the signal arrives."
A Decisive AI Agent
The Big Picture
Estimation asks "what is the value?" Detection asks a sharper, more consequential question: "is the thing there at all?" Is that a heartbeat or motion artifact, a real radar target or a clutter spike, a genuine machine fault or a sensor glitch, the wake word or background chatter? Every one of these is a decision between two competing stories about the same measurement, made under noise, where both possible mistakes carry a price. Detection theory is the mathematics of making that decision optimally: it tells you exactly how to turn a noisy observation into a yes-or-no verdict, how to set the trip point that trades false alarms against misses, and how to prove that no other rule does better at the same operating point. It is the bridge between the probability you have been building and the actions a sensor system actually takes.
This section builds directly on the likelihood functions of Section 4.2 and the posterior reasoning of Section 4.3; a detector is a likelihood comparison with a threshold. It also leans on the noise distributions from Section 4.1 and, for where that noise physically comes from, the measurement models of Chapter 2. The vocabulary here becomes the backbone of classical anomaly and change detection in Chapter 12 and of radar target detection in Chapter 44.
Two hypotheses and two ways to be wrong
Frame every detection as a contest between a null hypothesis \(H_0\) (nothing is there; only noise) and an alternative \(H_1\) (signal present). A decision rule partitions the space of possible observations into a "declare \(H_1\)" region and a "declare \(H_0\)" region. Because the observation is random, the rule can fail in two distinct ways, and the asymmetry between them is the whole game. A Type I error (false alarm) declares a signal when there is none; its probability is \(P_{\text{FA}} = P(\text{declare } H_1 \mid H_0)\), conventionally called \(\alpha\). A Type II error (miss) stays silent when the signal is real; its probability is \(P_{\text{M}} = P(\text{declare } H_0 \mid H_1) = \beta\). The complement of a miss is the quantity we actually want to maximize, the detection probability or power, \(P_{\text{D}} = 1 - \beta\).
These two error rates trade against each other along a single knob. Lower the threshold to catch more real signals and you inevitably admit more noise-only false alarms; raise it to silence the false alarms and you start missing real events. You cannot drive both to zero with a fixed amount of data. The engineering question is never "how do I eliminate errors" but "given that I must trade, which exchange rate do I want, and what rule achieves it most efficiently?" In short: a detector never abolishes error, it only sets the exchange rate you pay between crying wolf and missing the wolf. Figure 4.6.1 makes this concrete: it overlays the two hypothesis distributions and shows how a single threshold carves the observation axis into a false-alarm tail and a miss tail.
Common Misconception
The most common trap is treating a declaration of \(H_0\) as proof that no signal is present, "the detector stayed silent, so nothing happened." Staying silent only means the evidence failed to clear your threshold at the operating point (the specific threshold setting you have chosen to run at, one point on the trade-off curve) you chose; a miss (Type II error) is always possible and is in fact guaranteed at some rate, so absence of a declaration is not evidence of absence, especially when the deflection is small or the threshold is set conservatively to suppress false alarms.
Key Insight
A detector is not a single number; it is a whole curve of achievable (false-alarm, detection) pairs, and you choose one point on it by picking a threshold. Reporting "95 percent accuracy" hides this choice and is nearly meaningless when \(H_0\) and \(H_1\) are unbalanced, which they almost always are in sensing: cardiac events, machine faults, and real radar targets are rare, so a detector that always says "nothing" scores high accuracy while being useless. Always specify the operating point, never a lone accuracy figure.
The likelihood-ratio test is optimal
Choose this rule badly and you either bury operators under false brake events and phantom alarms or let a real cardiac event slip past in silence; choose it well and the very same noisy measurement delivers the highest detection rate physically possible at whatever false-alarm cost you can afford. Which rule wins? The Neyman-Pearson lemma gives a clean and slightly surprising answer: to maximize detection probability for any fixed false-alarm rate \(\alpha\), compare the likelihood ratio to a threshold and nothing else. Given observation \(x\), form
$$\Lambda(x) = \frac{p(x \mid H_1)}{p(x \mid H_0)}, \qquad \text{declare } H_1 \iff \Lambda(x) \ge \eta,$$where the threshold \(\eta\) is tuned so the false-alarm rate equals your budget \(\alpha\). No other test, however clever, achieves higher power at that \(\alpha\). The threshold need not come from a false-alarm budget at all: when you instead know the prior probabilities of the two hypotheses and the cost of each kind of error, the same ratio is compared against a threshold set by the cost ratio times the prior odds, which is the Bayes (minimum-risk) rule that Exercise (c) asks you to derive. This is a strong optimality result: the entire high-dimensional observation collapses to one sufficient statistic (a single number that carries every bit of the data relevant to choosing between the two hypotheses, so nothing is lost by discarding the rest), the ratio, and the only free choice left is where to cut it. When the two hypotheses are Gaussian with the same covariance, taking the logarithm turns the ratio into a linear function of \(x\) (the logarithm is monotonic, so comparing \(\log\Lambda(x)\) to a threshold decides exactly the same cases as comparing \(\Lambda(x)\), just in a simpler algebraic form), which is why the classical matched filter (correlate the incoming signal with a template, then threshold) is the optimal detector for a known waveform in white Gaussian noise. Detecting a chirp in radar, a QRS complex in an electrocardiogram (ECG), or a known vibration signature in a bearing all reduce to the same correlate-and-threshold skeleton.
Checkpoint
So far: the Neyman-Pearson lemma says the optimal detector compares the likelihood ratio (a single sufficient statistic) to a threshold, and for a known signal in Gaussian noise that ratio becomes the matched filter, correlate against a template then threshold.
For a known signal of energy \(E\) in white noise of power spectral density \(N_0/2\), the achievable performance depends only on the ratio \(d^2 = 2E/N_0\), the deflection or effective signal-to-noise ratio (SNR). Larger \(d\) pushes the two hypothesis distributions apart and lets you get both low \(\alpha\) and high \(P_{\text{D}}\); that single scalar summarizes how detectable the signal fundamentally is, independent of the threshold you later pick. Yet that lone number says nothing about which threshold is worth choosing, so to expose the full menu of operating points we sweep the threshold and plot the detector's entire trade-off curve.
Mental Model
Picture a jury weighing the exact same set of crime-scene clues against two rival stories: the defendant did it, or an innocent coincidence produced everything. The jury does not tally clues one by one; it asks a single comparative question, "how expected was this precise batch of evidence under the guilty story versus under the innocent story?" That ratio of expectedness is the likelihood ratio, and it is all the jury needs, no matter how many clues there were. The judge then sets a conviction bar, "beyond reasonable doubt," high or low depending on how much a wrongful conviction is feared: that bar is the threshold \(\eta\), and a wrongful conviction is the false alarm you are budgeting for. Neyman-Pearson says this one-ratio-against-one-bar procedure convicts the most guilty parties possible for any fixed tolerance of wrongful convictions, which is why every clue collapses into that single comparative score.
ROC curves: reading a detector's whole character
Sweep the threshold across its full range and plot \(P_{\text{D}}\) against \(P_{\text{FA}}\), and you trace the Receiver Operating Characteristic (ROC) curve, an idea that literally comes from World War II radar operators. The curve rises from \((0,0)\), where the threshold is so high nothing is ever declared, to \((1,1)\), where everything is. A useless detector that ignores the data sits on the diagonal; a perfect one hugs the top-left corner. The area under the curve (AUC) compresses the whole picture into one number: the probability that the detector ranks a random true event above a random noise sample. The curve itself is what you deploy against, because the right operating point depends on the relative cost of a false alarm versus a miss in your application. The code below builds an ROC from scratch for a Gaussian detection problem. Figure 4.6.2 illustrates ROC curve construction from threshold sweep.
The area under the curve deserves a precise reading of its own. Concretely, AUC is the probability that the detector assigns a higher score to a randomly drawn true event than to a randomly drawn noise sample, so 0.5 is coin-flip-useless and 1.0 is perfect separation. It matters because it collapses ranking quality across every possible threshold into one number, which lets you compare two detectors before committing to any operating point; it earns that summary by integrating \(P_{\text{D}}\) over all \(P_{\text{FA}}\) values as the threshold sweeps. Reach for AUC when you want a single threshold-free score to screen or rank candidate detectors, but switch back to a specific point on the ROC (or to a precision-recall curve) the moment you deploy, since deployment fixes exactly one threshold and rare-event costs can make whole regions of the curve irrelevant.
import numpy as np
def roc_from_scores(scores, labels, n_points=200):
# scores: detector statistic (e.g. log-likelihood ratio); labels: 1=signal, 0=noise
thr = np.linspace(scores.min(), scores.max(), n_points)
P = labels.sum(); N = (labels == 0).sum()
pfa, pd = [], []
for t in thr:
fire = scores >= t
pd.append(np.sum(fire & (labels == 1)) / P) # detection prob
pfa.append(np.sum(fire & (labels == 0)) / N) # false-alarm prob
pfa, pd = np.array(pfa), np.array(pd)
auc = np.trapz(pd[::-1], pfa[::-1]) # integrate along PFA
return pfa, pd, auc
rng = np.random.default_rng(0)
d = 2.0 # deflection (signal separation)
noise = rng.normal(0.0, 1.0, 20000) # H0 samples
signal = rng.normal(d, 1.0, 20000) # H1 samples
scores = np.concatenate([noise, signal])
labels = np.concatenate([np.zeros(20000), np.ones(20000)])
pfa, pd, auc = roc_from_scores(scores, labels)
print(f"AUC = {auc:.3f}") # ~0.921 for d = 2
# operating point closest to a 1% false-alarm budget:
i = np.argmin(np.abs(pfa - 0.01))
print(f"at PFA={pfa[i]:.3f}, detection prob PD={pd[i]:.3f}")
d lifts the whole curve toward the top-left corner and raises AUC; the final lines read off the detection probability you actually get if you cap false alarms at one percent. (As of 2024, NumPy 2.0 renamed np.trapz to np.trapezoid; the old name still works as a deprecated alias.)The last two lines refuse a single accuracy figure: they pin the operating point to a false-alarm budget, then read off the detection probability bought at that price. That is the honest way to quote a detector, and the discipline Chapter 65 demands for leakage-safe benchmarking.
Practical Example: The Radar That Kept Its False-Alarm Rate Constant
An automotive radar team ships a forward-collision detector. In the lab, a fixed threshold on return power works beautifully. On the road it falls apart: driving past a metal guardrail, tunnel walls, or heavy rain raises the background clutter power, and a fixed threshold that was tuned for open highway suddenly fires constantly, one false brake event after another. The fix is a constant false alarm rate (CFAR) detector, a direct application of Neyman-Pearson thinking: rather than compare each range-Doppler cell to a fixed number, estimate the local noise-plus-clutter level from neighboring cells and set the threshold a fixed multiple above that. The threshold now floats with the environment so the false-alarm probability typically stays close to its target, say, one in a million cells, across a wide range of clutter conditions (the guarantee is exact only when the local statistics match the assumed noise model), while detection of genuine targets that stick out above their local background is preserved. The team stopped chasing a magic power value and instead fixed the error rate they cared about; the threshold took care of itself. This same adaptive-threshold pattern reappears for machine-fault detection in Chapter 37, where the "clutter" is normal operating vibration.
Deciding over time, and the peril of many tests
Adapting the threshold across neighboring cells in space, as CFAR does, is only half the story, because a sensor stream also forces us to decide as evidence unfolds over time. Two extensions matter constantly in streaming sensor systems. First, you rarely get one shot. If samples keep arriving, Wald's sequential probability ratio test (SPRT) accumulates the log-likelihood ratio. It declares the moment that running total crosses an upper bound (signal) or lower bound (noise), and otherwise waits for more data. For the same error rates, sequential testing reaches a decision with fewer samples on average than any fixed-sample test. That efficiency is why it underlies low-power wake-word and change-point detectors: decide early on easy cases, and keep listening only when the evidence is genuinely ambiguous. Change detection in Chapter 12 builds its cumulative sum (CUSUM) algorithm directly on this accumulating-evidence idea.
Step-Through: Wald's Sequential Probability Ratio Test
Trace the SPRT deciding between \(H_0: x \sim \mathcal{N}(0,1)\) and \(H_1: x \sim \mathcal{N}(1,1)\), with target error rates \(\alpha = \beta = 0.05\). Each sample adds a log-likelihood increment \(\log\frac{p_1(x)}{p_0(x)} = x - 0.5\) to a running total \(S\). We stop and declare \(H_1\) when \(S \ge A = \log\frac{1-\beta}{\alpha} = \log 19 = 2.944\), or declare \(H_0\) when \(S \le B = \log\frac{\beta}{1-\alpha} = \log 0.0526 = -2.944\). Feeding it a stream that truly came from \(H_1\):
- \(x_1 = 0.3\): increment \(-0.20\), running \(S = -0.20\). Inside \((-2.944,\ 2.944)\), keep listening.
- \(x_2 = 1.8\): increment \(+1.30\), running \(S = 1.10\). Still inside, keep listening.
- \(x_3 = -0.4\): increment \(-0.90\), running \(S = 0.20\). A misleading sample pulls it back; keep listening.
- \(x_4 = 2.1\): increment \(+1.60\), running \(S = 1.80\). Keep listening.
- \(x_5 = 1.2\): increment \(+0.70\), running \(S = 2.50\). Keep listening.
- \(x_6 = 1.5\): increment \(+1.00\), running \(S = 3.50 \ge 2.944\). Cross the upper bound, declare \(H_1\) and stop.
The decision arrives after six samples, and a fixed-sample test tuned to the same error rates would on average have needed more (the saving is in the expected sample count, not guaranteed on every single run). Notice sample three temporarily eroded the evidence: the SPRT does not panic, it simply waits until the accumulated ratio genuinely clears a bound.
Real-World Application: Gravitational-Wave Detection (LIGO)
When the Laser Interferometer Gravitational-Wave Observatory (LIGO) searches for a black-hole merger, the signal is buried far below the instrument noise, so the pipeline correlates the strain data against a bank of hundreds of thousands of pre-computed waveform templates, the exact matched-filter, correlate-and-threshold detector this section derives for a known signal in noise. Each candidate is scored by its false-alarm rate, and only events rarer than roughly one per hundred thousand years of background are declared, which is precisely a Neyman-Pearson threshold set to a punishing false-alarm budget. The first detection, GW150914 in 2015, cleared that bar with a false-alarm rate below one per 200,000 years.
The Detection Test So Good It Was Classified
Abraham Wald developed the sequential probability ratio test in 1943 while working for the wartime Statistical Research Group at Columbia. It was so effective at cutting the number of samples needed to accept or reject munitions batches, sometimes by more than half, that the U.S. military classified it as a secret weapon and blocked publication until after the war. The reasoning was blunt: the same math that let American inspectors clear good shells faster would let the enemy do likewise, so the equations were treated like a piece of ordnance. Wald could not publish his own theorem until 1945.
Lab: Matched-Filter Heartbeat Detection on a Real ECG
Goal (about 25 minutes): build a matched-filter QRS detector on a real ECG and see the ROC trade-off with your own eyes, using no synthetic data.
Tools: Python with scipy, numpy, scikit-learn, and matplotlib. The signal ships inside SciPy: scipy.datasets.electrocardiogram() returns about five minutes of real ECG sampled at 360 Hz.
- Load and template: load the ECG, then hand-pick one clean heartbeat by slicing a roughly 40-sample window around a visible R-peak; normalize it to unit energy. This is your known-signal template.
- Score: slide the template across the whole record with
numpy.correlate(orscipy.signal.correlate) to produce a matched-filter score at every sample. - Label: mark ground-truth beats with
scipy.signal.find_peakson the raw signal (tune the height and distance so the count looks right, around 70 beats per minute), and call a score-peak a true detection if it lands within a few samples of a labeled beat. - Vary and observe: sweep the detection threshold and, at each level, record detection probability against false-alarm count; feed the score-peaks and labels to
sklearn.metrics.roc_curveand plot the ROC. Then add white noise to the ECG at falling SNR and watch the curve sag toward the diagonal, confirming that detectability is set by the deflection, not by the threshold.
Observe how a threshold that catches every beat also fires on tall T-waves, exactly the false-alarm-versus-miss exchange this section is about.
Second, beware the multiplicity trap. A monitoring system that runs one test per second at a one-percent false-alarm rate produces, in expectation, 36 false alarms every hour purely by chance, with the data behaving exactly as it should. A p-value is the probability, under \(H_0\), of a statistic at least as extreme as observed; it is not the probability that \(H_0\) is true, and it is not a measure of effect size. Because that p-value is uniform under \(H_0\), enough tests will always surface small ones. When you test many hypotheses, control the family-wide error (a Bonferroni cap on \(\alpha\), or a false-discovery-rate (FDR) procedure) rather than each test in isolation. Sensor fleets that ignore this drown their operators in spurious alerts and train them to ignore the real ones.
Right Tool: ROC, AUC, and Threshold Tuning in Two Calls
The hand-rolled ROC loop above is fine for teaching, but production code should not reimplement it. sklearn.metrics.roc_curve and roc_auc_score collapse the whole sweep, the AUC integration, and the numerically careful threshold handling into two lines, roughly a 90 percent reduction over the from-scratch version, and add precision-recall variants for the heavily imbalanced case that dominates rare-event sensing. For classical detection statistics and multiple-testing corrections, scipy.stats and statsmodels.stats.multitest provide the likelihood-ratio tests, p-value machinery, and Bonferroni/FDR adjustments so you configure the error control rather than derive it. Reserve the from-scratch code for understanding what these calls compute.
Research Frontier
Neyman-Pearson hands you an optimal threshold only when both likelihoods are known, yet modern sensor detectors are neural networks whose raw scores carry no such calibration, so a threshold chosen on validation data can silently miss its false-alarm target in deployment. Conformal risk control (Angelopoulos, Bates, Fisch, Lei, Schuster, and Jordan, 2024) closes this gap: it wraps any black-box detector score with a distribution-free procedure that provably caps a chosen error rate, for example the miss rate, at a user-set level using only an exchangeable calibration set and no likelihood model at all. This extends the classical "tune the threshold to a false-alarm budget" discipline of this section from equal-variance Gaussians to arbitrary learned scores, and it is now being applied to false-alarm control in learned radar and biomedical detectors where the underlying distributions are unknown.
Exercise
Using the two-Gaussian setup from the code block: (a) Re-run for deflections \(d \in \{0.5, 1, 2, 4\}\) and tabulate AUC and the detection probability achievable at a one-percent false-alarm budget; explain the trend in terms of how far apart the hypotheses sit. (b) Derive why, for equal-variance Gaussians, thresholding the log-likelihood ratio is equivalent to thresholding the raw sample. (c) Your application says a missed detection is 20 times as costly as a false alarm. Write the expression for the Bayes-optimal threshold in terms of the ratio of costs and the prior odds of \(H_1\), and compute where it lands.
Self-Check
- Why is a single accuracy number a poor summary of a detector when signal events are rare, and what should you report instead?
- State the Neyman-Pearson lemma in one sentence, and explain why it means the entire observation can be reduced to one scalar before thresholding.
- A dashboard runs 3600 independent tests per hour, each at \(\alpha = 0.01\). Roughly how many false alarms should you expect per hour under \(H_0\), and which correction would you apply to fix it?
Try It: Build and Break a Matched-Filter Detector
In about forty lines of Python (numpy, scikit-learn, matplotlib) you can watch Neyman-Pearson theory turn into a working ROC curve on a laptop.
- Make a template: build a 64-sample pulse as a Gaussian-windowed sinusoid and normalize it to unit energy with numpy.
- Synthesize data: generate 5000 noise-only windows (white Gaussian noise) and 5000 signal windows (template plus that same noise) at a chosen SNR; label them 0 and 1.
- Score every window: compute the matched-filter statistic as the dot product of each window with the normalized template, which is the optimal statistic for this known-signal-in-white-noise problem.
- Draw the ROC: pass the scores and labels to
sklearn.metrics.roc_curveandroc_auc_score, then plot \(P_{\text{D}}\) against \(P_{\text{FA}}\), and read off the detection probability at the threshold whose \(P_{\text{FA}}\) is closest to 0.01. - Break it: drop the SNR by 6 dB, rerun, and watch the curve sag toward the diagonal and the AUC fall, confirming that detectability is governed by the deflection, not by any cleverness in the threshold.
What's Next
Section 4.7 turns to Monte Carlo and sampling, the workhorse for the many detection and estimation integrals that have no closed form. When the likelihood ratio, the false-alarm rate, or the threshold cannot be computed analytically, we estimate them by drawing samples, and the same sampling machinery will later power the particle filters and Bayesian computations that recur throughout the book.