1use crate::base::date::{datetime_from_serial, days_in_month_in, serial_from_ymd};
29use crate::base::{CellError, DateSystem, Value};
30
31use crate::functions::special::norm_inv;
32use crate::functions::util::{R, arg, as_array, has, num, num_val};
33use crate::functions::{Arg, Ctx, FnSpec};
34
35type C<'a> = &'a mut dyn Ctx;
36
37pub(crate) const MAX_POINTS: usize = 200_000;
39pub(crate) const MAX_SEASON: usize = 8_760;
41const MAX_MISSING: f64 = 0.3;
43const INIT_POINTS: usize = 10;
45const NM_ITERATIONS: usize = 400;
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub(crate) enum Season {
53 Detect,
54 None,
55 Fixed(usize),
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub(crate) enum Aggregate {
61 Average,
62 Count,
63 CountA,
64 Max,
65 Median,
66 Min,
67 Sum,
68}
69
70#[derive(Clone, Copy, Debug)]
71pub(crate) struct Options {
72 pub seasonality: Season,
73 pub interpolate: bool,
75 pub aggregate: Aggregate,
76}
77
78impl Default for Options {
79 fn default() -> Self {
80 Options { seasonality: Season::Detect, interpolate: true, aggregate: Aggregate::Average }
81 }
82}
83
84fn whole(a: &[Arg], i: usize) -> R<Option<f64>> {
85 if !has(a, i) {
86 return Ok(None);
87 }
88 let v = num(a, i)?;
89 if !v.is_finite() {
90 return Err(CellError::Num);
91 }
92 Ok(Some(v.trunc()))
93}
94
95pub(crate) fn season_arg(a: &[Arg], i: usize) -> R<Season> {
97 match whole(a, i)? {
98 None => Ok(Season::Detect),
99 Some(1.0) => Ok(Season::Detect),
100 Some(0.0) => Ok(Season::None),
101 Some(v) if v >= 2.0 && v <= MAX_SEASON as f64 => Ok(Season::Fixed(v as usize)),
102 Some(_) => Err(CellError::Num),
103 }
104}
105
106fn completion_arg(a: &[Arg], i: usize) -> R<bool> {
108 match whole(a, i)? {
109 None => Ok(true),
110 Some(1.0) => Ok(true),
111 Some(0.0) => Ok(false),
112 Some(_) => Err(CellError::Num),
113 }
114}
115
116fn aggregate_arg(a: &[Arg], i: usize) -> R<Aggregate> {
118 Ok(match whole(a, i)? {
119 None => Aggregate::Average,
120 Some(v) => match v as i64 {
121 0 | 1 => Aggregate::Average,
122 2 => Aggregate::Count,
123 3 => Aggregate::CountA,
124 4 => Aggregate::Max,
125 5 => Aggregate::Median,
126 6 => Aggregate::Min,
127 7 => Aggregate::Sum,
128 _ => return Err(CellError::Num),
129 },
130 })
131}
132
133#[derive(Clone, Copy, Debug)]
139enum Axis {
140 Linear { t0: f64, step: f64 },
142 Months { sys: DateSystem, month0: i64, step: i64, day: Option<u32> },
145}
146
147fn month_parts(sys: DateSystem, t: f64) -> Option<(i64, f64)> {
148 let d = datetime_from_serial(sys, t.floor())?;
149 if d.day == 0 {
150 return None;
151 }
152 Some((i64::from(d.year) * 12 + i64::from(d.month) - 1, f64::from(d.day)))
153}
154
155impl Axis {
156 fn position(&self, t: f64) -> Option<f64> {
157 match *self {
158 Axis::Linear { t0, step } => Some((t - t0) / step),
159 Axis::Months { sys, month0, step, day } => {
160 let d = datetime_from_serial(sys, t.floor())?;
161 if d.day == 0 {
162 return None;
163 }
164 let month = i64::from(d.year) * 12 + i64::from(d.month) - 1;
165 let dim = f64::from(days_in_month_in(sys, d.year, d.month));
166 if dim <= 0.0 {
167 return None;
168 }
169 let anchor = day.map_or(dim, |a| f64::from(a).min(dim));
170 let offset = (f64::from(d.day) - anchor + (t - t.floor())) / dim;
171 Some(((month - month0) as f64 + offset) / step as f64)
172 }
173 }
174 }
175}
176
177fn gcd(mut a: i64, mut b: i64) -> i64 {
178 while b != 0 {
179 let r = a % b;
180 a = b;
181 b = r;
182 }
183 a.abs()
184}
185
186fn detect_axis(times: &[f64], sys: DateSystem) -> R<Axis> {
189 let (Some(&t0), Some(&t1)) = (times.first(), times.get(1)) else { return Err(CellError::Num) };
190 let step = times.windows(2).map(|w| w.get(1).copied().unwrap_or(t1) - w.first().copied().unwrap_or(t0)).fold(f64::INFINITY, f64::min);
191 if step.is_finite() && step > 0.0 {
192 let even = times.windows(2).all(|w| match (w.first(), w.get(1)) {
193 (Some(a), Some(b)) => {
194 let r = (b - a) / step;
195 (r - r.round()).abs() <= 1e-6
196 }
197 _ => true,
198 });
199 if even {
200 return Ok(Axis::Linear { t0, step });
201 }
202 }
203 let mut months = Vec::with_capacity(times.len());
205 let mut same_day: Option<u32> = None;
206 let mut all_same = true;
207 let mut all_end = true;
208 for &t in times {
209 if t.fract() != 0.0 {
210 return Err(CellError::Num);
211 }
212 let d = datetime_from_serial(sys, t).ok_or(CellError::Num)?;
213 if d.day == 0 {
214 return Err(CellError::Num);
215 }
216 months.push(i64::from(d.year) * 12 + i64::from(d.month) - 1);
217 match same_day {
218 None => same_day = Some(d.day),
219 Some(x) if x != d.day => all_same = false,
220 _ => {}
221 }
222 if d.day != days_in_month_in(sys, d.year, d.month) {
223 all_end = false;
224 }
225 }
226 let day = if all_same {
227 same_day
228 } else if all_end {
229 None
230 } else {
231 return Err(CellError::Num);
232 };
233 let mut g = 0i64;
234 for w in months.windows(2) {
235 if let (Some(a), Some(b)) = (w.first(), w.get(1)) {
236 let d = b - a;
237 if d <= 0 {
238 return Err(CellError::Num);
239 }
240 g = gcd(g, d);
241 }
242 }
243 let month0 = month_parts(sys, t0).ok_or(CellError::Num)?.0;
244 if g <= 0 {
245 return Err(CellError::Num);
246 }
247 Ok(Axis::Months { sys, month0, step: g, day })
248}
249
250#[derive(Clone, Debug)]
252pub(crate) struct Series {
253 pub y: Vec<f64>,
254 axis: Axis,
255 pub step: f64,
257}
258
259impl Series {
260 pub fn horizon(&self, t: f64) -> R<f64> {
262 let p = self.axis.position(t).ok_or(CellError::Num)?;
263 let h = p - (self.y.len() as f64 - 1.0);
264 if !h.is_finite() || h < -1e-9 {
265 return Err(CellError::Num);
266 }
267 Ok(h.max(0.0))
268 }
269}
270
271fn aggregate(kind: Aggregate, xs: &mut [f64]) -> Option<f64> {
272 let n = xs.len() as f64;
273 match kind {
274 Aggregate::Count | Aggregate::CountA => Some(n),
275 _ if xs.is_empty() => None,
276 Aggregate::Average => Some(xs.iter().sum::<f64>() / n),
277 Aggregate::Sum => Some(xs.iter().sum()),
278 Aggregate::Max => xs.iter().copied().reduce(f64::max),
279 Aggregate::Min => xs.iter().copied().reduce(f64::min),
280 Aggregate::Median => {
281 xs.sort_by(f64::total_cmp);
282 let k = xs.len() / 2;
283 let hi = xs.get(k).copied()?;
284 if xs.len() % 2 == 1 { Some(hi) } else { Some((xs.get(k.checked_sub(1)?).copied()? + hi) / 2.0) }
285 }
286 }
287}
288
289pub(crate) fn series(values: &Value, timeline: &Value, opts: &Options, sys: DateSystem) -> R<Series> {
292 let va = as_array(values);
293 let ta = as_array(timeline);
294 if va.data.len() != ta.data.len() {
295 return Err(CellError::NA);
296 }
297 if va.data.len() > MAX_POINTS {
298 return Err(CellError::Num);
299 }
300 let mut points: Vec<(f64, Option<f64>)> = Vec::with_capacity(va.data.len());
301 for (t, v) in ta.data.iter().zip(va.data.iter()) {
302 if let Value::Error(e) = t {
303 return Err(*e);
304 }
305 if let Value::Error(e) = v {
306 return Err(*e);
307 }
308 let value = match v {
309 Value::Number(x) => Some(*x),
310 Value::Empty => None,
311 _ => return Err(CellError::Value),
312 };
313 let time = match t {
314 Value::Number(x) if x.is_finite() => *x,
315 Value::Empty if value.is_none() => continue,
317 _ => return Err(CellError::Value),
318 };
319 points.push((time, value));
320 }
321 points.sort_by(|a, b| a.0.total_cmp(&b.0));
322 let mut times: Vec<f64> = points.iter().map(|p| p.0).collect();
323 times.dedup();
324 if times.len() < 2 {
325 return Err(CellError::Num);
326 }
327 let axis = detect_axis(×, sys)?;
328 let (Some(&first), Some(&last)) = (times.first(), times.last()) else { return Err(CellError::Num) };
329 let span = axis.position(last).ok_or(CellError::Num)?.round();
330 if !(span >= 1.0 && span < MAX_POINTS as f64) {
331 return Err(CellError::Num);
332 }
333 let len = span as usize + 1;
334 let mut slots: Vec<Option<f64>> = vec![None; len];
336 let mut group: Vec<f64> = Vec::new();
337 let mut group_at: Option<usize> = None;
338 let flush = |at: Option<usize>, group: &mut Vec<f64>, slots: &mut Vec<Option<f64>>| {
339 if let Some(i) = at
340 && let Some(s) = slots.get_mut(i)
341 {
342 *s = aggregate(opts.aggregate, group);
343 }
344 group.clear();
345 };
346 for &(t, v) in &points {
347 let p = axis.position(t).ok_or(CellError::Num)?.round();
348 if !(p >= 0.0 && p < len as f64) {
349 return Err(CellError::Num);
350 }
351 let p = p as usize;
352 if group_at != Some(p) {
353 flush(group_at, &mut group, &mut slots);
354 group_at = Some(p);
355 }
356 if let Some(x) = v {
357 group.push(x);
358 }
359 }
360 flush(group_at, &mut group, &mut slots);
361 let known = slots.iter().filter(|s| s.is_some()).count();
362 if known == 0 || (len - known) as f64 > MAX_MISSING * len as f64 {
363 return Err(CellError::Num);
364 }
365 let y = if opts.interpolate { interpolate(&slots) } else { slots.iter().map(|s| s.unwrap_or(0.0)).collect() };
366 let step = match axis {
367 Axis::Linear { step, .. } => step,
368 Axis::Months { .. } => (last - first) / span,
369 };
370 Ok(Series { y, axis, step })
371}
372
373fn interpolate(slots: &[Option<f64>]) -> Vec<f64> {
376 let mut out = Vec::with_capacity(slots.len());
377 let mut known = slots.iter().enumerate().filter_map(|(j, s)| {
379 #[cfg(test)]
380 tests::INTERPOLATION_VISITS.with(|visits| visits.set(visits.get() + 1));
381 s.map(|v| (j, v))
382 });
383 let mut next = known.next();
384 let mut prev: Option<(usize, f64)> = None;
385 for (i, s) in slots.iter().enumerate() {
386 if let Some(v) = s {
387 out.push(*v);
388 prev = Some((i, *v));
389 next = known.next();
390 continue;
391 }
392 out.push(match (prev, next) {
393 (Some((a, va)), Some((b, vb))) => va + (vb - va) * (i - a) as f64 / (b - a) as f64,
394 (Some((_, v)), None) | (None, Some((_, v))) => v,
395 (None, None) => 0.0,
396 });
397 }
398 out
399}
400
401#[derive(Clone, Copy, Debug)]
403pub struct Timeline {
404 axis: Axis,
405 last: i64,
407}
408
409impl Timeline {
410 pub fn detect(times: &[f64], sys: DateSystem) -> Option<Timeline> {
414 if times.iter().any(|t| !t.is_finite()) {
415 return None;
416 }
417 let mut sorted = times.to_vec();
418 sorted.sort_by(f64::total_cmp);
419 sorted.dedup();
420 let axis = detect_axis(&sorted, sys).ok()?;
421 let last = axis.position(*sorted.last()?)?.round();
422 if !(0.0..MAX_POINTS as f64).contains(&last) || (last + 1.0 - sorted.len() as f64) > MAX_MISSING * (last + 1.0) {
424 return None;
425 }
426 Some(Timeline { axis, last: last as i64 })
427 }
428
429 pub fn steps_after_last(&self, t: f64) -> Option<f64> {
431 let p = self.axis.position(t)? - self.last as f64;
432 p.is_finite().then_some(p)
433 }
434
435 pub fn after_last(&self, k: u32) -> Option<f64> {
437 let pos = self.last.checked_add(i64::from(k))?;
438 match self.axis {
439 Axis::Linear { t0, step } => {
440 let t = t0 + pos as f64 * step;
441 t.is_finite().then_some(t)
442 }
443 Axis::Months { sys, month0, step, day } => {
444 let month = month0.checked_add(pos.checked_mul(step)?)?;
445 let (y, m) = (month.div_euclid(12), month.rem_euclid(12) + 1);
446 let dim = i64::from(days_in_month_in(sys, i32::try_from(y).ok()?, u32::try_from(m).ok()?));
447 let d = day.map_or(dim, |d| i64::from(d).min(dim));
448 if y < 1900 {
449 return None;
450 }
451 serial_from_ymd(sys, y, m, d)
452 }
453 }
454 }
455}
456
457fn detrend(y: &[f64]) -> Vec<f64> {
463 let (a, b) = line(y.iter().enumerate().map(|(i, v)| (i as f64, *v)));
464 y.iter().enumerate().map(|(i, v)| v - (a + b * i as f64)).collect()
465}
466
467fn line(points: impl Iterator<Item = (f64, f64)> + Clone) -> (f64, f64) {
469 let (mut n, mut sx, mut sy) = (0.0, 0.0, 0.0);
470 for (x, y) in points.clone() {
471 n += 1.0;
472 sx += x;
473 sy += y;
474 }
475 if n == 0.0 {
476 return (0.0, 0.0);
477 }
478 let (mx, my) = (sx / n, sy / n);
479 let (mut sxy, mut sxx) = (0.0, 0.0);
480 for (x, y) in points {
481 sxy += (x - mx) * (y - my);
482 sxx += (x - mx) * (x - mx);
483 }
484 let b = if sxx > 0.0 { sxy / sxx } else { 0.0 };
485 (my - b * mx, b)
486}
487
488pub(crate) fn detect_season(y: &[f64]) -> usize {
490 let n = y.len();
491 if n < 4 {
492 return 0;
493 }
494 let r = detrend(y);
495 let var: f64 = r.iter().map(|v| v * v).sum();
496 let scale: f64 = y.iter().map(|v| v * v).sum::<f64>();
497 if !(var.is_finite() && var > 1e-20 * scale.max(f64::MIN_POSITIVE)) {
498 return 0;
499 }
500 let max_lag = (n / 2).min(MAX_SEASON);
501 let acf: Vec<f64> = (0..=max_lag.saturating_add(1).min(n.saturating_sub(1)))
502 .map(|k| r.iter().skip(k).zip(r.iter()).map(|(a, b)| a * b).sum::<f64>() / var)
503 .collect();
504 let bound = 2.0 / (n as f64).sqrt();
505 let mut best = 0usize;
506 let mut best_r = bound;
507 for k in 2..=max_lag {
508 let (Some(&prev), Some(&here)) = (acf.get(k - 1), acf.get(k)) else { break };
509 let next = acf.get(k + 1).copied().unwrap_or(f64::NEG_INFINITY);
510 if here > prev && here >= next && here > best_r {
511 best = k;
512 best_r = here;
513 }
514 }
515 best
516}
517
518#[derive(Clone, Debug)]
523struct Init {
524 level: f64,
525 trend: f64,
526 season: Vec<f64>,
528}
529
530fn initial_states(y: &[f64], m: usize) -> Init {
532 let n = y.len();
533 let mut season = vec![0.0; m];
534 if m >= 2 {
535 if n >= 2 * m {
536 let len = n.min(4 * m).max(2 * m);
538 let w = y.get(..len).unwrap_or(y);
539 let mut prefix = Vec::with_capacity(w.len() + 1);
540 prefix.push(0.0);
541 let mut acc = 0.0;
542 for v in w {
543 acc += v;
544 prefix.push(acc);
545 }
546 let sum = |a: usize, b: usize| prefix.get(b).copied().unwrap_or(0.0) - prefix.get(a).copied().unwrap_or(0.0);
547 let h = m / 2;
548 let mut totals = vec![0.0; m];
549 let mut counts = vec![0usize; m];
550 for t in h..w.len().saturating_sub(h) {
551 let ma = if m % 2 == 1 {
552 sum(t - h, t + h + 1) / m as f64
553 } else {
554 let ends = w.get(t - h).copied().unwrap_or(0.0) + w.get(t + h).copied().unwrap_or(0.0);
555 (sum(t - h + 1, t + h) + ends / 2.0) / m as f64
556 };
557 if let (Some(tot), Some(c), Some(v)) = (totals.get_mut(t % m), counts.get_mut(t % m), w.get(t)) {
558 *tot += v - ma;
559 *c += 1;
560 }
561 }
562 for ((s, tot), c) in season.iter_mut().zip(&totals).zip(&counts) {
563 *s = if *c > 0 { tot / *c as f64 } else { 0.0 };
564 }
565 } else {
566 let r = detrend(y);
568 let mut counts = vec![0usize; m];
569 for (t, v) in r.iter().enumerate() {
570 if let (Some(s), Some(c)) = (season.get_mut(t % m), counts.get_mut(t % m)) {
571 *s += v;
572 *c += 1;
573 }
574 }
575 for (s, c) in season.iter_mut().zip(&counts) {
576 if *c > 0 {
577 *s /= *c as f64;
578 }
579 }
580 }
581 let mean = season.iter().sum::<f64>() / m as f64;
582 for s in &mut season {
583 *s -= mean;
584 }
585 }
586 let k = n.min(INIT_POINTS);
587 let adjusted = y.iter().take(k).enumerate().map(|(t, v)| {
588 let s = if m >= 2 { season.get(t % m).copied().unwrap_or(0.0) } else { 0.0 };
589 ((t + 1) as f64, v - s)
590 });
591 let (level, trend) = line(adjusted);
592 Init { level, trend, season }
593}
594
595type Params = [f64; 3];
597
598#[derive(Clone, Debug)]
599struct State {
600 level: f64,
601 trend: f64,
602 season: Vec<f64>,
603 sse: f64,
604}
605
606fn filter(y: &[f64], init: &Init, p: Params, mut errors: Option<&mut Vec<f64>>) -> State {
608 let [alpha, beta, gamma] = p;
609 let m = init.season.len();
610 let (mut l, mut b) = (init.level, init.trend);
611 let mut season = init.season.clone();
612 let mut sse = 0.0;
613 for (t, v) in y.iter().enumerate() {
614 let slot = if m > 0 { season.get_mut(t % m) } else { None };
615 let s = slot.as_ref().map_or(0.0, |s| **s);
616 let e = v - (l + b + s);
617 if let Some(slot) = slot {
618 *slot = s + gamma * e;
619 }
620 l = l + b + alpha * e;
621 b += alpha * beta * e;
622 sse += e * e;
623 if let Some(errs) = errors.as_deref_mut() {
624 errs.push(e);
625 }
626 }
627 State { level: l, trend: b, season, sse }
628}
629
630fn objective(y: &[f64], init: &Init, p: Params) -> f64 {
631 let sse = filter(y, init, p, None).sse;
632 if sse.is_finite() { sse } else { f64::INFINITY }
633}
634
635fn clamp(p: Params, active: usize) -> Params {
636 let mut q = [0.0; 3];
637 for (i, (o, v)) in q.iter_mut().zip(p).enumerate() {
638 *o = if i < active { v.clamp(0.0, 1.0) } else { 0.0 };
639 }
640 q
641}
642
643fn optimise(y: &[f64], init: &Init) -> Params {
645 let active = if init.season.is_empty() { 2 } else { 3 };
646 let f = |p: Params| objective(y, init, clamp(p, active));
647 const GRID: [f64; 5] = [0.05, 0.25, 0.5, 0.75, 0.95];
649 let mut best: (Params, f64) = ([0.5, 0.1, 0.1], f64::INFINITY);
650 for a in GRID {
651 for b in GRID {
652 for g in if active == 3 { &GRID[..] } else { &GRID[..1] } {
653 let p = clamp([a, b, *g], active);
654 let v = f(p);
655 if v < best.1 {
656 best = (p, v);
657 }
658 }
659 }
660 }
661 let mut simplex: Vec<(Params, f64)> = vec![best];
663 for i in 0..active {
664 let mut p = best.0;
665 if let Some(x) = p.get_mut(i) {
666 *x = if *x > 0.5 { *x - 0.1 } else { *x + 0.1 };
667 }
668 let p = clamp(p, active);
669 simplex.push((p, f(p)));
670 }
671 let lerp = |a: Params, b: Params, t: f64| -> Params {
672 let mut o = [0.0; 3];
673 for ((o, x), y) in o.iter_mut().zip(a).zip(b) {
674 *o = x + t * (y - x);
675 }
676 clamp(o, active)
677 };
678 for _ in 0..NM_ITERATIONS {
679 simplex.sort_by(|a, b| a.1.total_cmp(&b.1));
680 let (Some(&lo), Some(&hi)) = (simplex.first(), simplex.last()) else { break };
681 let second = simplex.len().checked_sub(2).and_then(|i| simplex.get(i)).map_or(hi.1, |s| s.1);
682 let size = simplex.iter().map(|s| s.0.iter().zip(lo.0).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max)).fold(0.0, f64::max);
683 if (hi.1 - lo.1).abs() <= 1e-12 * lo.1.abs() + 1e-300 || size < 1e-7 {
684 break;
685 }
686 let k = simplex.len().saturating_sub(1).max(1) as f64;
687 let mut c = [0.0; 3];
688 for s in simplex.iter().take(simplex.len().saturating_sub(1)) {
689 for (ci, x) in c.iter_mut().zip(s.0) {
690 *ci += x / k;
691 }
692 }
693 let reflected = lerp(c, hi.0, -1.0);
694 let fr = f(reflected);
695 let replace = if fr < lo.1 {
696 let expanded = lerp(c, hi.0, -2.0);
697 let fe = f(expanded);
698 Some(if fe < fr { (expanded, fe) } else { (reflected, fr) })
699 } else if fr < second {
700 Some((reflected, fr))
701 } else {
702 let contracted = lerp(c, hi.0, 0.5);
703 let fc = f(contracted);
704 if fc < hi.1 { Some((contracted, fc)) } else { None }
705 };
706 match replace {
707 Some(v) => {
708 if let Some(last) = simplex.last_mut() {
709 *last = v;
710 }
711 }
712 None => {
713 for s in simplex.iter_mut().skip(1) {
714 let p = lerp(lo.0, s.0, 0.5);
715 *s = (p, f(p));
716 }
717 }
718 }
719 }
720 simplex.sort_by(|a, b| a.1.total_cmp(&b.1));
721 clamp(simplex.first().map_or(best.0, |s| s.0), active)
722}
723
724#[derive(Clone, Debug)]
726pub(crate) struct Fit {
727 pub alpha: f64,
728 pub beta: f64,
729 pub gamma: f64,
730 pub m: usize,
731 n: usize,
732 state: State,
733 pub errors: Vec<f64>,
734}
735
736pub(crate) fn fit(y: &[f64], m: usize) -> Fit {
737 let m = if m >= 2 { m } else { 0 };
738 let init = initial_states(y, m);
739 let p = optimise(y, &init);
740 let mut errors = Vec::with_capacity(y.len());
741 let state = filter(y, &init, p, Some(&mut errors));
742 let [alpha, beta, gamma] = p;
743 Fit { alpha, beta, gamma, m, n: y.len(), state, errors }
744}
745
746impl Fit {
747 fn at(&self, k: f64) -> f64 {
748 let s = if self.m > 0 {
749 let idx = ((self.n as f64 - 1.0) + k).rem_euclid(self.m as f64);
750 self.state.season.get(idx as usize).copied().unwrap_or(0.0)
751 } else {
752 0.0
753 };
754 self.state.level + k * self.state.trend + s
755 }
756
757 pub fn forecast(&self, h: f64) -> f64 {
759 let lo = h.floor();
760 let a = self.at(lo);
761 if h == lo { a } else { a + (h - lo) * (self.at(lo + 1.0) - a) }
762 }
763
764 pub fn forecast_sd(&self, h: f64) -> f64 {
766 let sigma2 = self.state.sse / self.n.max(1) as f64;
767 let k = (h.ceil().max(1.0)) - 1.0;
768 let (a, b, g) = (self.alpha, self.alpha * self.beta, self.gamma);
769 let mut sum = k * a * a + a * b * k * (k + 1.0) + b * b * k * (k + 1.0) * (2.0 * k + 1.0) / 6.0;
771 if self.m > 0 {
772 let m = self.m as f64;
773 let q = (k / m).floor();
774 sum += 2.0 * g * (q * a + b * m * q * (q + 1.0) / 2.0) + g * g * q;
775 }
776 (sigma2 * (1.0 + sum)).max(0.0).sqrt()
777 }
778}
779
780struct Prepared {
785 series: Series,
786 m: usize,
787}
788
789fn prepare(a: &[Arg], c: C, values: usize, timeline: usize, opts: Options) -> R<Prepared> {
790 let series = series(&arg(a, values)?.value, &arg(a, timeline)?.value, &opts, c.date_system())?;
791 let m = match opts.seasonality {
792 Season::Detect => detect_season(&series.y),
793 Season::None => 0,
794 Season::Fixed(m) => m,
795 };
796 Ok(Prepared { series, m })
797}
798
799fn options(a: &[Arg], season: Option<usize>, completion: usize, aggregation: usize) -> R<Options> {
800 Ok(Options {
801 seasonality: match season {
802 Some(i) => season_arg(a, i)?,
803 None => Season::Detect,
804 },
805 interpolate: completion_arg(a, completion)?,
806 aggregate: aggregate_arg(a, aggregation)?,
807 })
808}
809
810fn forecast_ets(a: &[Arg], c: C) -> R<Value> {
811 let target = num(a, 0)?;
812 let opts = options(a, Some(3), 4, 5)?;
813 let p = prepare(a, c, 1, 2, opts)?;
814 let h = p.series.horizon(target)?;
815 num_val(fit(&p.series.y, p.m).forecast(h))
816}
817
818fn forecast_ets_confint(a: &[Arg], c: C) -> R<Value> {
819 let target = num(a, 0)?;
820 let level = if has(a, 3) { num(a, 3)? } else { 0.95 };
821 if !(level > 0.0 && level < 1.0) {
822 return Err(CellError::Num);
823 }
824 let opts = options(a, Some(4), 5, 6)?;
825 let p = prepare(a, c, 1, 2, opts)?;
826 let h = p.series.horizon(target)?;
827 let z = norm_inv((1.0 + level) / 2.0);
828 num_val(z * fit(&p.series.y, p.m).forecast_sd(h))
829}
830
831fn forecast_ets_seasonality(a: &[Arg], c: C) -> R<Value> {
832 let opts = options(a, None, 2, 3)?;
833 let p = prepare(a, c, 0, 1, opts)?;
834 num_val(p.m as f64)
835}
836
837fn forecast_ets_stat(a: &[Arg], c: C) -> R<Value> {
838 let kind = num(a, 2)?;
839 if !kind.is_finite() || !(1.0..9.0).contains(&kind.trunc()) {
840 return Err(CellError::Num);
841 }
842 let kind = kind.trunc() as u8;
843 let opts = options(a, Some(3), 4, 5)?;
844 let p = prepare(a, c, 0, 1, opts)?;
845 if kind == 8 {
846 return num_val(p.series.step);
847 }
848 let y = &p.series.y;
849 let f = fit(y, p.m);
850 let n = f.errors.len().max(1) as f64;
851 let mae = f.errors.iter().map(|e| e.abs()).sum::<f64>() / n;
852 num_val(match kind {
853 1 => f.alpha,
854 2 => f.beta,
855 3 => f.gamma,
856 4 => {
857 let naive: Vec<f64> = y.windows(2).filter_map(|w| Some((w.get(1)? - w.first()?).abs())).collect();
858 let d = naive.iter().sum::<f64>() / naive.len().max(1) as f64;
859 if d == 0.0 {
860 return Err(CellError::Div0);
861 }
862 mae / d
863 }
864 5 => {
865 let terms: Vec<f64> = y
866 .iter()
867 .zip(&f.errors)
868 .filter_map(|(v, e)| {
869 let denom = (v.abs() + (v - e).abs()) / 2.0;
870 (denom > 0.0).then(|| e.abs() / denom)
871 })
872 .collect();
873 terms.iter().sum::<f64>() / terms.len().max(1) as f64
874 }
875 6 => mae,
876 7 => (f.errors.iter().map(|e| e * e).sum::<f64>() / n).sqrt(),
877 _ => return Err(CellError::Num),
878 })
879}
880
881const S_ETS: &[bool] = &[true, false, false, true, true, true, true];
882const S_ETS_SEASON: &[bool] = &[false, false, true, true];
883const S_ETS_STAT: &[bool] = &[false, false, true, true, true, true];
884
885pub(crate) fn specs() -> Vec<FnSpec> {
886 vec![
887 f!(
888 "FORECAST.ETS",
889 3,
890 6,
891 Statistical,
892 S_ETS,
893 "FORECAST.ETS(target_date, values, timeline, [seasonality], [data_completion], [aggregation])",
894 "Forecasts a value by additive (AAA) exponential smoothing.",
895 forecast_ets
896 ),
897 f!(
898 "FORECAST.ETS.CONFINT",
899 3,
900 7,
901 Statistical,
902 S_ETS,
903 "FORECAST.ETS.CONFINT(target_date, values, timeline, [confidence_level], [seasonality], [data_completion], [aggregation])",
904 "Half-width of the confidence interval around a FORECAST.ETS value.",
905 forecast_ets_confint
906 ),
907 f!(
908 "FORECAST.ETS.SEASONALITY",
909 2,
910 4,
911 Statistical,
912 S_ETS_SEASON,
913 "FORECAST.ETS.SEASONALITY(values, timeline, [data_completion], [aggregation])",
914 "Length of the repeating pattern detected in a time series.",
915 forecast_ets_seasonality
916 ),
917 f!(
918 "FORECAST.ETS.STAT",
919 3,
920 6,
921 Statistical,
922 S_ETS_STAT,
923 "FORECAST.ETS.STAT(values, timeline, statistic_type, [seasonality], [data_completion], [aggregation])",
924 "A statistic of the FORECAST.ETS model: 1 alpha, 2 beta, 3 gamma, 4 MASE, 5 SMAPE, 6 MAE, 7 RMSE, 8 step size.",
925 forecast_ets_stat
926 ),
927 ]
928}
929
930#[cfg(test)]
931mod tests {
932 use super::*;
933 use crate::functions::util::testutil::*;
934
935 thread_local! {
936 pub(super) static INTERPOLATION_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
937 }
938
939 #[test]
940 fn interpolation_preserves_gaps_and_endpoint_values() {
941 for (slots, expected) in [
942 (
943 vec![None, Some(4.0), None, None, Some(13.0), Some(-2.0), None, Some(8.0), None, None],
944 vec![4.0, 4.0, 7.0, 10.0, 13.0, -2.0, 3.0, 8.0, 8.0, 8.0],
945 ),
946 (vec![Some(-7.0), None, Some(4.0)], vec![-7.0, -1.5, 4.0]),
947 (vec![Some(3.0), Some(-7.0), Some(2.0)], vec![3.0, -7.0, 2.0]),
948 (vec![None, None, None], vec![0.0, 0.0, 0.0]),
949 (vec![Some(6.0)], vec![6.0]),
950 (vec![], vec![]),
951 ] {
952 assert_eq!(interpolate(&slots), expected, "{slots:?}");
953 }
954 }
955
956 #[test]
957 fn interpolation_lookahead_work_is_linear() {
958 let mut measured = Vec::new();
959 for len in [2_000, 4_000] {
960 let missing = len * 3 / 10;
961 let gap = (len - missing) / 2..(len + missing) / 2;
962 let expected: Vec<f64> = (0..len).map(|i| (3 * i + 5) as f64).collect();
963 let slots: Vec<Option<f64>> = expected.iter().enumerate().map(|(i, &v)| (!gap.contains(&i)).then_some(v)).collect();
964 INTERPOLATION_VISITS.with(|visits| visits.set(0));
965 let actual = interpolate(&slots);
966 let visits = INTERPOLATION_VISITS.with(|visits| visits.get());
967 assert_eq!(actual, expected);
968 measured.push((len, visits));
969 }
970 eprintln!("interpolation lookahead visits (slots, visits): {measured:?}");
972 for (len, visits) in measured {
973 assert!(visits > 0 && visits <= 2 * len, "{len} slots required {visits} lookahead visits");
974 }
975 }
976
977 #[test]
978 fn series_completes_a_long_gap_at_the_missing_point_limit() {
979 let len = 2_000;
980 let gap = 700..1_300; let mut times: Vec<f64> = (0..len).filter(|i| !gap.contains(i)).map(|i| (i + 1) as f64).collect();
982 let mut values: Vec<f64> = times.iter().map(|t| 3.0 * t + 2.0).collect();
983 let options = Options::default();
984 let completed = series(&col(&values), &col(×), &options, DateSystem::D1900).unwrap();
985 let expected: Vec<f64> = (0..len).map(|i| (3 * i + 5) as f64).collect();
986 assert_eq!(completed.y, expected);
987 assert_eq!(completed.step, 1.0);
988 assert_eq!(completed.horizon((len + 1) as f64), Ok(1.0));
989
990 let zeroes = series(&col(&values), &col(×), &Options { interpolate: false, ..options }, DateSystem::D1900).unwrap();
991 for (i, &value) in zeroes.y.iter().enumerate() {
992 assert_eq!(value, if gap.contains(&i) { 0.0 } else { expected[i] });
993 }
994
995 times.remove(gap.start);
997 values.remove(gap.start);
998 assert!(matches!(series(&col(&values), &col(×), &options, DateSystem::D1900), Err(CellError::Num)));
999 }
1000
1001 fn range(xs: &[f64]) -> Arg {
1002 rf(col(xs))
1003 }
1004
1005 fn timeline(n: usize) -> Vec<f64> {
1006 (1..=n).map(|t| t as f64).collect()
1007 }
1008
1009 fn seasonal(n: usize) -> Vec<f64> {
1011 let pattern = [4.0, -2.0, 7.0, -9.0];
1012 (1..=n).map(|t| 3.0 + 2.0 * t as f64 + pattern[(t - 1) % 4]).collect()
1013 }
1014
1015 #[test]
1016 fn straight_line_forecasts_exactly() {
1017 let ys: Vec<f64> = (1..=20).map(|t| 3.0 + 2.0 * t as f64).collect();
1018 let ts = timeline(20);
1019 close_tol(ev("FORECAST.ETS", vec![n(25.0), range(&ys), range(&ts)]), 53.0, 1e-9);
1020 close_tol(ev("FORECAST.ETS", vec![n(20.5), range(&ys), range(&ts), n(0.0)]), 44.0, 1e-9);
1021 close(ev("FORECAST.ETS.SEASONALITY", vec![range(&ys), range(&ts)]), 0.0);
1022 close_tol(ev("FORECAST.ETS.CONFINT", vec![n(25.0), range(&ys), range(&ts)]), 0.0, 1e-9);
1023 close(ev("FORECAST.ETS.STAT", vec![range(&ys), range(&ts), n(8.0)]), 1.0);
1024 close_tol(ev("FORECAST.ETS.STAT", vec![range(&ys), range(&ts), n(6.0)]), 0.0, 1e-9);
1025 let v = ev("FORECAST.ETS", vec![av(row(&[21.0, 22.0])), range(&ys), range(&ts)]);
1027 let r = rows_of(&v);
1028 close_tol(r[0][0].clone(), 45.0, 1e-9);
1029 close_tol(r[0][1].clone(), 47.0, 1e-9);
1030 }
1031
1032 #[test]
1033 fn seasonal_series_is_detected_and_continued() {
1034 let ys = seasonal(24);
1035 let ts = timeline(24);
1036 close(ev("FORECAST.ETS.SEASONALITY", vec![range(&ys), range(&ts)]), 4.0);
1037 close_tol(ev("FORECAST.ETS", vec![n(27.0), range(&ys), range(&ts)]), 64.0, 1e-7);
1039 close_tol(ev("FORECAST.ETS", vec![n(28.0), range(&ys), range(&ts), n(4.0)]), 50.0, 1e-7);
1040 let mut rev_y = ys.clone();
1042 let mut rev_t = ts.clone();
1043 rev_y.reverse();
1044 rev_t.reverse();
1045 close_tol(ev("FORECAST.ETS", vec![n(27.0), range(&rev_y), range(&rev_t)]), 64.0, 1e-7);
1046 }
1047
1048 #[test]
1049 fn noisy_series_statistics_are_consistent() {
1050 let ys: Vec<f64> = seasonal(48).iter().enumerate().map(|(i, v)| v + ((i * 37 % 11) as f64 - 5.0) * 0.4).collect();
1052 let ts = timeline(48);
1053 let stat = |k: f64| match ev("FORECAST.ETS.STAT", vec![range(&ys), range(&ts), n(k)]) {
1054 Value::Number(x) => x,
1055 other => panic!("statistic {k}: {other:?}"),
1056 };
1057 for k in [1.0, 2.0, 3.0] {
1058 assert!((0.0..=1.0).contains(&stat(k)), "parameter {k} out of range");
1059 }
1060 let (mase, smape, mae, rmse) = (stat(4.0), stat(5.0), stat(6.0), stat(7.0));
1061 assert!(mae > 0.0 && rmse >= mae && mase > 0.0 && smape > 0.0 && smape < 2.0);
1062 let conf = |t: f64, c: f64| match ev("FORECAST.ETS.CONFINT", vec![n(t), range(&ys), range(&ts), n(c)]) {
1063 Value::Number(x) => x,
1064 other => panic!("confint: {other:?}"),
1065 };
1066 assert!(conf(49.0, 0.95) > 0.0);
1067 assert!(conf(60.0, 0.95) > conf(49.0, 0.95), "the interval widens with the horizon");
1068 assert!(conf(49.0, 0.99) > conf(49.0, 0.95));
1069 match ev("FORECAST.ETS", vec![n(51.0), range(&ys), range(&ts)]) {
1071 Value::Number(x) => assert!((x - (3.0 + 102.0 + 7.0)).abs() < 3.0, "forecast {x}"),
1072 other => panic!("{other:?}"),
1073 }
1074 }
1075
1076 #[test]
1077 fn monthly_dates_missing_points_and_duplicates() {
1078 let mut ts = Vec::new();
1080 let mut serial = 45292.0;
1081 for i in 0..24 {
1082 ts.push(serial);
1083 let (y, m) = (2024 + i / 12, (i % 12) as u32 + 1);
1084 serial += f64::from(crate::base::date::days_in_month(y, m));
1085 }
1086 let ys: Vec<f64> = (0..24).map(|i| 100.0 + 5.0 * i as f64).collect();
1087 close_tol(ev("FORECAST.ETS", vec![n(46023.0), range(&ys), range(&ts), n(0.0)]), 220.0, 1e-9);
1089 close_tol(ev("FORECAST.ETS", vec![n(46082.0), range(&ys), range(&ts), n(0.0)]), 230.0, 1e-9);
1090 let gaps_t: Vec<f64> = [1.0, 2.0, 4.0, 5.0, 6.0, 8.0, 9.0, 10.0].to_vec();
1092 let gaps_y: Vec<f64> = gaps_t.iter().map(|t| 10.0 * t).collect();
1093 close_tol(ev("FORECAST.ETS", vec![n(12.0), range(&gaps_y), range(&gaps_t), n(0.0)]), 120.0, 1e-9);
1094 assert_ne!(ev("FORECAST.ETS", vec![n(12.0), range(&gaps_y), range(&gaps_t), n(0.0), n(0.0)]), Value::Number(120.0));
1095 let dup_t = [1.0, 2.0, 2.0, 3.0, 4.0, 5.0];
1097 let dup_y = [2.0, 3.0, 5.0, 6.0, 8.0, 10.0];
1098 close_tol(ev("FORECAST.ETS", vec![n(6.0), range(&dup_y), range(&dup_t), n(0.0)]), 12.0, 1e-9);
1099 close_tol(ev("FORECAST.ETS.STAT", vec![range(&dup_y), range(&dup_t), n(8.0)]), 1.0, 1e-12);
1100 assert_ne!(ev("FORECAST.ETS", vec![n(6.0), range(&dup_y), range(&dup_t), n(0.0), n(1.0), n(7.0)]), Value::Number(12.0));
1101 }
1102
1103 #[test]
1104 fn documented_errors() {
1105 let ys = seasonal(12);
1106 let ts = timeline(12);
1107 let ets = |args: Vec<Arg>| ev("FORECAST.ETS", args);
1108 is_err(ets(vec![n(5.0), range(&ys), range(&ts)]), CellError::Num);
1110 is_err(ets(vec![n(13.0), range(&ys), range(&ts[..11])]), CellError::NA);
1112 is_err(ets(vec![n(5.0), range(&[1.0, 2.0, 3.0]), range(&[1.0, 2.0, 4.5])]), CellError::Num);
1114 for (i, bad) in [(3, 8761.0), (3, -1.0), (4, 2.0), (5, 8.0)] {
1116 let mut args = vec![n(13.0), range(&ys), range(&ts), n(1.0), n(1.0), n(0.0)];
1117 args[i] = n(bad);
1118 is_err(ets(args), CellError::Num);
1119 }
1120 is_err(ets(vec![n(30.0), range(&[1.0, 2.0, 3.0]), range(&[1.0, 2.0, 20.0])]), CellError::Num);
1122 is_err(ets(vec![n(13.0), range(&[1.0, 2.0]), rf(arr(vec![vec![nv(1.0)], vec![tv("x")]]))]), CellError::Value);
1124 is_err(ets(vec![n(13.0), rf(arr(vec![vec![nv(1.0)], vec![Value::Error(CellError::Div0)]])), range(&[1.0, 2.0])]), CellError::Div0);
1125 is_err(ev("FORECAST.ETS.CONFINT", vec![n(13.0), range(&ys), range(&ts), n(1.0)]), CellError::Num);
1127 is_err(ev("FORECAST.ETS.CONFINT", vec![n(13.0), range(&ys), range(&ts), n(0.0)]), CellError::Num);
1128 is_err(ev("FORECAST.ETS.STAT", vec![range(&ys), range(&ts), n(9.0)]), CellError::Num);
1129 is_err(ev("FORECAST.ETS.STAT", vec![range(&ys), range(&ts), n(0.0)]), CellError::Num);
1130 is_err(ets(vec![n(1e15), range(&[1.0, 2.0, 3.0]), range(&[0.0, 1.0, 1e12])]), CellError::Num);
1132 is_err(ets(vec![n(1e308), range(&ys), range(&ts)]), CellError::Num);
1133 }
1134
1135 #[test]
1136 fn timelines_continue_past_their_last_point() {
1137 let tl = Timeline::detect(&[3.0, 1.0, 2.0, 2.0, 5.0], DateSystem::D1900).expect("numeric step");
1138 assert_eq!(tl.after_last(1), Some(6.0));
1139 assert_eq!(tl.steps_after_last(8.5), Some(3.5));
1140 let tl = Timeline::detect(&[45322.0, 45351.0, 45382.0], DateSystem::D1900).expect("months");
1142 assert_eq!(tl.after_last(1), Some(45412.0)); assert_eq!(tl.steps_after_last(45412.0), Some(1.0));
1144 assert!(Timeline::detect(&[1.0, 2.0, 4.5], DateSystem::D1900).is_none());
1145 assert!(Timeline::detect(&[1.0, 2.0, 9.0], DateSystem::D1900).is_none(), "too many missing points");
1146 assert!(Timeline::detect(&[1.0], DateSystem::D1900).is_none());
1147 assert!(Timeline::detect(&[1.0, f64::NAN], DateSystem::D1900).is_none());
1148 }
1149}