anomaly_detection 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- /*!
2
- * STL C++ v0.1.3
1
+ /*
2
+ * STL C++ v0.3.0
3
3
  * https://github.com/ankane/stl-cpp
4
4
  * Unlicense OR MIT License
5
5
  *
@@ -8,120 +8,177 @@
8
8
  * Cleveland, R. B., Cleveland, W. S., McRae, J. E., & Terpenning, I. (1990).
9
9
  * STL: A Seasonal-Trend Decomposition Procedure Based on Loess.
10
10
  * Journal of Official Statistics, 6(1), 3-33.
11
+ *
12
+ * Bandara, K., Hyndman, R. J., & Bergmeir, C. (2021).
13
+ * MSTL: A Seasonal-Trend Decomposition Algorithm for Time Series with Multiple Seasonal Patterns.
14
+ * arXiv:2107.13462 [stat.AP]. https://doi.org/10.48550/arXiv.2107.13462
11
15
  */
12
16
 
13
17
  #pragma once
14
18
 
15
19
  #include <algorithm>
16
20
  #include <cmath>
21
+ #include <cstddef>
17
22
  #include <numeric>
18
23
  #include <optional>
24
+ #include <span>
19
25
  #include <stdexcept>
26
+ #include <tuple>
27
+ #include <utility>
20
28
  #include <vector>
21
29
 
22
30
  namespace stl {
23
31
 
24
- bool est(const float* y, size_t n, size_t len, int ideg, float xs, float* ys, size_t nleft, size_t nright, float* w, bool userw, const float* rw) {
25
- auto range = ((float) n) - 1.0;
26
- auto h = std::max(xs - ((float) nleft), ((float) nright) - xs);
32
+ namespace detail {
33
+
34
+ // TODO use span.at() for C++26
35
+ template<typename T>
36
+ T& span_at(std::span<T> sp, size_t pos) {
37
+ if (pos >= sp.size()) [[unlikely]] {
38
+ throw std::out_of_range("pos >= size()");
39
+ }
40
+ return sp[pos];
41
+ }
42
+
43
+ template<typename T>
44
+ bool est(
45
+ const std::vector<T>& y,
46
+ size_t n,
47
+ size_t len,
48
+ int ideg,
49
+ T xs,
50
+ T& ys,
51
+ size_t nleft,
52
+ size_t nright,
53
+ std::vector<T>& w,
54
+ bool userw,
55
+ const std::vector<T>& rw
56
+ ) {
57
+ T range = static_cast<T>(n) - static_cast<T>(1.0);
58
+ T h = std::max(xs - static_cast<T>(nleft), static_cast<T>(nright) - xs);
27
59
 
28
60
  if (len > n) {
29
- h += (float) ((len - n) / 2);
61
+ h += static_cast<T>((len - n) / 2);
30
62
  }
31
63
 
32
- auto h9 = 0.999 * h;
33
- auto h1 = 0.001 * h;
64
+ T h9 = static_cast<T>(0.999) * h;
65
+ T h1 = static_cast<T>(0.001) * h;
34
66
 
35
67
  // compute weights
36
- auto a = 0.0;
37
- for (auto j = nleft; j <= nright; j++) {
38
- w[j - 1] = 0.0;
39
- auto r = fabs(((float) j) - xs);
68
+ T a = 0.0;
69
+ for (size_t j = nleft; j <= nright; j++) {
70
+ w.at(j - 1) = 0.0;
71
+ T r = std::abs(static_cast<T>(j) - xs);
40
72
  if (r <= h9) {
41
73
  if (r <= h1) {
42
- w[j - 1] = 1.0;
74
+ w.at(j - 1) = 1.0;
43
75
  } else {
44
- w[j - 1] = pow(1.0 - pow(r / h, 3), 3);
76
+ w.at(j - 1) = static_cast<T>(std::pow(1.0 - std::pow(r / h, 3.0), 3.0));
45
77
  }
46
78
  if (userw) {
47
- w[j - 1] *= rw[j - 1];
79
+ w.at(j - 1) *= rw.at(j - 1);
48
80
  }
49
- a += w[j - 1];
81
+ a += w.at(j - 1);
50
82
  }
51
83
  }
52
84
 
53
85
  if (a <= 0.0) {
54
86
  return false;
55
- } else { // weighted least squares
56
- for (auto j = nleft; j <= nright; j++) { // make sum of w(j) == 1
57
- w[j - 1] /= a;
87
+ } else {
88
+ // weighted least squares
89
+ for (size_t j = nleft; j <= nright; j++) {
90
+ // make sum of w(j) == 1
91
+ w.at(j - 1) /= a;
58
92
  }
59
93
 
60
- if (h > 0.0 && ideg > 0) { // use linear fit
61
- auto a = 0.0;
62
- for (auto j = nleft; j <= nright; j++) { // weighted center of x values
63
- a += w[j - 1] * ((float) j);
94
+ if (h > 0.0 && ideg > 0) {
95
+ // use linear fit
96
+ T a = 0.0;
97
+ for (size_t j = nleft; j <= nright; j++) {
98
+ // weighted center of x values
99
+ a += w.at(j - 1) * static_cast<T>(j);
64
100
  }
65
- auto b = xs - a;
66
- auto c = 0.0;
67
- for (auto j = nleft; j <= nright; j++) {
68
- c += w[j - 1] * pow(((float) j) - a, 2);
101
+ T b = xs - a;
102
+ T c = 0.0;
103
+ for (size_t j = nleft; j <= nright; j++) {
104
+ c += w.at(j - 1) * std::pow(static_cast<T>(j) - a, static_cast<T>(2.0));
69
105
  }
70
- if (sqrt(c) > 0.001 * range) {
106
+ if (std::sqrt(c) > 0.001 * range) {
71
107
  b /= c;
72
108
 
73
109
  // points are spread out enough to compute slope
74
- for (auto j = nleft; j <= nright; j++) {
75
- w[j - 1] *= b * (((float) j) - a) + 1.0;
110
+ for (size_t j = nleft; j <= nright; j++) {
111
+ w.at(j - 1) *= b * (static_cast<T>(j) - a) + static_cast<T>(1.0);
76
112
  }
77
113
  }
78
114
  }
79
115
 
80
- *ys = 0.0;
81
- for (auto j = nleft; j <= nright; j++) {
82
- *ys += w[j - 1] * y[j - 1];
116
+ ys = 0.0;
117
+ for (size_t j = nleft; j <= nright; j++) {
118
+ ys += w.at(j - 1) * y.at(j - 1);
83
119
  }
84
120
 
85
121
  return true;
86
122
  }
87
123
  }
88
124
 
89
- void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool userw, const float* rw, float* ys, float* res) {
125
+ template<typename T>
126
+ void ess(
127
+ const std::vector<T>& y,
128
+ size_t n,
129
+ size_t len,
130
+ int ideg,
131
+ size_t njump,
132
+ bool userw,
133
+ const std::vector<T>& rw,
134
+ std::span<T> ys,
135
+ std::vector<T>& res
136
+ ) {
90
137
  if (n < 2) {
91
- ys[0] = y[0];
138
+ span_at(ys, 0) = y.at(0);
92
139
  return;
93
140
  }
94
141
 
95
142
  size_t nleft = 0;
96
143
  size_t nright = 0;
97
144
 
98
- auto newnj = std::min(njump, n - 1);
145
+ size_t newnj = std::min(njump, n - 1);
99
146
  if (len >= n) {
100
147
  nleft = 1;
101
148
  nright = n;
102
149
  for (size_t i = 1; i <= n; i += newnj) {
103
- auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
150
+ bool ok = est(
151
+ y, n, len, ideg, static_cast<T>(i), span_at(ys, i - 1), nleft, nright, res, userw,
152
+ rw
153
+ );
104
154
  if (!ok) {
105
- ys[i - 1] = y[i - 1];
155
+ span_at(ys, i - 1) = y.at(i - 1);
106
156
  }
107
157
  }
108
- } else if (newnj == 1) { // newnj equal to one, len less than n
109
- auto nsh = (len + 1) / 2;
158
+ } else if (newnj == 1) {
159
+ // newnj equal to one, len less than n
160
+ size_t nsh = (len + 1) / 2;
110
161
  nleft = 1;
111
162
  nright = len;
112
- for (size_t i = 1; i <= n; i++) { // fitted value at i
163
+ for (size_t i = 1; i <= n; i++) {
164
+ // fitted value at i
113
165
  if (i > nsh && nright != n) {
114
166
  nleft += 1;
115
167
  nright += 1;
116
168
  }
117
- auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
169
+ bool ok = est(
170
+ y, n, len, ideg, static_cast<T>(i), span_at(ys, i - 1), nleft, nright, res, userw,
171
+ rw
172
+ );
118
173
  if (!ok) {
119
- ys[i - 1] = y[i - 1];
174
+ span_at(ys, i - 1) = y.at(i - 1);
120
175
  }
121
176
  }
122
- } else { // newnj greater than one, len less than n
123
- auto nsh = (len + 1) / 2;
124
- for (size_t i = 1; i <= n; i += newnj) { // fitted value at i
177
+ } else {
178
+ // newnj greater than one, len less than n
179
+ size_t nsh = (len + 1) / 2;
180
+ for (size_t i = 1; i <= n; i += newnj) {
181
+ // fitted value at i
125
182
  if (i < nsh) {
126
183
  nleft = 1;
127
184
  nright = len;
@@ -132,380 +189,714 @@ void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool user
132
189
  nleft = i - nsh + 1;
133
190
  nright = len + i - nsh;
134
191
  }
135
- auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
192
+ bool ok = est(
193
+ y, n, len, ideg, static_cast<T>(i), span_at(ys, i - 1), nleft, nright, res, userw,
194
+ rw
195
+ );
136
196
  if (!ok) {
137
- ys[i - 1] = y[i - 1];
197
+ span_at(ys, i - 1) = y.at(i - 1);
138
198
  }
139
199
  }
140
200
  }
141
201
 
142
202
  if (newnj != 1) {
143
203
  for (size_t i = 1; i <= n - newnj; i += newnj) {
144
- auto delta = (ys[i + newnj - 1] - ys[i - 1]) / ((float) newnj);
145
- for (auto j = i + 1; j <= i + newnj - 1; j++) {
146
- ys[j - 1] = ys[i - 1] + delta * ((float) (j - i));
204
+ T delta = (span_at(ys, i + newnj - 1) - span_at(ys, i - 1)) / static_cast<T>(newnj);
205
+ for (size_t j = i + 1; j <= i + newnj - 1; j++) {
206
+ span_at(ys, j - 1) = span_at(ys, i - 1) + delta * static_cast<T>(j - i);
147
207
  }
148
208
  }
149
- auto k = ((n - 1) / newnj) * newnj + 1;
209
+ size_t k = ((n - 1) / newnj) * newnj + 1;
150
210
  if (k != n) {
151
- auto ok = est(y, n, len, ideg, (float) n, &ys[n - 1], nleft, nright, res, userw, rw);
211
+ bool ok = est(
212
+ y, n, len, ideg, static_cast<T>(n), span_at(ys, n - 1), nleft, nright, res, userw,
213
+ rw
214
+ );
152
215
  if (!ok) {
153
- ys[n - 1] = y[n - 1];
216
+ span_at(ys, n - 1) = y.at(n - 1);
154
217
  }
155
218
  if (k != n - 1) {
156
- auto delta = (ys[n - 1] - ys[k - 1]) / ((float) (n - k));
157
- for (auto j = k + 1; j <= n - 1; j++) {
158
- ys[j - 1] = ys[k - 1] + delta * ((float) (j - k));
219
+ T delta = (span_at(ys, n - 1) - span_at(ys, k - 1)) / static_cast<T>(n - k);
220
+ for (size_t j = k + 1; j <= n - 1; j++) {
221
+ span_at(ys, j - 1) = span_at(ys, k - 1) + delta * static_cast<T>(j - k);
159
222
  }
160
223
  }
161
224
  }
162
225
  }
163
226
  }
164
227
 
165
- void ma(const float* x, size_t n, size_t len, float* ave) {
166
- auto newn = n - len + 1;
167
- auto flen = (float) len;
168
- auto v = 0.0;
228
+ template<typename T>
229
+ void ma(const std::vector<T>& x, size_t n, size_t len, std::vector<T>& ave) {
230
+ size_t newn = n - len + 1;
231
+ auto flen = static_cast<double>(len);
232
+ double v = 0.0;
169
233
 
170
234
  // get the first average
171
235
  for (size_t i = 0; i < len; i++) {
172
- v += x[i];
236
+ v += x.at(i);
173
237
  }
174
238
 
175
- ave[0] = v / flen;
239
+ ave.at(0) = static_cast<T>(v / flen);
176
240
  if (newn > 1) {
177
- auto k = len;
178
- auto m = 0;
241
+ size_t k = len;
242
+ size_t m = 0;
179
243
  for (size_t j = 1; j < newn; j++) {
180
244
  // window down the array
181
- v = v - x[m] + x[k];
182
- ave[j] = v / flen;
245
+ v = v - x.at(m) + x.at(k);
246
+ ave.at(j) = static_cast<T>(v / flen);
183
247
  k += 1;
184
248
  m += 1;
185
249
  }
186
250
  }
187
251
  }
188
252
 
189
- void fts(const float* x, size_t n, size_t np, float* trend, float* work) {
253
+ template<typename T>
254
+ void fts(
255
+ const std::vector<T>& x,
256
+ size_t n,
257
+ size_t np,
258
+ std::vector<T>& trend,
259
+ std::vector<T>& work
260
+ ) {
190
261
  ma(x, n, np, trend);
191
262
  ma(trend, n - np + 1, np, work);
192
263
  ma(work, n - 2 * np + 2, 3, trend);
193
264
  }
194
265
 
195
- void rwts(const float* y, size_t n, const float* fit, float* rw) {
196
- for (size_t i = 0; i < n; i++) {
197
- rw[i] = fabs(y[i] - fit[i]);
266
+ template<typename T>
267
+ void rwts(std::span<const T> y, const std::vector<T>& fit, std::vector<T>& rw) {
268
+ // TODO use std::views::zip for C++23
269
+ for (size_t i = 0; i < y.size(); i++) {
270
+ rw.at(i) = std::abs(span_at(y, i) - fit.at(i));
198
271
  }
199
272
 
200
- auto mid1 = (n - 1) / 2;
201
- auto mid2 = n / 2;
273
+ size_t n = y.size();
274
+ size_t mid1 = (n - 1) / 2;
275
+ size_t mid2 = n / 2;
202
276
 
203
277
  // sort
204
- std::sort(rw, rw + n);
278
+ std::ranges::sort(rw);
205
279
 
206
- auto cmad = 3.0 * (rw[mid1] + rw[mid2]); // 6 * median abs resid
207
- auto c9 = 0.999 * cmad;
208
- auto c1 = 0.001 * cmad;
280
+ T cmad = static_cast<T>(3.0) * (rw.at(mid1) + rw.at(mid2)); // 6 * median abs resid
281
+ T c9 = static_cast<T>(0.999) * cmad;
282
+ T c1 = static_cast<T>(0.001) * cmad;
209
283
 
210
- for (size_t i = 0; i < n; i++) {
211
- auto r = fabs(y[i] - fit[i]);
284
+ // TODO use std::views::zip for C++23
285
+ for (size_t i = 0; i < y.size(); i++) {
286
+ T r = std::abs(span_at(y, i) - fit.at(i));
212
287
  if (r <= c1) {
213
- rw[i] = 1.0;
288
+ rw.at(i) = 1.0;
214
289
  } else if (r <= c9) {
215
- rw[i] = pow(1.0 - pow(r / cmad, 2), 2);
290
+ rw.at(i) = static_cast<T>(std::pow(1.0 - std::pow(r / cmad, 2.0), 2.0));
216
291
  } else {
217
- rw[i] = 0.0;
292
+ rw.at(i) = 0.0;
218
293
  }
219
294
  }
220
295
  }
221
296
 
222
- void ss(const float* y, size_t n, size_t np, size_t ns, int isdeg, size_t nsjump, bool userw, float* rw, float* season, float* work1, float* work2, float* work3, float* work4) {
297
+ template<typename T>
298
+ void ss(
299
+ const std::vector<T>& y,
300
+ size_t n,
301
+ size_t np,
302
+ size_t ns,
303
+ int isdeg,
304
+ size_t nsjump,
305
+ bool userw,
306
+ std::vector<T>& rw,
307
+ std::vector<T>& season,
308
+ std::vector<T>& work1,
309
+ std::vector<T>& work2,
310
+ std::vector<T>& work3,
311
+ std::vector<T>& work4
312
+ ) {
223
313
  for (size_t j = 1; j <= np; j++) {
224
314
  size_t k = (n - j) / np + 1;
225
315
 
226
316
  for (size_t i = 1; i <= k; i++) {
227
- work1[i - 1] = y[(i - 1) * np + j - 1];
317
+ work1.at(i - 1) = y.at((i - 1) * np + j - 1);
228
318
  }
229
319
  if (userw) {
230
320
  for (size_t i = 1; i <= k; i++) {
231
- work3[i - 1] = rw[(i - 1) * np + j - 1];
321
+ work3.at(i - 1) = rw.at((i - 1) * np + j - 1);
232
322
  }
233
323
  }
234
- ess(work1, k, ns, isdeg, nsjump, userw, work3, work2 + 1, work4);
235
- auto xs = 0.0;
236
- auto nright = std::min(ns, k);
237
- auto ok = est(work1, k, ns, isdeg, xs, &work2[0], 1, nright, work4, userw, work3);
324
+ ess(work1, k, ns, isdeg, nsjump, userw, work3, std::span{work2}.subspan(1), work4);
325
+ T xs = 0.0;
326
+ size_t nright = std::min(ns, k);
327
+ bool ok = est(work1, k, ns, isdeg, xs, work2.at(0), 1, nright, work4, userw, work3);
238
328
  if (!ok) {
239
- work2[0] = work2[1];
329
+ work2.at(0) = work2.at(1);
240
330
  }
241
- xs = k + 1;
242
- size_t nleft = std::max(1, (int) k - (int) ns + 1);
243
- ok = est(work1, k, ns, isdeg, xs, &work2[k + 1], nleft, k, work4, userw, work3);
331
+ xs = static_cast<T>(k + 1);
332
+ size_t nleft = static_cast<size_t>(
333
+ std::max(1, static_cast<int>(k) - static_cast<int>(ns) + 1)
334
+ );
335
+ ok = est(work1, k, ns, isdeg, xs, work2.at(k + 1), nleft, k, work4, userw, work3);
244
336
  if (!ok) {
245
- work2[k + 1] = work2[k];
337
+ work2.at(k + 1) = work2.at(k);
246
338
  }
247
339
  for (size_t m = 1; m <= k + 2; m++) {
248
- season[(m - 1) * np + j - 1] = work2[m - 1];
340
+ season.at((m - 1) * np + j - 1) = work2.at(m - 1);
249
341
  }
250
342
  }
251
343
  }
252
344
 
253
- void onestp(const float* y, size_t n, size_t np, size_t ns, size_t nt, size_t nl, int isdeg, int itdeg, int ildeg, size_t nsjump, size_t ntjump, size_t nljump, size_t ni, bool userw, float* rw, float* season, float* trend, float* work1, float* work2, float* work3, float* work4, float* work5) {
345
+ template<typename T>
346
+ void onestp(
347
+ std::span<const T> y,
348
+ size_t np,
349
+ size_t ns,
350
+ size_t nt,
351
+ size_t nl,
352
+ int isdeg,
353
+ int itdeg,
354
+ int ildeg,
355
+ size_t nsjump,
356
+ size_t ntjump,
357
+ size_t nljump,
358
+ size_t ni,
359
+ bool userw,
360
+ std::vector<T>& rw,
361
+ std::vector<T>& season,
362
+ std::vector<T>& trend,
363
+ std::vector<T>& work1,
364
+ std::vector<T>& work2,
365
+ std::vector<T>& work3,
366
+ std::vector<T>& work4,
367
+ std::vector<T>& work5
368
+ ) {
369
+ size_t n = y.size();
370
+
254
371
  for (size_t j = 0; j < ni; j++) {
255
- for (size_t i = 0; i < n; i++) {
256
- work1[i] = y[i] - trend[i];
372
+ // TODO use std::views::zip for C++23
373
+ for (size_t i = 0; i < y.size(); i++) {
374
+ work1.at(i) = span_at(y, i) - trend.at(i);
257
375
  }
258
376
 
259
377
  ss(work1, n, np, ns, isdeg, nsjump, userw, rw, work2, work3, work4, work5, season);
260
378
  fts(work2, n + 2 * np, np, work3, work1);
261
- ess(work3, n, nl, ildeg, nljump, false, work4, work1, work5);
379
+ ess(work3, n, nl, ildeg, nljump, false, work4, std::span{work1}, work5);
380
+ // TODO use std::views::zip for C++23
262
381
  for (size_t i = 0; i < n; i++) {
263
- season[i] = work2[np + i] - work1[i];
382
+ season.at(i) = work2.at(np + i) - work1.at(i);
264
383
  }
265
- for (size_t i = 0; i < n; i++) {
266
- work1[i] = y[i] - season[i];
384
+ // TODO use std::views::zip for C++23
385
+ for (size_t i = 0; i < y.size(); i++) {
386
+ work1.at(i) = span_at(y, i) - season.at(i);
267
387
  }
268
- ess(work1, n, nt, itdeg, ntjump, userw, rw, trend, work3);
388
+ ess(work1, n, nt, itdeg, ntjump, userw, rw, std::span{trend}, work3);
269
389
  }
270
390
  }
271
391
 
272
- void stl(const float* y, size_t n, size_t np, size_t ns, size_t nt, size_t nl, int isdeg, int itdeg, int ildeg, size_t nsjump, size_t ntjump, size_t nljump, size_t ni, size_t no, float* rw, float* season, float* trend) {
392
+ template<typename T>
393
+ void stl(
394
+ std::span<const T> y,
395
+ size_t np,
396
+ size_t ns,
397
+ size_t nt,
398
+ size_t nl,
399
+ int isdeg,
400
+ int itdeg,
401
+ int ildeg,
402
+ size_t nsjump,
403
+ size_t ntjump,
404
+ size_t nljump,
405
+ size_t ni,
406
+ size_t no,
407
+ std::vector<T>& rw,
408
+ std::vector<T>& season,
409
+ std::vector<T>& trend
410
+ ) {
411
+ size_t n = y.size();
412
+
273
413
  if (ns < 3) {
274
- throw std::invalid_argument("seasonal_length must be at least 3");
414
+ throw std::invalid_argument{"seasonal_length must be at least 3"};
275
415
  }
276
416
  if (nt < 3) {
277
- throw std::invalid_argument("trend_length must be at least 3");
417
+ throw std::invalid_argument{"trend_length must be at least 3"};
278
418
  }
279
419
  if (nl < 3) {
280
- throw std::invalid_argument("low_pass_length must be at least 3");
420
+ throw std::invalid_argument{"low_pass_length must be at least 3"};
281
421
  }
282
422
  if (np < 2) {
283
- throw std::invalid_argument("period must be at least 2");
423
+ throw std::invalid_argument{"period must be at least 2"};
284
424
  }
285
425
 
286
426
  if (isdeg != 0 && isdeg != 1) {
287
- throw std::invalid_argument("seasonal_degree must be 0 or 1");
427
+ throw std::invalid_argument{"seasonal_degree must be 0 or 1"};
288
428
  }
289
429
  if (itdeg != 0 && itdeg != 1) {
290
- throw std::invalid_argument("trend_degree must be 0 or 1");
430
+ throw std::invalid_argument{"trend_degree must be 0 or 1"};
291
431
  }
292
432
  if (ildeg != 0 && ildeg != 1) {
293
- throw std::invalid_argument("low_pass_degree must be 0 or 1");
433
+ throw std::invalid_argument{"low_pass_degree must be 0 or 1"};
294
434
  }
295
435
 
296
436
  if (ns % 2 != 1) {
297
- throw std::invalid_argument("seasonal_length must be odd");
437
+ throw std::invalid_argument{"seasonal_length must be odd"};
298
438
  }
299
439
  if (nt % 2 != 1) {
300
- throw std::invalid_argument("trend_length must be odd");
440
+ throw std::invalid_argument{"trend_length must be odd"};
301
441
  }
302
442
  if (nl % 2 != 1) {
303
- throw std::invalid_argument("low_pass_length must be odd");
443
+ throw std::invalid_argument{"low_pass_length must be odd"};
304
444
  }
305
445
 
306
- auto work1 = std::vector<float>(n + 2 * np);
307
- auto work2 = std::vector<float>(n + 2 * np);
308
- auto work3 = std::vector<float>(n + 2 * np);
309
- auto work4 = std::vector<float>(n + 2 * np);
310
- auto work5 = std::vector<float>(n + 2 * np);
446
+ std::vector<T> work1(n + 2 * np);
447
+ std::vector<T> work2(n + 2 * np);
448
+ std::vector<T> work3(n + 2 * np);
449
+ std::vector<T> work4(n + 2 * np);
450
+ std::vector<T> work5(n + 2 * np);
311
451
 
312
- auto userw = false;
452
+ bool userw = false;
313
453
  size_t k = 0;
314
454
 
315
455
  while (true) {
316
- onestp(y, n, np, ns, nt, nl, isdeg, itdeg, ildeg, nsjump, ntjump, nljump, ni, userw, rw, season, trend, work1.data(), work2.data(), work3.data(), work4.data(), work5.data());
456
+ onestp(
457
+ y,
458
+ np,
459
+ ns,
460
+ nt,
461
+ nl,
462
+ isdeg,
463
+ itdeg,
464
+ ildeg,
465
+ nsjump,
466
+ ntjump,
467
+ nljump,
468
+ ni,
469
+ userw,
470
+ rw,
471
+ season,
472
+ trend,
473
+ work1,
474
+ work2,
475
+ work3,
476
+ work4,
477
+ work5
478
+ );
317
479
  k += 1;
318
480
  if (k > no) {
319
481
  break;
320
482
  }
321
483
  for (size_t i = 0; i < n; i++) {
322
- work1[i] = trend[i] + season[i];
484
+ work1.at(i) = trend.at(i) + season.at(i);
323
485
  }
324
- rwts(y, n, work1.data(), rw);
486
+ rwts(y, work1, rw);
325
487
  userw = true;
326
488
  }
327
489
 
328
490
  if (no <= 0) {
329
491
  for (size_t i = 0; i < n; i++) {
330
- rw[i] = 1.0;
492
+ rw.at(i) = 1.0;
331
493
  }
332
494
  }
333
495
  }
334
496
 
335
- float var(const std::vector<float>& series) {
336
- auto mean = std::accumulate(series.begin(), series.end(), 0.0) / series.size();
337
- std::vector<float> tmp;
338
- tmp.reserve(series.size());
497
+ template<typename T>
498
+ double var(const std::vector<T>& series) {
499
+ double mean = std::accumulate(series.begin(), series.end(), 0.0)
500
+ / static_cast<double>(series.size());
501
+ double sum = 0.0;
339
502
  for (auto v : series) {
340
- tmp.push_back(pow(v - mean, 2));
503
+ double diff = v - mean;
504
+ sum += diff * diff;
341
505
  }
342
- return std::accumulate(tmp.begin(), tmp.end(), 0.0) / (series.size() - 1);
506
+ return sum / static_cast<double>(series.size() - 1);
343
507
  }
344
508
 
345
- class StlResult {
346
- public:
347
- std::vector<float> seasonal;
348
- std::vector<float> trend;
349
- std::vector<float> remainder;
350
- std::vector<float> weights;
351
-
352
- inline float seasonal_strength() {
353
- std::vector<float> sr;
354
- sr.reserve(remainder.size());
355
- for (size_t i = 0; i < remainder.size(); i++) {
356
- sr.push_back(seasonal[i] + remainder[i]);
357
- }
358
- return std::max(0.0, 1.0 - var(remainder) / var(sr));
509
+ template<typename T>
510
+ double strength(const std::vector<T>& component, const std::vector<T>& remainder) {
511
+ std::vector<T> sr;
512
+ sr.reserve(remainder.size());
513
+ for (size_t i = 0; i < remainder.size(); i++) {
514
+ sr.push_back(component.at(i) + remainder.at(i));
359
515
  }
516
+ return std::max(0.0, 1.0 - var(remainder) / var(sr));
517
+ }
360
518
 
361
- inline float trend_strength() {
362
- std::vector<float> tr;
363
- tr.reserve(remainder.size());
364
- for (size_t i = 0; i < remainder.size(); i++) {
365
- tr.push_back(trend[i] + remainder[i]);
366
- }
367
- return std::max(0.0, 1.0 - var(remainder) / var(tr));
368
- }
519
+ } // namespace detail
520
+
521
+ /// A set of STL parameters.
522
+ struct StlParams {
523
+ /// Sets the length of the seasonal smoother.
524
+ std::optional<size_t> seasonal_length = std::nullopt;
525
+ /// Sets the length of the trend smoother.
526
+ std::optional<size_t> trend_length = std::nullopt;
527
+ /// Sets the length of the low-pass filter.
528
+ std::optional<size_t> low_pass_length = std::nullopt;
529
+ /// Sets the degree of locally-fitted polynomial in seasonal smoothing.
530
+ int seasonal_degree = 0;
531
+ /// Sets the degree of locally-fitted polynomial in trend smoothing.
532
+ int trend_degree = 1;
533
+ /// Sets the degree of locally-fitted polynomial in low-pass smoothing.
534
+ std::optional<int> low_pass_degree = std::nullopt;
535
+ /// Sets the skipping value for seasonal smoothing.
536
+ std::optional<size_t> seasonal_jump = std::nullopt;
537
+ /// Sets the skipping value for trend smoothing.
538
+ std::optional<size_t> trend_jump = std::nullopt;
539
+ /// Sets the skipping value for low-pass smoothing.
540
+ std::optional<size_t> low_pass_jump = std::nullopt;
541
+ /// Sets the number of loops for updating the seasonal and trend components.
542
+ std::optional<size_t> inner_loops = std::nullopt;
543
+ /// Sets the number of iterations of robust fitting.
544
+ std::optional<size_t> outer_loops = std::nullopt;
545
+ /// Sets whether robustness iterations are to be used.
546
+ bool robust = false;
369
547
  };
370
548
 
371
- class StlParams {
372
- std::optional<size_t> ns_ = std::nullopt;
373
- std::optional<size_t> nt_ = std::nullopt;
374
- std::optional<size_t> nl_ = std::nullopt;
375
- int isdeg_ = 0;
376
- int itdeg_ = 1;
377
- std::optional<int> ildeg_ = std::nullopt;
378
- std::optional<size_t> nsjump_ = std::nullopt;
379
- std::optional<size_t> ntjump_ = std::nullopt;
380
- std::optional<size_t> nljump_ = std::nullopt;
381
- std::optional<size_t> ni_ = std::nullopt;
382
- std::optional<size_t> no_ = std::nullopt;
383
- bool robust_ = false;
549
+ /// Seasonal-trend decomposition using Loess (STL).
550
+ template<typename T = float>
551
+ class Stl {
552
+ public:
553
+ /// Decomposes a time series from a vector.
554
+ Stl(const std::vector<T>& series, size_t period, const StlParams& params = StlParams());
384
555
 
385
- public:
386
- inline StlParams seasonal_length(size_t ns) {
387
- this->ns_ = ns;
388
- return *this;
556
+ /// Decomposes a time series from a span.
557
+ Stl(std::span<const T> series, size_t period, const StlParams& params = StlParams());
558
+
559
+ /// Returns the seasonal component.
560
+ const std::vector<T>& seasonal() const {
561
+ return seasonal_;
389
562
  }
390
563
 
391
- inline StlParams trend_length(size_t nt) {
392
- this->nt_ = nt;
393
- return *this;
564
+ /// Returns the trend component.
565
+ const std::vector<T>& trend() const {
566
+ return trend_;
394
567
  }
395
568
 
396
- inline StlParams low_pass_length(size_t nl) {
397
- this->nl_ = nl;
398
- return *this;
569
+ /// Returns the remainder.
570
+ const std::vector<T>& remainder() const {
571
+ return remainder_;
399
572
  }
400
573
 
401
- inline StlParams seasonal_degree(int isdeg) {
402
- this->isdeg_ = isdeg;
403
- return *this;
574
+ /// Returns the weights.
575
+ const std::vector<T>& weights() const {
576
+ return weights_;
404
577
  }
405
578
 
406
- inline StlParams trend_degree(int itdeg) {
407
- this->itdeg_ = itdeg;
408
- return *this;
579
+ /// Returns the seasonal strength.
580
+ double seasonal_strength() const {
581
+ return detail::strength(seasonal_, remainder_);
409
582
  }
410
583
 
411
- inline StlParams low_pass_degree(int ildeg) {
412
- this->ildeg_ = ildeg;
413
- return *this;
584
+ /// Returns the trend strength.
585
+ double trend_strength() const {
586
+ return detail::strength(trend_, remainder_);
414
587
  }
415
588
 
416
- inline StlParams seasonal_jump(size_t nsjump) {
417
- this->nsjump_ = nsjump;
418
- return *this;
589
+ private:
590
+ std::vector<T> seasonal_;
591
+ std::vector<T> trend_;
592
+ std::vector<T> remainder_;
593
+ std::vector<T> weights_;
594
+ };
595
+
596
+ template<typename T>
597
+ Stl<T>::Stl(std::span<const T> series, size_t period, const StlParams& params) {
598
+ std::span<const T> y = series;
599
+ size_t np = period;
600
+ size_t n = series.size();
601
+
602
+ if (n / 2 < np) {
603
+ throw std::invalid_argument{"series has less than two periods"};
419
604
  }
420
605
 
421
- inline StlParams trend_jump(size_t ntjump) {
422
- this->ntjump_ = ntjump;
423
- return *this;
606
+ size_t ns = params.seasonal_length.value_or(np);
607
+
608
+ int isdeg = params.seasonal_degree;
609
+ int itdeg = params.trend_degree;
610
+
611
+ std::vector<T> seasonal(n);
612
+ std::vector<T> trend(n);
613
+ std::vector<T> remainder;
614
+ std::vector<T> weights(n);
615
+
616
+ int ildeg = params.low_pass_degree.value_or(itdeg);
617
+ size_t newns = std::max(ns, static_cast<size_t>(3));
618
+ if (newns % 2 == 0) {
619
+ newns += 1;
424
620
  }
425
621
 
426
- inline StlParams low_pass_jump(size_t nljump) {
427
- this->nljump_ = nljump;
428
- return *this;
622
+ size_t newnp = std::max(np, static_cast<size_t>(2));
623
+ auto nt = static_cast<size_t>(
624
+ std::ceil((1.5 * static_cast<float>(newnp)) / (1.0 - 1.5 / static_cast<float>(newns)))
625
+ );
626
+ nt = params.trend_length.value_or(nt);
627
+ nt = std::max(nt, static_cast<size_t>(3));
628
+ if (nt % 2 == 0) {
629
+ nt += 1;
429
630
  }
430
631
 
431
- inline StlParams inner_loops(bool ni) {
432
- this->ni_ = ni;
433
- return *this;
632
+ size_t nl = params.low_pass_length.value_or(newnp);
633
+ if (nl % 2 == 0 && !params.low_pass_length.has_value()) {
634
+ nl += 1;
434
635
  }
435
636
 
436
- inline StlParams outer_loops(bool no) {
437
- this->no_ = no;
438
- return *this;
637
+ size_t ni = params.inner_loops.value_or(params.robust ? 1 : 2);
638
+ size_t no = params.outer_loops.value_or(params.robust ? 15 : 0);
639
+
640
+ size_t nsjump = params.seasonal_jump.value_or(
641
+ static_cast<size_t>(std::ceil(static_cast<float>(newns) / 10.0))
642
+ );
643
+ size_t ntjump = params.trend_jump.value_or(
644
+ static_cast<size_t>(std::ceil(static_cast<float>(nt) / 10.0))
645
+ );
646
+ size_t nljump = params.low_pass_jump.value_or(
647
+ static_cast<size_t>(std::ceil(static_cast<float>(nl) / 10.0))
648
+ );
649
+
650
+ detail::stl(
651
+ y,
652
+ newnp,
653
+ newns,
654
+ nt,
655
+ nl,
656
+ isdeg,
657
+ itdeg,
658
+ ildeg,
659
+ nsjump,
660
+ ntjump,
661
+ nljump,
662
+ ni,
663
+ no,
664
+ weights,
665
+ seasonal,
666
+ trend
667
+ );
668
+
669
+ remainder.reserve(n);
670
+ // TODO use std::views::zip for C++23
671
+ for (size_t i = 0; i < y.size(); i++) {
672
+ remainder.push_back(detail::span_at(y, i) - seasonal.at(i) - trend.at(i));
673
+ }
674
+
675
+ seasonal_ = std::move(seasonal);
676
+ trend_ = std::move(trend);
677
+ remainder_ = std::move(remainder);
678
+ weights_ = std::move(weights);
679
+ }
680
+
681
+ template<typename T>
682
+ Stl<T>::Stl(const std::vector<T>& series, size_t period, const StlParams& params) :
683
+ Stl(std::span{series}, period, params) {}
684
+
685
+ /// A set of MSTL parameters.
686
+ struct MstlParams {
687
+ /// Sets the number of iterations.
688
+ size_t iterations = 2;
689
+ /// Sets lambda for Box-Cox transformation.
690
+ std::optional<float> lambda = std::nullopt;
691
+ /// Sets the lengths of the seasonal smoothers.
692
+ std::optional<std::vector<size_t>> seasonal_lengths = std::nullopt;
693
+ /// Sets the STL parameters.
694
+ StlParams stl_params = StlParams();
695
+ };
696
+
697
+ /// Multiple seasonal-trend decomposition using Loess (MSTL).
698
+ template<typename T = float>
699
+ class Mstl {
700
+ public:
701
+ /// Decomposes a time series from a vector.
702
+ Mstl(
703
+ const std::vector<T>& series,
704
+ const std::vector<size_t>& periods,
705
+ const MstlParams& params = MstlParams()
706
+ );
707
+
708
+ /// Decomposes a time series from a span.
709
+ Mstl(
710
+ std::span<const T> series,
711
+ std::span<const size_t> periods,
712
+ const MstlParams& params = MstlParams()
713
+ );
714
+
715
+ /// Returns the seasonal component.
716
+ const std::vector<std::vector<T>>& seasonal() const {
717
+ return seasonal_;
718
+ }
719
+
720
+ /// Returns the trend component.
721
+ const std::vector<T>& trend() const {
722
+ return trend_;
723
+ }
724
+
725
+ /// Returns the remainder.
726
+ const std::vector<T>& remainder() const {
727
+ return remainder_;
728
+ }
729
+
730
+ /// Returns the seasonal strength.
731
+ std::vector<double> seasonal_strength() const {
732
+ std::vector<double> res;
733
+ res.reserve(seasonal_.size());
734
+ for (const auto& s : seasonal_) {
735
+ res.push_back(detail::strength(s, remainder_));
736
+ }
737
+ return res;
439
738
  }
440
739
 
441
- inline StlParams robust(bool robust) {
442
- this->robust_ = robust;
443
- return *this;
740
+ /// Returns the trend strength.
741
+ double trend_strength() const {
742
+ return detail::strength(trend_, remainder_);
444
743
  }
445
744
 
446
- StlResult fit(const float* y, size_t n, size_t np);
447
- StlResult fit(const std::vector<float>& y, size_t np);
745
+ private:
746
+ std::vector<std::vector<T>> seasonal_;
747
+ std::vector<T> trend_;
748
+ std::vector<T> remainder_;
448
749
  };
449
750
 
450
- StlParams params() {
451
- return StlParams();
452
- }
751
+ namespace detail {
453
752
 
454
- StlResult StlParams::fit(const float* y, size_t n, size_t np) {
455
- if (n < 2 * np) {
456
- throw std::invalid_argument("series has less than two periods");
753
+ template<typename T>
754
+ std::vector<T> box_cox(std::span<const T> y, float lambda) {
755
+ std::vector<T> res;
756
+ res.reserve(y.size());
757
+ if (lambda != 0.0) {
758
+ for (auto yi : y) {
759
+ res.push_back(static_cast<T>(std::pow(yi, lambda) - 1.0) / lambda);
760
+ }
761
+ } else {
762
+ for (auto yi : y) {
763
+ res.push_back(std::log(yi));
764
+ }
457
765
  }
766
+ return res;
767
+ }
458
768
 
459
- auto ns = this->ns_.value_or(np);
769
+ template<typename T>
770
+ std::tuple<std::vector<T>, std::vector<T>, std::vector<std::vector<T>>> mstl(
771
+ std::span<const T> x,
772
+ std::span<const size_t> seas_ids,
773
+ size_t iterate,
774
+ std::optional<float> lambda,
775
+ const std::optional<std::vector<size_t>>& swin,
776
+ const StlParams& stl_params
777
+ ) {
778
+ // keep track of indices instead of sorting seas_ids
779
+ // so order is preserved with seasonality
780
+ std::vector<size_t> indices(seas_ids.size());
781
+ std::iota(indices.begin(), indices.end(), 0);
782
+ std::ranges::sort(indices, [&seas_ids](size_t a, size_t b) {
783
+ return span_at(seas_ids, a) < span_at(seas_ids, b);
784
+ });
785
+
786
+ if (seas_ids.size() == 1) {
787
+ iterate = 1;
788
+ }
789
+
790
+ std::vector<std::vector<T>> seasonality;
791
+ seasonality.reserve(seas_ids.size());
792
+ std::vector<T> trend;
793
+
794
+ std::vector<T> deseas = lambda.has_value()
795
+ ? box_cox(x, lambda.value())
796
+ : std::vector<T>(x.begin(), x.end());
797
+
798
+ if (!seas_ids.empty()) {
799
+ for (size_t i = 0; i < seas_ids.size(); i++) {
800
+ seasonality.push_back(std::vector<T>());
801
+ }
460
802
 
461
- auto isdeg = this->isdeg_;
462
- auto itdeg = this->itdeg_;
803
+ for (size_t j = 0; j < iterate; j++) {
804
+ for (size_t i = 0; i < indices.size(); i++) {
805
+ size_t idx = indices.at(i);
463
806
 
464
- auto res = StlResult {
465
- std::vector<float>(n),
466
- std::vector<float>(n),
467
- std::vector<float>(),
468
- std::vector<float>(n)
469
- };
807
+ if (j > 0) {
808
+ for (size_t ii = 0; ii < deseas.size(); ii++) {
809
+ deseas.at(ii) += seasonality.at(idx).at(ii);
810
+ }
811
+ }
470
812
 
471
- auto ildeg = this->ildeg_.value_or(itdeg);
472
- auto newns = std::max(ns, (size_t) 3);
473
- if (newns % 2 == 0) {
474
- newns += 1;
475
- }
813
+ StlParams params = stl_params;
814
+ if (swin) {
815
+ params.seasonal_length = swin.value().at(idx);
816
+ } else if (!stl_params.seasonal_length.has_value()) {
817
+ params.seasonal_length = 7 + 4 * (i + 1);
818
+ }
819
+ Stl<T> fit{deseas, span_at(seas_ids, idx), params};
476
820
 
477
- auto newnp = std::max(np, (size_t) 2);
478
- auto nt = (size_t) ceil((1.5 * newnp) / (1.0 - 1.5 / (float) newns));
479
- nt = this->nt_.value_or(nt);
480
- nt = std::max(nt, (size_t) 3);
481
- if (nt % 2 == 0) {
482
- nt += 1;
821
+ seasonality.at(idx) = fit.seasonal();
822
+ trend = fit.trend();
823
+
824
+ for (size_t ii = 0; ii < deseas.size(); ii++) {
825
+ deseas.at(ii) -= seasonality.at(idx).at(ii);
826
+ }
827
+ }
828
+ }
829
+ } else {
830
+ // TODO use Friedman's Super Smoother for trend
831
+ throw std::invalid_argument{"periods must not be empty"};
483
832
  }
484
833
 
485
- auto nl = this->nl_.value_or(newnp);
486
- if (nl % 2 == 0 && !this->nl_.has_value()) {
487
- nl += 1;
834
+ std::vector<T> remainder;
835
+ remainder.reserve(x.size());
836
+ for (size_t i = 0; i < x.size(); i++) {
837
+ remainder.push_back(deseas.at(i) - trend.at(i));
488
838
  }
489
839
 
490
- auto ni = this->ni_.value_or(this->robust_ ? 1 : 2);
491
- auto no = this->no_.value_or(this->robust_ ? 15 : 0);
840
+ return std::make_tuple(trend, remainder, seasonality);
841
+ }
492
842
 
493
- auto nsjump = this->nsjump_.value_or((size_t) ceil(((float) newns) / 10.0));
494
- auto ntjump = this->ntjump_.value_or((size_t) ceil(((float) nt) / 10.0));
495
- auto nljump = this->nljump_.value_or((size_t) ceil(((float) nl) / 10.0));
843
+ } // namespace detail
844
+
845
+ template<typename T>
846
+ Mstl<T>::Mstl(
847
+ std::span<const T> series,
848
+ std::span<const size_t> periods,
849
+ const MstlParams& params
850
+ ) {
851
+ // return error to be consistent with stl
852
+ // and ensure seasonal is always same length as periods
853
+ for (auto v : periods) {
854
+ if (v < 2) {
855
+ throw std::invalid_argument{"periods must be at least 2"};
856
+ }
857
+ }
496
858
 
497
- stl(y, n, newnp, newns, nt, nl, isdeg, itdeg, ildeg, nsjump, ntjump, nljump, ni, no, res.weights.data(), res.seasonal.data(), res.trend.data());
859
+ // return error to be consistent with stl
860
+ // and ensure seasonal is always same length as periods
861
+ for (auto v : periods) {
862
+ if (series.size() < v * 2) {
863
+ throw std::invalid_argument{"series has less than two periods"};
864
+ }
865
+ }
498
866
 
499
- res.remainder.reserve(n);
500
- for (size_t i = 0; i < n; i++) {
501
- res.remainder.push_back(y[i] - res.seasonal[i] - res.trend[i]);
867
+ if (params.lambda.has_value()) {
868
+ float lambda = params.lambda.value();
869
+ if (lambda < 0 || lambda > 1) {
870
+ throw std::invalid_argument{"lambda must be between 0 and 1"};
871
+ }
502
872
  }
503
873
 
504
- return res;
505
- }
874
+ if (params.seasonal_lengths.has_value()) {
875
+ if (params.seasonal_lengths.value().size() != periods.size()) {
876
+ throw std::invalid_argument{"seasonal_lengths must have the same length as periods"};
877
+ }
878
+ }
506
879
 
507
- StlResult StlParams::fit(const std::vector<float>& y, size_t np) {
508
- return StlParams::fit(y.data(), y.size(), np);
509
- }
880
+ auto [trend, remainder, seasonal] = detail::mstl(
881
+ series,
882
+ periods,
883
+ params.iterations,
884
+ params.lambda,
885
+ params.seasonal_lengths,
886
+ params.stl_params
887
+ );
510
888
 
889
+ seasonal_ = std::move(seasonal);
890
+ trend_ = std::move(trend);
891
+ remainder_ = std::move(remainder);
511
892
  }
893
+
894
+ template<typename T>
895
+ Mstl<T>::Mstl(
896
+ const std::vector<T>& series,
897
+ const std::vector<size_t>& periods,
898
+ const MstlParams& params
899
+ ) :
900
+ Mstl(std::span{series}, std::span{periods}, params) {}
901
+
902
+ } // namespace stl