diff --git a/lib/tealeaves.rb b/lib/tealeaves.rb index 085cbb2..307ae8a 100644 --- a/lib/tealeaves.rb +++ b/lib/tealeaves.rb @@ -1,5 +1,17 @@ require 'tealeaves/moving_average' require 'tealeaves/seasonal_components' +require 'tealeaves/forecast' require 'tealeaves/naive_forecast' require 'tealeaves/single_exponential_smoothing_forecast' +require 'tealeaves/brute_force_optimization' require 'tealeaves/exponential_smoothing_forecast' + +module TeaLeaves + def self.optimal_model(time_series, period) + BruteForceOptimization.new(time_series, period).optimize + end + + def self.forecast(time_series, period, periods_ahead=nil) + optimal_model(time_series, period).predict(periods_ahead) + end +end diff --git a/lib/tealeaves/brute_force_optimization.rb b/lib/tealeaves/brute_force_optimization.rb new file mode 100644 index 0000000..3420f5c --- /dev/null +++ b/lib/tealeaves/brute_force_optimization.rb @@ -0,0 +1,84 @@ +module TeaLeaves + class BruteForceOptimization + INITIAL_PARAMETER_VALUES = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0].freeze + + def initialize(time_series, period, opts={}) + @time_series = time_series + @period = period + @opts = opts + end + + + def optimize + [0.1, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625].inject(optimum(initial_models)) do |model, change| + improve_model(model, change) + end + end + + def initial_test_parameters(opts={}) + parameters = [] + INITIAL_PARAMETER_VALUES.each do |alpha| + parameters << {:alpha => alpha, :seasonality => :none, :trend => :none} + + unless opts[:seasonality] == :none && opts[:trend] == :none + INITIAL_PARAMETER_VALUES.each do |b| + parameters << {:alpha => alpha, :beta => b, :seasonality => :none, :trend => :additive} + parameters << {:alpha => alpha, :beta => b, :seasonality => :none, :trend => :multiplicative} + parameters << {:alpha => alpha, :gamma => b, :trend => :none, :seasonality => :additive} + parameters << {:alpha => alpha, :gamma => b, :trend => :none, :seasonality => :multiplicative} + + INITIAL_PARAMETER_VALUES.each do |gamma| + [:additive, :multiplicative].each do |trend| + [:additive, :multiplicative].each do |seasonality| + parameters << { + :alpha => alpha, + :beta => b, + :gamma => gamma, + :trend => trend, + :seasonality => seasonality + } + end + end + end + end + end + end + + parameters + end + + private + + def improve_model(model, change) + trend_operations = model.trend == :none ? [nil] : [:+, :-, nil] + season_operations = model.seasonality == :none ? [nil] : [:+, :-, nil] + permutations = [:+, :-, nil].product(trend_operations, season_operations) + optimum(permutations.map do |(op_1,op_2,op_3)| + new_opts = {} + set_value(new_opts, :alpha, model, op_1, change) + set_value(new_opts, :beta, model, op_2, change) + set_value(new_opts, :gamma, model, op_3, change) + model.improve(new_opts) + end) + end + + def set_value(hsh, key, model, op, change) + unless op.nil? + new_value = model.send(key).send(op, change) + if new_value >= 0.0 && new_value <= 1.0 + hsh[key] = new_value + end + end + end + + def optimum(models) + models.min_by(&:mean_squared_error) + end + + def initial_models + initial_test_parameters.map do |parameters| + ExponentialSmoothingForecast.new(@time_series, @period, parameters) + end + end + end +end diff --git a/lib/tealeaves/exponential_smoothing_forecast.rb b/lib/tealeaves/exponential_smoothing_forecast.rb index d101477..c0867c0 100644 --- a/lib/tealeaves/exponential_smoothing_forecast.rb +++ b/lib/tealeaves/exponential_smoothing_forecast.rb @@ -68,6 +68,8 @@ def apply(forecast, parameters, n) end end + attr_reader :alpha, :beta, :gamma, :trend, :seasonality + def initialize(time_series, period, opts={}) @time_series = time_series @period = period @@ -88,6 +90,11 @@ def initialize(time_series, period, opts={}) calculate_one_step_ahead_forecasts end + def improve(opts) + new_opts = {:alpha => @alpha, :beta => @beta, :gamma => @gamma, :trend => @trend, :seasonality => @seasonality}.merge(opts) + self.class.new(@time_series, @period, new_opts) + end + attr_reader :model_parameters def initial_level @@ -114,7 +121,7 @@ def initial_parameters def predict(n=nil) if n.nil? - forecast(@model_parameters) + forecast(@model_parameters).first else (1..n).map {|i| forecast(@model_parameters, i).first } end diff --git a/spec/brute_force_optimization_spec.rb b/spec/brute_force_optimization_spec.rb new file mode 100644 index 0000000..a18cecd --- /dev/null +++ b/spec/brute_force_optimization_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe TeaLeaves::BruteForceOptimization do + it "should have 1014 initial test models" do + described_class.new([1,2,3,4], 1).initial_test_parameters.size.should == 1014 + end + + it "should produce an initial model" do + + end +end