Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

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

	} elsif ( $self->{_use_c} ) {
		$self->{trees}
			= $self->_build_forest_c( $train, $psi, $limit, $self->{n_trees} );
	} else {
		my @trees;
		for ( 1 .. $self->{n_trees} ) {
			my $sample = _subsample( $train, $psi );
			push @trees, $self->_build_tree( $sample, 0, $limit );
		}
		$self->{trees} = \@trees;
	}

	# On a re-fit, packed scoring buffers from the previous fit are still
	# sitting on the object; score_samples() below would pick them up and
	# learn the contamination threshold against the OLD forest.  Drop them
	# so the training-set scoring runs pure-Perl against the trees just
	# built; _rebuild_c_trees repacks from the new trees at the end.
	delete @$self{qw(_c_nodes _c_coef_idx _c_coef_val)};

	# If a contamination rate was requested, learn the score cutoff that flags
	# that fraction of the training set. The threshold lands midway inside a
	# real gap between flagged and unflagged training scores (ties at the
	# k-boundary shift the cut to the nearest gap -- see
	# _threshold_from_ranked), so it sits strictly between attainable values:
	# unambiguous and robust to the tiny float rounding introduced by JSON
	# serialisation.
	#
	# Under voting => 'majority' the value predict() thresholds against is
	# the PER-TREE score, so the quantity to rank is each training point's
	# majority pivot -- the per-tree cutoff at which that point loses its
	# majority (see _majority_pivot_scores).  A point is flagged iff its
	# pivot >= threshold, exactly the relation the mean-mode score has, so
	# the midpoint selection below serves both modes unchanged.
	$self->_learn_contamination_threshold($train)
		if defined $self->{contamination};

	$self->_rebuild_c_trees() if $self->{_use_c};
	return $self;
} ## end sub fit

=head2 pack_data(\@data)

Returns an opaque, blessed wrapper around the input dataset that the
scoring methods can use directly, skipping the per-call work of walking
the arrayref-of-arrayrefs and converting each cell into a double.  At
high feature counts this is a meaningful win when the same dataset is
scored repeatedly (e.g. interactive threshold tuning, dashboards,
plotting that updates as parameters change).

Requires the Inline::C backend; croaks if C<use_c> is false.

    my $packed = $forest->pack_data(\@data);

    # Now any of these accept either an arrayref or the packed wrapper:
    my $scores = $forest->score_samples($packed);
    my $flags  = $forest->predict($packed, 0.6);
    my ($s, $l) = $forest->score_predict_split($packed);

The wrapper has C<n_pts> and C<n_feats> accessors for introspection.
The feature count is matched against the model on every call; passing a
packed dataset built for a different feature count is a fatal error.

=cut

=head2 path_lengths(\@data)

Returns an arrayref of the mean isolation depth per sample, for inspection.

    my $lengths = $forest->path_lengths(\@data);

    print "x, y, length\n";

    my $int=0;
    while (defined($data[$int])) {
        print $data[$int][0].', '.$data[$int][1].', '.$lengths->[$int]."\n";

        $int++;
    }

=cut

sub path_lengths {
	my ( $self, $data ) = @_;
	$self->_check_fitted;
	my $trees = $self->{trees};
	my $t     = scalar @$trees;

	if ( $self->{_use_c} && $self->{_c_nodes} ) {
		my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data);
		my $sums_packed = "\0" x ( $n_pts * 8 );
		score_all_xs(
			$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val},
			$x_packed,         $sums_packed,         $n_pts,
			$nf,               $t,                   $self->{_use_openmp}
		);
		my $result = [];
		finalize_path_lengths_xs( $sums_packed, $n_pts, $t + 0.0, $result );
		return $result;
	} ## end if ( $self->{_use_c} && $self->{_c_nodes} )

	$data = $self->_prepare_perl_input($data);
	my $nan = $self->{missing} eq 'nan' ? 1 : 0;

	# Pure-Perl fallback (tree-outer, sample-inner for cache locality).
	my @sums = (0) x @$data;
	for my $tree (@$trees) {
		for my $i ( 0 .. $#$data ) {
			$sums[$i] += _path_length( $data->[$i], $tree, 0, $nan );
		}
	}
	return [ map { $_ / $t } @sums ];
} ## end sub path_lengths

=head2 predict(\@data, $threshold)

Returns an arrayref of 0/1 labels for the specified data.

If threshold is not specified it uses the contamination-learned cutoff (if
C<fit> was called with C<contamination>), otherwise 0.5.

Under C<< voting => 'majority' >> the threshold is the per-tree score

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

	} ## end if ( $self->{voting} eq 'majority' )

	# Fast path: threshold the raw path-length sums directly, skipping the
	# per-point exp() and the intermediate scores arrayref.
	# Derivation: score = exp(-sum * log(2) / (c*t))
	#   so   score >= T   iff   sum <= -log(T) * c * t / log(2)
	# Only valid for a normal threshold in (0, 1) and a positive c.
	if (   $self->{_use_c}
		&& $self->{_c_nodes}
		&& $self->{c_psi} > 0
		&& $threshold > 0
		&& $threshold < 1 )
	{
		my $trees = $self->{trees};
		my $t     = scalar @$trees;
		my $c     = $self->{c_psi};
		my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data);
		my $sums_packed = "\0" x ( $n_pts * 8 );
		score_all_xs(
			$self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val},
			$x_packed,         $sums_packed,         $n_pts,
			$nf,               $t,                   $self->{_use_openmp}
		);
		my $sum_threshold = -log($threshold) * $c * $t / log(2);
		my $result        = [];
		predict_sums_xs( $sums_packed, $n_pts, $sum_threshold, $result );
		return $result;
	} ## end if ( $self->{_use_c} && $self->{_c_nodes} ...)

	# Fallback: edge thresholds, c==0, or no C backend.
	my $scores = $self->score_samples( $self->_to_arrayref($data) );
	return [ map { $_ >= $threshold ? 1 : 0 } @$scores ];
} ## end sub predict

=head2 predict_tagged(\%row, $threshold)

Predicts whether a single sample is an anomaly using a hashref of named
feature values.  The model must have been fitted (or loaded from a model
that was fitted) with feature names stored via C<feature_names>.

C<$threshold> defaults the same way as in C<predict>.

Returns a scalar 1 (anomaly) or 0 (normal).

    my $label = $forest->predict_tagged(
        { cpu => 0.9, mem => 0.4, disk => 0.1 },
    );

Croaks if the model has no stored feature names, if the hashref contains a
key that is not a known feature name, or if a feature name is absent from the
hashref.

=cut

=head2 tagged_row_to_array(\%row, $caller)

Validates a hashref of named feature values against the model's stored
C<feature_names> and returns a positional arrayref ready to pass to any
of the scoring or prediction methods.

C<$caller> is a string used in error messages to identify which method
triggered the validation (pass the calling method's name).

    my $vec = $forest->tagged_row_to_array(\%row, 'my_method');
    # returns e.g. [0.9, 0.4, 0.1] ordered by feature_names

Croaks if:

=over 4

=item * C<$row> is not a hashref

=item * the model has no stored C<feature_names>

=item * the hashref contains a key that is not a known feature name

=item * a feature name is absent from the hashref

=back

=cut

sub tagged_row_to_array {
	my ( $self, $row, $caller ) = @_;
	croak "$caller requires a hashref"
		unless ref $row eq 'HASH';
	croak "this model has no stored feature_names; " . "refit with -t tags or pass feature_names to new()"
		unless defined $self->{feature_names}
		&& ref $self->{feature_names} eq 'ARRAY'
		&& @{ $self->{feature_names} };

	my @names = @{ $self->{feature_names} };

	my @unknown = grep {
		my $k = $_;
		!grep { $_ eq $k } @names
	} keys %$row;
	croak "unknown feature name(s) in hashref: " . join( ', ', sort @unknown )
		if @unknown;

	my @missing = grep { !exists $row->{$_} } @names;
	croak "missing feature name(s) in hashref: " . join( ', ', @missing )
		if @missing;

	return [ map { $row->{$_} } @names ];
} ## end sub tagged_row_to_array

sub predict_tagged {
	my ( $self, $row, $threshold ) = @_;
	my $vec    = $self->tagged_row_to_array( $row, 'predict_tagged' );
	my $result = $self->predict( [$vec], $threshold );
	return $result->[0];
}

=head2 score_samples(\@data)

Returns an arrayref of anomaly scores, between 0 and 1.

Scores near 1 are strong anomalies (isolated quickly).

Scores well below 0.5 are normal.



( run in 1.237 second using v1.01-cache-2.11-cpan-c966e8aa7e8 )