File Coverage

blib/lib/App/Chart/Series/Calculation.pm
Criterion Covered Total %
statement 36 89 40.4
branch 4 14 28.5
condition n/a
subroutine 10 17 58.8
pod 0 7 0.0
total 50 127 39.3


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010 Kevin Ryde
2              
3             # This file is part of Chart.
4             #
5             # Chart is free software; you can redistribute it and/or modify it under the
6             # terms of the GNU General Public License as published by the Free Software
7             # Foundation; either version 3, or (at your option) any later version.
8             #
9             # Chart is distributed in the hope that it will be useful, but WITHOUT ANY
10             # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU General Public License along
15             # with Chart. If not, see <http://www.gnu.org/licenses/>.
16              
17             package App::Chart::Series::Calculation;
18 1     1   571 use 5.010;
  1         4  
19 1     1   5 use strict;
  1         1  
  1         18  
20 1     1   4 use warnings;
  1         2  
  1         23  
21 1     1   4 use Carp;
  1         2  
  1         50  
22 1     1   5 use List::Util qw(min max);
  1         2  
  1         804  
23             # use Locale::TextDomain ('App-Chart');
24              
25             sub identity {
26 0     0 0 0 return $_[0];
27             }
28              
29             sub delay {
30 1     1 0 5 my ($class, $N) = @_;
31 1         3 my @array;
32 1         3 my $pos = $N - 1; # initial extends
33             return sub {
34 5     5   11 my ($value) = @_;
35 5         12 my $ret = $array[$pos];
36 5         11 $array[$pos] = $value;
37 5 100       16 if (++$pos >= $N) { $pos = 0; }
  2         4  
38 5         11 return $ret;
39 1         8 };
40             }
41              
42             sub sma_and_stddev {
43 0     0 0 0 my ($class, $N) = @_;
44 0         0 my $delay_proc = $class->delay ($N);
45 0         0 my $total = 0;
46 0         0 my $total_squares = 0;
47 0         0 my $count = 0;
48             return sub {
49 0     0   0 my ($value) = @_;
50              
51             # drop old
52 0         0 my $old = $delay_proc->($value);
53 0 0       0 if (defined $old) {
54 0         0 $total -= $old;
55 0         0 $total_squares -= $old * $old;
56             } else {
57 0         0 $count++;
58             }
59              
60             # add new
61 0         0 $total += $value;
62 0         0 $total_squares += $value * $value;
63              
64 0         0 return ($total / $count,
65             sqrt(max (0, $total_squares*$count - $total*$total)) / $count);
66 0         0 };
67             }
68              
69             sub sum {
70 1     1 0 814 my ($class, $N) = @_;
71 1         7 my $delay_proc = $class->delay ($N);
72 1         4 my $total = 0;
73             return sub {
74 5     5   23 my ($value) = @_;
75              
76             # drop old
77 5         13 my $old = $delay_proc->($value);
78 5 100       17 if (defined $old) {
79 2         94 $total -= $old;
80             }
81             # add new
82 5         12 $total += $value;
83              
84 5         32 return $total;
85 1         6 };
86             }
87              
88             sub ma_proc_by_weights {
89 0     0 0 0 my @weights = @_;
90              
91             # $a[0] is the newest point, $a[1] the prev, through to $a[$N-1]
92 0         0 my @a;
93             my $total_weight;
94              
95             return sub {
96 0     0   0 my ($value) = @_;
97              
98 0         0 unshift @a, $value; # add new
99              
100             # keep last $N points
101 0 0       0 if (@a > @weights) {
102 0         0 pop @a; # drop old
103             } else {
104             # new total weight for bigger @a
105 0         0 $total_weight = List::Util::sum (map {$weights[$_]} 0 .. $#a);
  0         0  
106             }
107              
108 0 0       0 if ($total_weight == 0) {
109 0         0 return 0;
110             }
111 0         0 return (List::Util::sum (map {$a[$_] * $weights[$_]} 0 .. $#a)
  0         0  
112             / $total_weight);
113 0         0 };
114             }
115              
116             #------------------------------------------------------------------------------
117              
118             # http://mathworld.wolfram.com/LeastSquaresFitting.html
119             # Least squares generally, including deriving formula using
120             # derivative==0 as follows:
121             #
122             # The sum of squares is
123             #
124             # R^2(a,b) = Sum (y[i] - (a + b*x[i]))^2
125             #
126             # Partial derivative with b is
127             #
128             # d R^2(a,b)
129             # ---------- = Sum 2 * (y[i]-b*x[i]) * -x[i]
130             # db
131             #
132             # And want that to be zero at the minimum, so
133             #
134             # Sum -2*x[i]*y[i] + 2*b*Sum x[i]^2 = 0
135             #
136             # Sum x[i]*y[i]
137             # b = -------------
138             # Sum x[i]^2
139             #
140              
141             # Return a procedure which calculates a linear regression line fit over an
142             # accumulated window of $N values.
143             #
144             # Each call $proc->($y) enters a new value into the window, and the return
145             # is two values ($a, $b) where the line is $a+$b*X. The last point entered
146             # is at X=0 and the preceding ones at X=-1, X=-2, etc. A and B are
147             # #f if not enough data yet.
148             #
149             # To prime the window initially, call $proc with $N-1 many points preceding
150             # the first desired.
151             #
152             sub linreg {
153 0     0 0 0 my ($class, $N) = @_;
154              
155             # The X values used are centred around 0,
156             # $count=4: -1.5, -0.5, 0.5, 1.5
157             # $count=5: -2, -1, 0, 1, 2
158             # $count=6: -2.5, -1.5, -0.5, 0.5, 1.5, 2.5
159             # etc
160             # But the return is then adjusted $a is based on the last point as X=0
161             #
162             # @array,$pos is a circular list of $count many values. The one at
163             # @array[$pos] is the oldest and is replaced by a new value to cycle that
164             # in.
165             #
166             # $count is how many points are in @array.
167             #
168             # $y_total is the total of the past $count many Y values, ie. the values
169             # in @array.
170             #
171             # $xy2_total is the sum of 2*X*Y for each Y value in @array.
172             #
173             # $xx2_total is 2 * the sum of X*X for each X value used. This is a
174             # constant once $count stops at COUNT. A minimum 1 is enforced for the
175             # degenerate case of $N==0 (there's no slope to in that case but at least
176             # avoid a divide by zero).
177             #
178 0         0 my @array;
179 0         0 my $pos = $N - 1; # initial extends
180 0         0 my $count = 0;
181              
182 0         0 my $y_total = 0;
183 0         0 my $xy2_total = 0;
184 0         0 my $xx2_total = 0;
185              
186             return sub {
187 0     0   0 my ($y) = @_;
188              
189 0 0       0 if ($count >= $N) {
190             # drop oldest point
191 0         0 my $prev = $array[$pos];
192 0         0 $y_total -= $prev;
193 0         0 $xy2_total += ($count-1) * $prev;
194             } else {
195             # gaining a point
196 0         0 $count++;
197 0         0 $xy2_total += $y_total; # adj so below is 1 less x
198 0         0 $xx2_total = max (1, linreg_xx2_calc($count));
199             }
200 0         0 $array[$pos] = $y;
201 0 0       0 if (++$pos >= $N) { $pos = 0; }
  0         0  
202              
203             # shift xy products onto 2 less x each
204 0         0 $xy2_total -= ($y_total + $y_total);
205              
206             # add this point
207 0         0 $y_total += $y;
208 0         0 $xy2_total += ($count-1) * $y;
209              
210 0         0 my $b = $xy2_total / $xx2_total;
211              
212 0         0 return ($y_total/$count + $b*0.5*($count-1),
213             $b);
214 0         0 };
215             }
216              
217             # `xx2-calc' returns 2*Sum(X^2) for the set of N points centred around
218             # zero as described in linreg-slop-calc-proc below. This means for
219             # instance,
220             #
221             # N=4: 2 * [ (-1.5)^2 + (-0.5)^2 + (0.5)^2 + (1.5)^2 ]
222             # N=5: 2 * [ (-2)^2 + (-1)^2 + 0^2 + (1)^2 + (2)^2 ]
223             #
224             # This is (N^3-N)/6, which can be established by taking successive
225             # differences or verified by induction (done separately for odds and evens
226             # is easiest).
227             #
228             # N^3-N is always a multiple of 6, since it can be written (N-1)*N*(N+1)
229             # which is three consecutive numbers so one is certainly a multiple of 3
230             # and another a multiple of 2. The result is forced to inexact since
231             # that's what's wanted for the linreg-slope-calc-proc returns.
232             #
233             sub linreg_xx2_calc {
234 9     9 0 5836 my ($N) = @_;
235 9         33 return $N*($N*$N-1) / 6;
236             }
237              
238             1;