NAME

Algorithm::Classifier::IsolationForest::Online - Online (streaming) Isolation Forest anomaly detection

SYNOPSIS

use Algorithm::Classifier::IsolationForest::Online;

my $oif = Algorithm::Classifier::IsolationForest::Online->new(
    n_trees          => 100,
    window_size      => 2048,
    max_leaf_samples => 32,
    seed             => 42,
);

# stream data through the model; each point is learned and old
# points beyond the window are forgotten automatically
$oif->learn(\@warmup_rows);

# prequential operation: score each point against the model as it
# stood BEFORE that point was learned, then learn it
my $scores = $oif->score_learn(\@new_rows);

# or score without learning
my $scores2 = $oif->score_samples(\@query_rows);
my $labels  = $oif->predict(\@query_rows);

# persistence keeps the window, so a reloaded model keeps forgetting
# correctly as the stream continues
$oif->save('oiforest_model.json');
my $resumed = Algorithm::Classifier::IsolationForest::Online->load('oiforest_model.json');

DESCRIPTION

Implements Online Isolation Forest (Online-iForest; Leveni, Weigert Cassales, Pfahringer, Bifet & Boracchi 2024 -- see REFERENCES), a streaming variant of Isolation Forest for data that arrives continuously and whose distribution may drift. There is no fit(): the model learns points as they arrive and, once more than window_size points have been seen, forgets the oldest point for every new one so the model always reflects the most recent window_size points of the stream.

Trees never store data points. Each node keeps only a running count of the points that passed through it and the bounding box of their feature values. A leaf splits once enough points have accumulated (see max_leaf_samples and growth); because the actual points are gone, the split simulates them by sampling uniformly inside the leaf's bounding box. Forgetting reverses the process: counts are decremented along the forgotten point's path and a subtree whose count falls below its split requirement is collapsed back into a leaf.

Scoring follows the classic Isolation Forest intuition -- anomalies isolate at shallow depth -- but normalises by the depth budget log(n/max_leaf_samples) / log(4) of the current window rather than the batch model's c(psi). Scores are in (0, 1] with high values anomalous, directly comparable in spirit (though not numerically) to the parent class's scores.

Both learning and scoring are accelerated through the parent class's Inline::C backend when it is available; use_c covers them together.

Learning (and the per-row walks inside score_learn) runs in C directly against the live trees, drawing randomness through the same generator in the same order as the pure-Perl path -- so, like the parent's fit(), a learn() with a given seed produces bit-identical trees whether use_c is on or off (on nvsize == 8 perls; wide-NV perls keep extra low bits in the pure-Perl path). The knob changes speed, never results.

Batch scoring lazily flattens the mutable trees into the same packed node layout the batch scorer walks -- online trees are axis-only, and the online per-leaf depth adjustment rides in the slot the batch packer uses for its own leaf adjustment -- so score_samples, predict, path_lengths, score_predict_samples, and score_predict_split all run through the same C (and OpenMP, when linked) tree walk the parent uses, with identical results to the pure-Perl fallback. Any learn invalidates the packed snapshot; the next batch-scoring call repacks once. score_learn never touches the snapshot: it mutates the trees after every single point, so its rows are scored by walking the live trees in C instead.

A model needs to have seen at least max_leaf_samples points before tree structure exists at all; until then every point scores 1.0. Give the model a warm-up learn() pass before trusting scores or labels.

Models saved by this class carry their own format tag. Algorithm::Classifier::IsolationForest->load recognises it and dispatches here, so callers can load either model type through the parent class.

GENERAL METHODS

new(%args)

Inits the object.

- n_trees :: number of isolation trees in the ensemble
    default :: 100

- window_size :: how many of the most recent points the model reflects.
        Once the stream exceeds this, learning a point forgets the
        oldest retained point.  0 or undef disables forgetting: the
        model then learns from the whole stream and retains no window
        (so nothing is ever unlearned and threshold relearning needs
        caller-supplied data).
    default :: 2048

- max_leaf_samples :: how many points a leaf must accumulate before it
        splits (eta in the paper).  Also the unit of the depth budget:
        trees stop splitting past log(n/eta)/log(4).
    default :: 32

- growth :: how the split requirement scales with depth (the reference
        implementation's `type` parameter).
          adaptive :: a leaf at depth k needs max_leaf_samples * 2**k
                      points to split -- deeper splits need
                      exponentially more evidence
          fixed    :: max_leaf_samples points regardless of depth
    default :: adaptive

- subsample :: probability in (0, 1] that a given tree learns (or
        forgets) a given point, drawn independently per tree per point.
        Values below 1 increase diversity among trees on very dense
        streams.  Note that, as in the reference implementation, learn
        and forget draws are independent, so per-tree counts are only
        approximate under subsampling.
    default :: 1.0

- seed :: optional integer to seed srand with, for reproducible trees
        given the same stream in the same order.  Processed via
        abs(int()).  Seeding happens here in new(), since there is no
        fit() to do it in.
    default :: undef

- contamination :: expected fraction of anomalies, in (0, 0.5]. When
        set, the first predict()-family call learns a score threshold
        that flags this fraction of the current window, and uses it as
        the default cutoff.  The threshold does NOT track the stream
        automatically afterwards; call relearn_threshold() to refresh
        it.  undef => no learned threshold (predict() falls back to
        0.5).
    default :: undef

- missing :: how learn() treats undef (missing) feature cells.  Scoring
        always tolerates undef (mapped to 0), matching the parent
        class's long-standing behaviour.
          die  :: croak if a learned point contains an undef cell
          zero :: treat a missing cell as the value 0
    default :: die

- feature_names :: optional arrayref of per-feature labels enabling the
        *_tagged methods (and required by mungers below).
    default :: undef

- mungers :: optional hashref of declarative L<Algorithm::ToNumberMunger>
        specs; every tagged row (learn_tagged, score_learn_tagged, the
        scoring *_tagged methods, tagged_row_to_array) is munged from
        raw values into numbers through the compiled plan, and
        munge_rows() applies the scalar mungers to positional rows.
        Requires feature_names; spec errors croak here.  The spec is
        saved with the model.  Identical semantics to the parent
        class's knob -- see MUNGERS in
        L<Algorithm::Classifier::IsolationForest> for details and
        caveats.
    default :: undef

- schema_version :: optional opaque string identifying the revision of
        the variable schema this model was built against.  Never
        parsed; saved with the model.  Usually set via a prototype --
        see PROTOTYPES in L<Algorithm::Classifier::IsolationForest>
        (whose new_from_prototype creates online models too).
    default :: undef

- schema_description :: optional opaque free-text description of what
        the variable schema is.  Same handling as schema_version.
    default :: undef

- feature_descriptions :: optional hashref of 'feature name => free
        text' describing individual features.  Requires feature_names;
        every key must name an entry there or new() croaks.  Partial
        coverage is fine.  Saved with the model.
    default :: undef

- use_c :: boolean, override whether the parent class's Inline::C
        backend is used, for learning and scoring both (see
        DESCRIPTION).  When false the instance runs pure Perl even if
        the C backend compiled.  Results are identical either way --
        learn() builds bit-identical trees for the same seed (on
        nvsize == 8 perls) and scoring matches exactly -- so only
        speed differs.
    default :: $Algorithm::Classifier::IsolationForest::HAS_C

- use_openmp :: boolean, override whether OpenMP parallel scoring is
        used inside the C tree walk.  Ignored when use_c is false.
    default :: $Algorithm::Classifier::IsolationForest::HAS_OPENMP

learn(\@data)

Learns the passed samples, in order, as the next points of the stream. Once the model has seen more than window_size points, each learned point also forgets the oldest retained point, so the model tracks the most recent window_size points.

The data format matches the parent class's fit: an arrayref of arrayrefs, each inner arrayref one sample of numeric features. All samples must have the same feature count; the count is locked in by the first sample ever learned.

Returns $self, so it chains.

$oif->learn(\@rows);

learn_tagged(\%row)

learn_tagged(\@rows)

Learns one sample supplied as a hashref of named feature values, or a whole batch supplied as an arrayref of such hashrefs, in stream order. The model must have feature_names set. Rows go through "tagged_row_to_array" (and therefore through the munger plan when mungers is configured). Returns $self.

$oif->learn_tagged({ cpu => 0.9, mem => 0.4, disk => 0.1 });
$oif->learn_tagged(\@hashref_rows);

Croaks under the same conditions as "tagged_row_to_array", naming the offending row by index in the batch form.

score_learn(\@data)

Prequential (test-then-train) operation, the usual way to run a streaming detector: each sample is scored against the model as it stood before that sample was learned, then learned. Returns an arrayref of anomaly scores, one per sample, in input order.

Unlike the pure scoring methods this works on a brand-new model too (the first points of a stream simply score 1.0, as nothing is known yet).

my $scores = $oif->score_learn(\@rows);

score_learn_tagged(\%row)

Prequential score-then-learn for a single sample supplied as a hashref of named feature values. Returns the scalar anomaly score the sample had before it was learned.

my $score = $oif->score_learn_tagged({ cpu => 0.9, mem => 0.4 });

Croaks under the same conditions as "tagged_row_to_array".

score_samples(\@data)

Returns an arrayref of anomaly scores in (0, 1] without learning anything. Scores near 1 are strong anomalies (isolated at shallow depth); scores well below 0.5 are normal.

my $scores = $oif->score_samples(\@data);

score_sample_tagged(\%row)

Scores a single sample supplied as a hashref of named feature values, without learning it. Returns a scalar anomaly score in (0, 1].

my $score = $oif->score_sample_tagged({ cpu => 0.9, mem => 0.4 });

Croaks under the same conditions as "tagged_row_to_array".

path_lengths(\@data)

Returns an arrayref of the mean isolation depth per sample across the trees, for inspection -- the streaming counterpart of the parent class's method of the same name. Depths include the per-leaf count adjustment.

my $depths = $oif->path_lengths(\@data);

predict(\@data, $threshold)

Returns an arrayref of 0/1 labels for the specified data, without learning it.

If $threshold is not given, the contamination-learned cutoff is used when available (learned from the current window on first use -- see contamination in "new"), otherwise 0.5.

Note that absolute score levels depend on window_size and max_leaf_samples (shallower depth budgets compress scores downward), so the 0.5 fallback is a blunt default here -- anomalies reliably rank above normal points, but may sit below 0.5. Setting contamination, or passing a threshold calibrated from observed scores, is recommended.

my $labels = $oif->predict(\@data);

predict_tagged(\%row, $threshold)

Predicts whether a single sample, supplied as a hashref of named feature values, is an anomaly. Returns a scalar 1 (anomaly) or 0 (normal). $threshold defaults the same way as in "predict".

my $label = $oif->predict_tagged({ cpu => 0.9, mem => 0.4 });

Croaks under the same conditions as "tagged_row_to_array".

score_predict_samples(\@data, $threshold)

Returns an arrayref of [$score, $label] pairs, one per sample, without learning. $threshold defaults the same way as in "predict".

my $results = $oif->score_predict_samples(\@data);

score_predict_sample_tagged(\%row, $threshold)

Scores and classifies a single sample supplied as a hashref of named feature values. Returns a two-element arrayref [$score, $label]. $threshold defaults the same way as in "predict".

my $pair = $oif->score_predict_sample_tagged({ cpu => 0.9, mem => 0.4 });

Croaks under the same conditions as "tagged_row_to_array".

score_predict_split(\@data, $threshold)

Same values as "score_predict_samples" but returned as two flat arrayrefs. In list context returns ($scores_aref, $labels_aref).

my ($scores, $labels) = $oif->score_predict_split(\@data);

relearn_threshold(\@data)

Re-derives the contamination decision threshold so it flags the requested fraction of the current window (or of \@data, when passed). Call this after the stream has drifted, or on whatever cadence threshold freshness matters; learning alone never moves the threshold.

Requires contamination to have been set. With window_size => 0 no window is retained, so \@data must be supplied.

Returns $self, so it chains.

$oif->relearn_threshold;

decision_threshold

The score cutoff the predict methods use by default; undef unless contamination was set and a predict-family method or "relearn_threshold" has run.

feature_names

Returns the arrayref of feature name strings stored with the model, or undef if none were provided.

schema_version

Returns the user-owned schema version string stored with the model (usually via a prototype -- see PROTOTYPES in Algorithm::Classifier::IsolationForest), or undef if none was recorded.

schema_description

Returns the free-text description of the variable schema stored with the model, or undef if none was recorded.

feature_descriptions

Returns the hashref of per-feature description strings stored with the model, or undef if none were recorded. Keys are feature names; coverage may be partial.

window_count

Returns how many points the model currently retains in its sliding window (0 when window_size => 0).

seen

Returns the total number of points learned over the model's lifetime, including points that have since been forgotten.

tagged_row_to_array(\%row, $caller)

Validates a hashref of named feature values against the model's stored feature_names and returns a positional arrayref. Identical semantics to the parent class's method of the same name (to which it delegates); see there for the croak conditions.

munge_rows(\@rows)

Applies the model's scalar mungers to positional rows, exactly as the parent class's method of the same name (to which it delegates); a model without mungers returns the input unchanged.

MODEL SAVE/LOAD METHODS

Persistence keeps the sliding window alongside the trees, so a reloaded model continues forgetting correctly as the stream resumes. This makes saved online models larger than batch models by O(window_size * n_features). Perl's RNG state is not persisted: a save/reload point breaks bit-for-bit reproducibility of subsequent learning versus an uninterrupted run, though scoring of the reloaded model is exact.

to_json

Returns a JSON representation of the model.

my $json = $oif->to_json;

from_json($json)

Init the object from the model in the specified JSON string.

my $oif = Algorithm::Classifier::IsolationForest::Online->from_json($json);

save($path)

Saves the model to the specified path.

$oif->save($path);

load($path)

Init the object from the model in the specified file.

my $oif = Algorithm::Classifier::IsolationForest::Online->load($path);

to_prototype

Returns a prototype JSON string extracted from this model: its variable schema (feature_names, feature_descriptions, mungers, missing policy) plus its current tuning knobs, with "class": "online". Identical semantics to the parent class's method -- see PROTOTYPES in Algorithm::Classifier::IsolationForest for the file format and the croak/placeholder rules. seed is not emitted; pass it as an override when creating from the prototype.

my $proto_json = $oif->to_prototype;

REFERENCES

Filippo Leveni, Guilherme Weigert Cassales, Bernhard Pfahringer, Albert Bifet, Giacomo Boracchi (2024). Online Isolation Forest.

https://arxiv.org/abs/2505.09593

https://github.com/ineveLoppiliF/Online-Isolation-Forest

https://proceedings.mlr.press/v235/leveni24a.html