File Coverage

blib/lib/Algorithm/Classifier/IsolationForest/App/Command/info.pm
Criterion Covered Total %
statement 55 59 93.2
branch 21 26 80.7
condition 6 9 66.6
subroutine 9 11 81.8
pod 4 5 80.0
total 95 110 86.3


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;