File Coverage

lib/PDL/Stats/TS.pd
Criterion Covered Total %
statement 62 102 60.7
branch 7 30 23.3
condition 2 18 11.1
subroutine 11 13 84.6
pod 0 7 0.0
total 82 170 48.2


line stmt bran cond sub pod time code
1             use strict;
2             use warnings;
3              
4             my $F = [qw(F D)];
5              
6             pp_addpm({At=>'Top'}, <<'EOD');
7              
8             =encoding utf8
9              
10             =head1 NAME
11              
12             PDL::Stats::TS -- basic time series functions
13              
14             =head1 DESCRIPTION
15              
16             The terms FUNCTIONS and METHODS are arbitrarily used to refer to
17             methods that are threadable and methods that are NOT threadable,
18             respectively. Plots require L.
19              
20             ***EXPERIMENTAL!*** In particular, bad value support is spotty and may be shaky. USE WITH DISCRETION!
21              
22             =head1 SYNOPSIS
23              
24             use PDL::LiteF;
25             use PDL::Stats::TS;
26              
27             my $r = $data->acf(5);
28              
29             =cut
30 1     1   9  
  1         5  
  1         45  
31 1     1   6 use strict;
  1         3  
  1         78  
32 1     1   6 use warnings;
  1         2  
  1         110  
33 1     1   7 use Carp;
  1         2  
  1         9  
34 1     1   3517 use PDL::LiteF;
  1         6  
  1         25  
35 1     1   1313 use PDL::Stats::Basic;
  1         5  
  1         16  
36             use PDL::Stats::Kmeans;
37              
38             EOD
39              
40             pp_addhdr('
41             #include
42             #define Z10 1.64485362695147
43             #define Z05 1.95996398454005
44             #define Z01 2.5758293035489
45             #define Z001 3.29052673149193
46              
47             '
48             );
49              
50             pp_def('acf',
51             Pars => 'x(t); [o]r(h)',
52             OtherPars => 'IV lag=>h',
53             GenericTypes => $F,
54             Code => '
55             $GENERIC(x) s, s2, m, cov0, covh;
56             s=0; s2=0; m=0; cov0=0; covh=0;
57             PDL_Indx T, i;
58             T = $SIZE(t);
59             loop(t) %{
60             s += $x();
61             s2 += $x()*$x();
62             %}
63             m = s/T;
64             cov0 = s2 - T * m * m;
65             loop (h) %{
66             if (h) {
67             covh = 0;
68             for (i=0; i
69             covh += ($x(t=>i) - m) * ($x(t=>i+h) - m);
70             }
71             $r() = covh / cov0;
72             }
73             else {
74             $r() = 1;
75             }
76             %}
77             ',
78             PMCode => pp_line_numbers(__LINE__, <<'EOF'),
79             sub PDL::acf {
80 2     2 0 6105 my ($self, $h) = @_;
81 2   33     8 $h ||= $self->dim(0) - 1;
82 2         8 PDL::_acf_int($self, my $r = PDL->null, $h+1);
83 2         66 $r;
84             }
85             EOF
86             Doc => <<'EOD',
87             =for ref
88              
89             Autocorrelation function for up to lag h. If h is not specified it's set to t-1 by default.
90              
91             acf does not process bad values.
92              
93             =for example
94              
95             usage:
96              
97             pdl> $a = sequence 10
98              
99             # lags 0 .. 5
100              
101             pdl> p $a->acf(5)
102             [1 0.7 0.41212121 0.14848485 -0.078787879 -0.25757576]
103             EOD
104             );
105              
106             pp_def('acvf',
107             Pars => 'x(t); [o]v(h)',
108             OtherPars => 'IV lag=>h;',
109             GenericTypes => $F,
110             Code => '
111             $GENERIC(x) s, s2, m, covh;
112             s=0; s2=0; m=0; covh=0;
113             long T, i;
114             T = $SIZE(t);
115             loop(t) %{
116             s += $x();
117             s2 += $x()*$x();
118             %}
119             m = s/T;
120             loop (h) %{
121             if (h) {
122             covh = 0;
123             for (i=0; i
124             covh += ($x(t=>i) - m) * ($x(t=>i+h) - m);
125             }
126             $v() = covh;
127             }
128             else {
129             $v() = s2 - T * m * m;
130             }
131             %}
132             ',
133             PMCode => pp_line_numbers(__LINE__, <<'EOF'),
134             sub PDL::acvf {
135 1     1 0 284276 my ($self, $h) = @_;
136 1   33     5 $h ||= $self->dim(0) - 1;
137 1         7 PDL::_acvf_int($self, my $v = PDL->null, $h+1);
138 1         34 $v;
139             }
140             EOF
141             Doc => <<'EOD',
142             =for ref
143              
144             Autocovariance function for up to lag h. If h is not specified it's set to t-1 by default.
145              
146             acvf does not process bad values.
147              
148             =for example
149              
150             usage:
151              
152             pdl> $a = sequence 10
153              
154             # lags 0 .. 5
155              
156             pdl> p $a->acvf(5)
157             [82.5 57.75 34 12.25 -6.5 -21.25]
158              
159             # autocorrelation
160              
161             pdl> p $a->acvf(5) / $a->acvf(0)
162             [1 0.7 0.41212121 0.14848485 -0.078787879 -0.25757576]
163             EOD
164             );
165              
166             pp_def('dseason',
167             Pars => 'x(t); indx d(); [o]xd(t)',
168             GenericTypes => $F,
169             HandleBad => 1,
170             Code => '
171             PDL_Indx i, max = PDL_IF_BAD(,$SIZE(t))-1, min = PDL_IF_BAD(-1,0);
172             PDL_Indx q = ($d() % 2)? ($d() - 1) / 2 : $d() / 2;
173             /*find good min and max ind*/
174             loop (t) %{
175             PDL_IF_BAD(if ($ISBAD($x())) continue;,)
176             if (min < 0) min = t;
177             max = t;
178             %}
179             if ($d() % 2) {
180             loop(t) %{
181             PDL_IF_BAD(if (t < min || t > max) { $SETBAD(xd()); continue; },)
182             $GENERIC(x) sum = 0; PDL_IF_BAD(PDL_Indx dd = 0;,)
183             for (i=-q; i<=q; i++) {
184             PDL_Indx ti = (t+i < min)? min
185             : (t+i > max)? max
186             : t+i
187             ;
188             PDL_IF_BAD(if ($ISBAD($x(t=>ti))) continue; dd++;,)
189             sum += $x(t=>ti);
190             }
191             PDL_IF_BAD(if (!dd) { $SETBAD(xd()); continue; },)
192             $xd() = sum / PDL_IF_BAD(dd,$d());
193             %}
194             } else {
195             loop(t) %{
196             PDL_IF_BAD(if (t < min || t > max) { $SETBAD(xd()); continue; },)
197             $GENERIC(x) sum = 0; PDL_IF_BAD(PDL_Indx dd = 0;,)
198             for (i=-q; i<=q; i++) {
199             PDL_Indx ti = (t+i < min)? min
200             : (t+i > max)? max
201             : t+i
202             ;
203             PDL_IF_BAD(if ($ISBAD($x(t=>ti))) continue; dd++;,)
204             sum += (i == q || i == -q)? .5 * $x(t=>ti) : $x(t=>ti);
205             }
206             PDL_IF_BAD(if (!dd) { $SETBAD(xd()); continue; }
207             dd--;
208             if ( ($ISBAD(x(t=>t-q)) && $ISGOOD(x(t=>t+q)) )
209             || ($ISBAD(x(t=>t+q)) && $ISGOOD(x(t=>t-q)) ) )
210             dd += .5;
211             ,)
212             $xd() = sum / PDL_IF_BAD(dd,$d());
213             %}
214             }
215             ',
216             Doc => 'Deseasonalize data using moving average filter the size of period d.',
217             );
218              
219             pp_def('fill_ma',
220             Pars => 'x(t); indx q(); [o]xf(t)',
221             GenericTypes => $F,
222             HandleBad => 1,
223             Code => '
224             $GENERIC(x) sum, xx;
225             PDL_Indx i, n, max = $SIZE(t) - 1;
226             loop(t) %{
227             PDL_IF_BAD(if ($ISBAD(x())) {
228             n=0; sum=0;
229             for (i=-$q(); i<=$q(); i++) {
230             xx = (t+i < 0)? $x(t=>0)
231             : (t+i > max)? $x(t=>max)
232             : $x(t=>t+i)
233             ;
234             if ($ISGOODVAR(xx,x)) {
235             sum += xx;
236             n ++;
237             }
238             }
239             if (n) {
240             $xf() = sum / n;
241             }
242             else {
243             $SETBAD(xf());
244             }
245             continue;
246             },)
247             $xf() = $x();
248             %}
249             ',
250             PMCode => pp_line_numbers(__LINE__, <<'EOF'),
251             sub PDL::fill_ma {
252 1     1 0 9416 my ($x, $q) = @_;
253 1         5 PDL::_fill_ma_int($x, $q, my $x_filled = PDL->null);
254 1         52 $x_filled->check_badflag;
255             # carp "ma window too small, still has bad value"
256             # if $x_filled->badflag;
257 1         173 return $x_filled;
258             }
259             EOF
260             Doc => <<'EOD',
261             =for ref
262              
263             Fill missing value with moving average. xf(t) = sum(x(t-q .. t-1, t+1 .. t+q)) / 2q.
264              
265             =for bad
266              
267             fill_ma does handle bad values. Output pdl bad flag is cleared unless the specified window size q is too small and there are still bad values.
268             EOD
269             );
270              
271             pp_def('filter_exp',
272             Pars => 'x(t); a(); [o]xf(t)',
273             GenericTypes => $F,
274             Code => '
275             $GENERIC(x) b, m;
276             b = 1 - $a();
277             loop(t) %{
278             if (t) {
279             m = $a() * $x() + b * m;
280             }
281             else {
282             m = $x();
283             }
284             $xf() = m;
285             %}
286             ',
287             Doc => 'Filter, exponential smoothing. xf(t) = a * x(t) + (1-a) * xf(t-1)',
288             );
289              
290             pp_def('filter_ma',
291             Pars => 'x(t); indx q(); [o]xf(t)',
292             GenericTypes => $F,
293             Code => '
294             $GENERIC(x) sum;
295             PDL_Indx i, n, max;
296             n = 2 * $q() + 1;
297             max = $SIZE(t) - 1;
298             loop(t) %{
299             sum = 0;
300             for (i=-$q(); i<=$q(); i++) {
301             sum += (t+i < 0)? $x(t=>0)
302             : (t+i > max)? $x(t=>max)
303             : $x(t=>t+i)
304             ;
305             }
306             $xf() = sum / n;
307             %}
308             ',
309             Doc => 'Filter, moving average. xf(t) = sum(x(t-q .. t+q)) / (2q + 1)',
310             );
311              
312             pp_def('mae',
313             Pars => 'a(n); b(n); [o]c()',
314             GenericTypes => $F,
315             HandleBad => 1,
316             Code => '
317             $GENERIC(c) sum;
318             sum = 0;
319             PDL_Indx N = PDL_IF_BAD(0,$SIZE(n));
320             loop(n) %{
321             PDL_IF_BAD(if ($ISBAD($a()) || $ISBAD(b())) continue; N++;,)
322             sum += fabs( $a() - $b() );
323             %}
324             if (N < 1) { $SETBAD(c()); continue; }
325             $c() = sum / N;
326             ',
327             Doc => 'Mean absolute error. MAE = 1/n * sum( abs(y - y_pred) )',
328             );
329              
330             pp_def('mape',
331             Pars => 'a(n); b(n); [o]c()',
332             GenericTypes => $F,
333             HandleBad => 1,
334             Code => '
335             $GENERIC(c) sum;
336             sum = 0;
337             PDL_Indx N = PDL_IF_BAD(0,$SIZE(n));
338             loop(n) %{
339             PDL_IF_BAD(if ($ISBAD($a()) || $ISBAD(b())) continue; N++;,)
340             sum += fabs( ($a() - $b()) / $a() );
341             %}
342             if (N < 1) { $SETBAD(c()); continue; }
343             $c() = sum / N;
344             ',
345             Doc => 'Mean absolute percent error. MAPE = 1/n * sum(abs((y - y_pred) / y))',
346             );
347              
348             pp_def('wmape',
349             Pars => 'a(n); b(n); [o]c()',
350             GenericTypes => $F,
351             HandleBad => 1,
352             Code => '
353             $GENERIC(c) sum_e=0, sum=0;
354             loop(n) %{
355             PDL_IF_BAD(if ($ISBAD($a()) || $ISBAD(b())) continue;,)
356             sum_e += fabs( $a() - $b() );
357             sum += fabs( $a() );
358             %}
359             if (!sum) { $SETBAD(c()); continue; }
360             $c() = sum_e / sum;
361             ',
362             Doc => 'Weighted mean absolute percent error. avg(abs(error)) / avg(abs(data)). Much more robust compared to mape with division by zero error (cf. Schütz, W., & Kolassa, 2006).',
363             );
364              
365             pp_def('portmanteau',
366             Pars => 'r(h); longlong t(); [o]Q()',
367             GenericTypes => $F,
368             Code => '
369             $GENERIC(r) sum;
370             sum = 0;
371             loop(h) %{
372             if (h)
373             sum += $r()*$r() / ($t() - h);
374             %}
375             $Q() = $t() * ($t()+2) * sum;
376             ',
377             Doc => '
378             =for ref
379              
380             Portmanteau significance test (Ljung-Box) for autocorrelations.
381              
382             =for example
383              
384             Usage:
385              
386             pdl> $a = sequence 10
387              
388             # acf for lags 0-5
389             # lag 0 excluded from portmanteau
390              
391             pdl> p $chisq = $a->acf(5)->portmanteau( $a->nelem )
392             11.1753902662994
393              
394             # get p-value from chisq distr
395              
396             pdl> use PDL::GSL::CDF
397             pdl> p 1 - gsl_cdf_chisq_P( $chisq, 5 )
398             0.0480112934306748
399             ',
400             );
401              
402             pp_def('pred_ar',
403             Pars => 'x(p); b(p); [o]pred(t)',
404             OtherPars => 'IV end=>t;',
405             GenericTypes => $F,
406             Code => '
407             PDL_Indx ord = $SIZE(p);
408             $GENERIC(x) xt, xp[ord];
409             loop (t) %{
410             if (t < ord) {
411             xp[t] = $x(p=>t);
412             $pred() = xp[t];
413             }
414             else {
415             xt = 0;
416             loop(p) %{
417             xt += xp[p] * $b(p=>ord-p-1);
418             xp[p] = (p < ord - 1)? xp[p+1] : xt;
419             %}
420             $pred() = xt;
421             }
422             %}
423             ',
424             PMCode => pp_line_numbers(__LINE__, <<'EOF'),
425             sub PDL::pred_ar {
426 2     2 0 1544 my ($x, $b, $t, $opt) = @_;
427 2         5 my %opt = ( CONST => 1 );
428 2 100       6 if ($opt) { $opt{uc $_} = $opt->{$_} for keys %$opt; }
  1         5  
429 2         6 $b = PDL->topdl($b); # allows passing simple number
430 2         1 my $ext;
431 2 100       5 if ($opt{CONST}) {
432 1         5 my $t_ = $t - ( $x->dim(0) - $b->dim(0) + 1 );
433 1         6 PDL::_pred_ar_int($x->slice([-$b->dim(0)+1,-1]), $b->slice('0:-2'), $ext = PDL->null, $t_);
434 1         99 $ext->slice([$b->dim(0)-1,-1]) += $b->slice(-1);
435 1         57 return $x->append( $ext->slice([$b->dim(0)-1,-1]) );
436             } else {
437 1         5 my $t_ = $t - ( $x->dim(0) - $b->dim(0) );
438 1         5 PDL::_pred_ar_int($x->slice([-$b->dim(0),-1]), $b, $ext = PDL->null, $t_);
439 1         62 return $x->append($ext->slice([$b->dim(0),-1]));
440             }
441             }
442             EOF
443             Doc => <<'EOD',
444             =for ref
445              
446             Calculates predicted values up to period t (extend current series up to period t) for autoregressive series, with or without constant. If there is constant, it is the last element in b, as would be returned by ols or ols_t.
447              
448             pred_ar does not process bad values.
449              
450             =for options
451              
452             CONST => 1,
453              
454             =for example
455              
456             Usage:
457              
458             pdl> $x = sequence 2
459              
460             # last element is constant
461             pdl> $b = pdl(.8, -.2, .3)
462              
463             pdl> p $x->pred_ar($b, 7)
464             [0 1 1.1 0.74 0.492 0.3656 0.31408]
465              
466             # no constant
467             pdl> p $x->pred_ar($b(0:1), 7, {const=>0})
468             [0 1 0.8 0.44 0.192 0.0656 0.01408]
469             EOD
470             );
471              
472             pp_addpm pp_line_numbers(__LINE__, <<'EOD');
473              
474             =head2 season_m
475              
476             Given length of season, returns seasonal mean and variance for each period
477             (returns seasonal mean only in scalar context).
478              
479             =for options
480              
481             Default options (case insensitive):
482              
483             START_POSITION => 0, # series starts at this position in season
484             MISSING => -999, # internal mark for missing points in season
485             PLOT => 0, # boolean
486             # see PDL::Graphics::Simple for next options
487             WIN => undef, # pass pgswin object for more plotting control
488             COLOR => 1,
489              
490             =for usage
491              
492             my ($m, $ms) = $data->season_m( 24, { START_POSITION=>2 } );
493              
494             =cut
495              
496             *season_m = \&PDL::season_m;
497             sub PDL::season_m {
498 1     1 0 3680 my ($self, $d, $opt) = @_;
499 1         6 my %opt = (
500             START_POSITION => 0, # series starts at this position in season
501             MISSING => -999, # internal mark for missing points in season
502             PLOT => 0,
503             WIN => undef, # pass pgswin object for more plotting control
504             COLOR => 1,
505             );
506 1 50       3 if ($opt) { $opt{uc $_} = $opt->{$_} for keys %$opt; }
  1         5  
507              
508 1         5 my $n_season = ($self->dim(0) + $opt{START_POSITION}) / $d;
509 1         2 $n_season = pdl($n_season)->ceil->sum->sclr;
510              
511 1         163 my @dims = $self->dims;
512 1         2 $dims[0] = $n_season * $d;
513 1         4 my $data = zeroes( @dims ) + $opt{MISSING};
514              
515 1         40 $data->slice([$opt{START_POSITION},$opt{START_POSITION} + $self->dim(0)-1]) .= $self;
516 1         51 $data->badflag(1);
517 1         37 $data->inplace->setvaltobad( $opt{MISSING} );
518              
519 1         4 my $s = sequence $d;
520 1         48 $s = $s->dummy(1, $n_season)->flat;
521 1         58 $s = $s->iv_cluster();
522              
523 1         33 my ($m, $ms) = $data->centroid( $s );
524              
525 1 50       5 if ($opt{PLOT}) {
526 0         0 require PDL::Graphics::Simple;
527 0   0     0 my $w = $opt{WIN} || PDL::Graphics::Simple::pgswin();
528 0         0 my $seq = sequence($d);
529 0         0 my $errb_length = sqrt( $ms / $s->sumover )->squeeze;
530 0         0 my $col = $opt{COLOR};
531 0         0 my @plots = map +(with=>'lines', ke=>"Data $col", style=>$col++, $seq, $_), $m->dog;
532 0 0 0     0 push @plots, with=>'errorbars', ke=>'Error', style=>$opt{COLOR}, $seq, $m->squeeze, $errb_length
533             if $m->squeeze->ndims < 2 && ($errb_length > 0)->any;
534 0         0 $w->plot(@plots, { xlabel=>'period', ylabel=>'mean' });
535             }
536              
537 1 50       14 return wantarray? ($m, $ms) : $m;
538             }
539              
540             =head2 plot_dseason
541              
542             =for ref
543              
544             Plots deseasonalized data and original data points. Opens and closes
545             default window for plotting unless a C object is passed in
546             options. Returns deseasonalized data.
547              
548             =for options
549              
550             Default options (case insensitive):
551              
552             WIN => undef,
553             COLOR => 1, # data point color
554              
555             =cut
556              
557             *plot_dseason = \&PDL::plot_dseason;
558             sub PDL::plot_dseason {
559 0     0 0   require PDL::Graphics::Simple;
560 0           my ($self, $d, $opt) = @_;
561 0 0         !defined($d) and croak "please set season period length";
562 0           $self = $self->squeeze;
563 0           my %opt = (
564             WIN => undef,
565             COLOR => 1, # data point color
566             );
567 0 0         if ($opt) { $opt{uc $_} = $opt->{$_} for keys %$opt; }
  0            
568 0           my $dsea = $self->dseason($d);
569 0   0       my $w = $opt{WIN} || PDL::Graphics::Simple::pgswin();
570 0           my $seq = sequence($self->dim(0));
571 0           my $col = $opt{COLOR};
572 0           my @plots = map +(with=>'lines', ke=>"Data $col", style=>$col++, $seq, $_), $dsea->dog;
573 0           $col = $opt{COLOR};
574 0           push @plots, map +(with=>'points', ke=>"De-seasonalised $col", style=>$col++, $seq, $_), $self->dog;
575 0           $w->plot(@plots, { xlabel=>'T', ylabel=>'DV' });
576 0           return $dsea;
577             }
578              
579             =head1 METHODS
580              
581             =head2 plot_acf
582              
583             =for ref
584              
585             Plots and returns autocorrelations for a time series.
586              
587             =for options
588              
589             Default options (case insensitive):
590              
591             SIG => 0.05, # can specify .10, .05, .01, or .001
592             WIN => undef,
593              
594             =for usage
595              
596             Usage:
597              
598             pdl> $a = sequence 10
599              
600             pdl> p $r = $a->plot_acf(5)
601             [1 0.7 0.41212121 0.14848485 -0.078787879 -0.25757576]
602              
603             =cut
604              
605             *plot_acf = \&PDL::plot_acf;
606             sub PDL::plot_acf {
607 0     0 0   require PDL::Graphics::Simple;
608 0 0         my $opt = ref($_[-1]) eq 'HASH' ? pop @_ : undef;
609 0           my ($self, $h) = @_;
610 0           my $r = $self->acf($h);
611 0           my %opt = (
612             SIG => 0.05,
613             WIN => undef,
614             );
615 0 0         if ($opt) { $opt{uc $_} = $opt->{$_} for keys %$opt; }
  0            
616             my $y_sig = ($opt{SIG} == 0.10)? 1.64485362695147
617             : ($opt{SIG} == 0.05)? 1.95996398454005
618             : ($opt{SIG} == 0.01)? 2.5758293035489
619 0 0         : ($opt{SIG} == 0.001)? 3.29052673149193
    0          
    0          
    0          
620             : 0
621             ;
622 0 0         unless ($y_sig) {
623 0           carp "SIG outside of recognized value. default to 0.05";
624 0           $y_sig = 1.95996398454005;
625             }
626 0   0       my $w = $opt{WIN} || PDL::Graphics::Simple::pgswin();
627 0           my $seq = pdl(-1,$h+1);
628 0           my $y_seq = ones(2) * $y_sig / sqrt($self->dim(0)) * -1;
629 0           $w->plot(
630             with=>'lines', $seq, zeroes(2), # x axis
631             with=>'lines', style=>2, $seq, $y_seq,
632             with=>'lines', style=>2, $seq, -$y_seq,
633             (map +(with=>'lines', ones(2)*$_, pdl(0, $r->slice("($_)"))), 0..$h), { xlabel=>'lag', ylabel=>'acf', }
634             );
635 0           $r;
636             }
637              
638             =head1 REFERENCES
639              
640             Brockwell, P.J., & Davis, R.A. (2002). Introduction to Time Series and Forecasting (2nd ed.). New York, NY: Springer.
641              
642             Schütz, W., & Kolassa, S. (2006). Foresight: advantages of the MAD/Mean ratio over the MAPE. Retrieved Jan 28, 2010, from http://www.saf-ag.com/226+M5965d28cd19.html
643              
644             =head1 AUTHOR
645              
646             Copyright (C) 2009 Maggie J. Xiong
647              
648             All rights reserved. There is no warranty. You are allowed to redistribute this software / documentation as described in the file COPYING in the PDL distribution.
649              
650             =cut
651              
652             EOD
653              
654             pp_done();