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 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: []