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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/NOTICE.txt +1 -1
- data/README.md +4 -4
- data/ext/anomaly_detection/anomaly_detection.hpp +158 -101
- data/ext/anomaly_detection/dist.h +31 -9
- data/ext/anomaly_detection/ext.cpp +24 -14
- data/ext/anomaly_detection/extconf.rb +1 -1
- data/ext/anomaly_detection/stl.hpp +638 -247
- data/lib/anomaly_detection/version.rb +1 -1
- data/licenses/LICENSE-MIT-dist-h.txt +1 -1
- data/licenses/LICENSE-MIT-stl-cpp.txt +1 -1
- data/licenses/NOTICE-AnomalyDetection-cpp.txt +1 -1
- metadata +6 -10
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
* STL C++ v0.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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 += (
|
|
61
|
+
h += static_cast<T>((len - n) / 2);
|
|
30
62
|
}
|
|
31
63
|
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
for (
|
|
38
|
-
w
|
|
39
|
-
|
|
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
|
|
74
|
+
w.at(j - 1) = 1.0;
|
|
43
75
|
} else {
|
|
44
|
-
w
|
|
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
|
|
79
|
+
w.at(j - 1) *= rw.at(j - 1);
|
|
48
80
|
}
|
|
49
|
-
a += w
|
|
81
|
+
a += w.at(j - 1);
|
|
50
82
|
}
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
if (a <= 0.0) {
|
|
54
86
|
return false;
|
|
55
|
-
} else {
|
|
56
|
-
|
|
57
|
-
|
|
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) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
for (
|
|
68
|
-
c += w
|
|
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 (
|
|
75
|
-
w
|
|
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
|
-
|
|
81
|
-
for (
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
155
|
+
span_at(ys, i - 1) = y.at(i - 1);
|
|
106
156
|
}
|
|
107
157
|
}
|
|
108
|
-
} else if (newnj == 1) {
|
|
109
|
-
|
|
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++) {
|
|
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
|
-
|
|
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
|
|
174
|
+
span_at(ys, i - 1) = y.at(i - 1);
|
|
120
175
|
}
|
|
121
176
|
}
|
|
122
|
-
} else {
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
145
|
-
for (
|
|
146
|
-
ys
|
|
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
|
-
|
|
209
|
+
size_t k = ((n - 1) / newnj) * newnj + 1;
|
|
150
210
|
if (k != n) {
|
|
151
|
-
|
|
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
|
|
216
|
+
span_at(ys, n - 1) = y.at(n - 1);
|
|
154
217
|
}
|
|
155
218
|
if (k != n - 1) {
|
|
156
|
-
|
|
157
|
-
for (
|
|
158
|
-
ys
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
auto
|
|
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
|
|
236
|
+
v += x.at(i);
|
|
173
237
|
}
|
|
174
238
|
|
|
175
|
-
ave
|
|
239
|
+
ave.at(0) = static_cast<T>(v / flen);
|
|
176
240
|
if (newn > 1) {
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
182
|
-
ave
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
|
278
|
+
std::ranges::sort(rw);
|
|
205
279
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
|
288
|
+
rw.at(i) = 1.0;
|
|
214
289
|
} else if (r <= c9) {
|
|
215
|
-
rw
|
|
290
|
+
rw.at(i) = static_cast<T>(std::pow(1.0 - std::pow(r / cmad, 2.0), 2.0));
|
|
216
291
|
} else {
|
|
217
|
-
rw
|
|
292
|
+
rw.at(i) = 0.0;
|
|
218
293
|
}
|
|
219
294
|
}
|
|
220
295
|
}
|
|
221
296
|
|
|
222
|
-
|
|
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
|
|
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
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
329
|
+
work2.at(0) = work2.at(1);
|
|
240
330
|
}
|
|
241
|
-
xs = k + 1;
|
|
242
|
-
size_t nleft =
|
|
243
|
-
|
|
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
|
|
337
|
+
work2.at(k + 1) = work2.at(k);
|
|
246
338
|
}
|
|
247
339
|
for (size_t m = 1; m <= k + 2; m++) {
|
|
248
|
-
season
|
|
340
|
+
season.at((m - 1) * np + j - 1) = work2.at(m - 1);
|
|
249
341
|
}
|
|
250
342
|
}
|
|
251
343
|
}
|
|
252
344
|
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
382
|
+
season.at(i) = work2.at(np + i) - work1.at(i);
|
|
264
383
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
414
|
+
throw std::invalid_argument{"seasonal_length must be at least 3"};
|
|
275
415
|
}
|
|
276
416
|
if (nt < 3) {
|
|
277
|
-
throw std::invalid_argument
|
|
417
|
+
throw std::invalid_argument{"trend_length must be at least 3"};
|
|
278
418
|
}
|
|
279
419
|
if (nl < 3) {
|
|
280
|
-
throw std::invalid_argument
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
437
|
+
throw std::invalid_argument{"seasonal_length must be odd"};
|
|
298
438
|
}
|
|
299
439
|
if (nt % 2 != 1) {
|
|
300
|
-
throw std::invalid_argument
|
|
440
|
+
throw std::invalid_argument{"trend_length must be odd"};
|
|
301
441
|
}
|
|
302
442
|
if (nl % 2 != 1) {
|
|
303
|
-
throw std::invalid_argument
|
|
443
|
+
throw std::invalid_argument{"low_pass_length must be odd"};
|
|
304
444
|
}
|
|
305
445
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
452
|
+
bool userw = false;
|
|
313
453
|
size_t k = 0;
|
|
314
454
|
|
|
315
455
|
while (true) {
|
|
316
|
-
onestp(
|
|
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
|
|
484
|
+
work1.at(i) = trend.at(i) + season.at(i);
|
|
323
485
|
}
|
|
324
|
-
rwts(y,
|
|
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
|
|
492
|
+
rw.at(i) = 1.0;
|
|
331
493
|
}
|
|
332
494
|
}
|
|
333
495
|
}
|
|
334
496
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
std::
|
|
338
|
-
|
|
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
|
-
|
|
503
|
+
double diff = v - mean;
|
|
504
|
+
sum += diff * diff;
|
|
341
505
|
}
|
|
342
|
-
return
|
|
506
|
+
return sum / static_cast<double>(series.size() - 1);
|
|
343
507
|
}
|
|
344
508
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
std::vector<
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
return
|
|
564
|
+
/// Returns the trend component.
|
|
565
|
+
const std::vector<T>& trend() const {
|
|
566
|
+
return trend_;
|
|
394
567
|
}
|
|
395
568
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return
|
|
569
|
+
/// Returns the remainder.
|
|
570
|
+
const std::vector<T>& remainder() const {
|
|
571
|
+
return remainder_;
|
|
399
572
|
}
|
|
400
573
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
return
|
|
574
|
+
/// Returns the weights.
|
|
575
|
+
const std::vector<T>& weights() const {
|
|
576
|
+
return weights_;
|
|
404
577
|
}
|
|
405
578
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return
|
|
579
|
+
/// Returns the seasonal strength.
|
|
580
|
+
double seasonal_strength() const {
|
|
581
|
+
return detail::strength(seasonal_, remainder_);
|
|
409
582
|
}
|
|
410
583
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return
|
|
584
|
+
/// Returns the trend strength.
|
|
585
|
+
double trend_strength() const {
|
|
586
|
+
return detail::strength(trend_, remainder_);
|
|
414
587
|
}
|
|
415
588
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
740
|
+
/// Returns the trend strength.
|
|
741
|
+
double trend_strength() const {
|
|
742
|
+
return detail::strength(trend_, remainder_);
|
|
444
743
|
}
|
|
445
744
|
|
|
446
|
-
|
|
447
|
-
|
|
745
|
+
private:
|
|
746
|
+
std::vector<std::vector<T>> seasonal_;
|
|
747
|
+
std::vector<T> trend_;
|
|
748
|
+
std::vector<T> remainder_;
|
|
448
749
|
};
|
|
449
750
|
|
|
450
|
-
|
|
451
|
-
return StlParams();
|
|
452
|
-
}
|
|
751
|
+
namespace detail {
|
|
453
752
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
491
|
-
|
|
840
|
+
return std::make_tuple(trend, remainder, seasonality);
|
|
841
|
+
}
|
|
492
842
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
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
|
-
|
|
508
|
-
|
|
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
|