File Coverage

blib/lib/Quantum/Superpositions/Lazy/Statistics.pm
Criterion Covered Total %
statement 35 37 94.5
branch n/a
condition n/a
subroutine 13 14 92.8
pod 4 4 100.0
total 52 55 94.5


line stmt bran cond sub pod time code
1             package Quantum::Superpositions::Lazy::Statistics;
2             $Quantum::Superpositions::Lazy::Statistics::VERSION = '1.12';
3 15     15   182 use v5.24;
  15         72  
4 15     15   69 use warnings;
  15         35  
  15         339  
5 15     15   66 use Moo;
  15         35  
  15         80  
6 15     15   4418 use Quantum::Superpositions::Lazy::Role::Collapsible;
  15         34  
  15         420  
7 15     15   76 use Quantum::Superpositions::Lazy::State;
  15         37  
  15         534  
8 15     15   84 use Types::Standard qw(ArrayRef ConsumerOf InstanceOf);
  15         45  
  15         124  
9 15     15   17070 use Sort::Key qw(keysort nkeysort);
  15         39376  
  15         1173  
10 15     15   114 use List::Util qw(sum0);
  15         31  
  15         968  
11              
12             # This approximation should be well within the range of 32 bit
13             # floating point values - 6 digits (IEEE 754)
14 15     15   106 use constant HALF_APPROX => "0.500000";
  15         33  
  15         8390  
15              
16             sub transform_states
17             {
18             my ($items, $transformer) = @_;
19             my @transformed = map {
20             $_->clone_with(value => $transformer)
21             } @$items;
22              
23             return \@transformed;
24             }
25              
26             sub weight_to_probability
27             {
28             my ($item, $weight_sum) = @_;
29             return $item->clone_with(
30             weight => sub { shift() / $weight_sum }
31             ) if defined $item;
32              
33             return $item;
34             }
35              
36             sub weighted_mean
37             {
38             my ($list_ref, $weight_sum) = @_;
39             $weight_sum = sum0 map { $_->weight }
40             $list_ref->@*
41             unless defined $weight_sum;
42              
43             if ($weight_sum > 0) {
44             my @values = map { $_->value * $_->weight / $weight_sum } $list_ref->@*;
45             return sum0 @values;
46             }
47             return undef;
48             }
49              
50             # The sorting order is irrelevant here
51             sub weighted_median
52             {
53             my ($sorted_list_ref, $average) = @_;
54             $average //= 0;
55              
56             my $approx_half = sub {
57             my ($value) = @_;
58              
59             return HALF_APPROX eq substr(($value . (0 x length HALF_APPROX)), 0, length HALF_APPROX);
60             };
61              
62             my $running_sum = 0;
63             my $last_el;
64             my @found;
65              
66             for my $el (@{$sorted_list_ref}) {
67             $running_sum += $el->weight;
68              
69             if ($running_sum > 0.5) {
70             push @found, $last_el if $approx_half->($running_sum - $el->weight);
71             push @found, $el;
72             last;
73             }
74              
75             $last_el = $el;
76             }
77              
78             # if we're allowed to average the result, do that
79             return weighted_mean(\@found)
80             if $average;
81              
82             # get the lowest weight value if we can't average the two
83             # be biased towards the first value
84             return $found[1]->weight < $found[0]->weight ? $found[1]->value : $found[0]->value
85             if @found == 2;
86              
87             return @found > 0 ? $found[0]->value : undef;
88             }
89              
90             # CAUTION: float == comparison inside. Will only work for elements
91             # that were obtained in a similar fasion
92             sub find_border_elements
93             {
94             my ($sorted) = @_;
95             my @found;
96             for my $state (@$sorted) {
97             push @found, $state
98             if @found == 0 || $found[-1]->weight == $state->weight;
99             }
100              
101             return \@found;
102             }
103              
104             my %options = (
105             is => "ro",
106             lazy => 1,
107             init_arg => undef,
108             );
109              
110 15     15   121 use namespace::clean;
  15         41  
  15         128  
111              
112             our $implementation = __PACKAGE__;
113              
114             has "parent" => (
115             is => "ro",
116             isa => ConsumerOf ["Quantum::Superpositions::Lazy::Role::Collapsible"],
117             weak_ref => 1,
118             );
119              
120             # Sorted in ascending order
121             has "sorted_by_probability" => (
122             %options,
123             isa => ArrayRef [InstanceOf ["Quantum::Superpositions::Lazy::State"]],
124             default => sub {
125             my ($self) = @_;
126              
127             [
128             map {
129             weight_to_probability($_, $self->parent->weight_sum)
130             }
131             nkeysort {
132             $_->weight
133             }
134             $self->parent->states->@*
135             ];
136             },
137             );
138              
139             # Sorted in ascending order
140             # (we use sorted_by_probability to avoid copying states twice in weight_to_probability)
141             has "sorted_by_value_str" => (
142             %options,
143             isa => ArrayRef [InstanceOf ["Quantum::Superpositions::Lazy::State"]],
144             default => sub {
145             my ($self) = @_;
146              
147             [
148             keysort { $_->value }
149             $self->sorted_by_probability->@*
150             ];
151             },
152             );
153              
154             has "sorted_by_value_num" => (
155             %options,
156             isa => ArrayRef [InstanceOf ["Quantum::Superpositions::Lazy::State"]],
157             default => sub {
158             my ($self) = @_;
159              
160             [
161             nkeysort { $_->value }
162             $self->sorted_by_probability->@*
163             ];
164             },
165             );
166              
167             # Other consumer indicator
168             has "most_probable" => (
169             %options,
170             isa => InstanceOf ["Quantum::Superpositions::Lazy::Superposition"],
171             default => sub {
172             my ($self) = @_;
173              
174             my @sorted = reverse $self->sorted_by_probability->@*;
175             return Quantum::Superpositions::Lazy::Superposition->new(
176             states => find_border_elements(\@sorted)
177             );
178             },
179             );
180              
181             has "least_probable" => (
182             %options,
183             isa => InstanceOf ["Quantum::Superpositions::Lazy::Superposition"],
184             default => sub {
185             my ($self) = @_;
186              
187             my $sorted = $self->sorted_by_probability;
188             return Quantum::Superpositions::Lazy::Superposition->new(
189             states => find_border_elements($sorted)
190             );
191             },
192             );
193              
194             has "median_str" => (
195             %options,
196             default => sub {
197             my ($self) = @_;
198              
199             weighted_median($self->sorted_by_value_str);
200             },
201             );
202              
203             has "median_num" => (
204             %options,
205             default => sub {
206             my ($self) = @_;
207              
208             weighted_median($self->sorted_by_value_num, 1);
209             },
210             );
211              
212             has "mean" => (
213             %options,
214             default => sub {
215             my ($self) = @_;
216              
217             # since the mean won't return a state, we're free not
218             # to make copies of the states.
219             weighted_mean($self->parent->states, $self->parent->weight_sum);
220             },
221             );
222              
223             has "variance" => (
224             %options,
225             default => sub {
226             my ($self) = @_;
227              
228             # transform_states is required here so that we don't modify existing states
229             weighted_mean(
230             transform_states($self->parent->states, sub { $_[0]**2 }),
231             $self->parent->weight_sum
232             )
233             -
234             $self->mean**2;
235             },
236             );
237              
238             sub sorted_by_value
239             {
240 0     0 1 0 my ($self) = @_;
241 0         0 return $self->sorted_by_value_str;
242             }
243              
244             sub median
245             {
246 5     5 1 2468 my ($self) = @_;
247 5         90 return $self->median_str;
248             }
249              
250             sub expected_value
251             {
252 2     2 1 501 my ($self) = @_;
253 2         43 return $self->mean;
254             }
255              
256             sub standard_deviation
257             {
258 2     2 1 490 my ($self) = @_;
259 2         39 return sqrt $self->variance;
260             }
261              
262             1;
263              
264             __END__