stl-rb 0.1.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 +7 -0
- data/CHANGELOG.md +3 -0
- data/README.md +102 -0
- data/ext/stl/ext.cpp +48 -0
- data/ext/stl/extconf.rb +5 -0
- data/ext/stl/stl.hpp +482 -0
- data/lib/stl/version.rb +3 -0
- data/lib/stl-rb.rb +1 -0
- data/lib/stl.rb +42 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27deddcf999b886ec030e044a3875bf175918d6ca417cc3eca23490402526d98
|
4
|
+
data.tar.gz: 9876b7bfde1681a95c1b80b2c9072545d75d967cef1b48dccbff3111f77eb848
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4675511aa5679abe804a1c53d7914cf236a4e2045dd2cc52211a81a810ec657ce98a520fc89c87b121d0439e1c2e49a0f15233ad6f9c6f3c4143cb3f9db668ab
|
7
|
+
data.tar.gz: 95bdd4d19dd9ebf24acbc0980af10a9438fafbfdf2037140b3a19d3da6bd0fffa55c11356c1d8a0983e06ab08985af9ba79a3a2dfc5c5dd167a53ceb86e8499f
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# STL Ruby
|
2
|
+
|
3
|
+
Seasonal-trend decomposition for Ruby
|
4
|
+
|
5
|
+
[](https://github.com/ankane/stl-ruby/actions)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application’s Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'stl-rb'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Getting Started
|
16
|
+
|
17
|
+
Decompose a time series
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
series = {
|
21
|
+
Date.parse("2020-01-01") => 100,
|
22
|
+
Date.parse("2020-01-02") => 150,
|
23
|
+
Date.parse("2020-01-03") => 136,
|
24
|
+
# ...
|
25
|
+
}
|
26
|
+
|
27
|
+
Stl.decompose(series, period: 7)
|
28
|
+
```
|
29
|
+
|
30
|
+
Works great with [Groupdate](https://github.com/ankane/groupdate)
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
series = User.group_by_day(:created_at).count
|
34
|
+
Stl.decompose(series, period: 7)
|
35
|
+
```
|
36
|
+
|
37
|
+
Series can also be an array without times (the index is returned)
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
series = [100, 150, 136, ...]
|
41
|
+
Stl.decompose(series, period: 7)
|
42
|
+
```
|
43
|
+
|
44
|
+
Use robustness iterations
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
Stl.decompose(series, period: 7, robust: true)
|
48
|
+
```
|
49
|
+
|
50
|
+
## Options
|
51
|
+
|
52
|
+
Pass options
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
Stl.decompose(
|
56
|
+
series,
|
57
|
+
period: 7, # period of the seasonal component
|
58
|
+
seasonal_length: 7, # length of the seasonal smoother
|
59
|
+
trend_length: 15, # length of the trend smoother
|
60
|
+
low_pass_length: 7, # length of the low-pass filter
|
61
|
+
seasonal_degree: 0, # degree of locally-fitted polynomial in seasonal smoothing
|
62
|
+
trend_degree: 1, # degree of locally-fitted polynomial in trend smoothing
|
63
|
+
low_pass_degree: 1, # degree of locally-fitted polynomial in low-pass smoothing
|
64
|
+
seasonal_jump: 1, # skipping value for seasonal smoothing
|
65
|
+
trend_jump: 2, # skipping value for trend smoothing
|
66
|
+
low_pass_jump: 1, # skipping value for low-pass smoothing
|
67
|
+
inner_loops: 2, # number of loops for updating the seasonal and trend components
|
68
|
+
outer_loops: 0, # number of iterations of robust fitting
|
69
|
+
robust: false # if robustness iterations are to be used
|
70
|
+
)
|
71
|
+
```
|
72
|
+
|
73
|
+
## Credits
|
74
|
+
|
75
|
+
This library was ported from the [Fortran implementation](https://www.netlib.org/a/stl).
|
76
|
+
|
77
|
+
## References
|
78
|
+
|
79
|
+
- [STL: A Seasonal-Trend Decomposition Procedure Based on Loess](https://www.scb.se/contentassets/ca21efb41fee47d293bbee5bf7be7fb3/stl-a-seasonal-trend-decomposition-procedure-based-on-loess.pdf)
|
80
|
+
|
81
|
+
## History
|
82
|
+
|
83
|
+
View the [changelog](https://github.com/ankane/stl-ruby/blob/master/CHANGELOG.md)
|
84
|
+
|
85
|
+
## Contributing
|
86
|
+
|
87
|
+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
88
|
+
|
89
|
+
- [Report bugs](https://github.com/ankane/stl-ruby/issues)
|
90
|
+
- Fix bugs and [submit pull requests](https://github.com/ankane/stl-ruby/pulls)
|
91
|
+
- Write, clarify, or fix documentation
|
92
|
+
- Suggest or add new features
|
93
|
+
|
94
|
+
To get started with development:
|
95
|
+
|
96
|
+
```sh
|
97
|
+
git clone https://github.com/ankane/stl-ruby.git
|
98
|
+
cd stl-ruby
|
99
|
+
bundle install
|
100
|
+
bundle exec rake compile
|
101
|
+
bundle exec rake test
|
102
|
+
```
|
data/ext/stl/ext.cpp
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
// stl
|
2
|
+
#include "stl.hpp"
|
3
|
+
|
4
|
+
// rice
|
5
|
+
#include <rice/rice.hpp>
|
6
|
+
#include <rice/stl.hpp>
|
7
|
+
|
8
|
+
Rice::Array to_a(std::vector<float>& x) {
|
9
|
+
auto a = Rice::Array();
|
10
|
+
for (auto v : x) {
|
11
|
+
a.push(v);
|
12
|
+
}
|
13
|
+
return a;
|
14
|
+
}
|
15
|
+
|
16
|
+
extern "C"
|
17
|
+
void Init_ext() {
|
18
|
+
auto rb_mStl = Rice::define_module("Stl");
|
19
|
+
|
20
|
+
Rice::define_class_under<stl::StlParams>(rb_mStl, "StlParams")
|
21
|
+
.define_constructor(Rice::Constructor<stl::StlParams>())
|
22
|
+
.define_method("seasonal_length", &stl::StlParams::seasonal_length)
|
23
|
+
.define_method("trend_length", &stl::StlParams::trend_length)
|
24
|
+
.define_method("low_pass_length", &stl::StlParams::low_pass_length)
|
25
|
+
.define_method("seasonal_degree", &stl::StlParams::seasonal_degree)
|
26
|
+
.define_method("trend_degree", &stl::StlParams::trend_degree)
|
27
|
+
.define_method("low_pass_degree", &stl::StlParams::low_pass_degree)
|
28
|
+
.define_method("seasonal_jump", &stl::StlParams::seasonal_jump)
|
29
|
+
.define_method("trend_jump", &stl::StlParams::trend_jump)
|
30
|
+
.define_method("low_pass_jump", &stl::StlParams::low_pass_jump)
|
31
|
+
.define_method("inner_loops", &stl::StlParams::inner_loops)
|
32
|
+
.define_method("outer_loops", &stl::StlParams::outer_loops)
|
33
|
+
.define_method("robust", &stl::StlParams::robust)
|
34
|
+
.define_method(
|
35
|
+
"fit",
|
36
|
+
[](stl::StlParams& self, std::vector<float> series, size_t period, bool weights) {
|
37
|
+
auto result = self.fit(series, period);
|
38
|
+
|
39
|
+
auto ret = Rice::Hash();
|
40
|
+
ret[Rice::Symbol("seasonal")] = to_a(result.seasonal);
|
41
|
+
ret[Rice::Symbol("trend")] = to_a(result.trend);
|
42
|
+
ret[Rice::Symbol("remainder")] = to_a(result.remainder);
|
43
|
+
if (weights) {
|
44
|
+
ret[Rice::Symbol("weights")] = to_a(result.weights);
|
45
|
+
}
|
46
|
+
return ret;
|
47
|
+
});
|
48
|
+
}
|
data/ext/stl/extconf.rb
ADDED
data/ext/stl/stl.hpp
ADDED
@@ -0,0 +1,482 @@
|
|
1
|
+
/*!
|
2
|
+
* STL C++ v0.1.0
|
3
|
+
* https://github.com/ankane/stl-cpp
|
4
|
+
* Unlicense OR MIT License
|
5
|
+
*
|
6
|
+
* Ported from https://www.netlib.org/a/stl
|
7
|
+
*
|
8
|
+
* Cleveland, R. B., Cleveland, W. S., McRae, J. E., & Terpenning, I. (1990).
|
9
|
+
* STL: A Seasonal-Trend Decomposition Procedure Based on Loess.
|
10
|
+
* Journal of Official Statistics, 6(1), 3-33.
|
11
|
+
*/
|
12
|
+
|
13
|
+
#pragma once
|
14
|
+
|
15
|
+
#include <algorithm>
|
16
|
+
#include <cmath>
|
17
|
+
#include <optional>
|
18
|
+
#include <stdexcept>
|
19
|
+
#include <vector>
|
20
|
+
|
21
|
+
namespace stl {
|
22
|
+
|
23
|
+
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) {
|
24
|
+
auto range = ((float) n) - 1.0;
|
25
|
+
auto h = std::max(xs - ((float) nleft), ((float) nright) - xs);
|
26
|
+
|
27
|
+
if (len > n) {
|
28
|
+
h += (float) ((len - n) / 2);
|
29
|
+
}
|
30
|
+
|
31
|
+
auto h9 = 0.999 * h;
|
32
|
+
auto h1 = 0.001 * h;
|
33
|
+
|
34
|
+
// compute weights
|
35
|
+
auto a = 0.0;
|
36
|
+
for (auto j = nleft; j <= nright; j++) {
|
37
|
+
w[j - 1] = 0.0;
|
38
|
+
auto r = fabs(((float) j) - xs);
|
39
|
+
if (r <= h9) {
|
40
|
+
if (r <= h1) {
|
41
|
+
w[j - 1] = 1.0;
|
42
|
+
} else {
|
43
|
+
w[j - 1] = pow(1.0 - pow(r / h, 3), 3);
|
44
|
+
}
|
45
|
+
if (userw) {
|
46
|
+
w[j - 1] *= rw[j - 1];
|
47
|
+
}
|
48
|
+
a += w[j - 1];
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
if (a <= 0.0) {
|
53
|
+
return false;
|
54
|
+
} else { // weighted least squares
|
55
|
+
for (auto j = nleft; j <= nright; j++) { // make sum of w(j) == 1
|
56
|
+
w[j - 1] /= a;
|
57
|
+
}
|
58
|
+
|
59
|
+
if (h > 0.0 && ideg > 0) { // use linear fit
|
60
|
+
auto a = 0.0;
|
61
|
+
for (auto j = nleft; j <= nright; j++) { // weighted center of x values
|
62
|
+
a += w[j - 1] * ((float) j);
|
63
|
+
}
|
64
|
+
auto b = xs - a;
|
65
|
+
auto c = 0.0;
|
66
|
+
for (auto j = nleft; j <= nright; j++) {
|
67
|
+
c += w[j - 1] * pow(((float) j) - a, 2);
|
68
|
+
}
|
69
|
+
if (sqrt(c) > 0.001 * range) {
|
70
|
+
b /= c;
|
71
|
+
|
72
|
+
// points are spread out enough to compute slope
|
73
|
+
for (auto j = nleft; j <= nright; j++) {
|
74
|
+
w[j - 1] *= b * (((float) j) - a) + 1.0;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
*ys = 0.0;
|
80
|
+
for (auto j = nleft; j <= nright; j++) {
|
81
|
+
*ys += w[j - 1] * y[j - 1];
|
82
|
+
}
|
83
|
+
|
84
|
+
return true;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
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) {
|
89
|
+
if (n < 2) {
|
90
|
+
ys[0] = y[0];
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
94
|
+
auto nleft = 0;
|
95
|
+
auto nright = 0;
|
96
|
+
|
97
|
+
auto newnj = std::min(njump, n - 1);
|
98
|
+
if (len >= n) {
|
99
|
+
nleft = 1;
|
100
|
+
nright = n;
|
101
|
+
for (auto i = 1; i <= n; i += newnj) {
|
102
|
+
auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
103
|
+
if (!ok) {
|
104
|
+
ys[i - 1] = y[i - 1];
|
105
|
+
}
|
106
|
+
}
|
107
|
+
} else if (newnj == 1) { // newnj equal to one, len less than n
|
108
|
+
auto nsh = (len + 1) / 2;
|
109
|
+
nleft = 1;
|
110
|
+
nright = len;
|
111
|
+
for (auto i = 1; i <= n; i++) { // fitted value at i
|
112
|
+
if (i > nsh && nright != n) {
|
113
|
+
nleft += 1;
|
114
|
+
nright += 1;
|
115
|
+
}
|
116
|
+
auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
117
|
+
if (!ok) {
|
118
|
+
ys[i - 1] = y[i - 1];
|
119
|
+
}
|
120
|
+
}
|
121
|
+
} else { // newnj greater than one, len less than n
|
122
|
+
auto nsh = (len + 1) / 2;
|
123
|
+
for (auto i = 1; i <= n; i += newnj) { // fitted value at i
|
124
|
+
if (i < nsh) {
|
125
|
+
nleft = 1;
|
126
|
+
nright = len;
|
127
|
+
} else if (i >= n - nsh + 1) {
|
128
|
+
nleft = n - len + 1;
|
129
|
+
nright = n;
|
130
|
+
} else {
|
131
|
+
nleft = i - nsh + 1;
|
132
|
+
nright = len + i - nsh;
|
133
|
+
}
|
134
|
+
auto ok = est(y, n, len, ideg, (float) i, &ys[i - 1], nleft, nright, res, userw, rw);
|
135
|
+
if (!ok) {
|
136
|
+
ys[i - 1] = y[i - 1];
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
if (newnj != 1) {
|
142
|
+
for (auto i = 1; i <= n - newnj; i += newnj) {
|
143
|
+
auto delta = (ys[i + newnj - 1] - ys[i - 1]) / ((float) newnj);
|
144
|
+
for (auto j = i + 1; j <= i + newnj - 1; j++) {
|
145
|
+
ys[j - 1] = ys[i - 1] + delta * ((float) (j - i));
|
146
|
+
}
|
147
|
+
}
|
148
|
+
auto k = ((n - 1) / newnj) * newnj + 1;
|
149
|
+
if (k != n) {
|
150
|
+
auto ok = est(y, n, len, ideg, (float) n, &ys[n - 1], nleft, nright, res, userw, rw);
|
151
|
+
if (!ok) {
|
152
|
+
ys[n - 1] = y[n - 1];
|
153
|
+
if (k != n - 1) {
|
154
|
+
auto delta = (ys[n - 1] - ys[k - 1]) / ((float) (n - k));
|
155
|
+
for (auto j = k + 1; j <= n - 1; j++) {
|
156
|
+
ys[j - 1] = ys[k - 1] + delta * ((float) (j - k));
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
void ma(const float* x, size_t n, size_t len, float* ave) {
|
165
|
+
auto newn = n - len + 1;
|
166
|
+
auto flen = (float) len;
|
167
|
+
auto v = 0.0;
|
168
|
+
|
169
|
+
// get the first average
|
170
|
+
for (auto i = 0; i < len; i++) {
|
171
|
+
v += x[i];
|
172
|
+
}
|
173
|
+
|
174
|
+
ave[0] = v / flen;
|
175
|
+
if (newn > 1) {
|
176
|
+
auto k = len;
|
177
|
+
auto m = 0;
|
178
|
+
for (auto j = 1; j < newn; j++) {
|
179
|
+
// window down the array
|
180
|
+
v = v - x[m] + x[k];
|
181
|
+
ave[j] = v / flen;
|
182
|
+
k += 1;
|
183
|
+
m += 1;
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
|
188
|
+
void fts(const float* x, size_t n, size_t np, float* trend, float* work) {
|
189
|
+
ma(x, n, np, trend);
|
190
|
+
ma(trend, n - np + 1, np, work);
|
191
|
+
ma(work, n - 2 * np + 2, 3, trend);
|
192
|
+
}
|
193
|
+
|
194
|
+
void rwts(const float* y, size_t n, const float* fit, float* rw) {
|
195
|
+
for (auto i = 0; i < n; i++) {
|
196
|
+
rw[i] = fabs(y[i] - fit[i]);
|
197
|
+
}
|
198
|
+
|
199
|
+
auto mid1 = (n - 1) / 2;
|
200
|
+
auto mid2 = n / 2;
|
201
|
+
|
202
|
+
// sort
|
203
|
+
std::sort(rw, rw + n);
|
204
|
+
|
205
|
+
auto cmad = 3.0 * (rw[mid1] + rw[mid2]); // 6 * median abs resid
|
206
|
+
auto c9 = 0.999 * cmad;
|
207
|
+
auto c1 = 0.001 * cmad;
|
208
|
+
|
209
|
+
for (auto i = 0; i < n; i++) {
|
210
|
+
auto r = fabs(y[i] - fit[i]);
|
211
|
+
if (r <= c1) {
|
212
|
+
rw[i] = 1.0;
|
213
|
+
} else if (r <= c9) {
|
214
|
+
rw[i] = pow(1.0 - pow(r / cmad, 2), 2);
|
215
|
+
} else {
|
216
|
+
rw[i] = 0.0;
|
217
|
+
}
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
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) {
|
222
|
+
for (auto j = 1; j <= np; j++) {
|
223
|
+
auto k = (n - j) / np + 1;
|
224
|
+
|
225
|
+
for (auto i = 1; i <= k; i++) {
|
226
|
+
work1[i - 1] = y[(i - 1) * np + j - 1];
|
227
|
+
}
|
228
|
+
if (userw) {
|
229
|
+
for (auto i = 1; i <= k; i++) {
|
230
|
+
work3[i - 1] = rw[(i - 1) * np + j - 1];
|
231
|
+
}
|
232
|
+
}
|
233
|
+
ess(work1, k, ns, isdeg, nsjump, userw, work3, work2 + 1, work4);
|
234
|
+
auto xs = 0.0;
|
235
|
+
auto nright = std::min(ns, k);
|
236
|
+
auto ok = est(work1, k, ns, isdeg, xs, &work2[0], 1, nright, work4, userw, work3);
|
237
|
+
if (!ok) {
|
238
|
+
work2[0] = work2[1];
|
239
|
+
}
|
240
|
+
xs = k + 1;
|
241
|
+
size_t nleft = std::max(1, (int) k - (int) ns + 1);
|
242
|
+
ok = est(work1, k, ns, isdeg, xs, &work2[k + 1], nleft, k, work4, userw, work3);
|
243
|
+
if (!ok) {
|
244
|
+
work2[k + 1] = work2[k];
|
245
|
+
}
|
246
|
+
for (auto m = 1; m <= k + 2; m++) {
|
247
|
+
season[(m - 1) * np + j - 1] = work2[m - 1];
|
248
|
+
}
|
249
|
+
}
|
250
|
+
}
|
251
|
+
|
252
|
+
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) {
|
253
|
+
for (auto j = 0; j < ni; j++) {
|
254
|
+
for (auto i = 0; i < n; i++) {
|
255
|
+
work1[i] = y[i] - trend[i];
|
256
|
+
}
|
257
|
+
|
258
|
+
ss(work1, n, np, ns, isdeg, nsjump, userw, rw, work2, work3, work4, work5, season);
|
259
|
+
fts(work2, n + 2 * np, np, work3, work1);
|
260
|
+
ess(work3, n, nl, ildeg, nljump, false, work4, work1, work5);
|
261
|
+
for (auto i = 0; i < n; i++) {
|
262
|
+
season[i] = work2[np + i] - work1[i];
|
263
|
+
}
|
264
|
+
for (auto i = 0; i < n; i++) {
|
265
|
+
work1[i] = y[i] - season[i];
|
266
|
+
}
|
267
|
+
ess(work1, n, nt, itdeg, ntjump, userw, rw, trend, work3);
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
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) {
|
272
|
+
auto work1 = std::vector<float>(n + 2 * np);
|
273
|
+
auto work2 = std::vector<float>(n + 2 * np);
|
274
|
+
auto work3 = std::vector<float>(n + 2 * np);
|
275
|
+
auto work4 = std::vector<float>(n + 2 * np);
|
276
|
+
auto work5 = std::vector<float>(n + 2 * np);
|
277
|
+
|
278
|
+
auto userw = false;
|
279
|
+
auto k = 0;
|
280
|
+
|
281
|
+
if (ns < 3) {
|
282
|
+
throw std::invalid_argument("seasonal_length must be at least 3");
|
283
|
+
}
|
284
|
+
if (nt < 3) {
|
285
|
+
throw std::invalid_argument("trend_length must be at least 3");
|
286
|
+
}
|
287
|
+
if (nl < 3) {
|
288
|
+
throw std::invalid_argument("low_pass_length must be at least 3");
|
289
|
+
}
|
290
|
+
if (np < 2) {
|
291
|
+
throw std::invalid_argument("period must be at least 2");
|
292
|
+
}
|
293
|
+
|
294
|
+
if (isdeg != 0 && isdeg != 1) {
|
295
|
+
throw std::invalid_argument("seasonal_degree must be 0 or 1");
|
296
|
+
}
|
297
|
+
if (itdeg != 0 && itdeg != 1) {
|
298
|
+
throw std::invalid_argument("trend_degree must be 0 or 1");
|
299
|
+
}
|
300
|
+
if (ildeg != 0 && ildeg != 1) {
|
301
|
+
throw std::invalid_argument("low_pass_degree must be 0 or 1");
|
302
|
+
}
|
303
|
+
|
304
|
+
if (ns % 2 != 1) {
|
305
|
+
throw std::invalid_argument("seasonal_length must be odd");
|
306
|
+
}
|
307
|
+
if (nt % 2 != 1) {
|
308
|
+
throw std::invalid_argument("trend_length must be odd");
|
309
|
+
}
|
310
|
+
if (nl % 2 != 1) {
|
311
|
+
throw std::invalid_argument("low_pass_length must be odd");
|
312
|
+
}
|
313
|
+
|
314
|
+
while (true) {
|
315
|
+
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());
|
316
|
+
k += 1;
|
317
|
+
if (k > no) {
|
318
|
+
break;
|
319
|
+
}
|
320
|
+
for (auto i = 0; i < n; i++) {
|
321
|
+
work1[i] = trend[i] + season[i];
|
322
|
+
}
|
323
|
+
rwts(y, n, work1.data(), rw);
|
324
|
+
userw = true;
|
325
|
+
}
|
326
|
+
|
327
|
+
if (no <= 0) {
|
328
|
+
for (auto i = 0; i < n; i++) {
|
329
|
+
rw[i] = 1.0;
|
330
|
+
}
|
331
|
+
}
|
332
|
+
}
|
333
|
+
|
334
|
+
class StlResult {
|
335
|
+
public:
|
336
|
+
std::vector<float> seasonal;
|
337
|
+
std::vector<float> trend;
|
338
|
+
std::vector<float> remainder;
|
339
|
+
std::vector<float> weights;
|
340
|
+
};
|
341
|
+
|
342
|
+
class StlParams {
|
343
|
+
std::optional<size_t> ns_ = std::nullopt;
|
344
|
+
std::optional<size_t> nt_ = std::nullopt;
|
345
|
+
std::optional<size_t> nl_ = std::nullopt;
|
346
|
+
int isdeg_ = 0;
|
347
|
+
int itdeg_ = 1;
|
348
|
+
std::optional<int> ildeg_ = std::nullopt;
|
349
|
+
std::optional<size_t> nsjump_ = std::nullopt;
|
350
|
+
std::optional<size_t> ntjump_ = std::nullopt;
|
351
|
+
std::optional<size_t> nljump_ = std::nullopt;
|
352
|
+
std::optional<size_t> ni_ = std::nullopt;
|
353
|
+
std::optional<size_t> no_ = std::nullopt;
|
354
|
+
bool robust_ = false;
|
355
|
+
|
356
|
+
public:
|
357
|
+
inline StlParams seasonal_length(size_t ns) {
|
358
|
+
this->ns_ = ns;
|
359
|
+
return *this;
|
360
|
+
};
|
361
|
+
|
362
|
+
inline StlParams trend_length(size_t nt) {
|
363
|
+
this->nt_ = nt;
|
364
|
+
return *this;
|
365
|
+
};
|
366
|
+
|
367
|
+
inline StlParams low_pass_length(size_t nl) {
|
368
|
+
this->nl_ = nl;
|
369
|
+
return *this;
|
370
|
+
};
|
371
|
+
|
372
|
+
inline StlParams seasonal_degree(int isdeg) {
|
373
|
+
this->isdeg_ = isdeg;
|
374
|
+
return *this;
|
375
|
+
};
|
376
|
+
|
377
|
+
inline StlParams trend_degree(int itdeg) {
|
378
|
+
this->itdeg_ = itdeg;
|
379
|
+
return *this;
|
380
|
+
};
|
381
|
+
|
382
|
+
inline StlParams low_pass_degree(int ildeg) {
|
383
|
+
this->ildeg_ = ildeg;
|
384
|
+
return *this;
|
385
|
+
};
|
386
|
+
|
387
|
+
inline StlParams seasonal_jump(size_t nsjump) {
|
388
|
+
this->nsjump_ = nsjump;
|
389
|
+
return *this;
|
390
|
+
};
|
391
|
+
|
392
|
+
inline StlParams trend_jump(size_t ntjump) {
|
393
|
+
this->ntjump_ = ntjump;
|
394
|
+
return *this;
|
395
|
+
};
|
396
|
+
|
397
|
+
inline StlParams low_pass_jump(size_t nljump) {
|
398
|
+
this->nljump_ = nljump;
|
399
|
+
return *this;
|
400
|
+
};
|
401
|
+
|
402
|
+
inline StlParams inner_loops(bool ni) {
|
403
|
+
this->ni_ = ni;
|
404
|
+
return *this;
|
405
|
+
};
|
406
|
+
|
407
|
+
inline StlParams outer_loops(bool no) {
|
408
|
+
this->no_ = no;
|
409
|
+
return *this;
|
410
|
+
};
|
411
|
+
|
412
|
+
inline StlParams robust(bool robust) {
|
413
|
+
this->robust_ = robust;
|
414
|
+
return *this;
|
415
|
+
};
|
416
|
+
|
417
|
+
StlResult fit(const float* y, size_t n, size_t np);
|
418
|
+
StlResult fit(const std::vector<float>& y, size_t np);
|
419
|
+
};
|
420
|
+
|
421
|
+
StlParams params() {
|
422
|
+
return StlParams();
|
423
|
+
}
|
424
|
+
|
425
|
+
StlResult StlParams::fit(const float* y, size_t n, size_t np) {
|
426
|
+
if (n < 2 * np) {
|
427
|
+
throw std::invalid_argument("series has less than two periods");
|
428
|
+
}
|
429
|
+
|
430
|
+
auto ns = this->ns_.value_or(np);
|
431
|
+
|
432
|
+
auto isdeg = this->isdeg_;
|
433
|
+
auto itdeg = this->itdeg_;
|
434
|
+
|
435
|
+
auto res = StlResult {
|
436
|
+
std::vector<float>(n),
|
437
|
+
std::vector<float>(n),
|
438
|
+
std::vector<float>(),
|
439
|
+
std::vector<float>(n)
|
440
|
+
};
|
441
|
+
|
442
|
+
auto ildeg = this->ildeg_.value_or(itdeg);
|
443
|
+
auto newns = std::max(ns, (size_t) 3);
|
444
|
+
if (newns % 2 == 0) {
|
445
|
+
newns += 1;
|
446
|
+
}
|
447
|
+
|
448
|
+
auto newnp = std::max(np, (size_t) 2);
|
449
|
+
auto nt = (size_t) ceil((1.5 * newnp) / (1.0 - 1.5 / (float) newns));
|
450
|
+
nt = this->nt_.value_or(nt);
|
451
|
+
nt = std::max(nt, (size_t) 3);
|
452
|
+
if (nt % 2 == 0) {
|
453
|
+
nt += 1;
|
454
|
+
}
|
455
|
+
|
456
|
+
auto nl = this->nl_.value_or(newnp);
|
457
|
+
if (nl % 2 == 0 && !this->nl_.has_value()) {
|
458
|
+
nl += 1;
|
459
|
+
}
|
460
|
+
|
461
|
+
auto ni = this->ni_.value_or(this->robust_ ? 1 : 2);
|
462
|
+
auto no = this->no_.value_or(this->robust_ ? 15 : 0);
|
463
|
+
|
464
|
+
auto nsjump = this->nsjump_.value_or((size_t) ceil(((float) newns) / 10.0));
|
465
|
+
auto ntjump = this->ntjump_.value_or((size_t) ceil(((float) nt) / 10.0));
|
466
|
+
auto nljump = this->nljump_.value_or((size_t) ceil(((float) nl) / 10.0));
|
467
|
+
|
468
|
+
stl(y, n, newnp, newns, nt, nl, isdeg, itdeg, ildeg, nsjump, ntjump, nljump, ni, no, res.weights.data(), res.seasonal.data(), res.trend.data());
|
469
|
+
|
470
|
+
res.remainder.reserve(n);
|
471
|
+
for (auto i = 0; i < n; i++) {
|
472
|
+
res.remainder.push_back(y[i] - res.seasonal[i] - res.trend[i]);
|
473
|
+
}
|
474
|
+
|
475
|
+
return res;
|
476
|
+
}
|
477
|
+
|
478
|
+
StlResult StlParams::fit(const std::vector<float>& y, size_t np) {
|
479
|
+
return StlParams::fit(y.data(), y.size(), np);
|
480
|
+
}
|
481
|
+
|
482
|
+
}
|
data/lib/stl/version.rb
ADDED
data/lib/stl-rb.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "stl"
|
data/lib/stl.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# ext
|
2
|
+
require "stl/ext"
|
3
|
+
|
4
|
+
# modules
|
5
|
+
require "stl/version"
|
6
|
+
|
7
|
+
module Stl
|
8
|
+
def self.decompose(
|
9
|
+
series, period:,
|
10
|
+
seasonal_length: nil, trend_length: nil, low_pass_length: nil,
|
11
|
+
seasonal_degree: nil, trend_degree: nil, low_pass_degree: nil,
|
12
|
+
seasonal_jump: nil, trend_jump: nil, low_pass_jump: nil,
|
13
|
+
inner_loops: nil, outer_loops: nil, robust: false
|
14
|
+
)
|
15
|
+
params = StlParams.new
|
16
|
+
|
17
|
+
params.seasonal_length(seasonal_length) unless seasonal_length.nil?
|
18
|
+
params.trend_length(trend_length) unless trend_length.nil?
|
19
|
+
params.low_pass_length(low_pass_length) unless low_pass_length.nil?
|
20
|
+
|
21
|
+
params.seasonal_degree(seasonal_degree) unless seasonal_degree.nil?
|
22
|
+
params.trend_degree(trend_degree) unless trend_degree.nil?
|
23
|
+
params.low_pass_degree(low_pass_degree) unless low_pass_degree.nil?
|
24
|
+
|
25
|
+
params.seasonal_jump(seasonal_jump) unless seasonal_jump.nil?
|
26
|
+
params.trend_jump(trend_jump) unless trend_jump.nil?
|
27
|
+
params.low_pass_jump(low_pass_jump) unless low_pass_jump.nil?
|
28
|
+
|
29
|
+
params.inner_loops(inner_loops) unless inner_loops.nil?
|
30
|
+
params.outer_loops(outer_loops) unless outer_loops.nil?
|
31
|
+
params.robust(robust) unless robust.nil?
|
32
|
+
|
33
|
+
if series.is_a?(Hash)
|
34
|
+
sorted = series.sort_by { |k, _| k }
|
35
|
+
y = sorted.map(&:last)
|
36
|
+
else
|
37
|
+
y = series
|
38
|
+
end
|
39
|
+
|
40
|
+
params.fit(y, period, outer_loops.nil? ? robust : outer_loops > 0)
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stl-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kane
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rice
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.0.2
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.0.2
|
27
|
+
description:
|
28
|
+
email: andrew@ankane.org
|
29
|
+
executables: []
|
30
|
+
extensions:
|
31
|
+
- ext/stl/extconf.rb
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- CHANGELOG.md
|
35
|
+
- README.md
|
36
|
+
- ext/stl/ext.cpp
|
37
|
+
- ext/stl/extconf.rb
|
38
|
+
- ext/stl/stl.hpp
|
39
|
+
- lib/stl-rb.rb
|
40
|
+
- lib/stl.rb
|
41
|
+
- lib/stl/version.rb
|
42
|
+
homepage: https://github.com/ankane/stl-ruby
|
43
|
+
licenses:
|
44
|
+
- Unlicense OR MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.6'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.2.22
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Seasonal-trend decomposition for Ruby
|
65
|
+
test_files: []
|