ruby 52 lines · 8 steps

A weighted moving average forecaster in Ruby

A class that predicts the next value in a series by blending recent points with normalized weights.

Explained by highlit
1class WeightedMovingAverageForecaster
2 DEFAULT_WINDOW = 5
3 
4 def initialize(window: DEFAULT_WINDOW, weights: nil)
5 @window = window
6 @weights = normalize(weights || default_weights(window))
7 
8 unless @weights.size == @window
9 raise ArgumentError, "expected #{@window} weights, got #{@weights.size}"
10 end
11 end
12 
13 def forecast(series)
14 return nil if series.size < @window
15 
16 recent = series.last(@window)
17 recent.zip(@weights).sum { |value, weight| value * weight }
18 end
19 
20 def forecast_next(series, steps:)
21 working = series.dup
22 
23 Array.new(steps) do
24 predicted = forecast(working)
25 break [] if predicted.nil?
26 
27 working << predicted
28 predicted
29 end
30 end
31 
32 def rolling(series)
33 return [] if series.size < @window
34 
35 (@window..series.size).map do |upper|
36 forecast(series[0...upper])
37 end
38 end
39 
40 private
41 
42 def default_weights(window)
43 (1..window).to_a
44 end
45 
46 def normalize(weights)
47 total = weights.sum.to_f
48 raise ArgumentError, "weights must sum to a positive value" unless total.positive?
49 
50 weights.map { |w| w / total }
51 end
52end
01 / 01
STEP 01

Walkthrough

Space play ←→ step click any line
Three takeaways
  1. 1Normalizing weights so they sum to one lets the forecast stay on the same scale as the input values.
  2. 2Feeding a prediction back into the series turns a one-step forecast into a multi-step one.
  3. 3Validating configuration in the constructor catches mismatched inputs before any forecasting runs.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A weighted moving average forecaster in Ruby — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code