Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
# afterwards.
#
# Example:
# _heap_replace_root( \@heap, 0.9 ) if $score > $heap[0];
sub _heap_replace_root {
my ( $h, $x ) = @_;
$h->[0] = $x;
my $n = scalar @$h;
my $i = 0;
while (1) {
my $l = 2 * $i + 1;
my $r = 2 * $i + 2;
my $s = $i;
$s = $l if $l < $n && $h->[$l] < $h->[$s];
$s = $r if $r < $n && $h->[$r] < $h->[$s];
last if $s == $i;
@{$h}[ $s, $i ] = @{$h}[ $i, $s ];
$i = $s;
} ## end while (1)
return;
} ## end sub _heap_replace_root
#-------------------------------------------------------------------------------
# Draw $k samples without replacement via a partial Fisher-Yates shuffle of the
# index array.
#
# Args:
# $data :: the training set, an arrayref of feature-value arrayrefs.
# Never modified.
# $k :: how many samples to draw, an integer in [1, scalar @$data].
#
# Returns: an arrayref of $k row references. The rows are SHARED with
# $data, not copied, so nothing downstream may mutate them -- _build_tree
# only ever reads.
#
# Example:
# srand(42);
# my $sample = _subsample( \@training_rows, 256 );
# my $tree = $self->_build_tree( $sample, 0, 8 );
#-------------------------------------------------------------------------------
sub _subsample {
my ( $data, $k ) = @_;
my $n = scalar @$data;
my @idx = ( 0 .. $n - 1 );
for my $i ( 0 .. $k - 1 ) {
my $j = $i + int( rand( $n - $i ) );
@idx[ $i, $j ] = @idx[ $j, $i ];
}
my @chosen = @idx[ 0 .. $k - 1 ];
return [ @{$data}[@chosen] ];
} ## end sub _subsample
#-------------------------------------------------------------------------------
# Recursively build one isolation tree.
#
# A node is one of:
# leaf { leaf => 1, size => N }
# axis { attr => A, split => S, left => ..., right => ... }
# oblique { idx => [..], coef => [..], b => B, left => ..., right => ... }
#
# In both split styles the choice is restricted to features that actually vary
# across the points reaching the node: this avoids wasted levels on constant
# columns and lets a node leaf out exactly when its points are indistinguishable.
#
# Args:
# $X :: the points reaching this node, an arrayref of feature-value
# arrayrefs. Read-only; undef cells only appear under
# missing => 'nan'.
# $depth :: this node's depth, 0 at the root.
# $limit :: the height limit from _resolve_geometry. At or past it the
# node leafs out and c(size) covers the rest.
#
# Returns: the node as an arrayref in the layout above, with children
# already recursed into -- so calling it on the root returns the whole tree.
#
# Example:
# my $tree = $self->_build_tree( _subsample( \@rows, 256 ), 0, 8 );
# # [ 1, 2, 0.37, [ ... ], [ ... ] ] -- an axis split on feature 2
#-------------------------------------------------------------------------------
sub _build_tree {
my ( $self, $X, $depth, $limit ) = @_;
my $size = scalar @$X;
return [ _NODE_LEAF, $size ]
if $depth >= $limit || $size <= 1;
my $nf = $self->{n_features};
my $nan = $self->{missing} eq 'nan';
# Per-feature min and max within this node, in a single pass. Missing
# (undef) cells never reach here under die/zero/impute -- those fill the
# data before fit -- so the "next unless defined" guard is only needed
# in nan mode, where missing values must not constrain a feature's
# range; every other strategy skips it since every cell is defined and
# the check would never fire.
my ( @lo, @hi );
if ($nan) {
for my $row (@$X) {
for my $f ( 0 .. $nf - 1 ) {
my $v = $row->[$f];
next unless defined $v;
$lo[$f] = $v if !defined $lo[$f] || $v < $lo[$f];
$hi[$f] = $v if !defined $hi[$f] || $v > $hi[$f];
}
}
} else {
for my $row (@$X) {
for my $f ( 0 .. $nf - 1 ) {
my $v = $row->[$f];
$lo[$f] = $v if !defined $lo[$f] || $v < $lo[$f];
$hi[$f] = $v if !defined $hi[$f] || $v > $hi[$f];
}
}
}
# Features with spread are the only ones that can split the data. A
# feature whose values are all missing within this node has an undef
# range and is excluded.
my @varying = grep { defined $lo[$_] && $lo[$_] < $hi[$_] } 0 .. $nf - 1;
# No spread on any feature => all points identical => cannot isolate.
( run in 0.473 second using v1.01-cache-2.11-cpan-b16cb0d3907 )