Skip to content

Models

ABERRANT groups detectors by the structure they model, not by a single shared score scale. Choose a family from the event shape and anomaly mechanism first; then compare warm-up, state growth, latency, and calibration within that family.

Choose by problem shape

Problem Useful starting point Why
General multivariate events OnlineIsolationForest Incremental trees with explicit sliding-window unlearning and a bounded [0, 1] score
Periodically rebuilt reference model ASDIsolationForest or StreamRandomHistogramForest Replaces a forest from recent-window state rather than adapting every old split forever
Local density or neighborhood isolation LocalOutlierFactor, SDOStream, or a cell-neighborhood detector Scores relative to nearby points, observers, or radius cells
Fixed-size projection/frequency state StreamingLODA, MStream, or StreamingRSHash Memory is controlled by projection, histogram, or sketch dimensions
Dynamic edges MIDAS, ISCONNA, AnoEdgeL, or SignedGraphSketchDetector Models repeated edges, endpoint patterns, dense submatrices, or graph-level structure
Scalar time-series discords RollingMatrixProfile or XLagDAMP Exact rolling nearest-subsequence scores or approximate DAMP discord scores over bounded history
Departures from a known seasonal forecast SeasonalResidualDetector Scores one-step-ahead errors from additive level, trend, and seasonal components
Changing relationships between time-series channels MultivariateRollingMatrixProfile Exact joint subsequence novelty requiring all channels to match the same historical interval
Interpretable local statistic change aberrant.model.stat Measures candidate-induced changes in means, spread, moments, covariance, or correlation
Learned reconstruction error OnlineAutoencoderEnsemble or optional Autoencoder Uses NumPy or user-supplied PyTorch autoencoders
Static or adaptive score policy ThresholdModel or QuantileThreshold Converts feature or detector-score boundaries into a decision-oriented output

Feature scaling is part of the model specification. Distance-based methods and reconstruction models are scale-sensitive. HalfSpaceTrees specifically expects features scaled to [0, 1]. Isolation methods based on randomized cuts often need less scaling, but a transform can still change their geometry.

Score contracts

Isolation forests

Class Default score Built-in warm-up State policy
ASDIsolationForest [0, 1]; 0.0 before a forest exists First complete window_size Bounded recent window; complete forest replacement every retrain_interval events
HalfSpaceTrees [0, 1]; 0.0 before reference mass exists First complete window_size Fixed trees and two window-mass counters; no retained sample window
MondrianIsolationForest [0, 1]; 0.0 through the first learned event At least two learned events Growing online Mondrian trees; no automatic forgetting or state bound
OnlineIsolationForest [0, 1]; early scores are possible but not calibrated No explicit zero-score warm-up At most window_size retained events plus trees; evicted events are unlearned
RandomCutForest Raw non-negative CoDisp by default; optional [0, 1) transform warmup_samples inserted shingles, defaulting to sample_size At most sample_size shingled points plus shingle history
StreamRandomHistogramForest Non-negative summed log leaf-mass score First complete window_size Bounded alternating reference/current-window forest state
XStream [0, 1]; 0.0 until ready Projection initialization plus one complete reference window Fixed count-min sketches, chains, and bounded feature-hash cache when configured

MondrianIsolationForest is an ABERRANT hybrid: it uses online Mondrian block extension but Isolation Forest path-length scoring. The original Mondrian Forest is supervised and does not define this anomaly score.

Its default lifetime mode can keep growing on expanding streams. For bounded retention, set window_size: trees periodically rebuild from the latest complete window, representing at most twice that window minus one event between rebuilds. This changes the reference history and introduces periodic rebuild work.

OnlineIsolationForest.n_jobs=1 executes tree work sequentially; n_jobs=-1 uses all logical CPUs reported by the operating system. Positive values request that many worker threads.

The raw and normalized Random Cut Forest forms preserve ranking but not numeric scale:

Python
from aberrant.model.iforest import RandomCutForest

history = [
    {"x": 0.0, "y": 0.1},
    {"x": 0.1, "y": -0.1},
    {"x": -0.1, "y": 0.0},
    {"x": 0.2, "y": 0.1},
]
query = {"x": 4.0, "y": 4.0}

raw_detector = RandomCutForest(
    n_trees=10,
    sample_size=4,
    warmup_samples=4,
    normalize_score=False,
    seed=3,
)
bounded_detector = RandomCutForest(
    n_trees=10,
    sample_size=4,
    warmup_samples=4,
    normalize_score=True,
    score_scale=8.0,
    seed=3,
)

for event in history:
    raw_detector.learn_one(event)
    bounded_detector.learn_one(event)

print(f"raw CoDisp: {raw_detector.score_one(query):.3f}")
print(f"bounded transform: {bounded_detector.score_one(query):.3f}")

Primary method sources include Isolation Forest, Half-Space Trees, Online Isolation Forest, Random Cut Forest, and xStream. Class docstrings and the isolation API identify the exact source used by each implementation.

Distance and neighborhood detectors

Class Default score Built-in warm-up State policy
KNN Engine-defined; the FAISS engine returns mean Euclidean distance to the k nearest retained points Engine-defined; FAISS returns 0.0 before warm_up Engine-defined; FAISS uses a window_size sliding window
LocalOutlierFactor Non-negative LOF; values near 1 indicate density comparable to neighbors, larger values indicate lower local density Returns 0.0 until more than k points are learned At most window_size retained points
CellNeighborhoodDetector [0, 1] from radius-neighbor scarcity At least warm_up_slides * slide_size events and k + 1 retained points At most window_size points plus cell indexes
SDOStream Non-negative median distance to nearest active observers warm_up_observers, defaulting to at least x_neighbors Exactly up to k observers with exponentially faded activity
StationaryRegionNeighborDetector [0, 1] from radius-neighbor scarcity At least warm_up_slides * slide_size events and k + 1 retained points At most window_size points plus cells and query caches

For the FAISS-backed KNN, configure warm_up >= k; otherwise the engine can be warm while still holding too few points for the requested neighbor count. Install it with aberrant[faiss].

CellNeighborhoodDetector is a point-scoring, NETS-inspired adaptation, not the paper's exact set-level outlier procedure. Likewise, StationaryRegionNeighborDetector uses radius counts and per-cell cache invalidation rather than STARE's exact KDE/top-n procedure. The distinction is intentional and documented in their API reference. SDOStream follows the fixed observer and exponential-fading structure described in the SDOstream paper.

Sketch detectors

Class Default score Built-in warm-up State policy
MStream Non-negative log1p transform of candidate-inclusive multi-aspect sketch statistics A bucket-index distance of warm_up_buckets from the first learned bucket; default 0 Fixed numeric, categorical, and record sketches
StreamingLODA Non-negative mean negative log histogram density Exactly warm_up_samples learned events Fixed projections and histograms; bin edges freeze after warm-up, counts optionally decay
StreamingRSHash [0, 1] from low faded hash occupancy Exactly warm_up_samples learned events Fixed subspaces and count tables with fading

These are bounded streaming adaptations. StreamingLODA fixes its projection count and histogram edges rather than reproducing the original paper's model selection procedure; StreamingRSHash adds online standardization and fading instead of reproducing the paper's window procedure. See the primary LODA, MStream, and RS-Hash sources for the methods from which they derive.

For all three classes, time_key=None uses one-based arrival order. With an explicit time_key, timestamps must be finite and non-decreasing; MStream additionally requires integer-like time buckets. The time field is removed before feature vectorization. MStream warm-up uses the integer bucket-index distance, so gaps between explicit bucket numbers count toward warm_up_buckets even when no event was observed in those buckets.

Graph-stream detectors

Class What it scores Default score State policy
AnoEdgeL Candidate edge membership in maintained local dense submatrices Non-negative raw score; optional [0, 1) Fixed higher-order sketches and configured submatrices
ISCONNA Edge/endpoint frequency, consecutive-width, and gap patterns Non-negative raw combined G-test score; optional [0, 1) Fixed count-min sketches
MIDAS Candidate-inclusive edge microcluster statistic; MIDAS-R also scores endpoints Non-negative raw chi-square-style score; optional [0, 1) Fixed count-min sketches
SignedGraphSketchDetector Change in a graph's signed shingle sketch relative to online centroids Non-negative distance-plus-novelty score; optional [0, 1) At most max_graphs sketches plus fixed centroids

AnoEdgeL, ISCONNA, and MIDAS require integer-like source, destination, and explicit timestamp values. Their timestamps are non-decreasing integer buckets; equal timestamps represent edges in the same bucket. If time_key=None, each arrival receives the next implicit bucket.

SignedGraphSketchDetector additionally requires a graph identifier. Its identifiers are finite numeric values, and its explicit timestamps are also integer-like. It is a signed-count/Euclidean-centroid structural adaptation; it does not reproduce StreamSpot's Hamming-sketch clustering workflow.

This complete edge-stream example uses no dataset download:

Python
from aberrant.model.graph import MIDAS

detector = MIDAS(
    source_key="src",
    destination_key="dst",
    time_key="t",
    warm_up_samples=2,
    normalize_score=True,
    seed=11,
)

edges = [
    {"src": 1.0, "dst": 2.0, "t": 1.0},
    {"src": 1.0, "dst": 2.0, "t": 1.0},
    {"src": 1.0, "dst": 3.0, "t": 2.0},
    {"src": 9.0, "dst": 10.0, "t": 3.0},
]

for edge in edges:
    score = detector.score_one(edge)
    detector.learn_one(edge)
    print(f"edge={int(edge['src'])}->{int(edge['dst'])}, score={score:.3f}")

Primary references are linked from each class, including MIDAS, ISCONNA, AnoGraph/AnoEdge, and StreamSpot.

Seasonal forecast residuals

SeasonalResidualDetector scores an observation against its expected value for the current seasonal position. It supports one consistently named feature, additive seasonality, and a known period at a fixed sampling cadence. For hourly data with a daily cycle, set season_length=24.

Python
from aberrant.model.timeseries import SeasonalResidualDetector

detector = SeasonalResidualDetector(season_length=4, key="requests", min_scale=1.0)
for value in [10.0, 20.0, 15.0, 5.0] * 3:
    event = {"requests": value}
    score, forecast, residual = detector.explain_one(event)
    if detector.is_ready:
        print(score, forecast, residual)
    detector.learn_one(event)

The first 2 * season_length learned events initialize the components. Before readiness, score_one returns 0.0 and explain_one returns (0.0, None, None). Each cycle's mean defines its midpoint level; the difference between cycle means divided by the period estimates trend. The average detrended value at each seasonal position defines seasonality, and the second cycle's midpoint level is advanced to its final position. Initialization is deterministic and uses only learned observations.

Thereafter, the prior forecast is level + trend + season[phase]. Learning updates level from the seasonally adjusted observation, trend from the level change, and that phase's seasonal component from the observation minus the prior level and trend. The alpha, beta, and gamma parameters follow the additive Holt-Winters component equations; beta denotes beta-star and gamma must be at most 1-alpha. Smoothing weights are supplied by the caller, with no parameter optimization.

The default score is absolute forecast error divided by the previously learned mean absolute error, floored at min_scale. Warm-up initializes this scale from its fitted errors; later updates smooth actual prior forecast errors using residual_alpha. Choose min_scale in the feature's units for nearly noiseless series. This is an error ratio, with no probability interpretation or fixed upper bound. Set normalize=False for absolute error in input units. explain_one returns (score, forecast, signed_residual); a negative residual identifies an unexpectedly low observation. All scoring calls are read-only.

Every successful learn_one advances one seasonal position, even without a preceding score. Learn every observation at the intended cadence: skipping an anomaly update would also skip its position. Learning anomalies can change both the forecast and residual scale. Fill missing observations upstream; this model does not inspect timestamps or infer gaps. Use consistent input units, since an adaptive upstream scaler changes the coordinates of its retained forecast. It does not discover seasonal periods or support multiple simultaneous cycles.

State and initialization work are O(season_length); steady scoring and learning are O(1). reset clears learned state and any inferred name while preserving an explicitly configured key. Invalid events and unrepresentable arithmetic leave learned state unchanged; the latter raises OverflowError. The catalog ID is seasonal_residual_detector. The seeded example shows a low observation that is ordinary at another seasonal position.

Time-series discord detection

RollingMatrixProfile gives an exact nearest-subsequence distance on every event after warm-up, using only the current event and previously learned values. It accepts exactly one consistently named finite numeric feature. Higher scores indicate greater novelty; match_one also returns the nearest reference's absolute, zero-based start index since the last reset.

Python
from aberrant.model.timeseries import RollingMatrixProfile

detector = RollingMatrixProfile(subsequence_length=4, window_size=32)
for value in [0.0, 1.0, 0.0, -1.0] * 20:
    event = {"value": value}
    score, reference_start = detector.match_one(event)
    # score_one(event) returns the same score without the reference index.
    if detector.is_ready:
        print(score, reference_start)
    detector.learn_one(event)

window_size counts the entire comparison window including the candidate; the oldest sample is excluded from scoring when it would fall outside that window. Learning retains at most window_size samples, defaulting to 16 * subsequence_length. Expired references cannot remain nearest matches. Scoring never changes history or locks the feature name; learning works even without preceding scoring calls.

With subsequence length m, the default exclusion radius is ceil(m / 4). Reference starts must be more than that radius behind the query start, so overlap is allowed. Set exclusion_zone=m-1 for nonoverlapping references. The window must hold at least m + exclusion_zone + 1 samples. Scoring starts after m + exclusion_zone learned samples, without waiting for the window to fill; earlier calls return 0.0 and no match. Use is_ready to distinguish warm-up zeros from genuine zero distances. Scores belong to the subsequence ending at the current event, and equal distances choose the earliest retained reference.

By default, each subsequence is independently z-normalized using population standard deviation, so scores describe shape rather than absolute level or amplitude. Normalized distances range from zero to 2 * sqrt(m), apart from floating-point roundoff. Two constant subsequences have distance zero even at different levels; a constant and a nonconstant have distance sqrt(m). Use normalize=False for ordinary Euclidean distances when level and amplitude changes matter. Raw distances are not normalized to [0, 1]; distances beyond the floating-point range raise OverflowError.

The implementation recomputes an FFT distance profile and directly checks potential minima. Memory is O(window_size); typical scoring work is O(window_size * log(window_size)), with up to O(window_size * subsequence_length) for numerically ambiguous candidates. Learning computes only the newest window's statistics. No restart or batch retraining is needed as history rolls. This API supplies the latest left-profile score and match; it does not maintain a historical profile or return global motif/discord rankings. The exclusion and constant-window conventions follow STUMPY.

MultivariateRollingMatrixProfile extends this scoring contract to aligned observations with one or more fixed, named channels. At each eligible historical start it takes the largest channel distance, then selects the interval with the smallest such distance: min(reference, max(channel, distance)). Every channel shares the same reference start. Separate scalar detectors can each find a familiar shape at different times and miss a new relationship between channels; requiring a common match can expose that change.

Python
from aberrant.model.timeseries import MultivariateRollingMatrixProfile

detector = MultivariateRollingMatrixProfile(
    subsequence_length=4, window_size=32, exclusion_zone=3
)
for value in [0.0, 1.0, 0.0, -1.0] * 20:
    event = {"sensor_a": value, "sensor_b": 2 * value}
    score, reference_start, channel_distances = detector.explain_one(event)
    if detector.is_ready:
        print(score, reference_start, channel_distances)
    detector.learn_one(event)

explain_one returns a fresh mapping of distances at the selected common match, in sorted feature-name order. Its maximum equals the score. These distances describe the mismatch with that interval, not a causal diagnosis or each channel's independently nearest match. match_one returns just the score and reference index; score_one returns just the score. All three are read-only.

The first successful learn_one fixes the feature-key set; every subsequent event must include all channels. Input order is canonicalized, but observations must already be aligned. Warm-up lasts m + exclusion_zone learned events; explain_one returns (0.0, None, {}) before readiness. The same overlap rules, earliest-match tie breaking, bounded eviction, and reset behavior as the scalar model apply. Use exclusion_zone=m-1 to prohibit overlapping matches.

With normalize=True, each channel's query and reference windows are normalized independently. This preserves shape and relative timing differences while removing per-channel level and amplitude differences. Constant-window distances use the same zero/sqrt(m) convention as the scalar model. In raw mode, channels with larger units can dominate the maximum; use comparable units or fixed scaling when that is inappropriate. Raw scores beyond the floating-point range raise OverflowError. Adding an identical channel does not dilute the score.

For d fixed channels, typical scoring costs O(d * W * log(W)), with direct numerical refinement up to O(d * W * m) and learning cost O(d * m). State and working memory are O(d * W). Scoring continues through eviction without retraining. Both rolling models provide exact nearest distances; XLagDAMP's early-abandoned scores described below can be approximate.

This is a causal, bounded-window adaptation of the pre-max aggregation in Matrix Profile for Anomaly Detection on Multidimensional Time Series. The catalog ID is multivariate_rolling_matrix_profile; it is available in the core installation and in declarative model pipelines. The seeded streaming example demonstrates a changed phase relationship and its common-match explanation.

XLagDAMP accepts exactly one consistently named scalar feature. It scores the subsequence ending at the candidate event by z-normalized Euclidean distance to its nearest preceding subsequence. Scores are 0.0 before start_index; after that, high peaks are left-discord candidates. Early-abandoned non-peak scores can be approximate. Memory is bounded by x_lag + subsequence_length - 1 learned values.

The implementation follows the original X-Lag Amnesic, backward-only lookahead=0 procedure from Matrix Profile XXIV. Constant subsequences are rejected because z-normalized distance is undefined for zero variance under the reference restriction.

Statistical detectors

The univariate classes accept exactly one feature and retain at most window_size values:

  • MovingAverage, MovingHarmonicAverage, and MovingGeometricAverage;
  • MovingMedian and MovingQuantile;
  • MovingVariance, MovingInterquartileRange, and MovingAverageAbsoluteDeviation;
  • MovingKurtosis and MovingSkewness.

Their score is the change in the configured statistic after provisionally adding the candidate to the current window. It is 0.0 for an empty window. abs_diff=True returns magnitude; abs_diff=False preserves direction. MovingGeometricAverage.score_one requires a positive candidate, while learn_one ignores non-positive values. Its absoluteValues mode operates on successive growth factors.

Python
from aberrant.model.stat import MovingAverage

detector = MovingAverage(window_size=3, key="latency_ms")
for value in [10.0, 11.0, 9.0]:
    detector.learn_one({"latency_ms": value})

candidate = {"latency_ms": 25.0}
print(f"mean-change score: {detector.score_one(candidate):.3f}")
detector.learn_one(candidate)

MovingCovariance and MovingCorrelationCoefficient require exactly two features and score candidate-induced statistic change. MovingMahalanobisDistance accepts a fixed multivariate schema and returns squared Mahalanobis distance from the current window mean; it returns 0.0 with fewer than three learned points.

SVM detectors

IncrementalOneClassSVMAdaptiveKernel is a custom, budgeted RBF-kernel heuristic with running standardization and adaptive gamma. It returns the negative decision function, so higher values are more anomalous, and bounds support-vector state with sv_budget.

GraphGatedOneClassSVM routes updates and scoring through a user-supplied graph of incremental linear one-class SVM heuristics. It is unrelated to the published GADGET distributed optimization algorithm. Both classes are marked experimental; they do not claim parity with a published one-class SVM solver.

Reconstruction detectors

OnlineAutoencoderEnsemble is NumPy-backed and available in the base install. It has three observable phases:

  1. feature_map_warmup: collect correlation statistics for feature_map_grace events;
  2. detector_warmup: train sub-autoencoders and the output autoencoder for ad_grace events;
  3. ready: return non-negative reconstruction-error scores, optionally continuing to adapt when adaptive_after_warmup=True.

The parameter names (feature_map_grace, ad_grace) and phase names are deliberately different. This is a lightweight KitNET-inspired adaptation using raw inputs and simple NumPy autoencoders, not score- or procedure-equivalent to the authors' normalized denoising implementation. The source method is Kitsune/KitNET.

Autoencoder requires aberrant[dl]. The caller supplies an ABERRANT Architecture, a PyTorch optimizer, and a reconstruction loss. It has no built-in warm-up policy; score_one returns the supplied criterion's loss and learn_one performs one optimizer step.

Core utility models

Class Contract
ThresholdModel Stateless binary output: 1.0 when any configured ceiling or floor is violated, otherwise 0.0. Scalar bounds apply to every supplied feature; mapping bounds ignore unconfigured feature names.
QuantileThreshold Sliding score-window policy. It becomes ready after min(window_size, max(10, floor(0.1 * window_size))) learned scores and returns 1.0 at or above the quantile for any threshold sign. Below a positive threshold it returns max(score / threshold, 0.0); below a zero or negative threshold it returns 0.0.
NullModel Stateless baseline that always returns 0.0.
RandomModel Uniform [0, 1) baseline. Each score_one consumes one draw from its model-local generator; learn_one is a no-op.

QuantileThreshold is adaptive but not contamination-resistant: learning anomalous scores moves the score distribution it uses. Decide explicitly whether every score, only accepted-normal scores, or a delayed/calibration stream should update it.