stl-rb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://github.com/ankane/stl-ruby/workflows/build/badge.svg?branch=master)](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: []
|