Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/80-sklearn-comparison-undef.t view on Meta::CPAN
};
} ## end sub make_2d_grid_dataset
# N-D Gaussian inliers + corner outliers far from origin in every axis.
# Test points still only carry signal in column 0; the other nf-1 columns
# are undef. Seeded deterministically per 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;
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;
}
# Empirical gaps with 1 signal column out of nf, 100 trees, seed 42:
# nf=5 -> ~0.13 nf=10 -> ~0.05
# The threshold is set well under the observed value so trivial RNG
# noise doesn't flap the test, but high enough to still detect a real
# regression that would collapse the gap further.
my $mean_gap_min = $nf <= 5 ? 0.08 : 0.025;
my ( $undef_test, $zero_test ) = make_undef_test_points($nf);
return {
label => "${nf}d_gaussian",
n_feat => $nf,
train => [ @inliers, @outliers ],
undef_test => $undef_test,
zero_test => $zero_test,
n_in_test => 19,
n_out_test => 6,
mean_gap_min => $mean_gap_min,
};
} ## 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 (cross-language subtests are skipped if
# absent; pure-Perl subtests still run)
# -----------------------------------------------------------------------
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;
}
}
# -----------------------------------------------------------------------
# Python helper: scores all datasets in one subprocess. Each argv spec
# is "csv_path|label|n_train"; the CSV concatenates training rows then
# test rows, and n_train is the split point. Test rows may use the
# token "nan" / "undef" / empty for missing values.
#
# sklearn score_samples convention: lower = more anomalous (opposite of
# Perl), 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
def parse_csv(path):
rows = []
with open(path) as f:
for line in f:
line = line.strip()
if not line:
continue
row = []
for tok in line.split(','):
tok = tok.strip()
if tok.lower() in ('nan', 'undef', ''):
row.append(float('nan'))
else:
row.append(float(tok))
rows.append(row)
return rows
results = {}
for spec in sys.argv[1:]:
csv_path, label, n_train = spec.split('|', 2)
n_train = int(n_train)
rows = parse_csv(csv_path)
X_train = np.array(rows[:n_train], dtype=float)
X_test = np.array(rows[n_train:], dtype=float)
# Mirror Perl's undef-to-0 numeric coercion.
X_test_clean = np.where(np.isnan(X_test), 0.0, X_test)
psi = min(256, len(X_train))
clf = IsolationForest(
n_estimators=100,
max_samples=psi,
contamination='auto',
random_state=42,
)
clf.fit(X_train)
results[label] = clf.score_samples(X_test_clean).tolist()
print(json.dumps(results))
END_PY
# Run Python once for all datasets, keyed by label.
my $sk_by_label;
if ( defined $python_bin ) {
my ( $py_fh, $py_path ) = tempfile( SUFFIX => '.py', UNLINK => 1 );
print $py_fh $py_script;
close $py_fh;
my @specs;
for my $ds (@datasets) {
( run in 1.009 second using v1.01-cache-2.11-cpan-995e09ba956 )