In this article
Before you start
This page belongs to the machine learning algorithms path and leans on probability and statistics.
Why interviewers ask this
A monitoring system that learns while it predicts fails in ways a notebook never shows. The question separates candidates who know a library name from candidates who know the constraints: data arrives forever, memory is bounded, and the model sees each point once.
A weak answer says "train an isolation forest and call predict on each new row." A strong answer explains why that batch detector must be refitted, names a streaming alternative, and states the order of the loop: score, decide, then learn. A Staff-level answer adds what breaks: scores that are not probabilities, thresholds that drift, anomalies that arrive in clusters, and evaluation that must also run on a stream. The topic fits machine-learning system design questions about monitoring, fraud and sensor data, and a related page covers anomaly detection on a time series.
Anomaly detection on streaming data from first principles
- Streaming anomaly detection
- Streaming anomaly detection is a way of finding unusual observations in data that arrives continuously, in which a detector scores each new point once, updates a small model of normal behavior, and keeps a fixed-size summary instead of the full history, so memory and time per point stay bounded.
- Anomaly score
- An anomaly score is a number a detector assigns to one observation to say how unusual it is, where a higher value means more anomalous; its scale belongs to the detector, so it is not a probability and needs a threshold before it becomes a decision.
- Half-space trees
- Half-space trees are an online variant of isolation forests: a set of random trees whose nodes count how many recent points fall in each region, so a point landing in a sparse region receives a high anomaly score, with the counts kept over windows of recent points.
- Welford's online algorithm
- Welford's online algorithm is a single-pass method that updates a running mean and variance one value at a time, without storing the values, and avoids the precision loss of summing raw squares.
In plain words
Picture a night guard who knows what the building normally sounds like. Each night the guard hears a sound and decides if it is odd. Then the guard adjusts the idea of "normal" a little. The analogy breaks in one place. The guard remembers every night, but a streaming detector keeps only a summary, such as a mean and variance or a set of counters.
The constraints are specific. Lavin and Ahmad (2015) say streaming detection requires detectors to process data in real time, not batches, and to learn while simultaneously making predictions. The PySAD paper lists bounded memory, single-pass processing and constant-time complexity.
In one precise sentence: a streaming detector maps the next point and a small state to a score, then maps both to a new state.
Streaming detectors update one point at a time and keep a small summary instead of the history. The trade-offs section below compares them with batch detectors.
Step 1: Keep a small running model of normal
The running model is a state that stays the same size however many points arrive. For one metric, that state is a count, a mean and a variance, updated by Welford's online algorithm, which goes back to Welford (1962) and needs no stored values. Let x be the new value and n the count after adding x, so the old state summarised n−1 points, with old mean μ and old variance v. The updates are:
Many references keep the running sum of squared distances instead, written , with and a variance of ; it gives the same numbers as the formula above.
For many features at once, River's half-space trees hold the state as trees. The defaults are 10 trees, height 8 and a window size of 250 observations. They assume each feature lies between 0 and 1, so scale online with MinMaxScaler or pass limits; the feature scaling guide covers scaling. You now know what the state is; the next step uses it.
Step 2: Score each point before the model learns it
Scoring turns a new point into one number using only what the model learned from earlier points. In River every detector has a score_one method instead of predict_one, and normal observations should have a low score, whereas anomalous observations should have a high score. For half-space trees, high scores indicate anomalies.
The order matters. River's QuantileFilter example calls score_one, then classifies the score, then calls learn_one, so the model never sees a point before it scores it. If the model learns a spike first, the spike moves the mean and widens the variance, and the spike looks less odd. The worked example puts numbers on this.
The score is not a probability. The overview says the range of the scores is relative to each estimator, so the same number means different things for different detectors.
Step 3: Turn the score into a decision with a threshold
A cutoff on the score, called a threshold, turns the score into an alert or no alert. It can be a fixed constant or a running quantile of recent scores. River's QuantileFilter is the quantile kind; its documentation example wraps half-space trees with q=0.95 (an illustrative quantile) and a scaler in a pipeline.
For the z-score detector from Step 1, a cutoff of 3 standard deviations is a common illustrative choice, not a rule. A lower cutoff raises more alerts and more false alarms; a higher one misses weaker anomalies. Pick it from the cost of a false alarm against the cost of a miss, and say so in an interview.
The threshold is the part most likely to go stale. A fixed constant stays right only while the score's scale stays the same, and scores from a detector that keeps learning can drift. You now have a decision for each point; the last step keeps the model current.
Step 4: Learn from the point and move on
Learning updates the state with the point that was just scored, then discards the point. In River that is learn_one. The detector then keeps its fixed size: the z-score model keeps three numbers, and the half-space trees keep counts of recent points, so memory does not grow. Tan, Ting and Liu (2011) introduced half-space trees; their counts alternate between a reference window and a latest window of the window-size points.
Half-space trees also struggle when anomalies are packed together in windows: a burst of anomalies fills the counts and starts to look normal. The distribution shift page covers how to tell drift from a real anomaly.
Forgetting: what to do when normal itself moves
The z-score model above never forgets. Its mean moves by only 1/n of the gap at each point, so after a long history a real level shift is absorbed slowly and the detector keeps alarming. For non-stationary data, use an exponentially weighted mean and variance with a decay α between 0 and 1: and . Their effective memory is roughly 1/α recent points. Windowed statistics are the alternative.
Masking is the opposite risk. A learned spike inflates the variance and hides the next anomalies, so many systems skip learn_one on flagged points or use robust statistics such as the median absolute deviation. Skipping can also freeze the model after a real shift, so weigh the cost of each error.
Step-by-step description
- A small state describes normal.
- The point is scored before learning.
- The score becomes alert or no alert.
- The model learns the point last.
- The running model keeps a small state of a mean and variance or a set of trees.
- The model scores the point before it sees the point.
- A threshold turns the score into an alert or no alert.
- The model learns the point only after the point has been scored.
Knowing it is half the job. Saying it is the other half.
Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.
Start Free Trial →Worked example
The stream is 10, 12, 11, 13, and then a spike of 20 (illustrative values chosen so each step can be checked by hand). Welford's updates give a mean of 11.5 and a population variance of 1.25 after the first four points, and numpy's variance of the same four values agrees.1
Each point is scored against the model built from earlier points only. The 13 has a z-score of 2.45 against a mean of 11 and a standard deviation of 0.8165, so it stays under a cutoff of 3. The 20 scores 7.60 against a mean of 11.5 and a standard deviation of 1.118, so it is flagged.1
| Quantity | Value (computed, illustrative stream) |
|---|---|
| Mean after the first four points | 11.5 |
| Population variance after the first four points | 1.25 |
| z-score of 13, scored before learning | 2.45 |
| z-score of 20, scored before learning | 7.60 |
| z-score of 20 if learned first | 1.92 |
| Nodes in one tree of height 8 | 511 |
| Nodes in 10 trees of height 8 | 5,110 |
| Nodes in one tree of height 12 | 8,191 |
| Nodes in one tree of height 16 | 131,071 |
Scored before learning, the value 20 has a z-score of 7.60; scored after learning, it falls to 1.92, because the mean moves to 13.2 and the variance jumps to 12.56.1 The node counts follow River's statement that a tree of height h has 2 to the power h+1, minus 1, nodes and that computation time grows exponentially with tree height.1
Text description of the diagram
- Scored before learning, the value 20 has a z-score of 7.60.
- Scored after learning, the value 20 falls to a z-score of 1.92.
The pure-Python detector below is the loop of the four steps for one metric; the explanation follows the code, not the other way round.
import math
n, mean, var = 0, 0.0, 0.0
for x in [10, 12, 11, 13, 20]:
if n >= 2 and var > 0: # Step 2: score first
z = (x - mean) / math.sqrt(var)
print(x, round(z, 2), "ANOMALY" if abs(z) > 3 else "ok")
n += 1 # Step 4: then learn
new_mean = mean + (x - mean) / n
var = (var * (n - 1) + (x - mean) * (x - new_mean)) / n
mean = new_meanThe saved output of this loop on the stream is below; the warm-up points 10 and 12 are not scored.1
11 0.0 ok
13 2.45 ok
20 7.6 ANOMALYFor several features, the pattern is the same loop with a scaler and half-space trees. The snippet below follows the example on River's QuantileFilter documentation page (read on 2026-10-26); we did not run it for this page, so check the current River application programming interface (API) before relying on it.
from river import anomaly, datasets, preprocessing
model = preprocessing.MinMaxScaler() | anomaly.QuantileFilter(
anomaly.HalfSpaceTrees(seed=42), q=0.95
)
for x, _ in datasets.CreditCard().take(2000):
score = model.score_one(x) # score first
is_anomaly = model["QuantileFilter"].classify(score) # then decide
model.learn_one(x) # then learn
Trade-offs and edge cases
The right detector depends on the data. The table below is our own rule of thumb, not a vendor recommendation; LODA (Lightweight On-line Detector of Anomalies) is one of the detectors listed in River's anomaly overview.
| Stream | Detector to try first |
|---|---|
| One metric, no strong seasonality | A robust z-score built on the median absolute deviation |
| Cyclic metric | Remove the cycle first, then score the remainder |
| Many features, first try | LODA |
| Many features with known ranges | HalfSpaceTrees |
Why not reuse a batch detector? The scikit-learn undefined documentation shows a fit method and no partial_fit, and the PySAD paper says scikit-learn focuses on batch processing and PyOD lacks streaming optimizations. Batch detectors are refitted on stored history to take in new data, while streaming detectors update with one point at a time and keep a fixed-size summary such as a window, counters or a set of trees.
Text description of the diagram
- Batch detectors are refitted on stored history to take in new data.
- Streaming detectors update with one point at a time and keep a fixed-size summary such as a window, counters or a set of trees.
Evaluation also changes. River's streaming metric for the area under the ROC curve approximates the curve with a fixed set of thresholds, so its Shuttle example uses the scale-invariant rolling version of that metric over a window of 10,000 points instead. An exact area under the curve (AUC) of the ROC curve depends only on the ranking of the scores, so unbounded scores are not the problem.
A rolling window suits a stream for two other reasons: the data is non-stationary, so a recent window shows current quality, and evaluation is prequential, meaning each point is scored and then learned. The Shuttle page shows 90.07% for half-space trees and 96.46% for LODA on that one dataset, which is documentation output we did not reproduce, so do not generalize it.
The Numenta Anomaly Benchmark (NAB) corpus has 58 time-series files for research in streaming anomaly detection. Its authors describe a perfect detector as one that would detect all anomalies as soon as possible, trigger no false alarms, which is why a streaming score rewards earliness and not only accuracy.
Edge cases: a naive running sum of squares can lose precision when the mean is large relative to the spread (general numerical-analysis knowledge, and the reason to prefer Welford's one-pass update); a stream with a long quiet start gives a variance of zero, so score only after a warm-up; and a changing scale needs a threshold that follows it.
How to answer it in an interview
How would you do anomaly detection on streaming data in Python?
I'd run a loop that scores each point before it updates the model. For one metric I'd keep a running mean and variance with Welford's algorithm and flag a large z-score. For many features I'd use half-space trees from River, scaling features to the zero-to-one range first. The score is relative, not a probability, so I'd set the threshold from a running quantile or from the cost of false alarms. The trade-off is adaptation: forgetting lets the model follow a real level shift but can absorb a slow fault, so I'd use exponentially weighted statistics, skip learning on flagged points, and evaluate on a rolling window. I would not refit a batch model on every point.
The spoken structure: name the constraint, give the loop order, pick a detector, then say how the threshold and drift can fail.
Common mistakes
- "I'll call
IsolationForestorPyODon each new point." (wrong) Those are batch detectors: scikit-learn's undefined hasfitbut nopartial_fit, so new data means a refit on stored history. (right) "I'd use a streaming detector such as half-space trees, which updates per point." - "The anomaly score is a probability." (wrong) River says the range of the scores is relative to each estimator. (right) "The score is relative, so I calibrate a threshold on it."
- "Learn first, then score." (wrong) The model has already absorbed the spike, as the worked example shows. (right) "Score first, then learn."
- "Half-space trees work on raw features." (wrong) They assume values between 0 and 1. (right) "I scale online with
MinMaxScaleror set the limits." - "I'll evaluate once on a fixed test set." (wrong) The stream is non-stationary, so one number hides current quality, and River's streaming metric for the area under the ROC curve approximates the curve with fixed thresholds. (right) "I score before I learn and report a rolling metric such as a rolling area under the ROC curve, or a benchmark score that rewards early detection."
Follow-up questions
How much memory does a streaming detector need?
A fixed amount, whatever the stream length. The z-score model keeps three numbers, and River's default half-space trees hold 5,110 nodes in total (10 trees of height 8, 511 nodes each).1 A larger tree height grows the node count exponentially.
Why can't you call an isolation forest on each new point?
An isolation forest is a batch model: scikit-learn's undefined has fit but no partial_fit, so new data means refitting on stored history. Half-space trees are the online variant that update one point at a time.
How do you pick the anomaly threshold on a stream?
Use a running quantile of recent scores or a fixed constant chosen from the cost of false alarms and misses. Revisit it when the score's scale drifts.
How do you evaluate a streaming anomaly detector?
Score each point before learning it and measure with a rolling metric such as a rolling ROC AUC, or with a benchmark that rewards early detection. The ROC curve page explains the base metric.
What happens to the detector when the data drifts?
A detector that keeps learning follows slow drift, which is good for a real change and bad for a gradual fault. See distribution shift for how to separate the two.
- Streaming anomaly detection keeps a fixed-size state and handles each point once.
- The loop is score, decide with a threshold, then learn.
- Scoring a spike after learning it can shrink its z-score from 7.60 to 1.92 in the worked example.
- Half-space trees need features scaled to the zero-to-one range, and their scores are relative, not probabilities.
- Evaluate on a rolling window in score-then-learn order, because the stream is non-stationary.
Sources
- HalfSpaceTrees - River API documentation — River (online-ml), vendor documentation, 26 Oct 2026
- Anomaly detection module overview - River API documentation — River (online-ml), vendor documentation, 26 Oct 2026
- QuantileFilter - River API documentation — River (online-ml), vendor documentation, 26 Oct 2026
- Shuttle dataset - River API documentation — River (online-ml), vendor documentation, 26 Oct 2026
- Evaluating Real-time Anomaly Detection Algorithms - the Numenta Anomaly Benchmark (arXiv 1510.03336) — arXiv (Lavin and Ahmad), 12 Oct 2015
- Numenta Anomaly Benchmark repository — GitHub (Numenta), official repository, 26 Oct 2026
- PySAD: A Streaming Anomaly Detection Framework in Python (arXiv 2009.02572; the HTML revision read on 2026-10-26, first submitted 2020-09-05) — arXiv (Yilmaz and Kozat), 5 Sep 2020
- Note on a method for calculating corrected sums of squares and products (Technometrics 4(3)) — Technometrics (Welford), 1 Aug 1962
- IsolationForest - scikit-learn API documentation — scikit-learn, vendor documentation, 26 Oct 2026
- Fast Anomaly Detection for Streaming Data (IJCAI 2011) — IJCAI (Tan, Ting and Liu), 16 Jul 2011