view release on metacpan or search on metacpan
t/02-accel-selection.t
t/03-fit-determinism.t
t/04-accel-tuning.t
t/05-prebuilt-env.t
examples/README.md
examples/basic-anomaly-detection.pl
examples/axis-vs-extended.pl
examples/contamination-threshold.pl
examples/save-and-load.pl
examples/server-metrics.pl
examples/online-streaming.pl
benchmarking/bench-sklearn-scoring.pl
benchmarking/bench-fit.pl
benchmarking/bench-score.pl
benchmarking/bench-modes.pl
benchmarking/bench-fit-parallel.pl
benchmarking/bench-extended-fit-accel.pl
benchmarking/bench-axis-fit-accel.pl
benchmarking/bench-extended-predict-accel.pl
benchmarking/bench-axis-predict-accel.pl
benchmarking/BenchAccel.pm
benchmarking/bench-online-score-accel.pl view on Meta::CPAN
$m{c_openmp} = Algorithm::Classifier::IsolationForest::Online->new( %opts, use_c => 1, use_openmp => 1 )
if $HAS_C && $HAS_OPENMP;
for my $name ( sort keys %m ) {
srand(1);
$m{$name}->learn($stream);
}
return \%m;
} ## end sub build_models
print "=" x 70, "\n";
print " online (streaming) scoring accel benchmarks\n";
print " Algorithm::Classifier::IsolationForest::Online\n";
print "=" x 70, "\n";
printf "Backend availability: HAS_C=%d HAS_OPENMP=%d online_learn_xs=%d\n",
$HAS_C, $HAS_OPENMP,
Algorithm::Classifier::IsolationForest::Online::_HAS_ONLINE_XS;
print "(rates shown as calls/second wall-clock; higher is faster)\n";
print "(online_learn_xs=0 means the loaded C object predates the online\n"
. " learn accelerators -- rebuild or rerun with IF_RUNTIME_BUILD=1)\n"
unless Algorithm::Classifier::IsolationForest::Online::_HAS_ONLINE_XS;
examples/README.md view on Meta::CPAN
Each script seeds the RNG so its output is reproducible.
| Script | Shows |
|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `basic-anomaly-detection.pl` | The core workflow: `fit` â `score_samples` â `predict` on a Gaussian blob with known ring outliers, reported as a precision/recall summary and a ranked top-10. |
| `axis-vs-extended.pl` | `mode => 'axis'` vs `mode => 'extended'` on correlated (diagonal) data, illustrating how Extended Isolation Forest reduces the axis-aligned bias and better flags off-diagonal anomalies. |
| `contamination-threshold.pl` | Letting `contamination` auto-learn a cutoff at `fit` time, reading it back with `decision_threshold`, and how `predict` uses it by default vs a naive fixed 0.5. |
| `save-and-load.pl` | Persisting a trained model with `save`/`to_json` and restoring it with `load`/`from_json`, confirming a reloaded model scores bit-for-bit identically. |
| `server-metrics.pl` | An applied take: ranking server requests `[latency_ms, response_bytes]` by anomaly score, using `path_lengths` alongside `score_samples`, and writing the scored data to `request_scores.csv`. |
| `online-streaming.pl` | Online Isolation Forest (`::Online`) on a drifting stream: prequential `score_learn`, and how the sliding window makes the old regime anomalous and the new one normal after a drift. |
## Quick reference
```perl
use Algorithm::Classifier::IsolationForest;
my $if = Algorithm::Classifier::IsolationForest->new(
n_trees => 100, # ensemble size
sample_size => 256, # sub-sample per tree (psi)
examples/online-streaming.pl view on Meta::CPAN
#!/usr/bin/env perl
# online-streaming.pl
#
# Online (streaming) Isolation Forest on a drifting stream. The stream starts
# as a Gaussian blob at the origin, then drifts to a blob at (6, 6). An
# offline model would keep flagging the new regime forever; the online model
# forgets points as they age out of its sliding window, so within one window
# of the drift it treats the new regime as normal and the OLD regime as the
# anomaly.
#
# Points are processed prequentially (score-then-learn), the standard way to
# evaluate a streaming detector: every score reflects the model as it stood
# before that point influenced it.
#
# Run from the distribution root:
# perl -Ilib examples/online-streaming.pl
# or, if the module is installed:
# perl examples/online-streaming.pl
use strict;
use warnings;
use Algorithm::Classifier::IsolationForest::Online;
use constant PI => 3.14159265358979;
srand(7); # reproducible data; the forest gets its own seed below
sub gaussian {
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
L<https://ieeexplore.ieee.org/abstract/document/4781136>
Sahand Hariri, Matias Carrasco Kind, Robert J. Brunner (2020). Extended Isolation Forest. 1479 - 1489. 10.1109/TKDE.2019.2947676
L<https://ieeexplore.ieee.org/document/8888179>
Yousra Chabchoub, Maurras Ulbricht Togbe, Aliou Boly, Raja Chiky (2022). An In-Depth Study and Improvement of Isolation Forest. IEEE Access, vol. 10, 10219 - 10237. 10.1109/ACCESS.2022.3144425 (the Majority Voting Isolation Forest implemented by C<< ...
L<https://ieeexplore.ieee.org/document/9684896>
Filippo Leveni, Guilherme Weigert Cassales, Bernhard Pfahringer, Albert Bifet, Giacomo Boracchi (2024). Online Isolation Forest. (the streaming variant implemented by L<Algorithm::Classifier::IsolationForest::Online>)
L<https://arxiv.org/abs/2505.09593>
L<https://github.com/ineveLoppiliF/Online-Isolation-Forest>
L<https://proceedings.mlr.press/v235/leveni24a.html>
=cut
###
lib/Algorithm/Classifier/IsolationForest/App/Command/stream.pm view on Meta::CPAN
'learn-only',
'Only learn the input (warm-up); no scores are emitted. May not be combined with --score-only.'
],
[
'score-only',
'Only score the input against the model as-is; nothing is learned. May not be combined with --learn-only.'
],
[ 'threshold=f', 'Alternative decision threshold to use for the label column. 0 < $val < 1' ],
[
'save!',
'Save the updated model state back to -m after streaming (default on; --no-save to discard).',
{ 'default' => 1 }
],
# creation knobs, used only when -m does not exist yet
[ 'n=i', 'Number of isolation trees in the ensemble (new models only).' ],
[ 'window=i', 'Sliding window size; 0 disables forgetting (new models only).' ],
[ 'eta=i', 'max_leaf_samples: points a leaf accumulates before splitting (new models only).' ],
[ 'growth=s', "Leaf split-requirement growth, 'adaptive' or 'fixed' (new models only)." ],
[ 'subsample=f', 'Per-tree stream subsampling probability, in (0, 1] (new models only).' ],
[ 's=i', 'Seed int (new models only).' ],
lib/Algorithm/Classifier/IsolationForest/App/Command/stream.pm view on Meta::CPAN
'c=f',
'Contamination. Expected fraction of anomalies, in (0, 0.5]; learns the decision threshold from the window (new models only).'
],
[
't=s@',
'Feature name tag. Pass once per feature (e.g. -t cpu -t mem -t disk); the count must match the number of CSV columns or the command will die (new models only).'
],
[
'mungers=s',
'JSON file of Algorithm::ToNumberMunger specs, keyed by feature tag (new models only; requires -t). '
. 'Munged CSV columns may hold raw values; rows are munged before streaming and the spec is '
. 'saved with the model, so resumed runs munge identically. Scalar mungers only for CSV input.',
{ 'completion' => 'files' }
],
[
'prototype=s',
'JSON prototype file to create the model from (new models only): the variable schema and '
. 'schema_version/schema_description come from it, and its params supply knob defaults that the '
. 'creation switches override. May not be combined with -t or --mungers. See PROTOTYPES in the '
. 'module POD.',
{ 'completion' => 'files' }
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
# trusts a flag-matched prebuilt object without inspecting its symbol
# set). Probe once at load: without them, use_c still accelerates the
# packed-snapshot batch scoring -- those functions have been in the
# object all along -- and learning quietly stays pure Perl instead of
# crashing on an undefined XS sub. Rebuilding/reinstalling (or
# IF_RUNTIME_BUILD=1) restores the full set.
use constant _HAS_ONLINE_XS => defined &Algorithm::Classifier::IsolationForest::online_learn_row_xs ? 1 : 0;
=head1 NAME
Algorithm::Classifier::IsolationForest::Online - Online (streaming) Isolation Forest anomaly detection
=head1 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,
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
# 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');
=head1 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 C<fit()>: the model
C<learn>s points as they arrive and, once more than C<window_size> points
have been seen, forgets the oldest point for every new one so the model
always reflects the most recent C<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
C<max_leaf_samples> and C<growth>); because the actual points are gone,
the split simulates them by sampling uniformly inside the leaf's bounding
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
push @rows, $self->tagged_row_to_array( $row->[$i], "learn_tagged (row $i)" );
}
return $self->learn( \@rows );
}
my $vec = $self->tagged_row_to_array( $row, 'learn_tagged' );
return $self->learn( [$vec] );
} ## end sub learn_tagged
=head2 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 I<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);
=cut
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
sub score_sample_tagged {
my ( $self, $row ) = @_;
my $vec = $self->tagged_row_to_array( $row, 'score_sample_tagged' );
my $result = $self->score_samples( [$vec] );
return $result->[0];
}
=head2 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);
=cut
sub path_lengths {
my ( $self, $data ) = @_;
$self->_check_learned;
croak "path_lengths() expects an arrayref of samples"
lib/Algorithm/Classifier/IsolationForest/Online.pm view on Meta::CPAN
}
}
return ( $lo, $hi );
} ## end sub _box_of
#-------------------------------------------------------------------------------
# Scoring.
#-------------------------------------------------------------------------------
# Depth of the leaf $x lands in, plus the leaf's own depth budget -- the
# streaming analogue of the batch scorer's c(leaf size) adjustment.
# Scoring tolerates undef cells (mapped to 0), matching the parent class.
sub _depth_of {
my ( $self, $x, $node ) = @_;
my $depth = 0;
while ( $node->[_N_TYPE] ) {
$node = ( $x->[ $node->[_N_ATTR] ] // 0 ) < $node->[_N_SPLIT] ? $node->[_N_LEFT] : $node->[_N_RIGHT];
$depth++;
}
return $depth + $self->_rpl( $node->[_N_COUNT] );
}
t/42-prototype.t view on Meta::CPAN
}; ## end 'schema metadata knobs on new()' => sub
subtest 'validate_prototype croak matrix' => sub {
my @cases = (
[ 'not json', 'not { json', qr/did not parse as JSON/ ],
[ 'non-object', '[1,2]', qr/expected a JSON object/ ],
[ 'wrong format tag', { %{ proto_online() }, format => 'Nope' }, qr/format/ ],
[ 'future version', { %{ proto_online() }, version => 2 }, qr/newer than this module/ ],
[ 'unknown top-level key', { %{ proto_online() }, bogus => 1 }, qr/unknown top-level key 'bogus'/ ],
[ 'missing class', { %{ proto_online() }, class => undef }, qr/class of 'batch' or 'online'/ ],
[ 'bad class', { %{ proto_online() }, class => 'streaming' }, qr/class of 'batch' or 'online'/ ],
[
'missing schema_version', { %{ proto_online() }, schema_version => undef },
qr/non-empty schema_version/
],
[ 'empty schema_version', { %{ proto_online() }, schema_version => '' }, qr/non-empty schema_version/ ],
[
'missing schema_description',
{ %{ proto_online() }, schema_description => undef },
qr/non-empty schema_description/
],
t/80-sklearn-comparison-online.t view on Meta::CPAN
}
}
unless ( defined $python_bin ) {
plan skip_all => 'Python with scikit-learn is not installed; skipping cross-language comparison';
}
# -----------------------------------------------------------------------
# Python helper: one batch sklearn IsolationForest per dataset, JSON out.
# Identical to the batch test's helper (sklearn is the fixed reference the
# streaming model converges toward; it is fit once on the full dataset).
#
# sklearn score_samples convention: lower score = more anomalous -- the
# opposite direction from this module, so scores are negated before rank
# correlation.
# -----------------------------------------------------------------------
my $py_script = <<'END_PY';
import sys, json
import numpy as np
from sklearn.ensemble import IsolationForest