Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
package Algorithm::Classifier::IsolationForest;
use strict;
use warnings;
use Carp qw(croak);
use Config ();
use List::Util qw(min);
use Scalar::Util qw(looks_like_number);
use POSIX qw(ceil);
use JSON::PP ();
use File::Slurp qw(read_file write_file);
our $VERSION = '0.7.0';
use constant EULER => 0.5772156649015329;
# Narrowed to C double precision so _randn() multiplies by the exact
# constant _c_randn() uses. A no-op on nvsize == 8 perls.
use constant TWO_PI => unpack( 'd', pack 'd', 6.283185307179586 );
# Node-type tags stored in index 0 of every tree node arrayref.
# 0 is falsy, so while ($node->[0]) acts as while (!leaf).
use constant _NODE_LEAF => 0;
use constant _NODE_AXIS => 1;
use constant _NODE_OBLIQUE => 2;
# The Inline::C tree builder computes everything in C doubles. On a perl
# whose NV is wider than a double (-Duselongdouble / -Dusequadmath) the
# pure-Perl builder keeps extra low bits at every step, so the two
# backends would stop producing bit-identical trees for the same seed
# (the parity t/03-fit-determinism.t checks). _NV_IS_DOUBLE guards
# narrowing statements wherever the pure-Perl builder computes a value
# that gets STORED in a tree (split points, hyperplane coefficients and
# offsets, impute fills), rounding at the same points the C builder
# rounds. It is compile-time true on nvsize == 8 perls, so there the
# guarded statements are optimised away and cost nothing.
#
# The row-partition loops (v < split, dot <= b) are deliberately NOT
# narrowed: with both operands already double-exact an axis comparison
# is identical anyway, and an oblique dot product accumulated in a wider
# NV flips a comparison only when |dot - b| falls inside the NV-vs-double
# rounding gap (~1e-19 relative) -- negligible, and those are the hot
# loops.
use constant _NV_IS_DOUBLE => $Config::Config{nvsize} == 8;
# Round an NV to C double precision. Only ever reached on wide-NV
# perls -- see _NV_IS_DOUBLE.
#
# Args:
# $nv :: any number. On a -Duselongdouble / -Dusequadmath perl it may
# carry more precision than a C double can hold.
#
# Returns: the same number rounded to the nearest IEEE 754 double. A no-op
# for a value that is already double-exact.
#
# Example:
# _to_double( $lo + rand() * ( $hi - $lo ) ); # what the C builder stores
sub _to_double { unpack 'd', pack 'd', $_[0] }
# ---------------------------------------------------------------------------
# Optional Inline::C accelerator for the scoring hot path.
#
# pack_input_xs(data_sv, out_sv, n_pts, n_feats, miss_mode, fill_sv)
# Walks the Perl arrayref-of-arrayrefs and writes a packed double buffer
# into out_sv. Replaces the dominant per-call Perl map-pack loop.
# miss_mode selects how an undef cell is packed: 0 => 0.0, 1 => the
# per-feature fill from fill_sv (impute), 2 => NaN (nan strategy).
#
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
B<Census> -- count the rows (this is the true C<n> that fixes C<psi =
min(sample_size, n)>) and pin the feature width. By default this is an
I<index> pass (see C<index> below): a fast block-scan that also records each
data row's byte offset.
=item 2.
B<Gather> -- retain only the rows the trees actually sampled (chosen in
bounded memory via Floyd's algorithm), then build each tree from its
sub-sample exactly as L</fit> would. With the offset table from pass 1 this
B<seeks straight to the sampled rows> instead of re-scanning the file.
=item 3.
B<Threshold> (only when C<contamination> is set) -- score every row against
the built forest and place the cut at the contamination boundary. The cut
is B<exact>, not estimated: a single min-heap of the C<k+1> largest scores
(C<k = round(contamination * n)>) captures everything
L</decision_threshold> selection needs, with a rare extra pass to resolve a
tie block straddling the boundary. Under C<< use_c => 1 >> (the default when
the C backend is present) this pass runs through the C scorer, so it stays
fast even over millions of rows.
=back
Neither census variant parses cells into numbers or checks for missing
values: only the rows that actually train (via gather) or are scored (the
threshold pass) are validated, so a malformed or missing cell in a
never-used row is not reported. In particular, under C<< missing => 'die' >>
a missing cell is rejected exactly when it appears in a sampled training row.
Because rows are addressed by their position across passes, C<$path> must be
a stable, re-readable file (not a pipe or C<STDIN>).
The first line is skipped as a header when it holds any non-numeric text (a
feature-name row) or exactly matches the model's stored C<feature_names> --
data rows contain only numbers and empty cells. Pass C<< header => 1 >> to
force the first line to be skipped even when it is all-numeric.
Options:
=over 4
=item * C<< header => 1 >> -- always skip the first line as a column header
(otherwise a header is auto-detected as described above).
=item * C<< index => 0 >> -- disable the offset-index pass and use the
streaming two-pass reader instead. The index is on by default: it makes
gather a random-access read of just the sampled rows rather than a second
full scan, but costs an C<8 * n>-byte offset table. When that table would
exceed C<index_max> it is dropped automatically and the fit falls back to
streaming. (In index mode a blank line is one that is empty or contains only
whitespace, judged cheaply; pass C<< index => 0 >> for files where that
matters.)
=item * C<< index_max => bytes >> -- ceiling on the offset table (default
256 MiB). Above it, the index is abandoned mid-scan and gather streams.
=item * C<< c_scan => 0 >> -- validate each scored cell with
C<looks_like_number> in the threshold pass instead of letting the C packer
coerce it. C<c_scan> is on by default and takes effect only on the C
mean-voting scoring path, where it is a large saving over millions of rows;
it produces the identical result on valid data, but coerces a non-numeric
scored cell to C<0.0> (nudging the threshold) rather than dying on it.
=back
Every column is a feature; an empty cell is treated as a missing value and
handled by the model's C<missing> strategy. The resulting model is
identical in shape to one built by L</fit> and is fully deterministic given
the same file, C<seed>, and parameters (though not bit-identical to
L</fit>, whose RNG stream interleaves sampling and tree-building
differently).
Current limitations: C<mungers> and tagged/named columns are not supported
(load such data through L<fit_tagged|/fit_tagged(\@rows)>), and the trees are always built
serially in pure Perl -- C<parallel_fit>/C<use_openmp_fit> are ignored for
the build, though scoring is still accelerated when C<use_c> is on.
my $iforest = Algorithm::Classifier::IsolationForest->new(
n_trees => 100,
sample_size => 256,
contamination => 0.01,
seed => 42,
);
$iforest->fit_from_csv('huge.csv', header => 1);
=cut
sub fit_from_csv {
my ( $self, $path, %opt ) = @_;
croak "fit_from_csv() requires a path to a CSV file"
unless defined $path && length $path;
croak "fit_from_csv(): '$path' is not a readable file"
unless -f $path && -r _;
croak "fit_from_csv() does not support munger models; " . "load the data through fit_tagged/fit instead"
if _plan($self);
# Decide once whether the first line is a header, so every pass skips the
# same line and row indices stay aligned. header => 1 forces it; otherwise
# a first line carrying any non-numeric text (or matching stored
# feature_names) is a header -- data rows hold only numbers and blanks.
my $skip_first = $self->_detect_header( $path, $opt{header} );
# ---- Pass 1: census. Establish the true row count (which fixes psi) and
# pin the feature width. Neither census variant parses cells into numbers
# or checks for missing values -- only the rows that actually train
# (_numify_row, in gather) or get scored (the threshold pass) are validated,
# so a malformed or missing cell in a never-sampled row is not seen.
#
# By default the census is an "index" pass: a block-scan that also records
# each data row's byte offset, letting Pass 2 seek straight to the sampled
# rows instead of re-scanning the whole file. The offset table costs 8*n
# bytes; when that would top index_max (or index => 0 was passed) it is not
# built and both passes fall back to the streaming reader.
my $use_index = exists $opt{index} ? $opt{index} : 1;
my $index_max = defined $opt{index_max} ? $opt{index_max} : ( 256 * 1024 * 1024 );
my ( $n, $n_features, $offsets );
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
unless $want == $n_features;
}
$self->{n_features} = $n_features;
my ( $psi, $limit ) = $self->_resolve_geometry( $n, $n_features );
# ---- Choose which row indices each tree trains on: an independent,
# uniform psi-subset drawn without replacement. Floyd's algorithm draws
# each subset in O(psi) memory, never materialising the 0..n-1 index
# vector _subsample() uses -- for an out-of-core n that vector would not
# fit either. %want maps each needed row index to the (tree, slot) pairs
# awaiting it, so a row several trees picked is gathered once and shared.
srand( $self->{seed} ) if defined $self->{seed};
my @tree_idx; # tree => [ chosen row indices ]
my %want; # row index => [ [tree, slot], ... ]
for my $t ( 0 .. $self->{n_trees} - 1 ) {
my @idx = _sample_indices_distinct( $n, $psi );
$tree_idx[$t] = \@idx;
push @{ $want{ $idx[$_] } }, [ $t, $_ ] for 0 .. $#idx;
}
# ---- Pass 2: gather the sampled rows (validated + coerced by _numify_row).
# With an offset table this seeks straight to them; otherwise it re-scans.
my $pool
= $offsets
? $self->_gather_indexed( $path, $offsets, \%want )
: $self->_gather_stream( $path, $skip_first, \%want );
# Baselines for ablation explanations, learned from the gathered
# sub-sample -- like the impute fill below, the only rows bounded-
# memory fitting ever holds. Before _apply_missing_to_pool, which
# densifies the pool in place (the medians must come from present
# values only, matching fit()). Median is an exact order statistic,
# so the hash-order values() walk cannot change the result.
$self->{feature_baselines} = $self->_compute_feature_baselines( [ values %$pool ] );
# Apply the missing-value strategy to the retained rows (see fit()'s
# _prepare_fit_data; the impute fill is learned from this training
# sub-sample, the only value bounded-memory fitting can compute).
$self->_apply_missing_to_pool($pool);
# ---- Build. Each tree trains on its gathered sub-sample -- structurally
# identical to fit()'s per-tree _build_tree(_subsample(...)).
my @trees;
for my $t ( 0 .. $self->{n_trees} - 1 ) {
my $sample = [ @{$pool}{ @{ $tree_idx[$t] } } ];
push @trees, $self->_build_tree( $sample, 0, $limit );
}
$self->{trees} = \@trees;
# Repack the just-built trees for the C scorer up front. fit() scores its
# learned threshold pure-Perl because its training set is small; the
# streaming contamination pass here may score millions of rows, so paying
# one repack now lets that pass run through the C path instead (bit-
# identical result, minutes -> seconds). The delete first clears any stale
# buffers from a prior fit so the repack reflects the new forest.
delete @$self{qw(_c_nodes _c_coef_idx _c_coef_val)};
$self->_rebuild_c_trees() if $self->{_use_c};
# c_scan (default on): let the C scorer's packer coerce raw cells via SvNV
# in the contamination pass instead of validating each with looks_like_number
# in Perl -- a large saving over millions of rows. It only takes effect on
# the C mean-voting path; see _stream_scores.
my $c_scan = exists $opt{c_scan} ? $opt{c_scan} : 1;
$self->_learn_contamination_threshold_streaming( $path, $skip_first, $n, $c_scan )
if defined $self->{contamination};
return $self;
} ## end sub fit_from_csv
=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(
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
= defined $self->{extension_level}
? $self->{extension_level}
: $max_ext;
$ext = 0 if $ext < 0;
$ext = $max_ext if $ext > $max_ext;
$self->{extension_level_used} = $ext;
} else {
$self->{extension_level_used} = undef;
}
# Height limit: the average tree height ceil(log2(psi)). Past this depth the
# remaining points are scored using the c(size) adjustment instead.
my $limit
= defined $self->{max_depth}
? $self->{max_depth}
: ceil( log($psi) / log(2) );
$limit = 1 if $limit < 1;
$self->{max_depth_used} = $limit;
return ( $psi, $limit );
} ## end sub _resolve_geometry
#-------------------------------------------------------------------------------
# fit_from_csv() machinery.
#-------------------------------------------------------------------------------
# Inspect the first non-blank line and report whether it is a header to skip
# rather than train on. $forced (the header => option) makes it unconditional;
# otherwise a line is a header when it carries any non-numeric text -- data rows
# hold only numbers and empty cells -- or exactly matches stored feature_names.
#
# Args:
# $path :: path to the CSV file, which must be readable.
# $forced :: the caller's header => option. True skips detection and
# answers yes; undef or false runs the heuristic.
#
# Returns: 1 when the first non-blank line is a header to skip, 0 otherwise.
# An empty file answers 0 and lets the census report the real problem.
#
# Example:
# $self->_detect_header( 'train.csv', undef ); # 1 for "cpu,mem,disk"
# $self->_detect_header( 'train.csv', undef ); # 0 for "0.9,0.4,0.1"
sub _detect_header {
my ( $self, $path, $forced ) = @_;
open my $fh, '<', $path
or croak "fit_from_csv(): cannot open '$path': $!";
my $first;
while ( defined( my $line = <$fh> ) ) {
$line =~ s/\r?\n\z//;
next if $line =~ /^\s*$/;
$first = $line;
last;
}
close $fh;
return 0 unless defined $first; # empty file; the census will report it
return 1 if $forced;
my @f = split /,/, $first, -1;
for my $x (@f) {
next if !length $x; # empty cell: fine in data
return 1 unless looks_like_number($x); # text: it is a header
}
# All-numeric first line: only a header if it reproduces feature_names.
my $names = $self->{feature_names};
if ( ref $names eq 'ARRAY' && @$names == @f ) {
my $match = grep { $f[$_] eq $names->[$_] } 0 .. $#f;
return 1 if $match == @f;
}
return 0;
} ## end sub _detect_header
# Return a closure that yields ($row, $line_number) per non-blank CSV line and
# an empty list at EOF. An empty cell becomes undef (the missing-value marker).
# With $parse true each non-empty cell is validated and coerced to a number (a
# non-numeric cell dies); with $parse false the cells are left as raw strings --
# the cheap mode the census uses, which needs only the column count and empties.
# When $skip_first is set the first non-blank line (a header) is dropped. Blank
# lines are skipped, so a row's index is its position among the non-blank data
# lines -- stable across passes as long as the file does not change under us.
#
# Args:
# $path :: path to the CSV file, which must be readable.
# $skip_first :: true to drop the first non-blank line as a header.
# $parse :: true to validate and numify each non-empty cell (croaking on a
# non-numeric one), false to hand back raw strings.
#
# Returns: a closure. Each call returns the two-element list
# (\@fields, $line_number) for the next data line, or the empty list at EOF
# (where it also closes the handle). @fields holds numbers under
# $parse, strings otherwise, with undef for an empty cell.
#
# Example:
# my $reader = $self->_csv_reader( 'train.csv', 1, 1 );
# while ( my ( $row, $line ) = $reader->() ) {
# # $row = [ 0.9, undef, 0.1 ] for "0.9,,0.1" on line $line
# }
sub _csv_reader {
my ( $self, $path, $skip_first, $parse ) = @_;
open my $fh, '<', $path
or croak "fit_from_csv(): cannot open '$path': $!";
my $line_no = 0;
my $skipped = 0;
return sub {
while ( defined( my $line = <$fh> ) ) {
$line_no++;
$line =~ s/\r?\n\z//;
next if $line =~ /^\s*$/;
if ( $skip_first && !$skipped ) { $skipped = 1; next; }
my @fields = split /,/, $line, -1;
for my $f (@fields) {
if ( !length $f ) { $f = undef; next; }
next unless $parse;
croak "fit_from_csv(): line $line_no value '$f' is not a number"
unless looks_like_number($f);
$f += 0;
}
return ( \@fields, $line_no );
} ## end while ( defined( my $line = <$fh> ) )
close $fh;
return;
}; ## end sub
} ## end sub _csv_reader
# Validate and coerce a raw row (from a parse => 0 reader) into numbers in
# place: defined cells must look like numbers. An undef cell (empty CSV marker)
# passes through for zero/impute/nan, but croaks under the 'die' strategy -- so
# 'die' rejects a missing value exactly when it lands in a sampled training row.
# $where names the row for error messages. Only the rows a fit keeps are run
# through here, which is why a bad cell elsewhere is never reported.
#
# Args:
# $row :: arrayref of raw cells from a parse => 0 reader -- strings, with
# undef for the empty cells. Modified in place.
# $where :: a phrase naming the row for croak messages, e.g. "line 42" or
# "sampled row 17".
#
# Returns: nothing. $row's defined cells come back as numbers; undef cells
# stay undef unless missing => 'die', which croaks instead.
#
# Example:
# my $row = [ '0.9', undef, '0.1' ];
# $self->_numify_row( $row, 'line 42' ); # [ 0.9, undef, 0.1 ]
sub _numify_row {
my ( $self, $row, $where ) = @_;
my $die = $self->{missing} eq 'die';
for my $f (@$row) {
if ( !defined $f ) {
croak "fit_from_csv(): missing value in $where; construct with "
. "missing => 'zero', 'impute', or 'nan' to train on data "
. "with missing values"
if $die;
next;
}
croak "fit_from_csv(): $where value '$f' is not a number"
unless looks_like_number($f);
$f += 0;
} ## end for my $f (@$row)
return;
} ## end sub _numify_row
# Streaming census (index => 0 or offset table over budget): count the data
# rows and pin the feature width via the cheap parse => 0 reader. No cell
# validation -- that is deferred to the rows that train or get scored.
#
# Args:
# $path :: path to the CSV file, which must be readable.
# $skip_first :: true to drop the first non-blank line as a header.
#
# Returns: the two-element list ($n, $nf) -- the data row count and the
# feature width taken from the first data row. Both undef-free, though $n
# is 0 and $nf undef for a file with no data rows. Croaks on a row whose
# column count disagrees with the first row's.
#
# Example:
# my ( $n, $nf ) = $self->_census_stream( 'train.csv', 1 ); # ( 1_000_000, 4 )
sub _census_stream {
my ( $self, $path, $skip_first ) = @_;
my $reader = $self->_csv_reader( $path, $skip_first, 0 );
my ( $n, $nf ) = ( 0, undef );
while ( my ( $row, $line ) = $reader->() ) {
$nf //= scalar @$row;
croak "fit_from_csv(): line $line of '$path' has " . scalar(@$row) . " columns but expected $nf"
unless @$row == $nf;
$n++;
}
return ( $n, $nf );
} ## end sub _census_stream
# Streaming gather (no offset table): re-scan the file, numifying only the
# sampled rows. Companion to _census_stream.
#
# Args:
# $path :: path to the CSV file, which must be readable.
# $skip_first :: true to drop the first non-blank line as a header.
# $want :: hashref used as a set -- its keys are the data row indices to
# keep, counting from 0 over the non-blank data lines.
#
# Returns: hashref keyed by the same indices, each value the numified row as
# an arrayref. Indices absent from the file simply do not appear.
#
# Example:
# my $pool = $self->_gather_stream( 'train.csv', 1, { 0 => 1, 7 => 1 } );
# # { 0 => [ 0.9, 0.4 ], 7 => [ 1.2, 0.3 ] }
sub _gather_stream {
my ( $self, $path, $skip_first, $want ) = @_;
my $reader = $self->_csv_reader( $path, $skip_first, 0 );
my %pool;
my $i = 0;
while ( my ( $row, $line ) = $reader->() ) {
if ( exists $want->{$i} ) {
$self->_numify_row( $row, "line $line" );
$pool{$i} = $row;
}
$i++;
}
lib/Algorithm/Classifier/IsolationForest.pm view on Meta::CPAN
my @heap;
my $cap = $k + 1;
$self->_stream_scores(
$path,
$skip_first,
sub {
my $s = $_[0];
if ( @heap < $cap ) { _heap_push( \@heap, $s ) }
elsif ( $s > $heap[0] ) { _heap_replace_root( \@heap, $s ) }
},
$c_scan
);
my @desc = sort { $b <=> $a } @heap; # k+1 largest, descending
my $v = $desc[ $k - 1 ]; # k-th largest
my $lo = $desc[$k]; # (k+1)-th largest
# Clean gap at the boundary: cut midway, exactly as _threshold_from_ranked.
if ( $lo < $v ) {
$self->{threshold} = ( $v + $lo ) / 2.0;
return;
}
# A tie block of value $v straddles rank k. Reproduce _threshold_from_ranked's
# tie branch: locate the block's edges (i = first index at $v, j = first
# index below it) and the neighbouring scores with one more pass.
my ( $cnt_gt, $cnt_eq, $above, $below ) = ( 0, 0, undef, undef );
$self->_stream_scores(
$path,
$skip_first,
sub {
my $s = $_[0];
if ( $s > $v ) {
$cnt_gt++;
$above = $s if !defined $above || $s < $above; # smallest > $v
} elsif ( $s == $v ) {
$cnt_eq++;
} else {
$below = $s if !defined $below || $s > $below; # largest < $v
}
},
$c_scan
);
my $i = $cnt_gt; # first rank holding $v
my $j = $cnt_gt + $cnt_eq; # first rank below $v
if ( $i > 0 && ( $k - $i ) < ( $j - $k ) ) {
$self->{threshold} = ( $above + $v ) / 2.0; # exclude the block
} elsif ( $j < $n ) {
$self->{threshold} = ( $v + $below ) / 2.0; # include the block
} else {
$self->{threshold} = $v - 1e-9; # block runs to the end
}
return;
} ## end sub _learn_contamination_threshold_streaming
# Stream the CSV, score rows in flat batches (through the same mean/majority
# path score_samples/predict use), and invoke $cb->($score) for each row.
# c_scan fast path: when scoring runs through the C packer (mean voting, use_c,
# trees packed) the packer coerces each cell via SvNV, so the reader can run
# raw (parse => 0) and skip the Perl looks_like_number validation. Every other
# path parses (parse => 1) so its Perl scorer sees real numbers.
#
# Args:
# $path :: path to the CSV file, which must be readable.
# $skip_first :: true to drop the first non-blank line as a header.
# $cb :: coderef invoked once per data row as $cb->($score), in file order.
# The score is the mean-mode anomaly score, or the majority pivot
# under voting => 'majority'.
# $c_scan :: true to allow the raw-cell fast path described above.
#
# Returns: nothing; everything is delivered through $cb.
#
# Example:
# my $max;
# $self->_stream_scores( 'train.csv', 1,
# sub { $max = $_[0] if !defined $max || $_[0] > $max }, 1 );
sub _stream_scores {
my ( $self, $path, $skip_first, $cb, $c_scan ) = @_;
my $majority = $self->{voting} eq 'majority' ? 1 : 0;
my $c_coerce = $c_scan && $self->{_use_c} && $self->{_c_nodes} && !$majority;
my $reader = $self->_csv_reader( $path, $skip_first, $c_coerce ? 0 : 1 );
# The c_coerce path lets the C packer turn any non-numeric cell into 0.0 via
# SvNV; silence the per-cell "isn't numeric" warnings that intentional
# coercion raises (clean data produces none). Other warnings pass through.
local $SIG{__WARN__};
$SIG{__WARN__} = sub { $_[0] =~ /isn't numeric/ or warn $_[0] }
if $c_coerce;
my @batch;
my $flush = sub {
return unless @batch;
my $scores
= $majority
? $self->_majority_pivot_scores( \@batch )
: $self->score_samples( \@batch );
$cb->($_) for @$scores;
@batch = ();
};
while ( my ($row) = $reader->() ) {
push @batch, $row;
$flush->() if @batch >= 8192;
}
$flush->();
return;
} ## end sub _stream_scores
# Minimal binary min-heap over a plain arrayref (root = smallest). Paired
# with _heap_replace_root to keep the k+1 largest scores of a stream in
# bounded memory -- push until full, then replace the root whenever a
# bigger value turns up.
#
# Append a value and sift it up into place.
#
# Args:
# $h :: the heap, an arrayref maintained by these two functions alone.
# Modified in place.
# $x :: the number to insert.
#
# Returns: nothing. $h->[0] is the smallest value held afterwards.
( run in 1.693 second using v1.01-cache-2.11-cpan-4ab04211f4c )