tealeaves 0.0.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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +16 -0
- data/LICENSE +20 -0
- data/README.rdoc +40 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/tealeaves.rb +17 -0
- data/lib/tealeaves/brute_force_optimization.rb +84 -0
- data/lib/tealeaves/exponential_smoothing_forecast.rb +212 -0
- data/lib/tealeaves/forecast.rb +22 -0
- data/lib/tealeaves/moving_average.rb +110 -0
- data/lib/tealeaves/naive_forecast.rb +30 -0
- data/lib/tealeaves/seasonal_components.rb +28 -0
- data/lib/tealeaves/single_exponential_smoothing_forecast.rb +28 -0
- data/spec/brute_force_optimization_spec.rb +11 -0
- data/spec/exponential_smoothing_forecast_spec.rb +77 -0
- data/spec/moving_average_spec.rb +61 -0
- data/spec/naive_forecast_spec.rb +27 -0
- data/spec/seasonal_components_spec.rb +18 -0
- data/spec/single_exponential_smoothing_forecast_spec.rb +28 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +11 -0
- data/tealeaves.gemspec +82 -0
- metadata +195 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
group :development do
|
4
|
+
gem "rake"
|
5
|
+
gem "rspec", "~> 2.8.0"
|
6
|
+
gem "yard", "~> 0.7"
|
7
|
+
gem "rdoc", "~> 3.12"
|
8
|
+
gem "bundler", "~> 1.0.0"
|
9
|
+
gem "jeweler", "~> 1.8.3"
|
10
|
+
gem "rcov", ">= 0"
|
11
|
+
end
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem "rake"
|
15
|
+
gem "rspec", "~> 2.8.0"
|
16
|
+
end
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Roland Swingler
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
= tealeaves
|
2
|
+
|
3
|
+
{<img src="https://secure.travis-ci.org/knaveofdiamonds/tealeaves.png?branch=master" alt="Build Status" />}[http://travis-ci.org/knaveofdiamonds/tealeaves]
|
4
|
+
|
5
|
+
Tealeaves is a simple forecasting toolset for ruby, able to product short term forecasts for time series data.
|
6
|
+
|
7
|
+
It implements Exponential Smoothing methods, including those dealing with seasonality & trends, and has some basic functionality to determine optimal models.
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
require 'tealeaves'
|
12
|
+
|
13
|
+
# A set of time series data
|
14
|
+
data = [1.0, 3.3 ... 24.56]
|
15
|
+
|
16
|
+
# Period, for example 12 for monthly data
|
17
|
+
period = 12
|
18
|
+
|
19
|
+
# Get an 'optimal' model
|
20
|
+
TeaLeaves.optimal_model(data, period)
|
21
|
+
|
22
|
+
# Or next period's forecasts from the optimal model
|
23
|
+
TeaLeaves.forecast(data, period)
|
24
|
+
|
25
|
+
# Or the next n period's forecasts from the optimal model
|
26
|
+
TeaLeaves.forecast(data, period, 3)
|
27
|
+
|
28
|
+
== Note on Patches/Pull Requests
|
29
|
+
|
30
|
+
* Fork the project.
|
31
|
+
* Make your feature addition or bug fix.
|
32
|
+
* Add tests for it. This is important so I don't break it in a
|
33
|
+
future version unintentionally.
|
34
|
+
* Commit, do not mess with rakefile, version, or history.
|
35
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
36
|
+
* Send me a pull request. Bonus points for topic branches.
|
37
|
+
|
38
|
+
== Copyright
|
39
|
+
|
40
|
+
Copyright (c) 2010 Roland Swingler. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
if ENV["TRAVIS"]
|
7
|
+
Bundler.setup(:default, :test)
|
8
|
+
else
|
9
|
+
Bundler.setup(:default, :development)
|
10
|
+
end
|
11
|
+
rescue Bundler::BundlerError => e
|
12
|
+
$stderr.puts e.message
|
13
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
14
|
+
exit e.status_code
|
15
|
+
end
|
16
|
+
require 'rake'
|
17
|
+
|
18
|
+
begin
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
22
|
+
gem.name = "tealeaves"
|
23
|
+
gem.homepage = "http://github.com/knaveofdiamonds/tealeaves"
|
24
|
+
gem.license = "MIT"
|
25
|
+
gem.summary = %Q{Simple Forecasting Methods in Ruby}
|
26
|
+
gem.description = %Q{Exponential smoothing based forecasting methods for time series data}
|
27
|
+
gem.email = "roland.swingler@gmail.com"
|
28
|
+
gem.authors = ["Roland Swingler"]
|
29
|
+
# dependencies defined in Gemfile
|
30
|
+
end
|
31
|
+
Jeweler::RubygemsDotOrgTasks.new
|
32
|
+
|
33
|
+
require 'yard'
|
34
|
+
YARD::Rake::YardocTask.new
|
35
|
+
rescue LoadError => e
|
36
|
+
$stderr.puts "Not loading standard development tasks."
|
37
|
+
end
|
38
|
+
|
39
|
+
require 'rspec/core'
|
40
|
+
require 'rspec/core/rake_task'
|
41
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
42
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
43
|
+
end
|
44
|
+
|
45
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
46
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
47
|
+
spec.rcov = true
|
48
|
+
end
|
49
|
+
|
50
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/tealeaves.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'tealeaves/moving_average'
|
2
|
+
require 'tealeaves/seasonal_components'
|
3
|
+
require 'tealeaves/forecast'
|
4
|
+
require 'tealeaves/naive_forecast'
|
5
|
+
require 'tealeaves/single_exponential_smoothing_forecast'
|
6
|
+
require 'tealeaves/brute_force_optimization'
|
7
|
+
require 'tealeaves/exponential_smoothing_forecast'
|
8
|
+
|
9
|
+
module TeaLeaves
|
10
|
+
def self.optimal_model(time_series, period)
|
11
|
+
BruteForceOptimization.new(time_series, period).optimize
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.forecast(time_series, period, periods_ahead=nil)
|
15
|
+
optimal_model(time_series, period).predict(periods_ahead)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module TeaLeaves
|
2
|
+
class BruteForceOptimization
|
3
|
+
INITIAL_PARAMETER_VALUES = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0].freeze
|
4
|
+
|
5
|
+
def initialize(time_series, period, opts={})
|
6
|
+
@time_series = time_series
|
7
|
+
@period = period
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def optimize
|
13
|
+
[0.1, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625].inject(optimum(initial_models)) do |model, change|
|
14
|
+
improve_model(model, change)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initial_test_parameters(opts={})
|
19
|
+
parameters = []
|
20
|
+
INITIAL_PARAMETER_VALUES.each do |alpha|
|
21
|
+
parameters << {:alpha => alpha, :seasonality => :none, :trend => :none}
|
22
|
+
|
23
|
+
unless opts[:seasonality] == :none && opts[:trend] == :none
|
24
|
+
INITIAL_PARAMETER_VALUES.each do |b|
|
25
|
+
parameters << {:alpha => alpha, :beta => b, :seasonality => :none, :trend => :additive}
|
26
|
+
parameters << {:alpha => alpha, :beta => b, :seasonality => :none, :trend => :multiplicative}
|
27
|
+
parameters << {:alpha => alpha, :gamma => b, :trend => :none, :seasonality => :additive}
|
28
|
+
parameters << {:alpha => alpha, :gamma => b, :trend => :none, :seasonality => :multiplicative}
|
29
|
+
|
30
|
+
INITIAL_PARAMETER_VALUES.each do |gamma|
|
31
|
+
[:additive, :multiplicative].each do |trend|
|
32
|
+
[:additive, :multiplicative].each do |seasonality|
|
33
|
+
parameters << {
|
34
|
+
:alpha => alpha,
|
35
|
+
:beta => b,
|
36
|
+
:gamma => gamma,
|
37
|
+
:trend => trend,
|
38
|
+
:seasonality => seasonality
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
parameters
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def improve_model(model, change)
|
53
|
+
trend_operations = model.trend == :none ? [nil] : [:+, :-, nil]
|
54
|
+
season_operations = model.seasonality == :none ? [nil] : [:+, :-, nil]
|
55
|
+
permutations = [:+, :-, nil].product(trend_operations, season_operations)
|
56
|
+
optimum(permutations.map do |(op_1,op_2,op_3)|
|
57
|
+
new_opts = {}
|
58
|
+
set_value(new_opts, :alpha, model, op_1, change)
|
59
|
+
set_value(new_opts, :beta, model, op_2, change)
|
60
|
+
set_value(new_opts, :gamma, model, op_3, change)
|
61
|
+
model.improve(new_opts)
|
62
|
+
end)
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_value(hsh, key, model, op, change)
|
66
|
+
unless op.nil?
|
67
|
+
new_value = model.send(key).send(op, change)
|
68
|
+
if new_value >= 0.0 && new_value <= 1.0
|
69
|
+
hsh[key] = new_value
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def optimum(models)
|
75
|
+
models.min_by(&:mean_squared_error)
|
76
|
+
end
|
77
|
+
|
78
|
+
def initial_models
|
79
|
+
initial_test_parameters.map do |parameters|
|
80
|
+
ExponentialSmoothingForecast.new(@time_series, @period, parameters)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
require 'tealeaves/forecast'
|
2
|
+
|
3
|
+
module TeaLeaves
|
4
|
+
class ExponentialSmoothingForecast < Forecast
|
5
|
+
class SeasonalityStrategy
|
6
|
+
attr_reader :start_index
|
7
|
+
|
8
|
+
def initialize(period, gamma)
|
9
|
+
@gamma = gamma
|
10
|
+
@start_index = period
|
11
|
+
end
|
12
|
+
|
13
|
+
def new_values(observed_value, parameters, new_level)
|
14
|
+
new_seasonality = @gamma * t(observed_value, new_level) +
|
15
|
+
(1 - @gamma) * parameters[:seasonality].first
|
16
|
+
parameters[:seasonality].drop(1) << new_seasonality
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class AdditiveSeasonalityStrategy < SeasonalityStrategy
|
21
|
+
def p(value, params)
|
22
|
+
value - params[:seasonality].first
|
23
|
+
end
|
24
|
+
|
25
|
+
def t(value, new_level)
|
26
|
+
value - new_level
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply(forecast, parameters, n)
|
30
|
+
index = (n - 1) % parameters[:seasonality].size
|
31
|
+
forecast + parameters[:seasonality][index]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class MultiplicativeSeasonalityStrategy < SeasonalityStrategy
|
36
|
+
def p(value, params)
|
37
|
+
value / params[:seasonality].first
|
38
|
+
end
|
39
|
+
|
40
|
+
def t(value, new_level)
|
41
|
+
value / new_level
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply(forecast, parameters, n)
|
45
|
+
index = (n - 1) % parameters[:seasonality].size
|
46
|
+
forecast * parameters[:seasonality][index]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class NoSeasonalityStrategy < SeasonalityStrategy
|
51
|
+
def p(value, params)
|
52
|
+
value
|
53
|
+
end
|
54
|
+
|
55
|
+
def t(value, new_level)
|
56
|
+
end
|
57
|
+
|
58
|
+
def new_values(*args)
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
|
62
|
+
def start_index
|
63
|
+
1
|
64
|
+
end
|
65
|
+
|
66
|
+
def apply(forecast, parameters, n)
|
67
|
+
forecast
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_reader :alpha, :beta, :gamma, :trend, :seasonality
|
72
|
+
|
73
|
+
def initialize(time_series, period, opts={})
|
74
|
+
@time_series = time_series
|
75
|
+
@period = period
|
76
|
+
@alpha = opts[:alpha]
|
77
|
+
@beta = opts[:beta]
|
78
|
+
@gamma = opts[:gamma]
|
79
|
+
@trend = opts[:trend]
|
80
|
+
@seasonality = opts[:seasonality]
|
81
|
+
@seasonality_strategy = case @seasonality
|
82
|
+
when :none
|
83
|
+
NoSeasonalityStrategy.new(@period, opts[:gamma])
|
84
|
+
when :additive
|
85
|
+
AdditiveSeasonalityStrategy.new(@period, opts[:gamma])
|
86
|
+
when :multiplicative
|
87
|
+
MultiplicativeSeasonalityStrategy.new(@period, opts[:gamma])
|
88
|
+
end
|
89
|
+
|
90
|
+
calculate_one_step_ahead_forecasts
|
91
|
+
end
|
92
|
+
|
93
|
+
def improve(opts)
|
94
|
+
new_opts = {:alpha => @alpha, :beta => @beta, :gamma => @gamma, :trend => @trend, :seasonality => @seasonality}.merge(opts)
|
95
|
+
self.class.new(@time_series, @period, new_opts)
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :model_parameters
|
99
|
+
|
100
|
+
def initial_level
|
101
|
+
@initial_level ||= @time_series.take(@period).inject(&:+).to_f / @period
|
102
|
+
end
|
103
|
+
|
104
|
+
def initial_trend
|
105
|
+
period_1, period_2 = @time_series.each_slice(@period).take(2)
|
106
|
+
period_1.zip(period_2).map {|(a,b)| (b - a) / @period.to_f }.inject(&:+) / @period
|
107
|
+
end
|
108
|
+
|
109
|
+
def initial_seasonal_indices
|
110
|
+
operation = @seasonality == :multiplicative ? :/ : :-
|
111
|
+
@time_series.take(@period).map {|v| v.to_f.send(operation, initial_level) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def initial_parameters
|
115
|
+
{ :level => initial_level,
|
116
|
+
:trend => initial_trend,
|
117
|
+
:seasonality => initial_seasonal_indices,
|
118
|
+
:index => @seasonality_strategy.start_index
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def predict(n=nil)
|
123
|
+
if n.nil?
|
124
|
+
forecast(@model_parameters).first
|
125
|
+
else
|
126
|
+
(1..n).map {|i| forecast(@model_parameters, i).first }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the mean squared error of the forecast.
|
131
|
+
def mean_squared_error
|
132
|
+
return @mean_squared_error if @mean_squared_error
|
133
|
+
|
134
|
+
numerator = errors.drop(@seasonality_strategy.start_index).map {|i| i ** 2 }.inject(&:+)
|
135
|
+
@mean_squared_error = numerator / (errors.size - @seasonality_strategy.start_index).to_f
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def calculate_one_step_ahead_forecasts
|
141
|
+
forecasts = [nil] * @seasonality_strategy.start_index
|
142
|
+
parameters = initial_parameters
|
143
|
+
(@seasonality_strategy.start_index...@time_series.size).each do |i|
|
144
|
+
forecast, parameters = forecast(parameters)
|
145
|
+
forecasts << forecast
|
146
|
+
end
|
147
|
+
parameters[:index] -= 1
|
148
|
+
@model_parameters = parameters
|
149
|
+
@one_step_ahead_forecasts = forecasts
|
150
|
+
end
|
151
|
+
|
152
|
+
def forecast(parameters, n=1)
|
153
|
+
new_params = {}
|
154
|
+
new_params[:level] = new_level(parameters)
|
155
|
+
new_params[:trend] = new_trend(parameters, new_params[:level])
|
156
|
+
new_params[:seasonality] = new_seasonality(parameters, new_params[:level])
|
157
|
+
new_params[:index] = parameters[:index] + 1
|
158
|
+
|
159
|
+
pre_forecast = case @trend
|
160
|
+
when :none
|
161
|
+
parameters[:level]
|
162
|
+
when :additive
|
163
|
+
parameters[:level] + (n * parameters[:trend])
|
164
|
+
when :multiplicative
|
165
|
+
parameters[:level] * (parameters[:trend] ** n)
|
166
|
+
end
|
167
|
+
|
168
|
+
forecast = @seasonality_strategy.apply(pre_forecast, parameters, n)
|
169
|
+
[forecast, new_params]
|
170
|
+
end
|
171
|
+
|
172
|
+
def new_level(parameters)
|
173
|
+
@alpha * p(parameters) + (1 - @alpha) * q(parameters)
|
174
|
+
end
|
175
|
+
|
176
|
+
def new_trend(parameters, new_level)
|
177
|
+
unless @trend == :none
|
178
|
+
@beta * r(parameters, new_level) + (1 - @beta) * parameters[:trend]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def new_seasonality(parameters, new_level)
|
183
|
+
@seasonality_strategy.new_values(@time_series[parameters[:index]],
|
184
|
+
parameters,
|
185
|
+
new_level)
|
186
|
+
end
|
187
|
+
|
188
|
+
def p(params)
|
189
|
+
@seasonality_strategy.p(@time_series[params[:index]], params)
|
190
|
+
end
|
191
|
+
|
192
|
+
def q(params)
|
193
|
+
case @trend
|
194
|
+
when :none
|
195
|
+
params[:level]
|
196
|
+
when :additive
|
197
|
+
params[:level] + params[:trend]
|
198
|
+
when :multiplicative
|
199
|
+
params[:level] * params[:trend]
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def r(params, new_level)
|
204
|
+
case @trend
|
205
|
+
when :additive
|
206
|
+
new_level - params[:level]
|
207
|
+
when :multiplicative
|
208
|
+
new_level / params[:level]
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module TeaLeaves
|
2
|
+
class Forecast
|
3
|
+
# Returns an array of 1 step ahead forecasts. The initial value in
|
4
|
+
# this array will be nil - there is no way of predicting the first
|
5
|
+
# value of the series.
|
6
|
+
attr_reader :one_step_ahead_forecasts
|
7
|
+
|
8
|
+
# Returns the errors between the observed values and the one step
|
9
|
+
# ahead forecasts.
|
10
|
+
def errors
|
11
|
+
@errors ||= @time_series.zip(one_step_ahead_forecasts).map do |(observation, forecast)|
|
12
|
+
forecast - observation if forecast && observation
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the mean squared error of the forecast.
|
17
|
+
def mean_squared_error
|
18
|
+
numerator = errors.drop(1).map {|i| i ** 2 }.inject(&:+)
|
19
|
+
numerator / (errors.size - 1).to_f
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module TeaLeaves
|
2
|
+
# A calculator for simple & weighted moving averages.
|
3
|
+
class MovingAverage
|
4
|
+
attr_reader :weights, :span
|
5
|
+
|
6
|
+
# Returns a Weighted Moving Average calculator, given a list of
|
7
|
+
# weights.
|
8
|
+
#
|
9
|
+
# The list of weights should be an odd length, and should sum to
|
10
|
+
# 1.
|
11
|
+
#
|
12
|
+
# Examples:
|
13
|
+
#
|
14
|
+
# MovingAverage.weighted([0.15, 0.7, 0.15]) # => [0.2, 0.7, 0.1]
|
15
|
+
#
|
16
|
+
def self.weighted(weights)
|
17
|
+
new(weights)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a Simple Moving Average calculator, given a span n.
|
21
|
+
#
|
22
|
+
# Examples:
|
23
|
+
#
|
24
|
+
# MovingAverage.simple(5).weights # => [0.2, 0.2, 0.2, 0.2, 0.2]
|
25
|
+
#
|
26
|
+
# @param [Integer] n the span or order of the moving average,
|
27
|
+
# i.e. the number of terms to include in each average.
|
28
|
+
def self.simple(n)
|
29
|
+
raise ArgumentError.new("Number of terms must be positive") if n < 1
|
30
|
+
weights = n.odd?() ? [1.0 / n] * n : expand_weights([1.0 / n] * (n / 2) + [1.0 / (2 * n)])
|
31
|
+
new weights
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Returns a moving average of a moving average calculator.
|
36
|
+
#
|
37
|
+
# For Example, for a 3x3 MA:
|
38
|
+
#
|
39
|
+
# MovingAverage.multiple(3,3).weights #=> [1/9, 2/9, 1/3, 2/9, 1/9]
|
40
|
+
#
|
41
|
+
def self.multiple(m,n)
|
42
|
+
divisor = (m * n).to_f
|
43
|
+
weights = (1..m).map {|i| i / divisor }
|
44
|
+
num_of_center_weights = ((m + n) / 2) - m
|
45
|
+
num_of_center_weights = 0 if num_of_center_weights < 0
|
46
|
+
num_of_center_weights.times { weights << weights.last }
|
47
|
+
|
48
|
+
new(expand_weights(weights.reverse))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Creates a new MovingAverage given a list of weights.
|
52
|
+
#
|
53
|
+
# See also the class methods simple and weighted.
|
54
|
+
def initialize(weights)
|
55
|
+
@weights = weights
|
56
|
+
@span = @weights.length
|
57
|
+
check_weights
|
58
|
+
end
|
59
|
+
|
60
|
+
# Calculates the moving average for the given array of numbers.
|
61
|
+
#
|
62
|
+
# Moving averages won't include values for terms at the beginning or end of
|
63
|
+
# the array, so there will be fewer numbers than in the original.
|
64
|
+
def calculate(array)
|
65
|
+
return [] if @span > array.length
|
66
|
+
|
67
|
+
array.each_cons(@span).map do |window|
|
68
|
+
window.zip(weights).map {|(a,b)| a * b }.inject(&:+)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Error checking for weights
|
75
|
+
def check_weights
|
76
|
+
raise ArgumentError.new("Weights should be an odd list") unless @span.odd?
|
77
|
+
sum = weights.inject(&:+)
|
78
|
+
if sum < 0.999999 || sum > 1.000001
|
79
|
+
raise ArgumentError.new("Weights must sum to 1")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.expand_weights(weights)
|
84
|
+
left_side_weights = weights.reverse
|
85
|
+
left_side_weights.pop
|
86
|
+
left_side_weights + weights
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
module ArrayMethods
|
91
|
+
# Returns a moving average for this array, given either a number
|
92
|
+
# of terms or a list of weights.
|
93
|
+
#
|
94
|
+
# See MovingAverage for more detail.
|
95
|
+
#
|
96
|
+
def moving_average(average_specifier)
|
97
|
+
if average_specifier.kind_of?(Array)
|
98
|
+
avg = MovingAverage.weighted(average_specifier)
|
99
|
+
elsif average_specifier.kind_of?(Integer)
|
100
|
+
avg = MovingAverage.simple(average_specifier)
|
101
|
+
else
|
102
|
+
raise ArgumentError.new("Unknown weights")
|
103
|
+
end
|
104
|
+
|
105
|
+
avg.calculate(self)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
Array.send(:include, TeaLeaves::ArrayMethods)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'tealeaves/forecast'
|
2
|
+
|
3
|
+
module TeaLeaves
|
4
|
+
# A naive model just uses the current period's value as the
|
5
|
+
# prediction for the next period, i.e. F_t+1 = Y_t
|
6
|
+
class NaiveForecast < Forecast
|
7
|
+
# Creates a naive forecasting model for the given time series.
|
8
|
+
def initialize(time_series)
|
9
|
+
@time_series = time_series
|
10
|
+
@one_step_ahead_forecasts = [nil] + time_series
|
11
|
+
@one_step_ahead_forecasts.pop
|
12
|
+
end
|
13
|
+
|
14
|
+
# Returns Thiel's U Statistic. By definition, this is 1 for the
|
15
|
+
# Naive method.
|
16
|
+
def u_statistic
|
17
|
+
1
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a prediction for the next period, or for the next n
|
21
|
+
# periods.
|
22
|
+
def predict(n=nil)
|
23
|
+
if n.nil?
|
24
|
+
@time_series.last
|
25
|
+
else
|
26
|
+
[@time_series.last] * n
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module TeaLeaves
|
2
|
+
class SeasonalComponents
|
3
|
+
def initialize(period, data)
|
4
|
+
@period = period
|
5
|
+
@data = data
|
6
|
+
end
|
7
|
+
|
8
|
+
def seasonal_averages
|
9
|
+
@seasonal_averages ||= seasonal_groups.map do |group|
|
10
|
+
group.inject(&:+) / group.size.to_f
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def seasonal_factors(operation = :-)
|
15
|
+
@seasonal_factors ||= seasonal_averages.map {|i| i.send(operation, avg) }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def avg
|
21
|
+
@avg ||= seasonal_averages.inject(&:+) / @period.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
def seasonal_groups
|
25
|
+
@data.take(@period).zip( *(@data.drop(@period).each_slice(@period)) ).map(&:compact)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tealeaves/forecast'
|
2
|
+
|
3
|
+
module TeaLeaves
|
4
|
+
class SingleExponentialSmoothingForecast < Forecast
|
5
|
+
def initialize(time_series, alpha)
|
6
|
+
@time_series = time_series
|
7
|
+
@alpha = alpha
|
8
|
+
|
9
|
+
@one_step_ahead_forecasts = [nil]
|
10
|
+
|
11
|
+
([@time_series.first] + @time_series).inject do |a,b|
|
12
|
+
value = (1 - @alpha) * a + @alpha * b
|
13
|
+
@one_step_ahead_forecasts << value
|
14
|
+
value
|
15
|
+
end
|
16
|
+
|
17
|
+
@prediction = @one_step_ahead_forecasts.pop
|
18
|
+
end
|
19
|
+
|
20
|
+
def predict(n=nil)
|
21
|
+
if n.nil?
|
22
|
+
@prediction
|
23
|
+
else
|
24
|
+
[@prediction] * n
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::BruteForceOptimization do
|
4
|
+
it "should have 1014 initial test models" do
|
5
|
+
described_class.new([1,2,3,4], 1).initial_test_parameters.size.should == 1014
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should produce an initial model" do
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::ExponentialSmoothingForecast do
|
4
|
+
before :each do
|
5
|
+
@time_series = [1,2,3,5,
|
6
|
+
3,4,5,8,
|
7
|
+
6,7,8,10]
|
8
|
+
@options = {
|
9
|
+
:seasonality => :additive,
|
10
|
+
:trend => :additive,
|
11
|
+
:alpha => 0.7,
|
12
|
+
:beta => 0.1,
|
13
|
+
:gamma => 0.1
|
14
|
+
}
|
15
|
+
@forecast = described_class.new(@time_series, 4, @options)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should have an initial level of 11 / 4" do
|
19
|
+
@forecast.initial_level.
|
20
|
+
should be_within(0.001).of(11.0 / 4.0)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should have an initial trend" do
|
24
|
+
@forecast.initial_trend.
|
25
|
+
should be_within(0.0001).of(0.5625)
|
26
|
+
end
|
27
|
+
|
28
|
+
# [1 / (11/4.0), 2 / (11/4.0), 3 / (11/4.0), 5 / (11/4.0)]
|
29
|
+
it "should have initial seasonal indices for additive seasonality" do
|
30
|
+
@forecast.initial_seasonal_indices.should == [1 - (11/4.0), 2 - (11/4.0), 3 - (11/4.0), 5 - (11/4.0)]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have initial seasonal indices for additive seasonality" do
|
34
|
+
@options[:seasonality] = :multiplicative
|
35
|
+
@forecast = described_class.new(@time_series, 4, @options)
|
36
|
+
@forecast.initial_seasonal_indices.should == [1 / (11/4.0), 2 / (11/4.0), 3 / (11/4.0), 5 / (11/4.0)]
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should generate forecasts" do
|
40
|
+
data = [362,
|
41
|
+
385,
|
42
|
+
432,
|
43
|
+
341,
|
44
|
+
382,
|
45
|
+
409,
|
46
|
+
498,
|
47
|
+
387,
|
48
|
+
473,
|
49
|
+
513,
|
50
|
+
582,
|
51
|
+
474]
|
52
|
+
|
53
|
+
@forecast = described_class.new(data, 4,
|
54
|
+
:alpha => 0.822,
|
55
|
+
:beta => 0.055,
|
56
|
+
:gamma => 0.0,
|
57
|
+
:trend => :additive,
|
58
|
+
:seasonality => :multiplicative)
|
59
|
+
|
60
|
+
@forecast.initial_level.should == 380
|
61
|
+
@forecast.initial_trend.should == 9.75
|
62
|
+
@forecast.initial_seasonal_indices.zip([0.953, 1.013, 1.137, 0.897]).each do |(observed, expected)|
|
63
|
+
observed.should be_within(0.001).of(expected)
|
64
|
+
end
|
65
|
+
|
66
|
+
expected_values = [371.29, 414.64, 471.43, 399.3, 423.11, 506.60, 589.26, 471.93]
|
67
|
+
@forecast.one_step_ahead_forecasts.drop(4).zip(expected_values).each do |(observed, expected)|
|
68
|
+
observed.should be_within(0.03).of(expected)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should predict new values" do
|
73
|
+
@forecast.predict(4).zip([6.9, 8.4, 9.9, 12.5]).each do |(observed, expected)|
|
74
|
+
observed.should be_within(0.1).of(expected)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::MovingAverage do
|
4
|
+
it "should mix a #moving_average method into Array that takes a span" do
|
5
|
+
[0,1,2,6,4].moving_average(3).should == [1,3,4]
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should mix a #moving_average method into Array that takes weights" do
|
9
|
+
[0,1,2,6,4].moving_average([0.2,0.6,0.2]).should == [1.0, 2.6, 4.8]
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "a Simple Moving Average" do
|
13
|
+
it "should raise an ArgumentError if number of terms is < 1" do
|
14
|
+
lambda { described_class.simple(0) }.should raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have equal weights with an odd number of terms" do
|
18
|
+
described_class.simple(5).weights.should == [0.2, 0.2, 0.2, 0.2, 0.2]
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should have have half weights at the ends with an even number of terms" do
|
22
|
+
described_class.simple(4).weights.should == [0.125, 0.25, 0.25, 0.25, 0.125]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return 3 averages from #calculate with a 3 point MA over 5 terms" do
|
26
|
+
described_class.simple(3).calculate([0,1,2,6,4]).should == [1,3,4]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return 1 average from #calculate with a 4 point MA over 5 terms" do
|
30
|
+
described_class.simple(4).calculate([0,1,2,6,4]).should == [2.75]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return no averages from #calculate with a 7 point MA over 5 terms" do
|
34
|
+
described_class.simple(7).calculate([0,1,2,6,4]).should == []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "a Weighted Moving Average" do
|
39
|
+
it "raises an Argument error if the weights do not sum to 1" do
|
40
|
+
expect { described_class.weighted([0.5, 0.5, 0.1]) }.to raise_error(ArgumentError)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "raises an Argument error if the list of weights is not odd sized" do
|
44
|
+
expect { described_class.weighted([0.5, 0.5]) }.to raise_error(ArgumentError)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should allow asymmetric weights" do
|
48
|
+
described_class.weighted([0.6, 0.3, 0.1]).weights == [0.6, 0.3, 0.1]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "a Mutliple Moving Average" do
|
53
|
+
it "should combine weights in the 3x3 case" do
|
54
|
+
described_class.multiple(3,3).weights.should == [1.0/9, 2.0/9, 3.0/9, 2.0/9, 1.0/9]
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should combine weights in the 3x5 case" do
|
58
|
+
described_class.multiple(3,5).weights.should == [1.0/15, 2.0/15, 0.2, 0.2, 0.2, 2.0/15, 1.0/15]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::NaiveForecast do
|
4
|
+
before :each do
|
5
|
+
@time_series = [1,2,3]
|
6
|
+
end
|
7
|
+
|
8
|
+
it "provides forecasts such that F_t+1 = Y_t" do
|
9
|
+
described_class.new(@time_series).one_step_ahead_forecasts.should == [nil, 1, 2]
|
10
|
+
end
|
11
|
+
|
12
|
+
it "returns errors between one step ahead forecasts and observed values" do
|
13
|
+
described_class.new(@time_series).errors.should == [nil, -1, -1]
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns a prediction for the next value in the series" do
|
17
|
+
described_class.new(@time_series).predict.should == 3
|
18
|
+
end
|
19
|
+
|
20
|
+
it "returns n predictions for the next values, all the same" do
|
21
|
+
described_class.new(@time_series).predict(4).should == [3,3,3,3]
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns 1 as Thiel's U Statistic" do
|
25
|
+
described_class.new(@time_series).u_statistic.should == 1
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::SeasonalComponents do
|
4
|
+
it "returns seasonal averages for a period" do
|
5
|
+
described_class.new(4, [1,2,1,3,2,4,1,9]).seasonal_averages.
|
6
|
+
should == [1.5, 3, 1, 6]
|
7
|
+
end
|
8
|
+
|
9
|
+
it "returns seasonal averages for a period, with ragged data" do
|
10
|
+
described_class.new(4, [1,2,1,3,2,4,1,9, 3]).seasonal_averages.
|
11
|
+
should == [2, 3, 1, 6]
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns seasonal weights" do
|
15
|
+
described_class.new(4, [1,2,1,3,2,4,1,9,3]).seasonal_factors.
|
16
|
+
should == [-1.0, 0.0, -2.0, 3.0]
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe TeaLeaves::SingleExponentialSmoothingForecast do
|
4
|
+
before :each do
|
5
|
+
@time_series = [1,2,3]
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should generate one step ahead forecasts" do
|
9
|
+
described_class.new(@time_series, 0.7).one_step_ahead_forecasts.
|
10
|
+
should == [nil, 0.7 * 1 + 0.3 * 1, 0.7 * 2 + 0.3 * 1]
|
11
|
+
end
|
12
|
+
|
13
|
+
it "has one step ahead errors" do
|
14
|
+
described_class.new(@time_series, 0.7).errors.should == [nil, -1, -1.3]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has a predicition for the next period" do
|
18
|
+
described_class.new(@time_series, 0.7).predict.should == 0.7 * 3 + 0.3 * 1.7
|
19
|
+
end
|
20
|
+
|
21
|
+
it "has a predicition for the n next periods" do
|
22
|
+
described_class.new(@time_series, 0.7).predict(2).should == [0.7 * 3 + 0.3 * 1.7] * 2
|
23
|
+
end
|
24
|
+
|
25
|
+
it "calculates mean squared error" do
|
26
|
+
described_class.new(@time_series, 0.7).mean_squared_error.should be_within(0.001).of(1.345)
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'tealeaves'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
end
|
data/tealeaves.gemspec
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "tealeaves"
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Roland Swingler"]
|
12
|
+
s.date = "2012-02-21"
|
13
|
+
s.description = "Exponential smoothing based forecasting methods for time series data"
|
14
|
+
s.email = "roland.swingler@gmail.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
".travis.yml",
|
23
|
+
"Gemfile",
|
24
|
+
"LICENSE",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/tealeaves.rb",
|
29
|
+
"lib/tealeaves/brute_force_optimization.rb",
|
30
|
+
"lib/tealeaves/exponential_smoothing_forecast.rb",
|
31
|
+
"lib/tealeaves/forecast.rb",
|
32
|
+
"lib/tealeaves/moving_average.rb",
|
33
|
+
"lib/tealeaves/naive_forecast.rb",
|
34
|
+
"lib/tealeaves/seasonal_components.rb",
|
35
|
+
"lib/tealeaves/single_exponential_smoothing_forecast.rb",
|
36
|
+
"spec/brute_force_optimization_spec.rb",
|
37
|
+
"spec/exponential_smoothing_forecast_spec.rb",
|
38
|
+
"spec/moving_average_spec.rb",
|
39
|
+
"spec/naive_forecast_spec.rb",
|
40
|
+
"spec/seasonal_components_spec.rb",
|
41
|
+
"spec/single_exponential_smoothing_forecast_spec.rb",
|
42
|
+
"spec/spec.opts",
|
43
|
+
"spec/spec_helper.rb",
|
44
|
+
"tealeaves.gemspec"
|
45
|
+
]
|
46
|
+
s.homepage = "http://github.com/knaveofdiamonds/tealeaves"
|
47
|
+
s.licenses = ["MIT"]
|
48
|
+
s.require_paths = ["lib"]
|
49
|
+
s.rubygems_version = "1.8.10"
|
50
|
+
s.summary = "Simple Forecasting Methods in Ruby"
|
51
|
+
|
52
|
+
if s.respond_to? :specification_version then
|
53
|
+
s.specification_version = 3
|
54
|
+
|
55
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
56
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
57
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
|
58
|
+
s.add_development_dependency(%q<yard>, ["~> 0.7"])
|
59
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
60
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
61
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
62
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
63
|
+
else
|
64
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
65
|
+
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
66
|
+
s.add_dependency(%q<yard>, ["~> 0.7"])
|
67
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
70
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
74
|
+
s.add_dependency(%q<rspec>, ["~> 2.8.0"])
|
75
|
+
s.add_dependency(%q<yard>, ["~> 0.7"])
|
76
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
77
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
78
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
79
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
metadata
ADDED
@@ -0,0 +1,195 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tealeaves
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Roland Swingler
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-02-21 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
22
|
+
none: false
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
hash: 3
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
name: rake
|
31
|
+
prerelease: false
|
32
|
+
type: :development
|
33
|
+
requirement: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
hash: 47
|
41
|
+
segments:
|
42
|
+
- 2
|
43
|
+
- 8
|
44
|
+
- 0
|
45
|
+
version: 2.8.0
|
46
|
+
name: rspec
|
47
|
+
prerelease: false
|
48
|
+
type: :development
|
49
|
+
requirement: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ~>
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 5
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
- 7
|
60
|
+
version: "0.7"
|
61
|
+
name: yard
|
62
|
+
prerelease: false
|
63
|
+
type: :development
|
64
|
+
requirement: *id003
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 31
|
72
|
+
segments:
|
73
|
+
- 3
|
74
|
+
- 12
|
75
|
+
version: "3.12"
|
76
|
+
name: rdoc
|
77
|
+
prerelease: false
|
78
|
+
type: :development
|
79
|
+
requirement: *id004
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ~>
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 23
|
87
|
+
segments:
|
88
|
+
- 1
|
89
|
+
- 0
|
90
|
+
- 0
|
91
|
+
version: 1.0.0
|
92
|
+
name: bundler
|
93
|
+
prerelease: false
|
94
|
+
type: :development
|
95
|
+
requirement: *id005
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
version_requirements: &id006 !ruby/object:Gem::Requirement
|
98
|
+
none: false
|
99
|
+
requirements:
|
100
|
+
- - ~>
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
hash: 49
|
103
|
+
segments:
|
104
|
+
- 1
|
105
|
+
- 8
|
106
|
+
- 3
|
107
|
+
version: 1.8.3
|
108
|
+
name: jeweler
|
109
|
+
prerelease: false
|
110
|
+
type: :development
|
111
|
+
requirement: *id006
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
version_requirements: &id007 !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
hash: 3
|
119
|
+
segments:
|
120
|
+
- 0
|
121
|
+
version: "0"
|
122
|
+
name: rcov
|
123
|
+
prerelease: false
|
124
|
+
type: :development
|
125
|
+
requirement: *id007
|
126
|
+
description: Exponential smoothing based forecasting methods for time series data
|
127
|
+
email: roland.swingler@gmail.com
|
128
|
+
executables: []
|
129
|
+
|
130
|
+
extensions: []
|
131
|
+
|
132
|
+
extra_rdoc_files:
|
133
|
+
- LICENSE
|
134
|
+
- README.rdoc
|
135
|
+
files:
|
136
|
+
- .document
|
137
|
+
- .rspec
|
138
|
+
- .travis.yml
|
139
|
+
- Gemfile
|
140
|
+
- LICENSE
|
141
|
+
- README.rdoc
|
142
|
+
- Rakefile
|
143
|
+
- VERSION
|
144
|
+
- lib/tealeaves.rb
|
145
|
+
- lib/tealeaves/brute_force_optimization.rb
|
146
|
+
- lib/tealeaves/exponential_smoothing_forecast.rb
|
147
|
+
- lib/tealeaves/forecast.rb
|
148
|
+
- lib/tealeaves/moving_average.rb
|
149
|
+
- lib/tealeaves/naive_forecast.rb
|
150
|
+
- lib/tealeaves/seasonal_components.rb
|
151
|
+
- lib/tealeaves/single_exponential_smoothing_forecast.rb
|
152
|
+
- spec/brute_force_optimization_spec.rb
|
153
|
+
- spec/exponential_smoothing_forecast_spec.rb
|
154
|
+
- spec/moving_average_spec.rb
|
155
|
+
- spec/naive_forecast_spec.rb
|
156
|
+
- spec/seasonal_components_spec.rb
|
157
|
+
- spec/single_exponential_smoothing_forecast_spec.rb
|
158
|
+
- spec/spec.opts
|
159
|
+
- spec/spec_helper.rb
|
160
|
+
- tealeaves.gemspec
|
161
|
+
homepage: http://github.com/knaveofdiamonds/tealeaves
|
162
|
+
licenses:
|
163
|
+
- MIT
|
164
|
+
post_install_message:
|
165
|
+
rdoc_options: []
|
166
|
+
|
167
|
+
require_paths:
|
168
|
+
- lib
|
169
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
170
|
+
none: false
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
hash: 3
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
version: "0"
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
179
|
+
none: false
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
hash: 3
|
184
|
+
segments:
|
185
|
+
- 0
|
186
|
+
version: "0"
|
187
|
+
requirements: []
|
188
|
+
|
189
|
+
rubyforge_project:
|
190
|
+
rubygems_version: 1.8.10
|
191
|
+
signing_key:
|
192
|
+
specification_version: 3
|
193
|
+
summary: Simple Forecasting Methods in Ruby
|
194
|
+
test_files: []
|
195
|
+
|