Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/37-majority-voting.t view on Meta::CPAN
# Invalid values are rejected.
eval { $free->set_voting('plurality') };
like( $@, qr/must be 'mean' or 'majority'/, 'invalid voting value croaks' );
# Switching before fit just records the mode for the eventual fit.
my $pre = $CLASS->new( n_trees => 20, sample_size => 16, seed => 5 );
$pre->set_voting('majority');
is( $pre->{voting}, 'majority', 'set_voting before fit records the mode' );
$pre->fit( \@data );
is( $pre->{voting}, 'majority', 'mode survives the subsequent fit' );
}; ## end 'set_voting switches an existing model' => sub
subtest 'tagged single-row helpers work under majority voting' => sub {
my $f = $CLASS->new(
n_trees => 30,
sample_size => 32,
seed => 13,
voting => 'majority',
feature_names => [qw(x y z)],
)->fit( \@data );
my $out = { x => 12, y => 12, z => 12 };
my $in = { x => 0.5, y => 0.5, z => 0.5 };
my $score = $f->score_sample_tagged($out);
cmp_ok( $score, '>', 0.5, 'tagged outlier row has a majority vote fraction' );
is( $f->predict_tagged($out), 1, 'tagged outlier row predicted anomalous' );
is( $f->predict_tagged($in), 0, 'tagged inlier row predicted normal' );
my $pair = $f->score_predict_sample_tagged($out);
is( abs( $pair->[0] - $score ) < 1e-12 ? 1 : 0, 1, 'tagged pair score matches score_sample_tagged' );
is( $pair->[1], 1, 'tagged pair label matches predict_tagged' );
}; ## end 'tagged single-row helpers work under majority voting' => sub
# ------------------------------------------------------------------------
# CLI: fit --voting must validate the value and store it on the model.
# ------------------------------------------------------------------------
SKIP: {
my $bin = File::Spec->rel2abs('bin/iforest');
skip "bin/iforest not found", 1 unless -x $bin;
subtest 'CLI fit --voting' => sub {
require File::Temp;
my ( $fh, $csv ) = File::Temp::tempfile( SUFFIX => '.csv', UNLINK => 1 );
for my $row (@data) {
print $fh join( ',', @$row ) . "\n";
}
close $fh;
my $out = `$^X -Ilib $bin fit -i $csv -p --voting majority -s 5 2>&1`;
is( $?, 0, 'fit --voting majority exits 0' );
like( $out, qr/"voting"\s*:\s*"majority"/, 'printed model records voting => majority' );
$out = `$^X -Ilib $bin fit -i $csv -p --voting bogus -s 5 2>&1`;
isnt( $?, 0, 'fit --voting bogus exits non-zero' );
like( $out, qr/must be either mean or majority/, 'bogus voting value is rejected' );
}; ## end 'CLI fit --voting' => sub
} ## end SKIP:
# ------------------------------------------------------------------------
# CLI: set_voting flips a saved model, recalibrating when contamination
# was set and refusing to guess a threshold without the training data.
# ------------------------------------------------------------------------
SKIP: {
my $bin = File::Spec->rel2abs('bin/iforest');
skip "bin/iforest not found", 1 unless -x $bin;
subtest 'CLI set_voting' => sub {
require File::Temp;
my ( $cfh, $csv ) = File::Temp::tempfile( SUFFIX => '.csv', UNLINK => 1 );
print $cfh join( ',', @$_ ) . "\n" for @data;
close $cfh;
# A model with no contamination flips without needing the data, and
# carries no threshold to recalibrate.
my ( undef, $free ) = File::Temp::tempfile( SUFFIX => '.json', UNLINK => 1 );
system("$^X -Ilib $bin fit -i $csv -o $free -w --voting mean -s 5 >/dev/null 2>&1");
my $out = `$^X -Ilib $bin set_voting -m $free --voting majority 2>&1`;
is( $?, 0, 'set_voting on a non-contamination model exits 0' );
my $info = `$^X -Ilib $bin info -m $free 2>&1`;
like( $info, qr/voting\s+majority/, 'model was rewritten as majority in place' );
# A contamination model refuses to switch without -i...
my ( undef, $cm ) = File::Temp::tempfile( SUFFIX => '.json', UNLINK => 1 );
system("$^X -Ilib $bin fit -i $csv -o $cm -w --voting mean -c 0.05 -s 42 -n 60 >/dev/null 2>&1");
$out = `$^X -Ilib $bin set_voting -m $cm --voting majority 2>&1`;
isnt( $?, 0, 'switching a contamination model without -i exits non-zero' );
like( $out, qr/-i CSV training data is required/, 'the error names the missing -i data' );
# ...but succeeds with -i, and the recalibrated threshold matches a
# model fit directly as majority with the same knobs.
$out = `$^X -Ilib $bin set_voting -m $cm --voting majority -i $csv -p 2>&1`;
is( $?, 0, 'set_voting --voting majority -i exits 0' );
like( $out, qr/"voting"\s*:\s*"majority"/, 'printed model records voting => majority' );
# fit.pm leaves sample_size at the module default, so match only the
# knobs the CLI invocation set.
my $ref
= $CLASS->new( n_trees => 60, seed => 42, contamination => 0.05, voting => 'majority' )->fit( \@data );
my $switched = $CLASS->from_json($out);
cmp_ok( abs( $switched->decision_threshold - $ref->decision_threshold ),
'<', 1e-9, 'CLI-recalibrated threshold matches a direct majority fit' );
# Bogus value is rejected.
$out = `$^X -Ilib $bin set_voting -m $free --voting bogus 2>&1`;
isnt( $?, 0, 'set_voting --voting bogus exits non-zero' );
like( $out, qr/must be either mean or majority/, 'bogus voting value is rejected' );
}; ## end 'CLI set_voting' => sub
} ## end SKIP:
done_testing;
( run in 0.718 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )