| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest::App::Command::info; |
|
2
|
|
|
|
|
|
|
|
|
3
|
9
|
|
|
9
|
|
5455
|
use strict; |
|
|
9
|
|
|
|
|
15
|
|
|
|
9
|
|
|
|
|
259
|
|
|
4
|
9
|
|
|
9
|
|
29
|
use warnings; |
|
|
9
|
|
|
|
|
13
|
|
|
|
9
|
|
|
|
|
339
|
|
|
5
|
9
|
|
|
9
|
|
58
|
use Algorithm::Classifier::IsolationForest (); |
|
|
9
|
|
|
|
|
14
|
|
|
|
9
|
|
|
|
|
201
|
|
|
6
|
9
|
|
|
9
|
|
30
|
use Algorithm::Classifier::IsolationForest::App -command; |
|
|
9
|
|
|
|
|
12
|
|
|
|
9
|
|
|
|
|
63
|
|
|
7
|
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
sub opt_spec { |
|
9
|
|
|
|
|
|
|
return ( |
|
10
|
|
|
|
|
|
|
[ |
|
11
|
2
|
|
|
2
|
1
|
49631
|
'm=s', |
|
12
|
|
|
|
|
|
|
'Input model JSON file path/name.', |
|
13
|
|
|
|
|
|
|
{ 'default' => 'iforest_model.json', 'completion' => 'files' } |
|
14
|
|
|
|
|
|
|
], |
|
15
|
|
|
|
|
|
|
[ 'json', 'Emit machine-readable JSON instead of the text table.' ], |
|
16
|
|
|
|
|
|
|
); |
|
17
|
|
|
|
|
|
|
} ## end sub opt_spec |
|
18
|
|
|
|
|
|
|
|
|
19
|
0
|
|
|
0
|
1
|
0
|
sub abstract { 'Show the constructor params, fit-time metadata, and tree stats of a saved model' } |
|
20
|
|
|
|
|
|
|
|
|
21
|
|
|
|
|
|
|
sub description { |
|
22
|
0
|
|
|
0
|
1
|
0
|
'Loads a saved Algorithm::Classifier::IsolationForest model and prints the |
|
23
|
|
|
|
|
|
|
constructor params, fit-time metadata, and a handful of derived tree |
|
24
|
|
|
|
|
|
|
statistics (count, average/max depth, total nodes). |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
Use --json for a machine-readable dump suitable for piping into jq. |
|
27
|
|
|
|
|
|
|
' |
|
28
|
|
|
|
|
|
|
} |
|
29
|
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
sub validate { |
|
31
|
2
|
|
|
2
|
0
|
4
|
my ( $self, $opt, $args ) = @_; |
|
32
|
|
|
|
|
|
|
|
|
33
|
2
|
50
|
|
|
|
97
|
if ( !-f $opt->{'m'} ) { |
|
|
|
50
|
|
|
|
|
|
|
34
|
0
|
|
|
|
|
0
|
$self->usage_error( '-m, "' . $opt->{'m'} . '", is not a file or does not exist' ); |
|
35
|
|
|
|
|
|
|
} elsif ( !-r $opt->{'m'} ) { |
|
36
|
0
|
|
|
|
|
0
|
$self->usage_error( '-m, "' . $opt->{'m'} . '", is not readable' ); |
|
37
|
|
|
|
|
|
|
} |
|
38
|
2
|
|
|
|
|
9
|
return 1; |
|
39
|
|
|
|
|
|
|
} ## end sub validate |
|
40
|
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
# Tree-shape stats are derived once at load time. Each tree is a |
|
42
|
|
|
|
|
|
|
# nested arrayref structure -- leaf [0, size] or interior [1, ...] / |
|
43
|
|
|
|
|
|
|
# [2, ...] with children at fixed slots. |
|
44
|
|
|
|
|
|
|
sub _walk_tree { |
|
45
|
984
|
|
|
984
|
|
1352
|
my ( $node, $depth, $acc ) = @_; |
|
46
|
984
|
|
|
|
|
1113
|
$acc->{nodes}++; |
|
47
|
984
|
100
|
|
|
|
1460
|
if ( $node->[0] == 0 ) { # leaf |
|
48
|
522
|
|
|
|
|
599
|
$acc->{leaves}++; |
|
49
|
522
|
100
|
|
|
|
739
|
$acc->{max_depth} = $depth if $depth > $acc->{max_depth}; |
|
50
|
522
|
|
|
|
|
608
|
$acc->{depth_sum} += $depth; |
|
51
|
522
|
|
|
|
|
736
|
return; |
|
52
|
|
|
|
|
|
|
} |
|
53
|
|
|
|
|
|
|
# Axis interior nodes have children at slots 3,4; oblique at 4,5. |
|
54
|
462
|
50
|
|
|
|
699
|
my ( $li, $ri ) = $node->[0] == 1 ? ( 3, 4 ) : ( 4, 5 ); |
|
55
|
462
|
|
|
|
|
861
|
_walk_tree( $node->[$li], $depth + 1, $acc ); |
|
56
|
462
|
|
|
|
|
674
|
_walk_tree( $node->[$ri], $depth + 1, $acc ); |
|
57
|
|
|
|
|
|
|
} ## end sub _walk_tree |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
sub _tree_stats { |
|
60
|
2
|
|
|
2
|
|
4
|
my ($trees) = @_; |
|
61
|
2
|
|
|
|
|
11
|
my $acc = { nodes => 0, leaves => 0, max_depth => 0, depth_sum => 0 }; |
|
62
|
2
|
|
|
|
|
10
|
_walk_tree( $_, 0, $acc ) for @$trees; |
|
63
|
2
|
|
|
|
|
5
|
return $acc; |
|
64
|
|
|
|
|
|
|
} |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
sub execute { |
|
67
|
2
|
|
|
2
|
1
|
13
|
my ( $self, $opt, $args ) = @_; |
|
68
|
|
|
|
|
|
|
|
|
69
|
2
|
|
|
|
|
21
|
my $model = Algorithm::Classifier::IsolationForest->load( $opt->{'m'} ); |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
# Tree stats are not stored on the model -- they're cheap to derive. |
|
72
|
2
|
|
|
|
|
10
|
my $stats = _tree_stats( $model->{trees} ); |
|
73
|
2
|
|
|
|
|
4
|
my $n_trees = scalar @{ $model->{trees} }; |
|
|
2
|
|
|
|
|
6
|
|
|
74
|
2
|
50
|
|
|
|
9
|
my $avg_depth = $stats->{leaves} ? $stats->{depth_sum} / $stats->{leaves} : 0; |
|
75
|
2
|
50
|
|
|
|
7
|
my $avg_nodes = $n_trees ? $stats->{nodes} / $n_trees : 0; |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
my %info = ( |
|
78
|
|
|
|
|
|
|
'file' => $opt->{'m'}, |
|
79
|
|
|
|
|
|
|
'mode' => $model->{mode}, |
|
80
|
|
|
|
|
|
|
'n_trees' => $n_trees, |
|
81
|
|
|
|
|
|
|
'n_features' => $model->{n_features}, |
|
82
|
|
|
|
|
|
|
'sample_size' => $model->{sample_size}, |
|
83
|
|
|
|
|
|
|
'psi_used' => $model->{psi_used}, |
|
84
|
|
|
|
|
|
|
'c_psi' => $model->{c_psi}, |
|
85
|
|
|
|
|
|
|
'max_depth_used' => $model->{max_depth_used}, |
|
86
|
|
|
|
|
|
|
'extension_level' => $model->{extension_level_used}, |
|
87
|
|
|
|
|
|
|
'contamination' => $model->{contamination}, |
|
88
|
|
|
|
|
|
|
'threshold' => $model->{threshold}, |
|
89
|
|
|
|
|
|
|
'tree_total_nodes' => $stats->{nodes}, |
|
90
|
|
|
|
|
|
|
'tree_total_leaves' => $stats->{leaves}, |
|
91
|
|
|
|
|
|
|
'tree_max_depth' => $stats->{max_depth}, |
|
92
|
2
|
|
|
|
|
28
|
'tree_avg_depth' => $avg_depth, |
|
93
|
|
|
|
|
|
|
'tree_avg_nodes' => $avg_nodes, |
|
94
|
|
|
|
|
|
|
); |
|
95
|
|
|
|
|
|
|
|
|
96
|
2
|
100
|
|
|
|
6
|
if ( $opt->{'json'} ) { |
|
97
|
1
|
|
|
|
|
14
|
require JSON::PP; |
|
98
|
1
|
|
|
|
|
17
|
print JSON::PP->new->canonical(1)->pretty->encode( \%info ); |
|
99
|
1
|
|
|
|
|
819
|
return 1; |
|
100
|
|
|
|
|
|
|
} |
|
101
|
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
# Text-table output, in a stable order, with undef shown as "(unset)". |
|
103
|
1
|
|
|
|
|
5
|
my @order = qw( |
|
104
|
|
|
|
|
|
|
file mode n_trees n_features sample_size psi_used c_psi |
|
105
|
|
|
|
|
|
|
max_depth_used extension_level contamination threshold |
|
106
|
|
|
|
|
|
|
tree_total_nodes tree_total_leaves tree_max_depth |
|
107
|
|
|
|
|
|
|
tree_avg_depth tree_avg_nodes |
|
108
|
|
|
|
|
|
|
); |
|
109
|
|
|
|
|
|
|
|
|
110
|
1
|
|
|
|
|
2
|
for my $k (@order) { |
|
111
|
16
|
|
|
|
|
25
|
my $v = $info{$k}; |
|
112
|
16
|
100
|
|
|
|
24
|
$v = '(unset)' unless defined $v; |
|
113
|
|
|
|
|
|
|
# Pretty-print floats with a couple of decimals; leave ints raw. |
|
114
|
16
|
100
|
66
|
|
|
70
|
$v = sprintf( '%.4f', $v ) |
|
|
|
|
66
|
|
|
|
|
|
115
|
|
|
|
|
|
|
if defined $v |
|
116
|
|
|
|
|
|
|
&& $v =~ /^-?\d+\.\d+/ |
|
117
|
|
|
|
|
|
|
&& $k !~ /^tree_total_/; |
|
118
|
16
|
|
|
|
|
62
|
printf " %-20s %s\n", $k, $v; |
|
119
|
|
|
|
|
|
|
} ## end for my $k (@order) |
|
120
|
1
|
|
|
|
|
216
|
return 1; |
|
121
|
|
|
|
|
|
|
} ## end sub execute |
|
122
|
|
|
|
|
|
|
|
|
123
|
|
|
|
|
|
|
return 1; |