Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

Makefile.PL  view on Meta::CPAN

# what Inline::MakeMaker generates, with two differences: only the one
# module that actually embeds C gets a rule (Inline::MakeMaker emits one
# per .pm in lib/), and install mode is signalled to the module via
# IF_INSTALL_BUILD=1 in the rule's environment instead of a global
# -MInline=_INSTALL_ import, so the module can pass Inline's _INSTALL_
# config itself alongside NAME/VERSION.  Inline's install mode reads the
# version and blib/arch destination from @ARGV.  The trailing -e writes a
# stub .inl file satisfying the make dependency (also what
# Inline::MakeMaker does); the compiled object itself lands under
# blib/arch/auto/ and is picked up by `make install`.
sub MY::postamble {
	return '' unless $prebuilt;
	return <<'POSTAMBLE';
# --- Inline::C install-time build (generated by Makefile.PL):

Algorithm-Classifier-IsolationForest.inl : pm_to_blib
	IF_INSTALL_BUILD=1 $(PERL) "-Mblib" "-MAlgorithm::Classifier::IsolationForest" -e"require Inline; my %A = (modinlname => 'Algorithm-Classifier-IsolationForest.inl', module => 'Algorithm::Classifier::IsolationForest'); my %S = (API => \%A); Inline::s...

dynamic :: Algorithm-Classifier-IsolationForest.inl

POSTAMBLE
} ## end sub MY::postamble

my %WriteMakefileArgs = (
	NAME               => 'Algorithm::Classifier::IsolationForest',
	AUTHOR             => q{Zane C. Bowers-Hadley <vvelox@vvelox.net>},
	VERSION_FROM       => 'lib/Algorithm/Classifier/IsolationForest.pm',
	ABSTRACT_FROM      => 'lib/Algorithm/Classifier/IsolationForest.pm',
	LICENSE            => 'lgpl_2_1',
	MIN_PERL_VERSION   => '5.006',
	INST_SCRIPT        => 'bin',
	EXE_FILES          => ['src_bin/iforest'],

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

 * call in this function happens before the parallel region starts
 * (single-threaded), so the result still varies with the model's
 * `seed` the way every other code path does; it isn't used inside the
 * parallel loop.
 *
 * Each tree is built entirely with plain C data (row-index int arrays,
 * a growable TreeBuf of packed doubles/ints) -- no Perl API call
 * happens anywhere inside the parallel region. Each node record in
 * TreeBuf uses _pack_tree's 6-double SoA layout (see the file-top
 * comment), but the node ORDER differs: records are appended
 * post-order (a node is pushed after both its children, since child
 * indices must be known first), so the root is the last record --
 * _pack_tree's pre-order puts it at 0.  _unpack_forest accounts for
 * this.  Oblique coefficients are also always stored sparse (in the
 * random pool's order) -- the dense-pack fast path is skipped because
 * its only purpose is speeding up score_all_xs, and _rebuild_c_trees
 * reapplies it anyway once the caller unpacks these buffers back into
 * the standard Perl tree shape and re-derives the scoring buffers.
 *
 * After the parallel region, each tree's TreeBuf is copied into a Perl
 * string SV (one memcpy each, serially) and stored into nodes_rv /

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

        int p = _partition_lomuto(a, lo, hi);
        if (p == k) return a[p];
        if (p < k) lo = p + 1; else hi = p - 1;
    }
    return a[lo];
}

/* Median of a[0..n-1] (reorders a[]).  Odd n: the single middle order
 * statistic.  Even n: quickselect finds the lower-median at k = n/2-1,
 * which leaves every a[i > k] >= a[k] (the standard quickselect
 * post-condition) -- so the upper-median is just the min of that
 * remaining slice, one more linear scan instead of a second full
 * selection pass. */
static double _median_select(double *a, int n) {
    if (n % 2 == 1) {
        return _kth_smallest(a, n, n / 2);
    } else {
        int k = n / 2 - 1;
        double lower = _kth_smallest(a, n, k);
        double upper = a[k + 1];
        int i;

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


	return _unpack_forest( \@nodes, \@idx, \@val );
} ## end sub _build_forest_openmp

# Inverse of _pack_tree's SoA layout: given one tree's packed node
# buffer plus the shared idx/val coefficient buffers, reconstructs the
# ordinary nested-arrayref tree structure _build_tree/_build_node_c
# produce.  li/ri fields hold the child's absolute node index, so this
# just follows them recursively from whatever index the caller says the
# root lives at.  NOTE: _pack_tree numbers nodes DFS pre-order (root at
# 0), but build_forest_openmp_xs appends nodes post-order (children
# before parent), putting the root LAST -- the caller must pass the
# right root index for the buffer's origin.
sub _unpack_node {
	my ( $nodes, $idx, $val, $node_i ) = @_;
	my $off  = $node_i * 6;
	my $type = $nodes->[$off];

	if ( $type == 0 ) {
		return [ _NODE_LEAF, int( $nodes->[ $off + 1 ] ) ];
	} elsif ( $type == 1 ) {

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

			[ @{$val}[ $coff .. $coff + $num - 1 ] ],
			$b,
			_unpack_node( $nodes, $idx, $val, int($li) ),
			_unpack_node( $nodes, $idx, $val, int($ri) ),
		];
	} ## end else [ if ( $type == 0 ) ]
} ## end sub _unpack_node

# Unpacks every tree in the three per-tree packed-buffer arrayrefs
# build_forest_openmp_xs returns into the ordinary nested tree shape.
# The C builder pushes nodes post-order (a node is recorded after both
# of its children), so each tree's root is the LAST node record, not
# index 0 as in _pack_tree's pre-order layout.
sub _unpack_forest {
	my ( $nodes_list, $idx_list, $val_list ) = @_;
	my @trees;
	for my $i ( 0 .. $#$nodes_list ) {
		my @nodes = unpack( 'd*', $nodes_list->[$i] );
		my @idx   = unpack( 'l*', $idx_list->[$i] );
		my @val   = unpack( 'd*', $val_list->[$i] );
		my $root  = @nodes / 6 - 1;

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

			'extension_level' => $opt->{'e'},
			'contamination'   => $opt->{'c'},
			'feature_names'   => $opt->{'t'},
			'voting'          => $opt->{'voting'},
			'mungers'         => $mungers,
		);
	} ## end if ( !$iforest )

	# Munge the raw rows into numbers, then run the numeric validation
	# that was skipped at read time -- an unmunged column holding a
	# string is still an error, just reported post-munge.
	if ($has_mungers) {
		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' )

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

		for my $i ( 0 .. $#$scores ) {
			my $label = $scores->[$i] >= $threshold ? 1 : 0;
			if ( $opt->{'d'} ) {
				$results_string .= join( ',', @{ $data[$i] } ) . ',' . $scores->[$i] . ',' . $label . "\n";
			} else {
				$results_string .= $scores->[$i] . ',' . $label . "\n";
			}
		}
	} ## end else [ if ( $opt->{'learn_only'} ) ]

	# Refresh the contamination threshold against the post-stream window so
	# the saved model's default cutoff tracks the stream.
	if ( !$opt->{'score_only'} && defined $oif->{contamination} && $oif->window_count ) {
		$oif->relearn_threshold;
	}

	if ( $opt->{'save'} && !$opt->{'score_only'} ) {
		$oif->save( $opt->{'m'} );
	}

	if ( length $results_string ) {

t/35-online-accel.t  view on Meta::CPAN


	# Learn a drifted cluster; the stream length also forces window
	# evictions, so both learn and unlearn mutations are in play.
	srand(8);
	$m->learn( cluster( 300, 3 ) );
	ok( !$m->{_c_nodes}, 'snapshot dropped by learning' );

	my $c_after    = with_knobs( $m, 1, 0, sub { $m->score_samples( \@eval ) } );
	my $perl_after = with_knobs( $m, 0, 0, sub { $m->score_samples( \@eval ) } );
	for my $i ( 0 .. $#eval ) {
		cmp_ok( $c_after->[$i], '==', $perl_after->[$i], "post-mutation row $i matches fresh pure Perl" );
	}
	isnt( $c_after->[0], $before->[0], 'and the scores really did move with the drift' );
}; ## end 'mutation invalidates the packed snapshot' => sub

SKIP: {
	skip 'OpenMP not linked in', 1
		unless $Algorithm::Classifier::IsolationForest::HAS_OPENMP;

	subtest 'OpenMP on/off parity' => sub {
		my $m      = make_model();

t/39-online-stream.t  view on Meta::CPAN

	$rate /= scalar @$flags;
	cmp_ok( $rate, '>=', 0.02, 'window flag rate not far below contamination' );
	cmp_ok( $rate, '<=', 0.09, 'window flag rate not far above contamination' );

	# relearn_threshold tracks drift.
	my $old_thr = $m->decision_threshold;
	$m->learn( cluster( 600, 6 ) );
	my $ret = $m->relearn_threshold;
	is( $ret, $m, 'relearn_threshold chains' );
	isnt( $m->decision_threshold, $old_thr, 'threshold moved with the stream' );
	is( $m->predict( [ [ 6, 6 ] ] )->[0], 0, 'post-drift centre passes at the refreshed threshold' );

	ok(
		!eval { $class->new( n_trees => 5 )->relearn_threshold; 1 },
		'relearn_threshold croaks without contamination'
	);
}; ## end 'contamination threshold' => sub

subtest 'window_size 0 disables forgetting' => sub {
	srand(10);
	my $m = $class->new( seed => 13, n_trees => 20, window_size => 0, max_leaf_samples => 16 );



( run in 0.483 second using v1.01-cache-2.11-cpan-acf6aa7dc9e )