├── .document ├── .gitignore ├── .travis.yml ├── Gemfile ├── History.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── statsample-timeseries.rb └── statsample-timeseries │ ├── arima.rb │ ├── arima │ ├── kalman.rb │ └── likelihood.rb │ ├── daru_monkeys.rb │ ├── timeseries │ └── pacf.rb │ ├── utility.rb │ └── version.rb ├── statsample-timeseries.gemspec └── test ├── fixtures └── stock_data.csv ├── helper.rb ├── test_acf.rb ├── test_arima_ks.rb ├── test_arima_simulators.rb ├── test_matrix.rb ├── test_pacf.rb └── test_wald.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rcov generated 2 | coverage 3 | coverage.data 4 | 5 | # rdoc generated 6 | rdoc 7 | 8 | # yard generated 9 | doc 10 | .yardoc 11 | 12 | # bundler 13 | .bundle 14 | 15 | # jeweler generated 16 | pkg 17 | 18 | Gemfile.lock 19 | *.gem 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - 2.0.0 5 | - 2.1.0 6 | - 2.2.1 7 | 8 | script: "bundle exec rake test" 9 | 10 | install: 11 | - gem install bundler 12 | - bundle install 13 | 14 | before_install: 15 | - sudo apt-get update -qq 16 | - sudo apt-get install -y libgsl0-dev 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | * Statsample-timeseries now dependents on the latest statsample (2) and uses daru for data containers. 4 | * Removed redundant dependency on cucumber and migrated relevant cucumber tests to Test::Unit. -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This version of Statsample-Timeseries is licensed under the BSD 2-clause license. 2 | 3 | * http://sciruby.com 4 | * http://github.com/sciruby/sciruby/wiki/License 5 | 6 | You *must* read the Contributor Agreement before contributing code to the SciRuby Project. This is available online: 7 | 8 | * http://github.com/sciruby/sciruby/wiki/Contributor-Agreement 9 | 10 | ----- 11 | 12 | Copyright (c) 2013, Ankur Goel and Ruby Science Foundation 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 16 | 17 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 18 | 19 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statsample-timeseries 2 | 3 | [![Build Status](https://travis-ci.org/SciRuby/statsample-timeseries.png)](https://travis-ci.org/SciRuby/statsample-timeseries) 4 | 5 | Statsample-Timeseries is an extension to [Statsample](https://github.com/sciruby/statsample), a suite for advanced statistics with Ruby. 6 | 7 | ## Description 8 | 9 | Statsample-Timeseries is extension of Statsample, and incorporates helpful time series functions, estimation techniques, and modules, such as: 10 | 11 | * ACF 12 | * PACF 13 | * ARIMA 14 | * Kalman Filter 15 | * Log Likelihood 16 | * Autocovariances 17 | * Moving Averages 18 | 19 | Statsample-Timeseries is part of the [SciRuby project](http://sciruby.com). 20 | 21 | ## Dependency 22 | 23 | Please install [rb-gsl]() which is a Ruby wrapper over GNU Scientific Library. 24 | It enables us to use various minimization techniques during estimations. 25 | 26 | ## Installation 27 | 28 | `gem install statsample-timeseries` 29 | 30 | ## Usage 31 | 32 | To use the library: 33 | 34 | `require 'statsample-timeseries'` 35 | 36 | See [Ankur's blog posts](http://ankurgoel.com) for explanations and examples. 37 | 38 | ## Documentation 39 | 40 | The API doc is [online](http://rubygems.org/gems/statsample-timeseries). For more code examples see also the test files in the source tree. 41 | 42 | ## Contributing 43 | 44 | * Fork the project. 45 | * Create your feature branch 46 | * Add/Modify code. 47 | * Write equivalent documentation and *tests*. 48 | * Run `rake test` to verify that all tests pass. 49 | * Push your branch. 50 | * Pull request. :) 51 | 52 | ## Project home page 53 | 54 | For information on the source tree, documentation, issues and how to contribute, see [http://github.com/SciRuby/statsample-timeseries](git@github.com:SciRuby/statsample-timeseries.git). 55 | 56 | ## Copyright 57 | 58 | Copyright (c) 2013 Ankur Goel and the Ruby Science Foundation. See LICENSE.txt for further details. 59 | 60 | Statsample is (c) 2009-2013 Claudio Bustos and the Ruby Science Foundation. 61 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'rake' 3 | require 'bundler/gem_tasks' 4 | 5 | # Setup the necessary gems, specified in the gemspec. 6 | require 'bundler' 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts "Run `bundle install` to install missing gems" 12 | exit e.status_code 13 | end 14 | 15 | desc "Open IRB with statsample-timeseries loaded." 16 | task :console do 17 | require 'irb' 18 | require 'irb/completion' 19 | $:.unshift File.expand_path("../lib", __FILE__) 20 | require 'statsample-timeseries' 21 | ARGV.clear 22 | IRB.start 23 | end 24 | 25 | require 'rake/testtask' 26 | Rake::TestTask.new(:test) do |test| 27 | test.libs << 'lib' << 'test' 28 | test.pattern = 'test/**/test_*.rb' 29 | test.verbose = true 30 | end 31 | 32 | task :default => :test 33 | 34 | require 'rdoc/task' 35 | Rake::RDocTask.new do |rdoc| 36 | version = Statsample::TimeSeries::VERSION 37 | 38 | rdoc.rdoc_dir = 'rdoc' 39 | rdoc.title = "statsample-timeseries #{version}" 40 | rdoc.rdoc_files.include('README*') 41 | rdoc.rdoc_files.include('lib/**/*.rb') 42 | end 43 | -------------------------------------------------------------------------------- /lib/statsample-timeseries.rb: -------------------------------------------------------------------------------- 1 | require 'statsample' 2 | require 'statsample-timeseries/version' 3 | 4 | require_relative 'statsample-timeseries/daru_monkeys.rb' 5 | require_relative 'statsample-timeseries/arima.rb' 6 | require_relative 'statsample-timeseries/arima/kalman' 7 | require_relative 'statsample-timeseries/arima/likelihood' 8 | require_relative 'statsample-timeseries/utility.rb' 9 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/arima.rb: -------------------------------------------------------------------------------- 1 | require 'statsample-timeseries/arima/kalman' 2 | require 'statsample-timeseries/arima/likelihood' 3 | module Statsample 4 | module TimeSeries 5 | 6 | def self.arima 7 | #not passing (ds,p,i,q) elements for now 8 | #will do that once #arima is ready for all modelling 9 | Statsample::TimeSeries::ARIMA.new 10 | end 11 | 12 | class ARIMA 13 | 14 | #= Kalman filter on ARIMA model 15 | # == Arguments 16 | # 17 | #* *ts*: timeseries object 18 | #* *p*: AR order 19 | #* *i*: Integerated part order 20 | #* *q*: MA order 21 | # 22 | # == Usage 23 | # 24 | # ts = (1..100).map { rand }.to_ts 25 | # k_obj = Statsample::TimeSeries::ARIMA.ks(ts, 2, 1, 1) 26 | # k_obj.ar 27 | # #=> AR's phi coefficients 28 | # k_obj.ma 29 | # #=> MA's theta coefficients 30 | # 31 | #== Returns 32 | # Kalman filter object 33 | def self.ks(ts, p, i, q) 34 | #prototype 35 | if i > 0 36 | ts = ts.diff(i).reject { |x| x.nil? } 37 | end 38 | if Statsample.has_gsl? 39 | filter = Arima::KalmanFilter.new(ts, p, i, q) 40 | filter 41 | else 42 | raise("GSL not available. Install GSL and rb-gsl first") 43 | end 44 | end 45 | 46 | def ar(p) 47 | #AutoRegressive part of model 48 | #http://en.wikipedia.org/wiki/Autoregressive_model#Definition 49 | #For finding parameters(to fit), we will use either Yule-walker 50 | #or Burg's algorithm(more efficient) 51 | raise NotImplementedError 52 | end 53 | 54 | # Converts a linear array into a Daru vector 55 | #== Parameters 56 | # 57 | #* *arr*: Array which has to be converted to Daru vector 58 | def create_vector(arr) 59 | Daru::Vector.new(arr) 60 | end 61 | 62 | #=Yule Walker 63 | #Performs yule walker estimation on given timeseries, observations and order 64 | #==Parameters 65 | # 66 | #* *ts*: timeseries object 67 | #* *n* : number of observations 68 | #* *k* : order 69 | # 70 | #==Returns 71 | #phi and sigma vectors 72 | def yule_walker(ts, n, k) 73 | phi, sigma = Statsample::TimeSeries::Pacf.yule_walker(ts, k) 74 | return phi, sigma 75 | #return ar_sim(n, phi, sigma) 76 | end 77 | 78 | #=Levinson Durbin estimation 79 | #Performs levinson durbin estimation on given timeseries, observations and order 80 | #==Parameters 81 | # 82 | #* *ts*: timeseries object 83 | #* *n* : number of observations 84 | #* *k* : autoregressive order 85 | # 86 | #==Returns 87 | #phi and sigma vectors 88 | def levinson_durbin(ts, n, k) 89 | intermediate = Pacf::Pacf.levinson_durbin(ts, k) 90 | phi, sigma = intermediate[1], intermediate[0] 91 | return phi, sigma 92 | end 93 | 94 | #=Autoregressive Simulator 95 | #Simulates an autoregressive AR(p) model with specified number of 96 | #observations(n), with phi number of values for order p and sigma. 97 | # 98 | # == Analysis 99 | # 100 | # http://ankurgoel.com/blog/2013/07/20/ar-ma-arma-acf-pacf-visualizations/ 101 | # 102 | # == Parameters 103 | #* *n*: integer, number of observations 104 | #* *phi* :array of phi values, e.g: [0.35, 0.213] for p = 2 105 | #* *sigma*: float, sigma value for error generalization 106 | # 107 | # == Usage 108 | # 109 | # ar = ARIMA.new 110 | # ar.ar_sim(1500, [0.3, 0.9], 0.12) 111 | # # => AR(2) autoregressive series of 1500 values 112 | # 113 | #==Returns 114 | #Array of generated autoregressive series against attributes 115 | def ar_sim(n, phi, sigma) 116 | #using random number generator for inclusion of white noise 117 | err_nor = Distribution::Normal.rng(0, sigma) 118 | #creating buffer with 10 random values 119 | buffer = Array.new(10, err_nor.call()) 120 | 121 | x = buffer + Array.new(n, 0) 122 | 123 | #For now "phi" are the known model parameters 124 | #later we will obtain it by Yule-walker/Burg 125 | 126 | #instead of starting from 0, start from 11 127 | #and later take away buffer values for failsafe 128 | 11.upto(n+11) do |i| 129 | if i <= phi.size 130 | #dependent on previous accumulation of x 131 | backshifts = x[0...i].reverse 132 | else 133 | #dependent on number of phi size/order 134 | backshifts = x[(i - phi.size)...i].reverse 135 | end 136 | parameters = phi[0...backshifts.size] 137 | summation = (backshifts.map.with_index { |e,i| e*parameters[i] }).inject(:+) 138 | x[i] = summation + err_nor.call() 139 | end 140 | x - buffer 141 | end 142 | 143 | #=Moving Average Simulator 144 | #Simulates a moving average model with specified number of 145 | #observations(n), with theta values for order k and sigma 146 | # 147 | # == Parameters 148 | #* *n*: integer, number of observations 149 | #* *theta*: array of floats, e.g: [0.23, 0.732], must be < 1 150 | #* *sigma*: float, sigma value for whitenoise error 151 | # 152 | # == Usage 153 | # ar = ARIMA.new 154 | # ar.ma_sim(1500, [0.23, 0.732], 0.27) 155 | # 156 | # == Returns 157 | # Array of generated MA(q) model 158 | # 159 | # == Notes 160 | # n is number of observations (eg: 1000). theta are the model parameters 161 | # containting q values. q is the order of MA 162 | def ma_sim(n, theta, sigma) 163 | q = theta.size 164 | mean = theta.inject(:+) / q 165 | whitenoise_gen = Distribution::Normal.rng(0, sigma) 166 | x = Array.new(n, 0) 167 | noise_arr = (n+1).times.map { whitenoise_gen.call() } 168 | 169 | 1.upto(n) do |i| 170 | #take care that noise vector doesn't try to index -ve value: 171 | if i <= q 172 | noises = noise_arr[0..i].reverse 173 | else 174 | noises = noise_arr[(i-q)..i].reverse 175 | end 176 | weights = [1] + theta[0...noises.size - 1] 177 | 178 | summation = weights.map.with_index { |e,i| e*noises[i] }.inject(:+) 179 | x[i] = mean + summation 180 | end 181 | 182 | x 183 | end 184 | 185 | #=ARMA(Autoregressive and Moving Average) Simulator 186 | # ARMA is represented by: 187 | # http://upload.wikimedia.org/math/2/e/d/2ed0485927b4370ae288f1bc1fe2fc8b.png 188 | # This simulates the ARMA model against p, q and sigma. 189 | # If p = 0, then model is pure MA(q), 190 | # If q = 0, then model is pure AR(p), 191 | # otherwise, model is ARMA(p, q) represented by above. 192 | # 193 | # == Detailed analysis: 194 | # 195 | # http://ankurgoel.com/blog/2013/07/20/ar-ma-arma-acf-pacf-visualizations/ 196 | # 197 | # == Parameters 198 | # 199 | #* *n*: integer, number of observations 200 | #* *p*: array, contains p number of phi values for AR(p) process 201 | #* *q*: array, contains q number of theta values for MA(q) process 202 | #* *sigma*: float, sigma value for whitenoise error generation 203 | # 204 | # == Usage 205 | # 206 | # ar = ARIMA.new 207 | # ar.arma_sim(1500, [0.3, 0.272], [0.8, 0.317], 0.92) 208 | # 209 | # == Returns 210 | # 211 | # array of generated ARMA model values 212 | def arma_sim(n, p, q, sigma) 213 | #represented by : 214 | #http://upload.wikimedia.org/math/2/e/d/2ed0485927b4370ae288f1bc1fe2fc8b.png 215 | whitenoise_gen = Distribution::Normal.rng(0, sigma) 216 | noise_arr = (n+11).times.map { whitenoise_gen.call() } 217 | 218 | buffer = Array.new(10, whitenoise_gen.call()) 219 | x = buffer + Array.new(n, 0) 220 | 221 | 11.upto(n+11) do |i| 222 | if i <= p.size 223 | backshifts = x[0...i].reverse 224 | else 225 | backshifts = x[(i - p.size)...i].reverse 226 | end 227 | parameters = p[0...backshifts.size] 228 | ar_summation = backshifts.map.with_index { |e,i| e*parameters[i] }.inject(:+) 229 | 230 | if i <= q.size 231 | noises = noise_arr[0..i].reverse 232 | else 233 | noises = noise_arr[(i-q.size)..i].reverse 234 | end 235 | weights = [1] + q[0...noises.size - 1] 236 | ma_summation = (weights.map.with_index { |e,i| e*noises[i] }).inject(:+) 237 | x[i] = ar_summation + ma_summation 238 | end 239 | x - buffer 240 | end 241 | 242 | # Hannan-Rissanen for ARMA fit 243 | def self.hannan(ts, p, q, k) 244 | raise NotImplementedError 245 | end 246 | end 247 | end 248 | end 249 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/arima/kalman.rb: -------------------------------------------------------------------------------- 1 | require 'statsample-timeseries/arima/likelihood' 2 | module Statsample 3 | module TimeSeries 4 | module Arima 5 | 6 | class KalmanFilter 7 | include Statsample::TimeSeries 8 | include GSL::MultiMin if Statsample.has_gsl? 9 | 10 | #timeseries object 11 | attr_writer :ts 12 | #Autoregressive order 13 | attr_accessor :p 14 | #Integerated part order 15 | attr_accessor :i 16 | #Moving average order 17 | attr_accessor :q 18 | 19 | # Autoregressive coefficients 20 | attr_reader :ar 21 | # Moving average coefficients 22 | attr_reader :ma 23 | 24 | #Creates a new KalmanFilter object and computes the likelihood 25 | def initialize(ts=[], p=0, i=0, q=0) 26 | @ts = ts.to_a 27 | @p = p 28 | @i = i 29 | @q = q 30 | ks #call the filter 31 | end 32 | 33 | def ts 34 | Daru::Vector.new(@ts) 35 | end 36 | 37 | def to_s 38 | sprintf("ARIMA model(p = %d, i = %d, q = %d) on series(%d elements) - [%s]", 39 | @p, @i, @q, @ts.size, @ts.to_a.join(',')) 40 | end 41 | 42 | # = Kalman Filter 43 | # Function which minimizes KalmanFilter.ll iteratively for initial parameters 44 | # == Usage 45 | # @s = [-1.16025577,0.64758021,0.77158601,0.14989543,2.31358162,3.49213868,1.14826956,0.58169457,-0.30813868,-0.34741084,-1.41175595,0.06040081, -0.78230232,0.86734837,0.95015787,-0.49781397,0.53247330,1.56495187,0.30936619,0.09750217,1.09698829,-0.81315490,-0.79425607,-0.64568547,-1.06460320,1.24647894,0.66695937,1.50284551,1.17631218,1.64082872,1.61462736,0.06443761,-0.17583741,0.83918339,0.46610988,-0.54915270,-0.56417108,-1.27696654,0.89460084,1.49970338,0.24520493,0.26249138,-1.33744834,-0.57725961,1.55819543,1.62143157,0.44421891,-0.74000084 ,0.57866347,3.51189333,2.39135077,1.73046244,1.81783890,0.21454040,0.43520890,-1.42443856,-2.72124685,-2.51313877,-1.20243091,-1.44268002 ,-0.16777305,0.05780661,2.03533992,0.39187242,0.54987983,0.57865693,-0.96592469,-0.93278473,-0.75962671,-0.63216906,1.06776183, 0.17476059 ,0.06635860,0.94906227,2.44498583,-1.04990407,-0.88440073,-1.99838258,-1.12955558,-0.62654882,-1.36589161,-2.67456821,-0.97187696, -0.84431782 ,-0.10051809,0.54239549,1.34622861,1.25598105,0.19707759,3.29286114,3.52423499,1.69146333,-0.10150024,0.45222903,-0.01730516, -0.49828727, -1.18484684,-1.09531773,-1.17190808,0.30207662].to_ts 46 | # @kf=Statsample::TimeSeries::ARIMA.ks(@s,1,0,0) 47 | # #=> ks is implictly called in above operation 48 | # @kf.ar 49 | # #=> AR coefficients 50 | def ks 51 | initial = Array.new((@p+@q), 0.0) 52 | 53 | my_f = Proc.new{ |x, params| 54 | #In rb-gsl, params remain idle, x is varied upon 55 | #In R code, initial parameters varied in each iteration 56 | #my_func.set_params([(1..100).to_a.to_ts, p_value, q_value]) 57 | timeseries = params[0] 58 | p,q = params[1], params[2] 59 | params = x 60 | #puts x 61 | -Arima::KF::LogLikelihood.new(x.to_a, timeseries, p, q).log_likelihood 62 | #KalmanFilter.ll(x.to_a, timeseries, p, q) 63 | } 64 | np = @p + @q 65 | my_func = Function.alloc(my_f, np) 66 | my_func.set_params([@ts, @p, @q]) 67 | x = GSL::Vector.alloc(initial) 68 | ss = GSL::Vector.alloc(np) 69 | ss.set_all(0.1) 70 | 71 | minimizer = FMinimizer.alloc("nmsimplex", np) 72 | minimizer.set(my_func, x, ss) 73 | status = GSL::CONTINUE 74 | iter = 0 75 | while status == GSL::CONTINUE && iter < 100 76 | iter += 1 77 | begin 78 | status = minimizer.iterate 79 | status = minimizer.test_size(1e-2) 80 | x = minimizer.x 81 | rescue 82 | break 83 | end 84 | end 85 | @ar = (p > 0) ? x.to_a[0...p] : [] 86 | @ma = (q > 0) ? x.to_a[p...(p+q)] : [] 87 | x.to_a 88 | end 89 | 90 | 91 | #=Log Likelihood 92 | #Computes Log likelihood on given parameters, ARMA order and timeseries 93 | #==params 94 | #* *params*: array of floats, contains phi/theta parameters 95 | #* *timeseries*: timeseries object 96 | #* *p*: integer, AR(p) order 97 | #* *q*: integer, MA(q) order 98 | #==Returns 99 | #LogLikelihood object 100 | #==Usage 101 | # s = (1..100).map { rand }.to_ts 102 | # p, q = 1, 0 103 | # ll = KalmanFilter.log_likelihood([0.2], s, p, q) 104 | # ll.log_likelihood 105 | # #=> -22.66 106 | # ll.sigma 107 | # #=> 0.232 108 | def self.log_likelihood(params, timeseries, p, q) 109 | Arima::KF::LogLikelihood.new(params, timeseries, p, q) 110 | end 111 | 112 | 113 | def self.T(r, k, p) 114 | #=T 115 | #The coefficient matrix for the state vector in state equation 116 | # It's dimensions is r+k x r+k 117 | #==Parameters 118 | #* *r*: integer, r is max(p, q+1), where p and q are orders of AR and MA respectively 119 | #* *k*: integer, number of exogeneous variables in ARMA model 120 | #* *q*: integer, The AR coefficient of ARMA model 121 | 122 | #==References Statsmodels tsa, Durbin and Koopman Section 4.7 123 | raise NotImplementedError 124 | end 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/arima/likelihood.rb: -------------------------------------------------------------------------------- 1 | module Statsample 2 | module TimeSeries 3 | module Arima 4 | module KF 5 | class LogLikelihood 6 | 7 | #Gives log likelihood value of an ARMA(p, q) process on given parameters 8 | attr_reader :log_likelihood 9 | 10 | #Gives sigma value of an ARMA(p,q) process on given parameters 11 | attr_reader :sigma 12 | 13 | #Gives AIC(Akaike Information Criterion) 14 | #https://www.scss.tcd.ie/Rozenn.Dahyot/ST7005/13AICBIC.pdf 15 | attr_reader :aic 16 | 17 | def initialize(params, timeseries, p, q) 18 | @params = params 19 | @timeseries = timeseries.to_a 20 | @p = p 21 | @q = q 22 | ll 23 | end 24 | 25 | #===Log likelihood link function. 26 | #iteratively minimized by simplex algorithm via KalmanFilter.ks 27 | #Not meant to be used directly. Will make it private later. 28 | def ll 29 | params, timeseries = @params, @timeseries 30 | p, q = @p, @q 31 | 32 | phi = [] 33 | theta = [] 34 | phi = params[0...p] if p > 0 35 | theta = params[(p)...(p + q)] if q > 0 36 | 37 | [phi, theta].each do |v| 38 | if v.size>0 and v.map(&:abs).inject(:+) > 1 39 | return 40 | end 41 | end 42 | 43 | m = [p, q].max 44 | h = Matrix.column_vector(Array.new(m,0)) 45 | m.times do |i| 46 | h[i,0] = phi[i] if i< p 47 | h[i,0] = h[i,0] + theta[i] if i < q 48 | end 49 | 50 | t = Matrix.zero(m) 51 | #set_column is available in utility.rb 52 | t = t.set_column(0, phi) 53 | if m > 1 54 | t[0...(m-1), 1...m] = Matrix.I(m-1) 55 | #chances of extra constant 0 values as unbalanced column, so: 56 | t = Matrix.columns(t.column_vectors) 57 | end 58 | 59 | g = Matrix[[1]] 60 | a_t = Matrix.column_vector(Array.new(m,0)) 61 | n = timeseries.size 62 | z = Matrix.row_vector(Array.new(m,0)) 63 | z[0,0] = 1 64 | p_t = Matrix.I(m) 65 | v_t, f_t = Array.new(n,0), Array.new(n, 0) 66 | 67 | n.times do |i| 68 | v_t[i] = (z * a_t).map { |x| timeseries[i] - x }[0,0] 69 | f_t[i] = (z * p_t * (z.transpose)).map { |x| x + 1 }[0,0] 70 | 71 | k_t = ((t * p_t * z.transpose) + h).map { |x| x.quo f_t[i] } 72 | 73 | a_t = (t * a_t) + (k_t * v_t[i]) 74 | l_t = t - k_t * z 75 | j_t = h - k_t 76 | 77 | p_t = (t * p_t * (l_t.transpose)) + (h * (j_t.transpose)) 78 | end 79 | 80 | pot = v_t.map(&:square).zip(f_t).map { |x,y| x / y}.inject(:+) 81 | sigma_2 = pot.to_f / n.to_f 82 | 83 | f_t_log_sum = f_t.map { |x| Math.log(x) }.inject(:+) 84 | @log_likelihood = -0.5 * (n*Math.log(2*Math::PI) + n*Math.log(sigma_2) + f_t_log_sum + n) 85 | 86 | @sigma = sigma_2 87 | @aic = -(2 * @log_likelihood - 2*(p+q+1)) 88 | #puts ("ll = #{-ll}") 89 | return @log_likelihood 90 | end 91 | 92 | def to_s 93 | sprintf("LogLikelihood(p = %d, q = %d) on params: [%s]", 94 | @p, @q, @params.join(', ')) 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/daru_monkeys.rb: -------------------------------------------------------------------------------- 1 | require 'statsample-timeseries/timeseries/pacf' 2 | module Statsample::TimeSeriesShorthands 3 | # Creates a new Statsample::TimeSeries object 4 | # Argument should be equal to TimeSeries.new 5 | def to_time_series(*args) 6 | Daru::Vector.new(self, *args) 7 | end 8 | 9 | alias :to_ts :to_time_series 10 | end 11 | 12 | class Array 13 | include Statsample::TimeSeriesShorthands 14 | end 15 | 16 | module Daru 17 | class Vector 18 | include Statsample::TimeSeries::Pacf 19 | 20 | # = Partial Autocorrelation 21 | # Generates partial autocorrelation series for a timeseries 22 | # 23 | # == Arguments 24 | # 25 | #* *max_lags*: integer, optional - provide number of lags 26 | #* *method*: string. Default: 'yw'. 27 | # * *yw*: For yule-walker algorithm unbiased approach 28 | # * *mle*: For Maximum likelihood algorithm approach 29 | # * *ld*: Forr Levinson-Durbin recursive approach 30 | # 31 | # == Returns 32 | # 33 | # array of pacf 34 | def pacf(max_lags = nil, method = :yw) 35 | helper = Statsample::TimeSeries::Pacf 36 | method = method.downcase.to_sym 37 | max_lags ||= (10 * Math.log10(size)).to_i 38 | if method == :yw or method == :mle 39 | helper.pacf_yw(self, max_lags, method.to_s) 40 | elsif method == :ld 41 | series = self.acvf 42 | helper.levinson_durbin(series, max_lags, true)[2] 43 | else 44 | raise "Method presents for pacf are 'yw', 'mle' or 'ld'" 45 | end 46 | end 47 | 48 | # == Autoregressive estimation 49 | # Generates AR(k) series for the calling timeseries by yule walker. 50 | # 51 | # == Parameters 52 | # 53 | #* *n*: integer, (default = 1500) number of observations for AR. 54 | #* *k*: integer, (default = 1) order of AR process. 55 | # 56 | # == Returns 57 | # 58 | # Array constituting estimated AR series. 59 | def ar(n = 1500, k = 1) 60 | series = Statsample::TimeSeries.arima 61 | #series = Statsample::TimeSeries::ARIMA.new 62 | series.yule_walker(self, n, k) 63 | end 64 | end 65 | end 66 | 67 | module Statsample 68 | module TimeSeries 69 | 70 | # Deprecated. Use Daru::Vector. 71 | class Series < Daru::Vector 72 | def initialize *args, &block 73 | $stderr.puts "This class has been deprecated. Use Daru::Vector directly." 74 | super(*args, &block) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/timeseries/pacf.rb: -------------------------------------------------------------------------------- 1 | module Statsample 2 | module TimeSeries 3 | module Pacf 4 | class << self 5 | def pacf_yw(timeseries, max_lags, method = 'yw') 6 | #partial autocorrelation by yule walker equations. 7 | #Inspiration: StatsModels 8 | pacf = [1.0] 9 | arr = timeseries.to_a 10 | (1..max_lags).map do |i| 11 | pacf << yule_walker(arr, i, method)[0][-1] 12 | end 13 | pacf 14 | end 15 | 16 | 17 | #=Levinson-Durbin Algorithm 18 | #==Parameters 19 | #* *series*: timeseries, or a series of autocovariances 20 | #* *nlags*: integer(default: 10): largest lag to include in recursion or order of the AR process 21 | #* *is_acovf*: boolean(default: false): series is timeseries if it is false, else contains autocavariances 22 | # 23 | #==Returns: 24 | #* *sigma_v*: estimate of the error variance 25 | #* *arcoefs*: AR coefficients 26 | #* *pacf*: pacf function 27 | #* *sigma*: some function 28 | def levinson_durbin(series, nlags = 10, is_acovf = false) 29 | if is_acovf 30 | series = series.map(&:to_f) 31 | else 32 | #nlags = order(k) of AR in this case 33 | series = series.acvf.map(&:to_f)[0..nlags] 34 | end 35 | #phi = Array.new((nlags+1), 0.0) { Array.new(nlags+1, 0.0) } 36 | order = nlags 37 | phi = Matrix.zero(nlags + 1) 38 | sig = Array.new(nlags+1) 39 | 40 | #setting initial point for recursion: 41 | phi[1,1] = series[1]/series[0] 42 | #phi[1][1] = series[1]/series[0] 43 | sig[1] = series[0] - phi[1, 1] * series[1] 44 | 45 | 2.upto(order).each do |k| 46 | phi[k, k] = (series[k] - (Statsample::Vector.new(phi[1...k, k-1]) * series[1...k].reverse.to_ts).sum) / sig[k-1] 47 | #some serious refinement needed in above for matrix manipulation. Will do today 48 | 1.upto(k-1).each do |j| 49 | phi[j, k] = phi[j, k-1] - phi[k, k] * phi[k-j, k-1] 50 | end 51 | sig[k] = sig[k-1] * (1-phi[k, k] ** 2) 52 | 53 | end 54 | sigma_v = sig[-1] 55 | arcoefs_delta = phi.column(phi.column_size - 1) 56 | arcoefs = arcoefs_delta[1..arcoefs_delta.size] 57 | pacf = diag(phi) 58 | pacf[0] = 1.0 59 | return [sigma_v, arcoefs, pacf, sig, phi] 60 | end 61 | 62 | # Returns diagonal elements of matrices 63 | def diag(mat) 64 | return mat.each_with_index(:diagonal).map { |x, r, c| x } 65 | end 66 | 67 | 68 | #=Yule Walker Algorithm 69 | # 70 | # From the series, estimates AR(p)(autoregressive) parameter using 71 | # Yule-Waler equation. See - 72 | # http://en.wikipedia.org/wiki/Autoregressive_moving_average_model 73 | # 74 | # == Parameters 75 | # 76 | #* *ts*: timeseries 77 | #* *k*: order, default = 1 78 | #* *method*: can be 'yw' or 'mle'. If 'yw' then it is unbiased, denominator is (n - k) 79 | # 80 | # == Returns 81 | # 82 | #* *rho*: autoregressive coefficients 83 | #* *sigma*: sigma parameter 84 | def yule_walker(ts, k = 1, method='yw') 85 | n = ts.size 86 | mean = (ts.inject(:+) / n) 87 | ts = ts.map { |t| t - mean } 88 | 89 | if method == 'yw' 90 | #unbiased => denominator = (n - k) 91 | denom =->(k) { n - k } 92 | else 93 | #mle 94 | #denominator => (n) 95 | denom =->(k) { n } 96 | end 97 | r = Array.new(k + 1) { 0.0 } 98 | r[0] = ts.map { |x| x**2 }.inject(:+).to_f / denom.call(0).to_f 99 | 100 | 1.upto(k) do |l| 101 | r[l] = (ts[0...-l].zip(ts[l...n])).map do |x| 102 | x.inject(:*) 103 | end.inject(:+).to_f / denom.call(l).to_f 104 | end 105 | 106 | r_R = toeplitz(r[0...-1]) 107 | 108 | mat = Matrix.columns(r_R).inverse 109 | phi = solve_matrix(mat, r[1..r.size]) 110 | phi_vector = phi 111 | r_vector = r[1..-1] 112 | sigma = r[0] - (r_vector.map.with_index {|e,i| e*phi_vector[i] }).inject(:+) 113 | return [phi, sigma] 114 | end 115 | 116 | #=ToEplitz 117 | # 118 | # Generates teoeplitz matrix from an array 119 | # http://en.wikipedia.org/wiki/Toeplitz_matrix. 120 | # Toeplitz matrix are equal when they are stored in row & column major 121 | # 122 | # == Parameters 123 | # 124 | #* *arr*: array of integers; 125 | # 126 | # == Usage 127 | # 128 | # arr = [0,1,2,3] 129 | # Pacf.toeplitz(arr) 130 | # 131 | # #=> [[0, 1, 2, 3], 132 | # #=> [1, 0, 1, 2], 133 | # #=> [2, 1, 0, 1], 134 | # #=> [3, 2, 1, 0]] 135 | def toeplitz(arr) 136 | eplitz_matrix = Array.new(arr.size) { Array.new(arr.size) } 137 | 138 | 0.upto(arr.size - 1) do |i| 139 | j = 0 140 | index = i 141 | while i >= 0 do 142 | eplitz_matrix[index][j] = arr[i] 143 | j += 1 144 | i -= 1 145 | end 146 | i = index + 1; k = 1 147 | while i < arr.size do 148 | eplitz_matrix[index][j] = arr[k] 149 | i += 1; j += 1; k += 1 150 | end 151 | end 152 | eplitz_matrix 153 | end 154 | 155 | #=Solves matrix equations 156 | # 157 | # Solves for X in AX = B 158 | def solve_matrix(matrix, out_vector) 159 | solution_vector = Array.new(out_vector.size, 0) 160 | matrix = matrix.to_a 161 | k = 0 162 | matrix.each do |row| 163 | row.each_with_index do |element, i| 164 | solution_vector[k] += element * 1.0 * out_vector[i] 165 | end 166 | k += 1 167 | end 168 | solution_vector 169 | end 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/utility.rb: -------------------------------------------------------------------------------- 1 | class ::Matrix 2 | # == Squares of sum 3 | # 4 | # Does squares of sum in column order. 5 | # Necessary for computations in various processes 6 | def squares_of_sum 7 | (0...column_size).map do |j| 8 | self.column(j).sum**2 9 | end 10 | end 11 | 12 | # == Symmetric? 13 | # `symmetric?` is present in Ruby Matrix 1.9.3+, but not in 1.8.* 14 | # 15 | # == Returns 16 | # 17 | # bool 18 | def symmetric? 19 | return false unless square? 20 | 21 | (0...row_size).each do |i| 22 | 0.upto(i).each do |j| 23 | return false if self[i, j] != self[j, i] 24 | end 25 | end 26 | true 27 | end 28 | 29 | # == Cholesky decomposition 30 | # 31 | # Reference: http://en.wikipedia.org/wiki/Cholesky_decomposition 32 | # == Description 33 | # 34 | # Cholesky decomposition is reprsented by `M = L X L*`, where 35 | # M is the symmetric matrix and `L` is the lower half of cholesky matrix, 36 | # and `L*` is the conjugate form of `L`. 37 | # 38 | # == Returns 39 | # 40 | # Cholesky decomposition for a given matrix(if symmetric) 41 | # 42 | # == Utility 43 | # 44 | # Essential matrix function, requisite in kalman filter, least squares 45 | def cholesky 46 | raise ArgumentError, "Given matrix should be symmetric" unless symmetric? 47 | c = Matrix.zero(row_size) 48 | 0.upto(row_size - 1).each do |k| 49 | 0.upto(row_size - 1).each do |i| 50 | if i == k 51 | sum = (0..(k-1)).inject(0.0){ |sum, j| sum + c[k, j] ** 2 } 52 | value = Math.sqrt(self[k,k] - sum) 53 | c[k, k] = value 54 | elsif i > k 55 | sum = (0..(k-1)).inject(0.0){ |sum, j| sum + c[i, j] * c[k, j] } 56 | value = (self[k,i] - sum) / c[k, k] 57 | c[i, k] = value 58 | end 59 | end 60 | end 61 | c 62 | end 63 | 64 | #==Chain Product 65 | #Class method 66 | #Returns the chain product of two matrices 67 | #===Usage: 68 | #Let `a` be 4 * 3 matrix, 69 | #Let `b` be 3 * 3 matrix, 70 | #Let `c` be 3 * 1 matrix, 71 | #then `Matrix.chain_dot(a, b, c)` 72 | #===NOTE: 73 | # Send the matrices in multiplicative order with proper dimensions 74 | def self.chain_dot(*args) 75 | #inspired by Statsmodels 76 | begin 77 | args.reduce { |x, y| x * y } #perform matrix multiplication in order 78 | rescue ExceptionForMatrix::ErrDimensionMismatch 79 | puts "ExceptionForMatrix: Please provide matrices with proper multiplicative dimensions" 80 | end 81 | end 82 | 83 | 84 | #==Adds a column of constants. 85 | #Appends a column of ones to the matrix/array if first argument is false 86 | #If an n-array, first checks if one column of ones is already present 87 | #if present, then original(self) is returned, else, prepends with a vector of ones 88 | def add_constant(prepend = true) 89 | #for Matrix 90 | (0...column_size).each do |i| 91 | if self.column(i).map(&:to_f) == Object::Vector.elements(Array.new(row_size, 1.0)) 92 | return self 93 | end 94 | end 95 | #append/prepend a column of one's 96 | vectors = (0...row_size).map do |r| 97 | if prepend 98 | [1.0].concat(self.row(r).to_a) 99 | else 100 | self.row(r).to_a.push(1.0) 101 | end 102 | end 103 | return Matrix.rows(vectors) 104 | end 105 | 106 | #populates column i of given matrix with arr 107 | def set_column(i, arr) 108 | columns = self.column_vectors 109 | column = columns[i].to_a 110 | column[0...arr.size] = arr 111 | columns[i] = column 112 | return Matrix.columns(columns) 113 | end 114 | 115 | #populates row i of given matrix with arr 116 | def set_row(i, arr) 117 | #similar implementation as set_column 118 | #writing and commenting metaprogrammed version 119 | #Please to give opinion :) 120 | rows = self.row_vectors 121 | row = rows[i].to_a 122 | row[0...arr.size] = arr 123 | rows[i] = row 124 | return Matrix.rows(rows) 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/statsample-timeseries/version.rb: -------------------------------------------------------------------------------- 1 | module Statsample 2 | module TimeSeries 3 | VERSION = '0.3.0' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /statsample-timeseries.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'statsample-timeseries/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'statsample-timeseries' 8 | spec.version = Statsample::TimeSeries::VERSION 9 | spec.authors = ['Ankur Goel', 'Sameer Deshmukh'] 10 | spec.email = ['sameer.deshmukh93@gmail.com'] 11 | spec.summary = %q{statsample-timeseries is a statsample extension which includes many functions for time series analysis.} 12 | spec.description = %q{Various functions for time series analysis.} 13 | spec.homepage = '' 14 | spec.license = 'BSD-2' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_runtime_dependency 'statsample', '~> 2.0' 22 | spec.add_runtime_dependency 'daru', '~> 0.1' 23 | 24 | spec.add_development_dependency 'bundler', '~> 1.10' 25 | spec.add_development_dependency 'rb-gsl', '~> 1.16' 26 | spec.add_development_dependency 'rake', '~> 10.4' 27 | spec.add_development_dependency 'minitest', '~> 5.7' 28 | spec.add_development_dependency 'mocha', '~> 1.1' 29 | spec.add_development_dependency 'shoulda', '~> 3.5' 30 | spec.add_development_dependency 'awesome_print' 31 | end 32 | -------------------------------------------------------------------------------- /test/fixtures/stock_data.csv: -------------------------------------------------------------------------------- 1 | 17.66 2 | 17.65 3 | 17.68 4 | 17.66 5 | 17.68 6 | 17.67 7 | 17.68 8 | 17.68 9 | 17.67 10 | 17.67 11 | 17.68 12 | 17.71 13 | 17.74 14 | 17.72 15 | 17.73 16 | 17.76 17 | 17.74 18 | 17.69 19 | 17.69 20 | 17.67 21 | 17.66 22 | 17.67 23 | 17.69 24 | 17.69 25 | 17.68 26 | 17.65 27 | 17.65 28 | 17.64 29 | 17.63 30 | 17.64 31 | 17.67 32 | 17.68 33 | 17.7 34 | 17.68 35 | 17.69 36 | 17.69 37 | 17.72 38 | 17.71 39 | 17.71 40 | 17.71 41 | 17.69 42 | 17.69 43 | 17.71 44 | 17.72 45 | 17.71 46 | 17.68 47 | 17.68 48 | 17.68 49 | 17.69 50 | 17.68 51 | 17.68 52 | 17.69 53 | 17.67 54 | 17.69 55 | 17.71 56 | 17.7 57 | 17.7 58 | 17.71 59 | 17.73 60 | 17.74 61 | 17.74 62 | 17.74 63 | 17.76 64 | 17.77 65 | 17.55 66 | 17.55 67 | 17.5 68 | 17.46 69 | 17.49 70 | 17.54 71 | 17.51 72 | 17.54 73 | 17.57 74 | 17.54 75 | 17.52 76 | 17.53 77 | 17.56 78 | 17.55 79 | 17.55 80 | 17.54 81 | 17.55 82 | 17.55 83 | 17.55 84 | 17.54 85 | 17.52 86 | 17.53 87 | 17.51 88 | 17.52 89 | 17.5 90 | 17.5 91 | 17.5 92 | 17.49 93 | 17.46 94 | 17.47 95 | 17.48 96 | 17.45 97 | 17.41 98 | 17.39 99 | 17.38 100 | 17.43 101 | 17.44 102 | 17.43 103 | 17.43 104 | 17.46 105 | 17.46 106 | 17.47 107 | 17.47 108 | 17.45 109 | 17.48 110 | 17.49 111 | 17.5 112 | 17.49 113 | 17.48 114 | 17.49 115 | 17.47 116 | 17.47 117 | 17.44 118 | 17.44 119 | 17.43 120 | 17.45 121 | 17.42 122 | 17.43 123 | 17.43 124 | 17.44 125 | 17.44 126 | 17.43 127 | 17.41 128 | 17.41 129 | 17.38 130 | 17.38 131 | 17.37 132 | 17.37 133 | 17.37 134 | 17.3 135 | 17.28 136 | 17.27 137 | 17.19 138 | 16.41 139 | 16.44 140 | 16.48 141 | 16.53 142 | 16.51 143 | 16.57 144 | 16.54 145 | 16.59 146 | 16.64 147 | 16.6 148 | 16.65 149 | 16.69 150 | 16.69 151 | 16.68 152 | 16.64 153 | 16.65 154 | 16.66 155 | 16.64 156 | 16.61 157 | 16.65 158 | 16.67 159 | 16.66 160 | 16.65 161 | 16.61 162 | 16.59 163 | 16.57 164 | 16.55 165 | 16.55 166 | 16.57 167 | 16.54 168 | 16.6 169 | 16.62 170 | 16.6 171 | 16.59 172 | 16.61 173 | 16.66 174 | 16.69 175 | 16.67 176 | 16.65 177 | 16.66 178 | 16.65 179 | 16.65 180 | 16.68 181 | 16.68 182 | 16.67 183 | 16.64 184 | 16.73 185 | 16.76 186 | 16.75 187 | 16.79 188 | 16.8 189 | 16.77 190 | 16.74 191 | 16.76 192 | 16.83 193 | 16.84 194 | 16.82 195 | 16.89 196 | 16.93 197 | 16.94 198 | 16.9 199 | 16.92 200 | 16.88 201 | 16.85 202 | 16.87 203 | 16.8 204 | 16.79 205 | 16.85 206 | 16.85 207 | 16.8 208 | 16.82 209 | 16.85 210 | 16.9 211 | 16.86 212 | 16.79 213 | 16.75 214 | 16.78 215 | 17.06 216 | 17.05 217 | 17.04 218 | 17.02 219 | 17.01 220 | 17.02 221 | 17.05 222 | 17.07 223 | 17.08 224 | 17.09 225 | 17.1 226 | 17.11 227 | 17.09 228 | 17.1 229 | 17.1 230 | 17.12 231 | 17.17 232 | 17.16 233 | 17.17 234 | 17.18 235 | 17.18 236 | 17.18 237 | 17.17 238 | 17.15 239 | 17.14 240 | 17.13 241 | 17.14 242 | 17.13 243 | 17.12 244 | 17.12 245 | 17.09 246 | 17.09 247 | 17.11 248 | 17.06 249 | 17.07 250 | 17.06 251 | 17.07 252 | 17.06 253 | 17.09 254 | 17.05 255 | 17.04 256 | 17.04 257 | 16.99 258 | 17 259 | 17.03 260 | 17 261 | 16.97 262 | 16.96 263 | 16.98 264 | 16.98 265 | 16.98 266 | 17.03 267 | 17 268 | 17 269 | 17 270 | 17.02 271 | 17 272 | 17.02 273 | 17.01 274 | 17.02 275 | 17.03 276 | 17.03 277 | 17.01 278 | 17.03 279 | 17.03 280 | 17.03 281 | 17.01 282 | 17.03 283 | 17.05 284 | 17.05 285 | 17.08 286 | 17.04 287 | 17.01 288 | 17.03 289 | 17.02 290 | 17.03 291 | 17.04 292 | 17.05 293 | 17.37 294 | 17.35 295 | 17.34 296 | 17.32 297 | 17.29 298 | 17.29 299 | 17.22 300 | 17.26 301 | 17.3 302 | 17.34 303 | 17.33 304 | 17.39 305 | 17.4 306 | 17.39 307 | 17.48 308 | 17.5 309 | 17.47 310 | 17.43 311 | 17.4 312 | 17.42 313 | 17.46 314 | 17.48 315 | 17.48 316 | 17.46 317 | 17.46 318 | 17.45 319 | 17.43 320 | 17.44 321 | 17.48 322 | 17.43 323 | 17.45 324 | 17.47 325 | 17.46 326 | 17.46 327 | 17.48 328 | 17.48 329 | 17.48 330 | 17.46 331 | 17.5 332 | 17.55 333 | 17.58 334 | 17.57 335 | 17.56 336 | 17.59 337 | 17.61 338 | 17.62 339 | 17.63 340 | 17.62 341 | 17.61 342 | 17.61 343 | 17.62 344 | 17.64 345 | 17.65 346 | 17.61 347 | 17.62 348 | 17.66 349 | 17.65 350 | 17.64 351 | 17.63 352 | 17.64 353 | 17.64 354 | 17.64 355 | 17.63 356 | 17.61 357 | 17.61 358 | 17.62 359 | 17.63 360 | 17.64 361 | 17.65 362 | 17.66 363 | 17.68 364 | 17.69 365 | 17.69 366 | 17.69 367 | 17.66 368 | 17.69 369 | 17.69 370 | 17.62 371 | 17.68 372 | 17.64 373 | 17.65 374 | 17.61 375 | 17.52 376 | 17.56 377 | 17.55 378 | 17.55 379 | 17.48 380 | 17.45 381 | 17.46 382 | 17.46 383 | 17.44 384 | 17.47 385 | 17.5 386 | 17.49 387 | 17.5 388 | 17.53 389 | 17.53 390 | 17.54 391 | 17.51 392 | 17.51 393 | 17.53 394 | 17.53 395 | 17.53 396 | 17.55 397 | 17.55 398 | 17.54 399 | 17.56 400 | 17.59 401 | 17.57 402 | 17.58 403 | 17.58 404 | 17.57 405 | 17.59 406 | 17.57 407 | 17.55 408 | 17.51 409 | 17.51 410 | 17.52 411 | 17.52 412 | 17.53 413 | 17.55 414 | 17.59 415 | 17.61 416 | 17.61 417 | 17.6 418 | 17.6 419 | 17.62 420 | 17.65 421 | 17.62 422 | 17.6 423 | 17.6 424 | 17.62 425 | 17.61 426 | 17.62 427 | 17.63 428 | 17.64 429 | 17.65 430 | 17.61 431 | 17.62 432 | 17.64 433 | 17.63 434 | 17.62 435 | 17.6 436 | 17.57 437 | 17.57 438 | 17.6 439 | 17.59 440 | 17.6 441 | 17.61 442 | 17.61 443 | 17.63 444 | 17.63 445 | 17.59 446 | 17.58 447 | 17.76 448 | 17.79 449 | 17.76 450 | 17.73 451 | 17.74 452 | 17.73 453 | 17.67 454 | 17.66 455 | 17.66 456 | 17.64 457 | 17.63 458 | 17.62 459 | 17.61 460 | 17.6 461 | 17.61 462 | 17.61 463 | 17.6 464 | 17.6 465 | 17.64 466 | 17.65 467 | 17.65 468 | 17.63 469 | 17.61 470 | 17.6 471 | 17.63 472 | 17.63 473 | 17.62 474 | 17.63 475 | 17.64 476 | 17.62 477 | 17.63 478 | 17.65 479 | 17.64 480 | 17.6 481 | 17.59 482 | 17.59 483 | 17.58 484 | 17.58 485 | 17.6 486 | 17.6 487 | 17.6 488 | 17.6 489 | 17.6 490 | 17.58 491 | 17.59 492 | 17.6 493 | 17.6 494 | 17.6 495 | 17.59 496 | 17.59 497 | 17.58 498 | 17.58 499 | 17.65 500 | 17.65 501 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | begin 3 | Bundler.setup(:default, :development) 4 | rescue Bundler::BundlerError => e 5 | $stderr.puts e.message 6 | $stderr.puts "Run `bundle install` to install missing gems" 7 | exit e.status_code 8 | end 9 | require 'minitest/autorun' 10 | require 'shoulda' 11 | require 'shoulda-context' 12 | require 'mocha/setup' 13 | require 'awesome_print' 14 | 15 | #require 'statsample-timeseries' 16 | 17 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 18 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 19 | require 'statsample-timeseries' 20 | module MiniTest 21 | # class Unit 22 | class Test 23 | include Shoulda::Context::Assertions 24 | include Shoulda::Context::InstanceMethods 25 | extend Shoulda::Context::ClassMethods 26 | def self.should_with_gsl(name,&block) 27 | should(name) do 28 | if Statsample.has_gsl? 29 | instance_eval(&block) 30 | else 31 | skip("Requires GSL") 32 | end 33 | end 34 | end 35 | end 36 | # end 37 | 38 | module Assertions 39 | alias :assert_raise :assert_raises unless method_defined? :assert_raise 40 | alias :assert_not_equal :refute_equal unless method_defined? :assert_not_equal 41 | alias :assert_not_same :refute_same unless method_defined? :assert_not_same 42 | unless method_defined? :assert_nothing_raised 43 | def assert_nothing_raised(msg=nil) 44 | msg||="Nothing should be raised, but raised %s" 45 | begin 46 | yield 47 | not_raised=true 48 | rescue Exception => e 49 | not_raised=false 50 | msg=sprintf(msg,e) 51 | end 52 | assert(not_raised,msg) 53 | end 54 | end 55 | end 56 | end 57 | 58 | MiniTest.autorun 59 | -------------------------------------------------------------------------------- /test/test_acf.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | 3 | class StatsampleTimeSeriesPacfTestCase < MiniTest::Test 4 | context Statsample::TimeSeries do 5 | include Statsample::TimeSeries 6 | 7 | setup do 8 | Daru.lazy_update = true 9 | @timeseries = Daru::Vector.new((1..20).map { |e| e * 10 }) 10 | end 11 | 12 | teardown do 13 | Daru.lazy_update = false 14 | end 15 | 16 | should "cross-check ACF for 10 lags" do 17 | lags = 10 18 | result = @timeseries.acf(lags) 19 | assert_equal result.size, 11 20 | assert_equal result, [1.0, 0.85, 0.7015037593984963, 0.556015037593985, 21 | 0.4150375939849624, 0.2800751879699248, 0.15263157894736842, 22 | 0.034210526315789476, -0.07368421052631578, -0.16954887218045114, 23 | -0.2518796992481203] 24 | end 25 | 26 | should "cross-check ACF for 5 lags" do 27 | lags = 5 28 | result = @timeseries.acf(lags) 29 | assert_equal result.size, 6 30 | assert_equal result, [1.0, 0.85, 0.7015037593984963, 0.556015037593985, 31 | 0.4150375939849624, 0.2800751879699248] 32 | end 33 | 34 | should "first value should be 1" do 35 | lags = 2 36 | result = @timeseries.acf(lags) 37 | assert_equal result.size, 3 38 | assert_equal result.first, 1.0 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /test/test_arima_ks.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | 3 | class StatsampleArimaKSTestCase < MiniTest::Test 4 | 5 | context("AR(0.5) simulation") do 6 | #include Statsample::TimeSeries 7 | setup do 8 | Daru.lazy_update = true 9 | @s = Daru::Vector.new([-1.16025577,0.64758021,0.77158601,0.14989543,2.31358162,3.49213868,1.14826956,0.58169457,-0.30813868,-0.34741084,-1.41175595,0.06040081, -0.78230232,0.86734837,0.95015787,-0.49781397,0.53247330,1.56495187,0.30936619,0.09750217,1.09698829,-0.81315490,-0.79425607,-0.64568547,-1.06460320,1.24647894,0.66695937,1.50284551,1.17631218,1.64082872,1.61462736,0.06443761,-0.17583741,0.83918339,0.46610988,-0.54915270,-0.56417108,-1.27696654,0.89460084,1.49970338,0.24520493,0.26249138,-1.33744834,-0.57725961,1.55819543,1.62143157,0.44421891,-0.74000084 ,0.57866347,3.51189333,2.39135077,1.73046244,1.81783890,0.21454040,0.43520890,-1.42443856,-2.72124685,-2.51313877,-1.20243091,-1.44268002 ,-0.16777305,0.05780661,2.03533992,0.39187242,0.54987983,0.57865693,-0.96592469,-0.93278473,-0.75962671,-0.63216906,1.06776183, 0.17476059 ,0.06635860,0.94906227,2.44498583,-1.04990407,-0.88440073,-1.99838258,-1.12955558,-0.62654882,-1.36589161,-2.67456821,-0.97187696, -0.84431782 ,-0.10051809,0.54239549,1.34622861,1.25598105,0.19707759,3.29286114,3.52423499,1.69146333,-0.10150024,0.45222903,-0.01730516, -0.49828727, -1.18484684,-1.09531773,-1.17190808,0.30207662]) 10 | end 11 | 12 | teardown do 13 | Daru.lazy_update = false 14 | end 15 | 16 | if Statsample.has_gsl? 17 | context "passed through the Kalman Filter" do 18 | setup do 19 | @kf=Statsample::TimeSeries::ARIMA.ks(@s,1,0,0) 20 | end 21 | 22 | should "return correct object" do 23 | assert_instance_of Statsample::TimeSeries::Arima::KalmanFilter, @kf 24 | end 25 | 26 | should "return correct parameters" do 27 | assert_equal @kf.p,1 28 | assert_equal @kf.q,0 29 | assert_equal @kf.i,0 30 | end 31 | 32 | should "return correct ar estimators" do 33 | assert_equal @kf.ar.length,1 34 | assert_in_delta @kf.ar[0], 0.700 #0.564 35 | end 36 | should "return correct ma estimators" do 37 | assert_equal @kf.ma.length,0 38 | end 39 | end 40 | end 41 | context "passed through the Kalman Filter with AR(0.564)" do 42 | setup do 43 | @kf_likehood = Statsample::TimeSeries::Arima::KalmanFilter. 44 | log_likelihood([0.564],@s,1,0) 45 | end 46 | 47 | should "return correct object for log_likehood" do 48 | assert_instance_of Statsample::TimeSeries::Arima::KF::LogLikelihood, @kf_likehood 49 | end 50 | should "return correct log_likehood" do 51 | assert_in_delta -148.7003, @kf_likehood.log_likelihood 52 | end 53 | should "return correct sigma" do 54 | assert_in_delta 1.137915, @kf_likehood.sigma 55 | end 56 | should "return correct AIC value" do 57 | assert_in_delta 301.44, @kf_likehood.aic, 0.1 58 | end 59 | end 60 | 61 | context "passed through the Kalman Filter with AR(0.2)" do 62 | setup do 63 | @kf_likehood=Statsample::TimeSeries::Arima::KalmanFilter.log_likelihood([0.2],@s,1,0) 64 | end 65 | should "return correct object for log_likehood" do 66 | assert_instance_of Statsample::TimeSeries::Arima::KF::LogLikelihood, @kf_likehood 67 | end 68 | should "return correct log_likehood" do 69 | assert_in_delta -66.40337-0.5*@s.size*(Math.log(2*Math::PI)), @kf_likehood.log_likelihood, 0.01 70 | end 71 | should "return correct sigma" do 72 | assert_in_delta 1.378693, @kf_likehood.sigma, 0.01 73 | end 74 | end 75 | end 76 | 77 | context("ARMA(1, 1) process") do 78 | setup do 79 | @s = Daru::Vector.new([-1.16025577,0.64758021,0.77158601,0.14989543,2.31358162,3.49213868,1.14826956,0.58169457,-0.30813868,-0.34741084,-1.41175595,0.06040081, -0.78230232,0.86734837,0.95015787,-0.49781397,0.53247330,1.56495187,0.30936619,0.09750217,1.09698829,-0.81315490,-0.79425607,-0.64568547,-1.06460320,1.24647894,0.66695937,1.50284551,1.17631218,1.64082872,1.61462736,0.06443761,-0.17583741,0.83918339,0.46610988,-0.54915270,-0.56417108,-1.27696654,0.89460084,1.49970338,0.24520493,0.26249138,-1.33744834,-0.57725961,1.55819543,1.62143157,0.44421891,-0.74000084 ,0.57866347,3.51189333,2.39135077,1.73046244,1.81783890,0.21454040,0.43520890,-1.42443856,-2.72124685,-2.51313877,-1.20243091,-1.44268002 ,-0.16777305,0.05780661,2.03533992,0.39187242,0.54987983,0.57865693,-0.96592469,-0.93278473,-0.75962671,-0.63216906,1.06776183, 0.17476059 ,0.06635860,0.94906227,2.44498583,-1.04990407,-0.88440073,-1.99838258,-1.12955558,-0.62654882,-1.36589161,-2.67456821,-0.97187696, -0.84431782 ,-0.10051809,0.54239549,1.34622861,1.25598105,0.19707759,3.29286114,3.52423499,1.69146333,-0.10150024,0.45222903,-0.01730516, -0.49828727, -1.18484684,-1.09531773,-1.17190808,0.30207662]) 80 | end 81 | 82 | if Statsample.has_gsl? 83 | context "passed through the Kalman Filter" do 84 | setup do 85 | @kf = Statsample::TimeSeries::ARIMA.ks(@s, 2, 0, 1) 86 | end 87 | 88 | should "return correct parameters" do 89 | assert_equal @kf.p, 2 90 | assert_equal @kf.q, 1 91 | assert_equal @kf.i, 0 92 | end 93 | 94 | should "return correct AR estimators" do 95 | assert_equal @kf.ar.length, 2 96 | assert_in_delta @kf.ar[0], 0.46, 0.01 97 | assert_in_delta @kf.ar[1], -0.22, 0.01 98 | end 99 | 100 | should "return correct ma estimators" do 101 | assert_equal @kf.ma.length, 1 102 | assert_in_delta @kf.ma[0], 0.18, 0.01 103 | end 104 | end 105 | end 106 | 107 | context "passed through the LogLikelihood with ARMA([0.45, 0.16, 0.18])" do 108 | setup do 109 | log_likelihood = Statsample::TimeSeries::Arima::KF::LogLikelihood 110 | @ll = log_likelihood.new([0.45, 0.16, 0.18], @s, 2, 1) 111 | end 112 | 113 | should "return correct log likelihood" do 114 | assert_in_delta -148.22, @ll.log_likelihood, 0.01 115 | end 116 | 117 | should "return correct sigma" do 118 | assert_in_delta 1.14, @ll.sigma, 0.1 119 | end 120 | 121 | should "return correct AIC value" do 122 | assert_in_delta 304.44, @ll.aic, 0.01 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/test_arima_simulators.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | 3 | class StatsampleArimaSimulatorsTest < MiniTest::Test 4 | def setup 5 | Daru.lazy_update = true 6 | end 7 | 8 | def teardown 9 | Daru.lazy_update = false 10 | end 11 | 12 | def generate_acf(simulation) 13 | ts = Daru::Vector.new(simulation) 14 | ts.acf 15 | end 16 | 17 | def generate_pacf(simulation) 18 | ts = Daru::Vector.new(simulation) 19 | ts.pacf 20 | end 21 | 22 | context("AR(1) simulations") do 23 | include Statsample 24 | def self.generate_acf(simulation) 25 | ts = Daru::Vector.new(simulation) 26 | ts.acf 27 | end 28 | 29 | def self.generate_pacf(simulation) 30 | ts = Daru::Vector.new(simulation) 31 | ts.pacf 32 | end 33 | 34 | # TODO: Try to speed this up. 35 | @@series = TimeSeries.arima 36 | @@ar_1_positive = @@series.ar_sim(1500, [0.9], 2) 37 | @@ar_1_negative = @@series.ar_sim(1500, [-0.9], 2) 38 | 39 | #generating acf 40 | @@positive_acf = generate_acf(@@ar_1_positive) 41 | @@negative_acf = generate_acf(@@ar_1_negative) 42 | 43 | #generating pacf 44 | @@positive_pacf = generate_pacf(@@ar_1_positive) 45 | @@negative_pacf = generate_pacf(@@ar_1_negative) 46 | 47 | 48 | should "have exponential decay of acf on positive side with phi > 0" do 49 | acf = @@positive_acf 50 | assert_equal acf[0], 1.0 51 | assert_operator acf[1], :>=, 0.7 52 | assert_operator acf[acf.size - 1], :<=, 0.2 53 | #visualization: 54 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_positive_phi_acf.png 55 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_positive_phi_acf_line.png 56 | end 57 | 58 | should "have series with alternating sign on acf starting on negative side with phi < 0" do 59 | acf = @@negative_acf 60 | assert_equal acf[0], 1.0 61 | #testing for alternating series 62 | assert_operator acf[1], :<, 0 63 | assert_operator acf[2], :>, 0 64 | assert_operator acf[3], :<, 0 65 | assert_operator acf[4], :>, 0 66 | #visualization: 67 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_negative_phi_acf.png 68 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_negative_phi_acf_line.png 69 | end 70 | 71 | should "have positive spike on pacf at lag 1 for phi > 0" do 72 | pacf = @@positive_pacf 73 | assert_operator pacf[1], :>=, 0.7 74 | assert_operator pacf[2], :<=, 0.2 75 | assert_operator pacf[3], :<=, 0.14 76 | #visualization: 77 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_postive_phi_pacf.png 78 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_postive_phi_pacf_line.png 79 | end 80 | 81 | should "have negative spike on pacf at lag 1 for phi < 0" do 82 | pacf = @@negative_pacf 83 | assert_operator pacf[1], :<=, 0 84 | assert_operator pacf[1], :<=, -0.5 85 | assert_operator pacf[2], :>=, -0.5 86 | #visualizaton: 87 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%281%29_negative_phi_pacf.png 88 | #[hided @pacf[0] = 1 to convey accurate picture] 89 | end 90 | end 91 | 92 | context("AR(p) simulations") do 93 | include Statsample 94 | setup do 95 | Daru.lazy_update = true 96 | @series = TimeSeries.arima 97 | @ar_p_positive = @series.ar_sim(1500, [0.3, 0.5], 2) 98 | @ar_p_negative = @series.ar_sim(1500, [-0.3, -0.5], 2) 99 | end 100 | 101 | teardown do 102 | Daru.lazy_update = false 103 | end 104 | 105 | should "have damped sine wave starting on positive side on acf" do 106 | @acf = generate_acf(@ar_p_positive) 107 | assert_operator @acf[0], :>=, @acf[1] 108 | assert_operator @acf[1], :>=, 0.0 109 | #caution: sine curve can split on cartesian plane, 110 | #visualization: 111 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR(p)_positive_phi_sine_wave.png 112 | end 113 | 114 | should "have damped sine wave starting on negative side on acf" do 115 | @acf = generate_acf(@ar_p_negative) 116 | assert_operator @acf[0], :>=, @acf[1] 117 | assert_operator @acf[1], :<=, 0.0 118 | assert_operator @acf[1], :>=, @acf[2] 119 | #caution: sine curve can split on cartesian plane, 120 | #visualization: 121 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR%28p%29_negative_phi_acf_sine_wave.png 122 | end 123 | 124 | should "have spikes from 1 to p for pacf" do 125 | #here p = 2 126 | @pacf = generate_pacf(@ar_p_positive) 127 | assert_equal @pacf[0], 1.0 128 | assert_operator @pacf[1], :>, @pacf[3] 129 | assert_operator @pacf[1], :>, @pacf[4] 130 | assert_operator @pacf[1], :>, @pacf[5] 131 | assert_operator @pacf[2], :>, @pacf[3] 132 | assert_operator @pacf[2], :>, @pacf[4] 133 | #visualization: 134 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/AR(p)_positive_phi_pacf_spikes.png 135 | end 136 | end 137 | 138 | 139 | context("MA(1) simulations") do 140 | include Statsample 141 | setup do 142 | @series = TimeSeries.arima 143 | @ma_positive = @series.ar_sim(1500, [0.5], 2) 144 | @ma_negative = @series.ar_sim(1500, [-0.5], 2) 145 | end 146 | 147 | should "have one positive spike at lag 1 on acf at positive theta" do 148 | @acf = generate_acf(@ma_positive) 149 | assert_equal @acf[0], 1.0 150 | assert_operator @acf[1], :>=, 0 #test if positive 151 | #test if spike 152 | assert_operator @acf[2], :>=, 0.1 153 | assert_operator @acf[3], :<=, 0.2 154 | assert_operator @acf[4], :<=, 0.2 155 | #visualization: 156 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/MA%281%29_postive_acf.png 157 | end 158 | 159 | should "have one negative spike at lag 1 on acf at negative theta" do 160 | @acf = generate_acf(@ma_negative) 161 | assert_operator @acf[1], :<, 0 162 | assert_operator @acf[2], :>=, @acf[1] 163 | assert_operator @acf[3], :>=, @acf[1] 164 | #visualization: 165 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/MA%281%29_negative_acf.png 166 | #positive_vs_negative: 167 | #https://dl.dropboxusercontent.com/u/102071534/sciruby/MA%281%29_acf_positive_vs_negative.png 168 | end 169 | end 170 | 171 | context("MA(q) simulations") do 172 | include Statsample 173 | setup do 174 | @series = TimeSeries.arima 175 | @ma_positive = @series.ar_sim(1500, [0.5, 0.3, 0.2], 2) 176 | @ma_negative = @series.ar_sim(1500, [-0.5], 2) 177 | end 178 | 179 | should "have q positive spikes at lag 1 to q on acf at positive thetas" do 180 | @acf = generate_acf(@ma_positive) 181 | assert_operator @acf[1], :>=, @acf[2] 182 | assert_operator @acf[2], :>=, @acf[3] 183 | assert_operator @acf[3], :>=, @acf[4] 184 | #Visualization: http://jsfiddle.net/YeK2c/ 185 | end 186 | 187 | should "have damped sine wave on pacf at positive thetas" do 188 | #visualization: http://jsfiddle.net/7keHK/2/ 189 | end 190 | end 191 | 192 | context("Yule walker estimations") do 193 | include Statsample 194 | 195 | setup do 196 | @timeseries = Daru::Vector.new(100.times.map { rand }) 197 | @arma_simulation =->(n) { @timeseries.ar(n, k)} 198 | end 199 | 200 | # TODO: write tests 201 | end 202 | end 203 | 204 | -------------------------------------------------------------------------------- /test/test_matrix.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | class StatsampleMatrixTestCase < MiniTest::Test 3 | 4 | def setup_square_matrix(arr, n) 5 | #returns n * n matrix by slicing arr 6 | return Matrix.rows(arr.each_slice(n).to_a) 7 | end 8 | def setup 9 | @arr_square = (1..16) 10 | @mat_non_symmetric = setup_square_matrix(@arr_square, 4) 11 | 12 | @arr_non_square = (1..12).to_a 13 | #this is a 4 X 3 matrix 14 | @mat_non_square = Matrix.rows(@arr_non_square.each_slice(3).to_a) 15 | end 16 | 17 | #TESTS for matrix symmetricity - Matrix#symmetric? 18 | context("symmetric?") do 19 | 20 | should "return false for non-symmetric matrix" do 21 | assert_equal @mat_non_symmetric.symmetric?, false 22 | end 23 | 24 | should "return false for non-square matrix" do 25 | assert_equal @mat_non_square.symmetric?, false 26 | end 27 | 28 | should "return true for symmetrix matrix" do 29 | arr = %w[4 12 -16 12 37 -43 -16 -43 93].map(&:to_i) 30 | mat = setup_square_matrix(arr, 3) 31 | assert_equal mat.symmetric?, true 32 | end 33 | end 34 | 35 | #TESTS for cholesky decomposition - Matrix#cholesky 36 | context("Cholesky Decomposition") do 37 | 38 | should "raise error for non symmetric matrix" do 39 | assert_raises(ArgumentError) { @mat_non_symmetric.cholesky } 40 | end 41 | 42 | should "raise raise error if non-square matix" do 43 | arr = (1..12).to_a 44 | mat = Matrix.rows(arr.each_slice(3).to_a) 45 | assert_raises(ArgumentError) { @mat_non_square.cholesky } 46 | end 47 | 48 | should "give hermitian cholesky decomposed matrix for symmetrix matrix" do 49 | arr = %w[4 12 -16 12 37 -43 -16 -43 93].map(&:to_i) 50 | mat = setup_square_matrix(arr, 3) 51 | assert_equal Matrix[[2.0, 0, 0], [6.0, 1.0, 0], [-8.0, 5.0, 2.0]], mat.cholesky 52 | end 53 | end 54 | 55 | #TESTS for matrix squares of sum - Matrix#squares_of_sum 56 | context("Squares of sum") do 57 | 58 | should "return array of size 4 for matrix - #{@mat_non_symmetric}" do 59 | #equal to column size 60 | assert_equal @mat_non_symmetric.squares_of_sum.size, 4 61 | end 62 | 63 | should "return [784, 1024, 1296, 1600] for matrix - #{@mat_non_symmetric}" do 64 | assert_equal @mat_non_symmetric.squares_of_sum, [784, 1024, 1296, 1600] 65 | end 66 | end 67 | 68 | #TESTS for adding constants to matrix 69 | context("Add constant") do 70 | 71 | should "prepend all rows with ones" do 72 | mat = @mat_non_symmetric.add_constant 73 | assert_equal @mat_non_symmetric.column_size, 4 74 | assert_equal mat.column_size, 5 75 | assert_equal mat.column(0).to_a, [1.0, 1.0,1.0,1.0] 76 | end 77 | 78 | should "append all rows with ones if prepend = false" do 79 | mat = @mat_non_symmetric.add_constant(false) 80 | assert_equal @mat_non_symmetric.column_size, 4 81 | assert_equal mat.column_size, 5 82 | assert_equal mat.column(mat.column_size - 1).to_a, [1.0, 1.0,1.0,1.0] 83 | end 84 | 85 | should "not append/prepend if a column of ones already exists in matrix" do 86 | matrix = Matrix[[1, 2, 1, 4], [5, 6, 1, 8], [9, 10, 1, 12]] 87 | const_mat = matrix.add_constant 88 | assert_equal matrix.column_size, const_mat.column_size 89 | assert_equal matrix.row_size, const_mat.row_size 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/test_pacf.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | class StatsampleTimeSeriesPacfTestCase < MiniTest::Test 3 | context(Statsample::TimeSeries) do 4 | include Statsample::TimeSeries 5 | setup do 6 | Daru.lazy_update = true 7 | @ts = Daru::Vector.new((1..20).map { |x| x * 10 }) 8 | #setting up a proc to get a closure for pacf calling with variable lags and methods 9 | @pacf_proc =->(k, method) { @ts.pacf(k, method) } 10 | end 11 | 12 | teardown do 13 | Daru.lazy_update = false 14 | end 15 | 16 | should "return correct correct pacf size for lags = 5" do 17 | assert_equal @pacf_proc.call(5, 'yw').size, 6 18 | assert_equal @pacf_proc.call(5, 'mle').size, 6 19 | #first element is 1.0 20 | end 21 | 22 | should "return correct correct pacf size for lags = 10" do 23 | assert_equal @pacf_proc.call(10, 'yw').size, 11 24 | assert_equal @pacf_proc.call(10, 'mle').size, 11 25 | #first element is 1.0 26 | end 27 | 28 | should "have first element as 1.0" do 29 | assert_equal @pacf_proc.call(10, 'yw')[0], 1.0 30 | assert_equal @pacf_proc.call(10, 'mle')[0], 1.0 31 | end 32 | 33 | should "give correct pacf results for unbiased yule-walker" do 34 | result_10 = [1.0, 0.8947368421052632, -0.10582010582010604, -0.11350188273265083, -0.12357534824820737, -0.13686534216335522, -0.15470588235294147, -0.17938011883732036, -0.2151192288178601, -0.2707082833133261, -0.3678160919540221] 35 | result_5 = [1.0, 0.8947368421052632, -0.10582010582010604, -0.11350188273265083, -0.12357534824820737, -0.13686534216335522] 36 | assert_equal @pacf_proc.call(10, 'yw'), result_10 37 | assert_equal @pacf_proc.call(5, 'yw'), result_5 38 | 39 | #Checking for lag = (1..10) 40 | 1.upto(10) do |i| 41 | assert_equal @pacf_proc.call(i, 'yw'), result_10[0..i] 42 | end 43 | end 44 | 45 | should "give correct pacf results for mle yule-walker" do 46 | result_10 = [1.0, 0.85, -0.07566212829370711, -0.07635069706072706, -0.07698628638512295, -0.07747034005560738, -0.0776780981161499, -0.07744984679625189, -0.0765803323191094, -0.07480650005932366, -0.07179435184923755] 47 | result_5 = [1.0, 0.85, -0.07566212829370711, -0.07635069706072706, -0.07698628638512295, -0.07747034005560738] 48 | assert_equal @pacf_proc.call(10, 'mle'), result_10 49 | assert_equal @pacf_proc.call(5, 'mle'), result_5 50 | 51 | #Checking for lag = (1..10) 52 | 1.upto(10) do |i| 53 | assert_equal @pacf_proc.call(i, 'mle'), result_10[0..i] 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/test_wald.rb: -------------------------------------------------------------------------------- 1 | require(File.expand_path(File.dirname(__FILE__)+'/helper.rb')) 2 | 3 | class StatsampleWaldTest < MiniTest::Test 4 | # Wald test is useful to test a series of n acf with Chi-square 5 | # degree of freedom. It is extremely useful to test fit the fit of 6 | # an ARIMA model to test the residuals. 7 | 8 | include Statsample::Shorthand 9 | include Distribution 10 | 11 | def setup 12 | #create time series to evaluate later 13 | Daru.lazy_update = true 14 | @wald = Daru::Vector.new(100.times.map { rand(100) }) 15 | end 16 | 17 | def teardown 18 | Daru.lazy_update = false 19 | end 20 | 21 | def sum_of_squares_of_acf_series(lags) 22 | #perform sum of squares for a series of acf with specified lags 23 | acf = @wald.acf(lags) 24 | return acf.map { |x| x ** 2 }.inject(:+) 25 | end 26 | 27 | def chisquare_cdf(sum_of_squares, lags) 28 | 1 - ChiSquare.cdf(sum_of_squares, lags) 29 | end 30 | 31 | 32 | def test_wald_with_5_lags 33 | #number of lags for acf = 5 34 | lags = 5 35 | sum_of_squares = sum_of_squares_of_acf_series(lags) 36 | assert_in_delta chisquare_cdf(sum_of_squares, lags), 1, 0.05 37 | assert_equal @wald.acf(lags).size, lags + 1 38 | end 39 | 40 | 41 | def test_wald_with_10_lags 42 | #number of lags for acf = 10 43 | lags = 10 44 | sum_of_squares = sum_of_squares_of_acf_series(lags) 45 | assert_in_delta chisquare_cdf(sum_of_squares, lags), 1, 0.05 46 | assert_equal @wald.acf(lags).size, lags + 1 47 | end 48 | 49 | 50 | def test_wald_with_15_lags 51 | #number of lags for acf = 15 52 | lags = 15 53 | sum_of_squares = sum_of_squares_of_acf_series(lags) 54 | assert_in_delta chisquare_cdf(sum_of_squares, lags), 1, 0.05 55 | assert_equal @wald.acf(lags).size, lags + 1 56 | end 57 | 58 | 59 | def test_wald_with_20_lags 60 | #number of lags for acf = 20 61 | lags = 20 62 | sum_of_squares = sum_of_squares_of_acf_series(lags) 63 | assert_in_delta chisquare_cdf(sum_of_squares, lags), 1, 0.05 64 | assert_equal @wald.acf(lags).size, lags + 1 65 | end 66 | 67 | 68 | def test_wald_with_25_lags 69 | #number of lags for acf = 25 70 | lags = 25 71 | sum_of_squares = sum_of_squares_of_acf_series(lags) 72 | assert_in_delta chisquare_cdf(sum_of_squares, lags), 1, 0.05 73 | assert_equal @wald.acf(lags).size, lags + 1 74 | end 75 | end 76 | --------------------------------------------------------------------------------