Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN


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 0.528 second using v1.01-cache-2.11-cpan-a9496e3eb41 )