File Coverage

blib/lib/Algorithm/Classifier/IsolationForest/App/Command/fit.pm
Criterion Covered Total %
statement 51 68 75.0
branch 17 36 47.2
condition 4 12 33.3
subroutine 9 11 81.8
pod 4 5 80.0
total 85 132 64.3


line stmt bran cond sub pod time code
1             package Algorithm::Classifier::IsolationForest::App::Command::fit;
2              
3 9     9   5530 use strict;
  9         17  
  9         278  
4 9     9   34 use warnings;
  9         17  
  9         316  
5 9     9   33 use Algorithm::Classifier::IsolationForest ();
  9         15  
  9         148  
6 9     9   26 use Algorithm::Classifier::IsolationForest::App -command;
  9         16  
  9         56  
7 9     9   2539 use File::Slurp qw(read_file write_file);
  9         14  
  9         505  
8 9     9   39 use Scalar::Util qw(looks_like_number);
  9         11  
  9         6878  
9              
10             sub opt_spec {
11             return (
12 1     1 1 25597 [ '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             } ## end sub opt_spec
35              
36 0     0 1 0 sub abstract { 'Fits the model using the specified data and save it' }
37              
38             sub description {
39 0     0 1 0 'Fits the model using the specified data and save it
40              
41             The input format is expected to be CSV. All columns are used as features;
42             each row becomes one sample. Every row must have the same number of columns
43             and every value must be numeric.
44              
45             Switches to new args are like below...
46              
47             -n -> n_trees
48             -s -> seed
49             -m -> sample_size
50             -e -> extension_level
51             -c -> contamination
52              
53             ';
54             } ## end sub description
55              
56             sub validate {
57 1     1 0 3 my ( $self, $opt, $args ) = @_;
58              
59 1 50       35 if ( !defined( $opt->{'i'} ) ) {
    50          
    50          
60 0         0 $self->usage_error('-i has not been specified');
61             } elsif ( !-f $opt->{'i'} ) {
62 0         0 $self->usage_error( '-i, "' . $opt->{'i'} . '", is not a file' );
63             } elsif ( !-r $opt->{'i'} ) {
64 0         0 $self->usage_error( '-i, "' . $opt->{'i'} . '", is not readable' );
65             }
66              
67 1 50 33     10 if ( defined( $opt->{'s'} ) && $opt->{'s'} <= 0 ) {
68 0         0 $self->usage_error( '-s, "' . $opt->{'s'} . '", is less than or equal to 0, should be a positive int' );
69             }
70              
71 1 50 33     6 if ( !defined( $opt->{'extended'} ) && defined( $opt->{'e'} ) ) {
72 0         0 $self->usage_error('-e may not be used without --extended');
73             }
74              
75 1 50       2 if ( !$opt->{'p'} ) {
76 1 50 33     70 if ( -e $opt->{'o'} && !$opt->{'w'} ) {
77 0         0 $self->usage_error( '-o,"' . $opt->{'o'} . '", already exists and -w was not specified' );
78             }
79             }
80              
81 1 50 33     6 if ( defined( $opt->{'e'} ) && $opt->{'e'} < 0 ) {
82 0         0 $self->usage_error( '-e, "' . $opt->{'e'} . '", is less than 0... should be a float greater or equal to 0' );
83             }
84              
85 1         3 return 1;
86             } ## end sub validate
87              
88             sub execute {
89 1     1 1 6 my ( $self, $opt, $args ) = @_;
90              
91 1         3 my $mode = 'axis';
92 1 50       2 if ( $opt->{'extended'} ) {
93 0         0 $mode = 'extended';
94             }
95              
96 1         2 my @data;
97             my $expected_cols;
98              
99 1         2 my $line_int = 1;
100 1         8 foreach my $line ( read_file( $opt->{'i'} ) ) {
101 53         261 chomp($line);
102 53 50       100 next if $line =~ /^\s*$/;
103              
104 53         165 my @fields = split( /,/, $line, -1 );
105              
106 53 100       88 if ( !defined($expected_cols) ) {
    50          
107 1         2 $expected_cols = scalar @fields;
108 1 50       3 die( 'Line ' . $line_int . ' of "' . $opt->{'i'} . '" has no columns' )
109             if $expected_cols < 1;
110             } elsif ( scalar @fields != $expected_cols ) {
111             die( 'Line '
112             . $line_int . ' of "'
113 0         0 . $opt->{'i'}
114             . '" has '
115             . scalar(@fields)
116             . ' columns but expected '
117             . $expected_cols );
118             }
119              
120 53         58 my $col_int = 1;
121 53         65 for my $field (@fields) {
122             die( 'Line '
123             . $line_int . ' of "'
124 159 50       259 . $opt->{'i'}
125             . '" value for column '
126             . $col_int . ',"'
127             . $field
128             . '", does not appear to be a number' )
129             unless looks_like_number($field);
130 159         183 $col_int++;
131             } ## end for my $field (@fields)
132              
133 53         61 push @data, \@fields;
134              
135 53         60 $line_int++;
136             } ## end foreach my $line ( read_file( $opt->{'i'} ) )
137              
138 1 50       6 if ( defined( $opt->{'t'} ) ) {
139 0         0 my $n_tags = scalar @{ $opt->{'t'} };
  0         0  
140 0 0       0 my $n_features = defined($expected_cols) ? $expected_cols : 0;
141 0 0       0 die( 'Number of feature tags (' . $n_tags . ') does not match number of CSV columns (' . $n_features . ')' )
142             unless $n_tags == $n_features;
143             }
144              
145             my $iforest = Algorithm::Classifier::IsolationForest->new(
146             'mode' => $mode,
147             'n_trees' => $opt->{'n'},
148             'seed' => $opt->{'s'},
149             'sample_size' => $opt->{'m'},
150             'extension_level' => $opt->{'e'},
151             'contamination' => $opt->{'c'},
152 1         14 'feature_names' => $opt->{'t'},
153             );
154              
155 1         7 $iforest->fit( \@data );
156              
157 1         4 my $model = $iforest->to_json;
158              
159 1 50       19641 if ( $opt->{'p'} ) {
160 0         0 print $model. "\n";
161 0         0 exit 0;
162             }
163              
164 1         7 write_file( $opt->{'o'}, { 'atomic' => 1 }, $model );
165             } ## end sub execute
166              
167             return 1;