Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

LICENSE  view on Meta::CPAN


                            Preamble

  The licenses for most software are designed to take away your
freedom to share and change it.  By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.

  This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it.  You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.

  When we speak of free software, we are referring to freedom of use,
not price.  Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do

LICENSE  view on Meta::CPAN

particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices.  Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

  12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,

benchmarking/bench-voting.pl  view on Meta::CPAN

# both speed and detections.
#
# voting => 'majority' is aggregation-only: it builds the exact same
# trees as voting => 'mean' and differs solely in how the per-tree path
# lengths are combined at score/predict time.  So a fair comparison holds
# the trees fixed (same seed + data) and varies only the aggregation.
#
# Two things are measured:
#
#   1. SPEED.  predict() is where majority voting can win: it stops
#      walking a point's trees as soon as the majority outcome is decided
#      (the paper's "stop at majority"), whereas mean aggregation always
#      walks every tree.  score_samples() has no such early exit (the
#      vote fraction needs the full count), so it is shown as a contrast.
#      Timed across n_trees, query-set size, and feature count, under the
#      default backend (C + OpenMP when available).
#
#      How much predict() saves depends on the data: early exit triggers
#      sooner when points are clearly inliers or clearly outliers, later
#      when they sit near the decision boundary.  The gaussian-cluster +
#      planted-outlier data here is fairly separable, so this is closer

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

#     so no synchronisation is needed).  Tree pointers are extracted from
#     the AVs before the parallel region; the parallel region touches only
#     raw int / double buffers.
#
# vote_all_xs(nodes_av, idx_av, val_av, x_sv, sm_sv,
#             n_pts, n_feats, n_trees, depth_cut, min_votes, use_openmp)
#     Majority-voting (voting => 'majority') counterpart of score_all_xs:
#     instead of summing path lengths it counts, per point, how many trees
#     "vote anomalous" (path length <= depth_cut).  min_votes == 0 writes
#     the full vote count into sm[i]; min_votes > 0 writes a 0.0/1.0 label
#     with per-point early exit once the majority outcome is decided --
#     the MVIForest scoring loop.  See the function's own comment.
#
# Node layout (6 doubles per node, "IF_NZ = 6"):
#   leaf:    [0, size, c(size), 0, 0, 0]
#   axis:    [1, attr, split, li, ri, 0]
#   oblique: [2, coff, nf,  li, ri, b]
#
# c(size) is the expected-path-length adjustment for a leaf holding
# `size` points, precomputed by _pack_tree (it involves a log(); doing
# it at pack time keeps transcendentals out of the per-point per-tree

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

    }
    nodes_av = (AV*)SvRV(nodes_av_sv);
    idx_av   = (AV*)SvRV(idx_av_sv);
    val_av   = (AV*)SvRV(val_av_sv);

    /* C99 VLAs -- n_trees is small (typ. 100) and fits on the stack. */
    const double *node_ptrs[n_trees];
    const int    *idx_ptrs[n_trees];
    const double *val_ptrs[n_trees];

    /* forest_bytes totals every buffer the tree walks touch; it decides
     * between the two loop shapes below. */
    size_t forest_bytes = 0;
    for (ti = 0; ti < n_trees; ti++) {
        SV** np = av_fetch(nodes_av, ti, 0);
        SV** ip = av_fetch(idx_av,   ti, 0);
        SV** vp = av_fetch(val_av,   ti, 0);
        if (!np || !*np || !ip || !*ip || !vp || !*vp) {
            croak("score_all_xs: missing tree %d", ti);
        }
        node_ptrs[ti] = (const double*)SvPVbyte(*np, tl); forest_bytes += tl;

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

		# (see Makefile.PL); it must never short-circuit into loading an
		# older object.
		my $use_prebuilt
			= $prebuilt
			&& !$ENV{IF_RUNTIME_BUILD}
			&& !$ENV{IF_INSTALL_BUILD}
			&& $opt eq $def_opt
			&& $arch eq $def_arch
			&& $no_omp == $def_no_omp;

		# Inline::C hashes the C source to decide whether to rebuild but
		# does NOT include CCFLAGS / OPTIMIZE in that hash.  Without the
		# tag below, toggling IF_NATIVE/IF_ARCH/IF_OPT (or editing the
		# optimisation flags here) would silently reuse a cached binary
		# built with stale flags.  Embedding the active flags as a leading
		# comment forces the hash to differ when they change.  The OpenMP
		# and serial builds get distinct tags so they cache to separate
		# artefacts.
		my $omp_tag    = "/* if_build: openmp $opt_level */\n";
		my $serial_tag = "/* if_build: serial $opt_level */\n";

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

                        across all trees are averaged and normalised into
                        one anomaly score; predict() thresholds that score.
            majority :: Majority Voting Isolation Forest (MVIForest;
                        Chabchoub, Togbe, Boly & Chiky 2022 -- see
                        REFERENCES). Each tree scores the sample on its own
                        (s_i = 2**(-h_i / c(psi))) and votes it anomalous
                        when s_i >= the decision threshold; predict() flags
                        the sample when more than half of the trees
                        (int(n_trees/2) + 1) vote anomalous, and stops
                        walking trees per sample as soon as the outcome is
                        decided. The threshold argument/default of the
                        predict methods is therefore the PER-TREE cutoff
                        here, not a forest-level score cutoff.
                        score_samples() returns the fraction of trees
                        voting anomalous -- still in [0, 1], but discrete
                        in steps of 1/n_trees. contamination composes: fit()
                        learns the per-tree cutoff that flags the requested
                        fraction of the training set.
        default :: mean

    - parallel_fit :: positive integer N => build the trees across N forked

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

=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
cutoff each tree votes against, and a sample is labelled 1 when more than
half of the trees (C<int(n_trees/2) + 1>) vote it anomalous.  Tree walking
stops per sample as soon as the outcome is decided, so this is typically
cheaper than scoring.

    my $results = $forest->predict(\@data, $threshold);

    print "x, y, result\n";

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

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

	my ( $self, $data, $threshold ) = @_;
	$threshold
		= defined $threshold         ? $threshold
		: defined $self->{threshold} ? $self->{threshold}
		:                              0.5;
	$self->_check_fitted;

	# Majority voting: $threshold is the PER-TREE score cutoff and the
	# label is the majority of the tree votes (int(t/2) + 1).  Both the C
	# and the Perl loop stop walking a sample's remaining trees as soon
	# as the outcome is decided -- MVIForest's "stop at majority" saving.
	if ( $self->{voting} eq 'majority' ) {
		my $trees = $self->{trees};
		my $t     = scalar @$trees;
		my $cut   = _depth_cut( $threshold, $self->{c_psi} );
		my $maj   = _min_votes($t);

		if ( $self->{_use_c} && $self->{_c_nodes} ) {
			my ( $n_pts, $nf, $x_packed ) = $self->_resolve_input($data);
			my $labels_packed = "\0" x ( $n_pts * 8 );
			vote_all_xs( $self->{_c_nodes}, $self->{_c_coef_idx}, $self->{_c_coef_val},

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

			push @rows, [ @doubles[ $i * $nf .. ( $i + 1 ) * $nf - 1 ] ];
		}
		return \@rows;
	} ## end if ( ref $data eq 'Algorithm::Classifier::IsolationForest::PackedData')
	croak "expected arrayref or PackedData, got " . ( ref($data) || 'scalar' );
} ## end sub _to_arrayref

# ---------------------------------------------------------------------------
# Missing-value handling.
#
# The `missing` strategy chosen at new() decides how undef feature cells are
# treated.  Scoring always tolerates undef; the strategy governs fit() and
# how undef is represented for the scorer:
#
#   die    -- croak from fit() if the training data holds any undef cell.
#             Scoring still maps undef -> 0 (the long-standing behaviour).
#   zero   -- undef counts as the value 0, at fit and score time.
#   impute -- undef is replaced by a learned per-feature mean/median; the
#             fill vector is stored on the model and reused at score time.
#   nan    -- ranges are built over present values only and a point missing
#             the split feature is routed to the right child, consistently



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