| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest::App::Command::pack; |
|
2
|
|
|
|
|
|
|
|
|
3
|
81
|
|
|
81
|
|
52010
|
use strict; |
|
|
81
|
|
|
|
|
152
|
|
|
|
81
|
|
|
|
|
2749
|
|
|
4
|
81
|
|
|
81
|
|
303
|
use warnings; |
|
|
81
|
|
|
|
|
138
|
|
|
|
81
|
|
|
|
|
2998
|
|
|
5
|
81
|
|
|
81
|
|
320
|
use Algorithm::Classifier::IsolationForest (); |
|
|
81
|
|
|
|
|
163
|
|
|
|
81
|
|
|
|
|
1425
|
|
|
6
|
81
|
|
|
81
|
|
294
|
use Algorithm::Classifier::IsolationForest::App -command; |
|
|
81
|
|
|
|
|
127
|
|
|
|
81
|
|
|
|
|
580
|
|
|
7
|
81
|
|
|
81
|
|
21626
|
use File::Slurp qw(read_file write_file); |
|
|
81
|
|
|
|
|
167
|
|
|
|
81
|
|
|
|
|
4209
|
|
|
8
|
81
|
|
|
81
|
|
362
|
use Scalar::Util qw(looks_like_number); |
|
|
81
|
|
|
|
|
155
|
|
|
|
81
|
|
|
|
|
5295
|
|
|
9
|
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
# .iforest-packed v1 file layout (all little-endian): |
|
11
|
|
|
|
|
|
|
# |
|
12
|
|
|
|
|
|
|
# offset size field |
|
13
|
|
|
|
|
|
|
# ----------------------------------------------------------- |
|
14
|
|
|
|
|
|
|
# 0 8 magic -- ASCII "IFPKD\0\0\0" |
|
15
|
|
|
|
|
|
|
# 8 2 version (uint16, currently 1) |
|
16
|
|
|
|
|
|
|
# 10 2 reserved (uint16, must be 0) |
|
17
|
|
|
|
|
|
|
# 12 4 n_pts (uint32) |
|
18
|
|
|
|
|
|
|
# 16 4 n_feats (uint32) |
|
19
|
|
|
|
|
|
|
# 20 ... n_pts * n_feats packed doubles ('d' pack format, |
|
20
|
|
|
|
|
|
|
# little-endian per the IEEE-754 native layout) |
|
21
|
|
|
|
|
|
|
# |
|
22
|
|
|
|
|
|
|
# The format is intentionally minimal: the goal is to skip the CSV |
|
23
|
|
|
|
|
|
|
# parse + pack_input_xs cost on subsequent scoring runs. Models are |
|
24
|
|
|
|
|
|
|
# not embedded -- the caller must pair the .iforest-packed file with a |
|
25
|
|
|
|
|
|
|
# model that has the same n_features at score time. |
|
26
|
81
|
|
|
81
|
|
367
|
use constant MAGIC => 'IFPKD' . "\0\0\0"; # 8 bytes |
|
|
81
|
|
|
|
|
179
|
|
|
|
81
|
|
|
|
|
5200
|
|
|
27
|
81
|
|
|
81
|
|
370
|
use constant VERSION => 1; |
|
|
81
|
|
|
|
|
232
|
|
|
|
81
|
|
|
|
|
3457
|
|
|
28
|
81
|
|
|
81
|
|
340
|
use constant HEADER_LEN => 20; |
|
|
81
|
|
|
|
|
138
|
|
|
|
81
|
|
|
|
|
78903
|
|
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
sub _read_packed { |
|
31
|
2
|
|
|
2
|
|
8
|
my ($path) = @_; |
|
32
|
2
|
50
|
|
|
|
91
|
open my $fh, '<:raw', $path or die "open '$path' for read: $!\n"; |
|
33
|
2
|
|
|
|
|
7
|
my $hdr; |
|
34
|
2
|
50
|
|
|
|
50
|
read( $fh, $hdr, HEADER_LEN ) == HEADER_LEN |
|
35
|
|
|
|
|
|
|
or die "'$path' is shorter than a packed-file header\n"; |
|
36
|
2
|
|
|
|
|
17
|
my ( $magic, $version, $reserved, $n_pts, $n_feats ) = unpack( 'a8 v v V V', $hdr ); |
|
37
|
2
|
50
|
|
|
|
10
|
die "'$path' does not look like a .iforest-packed file\n" |
|
38
|
|
|
|
|
|
|
unless $magic eq MAGIC; |
|
39
|
2
|
50
|
|
|
|
8
|
die "'$path' is .iforest-packed version $version; only " . VERSION . " is supported\n" |
|
40
|
|
|
|
|
|
|
unless $version == VERSION; |
|
41
|
2
|
50
|
|
|
|
7
|
die "'$path' has non-zero reserved field $reserved\n" |
|
42
|
|
|
|
|
|
|
unless $reserved == 0; |
|
43
|
2
|
|
|
|
|
13
|
my $bytes; |
|
44
|
2
|
|
|
|
|
7
|
my $want = $n_pts * $n_feats * 8; |
|
45
|
2
|
0
|
|
|
|
12
|
read( $fh, $bytes, $want ) == $want |
|
|
|
50
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
or die "'$path' truncated: wanted $want bytes, got " . ( defined $bytes ? length($bytes) : 0 ) . "\n"; |
|
47
|
2
|
|
|
|
|
36
|
close $fh; |
|
48
|
2
|
|
|
|
|
18
|
return ( $n_pts, $n_feats, $bytes ); |
|
49
|
|
|
|
|
|
|
} ## end sub _read_packed |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
sub _write_packed { |
|
52
|
1
|
|
|
1
|
|
21
|
my ( $path, $n_pts, $n_feats, $bytes ) = @_; |
|
53
|
1
|
|
|
|
|
6
|
my $hdr = pack( 'a8 v v V V', MAGIC, VERSION, 0, $n_pts, $n_feats ); |
|
54
|
1
|
|
|
|
|
7
|
write_file( $path, { 'atomic' => 1, 'binmode' => ':raw' }, $hdr . $bytes ); |
|
55
|
|
|
|
|
|
|
} |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
# Helper used by the other commands so they all read the same format |
|
58
|
|
|
|
|
|
|
# the same way. Returns ($n_pts, $n_feats, $bytes_str). |
|
59
|
2
|
|
|
2
|
0
|
13
|
sub read_packed_file { _read_packed(@_) } |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
# Cheap, allocation-light magic check so consumer commands can peek at |
|
62
|
|
|
|
|
|
|
# a path without slurping the whole file. |
|
63
|
|
|
|
|
|
|
sub is_packed_file { |
|
64
|
4
|
|
|
4
|
0
|
14
|
my ($path) = @_; |
|
65
|
4
|
50
|
|
|
|
419
|
open my $fh, '<:raw', $path or return 0; |
|
66
|
4
|
|
|
|
|
13
|
my $magic; |
|
67
|
4
|
|
|
|
|
107
|
my $ok = read( $fh, $magic, 8 ) == 8; |
|
68
|
4
|
|
|
|
|
69
|
close $fh; |
|
69
|
4
|
|
66
|
|
|
66
|
return $ok && $magic eq MAGIC; |
|
70
|
|
|
|
|
|
|
} |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
sub opt_spec { |
|
73
|
|
|
|
|
|
|
return ( |
|
74
|
|
|
|
|
|
|
[ |
|
75
|
1
|
|
|
1
|
1
|
24922
|
'm=s', |
|
76
|
|
|
|
|
|
|
'Model JSON to validate n_features against.', |
|
77
|
|
|
|
|
|
|
{ 'default' => 'iforest_model.json', 'completion' => 'files' } |
|
78
|
|
|
|
|
|
|
], |
|
79
|
|
|
|
|
|
|
[ 'i=s', 'Input CSV to pack.', { 'completion' => 'files' } ], |
|
80
|
|
|
|
|
|
|
[ 'o=s', 'Output .iforest-packed file path.', { 'completion' => 'files' } ], |
|
81
|
|
|
|
|
|
|
[ 'w', 'Overwrite -o if it already exists.' ], |
|
82
|
|
|
|
|
|
|
); |
|
83
|
|
|
|
|
|
|
} ## end sub opt_spec |
|
84
|
|
|
|
|
|
|
|
|
85
|
0
|
|
|
0
|
1
|
0
|
sub abstract { 'Pre-pack a CSV dataset into a binary file the scoring commands can read directly' } |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
sub description { |
|
88
|
0
|
|
|
0
|
1
|
0
|
'Reads a CSV, validates that every row has the same numeric |
|
89
|
|
|
|
|
|
|
column count as the model expects, runs the data through pack_data, and |
|
90
|
|
|
|
|
|
|
writes a self-contained binary (.iforest-packed) the other iforest |
|
91
|
|
|
|
|
|
|
commands can consume directly. |
|
92
|
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
This skips the CSV parse + pack_input_xs cost on subsequent scoring |
|
94
|
|
|
|
|
|
|
runs. It is most useful when the same data set is scored repeatedly |
|
95
|
|
|
|
|
|
|
with different thresholds, e.g. during interactive tuning: |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
iforest pack -m model.json -i data.csv -o data.packed |
|
98
|
|
|
|
|
|
|
iforest predict -m model.json -i data.packed -t 0.55 -o pred-55.csv |
|
99
|
|
|
|
|
|
|
iforest predict -m model.json -i data.packed -t 0.65 -o pred-65.csv |
|
100
|
|
|
|
|
|
|
iforest predict -m model.json -i data.packed -t 0.75 -o pred-75.csv |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
The file format begins with the magic bytes "IFPKD\0\0\0". predict |
|
103
|
|
|
|
|
|
|
auto-detects it on its -i input. |
|
104
|
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
Requires the Inline::C backend; pure-Perl installs cannot produce or |
|
106
|
|
|
|
|
|
|
consume the packed format. |
|
107
|
|
|
|
|
|
|
'; |
|
108
|
|
|
|
|
|
|
} ## end sub description |
|
109
|
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
sub validate { |
|
111
|
1
|
|
|
1
|
0
|
3
|
my ( $self, $opt, $args ) = @_; |
|
112
|
|
|
|
|
|
|
|
|
113
|
1
|
50
|
|
|
|
44
|
if ( !defined $opt->{'i'} ) { |
|
|
|
50
|
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
114
|
0
|
|
|
|
|
0
|
$self->usage_error('-i has not been specified'); |
|
115
|
|
|
|
|
|
|
} elsif ( !-f $opt->{'i'} ) { |
|
116
|
0
|
|
|
|
|
0
|
$self->usage_error( '-i, "' . $opt->{'i'} . '", is not a file or does not exist' ); |
|
117
|
|
|
|
|
|
|
} elsif ( !-r $opt->{'i'} ) { |
|
118
|
0
|
|
|
|
|
0
|
$self->usage_error( '-i, "' . $opt->{'i'} . '", is not readable' ); |
|
119
|
|
|
|
|
|
|
} |
|
120
|
|
|
|
|
|
|
|
|
121
|
1
|
50
|
33
|
|
|
43
|
if ( !defined $opt->{'o'} ) { |
|
|
|
50
|
|
|
|
|
|
|
122
|
0
|
|
|
|
|
0
|
$self->usage_error('-o has not been specified'); |
|
123
|
|
|
|
|
|
|
} elsif ( -e $opt->{'o'} && !$opt->{'w'} ) { |
|
124
|
0
|
|
|
|
|
0
|
$self->usage_error( '-o, "' . $opt->{'o'} . '", already exists and -w was not specified' ); |
|
125
|
|
|
|
|
|
|
} |
|
126
|
|
|
|
|
|
|
|
|
127
|
1
|
50
|
|
|
|
17
|
if ( !-f $opt->{'m'} ) { |
|
|
|
50
|
|
|
|
|
|
|
128
|
0
|
|
|
|
|
0
|
$self->usage_error( '-m, "' . $opt->{'m'} . '", is not a file or does not exist' ); |
|
129
|
|
|
|
|
|
|
} elsif ( !-r $opt->{'m'} ) { |
|
130
|
0
|
|
|
|
|
0
|
$self->usage_error( '-m, "' . $opt->{'m'} . '", is not readable' ); |
|
131
|
|
|
|
|
|
|
} |
|
132
|
|
|
|
|
|
|
|
|
133
|
1
|
|
|
|
|
5
|
return 1; |
|
134
|
|
|
|
|
|
|
} ## end sub validate |
|
135
|
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
sub execute { |
|
137
|
1
|
|
|
1
|
1
|
5
|
my ( $self, $opt, $args ) = @_; |
|
138
|
|
|
|
|
|
|
|
|
139
|
1
|
50
|
|
|
|
4
|
die "iforest pack requires the Inline::C backend\n" |
|
140
|
|
|
|
|
|
|
unless $Algorithm::Classifier::IsolationForest::HAS_C; |
|
141
|
|
|
|
|
|
|
|
|
142
|
1
|
|
|
|
|
9
|
my $model = Algorithm::Classifier::IsolationForest->load( $opt->{'m'} ); |
|
143
|
1
|
|
|
|
|
2
|
my $nf = $model->{n_features}; |
|
144
|
|
|
|
|
|
|
|
|
145
|
1
|
|
|
|
|
3
|
my @data; |
|
146
|
1
|
|
|
|
|
2
|
my $line = 0; |
|
147
|
1
|
|
|
|
|
9
|
for my $row ( read_file( $opt->{'i'} ) ) { |
|
148
|
7
|
|
|
|
|
216
|
$line++; |
|
149
|
7
|
|
|
|
|
11
|
chomp $row; |
|
150
|
7
|
50
|
|
|
|
16
|
next if $row =~ /^\s*$/; |
|
151
|
7
|
|
|
|
|
16
|
my @f = split /,/, $row, -1; |
|
152
|
7
|
50
|
|
|
|
16
|
die "line $line of '$opt->{i}' has " . scalar(@f) . " columns but model has $nf features\n" |
|
153
|
|
|
|
|
|
|
unless scalar @f == $nf; |
|
154
|
7
|
|
|
|
|
10
|
for my $v (@f) { |
|
155
|
21
|
50
|
|
|
|
37
|
die "line $line of '$opt->{i}' value '$v' is not numeric\n" |
|
156
|
|
|
|
|
|
|
unless looks_like_number($v); |
|
157
|
|
|
|
|
|
|
} |
|
158
|
7
|
|
|
|
|
13
|
push @data, \@f; |
|
159
|
|
|
|
|
|
|
} ## end for my $row ( read_file( $opt->{'i'} ) ) |
|
160
|
1
|
50
|
|
|
|
3
|
die "input '$opt->{i}' contains no rows\n" unless @data; |
|
161
|
|
|
|
|
|
|
|
|
162
|
1
|
|
|
|
|
6
|
my $packed = $model->pack_data( \@data ); |
|
163
|
1
|
|
|
|
|
5
|
_write_packed( $opt->{'o'}, $packed->n_pts, $packed->n_feats, $packed->{packed} ); |
|
164
|
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
printf "wrote %s (%d rows, %d features, %d bytes payload)\n", |
|
166
|
1
|
|
|
|
|
1135
|
$opt->{'o'}, $packed->n_pts, $packed->n_feats, |
|
167
|
|
|
|
|
|
|
$packed->n_pts * $packed->n_feats * 8; |
|
168
|
|
|
|
|
|
|
|
|
169
|
1
|
|
|
|
|
225
|
return 1; |
|
170
|
|
|
|
|
|
|
} ## end sub execute |
|
171
|
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
return 1; |