stl-rb 0.3.0 → 0.3.1

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