Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/80-sklearn-comparison-online.t view on Meta::CPAN
push @inliers, [ map { gaussian( 0, 0.3 ) } 1 .. $nf ] for 1 .. $n_in;
my @outliers;
for ( 1 .. 8 ) {
my @row;
for ( 1 .. $nf ) {
my $mag = 5 + rand() * 3;
my $sign = rand() > 0.5 ? 1 : -1;
push @row, $mag * $sign;
}
push @outliers, \@row;
}
my @data = ( @inliers, @outliers );
# Deterministic Fisher-Yates shuffle for the stream order.
my @stream = @data;
srand( 4242 + $nf );
for my $i ( reverse 1 .. $#stream ) {
my $j = int( rand( $i + 1 ) );
@stream[ $i, $j ] = @stream[ $j, $i ];
}
return {
%spec,
label => "${nf}d_gaussian",
inliers => \@inliers,
outliers => \@outliers,
data => \@data,
stream => \@stream,
n_out => scalar @outliers,
};
} ## end sub make_dataset
my @datasets = (
make_dataset( n_feat => 2, n_in => 200, eta => 8, rho_min => 0.80 ),
make_dataset( n_feat => 5, n_in => 1000, eta => 8, rho_min => 0.72 ),
make_dataset( n_feat => 10, n_in => 1000, eta => 32, rho_min => 0.70 ),
make_dataset( n_feat => 20, n_in => 1000, eta => 8, rho_min => 0.55 ),
);
# -----------------------------------------------------------------------
# Locate Python + scikit-learn; skip the whole file if unavailable
# -----------------------------------------------------------------------
my $python_bin;
for my $cmd (qw(python3 python)) {
my $probe = `$cmd -c "import sklearn; print('ok')" 2>/dev/null`;
if ( defined $probe && $probe =~ /\bok\b/ ) {
$python_bin = $cmd;
last;
}
}
unless ( defined $python_bin ) {
plan skip_all => 'Python with scikit-learn is not installed; skipping cross-language comparison';
}
# -----------------------------------------------------------------------
# Python helper: one batch sklearn IsolationForest per dataset, JSON out.
# Identical to the batch test's helper (sklearn is the fixed reference the
# streaming model converges toward; it is fit once on the full dataset).
#
# sklearn score_samples convention: lower score = more anomalous -- the
# opposite direction from this module, so scores are negated before rank
# correlation.
# -----------------------------------------------------------------------
my $py_script = <<'END_PY';
import sys, json
import numpy as np
from sklearn.ensemble import IsolationForest
results = {}
for spec in sys.argv[1:]:
csv_path, label = spec.split('|', 1)
rows = []
with open(csv_path) as f:
for line in f:
line = line.strip()
if line:
rows.append([float(x) for x in line.split(',')])
X = np.array(rows)
psi = min(256, len(X))
clf = IsolationForest(
n_estimators=100,
max_samples=psi,
contamination='auto',
random_state=42,
)
clf.fit(X)
results[label] = clf.score_samples(X).tolist()
print(json.dumps(results))
END_PY
my ( $py_fh, $py_path ) = tempfile( SUFFIX => '.py', UNLINK => 1 );
print $py_fh $py_script;
close $py_fh;
my @specs;
for my $ds (@datasets) {
my ( $csv_fh, $csv_path ) = tempfile( SUFFIX => '.csv', UNLINK => 1 );
for my $row ( @{ $ds->{data} } ) {
print $csv_fh join( ',', @$row ) . "\n";
}
close $csv_fh;
push @specs, qq("$csv_path|$ds->{label}");
}
my $raw = `$python_bin "$py_path" @{[ join ' ', @specs ]} 2>/dev/null`;
my $py = eval { JSON::PP->new->decode($raw) };
unless ( defined $py && ref $py eq 'HASH' ) {
plan skip_all => 'Python/sklearn script returned unusable output; skipping';
}
# -----------------------------------------------------------------------
# Per-dataset test battery
# -----------------------------------------------------------------------
sub run_dataset_tests {
my ( $ds, $sk_all ) = @_;
( run in 0.715 second using v1.01-cache-2.11-cpan-7fcb06a456a )