stl-rb 0.3.0 → 0.3.1
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 +4 -0
- data/README.md +3 -3
- data/ext/stl/ext.cpp +3 -1
- data/ext/stl/stl.hpp +405 -89
- data/lib/stl/version.rb +1 -1
- metadata +5 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f27ba59a612d7502f6251944cf1f7f622ed9466b94351f888fd18ec04bb77faa
|
|
4
|
+
data.tar.gz: c7c6127cdbb8832a3d5fe89a1577c0ae20e1f8eda505b046b5f7c1520e2ae830
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e793bab429ba275161ef9d1d5566517a602d7a320a01edbe0e065e8ac21d90f6d7f70ea8e2708c583cff2e4cd848ff0955341f011cba102d2540730ecd09de10
|
|
7
|
+
data.tar.gz: 67b59c7a814295f852b3f2e26537826ef4879d33439f8603ce0d20877f6ae1254fc4f0f7b6e85fc595ff3ef1be63eb08aa5e72121572743d31ee0a93b4aa54ae
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -18,9 +18,9 @@ Decompose a time series
|
|
|
18
18
|
|
|
19
19
|
```ruby
|
|
20
20
|
series = {
|
|
21
|
-
Date.parse("
|
|
22
|
-
Date.parse("
|
|
23
|
-
Date.parse("
|
|
21
|
+
Date.parse("2025-01-01") => 100,
|
|
22
|
+
Date.parse("2025-01-02") => 150,
|
|
23
|
+
Date.parse("2025-01-03") => 136,
|
|
24
24
|
# ...
|
|
25
25
|
}
|
|
26
26
|
|
data/ext/stl/ext.cpp
CHANGED
data/ext/stl/stl.hpp
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* STL C++ v0.
|
|
2
|
+
* STL C++ v0.2.0
|
|
3
3
|
* https://github.com/ankane/stl-cpp
|
|
4
4
|
* Unlicense OR MIT License
|
|
5
5
|
*
|
|
@@ -8,6 +8,10 @@
|
|
|
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
|
|
@@ -17,16 +21,24 @@
|
|
|
17
21
|
#include <numeric>
|
|
18
22
|
#include <optional>
|
|
19
23
|
#include <stdexcept>
|
|
24
|
+
#include <tuple>
|
|
20
25
|
#include <vector>
|
|
21
26
|
|
|
27
|
+
#if __cplusplus >= 202002L
|
|
28
|
+
#include <span>
|
|
29
|
+
#endif
|
|
30
|
+
|
|
22
31
|
namespace stl {
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
namespace {
|
|
34
|
+
|
|
35
|
+
template<typename T>
|
|
36
|
+
bool est(const T* y, size_t n, size_t len, int ideg, T xs, T* ys, size_t nleft, size_t nright, T* w, bool userw, const T* rw) {
|
|
37
|
+
auto range = ((T) n) - 1.0;
|
|
38
|
+
auto h = std::max(xs - ((T) nleft), ((T) nright) - xs);
|
|
27
39
|
|
|
28
40
|
if (len > n) {
|
|
29
|
-
h += (
|
|
41
|
+
h += (T) ((len - n) / 2);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
auto h9 = 0.999 * h;
|
|
@@ -36,12 +48,12 @@ bool est(const float* y, size_t n, size_t len, int ideg, float xs, float* ys, si
|
|
|
36
48
|
auto a = 0.0;
|
|
37
49
|
for (auto j = nleft; j <= nright; j++) {
|
|
38
50
|
w[j - 1] = 0.0;
|
|
39
|
-
auto r =
|
|
51
|
+
auto r = std::abs(((T) j) - xs);
|
|
40
52
|
if (r <= h9) {
|
|
41
53
|
if (r <= h1) {
|
|
42
54
|
w[j - 1] = 1.0;
|
|
43
55
|
} else {
|
|
44
|
-
w[j - 1] = pow(1.0 - pow(r / h, 3), 3);
|
|
56
|
+
w[j - 1] = (T) std::pow(1.0 - std::pow(r / h, 3), 3);
|
|
45
57
|
}
|
|
46
58
|
if (userw) {
|
|
47
59
|
w[j - 1] *= rw[j - 1];
|
|
@@ -54,25 +66,25 @@ bool est(const float* y, size_t n, size_t len, int ideg, float xs, float* ys, si
|
|
|
54
66
|
return false;
|
|
55
67
|
} else { // weighted least squares
|
|
56
68
|
for (auto j = nleft; j <= nright; j++) { // make sum of w(j) == 1
|
|
57
|
-
w[j - 1] /= a;
|
|
69
|
+
w[j - 1] /= (T) a;
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
if (h > 0.0 && ideg > 0) { // use linear fit
|
|
61
73
|
auto a = 0.0;
|
|
62
74
|
for (auto j = nleft; j <= nright; j++) { // weighted center of x values
|
|
63
|
-
a += w[j - 1] * ((
|
|
75
|
+
a += w[j - 1] * ((T) j);
|
|
64
76
|
}
|
|
65
77
|
auto b = xs - a;
|
|
66
78
|
auto c = 0.0;
|
|
67
79
|
for (auto j = nleft; j <= nright; j++) {
|
|
68
|
-
c += w[j - 1] * pow(((
|
|
80
|
+
c += w[j - 1] * std::pow(((T) j) - a, 2);
|
|
69
81
|
}
|
|
70
|
-
if (sqrt(c) > 0.001 * range) {
|
|
82
|
+
if (std::sqrt(c) > 0.001 * range) {
|
|
71
83
|
b /= c;
|
|
72
84
|
|
|
73
85
|
// points are spread out enough to compute slope
|
|
74
86
|
for (auto j = nleft; j <= nright; j++) {
|
|
75
|
-
w[j - 1] *= b * (((
|
|
87
|
+
w[j - 1] *= (T) (b * (((T) j) - a) + 1.0);
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
90
|
}
|
|
@@ -86,7 +98,8 @@ bool est(const float* y, size_t n, size_t len, int ideg, float xs, float* ys, si
|
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
template<typename T>
|
|
102
|
+
void ess(const T* y, size_t n, size_t len, int ideg, size_t njump, bool userw, const T* rw, T* ys, T* res) {
|
|
90
103
|
if (n < 2) {
|
|
91
104
|
ys[0] = y[0];
|
|
92
105
|
return;
|
|
@@ -100,7 +113,7 @@ void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool user
|
|
|
100
113
|
nleft = 1;
|
|
101
114
|
nright = n;
|
|
102
115
|
for (size_t i = 1; i <= n; i += newnj) {
|
|
103
|
-
auto ok = est(y, n, len, ideg, (
|
|
116
|
+
auto ok = est(y, n, len, ideg, (T) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
|
104
117
|
if (!ok) {
|
|
105
118
|
ys[i - 1] = y[i - 1];
|
|
106
119
|
}
|
|
@@ -114,7 +127,7 @@ void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool user
|
|
|
114
127
|
nleft += 1;
|
|
115
128
|
nright += 1;
|
|
116
129
|
}
|
|
117
|
-
auto ok = est(y, n, len, ideg, (
|
|
130
|
+
auto ok = est(y, n, len, ideg, (T) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
|
118
131
|
if (!ok) {
|
|
119
132
|
ys[i - 1] = y[i - 1];
|
|
120
133
|
}
|
|
@@ -132,7 +145,7 @@ void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool user
|
|
|
132
145
|
nleft = i - nsh + 1;
|
|
133
146
|
nright = len + i - nsh;
|
|
134
147
|
}
|
|
135
|
-
auto ok = est(y, n, len, ideg, (
|
|
148
|
+
auto ok = est(y, n, len, ideg, (T) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
|
136
149
|
if (!ok) {
|
|
137
150
|
ys[i - 1] = y[i - 1];
|
|
138
151
|
}
|
|
@@ -141,60 +154,63 @@ void ess(const float* y, size_t n, size_t len, int ideg, size_t njump, bool user
|
|
|
141
154
|
|
|
142
155
|
if (newnj != 1) {
|
|
143
156
|
for (size_t i = 1; i <= n - newnj; i += newnj) {
|
|
144
|
-
auto delta = (ys[i + newnj - 1] - ys[i - 1]) / ((
|
|
157
|
+
auto delta = (ys[i + newnj - 1] - ys[i - 1]) / ((T) newnj);
|
|
145
158
|
for (auto j = i + 1; j <= i + newnj - 1; j++) {
|
|
146
|
-
ys[j - 1] = ys[i - 1] + delta * ((
|
|
159
|
+
ys[j - 1] = ys[i - 1] + delta * ((T) (j - i));
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
162
|
auto k = ((n - 1) / newnj) * newnj + 1;
|
|
150
163
|
if (k != n) {
|
|
151
|
-
auto ok = est(y, n, len, ideg, (
|
|
164
|
+
auto ok = est(y, n, len, ideg, (T) n, &ys[n - 1], nleft, nright, res, userw, rw);
|
|
152
165
|
if (!ok) {
|
|
153
166
|
ys[n - 1] = y[n - 1];
|
|
154
167
|
}
|
|
155
168
|
if (k != n - 1) {
|
|
156
|
-
auto delta = (ys[n - 1] - ys[k - 1]) / ((
|
|
169
|
+
auto delta = (ys[n - 1] - ys[k - 1]) / ((T) (n - k));
|
|
157
170
|
for (auto j = k + 1; j <= n - 1; j++) {
|
|
158
|
-
ys[j - 1] = ys[k - 1] + delta * ((
|
|
171
|
+
ys[j - 1] = ys[k - 1] + delta * ((T) (j - k));
|
|
159
172
|
}
|
|
160
173
|
}
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
|
|
165
|
-
|
|
178
|
+
template<typename T>
|
|
179
|
+
void ma(const T* x, size_t n, size_t len, T* ave) {
|
|
166
180
|
auto newn = n - len + 1;
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
double flen = (T) len;
|
|
182
|
+
double v = 0.0;
|
|
169
183
|
|
|
170
184
|
// get the first average
|
|
171
185
|
for (size_t i = 0; i < len; i++) {
|
|
172
186
|
v += x[i];
|
|
173
187
|
}
|
|
174
188
|
|
|
175
|
-
ave[0] = v / flen;
|
|
189
|
+
ave[0] = (T) (v / flen);
|
|
176
190
|
if (newn > 1) {
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
size_t k = len;
|
|
192
|
+
size_t m = 0;
|
|
179
193
|
for (size_t j = 1; j < newn; j++) {
|
|
180
194
|
// window down the array
|
|
181
195
|
v = v - x[m] + x[k];
|
|
182
|
-
ave[j] = v / flen;
|
|
196
|
+
ave[j] = (T) (v / flen);
|
|
183
197
|
k += 1;
|
|
184
198
|
m += 1;
|
|
185
199
|
}
|
|
186
200
|
}
|
|
187
201
|
}
|
|
188
202
|
|
|
189
|
-
|
|
203
|
+
template<typename T>
|
|
204
|
+
void fts(const T* x, size_t n, size_t np, T* trend, T* work) {
|
|
190
205
|
ma(x, n, np, trend);
|
|
191
206
|
ma(trend, n - np + 1, np, work);
|
|
192
207
|
ma(work, n - 2 * np + 2, 3, trend);
|
|
193
208
|
}
|
|
194
209
|
|
|
195
|
-
|
|
210
|
+
template<typename T>
|
|
211
|
+
void rwts(const T* y, size_t n, const T* fit, T* rw) {
|
|
196
212
|
for (size_t i = 0; i < n; i++) {
|
|
197
|
-
rw[i] =
|
|
213
|
+
rw[i] = std::abs(y[i] - fit[i]);
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
auto mid1 = (n - 1) / 2;
|
|
@@ -208,18 +224,19 @@ void rwts(const float* y, size_t n, const float* fit, float* rw) {
|
|
|
208
224
|
auto c1 = 0.001 * cmad;
|
|
209
225
|
|
|
210
226
|
for (size_t i = 0; i < n; i++) {
|
|
211
|
-
auto r =
|
|
227
|
+
auto r = std::abs(y[i] - fit[i]);
|
|
212
228
|
if (r <= c1) {
|
|
213
229
|
rw[i] = 1.0;
|
|
214
230
|
} else if (r <= c9) {
|
|
215
|
-
rw[i] = pow(1.0 - pow(r / cmad, 2), 2);
|
|
231
|
+
rw[i] = (T) std::pow(1.0 - std::pow(r / cmad, 2), 2);
|
|
216
232
|
} else {
|
|
217
233
|
rw[i] = 0.0;
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
}
|
|
221
237
|
|
|
222
|
-
|
|
238
|
+
template<typename T>
|
|
239
|
+
void ss(const T* y, size_t n, size_t np, size_t ns, int isdeg, size_t nsjump, bool userw, T* rw, T* season, T* work1, T* work2, T* work3, T* work4) {
|
|
223
240
|
for (size_t j = 1; j <= np; j++) {
|
|
224
241
|
size_t k = (n - j) / np + 1;
|
|
225
242
|
|
|
@@ -232,14 +249,14 @@ void ss(const float* y, size_t n, size_t np, size_t ns, int isdeg, size_t nsjump
|
|
|
232
249
|
}
|
|
233
250
|
}
|
|
234
251
|
ess(work1, k, ns, isdeg, nsjump, userw, work3, work2 + 1, work4);
|
|
235
|
-
|
|
252
|
+
T xs = 0.0;
|
|
236
253
|
auto nright = std::min(ns, k);
|
|
237
254
|
auto ok = est(work1, k, ns, isdeg, xs, &work2[0], 1, nright, work4, userw, work3);
|
|
238
255
|
if (!ok) {
|
|
239
256
|
work2[0] = work2[1];
|
|
240
257
|
}
|
|
241
258
|
xs = k + 1;
|
|
242
|
-
size_t nleft = std::max(1, (int) k - (int) ns + 1);
|
|
259
|
+
size_t nleft = (size_t) std::max(1, (int) k - (int) ns + 1);
|
|
243
260
|
ok = est(work1, k, ns, isdeg, xs, &work2[k + 1], nleft, k, work4, userw, work3);
|
|
244
261
|
if (!ok) {
|
|
245
262
|
work2[k + 1] = work2[k];
|
|
@@ -250,7 +267,8 @@ void ss(const float* y, size_t n, size_t np, size_t ns, int isdeg, size_t nsjump
|
|
|
250
267
|
}
|
|
251
268
|
}
|
|
252
269
|
|
|
253
|
-
|
|
270
|
+
template<typename T>
|
|
271
|
+
void onestp(const T* 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, T* rw, T* season, T* trend, T* work1, T* work2, T* work3, T* work4, T* work5) {
|
|
254
272
|
for (size_t j = 0; j < ni; j++) {
|
|
255
273
|
for (size_t i = 0; i < n; i++) {
|
|
256
274
|
work1[i] = y[i] - trend[i];
|
|
@@ -269,7 +287,8 @@ void onestp(const float* y, size_t n, size_t np, size_t ns, size_t nt, size_t nl
|
|
|
269
287
|
}
|
|
270
288
|
}
|
|
271
289
|
|
|
272
|
-
|
|
290
|
+
template<typename T>
|
|
291
|
+
void stl(const T* 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, T* rw, T* season, T* trend) {
|
|
273
292
|
if (ns < 3) {
|
|
274
293
|
throw std::invalid_argument("seasonal_length must be at least 3");
|
|
275
294
|
}
|
|
@@ -303,11 +322,11 @@ void stl(const float* y, size_t n, size_t np, size_t ns, size_t nt, size_t nl, i
|
|
|
303
322
|
throw std::invalid_argument("low_pass_length must be odd");
|
|
304
323
|
}
|
|
305
324
|
|
|
306
|
-
auto work1 = std::vector<
|
|
307
|
-
auto work2 = std::vector<
|
|
308
|
-
auto work3 = std::vector<
|
|
309
|
-
auto work4 = std::vector<
|
|
310
|
-
auto work5 = std::vector<
|
|
325
|
+
auto work1 = std::vector<T>(n + 2 * np);
|
|
326
|
+
auto work2 = std::vector<T>(n + 2 * np);
|
|
327
|
+
auto work3 = std::vector<T>(n + 2 * np);
|
|
328
|
+
auto work4 = std::vector<T>(n + 2 * np);
|
|
329
|
+
auto work5 = std::vector<T>(n + 2 * np);
|
|
311
330
|
|
|
312
331
|
auto userw = false;
|
|
313
332
|
size_t k = 0;
|
|
@@ -332,18 +351,20 @@ void stl(const float* y, size_t n, size_t np, size_t ns, size_t nt, size_t nl, i
|
|
|
332
351
|
}
|
|
333
352
|
}
|
|
334
353
|
|
|
335
|
-
|
|
354
|
+
template<typename T>
|
|
355
|
+
double var(const std::vector<T>& series) {
|
|
336
356
|
auto mean = std::accumulate(series.begin(), series.end(), 0.0) / series.size();
|
|
337
|
-
|
|
338
|
-
tmp.reserve(series.size());
|
|
357
|
+
double sum = 0.0;
|
|
339
358
|
for (auto v : series) {
|
|
340
|
-
|
|
359
|
+
double diff = v - mean;
|
|
360
|
+
sum += diff * diff;
|
|
341
361
|
}
|
|
342
|
-
return
|
|
362
|
+
return sum / (series.size() - 1);
|
|
343
363
|
}
|
|
344
364
|
|
|
345
|
-
|
|
346
|
-
|
|
365
|
+
template<typename T>
|
|
366
|
+
double strength(const std::vector<T>& component, const std::vector<T>& remainder) {
|
|
367
|
+
std::vector<T> sr;
|
|
347
368
|
sr.reserve(remainder.size());
|
|
348
369
|
for (size_t i = 0; i < remainder.size(); i++) {
|
|
349
370
|
sr.push_back(component[i] + remainder[i]);
|
|
@@ -351,24 +372,41 @@ float strength(const std::vector<float>& component, const std::vector<float>& re
|
|
|
351
372
|
return std::max(0.0, 1.0 - var(remainder) / var(sr));
|
|
352
373
|
}
|
|
353
374
|
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// A STL result.
|
|
378
|
+
template<typename T = float>
|
|
354
379
|
class StlResult {
|
|
355
380
|
public:
|
|
356
|
-
|
|
357
|
-
std::vector<
|
|
358
|
-
|
|
359
|
-
|
|
381
|
+
/// Returns the seasonal component.
|
|
382
|
+
std::vector<T> seasonal;
|
|
383
|
+
|
|
384
|
+
/// Returns the trend component.
|
|
385
|
+
std::vector<T> trend;
|
|
360
386
|
|
|
361
|
-
|
|
387
|
+
/// Returns the remainder.
|
|
388
|
+
std::vector<T> remainder;
|
|
389
|
+
|
|
390
|
+
/// Returns the weights.
|
|
391
|
+
std::vector<T> weights;
|
|
392
|
+
|
|
393
|
+
/// Returns the seasonal strength.
|
|
394
|
+
inline double seasonal_strength() const {
|
|
362
395
|
return strength(seasonal, remainder);
|
|
363
396
|
}
|
|
364
397
|
|
|
365
|
-
|
|
398
|
+
/// Returns the trend strength.
|
|
399
|
+
inline double trend_strength() const {
|
|
366
400
|
return strength(trend, remainder);
|
|
367
401
|
}
|
|
368
402
|
};
|
|
369
403
|
|
|
404
|
+
/// A set of STL parameters.
|
|
370
405
|
class StlParams {
|
|
406
|
+
public:
|
|
407
|
+
/// @private
|
|
371
408
|
std::optional<size_t> ns_ = std::nullopt;
|
|
409
|
+
private:
|
|
372
410
|
std::optional<size_t> nt_ = std::nullopt;
|
|
373
411
|
std::optional<size_t> nl_ = std::nullopt;
|
|
374
412
|
int isdeg_ = 0;
|
|
@@ -382,75 +420,104 @@ class StlParams {
|
|
|
382
420
|
bool robust_ = false;
|
|
383
421
|
|
|
384
422
|
public:
|
|
385
|
-
|
|
386
|
-
|
|
423
|
+
/// Sets the length of the seasonal smoother.
|
|
424
|
+
inline StlParams seasonal_length(size_t length) {
|
|
425
|
+
this->ns_ = length;
|
|
387
426
|
return *this;
|
|
388
427
|
}
|
|
389
428
|
|
|
390
|
-
|
|
391
|
-
|
|
429
|
+
/// Sets the length of the trend smoother.
|
|
430
|
+
inline StlParams trend_length(size_t length) {
|
|
431
|
+
this->nt_ = length;
|
|
392
432
|
return *this;
|
|
393
433
|
}
|
|
394
434
|
|
|
395
|
-
|
|
396
|
-
|
|
435
|
+
/// Sets the length of the low-pass filter.
|
|
436
|
+
inline StlParams low_pass_length(size_t length) {
|
|
437
|
+
this->nl_ = length;
|
|
397
438
|
return *this;
|
|
398
439
|
}
|
|
399
440
|
|
|
400
|
-
|
|
401
|
-
|
|
441
|
+
/// Sets the degree of locally-fitted polynomial in seasonal smoothing.
|
|
442
|
+
inline StlParams seasonal_degree(int degree) {
|
|
443
|
+
this->isdeg_ = degree;
|
|
402
444
|
return *this;
|
|
403
445
|
}
|
|
404
446
|
|
|
405
|
-
|
|
406
|
-
|
|
447
|
+
/// Sets the degree of locally-fitted polynomial in trend smoothing.
|
|
448
|
+
inline StlParams trend_degree(int degree) {
|
|
449
|
+
this->itdeg_ = degree;
|
|
407
450
|
return *this;
|
|
408
451
|
}
|
|
409
452
|
|
|
410
|
-
|
|
411
|
-
|
|
453
|
+
/// Sets the degree of locally-fitted polynomial in low-pass smoothing.
|
|
454
|
+
inline StlParams low_pass_degree(int degree) {
|
|
455
|
+
this->ildeg_ = degree;
|
|
412
456
|
return *this;
|
|
413
457
|
}
|
|
414
458
|
|
|
415
|
-
|
|
416
|
-
|
|
459
|
+
/// Sets the skipping value for seasonal smoothing.
|
|
460
|
+
inline StlParams seasonal_jump(size_t jump) {
|
|
461
|
+
this->nsjump_ = jump;
|
|
417
462
|
return *this;
|
|
418
463
|
}
|
|
419
464
|
|
|
420
|
-
|
|
421
|
-
|
|
465
|
+
/// Sets the skipping value for trend smoothing.
|
|
466
|
+
inline StlParams trend_jump(size_t jump) {
|
|
467
|
+
this->ntjump_ = jump;
|
|
422
468
|
return *this;
|
|
423
469
|
}
|
|
424
470
|
|
|
425
|
-
|
|
426
|
-
|
|
471
|
+
/// Sets the skipping value for low-pass smoothing.
|
|
472
|
+
inline StlParams low_pass_jump(size_t jump) {
|
|
473
|
+
this->nljump_ = jump;
|
|
427
474
|
return *this;
|
|
428
475
|
}
|
|
429
476
|
|
|
430
|
-
|
|
431
|
-
|
|
477
|
+
/// Sets the number of loops for updating the seasonal and trend components.
|
|
478
|
+
inline StlParams inner_loops(size_t loops) {
|
|
479
|
+
this->ni_ = loops;
|
|
432
480
|
return *this;
|
|
433
481
|
}
|
|
434
482
|
|
|
435
|
-
|
|
436
|
-
|
|
483
|
+
/// Sets the number of iterations of robust fitting.
|
|
484
|
+
inline StlParams outer_loops(size_t loops) {
|
|
485
|
+
this->no_ = loops;
|
|
437
486
|
return *this;
|
|
438
487
|
}
|
|
439
488
|
|
|
489
|
+
/// Sets whether robustness iterations are to be used.
|
|
440
490
|
inline StlParams robust(bool robust) {
|
|
441
491
|
this->robust_ = robust;
|
|
442
492
|
return *this;
|
|
443
493
|
}
|
|
444
494
|
|
|
445
|
-
|
|
446
|
-
|
|
495
|
+
/// Decomposes a time series from an array.
|
|
496
|
+
template<typename T>
|
|
497
|
+
StlResult<T> fit(const T* series, size_t series_size, size_t period) const;
|
|
498
|
+
|
|
499
|
+
/// Decomposes a time series from a vector.
|
|
500
|
+
template<typename T>
|
|
501
|
+
StlResult<T> fit(const std::vector<T>& series, size_t period) const;
|
|
502
|
+
|
|
503
|
+
#if __cplusplus >= 202002L
|
|
504
|
+
/// Decomposes a time series from a span.
|
|
505
|
+
template<typename T>
|
|
506
|
+
StlResult<T> fit(std::span<const T> series, size_t period) const;
|
|
507
|
+
#endif
|
|
447
508
|
};
|
|
448
509
|
|
|
449
|
-
|
|
510
|
+
/// Creates a new set of STL parameters.
|
|
511
|
+
inline StlParams params() {
|
|
450
512
|
return StlParams();
|
|
451
513
|
}
|
|
452
514
|
|
|
453
|
-
|
|
515
|
+
template<typename T>
|
|
516
|
+
StlResult<T> StlParams::fit(const T* series, size_t series_size, size_t period) const {
|
|
517
|
+
auto y = series;
|
|
518
|
+
auto np = period;
|
|
519
|
+
auto n = series_size;
|
|
520
|
+
|
|
454
521
|
if (n < 2 * np) {
|
|
455
522
|
throw std::invalid_argument("series has less than two periods");
|
|
456
523
|
}
|
|
@@ -460,11 +527,11 @@ StlResult StlParams::fit(const float* y, size_t n, size_t np) {
|
|
|
460
527
|
auto isdeg = this->isdeg_;
|
|
461
528
|
auto itdeg = this->itdeg_;
|
|
462
529
|
|
|
463
|
-
auto res = StlResult {
|
|
464
|
-
std::vector<
|
|
465
|
-
std::vector<
|
|
466
|
-
std::vector<
|
|
467
|
-
std::vector<
|
|
530
|
+
auto res = StlResult<T> {
|
|
531
|
+
std::vector<T>(n),
|
|
532
|
+
std::vector<T>(n),
|
|
533
|
+
std::vector<T>(),
|
|
534
|
+
std::vector<T>(n)
|
|
468
535
|
};
|
|
469
536
|
|
|
470
537
|
auto ildeg = this->ildeg_.value_or(itdeg);
|
|
@@ -503,8 +570,257 @@ StlResult StlParams::fit(const float* y, size_t n, size_t np) {
|
|
|
503
570
|
return res;
|
|
504
571
|
}
|
|
505
572
|
|
|
506
|
-
|
|
507
|
-
|
|
573
|
+
template<typename T>
|
|
574
|
+
StlResult<T> StlParams::fit(const std::vector<T>& series, size_t period) const {
|
|
575
|
+
return StlParams::fit(series.data(), series.size(), period);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
#if __cplusplus >= 202002L
|
|
579
|
+
template<typename T>
|
|
580
|
+
StlResult<T> StlParams::fit(std::span<const T> series, size_t period) const {
|
|
581
|
+
return StlParams::fit(series.data(), series.size(), period);
|
|
582
|
+
}
|
|
583
|
+
#endif
|
|
584
|
+
|
|
585
|
+
/// A MSTL result.
|
|
586
|
+
template<typename T = float>
|
|
587
|
+
class MstlResult {
|
|
588
|
+
public:
|
|
589
|
+
/// Returns the seasonal component.
|
|
590
|
+
std::vector<std::vector<T>> seasonal;
|
|
591
|
+
|
|
592
|
+
/// Returns the trend component.
|
|
593
|
+
std::vector<T> trend;
|
|
594
|
+
|
|
595
|
+
/// Returns the remainder.
|
|
596
|
+
std::vector<T> remainder;
|
|
597
|
+
|
|
598
|
+
/// Returns the seasonal strength.
|
|
599
|
+
inline std::vector<double> seasonal_strength() const {
|
|
600
|
+
std::vector<double> res;
|
|
601
|
+
for (auto& s : seasonal) {
|
|
602
|
+
res.push_back(strength(s, remainder));
|
|
603
|
+
}
|
|
604
|
+
return res;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/// Returns the trend strength.
|
|
608
|
+
inline double trend_strength() const {
|
|
609
|
+
return strength(trend, remainder);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/// A set of MSTL parameters.
|
|
614
|
+
class MstlParams {
|
|
615
|
+
size_t iterate_ = 2;
|
|
616
|
+
std::optional<float> lambda_ = std::nullopt;
|
|
617
|
+
std::optional<std::vector<size_t>> swin_ = std::nullopt;
|
|
618
|
+
StlParams stl_params_;
|
|
619
|
+
|
|
620
|
+
public:
|
|
621
|
+
/// Sets the number of iterations.
|
|
622
|
+
inline MstlParams iterations(size_t iterations) {
|
|
623
|
+
this->iterate_ = iterations;
|
|
624
|
+
return *this;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/// Sets lambda for Box-Cox transformation.
|
|
628
|
+
inline MstlParams lambda(float lambda) {
|
|
629
|
+
this->lambda_ = lambda;
|
|
630
|
+
return *this;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/// Sets the lengths of the seasonal smoothers.
|
|
634
|
+
inline MstlParams seasonal_lengths(const std::vector<size_t>& lengths) {
|
|
635
|
+
this->swin_ = lengths;
|
|
636
|
+
return *this;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// Sets the STL parameters.
|
|
640
|
+
inline MstlParams stl_params(const StlParams& stl_params) {
|
|
641
|
+
this->stl_params_ = stl_params;
|
|
642
|
+
return *this;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/// Decomposes a time series from an array.
|
|
646
|
+
template<typename T>
|
|
647
|
+
MstlResult<T> fit(const T* series, size_t series_size, const size_t* periods, size_t periods_size) const;
|
|
648
|
+
|
|
649
|
+
/// Decomposes a time series from a vector.
|
|
650
|
+
template<typename T>
|
|
651
|
+
MstlResult<T> fit(const std::vector<T>& series, const std::vector<size_t>& periods) const;
|
|
652
|
+
|
|
653
|
+
#if __cplusplus >= 202002L
|
|
654
|
+
/// Decomposes a time series from a span.
|
|
655
|
+
template<typename T>
|
|
656
|
+
MstlResult<T> fit(std::span<const T> series, std::span<const size_t> periods) const;
|
|
657
|
+
#endif
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/// Creates a new set of MSTL parameters.
|
|
661
|
+
inline MstlParams mstl_params() {
|
|
662
|
+
return MstlParams();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
namespace {
|
|
666
|
+
|
|
667
|
+
template<typename T>
|
|
668
|
+
std::vector<T> box_cox(const T* y, size_t y_size, float lambda) {
|
|
669
|
+
std::vector<T> res;
|
|
670
|
+
res.reserve(y_size);
|
|
671
|
+
if (lambda != 0.0) {
|
|
672
|
+
for (size_t i = 0; i < y_size; i++) {
|
|
673
|
+
res.push_back((T) (std::pow(y[i], lambda) - 1.0) / lambda);
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
for (size_t i = 0; i < y_size; i++) {
|
|
677
|
+
res.push_back(std::log(y[i]));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return res;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
template<typename T>
|
|
684
|
+
std::tuple<std::vector<T>, std::vector<T>, std::vector<std::vector<T>>> mstl(
|
|
685
|
+
const T* x,
|
|
686
|
+
size_t k,
|
|
687
|
+
const size_t* seas_ids,
|
|
688
|
+
size_t seas_size,
|
|
689
|
+
size_t iterate,
|
|
690
|
+
std::optional<float> lambda,
|
|
691
|
+
const std::optional<std::vector<size_t>>& swin,
|
|
692
|
+
const StlParams& stl_params
|
|
693
|
+
) {
|
|
694
|
+
// keep track of indices instead of sorting seas_ids
|
|
695
|
+
// so order is preserved with seasonality
|
|
696
|
+
std::vector<size_t> indices;
|
|
697
|
+
for (size_t i = 0; i < seas_size; i++) {
|
|
698
|
+
indices.push_back(i);
|
|
699
|
+
}
|
|
700
|
+
std::sort(indices.begin(), indices.end(), [&seas_ids](size_t a, size_t b) {
|
|
701
|
+
return seas_ids[a] < seas_ids[b];
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (seas_size == 1) {
|
|
705
|
+
iterate = 1;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
std::vector<std::vector<T>> seasonality;
|
|
709
|
+
seasonality.reserve(seas_size);
|
|
710
|
+
std::vector<T> trend;
|
|
711
|
+
|
|
712
|
+
auto deseas = lambda.has_value() ? box_cox(x, k, lambda.value()) : std::vector<T>(x, x + k);
|
|
713
|
+
|
|
714
|
+
if (seas_size != 0) {
|
|
715
|
+
for (size_t i = 0; i < seas_size; i++) {
|
|
716
|
+
seasonality.push_back(std::vector<T>());
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
for (size_t j = 0; j < iterate; j++) {
|
|
720
|
+
for (size_t i = 0; i < indices.size(); i++) {
|
|
721
|
+
auto idx = indices[i];
|
|
722
|
+
|
|
723
|
+
if (j > 0) {
|
|
724
|
+
for (size_t ii = 0; ii < deseas.size(); ii++) {
|
|
725
|
+
deseas[ii] += seasonality[idx][ii];
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
StlResult<T> fit;
|
|
730
|
+
if (swin) {
|
|
731
|
+
StlParams clone = stl_params;
|
|
732
|
+
fit = clone.seasonal_length((*swin)[idx]).fit(deseas, seas_ids[idx]);
|
|
733
|
+
} else if (stl_params.ns_.has_value()) {
|
|
734
|
+
fit = stl_params.fit(deseas, seas_ids[idx]);
|
|
735
|
+
} else {
|
|
736
|
+
StlParams clone = stl_params;
|
|
737
|
+
fit = clone.seasonal_length(7 + 4 * (i + 1)).fit(deseas, seas_ids[idx]);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
seasonality[idx] = fit.seasonal;
|
|
741
|
+
trend = fit.trend;
|
|
742
|
+
|
|
743
|
+
for (size_t ii = 0; ii < deseas.size(); ii++) {
|
|
744
|
+
deseas[ii] -= seasonality[idx][ii];
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
// TODO use Friedman's Super Smoother for trend
|
|
750
|
+
throw std::invalid_argument("periods must not be empty");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
std::vector<T> remainder;
|
|
754
|
+
remainder.reserve(k);
|
|
755
|
+
for (size_t i = 0; i < k; i++) {
|
|
756
|
+
remainder.push_back(deseas[i] - trend[i]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return std::make_tuple(trend, remainder, seasonality);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
template<typename T>
|
|
765
|
+
MstlResult<T> MstlParams::fit(const T* series, size_t series_size, const size_t* periods, size_t periods_size) const {
|
|
766
|
+
// return error to be consistent with stl
|
|
767
|
+
// and ensure seasonal is always same length as periods
|
|
768
|
+
for (size_t i = 0; i < periods_size; i++) {
|
|
769
|
+
if (periods[i] < 2) {
|
|
770
|
+
throw std::invalid_argument("periods must be at least 2");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// return error to be consistent with stl
|
|
775
|
+
// and ensure seasonal is always same length as periods
|
|
776
|
+
for (size_t i = 0; i < periods_size; i++) {
|
|
777
|
+
if (series_size < periods[i] * 2) {
|
|
778
|
+
throw std::invalid_argument("series has less than two periods");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (lambda_.has_value()) {
|
|
783
|
+
auto lambda = lambda_.value();
|
|
784
|
+
if (lambda < 0 || lambda > 1) {
|
|
785
|
+
throw std::invalid_argument("lambda must be between 0 and 1");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (swin_.has_value()) {
|
|
790
|
+
auto swin = swin_.value();
|
|
791
|
+
if (swin.size() != periods_size) {
|
|
792
|
+
throw std::invalid_argument("seasonal_lengths must have the same length as periods");
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
auto [trend, remainder, seasonal] = mstl(
|
|
797
|
+
series,
|
|
798
|
+
series_size,
|
|
799
|
+
periods,
|
|
800
|
+
periods_size,
|
|
801
|
+
iterate_,
|
|
802
|
+
lambda_,
|
|
803
|
+
swin_,
|
|
804
|
+
stl_params_
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
return MstlResult<T> {
|
|
808
|
+
seasonal,
|
|
809
|
+
trend,
|
|
810
|
+
remainder
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
template<typename T>
|
|
815
|
+
MstlResult<T> MstlParams::fit(const std::vector<T>& series, const std::vector<size_t>& periods) const {
|
|
816
|
+
return MstlParams::fit(series.data(), series.size(), periods.data(), periods.size());
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
#if __cplusplus >= 202002L
|
|
820
|
+
template<typename T>
|
|
821
|
+
MstlResult<T> MstlParams::fit(std::span<const T> series, std::span<const size_t> periods) const {
|
|
822
|
+
return MstlParams::fit(series.data(), series.size(), periods.data(), periods.size());
|
|
508
823
|
}
|
|
824
|
+
#endif
|
|
509
825
|
|
|
510
826
|
}
|
data/lib/stl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stl-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew Kane
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rice
|
|
@@ -16,15 +15,14 @@ dependencies:
|
|
|
16
15
|
requirements:
|
|
17
16
|
- - ">="
|
|
18
17
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 4.
|
|
18
|
+
version: '4.7'
|
|
20
19
|
type: :runtime
|
|
21
20
|
prerelease: false
|
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
22
|
requirements:
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 4.
|
|
27
|
-
description:
|
|
25
|
+
version: '4.7'
|
|
28
26
|
email: andrew@ankane.org
|
|
29
27
|
executables: []
|
|
30
28
|
extensions:
|
|
@@ -43,7 +41,6 @@ homepage: https://github.com/ankane/stl-ruby
|
|
|
43
41
|
licenses:
|
|
44
42
|
- Unlicense OR MIT
|
|
45
43
|
metadata: {}
|
|
46
|
-
post_install_message:
|
|
47
44
|
rdoc_options: []
|
|
48
45
|
require_paths:
|
|
49
46
|
- lib
|
|
@@ -58,8 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
55
|
- !ruby/object:Gem::Version
|
|
59
56
|
version: '0'
|
|
60
57
|
requirements: []
|
|
61
|
-
rubygems_version: 3.
|
|
62
|
-
signing_key:
|
|
58
|
+
rubygems_version: 3.6.9
|
|
63
59
|
specification_version: 4
|
|
64
60
|
summary: Seasonal-trend decomposition for Ruby
|
|
65
61
|
test_files: []
|