Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

lib/Algorithm/Classifier/IsolationForest.pm  view on Meta::CPAN

                        walking trees per sample as soon as the outcome is
                        decided. The threshold argument/default of the
                        predict methods is therefore the PER-TREE cutoff
                        here, not a forest-level score cutoff.
                        score_samples() returns the fraction of trees
                        voting anomalous -- still in [0, 1], but discrete
                        in steps of 1/n_trees. contamination composes: fit()
                        learns the per-tree cutoff that flags the requested
                        fraction of the training set.
        default :: mean

    - parallel_fit :: positive integer N => build the trees across N forked
          worker processes during fit(). Each worker gets a derived seed
          (parent seed + worker_id * 1009) so the parallel fit is
          reproducible across runs at fixed worker count -- but the trees
          produced are NOT bit-identical to a serial fit with the same
          seed, because the RNG draws happen in a different order.
          Inference is unaffected. Falls back silently to serial on
          platforms without a real fork() (e.g. Windows without Cygwin).
        default :: undef (serial)

    - use_c :: boolean, override whether the Inline::C backend is used for
          both scoring and fit()'s tree builder.  When false the instance
          falls back to pure Perl for both even if the C backend compiled
          successfully.  When true (or unset) the C backend is used if
          available ($HAS_C).  fit() with use_c on produces bit-identical
          trees to use_c off for the same seed -- only build speed differs.
        default :: $HAS_C

    - use_openmp :: boolean, override whether OpenMP parallel scoring is
          used inside score_all_xs().  When false the C tree walk runs
          single-threaded even if OpenMP was linked in.  Ignored when
          use_c is false (pure Perl has no OpenMP path).
        default :: $HAS_OPENMP

    - use_openmp_fit :: boolean, build fit()'s trees across OpenMP threads
          (one tree per thread) instead of the single-threaded C builder.
          Opt-in and off by default: unlike use_c/use_openmp, this changes
          which trees get built. Perl's RNG isn't safe to call from
          multiple OS threads sharing one interpreter, so this path seeds
          an independent PRNG per tree from the tree index rather than
          Drand01() -- trees differ from the use_c (single-threaded)
          and pure-Perl paths even with the same seed, though a fixed
          seed and n_trees still reproduce the same trees regardless of
          OMP_NUM_THREADS or scheduling. Does NOT compose with
          parallel_fit: a forked child starting its own OpenMP region
          after the parent process has used OpenMP for anything can
          hang (a general fork()+libgomp limitation), so parallel_fit's
          workers always use the single-threaded C builder regardless
          of this setting -- setting both just means parallel_fit wins.
          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
          exactly as it did training input.  See L</MUNGERS> 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
          or compared numerically; saved with the model and shown by
          `iforest info`.  Usually set from a prototype (see
          L</PROTOTYPES>) rather than passed directly.
        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 (a description for a feature
          that does not exist croaks -- it is either a typo or a stale
          leftover from a schema change).  Partial coverage is fine.
          Saved with the model and shown beside each tag by
          `iforest info`.
        default :: undef

Note: log2 under Perl is as below...

    log($psi) / log(2)

=cut

sub new {
	my ( $class, %args ) = @_;

	my $mode = $args{mode} // 'axis';
	croak "mode must be 'axis' or 'extended'"
		unless $mode eq 'axis' || $mode eq 'extended';

	# How fit() treats undef (missing) feature cells.  Scoring always
	# tolerates undef regardless of this setting -- it governs fit only.
	#   die    :: croak if the training data contains any undef cell (default)
	#   zero   :: treat a missing cell as the value 0
	#   impute :: replace a missing cell with the per-feature mean/median
	#             learned from the present values at fit time
	#   nan    :: build ranges over present values only and route a point
	#             missing the split feature consistently to one branch, at
	#             both fit and score time
	my $missing = $args{missing} // 'die';
	croak "missing must be one of: die, zero, impute, nan"
		unless $missing =~ /\A(?:die|zero|impute|nan)\z/;

lib/Algorithm/Classifier/IsolationForest.pm  view on Meta::CPAN

	my $scores = $self->score_samples( $self->_to_arrayref($data) );
	return [ map { $_ >= $threshold ? 1 : 0 } @$scores ];
} ## end sub predict

=head2 predict_tagged(\%row, $threshold)

Predicts whether a single sample is an anomaly using a hashref of named
feature values.  The model must have been fitted (or loaded from a model
that was fitted) with feature names stored via C<feature_names>.

C<$threshold> defaults the same way as in C<predict>.

Returns a scalar 1 (anomaly) or 0 (normal).

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

Croaks if the model has no stored feature names, if the hashref contains a
key that is not a known feature name, or if a feature name is absent from the
hashref.

=cut

=head2 tagged_row_to_array(\%row, $caller)

Validates a hashref of named feature values against the model's stored
C<feature_names> and returns a positional arrayref ready to pass to any
of the scoring or prediction methods.

C<$caller> is a string used in error messages to identify which method
triggered the validation (pass the calling method's name).

    my $vec = $forest->tagged_row_to_array(\%row, 'my_method');
    # returns e.g. [0.9, 0.4, 0.1] ordered by feature_names

Croaks if:

=over 4

=item * C<$row> is not a hashref

=item * the model has no stored C<feature_names>

=item * the hashref contains a key that is not a known feature name

=item * a feature name is absent from the hashref

=back

=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'
		&& @{ $self->{feature_names} };

	my @names = @{ $self->{feature_names} };

	my @unknown = grep {
		my $k = $_;
		!grep { $_ eq $k } @names
	} keys %$row;
	croak "unknown feature name(s) in hashref: " . join( ', ', sort @unknown )
		if @unknown;

	my @missing = grep { !exists $row->{$_} } @names;
	croak "missing feature name(s) in hashref: " . join( ', ', @missing )
		if @missing;

	return [ map { $row->{$_} } @names ];
} ## end sub tagged_row_to_array

sub predict_tagged {
	my ( $self, $row, $threshold ) = @_;
	my $vec    = $self->tagged_row_to_array( $row, 'predict_tagged' );
	my $result = $self->predict( [$vec], $threshold );
	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"
		unless ref $rows eq 'ARRAY';
	my $plan = _plan($self);
	return $rows unless $plan;
	my @out;
	for my $i ( 0 .. $#$rows ) {
		my $munged = eval { $plan->apply_positional( $rows->[$i] ) };
		croak "munge_rows (row $i): $@" if $@;
		push @out, $munged;
	}
	return \@out;
} ## end sub munge_rows

=head2 score_samples(\@data)

Returns an arrayref of anomaly scores, between 0 and 1.

Scores near 1 are strong anomalies (isolated quickly).

Scores well below 0.5 are normal.

Scores ~0.5 means the points are hard to tell apart.

Under C<< voting => 'majority' >> the returned value is instead the
fraction of trees voting the sample anomalous at the model's decision
threshold (the contamination-learned cutoff if present, otherwise 0.5) --
still in [0, 1], but discrete in steps of C<1/n_trees>, with a majority
label corresponding to a fraction strictly above 0.5.

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

    print "x, y, score\n";

    my $int=0;
    while (defined($data[$int])) {
        print $data[$int][0].', '.$data[$int][1].', '.$scores->[$int]."\n";

        $int++;
    }

=cut

sub score_samples {
	my ( $self, $data ) = @_;
	$self->_check_fitted;
	my $c     = $self->{c_psi};
	my $trees = $self->{trees};
	my $t     = scalar @$trees;

	# Majority voting: the "score" is the fraction of trees voting the
	# sample anomalous at the model's decision threshold (contamination-



( run in 0.844 second using v1.01-cache-2.11-cpan-a9496e3eb41 )