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 )