stl-rb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (2021-10-16)
2
+
3
+ - First release
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
+ }
@@ -0,0 +1,5 @@
1
+ require "mkmf-rice"
2
+
3
+ $CXXFLAGS += " -std=c++17 $(optflags)"
4
+
5
+ create_makefile("stl/ext")
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
+ }
@@ -0,0 +1,3 @@
1
+ module Stl
2
+ VERSION = "0.1.0"
3
+ end
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: []