Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/80-sklearn-comparison.t view on Meta::CPAN
};
} ## end sub make_2d_grid_dataset
# N-D Gaussian inliers + corner-style outliers far from origin in every axis.
# Deterministic via a fixed srand seed derived from the dimension.
sub make_nd_gaussian_dataset {
my ($nf) = @_;
srand( 20260629 + $nf );
my @inliers;
push @inliers, [ map { gaussian( 0, 0.3 ) } 1 .. $nf ] for 1 .. 200;
# Outliers: each coordinate at magnitude 5..8 with random sign so the
# point sits at a "corner" of the bounding box, well outside the inlier
# cluster along *every* axis. Eight of them, to match the 2D dataset.
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;
}
return {
label => "${nf}d_gaussian",
n_feat => $nf,
inliers => \@inliers,
outliers => \@outliers,
data => [ @inliers, @outliers ],
n_in => scalar @inliers,
n_out => scalar @outliers,
};
} ## end sub make_nd_gaussian_dataset
my @datasets = ( make_2d_grid_dataset(), make_nd_gaussian_dataset(5), make_nd_gaussian_dataset(10), );
# -----------------------------------------------------------------------
# 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: train sklearn IsolationForest per dataset and emit JSON.
#
# Receives "csv_path|label" pairs on argv and returns a JSON object keyed
# by label with the corresponding score_samples() output. Batching all
# datasets into a single subprocess avoids paying Python + sklearn import
# cost more than once.
#
# sklearn score_samples convention: lower score = more anomalous. That
# is the opposite direction from this module (higher = more anomalous),
# so we negate sklearn scores before computing 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;
# Write one CSV per dataset, then build the argv spec.
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 {
( run in 0.858 second using v1.01-cache-2.11-cpan-995e09ba956 )