File Coverage

blib/lib/Algorithm/Classifier/IsolationForest/App/Command/fit.pm
Criterion Covered Total %
statement 59 70 84.2
branch 24 38 63.1
condition 5 12 41.6
subroutine 9 11 81.8
pod 4 5 80.0
total 101 136 74.2


line stmt bran cond sub pod time code
1             package Algorithm::Classifier::IsolationForest::App::Command::fit;
2              
3 22     22   13675 use strict;
  22         37  
  22         681  
4 22     22   78 use warnings;
  22         31  
  22         810  
5 22     22   89 use Algorithm::Classifier::IsolationForest ();
  22         34  
  22         388  
6 22     22   78 use Algorithm::Classifier::IsolationForest::App -command;
  22         108  
  22         254  
7 22     22   6080 use File::Slurp qw(read_file write_file);
  22         38  
  22         1229  
8 22     22   93 use Scalar::Util qw(looks_like_number);
  22         30  
  22         17960  
9              
10             sub opt_spec {
11             return (
12 6     6 1 152230 [ 'i=s', 'CSV to use.', { completion => 'files' } ],
13             [ 'o=s', 'Output JSON file path/name.', { 'default' => 'iforest_model.json', 'completion' => 'files' } ],
14             [ 'p', 'Print the results instead of saving it.' ],
15             [ 'w', 'Overwrite the file if it already exists.' ],
16             [ 's=i', 'Seed int' ],
17             [ 'extended', 'Use EIF instead of IF.' ],
18             [ 'n=i', 'Number of isolation trees in the ensemble' ],
19             [ 'm=i', 'Sub-sample size used to build each tree... max samples' ],
20             [ 'd=i', 'per-tree height limit... if not defined is set to ceil(log2(psi))' ],
21             [
22             'e=f',
23             'How many features take partin each split. 0 behaves like a single-feature (axis) cut; the maximum (n_features - 1) uses every varying feature. undef => maximum. Clamped to [0, n_features - 1] at fit time. May only be used with -e.'
24             ],
25             [
26             'c=f',
27             'Contamination. Expected fraction of anomalies, in (0, 0.5]. When given, fit() learns a score threshold that flags this fraction of the training set, and predict() uses it by default. undef => no learned threshold (predict() falls back to 0.5).'
28             ],
29             [
30             't=s@',
31             'Feature name tag. Pass once per feature (e.g. -t cpu -t mem -t disk); the count must match the number of CSV columns or the command will die.'
32             ],
33             [
34             'voting=s',
35             "Scoring-time aggregation: 'mean' (classic averaged score) or 'majority' (MVIForest: each tree votes against the decision threshold and the label is the majority vote).",
36             { 'default' => 'mean' }
37             ],
38             );
39             } ## end sub opt_spec
40              
41 0     0 1 0 sub abstract { 'Fits the model using the specified data and save it' }
42              
43             sub description {
44 0     0 1 0 'Fits the model using the specified data and save it
45              
46             The input format is expected to be CSV. All columns are used as features;
47             each row becomes one sample. Every row must have the same number of columns
48             and every value must be numeric.
49              
50             Switches to new args are like below...
51              
52             -n -> n_trees
53             -s -> seed
54             -m -> sample_size
55             -e -> extension_level
56             -c -> contamination
57             --voting -> voting
58              
59             ';
60             } ## end sub description
61              
62             sub validate {
63 6     6 0 17 my ( $self, $opt, $args ) = @_;
64              
65 6 50       264 if ( !defined( $opt->{'i'} ) ) {
    50          
    50          
66 0         0 $self->usage_error('-i has not been specified');
67             } elsif ( !-f $opt->{'i'} ) {
68 0         0 $self->usage_error( '-i, "' . $opt->{'i'} . '", is not a file' );
69             } elsif ( !-r $opt->{'i'} ) {
70 0         0 $self->usage_error( '-i, "' . $opt->{'i'} . '", is not readable' );
71             }
72              
73 6 50 33     55 if ( defined( $opt->{'s'} ) && $opt->{'s'} <= 0 ) {
74 0         0 $self->usage_error( '-s, "' . $opt->{'s'} . '", is less than or equal to 0, should be a positive int' );
75             }
76              
77 6 50 33     59 if ( !defined( $opt->{'extended'} ) && defined( $opt->{'e'} ) ) {
78 0         0 $self->usage_error('-e may not be used without --extended');
79             }
80              
81 6 100       20 if ( !$opt->{'p'} ) {
82 4 50 66     136 if ( -e $opt->{'o'} && !$opt->{'w'} ) {
83 0         0 $self->usage_error( '-o,"' . $opt->{'o'} . '", already exists and -w was not specified' );
84             }
85             }
86              
87 6 50 33     24 if ( defined( $opt->{'e'} ) && $opt->{'e'} < 0 ) {
88 0         0 $self->usage_error( '-e, "' . $opt->{'e'} . '", is less than 0... should be a float greater or equal to 0' );
89             }
90              
91 6 100       35 if ( $opt->{'voting'} !~ /\A(?:mean|majority)\z/ ) {
92 1         7 $self->usage_error( '--voting, "' . $opt->{'voting'} . '", must be either mean or majority' );
93             }
94              
95 5         18 return 1;
96             } ## end sub validate
97              
98             sub execute {
99 5     5 1 28 my ( $self, $opt, $args ) = @_;
100              
101 5         12 my $mode = 'axis';
102 5 50       15 if ( $opt->{'extended'} ) {
103 0         0 $mode = 'extended';
104             }
105              
106 5         11 my @data;
107             my $expected_cols;
108              
109 5         12 my $line_int = 1;
110 5         35 foreach my $line ( read_file( $opt->{'i'} ) ) {
111 295         1214 chomp($line);
112 295 50       486 next if $line =~ /^\s*$/;
113              
114 295         519 my @fields = split( /,/, $line, -1 );
115              
116 295 100       510 if ( !defined($expected_cols) ) {
    50          
117 5         9 $expected_cols = scalar @fields;
118 5 50       39 die( 'Line ' . $line_int . ' of "' . $opt->{'i'} . '" has no columns' )
119             if $expected_cols < 1;
120             } elsif ( scalar @fields != $expected_cols ) {
121             die( 'Line '
122             . $line_int . ' of "'
123 0         0 . $opt->{'i'}
124             . '" has '
125             . scalar(@fields)
126             . ' columns but expected '
127             . $expected_cols );
128             }
129              
130 295         341 my $col_int = 1;
131 295         371 for my $field (@fields) {
132             die( 'Line '
133             . $line_int . ' of "'
134 885 50       1366 . $opt->{'i'}
135             . '" value for column '
136             . $col_int . ',"'
137             . $field
138             . '", does not appear to be a number' )
139             unless looks_like_number($field);
140 885         1054 $col_int++;
141             } ## end for my $field (@fields)
142              
143 295         360 push @data, \@fields;
144              
145 295         407 $line_int++;
146             } ## end foreach my $line ( read_file( $opt->{'i'} ) )
147              
148 5 100       34 if ( defined( $opt->{'t'} ) ) {
149 1         1 my $n_tags = scalar @{ $opt->{'t'} };
  1         3  
150 1 50       3 my $n_features = defined($expected_cols) ? $expected_cols : 0;
151 1 50       3 die( 'Number of feature tags (' . $n_tags . ') does not match number of CSV columns (' . $n_features . ')' )
152             unless $n_tags == $n_features;
153             }
154              
155             my $iforest = Algorithm::Classifier::IsolationForest->new(
156             'mode' => $mode,
157             'n_trees' => $opt->{'n'},
158             'seed' => $opt->{'s'},
159             'sample_size' => $opt->{'m'},
160             'extension_level' => $opt->{'e'},
161             'contamination' => $opt->{'c'},
162             'feature_names' => $opt->{'t'},
163 5         73 'voting' => $opt->{'voting'},
164             );
165              
166 5         32 $iforest->fit( \@data );
167              
168 5         40 my $model = $iforest->to_json;
169              
170 5 100       322803 if ( $opt->{'p'} ) {
171 1         93 print $model. "\n";
172 1         726 exit 0;
173             }
174              
175 4         47 write_file( $opt->{'o'}, { 'atomic' => 1 }, $model );
176             } ## end sub execute
177              
178             return 1;