Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/38-online-basic.t view on Meta::CPAN
subtest 'scoring before any data croaks' => sub {
my $m = $class->new;
ok( !eval { $m->score_samples( [ [ 1, 2 ] ] ); 1 }, 'score_samples croaks unlearned' );
like( $@, qr/learn/, 'error mentions learn()' );
ok( !eval { $m->predict( [ [ 1, 2 ] ] ); 1 }, 'predict croaks unlearned' );
ok( !eval { $m->path_lengths( [ [ 1, 2 ] ] ); 1 }, 'path_lengths croaks unlearned' );
};
subtest 'learn/score sanity' => sub {
srand(7);
my $m = $class->new(
seed => 42,
n_trees => 50,
window_size => 256,
max_leaf_samples => 16,
);
my $ret = $m->learn( cluster( 400, 0 ) );
is( $ret, $m, 'learn returns $self' );
my $scores = $m->score_samples( [ [ 0, 0 ], [ 8, 8 ], [ -7, 7 ] ] );
is( scalar @$scores, 3, 'one score per sample' );
for my $s (@$scores) {
ok( $s > 0 && $s <= 1, "score $s in (0, 1]" );
}
cmp_ok( $scores->[1], '>', $scores->[0] + 0.03, 'far outlier scores above the cluster centre' );
cmp_ok( $scores->[2], '>', $scores->[0] + 0.03, 'other outlier scores above the cluster centre' );
my $depths = $m->path_lengths( [ [ 0, 0 ], [ 8, 8 ] ] );
cmp_ok( $depths->[0], '>', $depths->[1], 'inlier isolates deeper than the outlier' );
my ( $s2, $l2 ) = $m->score_predict_split( [ [ 0, 0 ], [ 8, 8 ] ], $scores->[0] + 0.01 );
is_deeply( $l2, [ 0, 1 ], 'score_predict_split labels against an explicit threshold' );
my $pairs = $m->score_predict_samples( [ [ 0, 0 ], [ 8, 8 ] ], $scores->[0] + 0.01 );
is_deeply( [ map { $_->[1] } @$pairs ], [ 0, 1 ], 'score_predict_samples labels agree' );
}; ## end 'learn/score sanity' => sub
subtest 'window bookkeeping' => sub {
srand(8);
my $m = $class->new( seed => 1, n_trees => 10, window_size => 64, max_leaf_samples => 16 );
$m->learn( cluster( 200, 0 ) );
is( $m->window_count, 64, 'window is capped at window_size' );
is( $m->seen, 200, 'seen counts the whole stream' );
for my $tree ( @{ $m->{trees} } ) {
is( $tree->{count}, 64, 'tree count tracks the window (subsample 1)' );
is( $tree->{root}[1], 64, 'root node count tracks the window' );
}
}; ## end 'window bookkeeping' => sub
subtest 'determinism under a fixed seed' => sub {
my @runs;
for ( 1 .. 2 ) {
srand(9);
my $m = $class->new( seed => 1234, n_trees => 20, window_size => 128, max_leaf_samples => 16 );
$m->learn( cluster( 300, 0 ) );
push @runs, [ $m->to_json, join( ',', @{ $m->score_samples( [ [ 0, 0 ], [ 5, 5 ] ] ) } ) ];
}
is( $runs[0][0], $runs[1][0], 'same seed + same stream => identical model JSON' );
is( $runs[0][1], $runs[1][1], 'same seed + same stream => identical scores' );
}; ## end 'determinism under a fixed seed' => sub
subtest 'feature count is locked by the first sample' => sub {
my $m = $class->new( n_trees => 5 );
$m->learn( [ [ 1, 2, 3 ] ] );
is( $m->{n_features}, 3, 'n_features learned from the first row' );
ok( !eval { $m->learn( [ [ 1, 2 ] ] ); 1 }, 'mismatched feature count croaks' );
like( $@, qr/expects 3/, 'error names the expected count' );
};
subtest 'missing-value handling' => sub {
my $die = $class->new( n_trees => 5 );
ok( !eval { $die->learn( [ [ 1, undef ] ] ); 1 }, "missing => 'die' croaks on undef" );
like( $@, qr/undef feature value/, 'error mentions the undef cell' );
my $zero = $class->new( n_trees => 5, missing => 'zero', window_size => 64, max_leaf_samples => 16, seed => 3 );
ok(
eval {
$zero->learn( [ map { [ $_ % 5, ( $_ % 2 ? undef : 1 ) ] } 1 .. 50 ] );
1;
},
"missing => 'zero' learns undef cells"
) or diag $@;
# Scoring tolerates undef regardless of strategy (mapped to 0).
srand(10);
my $m = $class->new( seed => 4, n_trees => 10, window_size => 64, max_leaf_samples => 16 );
$m->learn( cluster( 100, 0 ) );
ok( eval { $m->score_samples( [ [ undef, 1 ] ] ); 1 }, 'scoring a row with undef works' );
}; ## end 'missing-value handling' => sub
subtest 'score_learn is prequential' => sub {
srand(11);
my $m = $class->new( seed => 5, n_trees => 10, window_size => 64, max_leaf_samples => 16 );
my $first = $m->score_learn( [ [ 1, 1 ] ] );
is( scalar @$first, 1, 'one score per sample' );
is( $first->[0], 1.0, 'the very first point of a stream scores 1.0' );
is( $m->seen, 1, 'the point was learned' );
my $rows = cluster( 200, 0 );
my $scores = $m->score_learn($rows);
is( scalar @$scores, 200, 'one score per streamed sample' );
is( $m->seen, 201, 'stream advanced' );
# Against the now-warm model, an outlier still scores above an inlier
# through score_learn.
my $pair = $m->score_learn( [ [ 0, 0 ], [ 9, 9 ] ] );
cmp_ok( $pair->[1], '>', $pair->[0], 'outlier ranks above inlier prequentially' );
}; ## end 'score_learn is prequential' => sub
subtest 'growth => fixed' => sub {
srand(12);
my $m = $class->new( seed => 6, n_trees => 10, window_size => 128, max_leaf_samples => 16, growth => 'fixed' );
$m->learn( cluster( 200, 0 ) );
my $scores = $m->score_samples( [ [ 0, 0 ], [ 8, 8 ] ] );
cmp_ok( $scores->[1], '>', $scores->[0], 'fixed growth still separates outliers' );
};
subtest 'subsample' => sub {
srand(13);
my $m = $class->new( seed => 7, n_trees => 10, window_size => 128, max_leaf_samples => 16, subsample => 0.5 );
$m->learn( cluster( 300, 0 ) );
is( $m->window_count, 128, 'window is forest-level, unaffected by subsample' );
( run in 0.893 second using v1.01-cache-2.11-cpan-800906f7e73 )