File Coverage

blib/lib/Algorithm/Classifier/IsolationForest/App/Command/csv2plot.pm
Criterion Covered Total %
statement 21 67 31.3
branch 0 40 0.0
condition 0 33 0.0
subroutine 7 12 58.3
pod 4 5 80.0
total 32 157 20.3


line stmt bran cond sub pod time code
1             package Algorithm::Classifier::IsolationForest::App::Command::csv2plot;
2              
3 9     9   5876 use strict;
  9         17  
  9         273  
4 9     9   32 use warnings;
  9         15  
  9         302  
5 9     9   36 use Algorithm::Classifier::IsolationForest ();
  9         15  
  9         159  
6 9     9   26 use Algorithm::Classifier::IsolationForest::App -command;
  9         16  
  9         59  
7 9     9   2364 use File::Slurp qw(read_file write_file);
  9         15  
  9         524  
8 9     9   47 use File::Spec ();
  9         14  
  9         254  
9 9     9   35 use File::Temp qw(tempfile);
  9         59  
  9         6487  
10              
11             sub opt_spec {
12             return (
13 0     0 1   [ 'i=s', 'Input CSV for processing.', { 'completion' => 'files' } ],
14             [ 'o=s', 'PNG file to output to. Default: plot.png', { 'default' => 'plot.png', 'completion' => 'files' } ],
15             [ 'w', 'If the file specified via -o exists, over write it.' ],
16             [
17             'p=s',
18             'Type of plot. Default: auto',
19             { 'default' => 'auto', completion => [ 'auto', '2heat', '3range', '3binary' ] }
20             ],
21             [ 'print', 'Print what would be used with gnuplot instead of calling gnuplot' ],
22             [ 'open', 'Call xdg-open to open the generated graph.' ],
23             [ 'small-points', 'Use smaller points (ps 0.8) for dense 3range datasets.' ],
24             );
25             } ## end sub opt_spec
26              
27 0     0 1   sub abstract { 'Plot the CSV data used with iforest via gnuplot.' }
28              
29             sub description {
30 0     0 1   'Plot the CSV data used with iforest via gnuplot.
31              
32             Plot types are as below.
33              
34             auto: If there are two columns, 2heat. If there are 4 or more columns, 3range.
35             2heat: Use column 1 and 2 to generate a splatter plot over a heat map.
36             3range: Use columns 1 and 2 for x/y, and the second-to-last column for the
37             score gradient. Suitable for predict -d output (any number of features).
38             3binary: Use columns 1 and 2 for x/y, and the last column for normal/abnormal.
39             Suitable for predict -d output or gblob output.
40              
41             3range and 3binary require data outputted from predict with the -d flag.
42             For N-dimensional data, columns 1 and 2 are always used for the x/y axes.
43             ';
44             } ## end sub description
45              
46             sub validate {
47 0     0 0   my ( $self, $opt, $args ) = @_;
48              
49 0 0         if ( !defined( $opt->{'i'} ) ) {
    0          
    0          
50 0           $self->usage_error('-i has not been specified for a file to process');
51             } elsif ( !-f $opt->{'i'} ) {
52 0           $self->usage_error( '-i, "' . $opt->{'i'} . '", is not a file or does not exist' );
53             } elsif ( !-r $opt->{'i'} ) {
54 0           $self->usage_error( '-i, "' . $opt->{'i'} . '", is not readable' );
55             }
56              
57 0 0 0       if ( -e $opt->{'o'} && !$opt->{'w'} ) {
    0 0        
    0 0        
58 0           $self->usage_error( '-o, "' . $opt->{'o'} . '", already exists and -w is not given' );
59             } elsif ( -e $opt->{'o'} && !-f $opt->{'o'} ) {
60 0           $self->usage_error( '-o, "' . $opt->{'o'} . '", already exists and -w given but file is not a file' );
61             } elsif ( -e $opt->{'o'} && !-w $opt->{'o'} ) {
62 0           $self->usage_error( '-o, "' . $opt->{'o'} . '", already exists and -w given but file is not writable' );
63             }
64              
65 0 0 0       if ( ( $opt->{'p'} ne 'auto' )
      0        
      0        
66             && ( $opt->{'p'} ne '2heat' )
67             && ( $opt->{'p'} ne '3range' )
68             && ( $opt->{'p'} ne '3binary' ) )
69             {
70 0           $self->usage_error( '-p, "' . $opt->{'p'} . '", is not set to auto, 2heat, 3range, or 3binary' );
71             }
72              
73 0           return 1;
74             } ## end sub validate
75              
76             sub execute {
77 0     0 1   my ( $self, $opt, $args ) = @_;
78              
79 0           my @raw_csv = read_file( $opt->{'i'} );
80 0           my @first_fields = split( /,/, $raw_csv[0] );
81 0           my $ncols = scalar @first_fields;
82              
83 0 0 0       if ( $opt->{'p'} eq 'auto' && $ncols >= 4 ) {
    0 0        
    0 0        
    0 0        
    0 0        
    0          
84 0           $opt->{'p'} = '3range';
85             } elsif ( $opt->{'p'} eq 'auto' && $ncols >= 2 ) {
86 0           $opt->{'p'} = '2heat';
87             } elsif ( $opt->{'p'} eq 'auto' ) {
88 0           die('-p is set to auto and the specified CSV does not have enough columns');
89             } elsif ( $opt->{'p'} eq '2heat' && $ncols < 2 ) {
90 0           die('2heat specified but there is no column for y');
91             } elsif ( $opt->{'p'} eq '3range' && $ncols < 3 ) {
92 0           die('3range specified but there is no column for score');
93             } elsif ( $opt->{'p'} eq '3binary' && $ncols < 3 ) {
94 0           die('3binary specified but there is no column for truth');
95             }
96              
97             # For predict -d output: score is the second-to-last column, prediction is last.
98 0           my $score_col = $ncols - 1;
99 0           my $pred_col = $ncols;
100              
101 0           $opt->{'i'} = File::Spec->rel2abs( $opt->{'i'} );
102 0           $opt->{'o'} = File::Spec->rel2abs( $opt->{'o'} );
103              
104 0           my ( $tempfh, $tempfile ) = tempfile( 'UNLINK' => 1 );
105              
106 0 0         my $ps = $opt->{'small_points'} ? '0.8' : '1.5';
107              
108             my $gnu_plot_stuff = 'set terminal pngcairo size 1200,900 font "Sans,11"
109 0           set output "' . $opt->{'o'} . '"
110              
111             set datafile separator ","
112             set key autotitle columnhead
113              
114             set grid lc "gray70" lw 0.5 dt 2
115              
116             ';
117              
118 0 0         if ( $opt->{'p'} eq '3range' ) {
    0          
    0          
119             $gnu_plot_stuff = $gnu_plot_stuff . '
120             set palette defined (0 "#4575b4", 0.5 "#ffffbf", 1 "#d73027")
121             set cblabel "outlier score"
122 0           plot "' . $opt->{'i'} . '" using 1:2:' . $score_col . ' with points pt 7 ps ' . $ps . ' palette title ""
123             ';
124             } elsif ( $opt->{'p'} eq '3binary' ) {
125             $gnu_plot_stuff
126             = $gnu_plot_stuff
127             . 'plot "'
128             . $opt->{'i'}
129             . '" using 1:($'
130             . $pred_col
131             . '==0 ? $2 : 1/0) with points pt 7 ps 1.2 lc rgb "#4575b4" title "normal", \
132             "'
133 0           . $opt->{'i'}
134             . '" using 1:($'
135             . $pred_col
136             . '==1 ? $2 : 1/0) with points pt 13 ps 2.0 lc rgb "#d73027" title "abnormal"
137             ';
138             } elsif ( $opt->{'p'} eq '2heat' ) {
139             $gnu_plot_stuff = $gnu_plot_stuff . '
140             unset key
141             set view map
142             set palette cubehelix negative
143              
144             set dgrid 25,25 gauss kdensity 3, 3
145              
146 0           splot "' . $opt->{'i'} . '" using 1:2:(1) with pm3d, \
147             "" using 1:2:(1) with points lc "black" pt 7 ps 0.6 nogrid
148             ';
149             } ## end elsif ( $opt->{'p'} eq '2heat' )
150              
151 0 0         if ( $opt->{'print'} ) {
152 0           print $gnu_plot_stuff;
153 0           exit 0;
154             }
155              
156 0           write_file( $tempfile, $gnu_plot_stuff );
157              
158 0           system( 'gnuplot', $tempfile );
159 0 0         if ( $? != 0 ) {
160 0           die( '"gnuplot ' . $tempfile . '" exited non-zero' );
161             }
162              
163 0 0         if ( $opt->{'open'} ) {
164 0           system( 'xdg-open', $opt->{'o'} );
165             }
166              
167             } ## end sub execute
168              
169             return 1;