Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

lib/Algorithm/Classifier/IsolationForest/App/Command/bench.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::bench;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp  qw(read_file);
use Scalar::Util qw(looks_like_number);
use Time::HiRes  qw(time);

sub opt_spec {
	return (
		[
			'm=s',
			'Input model JSON file path/name.',
			{ 'default' => 'iforest_model.json', 'completion' => 'files' }
		],
		[ 'i=s',      'Input CSV (rows of features to score).',          { 'completion' => 'files' } ],

lib/Algorithm/Classifier/IsolationForest/App/Command/bench.pm  view on Meta::CPAN

	for my $row ( read_file($path) ) {
		$line++;
		chomp $row;
		next if $row =~ /^\s*$/;
		my @f = split /,/, $row, -1;
		$expected //= scalar @f;
		die "line $line of '$path' has $row but expected $expected columns\n"
			if scalar @f != $expected;
		for my $v (@f) {
			die "line $line of '$path' value '$v' is not numeric\n"
				unless looks_like_number($v);
		}
		push @data, \@f;
	} ## end for my $row ( read_file($path) )
	return ( \@data, $expected );
} ## end sub _read_csv

sub execute {
	my ( $self, $opt, $args ) = @_;

	my $model = Algorithm::Classifier::IsolationForest->load( $opt->{'m'} );

lib/Algorithm/Classifier/IsolationForest/App/Command/fit.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::fit;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp  qw(read_file write_file);
use Scalar::Util qw(looks_like_number);

sub opt_spec {
	return (
		[ 'i=s',      'CSV to use.',                 { completion => 'files' } ],
		[ 'o=s',      'Output JSON file path/name.', { 'default'  => 'iforest_model.json', 'completion' => 'files' } ],
		[ 'p',        'Print the results instead of saving it.' ],
		[ 'w',        'Overwrite the file if it already exists.' ],
		[ 's=i',      'Seed int' ],
		[ 'extended', 'Use EIF instead of IF.' ],
		[ 'n=i',      'Number of isolation trees in the ensemble' ],

lib/Algorithm/Classifier/IsolationForest/App/Command/fit.pm  view on Meta::CPAN

		if ( !$has_mungers ) {
			my $col_int = 1;
			for my $field (@fields) {
				die(      'Line '
						. $line_int . ' of "'
						. $opt->{'i'}
						. '" value for column '
						. $col_int . ',"'
						. $field
						. '", does not appear to be a number' )
					unless looks_like_number($field);
				$col_int++;
			} ## end for my $field (@fields)
		} ## end if ( !$has_mungers )

		push @data, \@fields;

		$line_int++;
	} ## end foreach my $line ( read_file( $opt->{'i'} ) )

	# The tag count must match the CSV width whether the tags came from -t

lib/Algorithm/Classifier/IsolationForest/App/Command/fit.pm  view on Meta::CPAN

		my $munged = $iforest->munge_rows( \@data );
		for my $i ( 0 .. $#$munged ) {
			for my $col ( 0 .. $#{ $munged->[$i] } ) {
				die(      'Line '
						. ( $i + 1 ) . ' of "'
						. $opt->{'i'}
						. '" value for column '
						. ( $col + 1 ) . ',"'
						. ( defined $munged->[$i][$col] ? $munged->[$i][$col] : 'undef' )
						. '", is not a number after munging' )
					unless looks_like_number( $munged->[$i][$col] );
			} ## end for my $col ( 0 .. $#{ $munged->[$i] } )
		} ## end for my $i ( 0 .. $#$munged )
		@data = @$munged;
	} ## end if ($has_mungers)

	$iforest->fit( \@data );

	my $model = $iforest->to_json;

	if ( $opt->{'p'} ) {

lib/Algorithm/Classifier/IsolationForest/App/Command/pack.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::pack;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp  qw(read_file write_file);
use Scalar::Util qw(looks_like_number);

# .iforest-packed v1 file layout (all little-endian):
#
#   offset  size  field
#   -----------------------------------------------------------
#        0    8   magic  -- ASCII "IFPKD\0\0\0"
#        8    2   version (uint16, currently 1)
#       10    2   reserved (uint16, must be 0)
#       12    4   n_pts   (uint32)
#       16    4   n_feats (uint32)

lib/Algorithm/Classifier/IsolationForest/App/Command/pack.pm  view on Meta::CPAN

	my $line = 0;
	for my $row ( read_file( $opt->{'i'} ) ) {
		$line++;
		chomp $row;
		next if $row =~ /^\s*$/;
		my @f = split /,/, $row, -1;
		die "line $line of '$opt->{i}' has " . scalar(@f) . " columns but model has $nf features\n"
			unless scalar @f == $nf;
		for my $v (@f) {
			die "line $line of '$opt->{i}' value '$v' is not numeric\n"
				unless looks_like_number($v);
		}
		push @data, \@f;
	} ## end for my $row ( read_file( $opt->{'i'} ) )
	die "input '$opt->{i}' contains no rows\n" unless @data;

	my $packed = $model->pack_data( \@data );
	_write_packed( $opt->{'o'}, $packed->n_pts, $packed->n_feats, $packed->{packed} );

	printf "wrote %s (%d rows, %d features, %d bytes payload)\n",
		$opt->{'o'}, $packed->n_pts, $packed->n_feats,

lib/Algorithm/Classifier/IsolationForest/App/Command/predict.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::predict;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::App -command;
use Algorithm::Classifier::IsolationForest::App::Command::pack ();
use File::Slurp                                                qw(read_file write_file);
use Scalar::Util                                               qw(looks_like_number);

sub opt_spec {
	return (
		[
			'm=s',
			'Input model JSON file path/name.',
			{ 'default' => 'iforest_model.json', 'completion' => 'files' }
		],
		[ 'i=s', 'Input CSV for processing.',                           { 'completion' => 'files' } ],
		[ 'o=s', 'Output to this file instead of printing.',            { 'completion' => 'files' } ],

lib/Algorithm/Classifier/IsolationForest/App/Command/predict.pm  view on Meta::CPAN

			if ( !$has_mungers ) {
				my $col_int = 1;
				for my $field (@fields) {
					die(      'Line '
							. $line_int . ' of "'
							. $opt->{'i'}
							. '" value for column '
							. $col_int . ',"'
							. $field
							. '", does not appear to be a number' )
						unless looks_like_number($field);
					$col_int++;
				} ## end for my $field (@fields)
			} ## end if ( !$has_mungers )

			push @data, \@fields;

			$line_int++;
		} ## end foreach my $line ( read_file( $opt->{'i'} ) )
		if ($has_mungers) {

lib/Algorithm/Classifier/IsolationForest/App/Command/predict.pm  view on Meta::CPAN

			my $munged = $iforest->munge_rows( \@data );
			for my $i ( 0 .. $#$munged ) {
				for my $col ( 0 .. $#{ $munged->[$i] } ) {
					die(      'Line '
							. ( $i + 1 ) . ' of "'
							. $opt->{'i'}
							. '" value for column '
							. ( $col + 1 ) . ',"'
							. ( defined $munged->[$i][$col] ? $munged->[$i][$col] : 'undef' )
							. '", is not a number after munging' )
						unless looks_like_number( $munged->[$i][$col] );
				} ## end for my $col ( 0 .. $#{ $munged->[$i] } )
			} ## end for my $i ( 0 .. $#$munged )
			$score_input = $munged;
		} else {
			$score_input = \@data;
		}
	} ## end else [ if ( Algorithm::Classifier::IsolationForest::App::Command::pack::is_packed_file...)]

	my $results = $iforest->score_predict_samples( $score_input, $opt->{'t'} );

lib/Algorithm/Classifier/IsolationForest/App/Command/set_voting.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::set_voting;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp  qw(read_file write_file);
use Scalar::Util qw(looks_like_number);

sub opt_spec {
	return (
		[
			'm=s',
			'Input model JSON file path/name.',
			{ 'default' => 'iforest_model.json', 'completion' => 'files' }
		],
		[
			'voting=s',

lib/Algorithm/Classifier/IsolationForest/App/Command/set_voting.pm  view on Meta::CPAN


			my $col_int = 1;
			for my $field (@fields) {
				die(      'Line '
						. $line_int . ' of "'
						. $opt->{'i'}
						. '" value for column '
						. $col_int . ',"'
						. $field
						. '", does not appear to be a number' )
					unless looks_like_number($field);
				$col_int++;
			} ## end for my $field (@fields)

			push @data, \@fields;

			$line_int++;
		} ## end foreach my $line ( read_file( $opt->{'i'} ) )
	} ## end if ( defined( $opt->{'i'} ) )

	# set_voting ignores the data argument unless it actually recalibrates, so

lib/Algorithm/Classifier/IsolationForest/App/Command/stream.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::stream;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest         ();
use Algorithm::Classifier::IsolationForest::Online ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp  qw(read_file write_file);
use Scalar::Util qw(looks_like_number);

sub opt_spec {
	return (
		[
			'm=s',
			'Online model JSON file path/name.  Created if it does not exist; resumed and updated if it does.',
			{ 'default' => 'oiforest_model.json', 'completion' => 'files' }
		],
		[ 'i=s', 'Input CSV to stream through the model, in row order.', { 'completion' => 'files' } ],
		[ 'o=s', 'Output the scores to this file instead of printing.',  { 'completion' => 'files' } ],

lib/Algorithm/Classifier/IsolationForest/App/Command/stream.pm  view on Meta::CPAN

		if ( !$has_mungers ) {
			my $col_int = 1;
			for my $field (@fields) {
				die(      'Line '
						. $line_int . ' of "'
						. $opt->{'i'}
						. '" value for column '
						. $col_int . ',"'
						. $field
						. '", does not appear to be a number' )
					unless looks_like_number($field);
				$col_int++;
			} ## end for my $field (@fields)
		} ## end if ( !$has_mungers )

		push @data, \@fields;

		$line_int++;
	} ## end foreach my $line ( read_file( $opt->{'i'} ) )

	# A prototype-created model already carries its tags; hold them to the

lib/Algorithm/Classifier/IsolationForest/App/Command/stream.pm  view on Meta::CPAN

		my $munged = $oif->munge_rows( \@data );
		for my $i ( 0 .. $#$munged ) {
			for my $col ( 0 .. $#{ $munged->[$i] } ) {
				die(      'Line '
						. ( $i + 1 ) . ' of "'
						. $opt->{'i'}
						. '" value for column '
						. ( $col + 1 ) . ',"'
						. ( defined $munged->[$i][$col] ? $munged->[$i][$col] : 'undef' )
						. '", is not a number after munging' )
					unless looks_like_number( $munged->[$i][$col] );
			} ## end for my $col ( 0 .. $#{ $munged->[$i] } )
		} ## end for my $i ( 0 .. $#$munged )
		$stream_rows = $munged;
	} ## end if ($has_mungers)

	# --- stream ------------------------------------------------------------
	my $results_string = '';
	if ( $opt->{'learn_only'} ) {
		$oif->learn($stream_rows);
	} else {

lib/Algorithm/Classifier/IsolationForest/App/Command/streamc.pm  view on Meta::CPAN

package Algorithm::Classifier::IsolationForest::App::Command::streamc;

use strict;
use warnings;
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp      qw(write_file);
use File::Spec       ();
use Scalar::Util     qw(looks_like_number);
use IO::Socket::UNIX ();
use IO::Select       ();

# JSON::MaybeXS codec and the connected socket, set up in execute.
my $JSON;
my $SOCK;
my $TIMEOUT;
my $READ_BUF = '';

sub opt_spec {

lib/Algorithm/Classifier/IsolationForest/App/Command/streamc.pm  view on Meta::CPAN

						. $line_int
						. ' of input has '
						. scalar(@fields)
						. ' columns but expected '
						. $expected_cols );
			}

			# Numeric-looking fields travel as JSON numbers, everything
			# else as strings for the daemon's munger plan to handle; the
			# daemon owns validation either way.
			push @rows, [ map { looks_like_number($_) ? 0 + $_ : $_ } @fields ];
			push @raw,  $line;
		} ## end else [ if ( $opt->{'jsonl'} ) ]

		$flush->() if scalar @rows >= $opt->{'batch'};
	} ## end while ( my $line = <$in_fh> )
	$flush->();

	if ( defined $opt->{'o'} ) {
		write_file( $opt->{'o'}, { 'atomic' => 1 }, $results );
	}

lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm  view on Meta::CPAN


use strict;
use warnings;
use Algorithm::Classifier::IsolationForest         ();
use Algorithm::Classifier::IsolationForest::Online ();
use Algorithm::Classifier::IsolationForest::App -command;
use File::Slurp      qw(read_file write_file);
use File::Path       qw(make_path);
use File::Basename   qw(dirname);
use File::Spec       ();
use Scalar::Util     qw(looks_like_number);
use IO::Socket::UNIX ();
use IO::Select       ();
use POSIX            qw(setsid strftime);
use Errno            qw();

# The daemon is a singleton per process, so its runtime state lives in
# file-scoped lexicals rather than being threaded through every helper.
my $JSON;          # JSON::MaybeXS codec (required at runtime, see execute)
my $OIF;           # the online model
my %OPT;           # resolved options

lib/Algorithm/Classifier/IsolationForest/App/Command/streamd.pm  view on Meta::CPAN

		if ( ref $OIF->{mungers} eq 'HASH' && %{ $OIF->{mungers} } ) {
			$vec = $OIF->munge_rows( [$row] )->[0];
		}
	} else {
		die 'row must be a JSON array (positional) or object (tagged)' . "\n";
	}

	for my $col ( 0 .. $#$vec ) {
		next if !defined $vec->[$col];    # undef defers to the model's missing policy
		die 'column ' . ( $col + 1 ) . ' is not a number after munging' . "\n"
			unless looks_like_number( $vec->[$col] );
	}

	if ( $mode eq 'learn' ) {
		$OIF->learn( [$vec] );
		$DIRTY = 1;
		return undef;
	}
	if ( $mode eq 'score' ) {
		return $OIF->score_samples( [$vec] )->[0];
	}

t/33-parallel-fit.t  view on Meta::CPAN

#!perl
# 33-parallel-fit.t
#
# Verifies the parallel_fit option:
#   1. Produces a fitted model with the requested number of trees.
#   2. score_samples on a held-out point looks like a normal score
#      (between 0 and 1, and clearly separates an obvious outlier).
#   3. Re-running the parallel fit with the same seed and worker count
#      gives bit-identical scores (cross-run reproducibility, which is
#      the parallel_fit contract -- serial-vs-parallel differs but
#      parallel-vs-parallel does not).
#   4. parallel_fit on a no-fork platform falls back silently to serial.

use strict;
use warnings;
use Test::More;



( run in 0.617 second using v1.01-cache-2.11-cpan-995e09ba956 )