| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package Algorithm::Classifier::IsolationForest::App::Command::info; |
|
2
|
|
|
|
|
|
|
|
|
3
|
22
|
|
|
22
|
|
13321
|
use strict; |
|
|
22
|
|
|
|
|
35
|
|
|
|
22
|
|
|
|
|
639
|
|
|
4
|
22
|
|
|
22
|
|
70
|
use warnings; |
|
|
22
|
|
|
|
|
30
|
|
|
|
22
|
|
|
|
|
753
|
|
|
5
|
22
|
|
|
22
|
|
111
|
use Algorithm::Classifier::IsolationForest (); |
|
|
22
|
|
|
|
|
32
|
|
|
|
22
|
|
|
|
|
442
|
|
|
6
|
22
|
|
|
22
|
|
80
|
use Algorithm::Classifier::IsolationForest::App -command; |
|
|
22
|
|
|
|
|
54
|
|
|
|
22
|
|
|
|
|
191
|
|
|
7
|
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
sub opt_spec { |
|
9
|
|
|
|
|
|
|
return ( |
|
10
|
|
|
|
|
|
|
[ |
|
11
|
6
|
|
|
6
|
1
|
147883
|
'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
|
6
|
|
|
6
|
0
|
43
|
my ( $self, $opt, $args ) = @_; |
|
32
|
|
|
|
|
|
|
|
|
33
|
6
|
50
|
|
|
|
216
|
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
|
6
|
|
|
|
|
22
|
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
|
5302
|
|
|
5302
|
|
7114
|
my ( $node, $depth, $acc ) = @_; |
|
46
|
5302
|
|
|
|
|
5933
|
$acc->{nodes}++; |
|
47
|
5302
|
100
|
|
|
|
7864
|
if ( $node->[0] == 0 ) { # leaf |
|
48
|
2776
|
|
|
|
|
3318
|
$acc->{leaves}++; |
|
49
|
2776
|
100
|
|
|
|
3950
|
$acc->{max_depth} = $depth if $depth > $acc->{max_depth}; |
|
50
|
2776
|
|
|
|
|
3241
|
$acc->{depth_sum} += $depth; |
|
51
|
2776
|
|
|
|
|
3929
|
return; |
|
52
|
|
|
|
|
|
|
} |
|
53
|
|
|
|
|
|
|
# Axis interior nodes have children at slots 3,4; oblique at 4,5. |
|
54
|
2526
|
50
|
|
|
|
3737
|
my ( $li, $ri ) = $node->[0] == 1 ? ( 3, 4 ) : ( 4, 5 ); |
|
55
|
2526
|
|
|
|
|
4451
|
_walk_tree( $node->[$li], $depth + 1, $acc ); |
|
56
|
2526
|
|
|
|
|
3482
|
_walk_tree( $node->[$ri], $depth + 1, $acc ); |
|
57
|
|
|
|
|
|
|
} ## end sub _walk_tree |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
sub _tree_stats { |
|
60
|
6
|
|
|
6
|
|
15
|
my ($trees) = @_; |
|
61
|
6
|
|
|
|
|
38
|
my $acc = { nodes => 0, leaves => 0, max_depth => 0, depth_sum => 0 }; |
|
62
|
6
|
|
|
|
|
39
|
_walk_tree( $_, 0, $acc ) for @$trees; |
|
63
|
6
|
|
|
|
|
33
|
return $acc; |
|
64
|
|
|
|
|
|
|
} |
|
65
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
sub execute { |
|
67
|
6
|
|
|
6
|
1
|
52
|
my ( $self, $opt, $args ) = @_; |
|
68
|
|
|
|
|
|
|
|
|
69
|
6
|
|
|
|
|
59
|
my $model = Algorithm::Classifier::IsolationForest->load( $opt->{'m'} ); |
|
70
|
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
# Tree stats are not stored on the model -- they're cheap to derive. |
|
72
|
6
|
|
|
|
|
72
|
my $stats = _tree_stats( $model->{trees} ); |
|
73
|
6
|
|
|
|
|
15
|
my $n_trees = scalar @{ $model->{trees} }; |
|
|
6
|
|
|
|
|
63
|
|
|
74
|
6
|
50
|
|
|
|
70
|
my $avg_depth = $stats->{leaves} ? $stats->{depth_sum} / $stats->{leaves} : 0; |
|
75
|
6
|
50
|
|
|
|
38
|
my $avg_nodes = $n_trees ? $stats->{nodes} / $n_trees : 0; |
|
76
|
|
|
|
|
|
|
|
|
77
|
|
|
|
|
|
|
# Feature-name tags are stored as an arrayref (via -t at fit time). |
|
78
|
6
|
|
|
|
|
35
|
my $tags = $model->{feature_names}; |
|
79
|
6
|
100
|
66
|
|
|
65
|
my $tagged = ( ref $tags eq 'ARRAY' && @$tags ) ? 1 : 0; |
|
80
|
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
my %info = ( |
|
82
|
|
|
|
|
|
|
'file' => $opt->{'m'}, |
|
83
|
|
|
|
|
|
|
'mode' => $model->{mode}, |
|
84
|
|
|
|
|
|
|
'voting' => $model->{voting}, |
|
85
|
|
|
|
|
|
|
'tagged' => $tagged, |
|
86
|
|
|
|
|
|
|
'feature_names' => $tagged ? $tags : undef, |
|
87
|
|
|
|
|
|
|
'n_trees' => $n_trees, |
|
88
|
|
|
|
|
|
|
'n_features' => $model->{n_features}, |
|
89
|
|
|
|
|
|
|
'sample_size' => $model->{sample_size}, |
|
90
|
|
|
|
|
|
|
'psi_used' => $model->{psi_used}, |
|
91
|
|
|
|
|
|
|
'c_psi' => $model->{c_psi}, |
|
92
|
|
|
|
|
|
|
'max_depth_used' => $model->{max_depth_used}, |
|
93
|
|
|
|
|
|
|
'extension_level' => $model->{extension_level_used}, |
|
94
|
|
|
|
|
|
|
'contamination' => $model->{contamination}, |
|
95
|
|
|
|
|
|
|
'threshold' => $model->{threshold}, |
|
96
|
|
|
|
|
|
|
'tree_total_nodes' => $stats->{nodes}, |
|
97
|
|
|
|
|
|
|
'tree_total_leaves' => $stats->{leaves}, |
|
98
|
|
|
|
|
|
|
'tree_max_depth' => $stats->{max_depth}, |
|
99
|
6
|
100
|
|
|
|
198
|
'tree_avg_depth' => $avg_depth, |
|
100
|
|
|
|
|
|
|
'tree_avg_nodes' => $avg_nodes, |
|
101
|
|
|
|
|
|
|
); |
|
102
|
|
|
|
|
|
|
|
|
103
|
6
|
100
|
|
|
|
26
|
if ( $opt->{'json'} ) { |
|
104
|
2
|
|
|
|
|
31
|
require JSON::PP; |
|
105
|
2
|
|
|
|
|
20
|
print JSON::PP->new->canonical(1)->pretty->encode( \%info ); |
|
106
|
2
|
|
|
|
|
2377
|
return 1; |
|
107
|
|
|
|
|
|
|
} |
|
108
|
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
# Text-table output, in a stable order, with undef shown as "(unset)". |
|
110
|
|
|
|
|
|
|
# feature_names is an arrayref -- rendered separately below. |
|
111
|
4
|
|
|
|
|
49
|
my @order = qw( |
|
112
|
|
|
|
|
|
|
file mode voting tagged n_trees n_features sample_size psi_used c_psi |
|
113
|
|
|
|
|
|
|
max_depth_used extension_level contamination threshold |
|
114
|
|
|
|
|
|
|
tree_total_nodes tree_total_leaves tree_max_depth |
|
115
|
|
|
|
|
|
|
tree_avg_depth tree_avg_nodes |
|
116
|
|
|
|
|
|
|
); |
|
117
|
|
|
|
|
|
|
|
|
118
|
4
|
|
|
|
|
9
|
for my $k (@order) { |
|
119
|
72
|
|
|
|
|
118
|
my $v = $info{$k}; |
|
120
|
72
|
100
|
|
|
|
104
|
$v = '(unset)' unless defined $v; |
|
121
|
|
|
|
|
|
|
# Pretty-print floats with a couple of decimals; leave ints raw. |
|
122
|
72
|
100
|
66
|
|
|
326
|
$v = sprintf( '%.4f', $v ) |
|
|
|
|
66
|
|
|
|
|
|
123
|
|
|
|
|
|
|
if defined $v |
|
124
|
|
|
|
|
|
|
&& $v =~ /^-?\d+\.\d+/ |
|
125
|
|
|
|
|
|
|
&& $k !~ /^tree_total_/; |
|
126
|
72
|
|
|
|
|
249
|
printf " %-20s %s\n", $k, $v; |
|
127
|
|
|
|
|
|
|
} ## end for my $k (@order) |
|
128
|
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
# Feature-name tags, one per line, in stored (positional) order. |
|
130
|
4
|
100
|
|
|
|
13
|
if ($tagged) { |
|
131
|
1
|
|
|
|
|
5
|
printf " %-20s %s\n", 'feature_names', join( ', ', @$tags ); |
|
132
|
1
|
|
|
|
|
4
|
for my $i ( 0 .. $#$tags ) { |
|
133
|
3
|
|
|
|
|
7
|
printf " [%d] %s\n", $i, $tags->[$i]; |
|
134
|
|
|
|
|
|
|
} |
|
135
|
|
|
|
|
|
|
} |
|
136
|
4
|
|
|
|
|
1148
|
return 1; |
|
137
|
|
|
|
|
|
|
} ## end sub execute |
|
138
|
|
|
|
|
|
|
|
|
139
|
|
|
|
|
|
|
return 1; |