Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
0.2.0 2026-06-30/14:15
- C acceleration via Inline::C for core fit and predict ops
- OpenMP support for parallel multi-threaded fitting and predict ops
- SIMD (AVX/SSE) acceleration where available
- Data packing support for compact model storage (new `pack` CLI command)
- Parallel fit capability
- New `score_predict_split` method
- New `accel` CLI command for querying available acceleration flags
- New `bench` CLI command for running built-in benchmarks
- New `info` CLI command with expanded model introspection
- Benchmarking scripts covering fit, predict, scoring, and accel modes
- Tests: accel flag detection, accel selection, undef column handling,
data packing, parallel fit, sklearn comparison (including undef),
and CLI
- minor tweaks to `csv2plot` for a bit nicer rendering
- minor POD fixes
0.1.0 2026-06-23/03:15
- add csv2plot helper command for graphing
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
Ignored (clamped to 0) when use_c is false or OpenMP isn't
linked in.
default :: 0
- 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, keyed as that module's compile() expects (scalar mungers by
their output tag, expanding mungers by any label with an 'into'
list, combining mungers by their output tag with a 'from' list).
When set, every tagged row -- the *_tagged methods, fit_tagged,
and tagged_row_to_array -- is munged from raw values (strings,
timestamps, status codes, ...) into numbers through the compiled
plan, and munge_rows() applies the scalar mungers to positional
rows. Requires feature_names; the plan compiles against them, so
any spec error croaks here in new(). Algorithm::ToNumberMunger is
an optional dependency, required only when a spec is given (or a
loaded model carrying one is used with tagged data). The spec is
saved with the model, so a loaded model munges scoring input
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
=cut
sub tagged_row_to_array {
my ( $self, $row, $caller ) = @_;
croak "$caller requires a hashref"
unless ref $row eq 'HASH';
# With mungers configured the compiled plan owns the row assembly:
# it knows the real input fields (munger 'from' sources included,
# which need not be tags at all) and croaks on a missing one. Extra
# keys are ignored -- with expanders and combiners in play, "the
# exact key set" is the plan's knowledge, not feature_names'.
if ( my $plan = _plan($self) ) {
my $vec = eval { $plan->apply_named($row) };
croak "$caller: $@" if $@;
return $vec;
}
croak "this model has no stored feature_names; " . "refit with -t tags or pass feature_names to new()"
unless defined $self->{feature_names}
&& ref $self->{feature_names} eq 'ARRAY'
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
return $result->[0];
}
=head2 munge_rows(\@rows)
Applies the model's scalar mungers to positional rows (arrayrefs in
C<feature_names> order), returning a new arrayref of munged rows. A
model without C<mungers> returns the input unchanged, so callers such
as the CLI can pass every dataset through unconditionally.
Croaks if the munger set contains expanding or combining mungers --
their inputs are named source fields that positional rows cannot
express; use the tagged methods (or L</fit_tagged>) for those.
my $numeric = $iforest->munge_rows(\@raw_rows);
=cut
sub munge_rows {
my ( $self, $rows ) = @_;
croak "munge_rows() expects an arrayref of rows"
lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm view on Meta::CPAN
{"rows": [[...]], "mode": "learn"} -> {"ok": {"learned": 1}}
{"cmd": "mode", "mode": "score"} -> {"ok": {"mode": "score"}}
{"cmd": "ping"} -> {"ok": "pong"}
{"cmd": "stats"} -> {"ok": {"seen": ..., ...}}
{"cmd": "save"} -> {"ok": {"saved": "oiforest-....json"}}
{"cmd": "relearn-threshold"} -> {"ok": {"threshold": 0.61}}
anything invalid -> {"error": "...", "tag": ...}
The array row form is positional (scalar mungers applied, like stream
CSV input); the object form is a tagged row and runs the full munger
plan, including expanding and combining mungers -- and, being JSON, the
raw values may safely contain commas, newlines, or any unicode.
A worked tagged example. Create the daemon around raw HTTP request
data, with mungers turning the raw values into numbers (mungers.json
here; a --prototype carrying the same schema works identically):
{ "method": { "munger": "http_method_enum", "default": -1 },
"path_len": { "munger": "length", "from": "path" },
"host_entropy": { "munger": "entropy", "from": "host" } }
lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm view on Meta::CPAN
# The effective label cutoff, resolved per message so relearns and the
# contamination refresh at save time take effect immediately.
sub _threshold {
return
defined $OPT{'threshold'} ? $OPT{'threshold'}
: defined $OIF->decision_threshold ? $OIF->decision_threshold
: 0.5;
}
# One row through the model. A JSON object is a tagged row (full munger
# plan, expanding/combining mungers included); a JSON array is positional
# (scalar mungers, like stream CSV input). Either way the final vector
# is validated numeric before it touches the model -- JSON delivers
# typed values, so anything non-numeric left after munging is a caller
# bug worth an explicit error rather than Perl's silent string-to-0.
# Returns the score, or undef in learn mode. Croaks on any problem.
sub _apply_row {
my ( $row, $mode ) = @_;
my $vec;
if ( ref $row eq 'HASH' ) {
t/41-mungers.t view on Meta::CPAN
#!perl
# 41-mungers.t
#
# Optional Algorithm::ToNumberMunger integration: a model can carry a
# declarative munger spec that turns raw tagged values into numbers, with
# the spec saved in the model JSON so a loaded model munges scoring input
# exactly as it did training input. Covers constructor validation, the
# tagged-method plan path (batch and Online), fit_tagged / learn_tagged
# batches, positional munge_rows, expanding mungers, and persistence --
# including the lazy plan recompile after from_json and the croak on an
# unknown munger name.
#
# Skipped entirely when Algorithm::ToNumberMunger is not installed (it is
# an optional dependency).
use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
t/41-mungers.t view on Meta::CPAN
like( $@, qr/missing value for 'path'/, 'error names the missing source' );
ok(
eval {
$f->score_sample_tagged( { %NORMAL, extra_key => 'ignored' } );
1;
},
'extra keys are ignored under a plan'
) or diag $@;
}; ## end 'batch: fit_tagged and tagged scoring on raw values' => sub
subtest 'expanding munger (datetime parts/into)' => sub {
srand(8);
my $f = $batch_class->new(
seed => 1,
n_trees => 30,
sample_size => 64,
feature_names => [ 'bytes', 'tod_sin', 'tod_cos' ],
mungers => {
time_of_day => {
munger => 'datetime',
from => 'stamp',
t/41-mungers.t view on Meta::CPAN
bytes => 500 + ( $i % 37 ),
stamp => sprintf( '2026-07-0%dT03:%02d:00', 1 + ( $i % 7 ), $min ),
};
}
$f->fit_tagged( \@rows );
my $night = $f->score_sample_tagged( { bytes => 510, stamp => '2026-07-03T03:10:00' } );
my $noon = $f->score_sample_tagged( { bytes => 510, stamp => '2026-07-03T15:00:00' } );
cmp_ok( $noon, '>', $night, 'off-schedule time scores above the usual window' );
# Positional munging cannot express an expander.
ok( !eval { $f->munge_rows( [ [ 1, 2, 3 ] ] ); 1 }, 'munge_rows croaks with an expanding munger' );
}; ## end 'expanding munger (datetime parts/into)' => sub
subtest 'persistence round trip (batch)' => sub {
my $dir = tempdir( CLEANUP => 1 );
srand(9);
my $f = $batch_class->new(
seed => 42,
n_trees => 40,
sample_size => 64,
contamination => 0.05,
feature_names => [@TAGS],
( run in 2.161 seconds using v1.01-cache-2.11-cpan-a9496e3eb41 )