bullshit 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.travis.yml +7 -0
- data/CHANGES +3 -0
- data/Gemfile +5 -0
- data/{README → README.rdoc} +0 -0
- data/Rakefile +31 -83
- data/VERSION +1 -1
- data/bullshit.gemspec +43 -0
- data/data/.keep +0 -0
- data/lib/bullshit.rb +9 -942
- data/lib/bullshit/version.rb +1 -1
- metadata +96 -37
- data/install.rb +0 -15
- data/make_doc.rb +0 -5
- data/tests/test_analysis.rb +0 -321
- data/tests/test_continued_fraction.rb +0 -40
- data/tests/test_distribution.rb +0 -69
- data/tests/test_functions.rb +0 -33
- data/tests/test_newton_bisection.rb +0 -28
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGES
CHANGED
data/Gemfile
ADDED
data/{README → README.rdoc}
RENAMED
File without changes
|
data/Rakefile
CHANGED
@@ -1,85 +1,33 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
ruby 'make_doc.rb'
|
33
|
-
end
|
34
|
-
|
35
|
-
if defined? Gem
|
36
|
-
spec = Gem::Specification.new do |s|
|
37
|
-
s.name = PKG_NAME
|
38
|
-
s.version = PKG_VERSION
|
39
|
-
s.summary = "Benchmarking is Bullshit"
|
40
|
-
s.description = ""
|
41
|
-
|
42
|
-
s.add_dependency('dslkit', '>= 0.2.5')
|
43
|
-
|
44
|
-
s.files = PKG_FILES
|
45
|
-
|
46
|
-
s.require_path = 'lib'
|
47
|
-
s.executables = 'bs_compare'
|
48
|
-
|
49
|
-
s.has_rdoc = true
|
50
|
-
s.rdoc_options <<
|
51
|
-
'--title' << 'Bullshit -- Benchmarking in Ruby' << '--main' << 'README'
|
52
|
-
s.test_files = Dir['tests/*.rb']
|
53
|
-
|
54
|
-
s.author = "Florian Frank"
|
55
|
-
s.email = "flori@ping.de"
|
56
|
-
s.homepage = "http://flori.github.com/#{PKG_NAME}"
|
57
|
-
s.rubyforge_project = PKG_NAME
|
58
|
-
end
|
59
|
-
|
60
|
-
Rake::GemPackageTask.new(spec) do |pkg|
|
61
|
-
pkg.need_tar = true
|
62
|
-
pkg.package_files += PKG_FILES
|
1
|
+
# vim: set filetype=ruby et sw=2 ts=2:
|
2
|
+
|
3
|
+
require 'gem_hadar'
|
4
|
+
|
5
|
+
GemHadar do
|
6
|
+
name 'bullshit'
|
7
|
+
author 'Florian Frank'
|
8
|
+
email 'flori@ping.de'
|
9
|
+
homepage "http://flori.github.com/#{name}"
|
10
|
+
summary 'Benchmarking is Bullshit'
|
11
|
+
description 'Library to benchmark ruby code and analyse the results'
|
12
|
+
test_dir 'tests'
|
13
|
+
ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock'
|
14
|
+
readme 'README.rdoc'
|
15
|
+
title "#{name.camelize} -- Benchmarking in Ruby"
|
16
|
+
executables << 'bs_compare'
|
17
|
+
|
18
|
+
dependency 'spruz', '~>0.2'
|
19
|
+
dependency 'dslkit', '~>0.2'
|
20
|
+
dependency 'more_math', '~>0.0.1'
|
21
|
+
clobber 'data/*.{dat,log}'
|
22
|
+
|
23
|
+
install_library do
|
24
|
+
libdir = CONFIG["sitelibdir"]
|
25
|
+
install('lib/bullshit.rb', libdir, :mode => 0644)
|
26
|
+
mkdir_p subdir = File.join(libdir, 'bullshit')
|
27
|
+
for f in Dir['lib/bullshit/*.rb']
|
28
|
+
install(f, subdir)
|
29
|
+
end
|
30
|
+
bindir = CONFIG["bindir"]
|
31
|
+
install('bin/bs_compare', bindir, :mode => 0755)
|
63
32
|
end
|
64
33
|
end
|
65
|
-
|
66
|
-
desc m = "Writing version information for #{PKG_VERSION}"
|
67
|
-
task :version do
|
68
|
-
puts m
|
69
|
-
File.open(File.join('lib', PKG_NAME, 'version.rb'), 'w') do |v|
|
70
|
-
v.puts <<EOT
|
71
|
-
module Bullshit
|
72
|
-
# Bullshit version
|
73
|
-
VERSION = '#{PKG_VERSION}'
|
74
|
-
VERSION_ARRAY = VERSION.split(/\\./).map { |x| x.to_i } # :nodoc:
|
75
|
-
VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
|
76
|
-
VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
|
77
|
-
VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
|
78
|
-
end
|
79
|
-
EOT
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
task :default => [ :version, :test ]
|
84
|
-
|
85
|
-
task :release => [ :clobber, :version, :package ]
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.2
|
data/bullshit.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{bullshit}
|
5
|
+
s.version = "0.1.2"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Florian Frank"]
|
9
|
+
s.date = %q{2011-07-17}
|
10
|
+
s.default_executable = %q{bs_compare}
|
11
|
+
s.description = %q{Library to benchmark ruby code and analyse the results}
|
12
|
+
s.email = %q{flori@ping.de}
|
13
|
+
s.executables = ["bs_compare"]
|
14
|
+
s.extra_rdoc_files = ["README.rdoc", "lib/bullshit/version.rb", "lib/bullshit.rb"]
|
15
|
+
s.files = [".gitignore", ".travis.yml", "CHANGES", "COPYING", "Gemfile", "README.rdoc", "Rakefile", "VERSION", "bin/bs_compare", "bullshit.gemspec", "data/.keep", "examples/compare.rb", "examples/fibonacci.rb", "examples/iteration.rb", "examples/josephus.rb", "examples/sorting.rb", "examples/throw_raise.rb", "lib/bullshit.rb", "lib/bullshit/version.rb", "tests/test_bullshit.rb", "tests/test_window.rb"]
|
16
|
+
s.homepage = %q{http://flori.github.com/bullshit}
|
17
|
+
s.rdoc_options = ["--title", "Bullshit -- Benchmarking in Ruby", "--main", "README.rdoc"]
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
s.rubygems_version = %q{1.6.2}
|
20
|
+
s.summary = %q{Benchmarking is Bullshit}
|
21
|
+
s.test_files = ["tests/test_bullshit.rb", "tests/test_window.rb"]
|
22
|
+
|
23
|
+
if s.respond_to? :specification_version then
|
24
|
+
s.specification_version = 3
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
27
|
+
s.add_development_dependency(%q<gem_hadar>, ["~> 0.0.5"])
|
28
|
+
s.add_runtime_dependency(%q<spruz>, ["~> 0.2"])
|
29
|
+
s.add_runtime_dependency(%q<dslkit>, ["~> 0.2"])
|
30
|
+
s.add_runtime_dependency(%q<more_math>, ["~> 0.0.1"])
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<gem_hadar>, ["~> 0.0.5"])
|
33
|
+
s.add_dependency(%q<spruz>, ["~> 0.2"])
|
34
|
+
s.add_dependency(%q<dslkit>, ["~> 0.2"])
|
35
|
+
s.add_dependency(%q<more_math>, ["~> 0.0.1"])
|
36
|
+
end
|
37
|
+
else
|
38
|
+
s.add_dependency(%q<gem_hadar>, ["~> 0.0.5"])
|
39
|
+
s.add_dependency(%q<spruz>, ["~> 0.2"])
|
40
|
+
s.add_dependency(%q<dslkit>, ["~> 0.2"])
|
41
|
+
s.add_dependency(%q<more_math>, ["~> 0.0.1"])
|
42
|
+
end
|
43
|
+
end
|
data/data/.keep
ADDED
File without changes
|
data/lib/bullshit.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
1
|
require 'dslkit'
|
2
2
|
require 'enumerator'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
rescue LoadError
|
7
|
-
end
|
4
|
+
require 'bullshit/version'
|
5
|
+
require 'more_math'
|
8
6
|
|
9
7
|
# Module that includes all constants of the bullshit library.
|
10
8
|
module Bullshit
|
@@ -12,147 +10,10 @@ module Bullshit
|
|
12
10
|
|
13
11
|
NAME_COLUMN_SIZE = 5 # Number of columns used for row names.
|
14
12
|
|
15
|
-
Infinity = 1.0 / 0 # Refers to floating point infinity.
|
16
|
-
|
17
13
|
RUBY_DESCRIPTION = "ruby %s (%s patchlevel %s) [%s]" %
|
18
14
|
[ RUBY_VERSION, RUBY_RELEASE_DATE, RUBY_PATCHLEVEL, RUBY_PLATFORM ]
|
19
15
|
|
20
|
-
|
21
|
-
#
|
22
|
-
# b_1
|
23
|
-
# a_0 + -------------------------
|
24
|
-
# b_2
|
25
|
-
# a_1 + --------------------
|
26
|
-
# b_3
|
27
|
-
# a_2 + ---------------
|
28
|
-
# b_4
|
29
|
-
# a_3 + ----------
|
30
|
-
# b_5
|
31
|
-
# a_4 + -----
|
32
|
-
# ...
|
33
|
-
#
|
34
|
-
class ContinuedFraction
|
35
|
-
# Creates a continued fraction instance. With the defaults for_a { 1 } and
|
36
|
-
# for_b { 1 } it approximates the golden ration phi if evaluated.
|
37
|
-
def initialize
|
38
|
-
@a = proc { 1.0 }
|
39
|
-
@b = proc { 1.0 }
|
40
|
-
end
|
41
|
-
|
42
|
-
# Creates a ContinuedFraction instances and passes its arguments to a call
|
43
|
-
# to for_a.
|
44
|
-
def self.for_a(arg = nil, &block)
|
45
|
-
new.for_a(arg, &block)
|
46
|
-
end
|
47
|
-
|
48
|
-
# Creates a ContinuedFraction instances and passes its arguments to a call
|
49
|
-
# to for_b.
|
50
|
-
def self.for_b(arg = nil, &block)
|
51
|
-
new.for_b(arg, &block)
|
52
|
-
end
|
53
|
-
|
54
|
-
# This method either takes a block or an argument +arg+. The argument +arg+
|
55
|
-
# has to respond to an integer index n >= 0 and return the value a_n. The
|
56
|
-
# block has to return the value for a_n when +n+ is passed as the first
|
57
|
-
# argument to the block. If a_n is dependent on an +x+ value (see the call
|
58
|
-
# method) the +x+ will be the second argument of the block.
|
59
|
-
def for_a(arg = nil, &block)
|
60
|
-
if arg and !block
|
61
|
-
@a = arg
|
62
|
-
elsif block and !arg
|
63
|
-
@a = block
|
64
|
-
else
|
65
|
-
raise ArgumentError, "exactly one argument or one block required"
|
66
|
-
end
|
67
|
-
self
|
68
|
-
end
|
69
|
-
|
70
|
-
# This method either takes a block or an argument +arg+. The argument +arg+
|
71
|
-
# has to respond to an integer index n >= 1 and return the value b_n. The
|
72
|
-
# block has to return the value for b_n when +n+ is passed as the first
|
73
|
-
# argument to the block. If b_n is dependent on an +x+ value (see the call
|
74
|
-
# method) the +x+ will be the second argument of the block.
|
75
|
-
def for_b(arg = nil, &block)
|
76
|
-
if arg and !block
|
77
|
-
@b = arg
|
78
|
-
elsif block and !arg
|
79
|
-
@b = block
|
80
|
-
else
|
81
|
-
raise ArgumentError, "exactly one argument or one block required"
|
82
|
-
end
|
83
|
-
self
|
84
|
-
end
|
85
|
-
|
86
|
-
# Returns the value for a_n or a_n(x).
|
87
|
-
def a(n, x = nil)
|
88
|
-
result = if x
|
89
|
-
@a[n, x]
|
90
|
-
else
|
91
|
-
@a[n]
|
92
|
-
end and result.to_f
|
93
|
-
end
|
94
|
-
|
95
|
-
# Returns the value for b_n or b_n(x).
|
96
|
-
def b(n, x = nil)
|
97
|
-
result = if x
|
98
|
-
@b[n, x]
|
99
|
-
else
|
100
|
-
@b[n]
|
101
|
-
end and result.to_f
|
102
|
-
end
|
103
|
-
|
104
|
-
# Evaluates the continued fraction for the value +x+ (if any) with the
|
105
|
-
# accuracy +epsilon+ and +max_iterations+ as the maximum number of
|
106
|
-
# iterations using the Wallis-method with scaling.
|
107
|
-
def call(x = nil, epsilon = 1E-16, max_iterations = 1 << 31)
|
108
|
-
c_0, c_1 = 1.0, a(0, x)
|
109
|
-
c_1 == nil and return 0 / 0.0
|
110
|
-
d_0, d_1 = 0.0, 1.0
|
111
|
-
result = c_1 / d_1
|
112
|
-
n = 0
|
113
|
-
error = 1 / 0.0
|
114
|
-
$DEBUG and warn "n=%u, a=%f, b=nil, c=%f, d=%f result=%f, error=nil" %
|
115
|
-
[ n, c_1, c_1, d_1, result ]
|
116
|
-
while n < max_iterations and error > epsilon
|
117
|
-
n += 1
|
118
|
-
a_n, b_n = a(n, x), b(n, x)
|
119
|
-
a_n and b_n or break
|
120
|
-
c_2 = a_n * c_1 + b_n * c_0
|
121
|
-
d_2 = a_n * d_1 + b_n * d_0
|
122
|
-
if c_2.infinite? or d_2.infinite?
|
123
|
-
if a_n != 0
|
124
|
-
c_2 = c_1 + (b_n / a_n * c_0)
|
125
|
-
d_2 = d_1 + (b_n / a_n * d_0)
|
126
|
-
elsif b_n != 0
|
127
|
-
c_2 = (a_n / b_n * c_1) + c_0
|
128
|
-
d_2 = (a_n / b_n * d_1) + d_0
|
129
|
-
else
|
130
|
-
raise Errno::ERANGE
|
131
|
-
end
|
132
|
-
end
|
133
|
-
r = c_2 / d_2
|
134
|
-
error = (r / result - 1).abs
|
135
|
-
|
136
|
-
result = r
|
137
|
-
|
138
|
-
$DEBUG and warn "n=%u, a=%f, b=%f, c=%f, d=%f, result=%f, error=%.16f" %
|
139
|
-
[ n, a_n, b_n, c_1, d_1, result, error ]
|
140
|
-
|
141
|
-
c_0, c_1 = c_1, c_2
|
142
|
-
d_0, d_1 = d_1, d_2
|
143
|
-
end
|
144
|
-
n >= max_iterations and raise Errno::ERANGE
|
145
|
-
result
|
146
|
-
end
|
147
|
-
|
148
|
-
alias [] call
|
149
|
-
|
150
|
-
# Returns this continued fraction as a Proc object which takes the same
|
151
|
-
# arguments like its call method does.
|
152
|
-
def to_proc
|
153
|
-
proc { |*a| call(*a) }
|
154
|
-
end
|
155
|
-
end
|
16
|
+
include MoreMath
|
156
17
|
|
157
18
|
module ModuleFunctions
|
158
19
|
module_function
|
@@ -203,6 +64,8 @@ module Bullshit
|
|
203
64
|
|
204
65
|
# A Clock instance is used to take measurements while benchmarking.
|
205
66
|
class Clock
|
67
|
+
include MoreMath
|
68
|
+
|
206
69
|
TIMES = [ :real, :total, :user, :system ]
|
207
70
|
|
208
71
|
ALL_COLUMNS = [ :scatter ] + TIMES + [ :repeat ]
|
@@ -314,12 +177,12 @@ module Bullshit
|
|
314
177
|
self
|
315
178
|
end
|
316
179
|
|
317
|
-
# Returns a Hash of
|
180
|
+
# Returns a Hash of Sequence object for all of TIMES's time keys.
|
318
181
|
def analysis
|
319
182
|
@analysis ||= Hash.new do |h, time|
|
320
183
|
time = time.to_sym
|
321
184
|
times = @times[time]
|
322
|
-
h[time] =
|
185
|
+
h[time] = MoreMath::Sequence.new(times)
|
323
186
|
end
|
324
187
|
end
|
325
188
|
|
@@ -342,7 +205,7 @@ module Bullshit
|
|
342
205
|
def to_a
|
343
206
|
if @repeat >= 1
|
344
207
|
(::Bullshit::Clock::ALL_COLUMNS).map do |t|
|
345
|
-
analysis[t].
|
208
|
+
analysis[t].elements
|
346
209
|
end.transpose
|
347
210
|
else
|
348
211
|
[]
|
@@ -530,7 +393,7 @@ module Bullshit
|
|
530
393
|
truncation = self.case.truncate_data
|
531
394
|
slope_angle = self.case.truncate_data.slope_angle.abs
|
532
395
|
time = self.case.compare_time.to_sym
|
533
|
-
ms = analysis[time].
|
396
|
+
ms = analysis[time].elements.reverse
|
534
397
|
offset = ms.size - 1
|
535
398
|
@slopes = []
|
536
399
|
ModuleFunctions.array_window(ms, truncation.window_size) do |data|
|
@@ -544,802 +407,6 @@ module Bullshit
|
|
544
407
|
end
|
545
408
|
end
|
546
409
|
|
547
|
-
# A histogram gives an overview of measurement time values.
|
548
|
-
class Histogram
|
549
|
-
# Create a Histogram for +clock+ using the measurements for +time+.
|
550
|
-
def initialize(analysis, bins)
|
551
|
-
@analysis = analysis
|
552
|
-
@bins = bins
|
553
|
-
@result = compute
|
554
|
-
end
|
555
|
-
|
556
|
-
# Number of bins for this Histogram.
|
557
|
-
attr_reader :bins
|
558
|
-
|
559
|
-
# Return the computed histogram as an array of arrays.
|
560
|
-
def to_a
|
561
|
-
@result
|
562
|
-
end
|
563
|
-
|
564
|
-
# Display this histogram to +output+, +width+ is the parameter for
|
565
|
-
# +prepare_display+
|
566
|
-
def display(output = $stdout, width = 50)
|
567
|
-
d = prepare_display(width)
|
568
|
-
for l, bar, r in d
|
569
|
-
output << "%11.5f -|%s\n" % [ (l + r) / 2.0, "*" * bar ]
|
570
|
-
end
|
571
|
-
self
|
572
|
-
end
|
573
|
-
|
574
|
-
private
|
575
|
-
|
576
|
-
# Returns an array of tuples (l, c, r) where +l+ is the left bin edge, +c+
|
577
|
-
# the +width+-normalized frequence count value, and +r+ the right bin
|
578
|
-
# edge. +width+ is usually an integer number representing the width of a
|
579
|
-
# histogram bar.
|
580
|
-
def prepare_display(width)
|
581
|
-
r = @result.reverse
|
582
|
-
factor = width.to_f / (r.transpose[1].max)
|
583
|
-
r.map { |l, c, r| [ l, (c * factor).round, r ] }
|
584
|
-
end
|
585
|
-
|
586
|
-
# Computes the histogram and returns it as an array of tuples (l, c, r).
|
587
|
-
def compute
|
588
|
-
@analysis.measurements.empty? and return []
|
589
|
-
last_r = -Infinity
|
590
|
-
min = @analysis.min
|
591
|
-
max = @analysis.max
|
592
|
-
step = (max - min) / bins.to_f
|
593
|
-
Array.new(bins) do |i|
|
594
|
-
l = min + i * step
|
595
|
-
r = min + (i + 1) * step
|
596
|
-
c = 0
|
597
|
-
@analysis.measurements.each do |x|
|
598
|
-
x > last_r and (x <= r || i == bins - 1) and c += 1
|
599
|
-
end
|
600
|
-
last_r = r
|
601
|
-
[ l, c, r ]
|
602
|
-
end
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
# This class is used to find the root of a function with Newton's bisection
|
607
|
-
# method.
|
608
|
-
class NewtonBisection
|
609
|
-
# Creates a NewtonBisection instance for +function+, a one-argument block.
|
610
|
-
def initialize(&function)
|
611
|
-
@function = function
|
612
|
-
end
|
613
|
-
|
614
|
-
# The function, passed into the constructor.
|
615
|
-
attr_reader :function
|
616
|
-
|
617
|
-
# Return a bracket around a root, starting from the initial +range+. The
|
618
|
-
# method returns nil, if no such bracket around a root could be found after
|
619
|
-
# +n+ tries with the scaling +factor+.
|
620
|
-
def bracket(range = -1..1, n = 50, factor = 1.6)
|
621
|
-
x1, x2 = range.first.to_f, range.last.to_f
|
622
|
-
x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
|
623
|
-
f1, f2 = @function[x1], @function[x2]
|
624
|
-
n.times do
|
625
|
-
f1 * f2 < 0 and return x1..x2
|
626
|
-
if f1.abs < f2.abs
|
627
|
-
f1 = @function[x1 += factor * (x1 - x2)]
|
628
|
-
else
|
629
|
-
f2 = @function[x2 += factor * (x2 - x1)]
|
630
|
-
end
|
631
|
-
end
|
632
|
-
return
|
633
|
-
end
|
634
|
-
|
635
|
-
# Find the root of function in +range+ and return it. The method raises a
|
636
|
-
# BullshitException, if no such root could be found after +n+ tries and in
|
637
|
-
# the +epsilon+ environment.
|
638
|
-
def solve(range = nil, n = 1 << 16, epsilon = 1E-16)
|
639
|
-
if range
|
640
|
-
x1, x2 = range.first.to_f, range.last.to_f
|
641
|
-
x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
|
642
|
-
elsif range = bracket
|
643
|
-
x1, x2 = range.first, range.last
|
644
|
-
else
|
645
|
-
raise ArgumentError, "bracket could not be determined"
|
646
|
-
end
|
647
|
-
f = @function[x1]
|
648
|
-
fmid = @function[x2]
|
649
|
-
f * fmid >= 0 and raise ArgumentError, "root must be bracketed in #{range}"
|
650
|
-
root = if f < 0
|
651
|
-
dx = x2 - x1
|
652
|
-
x1
|
653
|
-
else
|
654
|
-
dx = x1 - x2
|
655
|
-
x2
|
656
|
-
end
|
657
|
-
n.times do
|
658
|
-
fmid = @function[xmid = root + (dx *= 0.5)]
|
659
|
-
fmid < 0 and root = xmid
|
660
|
-
dx.abs < epsilon or fmid == 0 and return root
|
661
|
-
end
|
662
|
-
raise BullshitException, "too many iterations (#{n})"
|
663
|
-
end
|
664
|
-
end
|
665
|
-
|
666
|
-
module Functions
|
667
|
-
module_function
|
668
|
-
|
669
|
-
include Math
|
670
|
-
extend Math
|
671
|
-
|
672
|
-
LANCZOS_COEFFICIENTS = [
|
673
|
-
0.99999999999999709182,
|
674
|
-
57.156235665862923517,
|
675
|
-
-59.597960355475491248,
|
676
|
-
14.136097974741747174,
|
677
|
-
-0.49191381609762019978,
|
678
|
-
0.33994649984811888699e-4,
|
679
|
-
0.46523628927048575665e-4,
|
680
|
-
-0.98374475304879564677e-4,
|
681
|
-
0.15808870322491248884e-3,
|
682
|
-
-0.21026444172410488319e-3,
|
683
|
-
0.21743961811521264320e-3,
|
684
|
-
-0.16431810653676389022e-3,
|
685
|
-
0.84418223983852743293e-4,
|
686
|
-
-0.26190838401581408670e-4,
|
687
|
-
0.36899182659531622704e-5,
|
688
|
-
]
|
689
|
-
|
690
|
-
HALF_LOG_2_PI = 0.5 * log(2 * Math::PI)
|
691
|
-
|
692
|
-
# Returns the natural logarithm of Euler gamma function value for +x+ using
|
693
|
-
# the Lanczos approximation.
|
694
|
-
if method_defined?(:lgamma)
|
695
|
-
def log_gamma(x)
|
696
|
-
lgamma(x).first
|
697
|
-
end
|
698
|
-
else
|
699
|
-
def log_gamma(x)
|
700
|
-
if x.nan? || x <= 0
|
701
|
-
0 / 0.0
|
702
|
-
else
|
703
|
-
sum = 0.0
|
704
|
-
(LANCZOS_COEFFICIENTS.size - 1).downto(1) do |i|
|
705
|
-
sum += LANCZOS_COEFFICIENTS[i] / (x + i)
|
706
|
-
end
|
707
|
-
sum += LANCZOS_COEFFICIENTS[0]
|
708
|
-
tmp = x + 607.0 / 128 + 0.5
|
709
|
-
(x + 0.5) * log(tmp) - tmp + HALF_LOG_2_PI + log(sum / x)
|
710
|
-
end
|
711
|
-
rescue Errno::ERANGE, Errno::EDOM
|
712
|
-
0 / 0.0
|
713
|
-
end
|
714
|
-
end
|
715
|
-
|
716
|
-
# Returns the natural logarithm of the beta function value for +(a, b)+.
|
717
|
-
def log_beta(a, b)
|
718
|
-
log_gamma(a) + log_gamma(b) - log_gamma(a + b)
|
719
|
-
rescue Errno::ERANGE, Errno::EDOM
|
720
|
-
0 / 0.0
|
721
|
-
end
|
722
|
-
|
723
|
-
# Return an approximation value of Euler's regularized beta function for
|
724
|
-
# +x+, +a+, and +b+ with an error <= +epsilon+, but only iterate
|
725
|
-
# +max_iterations+-times.
|
726
|
-
def beta_regularized(x, a, b, epsilon = 1E-16, max_iterations = 1 << 16)
|
727
|
-
x, a, b = x.to_f, a.to_f, b.to_f
|
728
|
-
case
|
729
|
-
when a.nan? || b.nan? || x.nan? || a <= 0 || b <= 0 || x < 0 || x > 1
|
730
|
-
0 / 0.0
|
731
|
-
when x > (a + 1) / (a + b + 2)
|
732
|
-
1 - beta_regularized(1 - x, b, a, epsilon, max_iterations)
|
733
|
-
else
|
734
|
-
fraction = ContinuedFraction.for_b do |n, x|
|
735
|
-
if n % 2 == 0
|
736
|
-
m = n / 2.0
|
737
|
-
(m * (b - m) * x) / ((a + (2 * m) - 1) * (a + (2 * m)))
|
738
|
-
else
|
739
|
-
m = (n - 1) / 2.0
|
740
|
-
-((a + m) * (a + b + m) * x) / ((a + 2 * m) * (a + 2 * m + 1))
|
741
|
-
end
|
742
|
-
end
|
743
|
-
exp(a * log(x) + b * log(1.0 - x) - log(a) - log_beta(a, b)) /
|
744
|
-
fraction[x, epsilon, max_iterations]
|
745
|
-
end
|
746
|
-
rescue Errno::ERANGE, Errno::EDOM
|
747
|
-
0 / 0.0
|
748
|
-
end
|
749
|
-
|
750
|
-
# Return an approximation of the regularized gammaP function for +x+ and
|
751
|
-
# +a+ with an error of <= +epsilon+, but only iterate
|
752
|
-
# +max_iterations+-times.
|
753
|
-
def gammaP_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
|
754
|
-
x, a = x.to_f, a.to_f
|
755
|
-
case
|
756
|
-
when a.nan? || x.nan? || a <= 0 || x < 0
|
757
|
-
0 / 0.0
|
758
|
-
when x == 0
|
759
|
-
0.0
|
760
|
-
when 1 <= a && a < x
|
761
|
-
1 - gammaQ_regularized(x, a, epsilon, max_iterations)
|
762
|
-
else
|
763
|
-
n = 0
|
764
|
-
an = 1 / a
|
765
|
-
sum = an
|
766
|
-
while an.abs > epsilon && n < max_iterations
|
767
|
-
n += 1
|
768
|
-
an *= x / (a + n)
|
769
|
-
sum += an
|
770
|
-
end
|
771
|
-
if n >= max_iterations
|
772
|
-
raise Errno::ERANGE
|
773
|
-
else
|
774
|
-
exp(-x + a * log(x) - log_gamma(a)) * sum
|
775
|
-
end
|
776
|
-
end
|
777
|
-
rescue Errno::ERANGE, Errno::EDOM
|
778
|
-
0 / 0.0
|
779
|
-
end
|
780
|
-
|
781
|
-
# Return an approximation of the regularized gammaQ function for +x+ and
|
782
|
-
# +a+ with an error of <= +epsilon+, but only iterate
|
783
|
-
# +max_iterations+-times.
|
784
|
-
def gammaQ_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
|
785
|
-
x, a = x.to_f, a.to_f
|
786
|
-
case
|
787
|
-
when a.nan? || x.nan? || a <= 0 || x < 0
|
788
|
-
0 / 0.0
|
789
|
-
when x == 0
|
790
|
-
1.0
|
791
|
-
when a > x || a < 1
|
792
|
-
1 - gammaP_regularized(x, a, epsilon, max_iterations)
|
793
|
-
else
|
794
|
-
fraction = ContinuedFraction.for_a do |n, x|
|
795
|
-
(2 * n + 1) - a + x
|
796
|
-
end.for_b do |n, x|
|
797
|
-
n * (a - n)
|
798
|
-
end
|
799
|
-
exp(-x + a * log(x) - log_gamma(a)) *
|
800
|
-
fraction[x, epsilon, max_iterations] ** -1
|
801
|
-
end
|
802
|
-
rescue Errno::ERANGE, Errno::EDOM
|
803
|
-
0 / 0.0
|
804
|
-
end
|
805
|
-
|
806
|
-
ROOT2 = sqrt(2)
|
807
|
-
|
808
|
-
A = -8 * (Math::PI - 3) / (3 * Math::PI * (Math::PI - 4))
|
809
|
-
|
810
|
-
# Returns an approximate value for the error function's value for +x+.
|
811
|
-
def erf(x)
|
812
|
-
r = sqrt(1 - exp(-x ** 2 * (4 / Math::PI + A * x ** 2) / (1 + A * x ** 2)))
|
813
|
-
x < 0 ? -r : r
|
814
|
-
end unless method_defined?(:erf)
|
815
|
-
end
|
816
|
-
|
817
|
-
# This class is used to compute the T-Distribution.
|
818
|
-
class TDistribution
|
819
|
-
include Functions
|
820
|
-
|
821
|
-
# Returns a TDistribution instance for the degrees of freedom +df+.
|
822
|
-
def initialize(df)
|
823
|
-
@df = df
|
824
|
-
end
|
825
|
-
|
826
|
-
# Degrees of freedom.
|
827
|
-
attr_reader :df
|
828
|
-
|
829
|
-
# Returns the cumulative probability (p-value) of the TDistribution for the
|
830
|
-
# t-value +x+.
|
831
|
-
def probability(x)
|
832
|
-
if x == 0
|
833
|
-
0.5
|
834
|
-
else
|
835
|
-
t = beta_regularized(@df / (@df + x ** 2.0), 0.5 * @df, 0.5)
|
836
|
-
if x < 0.0
|
837
|
-
0.5 * t
|
838
|
-
else
|
839
|
-
1 - 0.5 * t
|
840
|
-
end
|
841
|
-
end
|
842
|
-
end
|
843
|
-
|
844
|
-
# Returns the inverse cumulative probability (t-value) of the TDistribution
|
845
|
-
# for the probability +p+.
|
846
|
-
def inverse_probability(p)
|
847
|
-
case
|
848
|
-
when p <= 0
|
849
|
-
-1 / 0.0
|
850
|
-
when p >= 1
|
851
|
-
1 / 0.0
|
852
|
-
else
|
853
|
-
begin
|
854
|
-
bisect = NewtonBisection.new { |x| probability(x) - p }
|
855
|
-
range = bisect.bracket(-10..10)
|
856
|
-
bisect.solve(range, 1_000_000)
|
857
|
-
rescue
|
858
|
-
0 / 0.0
|
859
|
-
end
|
860
|
-
end
|
861
|
-
end
|
862
|
-
end
|
863
|
-
|
864
|
-
# This class is used to compute the Normal Distribution.
|
865
|
-
class NormalDistribution
|
866
|
-
include Functions
|
867
|
-
|
868
|
-
# Creates a NormalDistribution instance for the values +mu+ and +sigma+.
|
869
|
-
def initialize(mu = 0.0, sigma = 1.0)
|
870
|
-
@mu, @sigma = mu.to_f, sigma.to_f
|
871
|
-
end
|
872
|
-
|
873
|
-
attr_reader :mu
|
874
|
-
|
875
|
-
attr_reader :sigma
|
876
|
-
|
877
|
-
# Returns the cumulative probability (p-value) of the NormalDistribution
|
878
|
-
# for the value +x+.
|
879
|
-
def probability(x)
|
880
|
-
0.5 * (1 + erf((x - @mu) / (@sigma * ROOT2)))
|
881
|
-
end
|
882
|
-
|
883
|
-
# Returns the inverse cumulative probability value of the
|
884
|
-
# NormalDistribution for the probability +p+.
|
885
|
-
def inverse_probability(p)
|
886
|
-
case
|
887
|
-
when p <= 0
|
888
|
-
-1 / 0.0
|
889
|
-
when p >= 1
|
890
|
-
1 / 0.0
|
891
|
-
when p == 0.5 # This is a bit sloppy, maybe improve this later.
|
892
|
-
@mu
|
893
|
-
else
|
894
|
-
begin
|
895
|
-
NewtonBisection.new { |x| probability(x) - p }.solve(nil, 1_000_000)
|
896
|
-
rescue
|
897
|
-
0 / 0.0
|
898
|
-
end
|
899
|
-
end
|
900
|
-
end
|
901
|
-
end
|
902
|
-
|
903
|
-
STD_NORMAL_DISTRIBUTION = NormalDistribution.new
|
904
|
-
|
905
|
-
# This class is used to compute the Chi-Square Distribution.
|
906
|
-
class ChiSquareDistribution
|
907
|
-
include Functions
|
908
|
-
|
909
|
-
# Creates a ChiSquareDistribution for +df+ degrees of freedom.
|
910
|
-
def initialize(df)
|
911
|
-
@df = df
|
912
|
-
@df_half = @df / 2.0
|
913
|
-
end
|
914
|
-
|
915
|
-
attr_reader :df
|
916
|
-
|
917
|
-
# Returns the cumulative probability (p-value) of the ChiSquareDistribution
|
918
|
-
# for the value +x+.
|
919
|
-
def probability(x)
|
920
|
-
if x < 0
|
921
|
-
0.0
|
922
|
-
else
|
923
|
-
gammaP_regularized(x / 2, @df_half)
|
924
|
-
end
|
925
|
-
end
|
926
|
-
|
927
|
-
# Returns the inverse cumulative probability value of the
|
928
|
-
# NormalDistribution for the probability +p+.
|
929
|
-
def inverse_probability(p)
|
930
|
-
case
|
931
|
-
when p <= 0, p >= 1
|
932
|
-
0.0
|
933
|
-
else
|
934
|
-
begin
|
935
|
-
bisect = NewtonBisection.new { |x| probability(x) - p }
|
936
|
-
range = bisect.bracket 0.5..10
|
937
|
-
bisect.solve(range, 1_000_000)
|
938
|
-
rescue
|
939
|
-
0 / 0.0
|
940
|
-
end
|
941
|
-
end
|
942
|
-
end
|
943
|
-
end
|
944
|
-
|
945
|
-
# This class computes a linear regression for the given image and domain data
|
946
|
-
# sets.
|
947
|
-
class LinearRegression
|
948
|
-
def initialize(image, domain = (0...image.size).to_a)
|
949
|
-
image.size != domain.size and raise ArgumentError,
|
950
|
-
"image and domain have unequal sizes"
|
951
|
-
@image, @domain = image, domain
|
952
|
-
compute
|
953
|
-
end
|
954
|
-
|
955
|
-
# The image data as an array.
|
956
|
-
attr_reader :image
|
957
|
-
|
958
|
-
# The domain data as an array.
|
959
|
-
attr_reader :domain
|
960
|
-
|
961
|
-
# The slope of the line.
|
962
|
-
attr_reader :a
|
963
|
-
|
964
|
-
# The offset of the line.
|
965
|
-
attr_reader :b
|
966
|
-
|
967
|
-
# Return true if the slope of the underlying data (not the sample data
|
968
|
-
# passed into the constructor of this LinearRegression instance) is likely
|
969
|
-
# (with alpha level _alpha_) to be zero.
|
970
|
-
def slope_zero?(alpha = 0.05)
|
971
|
-
df = @image.size - 2
|
972
|
-
return true if df <= 0 # not enough values to check
|
973
|
-
t = tvalue(alpha)
|
974
|
-
td = TDistribution.new df
|
975
|
-
t.abs <= td.inverse_probability(1 - alpha.abs / 2.0).abs
|
976
|
-
end
|
977
|
-
|
978
|
-
# Returns the residues of this linear regression in relation to the given
|
979
|
-
# domain and image.
|
980
|
-
def residues
|
981
|
-
result = []
|
982
|
-
@domain.zip(@image) do |x, y|
|
983
|
-
result << y - (@a * x + @b)
|
984
|
-
end
|
985
|
-
result
|
986
|
-
end
|
987
|
-
|
988
|
-
private
|
989
|
-
|
990
|
-
def compute
|
991
|
-
size = @image.size
|
992
|
-
sum_xx = sum_xy = sum_x = sum_y = 0.0
|
993
|
-
@domain.zip(@image) do |x, y|
|
994
|
-
x += 1
|
995
|
-
sum_xx += x ** 2
|
996
|
-
sum_xy += x * y
|
997
|
-
sum_x += x
|
998
|
-
sum_y += y
|
999
|
-
end
|
1000
|
-
@a = (size * sum_xy - sum_x * sum_y) / (size * sum_xx - sum_x ** 2)
|
1001
|
-
@b = (sum_y - @a * sum_x) / size
|
1002
|
-
self
|
1003
|
-
end
|
1004
|
-
|
1005
|
-
def tvalue(alpha = 0.05)
|
1006
|
-
df = @image.size - 2
|
1007
|
-
return 0.0 if df <= 0
|
1008
|
-
sse_y = 0.0
|
1009
|
-
@domain.zip(@image) do |x, y|
|
1010
|
-
f_x = a * x + b
|
1011
|
-
sse_y += (y - f_x) ** 2
|
1012
|
-
end
|
1013
|
-
mean = @image.inject(0.0) { |s, y| s + y } / @image.size
|
1014
|
-
sse_x = @domain.inject(0.0) { |s, x| s + (x - mean) ** 2 }
|
1015
|
-
t = a / (Math.sqrt(sse_y / df) / Math.sqrt(sse_x))
|
1016
|
-
t.nan? ? 0.0 : t
|
1017
|
-
end
|
1018
|
-
end
|
1019
|
-
|
1020
|
-
# This class is used to analyse the time measurements and compute their
|
1021
|
-
# statistics.
|
1022
|
-
class Analysis
|
1023
|
-
def initialize(measurements)
|
1024
|
-
@measurements = measurements
|
1025
|
-
@measurements.freeze
|
1026
|
-
end
|
1027
|
-
|
1028
|
-
# Returns the array of measurements.
|
1029
|
-
attr_reader :measurements
|
1030
|
-
|
1031
|
-
# Returns the number of measurements, on which the analysis is based.
|
1032
|
-
def size
|
1033
|
-
@measurements.size
|
1034
|
-
end
|
1035
|
-
|
1036
|
-
# Returns the variance of the measurements.
|
1037
|
-
def variance
|
1038
|
-
@variance ||= sum_of_squares / size
|
1039
|
-
end
|
1040
|
-
|
1041
|
-
# Returns the sample_variance of the measurements.
|
1042
|
-
def sample_variance
|
1043
|
-
@sample_variance ||= size > 1 ? sum_of_squares / (size - 1.0) : 0.0
|
1044
|
-
end
|
1045
|
-
|
1046
|
-
# Returns the sum of squares (the sum of the squared deviations) of the
|
1047
|
-
# measurements.
|
1048
|
-
def sum_of_squares
|
1049
|
-
@sum_of_squares ||= @measurements.inject(0.0) { |s, t| s + (t - arithmetic_mean) ** 2 }
|
1050
|
-
end
|
1051
|
-
|
1052
|
-
# Returns the standard deviation of the measurements.
|
1053
|
-
def standard_deviation
|
1054
|
-
@sample_deviation ||= Math.sqrt(variance)
|
1055
|
-
end
|
1056
|
-
|
1057
|
-
# Returns the standard deviation of the measurements in percentage of the
|
1058
|
-
# arithmetic mean.
|
1059
|
-
def standard_deviation_percentage
|
1060
|
-
@standard_deviation_percentage ||= 100.0 * standard_deviation / arithmetic_mean
|
1061
|
-
end
|
1062
|
-
|
1063
|
-
# Returns the sample standard deviation of the measurements.
|
1064
|
-
def sample_standard_deviation
|
1065
|
-
@sample_standard_deviation ||= Math.sqrt(sample_variance)
|
1066
|
-
end
|
1067
|
-
|
1068
|
-
# Returns the sample standard deviation of the measurements in percentage
|
1069
|
-
# of the arithmetic mean.
|
1070
|
-
def sample_standard_deviation_percentage
|
1071
|
-
@sample_standard_deviation_percentage ||= 100.0 * sample_standard_deviation / arithmetic_mean
|
1072
|
-
end
|
1073
|
-
|
1074
|
-
# Returns the sum of all measurements.
|
1075
|
-
def sum
|
1076
|
-
@sum ||= @measurements.inject(0.0) { |s, t| s + t }
|
1077
|
-
end
|
1078
|
-
|
1079
|
-
# Returns the arithmetic mean of the measurements.
|
1080
|
-
def arithmetic_mean
|
1081
|
-
@arithmetic_mean ||= sum / size
|
1082
|
-
end
|
1083
|
-
|
1084
|
-
alias mean arithmetic_mean
|
1085
|
-
|
1086
|
-
# Returns the harmonic mean of the measurements. If any of the measurements
|
1087
|
-
# is less than or equal to 0.0, this method returns NaN.
|
1088
|
-
def harmonic_mean
|
1089
|
-
@harmonic_mean ||= (
|
1090
|
-
sum = @measurements.inject(0.0) { |s, t|
|
1091
|
-
if t > 0
|
1092
|
-
s + 1.0 / t
|
1093
|
-
else
|
1094
|
-
break nil
|
1095
|
-
end
|
1096
|
-
}
|
1097
|
-
sum ? size / sum : 0 / 0.0
|
1098
|
-
)
|
1099
|
-
end
|
1100
|
-
|
1101
|
-
# Returns the geometric mean of the measurements. If any of the
|
1102
|
-
# measurements is less than 0.0, this method returns NaN.
|
1103
|
-
def geometric_mean
|
1104
|
-
@geometric_mean ||= (
|
1105
|
-
sum = @measurements.inject(0.0) { |s, t|
|
1106
|
-
case
|
1107
|
-
when t > 0
|
1108
|
-
s + Math.log(t)
|
1109
|
-
when t == 0
|
1110
|
-
break :null
|
1111
|
-
else
|
1112
|
-
break nil
|
1113
|
-
end
|
1114
|
-
}
|
1115
|
-
case sum
|
1116
|
-
when :null
|
1117
|
-
0.0
|
1118
|
-
when Float
|
1119
|
-
Math.exp(sum / size)
|
1120
|
-
else
|
1121
|
-
0 / 0.0
|
1122
|
-
end
|
1123
|
-
)
|
1124
|
-
end
|
1125
|
-
|
1126
|
-
# Returns the minimum of the measurements.
|
1127
|
-
def min
|
1128
|
-
@min ||= @measurements.min
|
1129
|
-
end
|
1130
|
-
|
1131
|
-
# Returns the maximum of the measurements.
|
1132
|
-
def max
|
1133
|
-
@max ||= @measurements.max
|
1134
|
-
end
|
1135
|
-
|
1136
|
-
# Returns the +p+-percentile of the measurements.
|
1137
|
-
# There are many methods to compute the percentile, this method uses the
|
1138
|
-
# the weighted average at x_(n + 1)p, which allows p to be in 0...100
|
1139
|
-
# (excluding the 100).
|
1140
|
-
def percentile(p = 50)
|
1141
|
-
(0...100).include?(p) or
|
1142
|
-
raise ArgumentError, "p = #{p}, but has to be in (0...100)"
|
1143
|
-
p /= 100.0
|
1144
|
-
@sorted ||= @measurements.sort
|
1145
|
-
r = p * (@sorted.size + 1)
|
1146
|
-
r_i = r.to_i
|
1147
|
-
r_f = r - r_i
|
1148
|
-
if r_i >= 1
|
1149
|
-
result = @sorted[r_i - 1]
|
1150
|
-
if r_i < @sorted.size
|
1151
|
-
result += r_f * (@sorted[r_i] - @sorted[r_i - 1])
|
1152
|
-
end
|
1153
|
-
else
|
1154
|
-
result = @sorted[0]
|
1155
|
-
end
|
1156
|
-
result
|
1157
|
-
end
|
1158
|
-
|
1159
|
-
alias median percentile
|
1160
|
-
|
1161
|
-
# Use an approximation of the Welch-Satterthwaite equation to compute the
|
1162
|
-
# degrees of freedom for Welch's t-test.
|
1163
|
-
def compute_welch_df(other)
|
1164
|
-
(sample_variance / size + other.sample_variance / other.size) ** 2 / (
|
1165
|
-
(sample_variance ** 2 / (size ** 2 * (size - 1))) +
|
1166
|
-
(other.sample_variance ** 2 / (other.size ** 2 * (other.size - 1))))
|
1167
|
-
end
|
1168
|
-
|
1169
|
-
# Returns the t value of the Welch's t-test between this Analysis
|
1170
|
-
# instance and the +other+.
|
1171
|
-
def t_welch(other)
|
1172
|
-
signal = arithmetic_mean - other.arithmetic_mean
|
1173
|
-
noise = Math.sqrt(sample_variance / size +
|
1174
|
-
other.sample_variance / other.size)
|
1175
|
-
signal / noise
|
1176
|
-
rescue Errno::EDOM
|
1177
|
-
0.0
|
1178
|
-
end
|
1179
|
-
|
1180
|
-
# Returns an estimation of the common standard deviation of the
|
1181
|
-
# measurements of this and +other+.
|
1182
|
-
def common_standard_deviation(other)
|
1183
|
-
Math.sqrt(common_variance(other))
|
1184
|
-
end
|
1185
|
-
|
1186
|
-
# Returns an estimation of the common variance of the measurements of this
|
1187
|
-
# and +other+.
|
1188
|
-
def common_variance(other)
|
1189
|
-
(size - 1) * sample_variance + (other.size - 1) * other.sample_variance /
|
1190
|
-
(size + other.size - 2)
|
1191
|
-
end
|
1192
|
-
|
1193
|
-
# Compute the # degrees of freedom for Student's t-test.
|
1194
|
-
def compute_student_df(other)
|
1195
|
-
size + other.size - 2
|
1196
|
-
end
|
1197
|
-
|
1198
|
-
# Returns the t value of the Student's t-test between this Analysis
|
1199
|
-
# instance and the +other+.
|
1200
|
-
def t_student(other)
|
1201
|
-
signal = arithmetic_mean - other.arithmetic_mean
|
1202
|
-
noise = common_standard_deviation(other) *
|
1203
|
-
Math.sqrt(size ** -1 + size ** -1)
|
1204
|
-
rescue Errno::EDOM
|
1205
|
-
0.0
|
1206
|
-
end
|
1207
|
-
|
1208
|
-
# Compute a sample size, that will more likely yield a mean difference
|
1209
|
-
# between this instance's measurements and those of +other+. Use +alpha+
|
1210
|
-
# and +beta+ as levels for the first- and second-order errors.
|
1211
|
-
def suggested_sample_size(other, alpha = 0.05, beta = 0.05)
|
1212
|
-
alpha, beta = alpha.abs, beta.abs
|
1213
|
-
signal = arithmetic_mean - other.arithmetic_mean
|
1214
|
-
df = size + other.size - 2
|
1215
|
-
pooled_variance_estimate = (sum_of_squares + other.sum_of_squares) / df
|
1216
|
-
td = TDistribution.new df
|
1217
|
-
(((td.inverse_probability(alpha) + td.inverse_probability(beta)) *
|
1218
|
-
Math.sqrt(pooled_variance_estimate)) / signal) ** 2
|
1219
|
-
end
|
1220
|
-
|
1221
|
-
# Return true, if the Analysis instance covers the +other+, that is their
|
1222
|
-
# arithmetic mean value is most likely to be equal for the +alpha+ error
|
1223
|
-
# level.
|
1224
|
-
def cover?(other, alpha = 0.05)
|
1225
|
-
t = t_welch(other)
|
1226
|
-
td = TDistribution.new(compute_welch_df(other))
|
1227
|
-
t.abs < td.inverse_probability(1 - alpha.abs / 2.0)
|
1228
|
-
end
|
1229
|
-
|
1230
|
-
# Return the confidence interval for the arithmetic mean with alpha level +alpha+ of
|
1231
|
-
# the measurements of this Analysis instance as a Range object.
|
1232
|
-
def confidence_interval(alpha = 0.05)
|
1233
|
-
td = TDistribution.new(size - 1)
|
1234
|
-
t = td.inverse_probability(alpha / 2).abs
|
1235
|
-
delta = t * sample_standard_deviation / Math.sqrt(size)
|
1236
|
-
(arithmetic_mean - delta)..(arithmetic_mean + delta)
|
1237
|
-
end
|
1238
|
-
|
1239
|
-
# Returns the array of autovariances (of length size - 1).
|
1240
|
-
def autovariance
|
1241
|
-
Array.new(size - 1) do |k|
|
1242
|
-
s = 0.0
|
1243
|
-
0.upto(size - k - 1) do |i|
|
1244
|
-
s += (@measurements[i] - arithmetic_mean) * (@measurements[i + k] - arithmetic_mean)
|
1245
|
-
end
|
1246
|
-
s / size
|
1247
|
-
end
|
1248
|
-
end
|
1249
|
-
|
1250
|
-
# Returns the array of autocorrelation values c_k / c_0 (of length size -
|
1251
|
-
# 1).
|
1252
|
-
def autocorrelation
|
1253
|
-
c = autovariance
|
1254
|
-
Array.new(c.size) { |k| c[k] / c[0] }
|
1255
|
-
end
|
1256
|
-
|
1257
|
-
# Returns the d-value for the Durbin-Watson statistic. The value is d << 2
|
1258
|
-
# for positive, d >> 2 for negative and d around 2 for no autocorrelation.
|
1259
|
-
def durbin_watson_statistic
|
1260
|
-
e = linear_regression.residues
|
1261
|
-
e.size <= 1 and return 2.0
|
1262
|
-
(1...e.size).inject(0.0) { |s, i| s + (e[i] - e[i - 1]) ** 2 } /
|
1263
|
-
e.inject(0.0) { |s, x| s + x ** 2 }
|
1264
|
-
end
|
1265
|
-
|
1266
|
-
# Returns the q value of the Ljung-Box statistic for the number of lags
|
1267
|
-
# +lags+. A higher value might indicate autocorrelation in the measurements of
|
1268
|
-
# this Analysis instance. This method returns nil if there weren't enough
|
1269
|
-
# (at least lags) lags available.
|
1270
|
-
def ljung_box_statistic(lags = 20)
|
1271
|
-
r = autocorrelation
|
1272
|
-
lags >= r.size and return
|
1273
|
-
n = size
|
1274
|
-
n * (n + 2) * (1..lags).inject(0.0) { |s, i| s + r[i] ** 2 / (n - i) }
|
1275
|
-
end
|
1276
|
-
|
1277
|
-
# This method tries to detect autocorrelation with the Ljung-Box
|
1278
|
-
# statistic. If enough lags can be considered it returns a hash with
|
1279
|
-
# results, otherwise nil is returned. The keys are
|
1280
|
-
# :lags:: the number of lags,
|
1281
|
-
# :alpha_level:: the alpha level for the test,
|
1282
|
-
# :q:: the value of the ljung_box_statistic,
|
1283
|
-
# :p:: the p-value computed, if p is higher than alpha no correlation was detected,
|
1284
|
-
# :detected:: true if a correlation was found.
|
1285
|
-
def detect_autocorrelation(lags = 20, alpha_level = 0.05)
|
1286
|
-
if q = ljung_box_statistic(lags)
|
1287
|
-
p = ChiSquareDistribution.new(lags).probability(q)
|
1288
|
-
return {
|
1289
|
-
:lags => lags,
|
1290
|
-
:alpha_level => alpha_level,
|
1291
|
-
:q => q,
|
1292
|
-
:p => p,
|
1293
|
-
:detected => p >= 1 - alpha_level,
|
1294
|
-
}
|
1295
|
-
end
|
1296
|
-
end
|
1297
|
-
|
1298
|
-
# Return a result hash with the number of :very_low, :low, :high, and
|
1299
|
-
# :very_high outliers, determined by the box plotting algorithm run with
|
1300
|
-
# :median and :iqr parameters. If no outliers were found or the iqr is
|
1301
|
-
# less than epsilon, nil is returned.
|
1302
|
-
def detect_outliers(factor = 3.0, epsilon = 1E-5)
|
1303
|
-
half_factor = factor / 2.0
|
1304
|
-
quartile1 = percentile(25)
|
1305
|
-
quartile3 = percentile(75)
|
1306
|
-
iqr = quartile3 - quartile1
|
1307
|
-
iqr < epsilon and return
|
1308
|
-
result = @measurements.inject(Hash.new(0)) do |h, t|
|
1309
|
-
extreme =
|
1310
|
-
case t
|
1311
|
-
when -Infinity..(quartile1 - factor * iqr)
|
1312
|
-
:very_low
|
1313
|
-
when (quartile1 - factor * iqr)..(quartile1 - half_factor * iqr)
|
1314
|
-
:low
|
1315
|
-
when (quartile1 + half_factor * iqr)..(quartile3 + factor * iqr)
|
1316
|
-
:high
|
1317
|
-
when (quartile3 + factor * iqr)..Infinity
|
1318
|
-
:very_high
|
1319
|
-
end and h[extreme] += 1
|
1320
|
-
h
|
1321
|
-
end
|
1322
|
-
unless result.empty?
|
1323
|
-
result[:median] = median
|
1324
|
-
result[:iqr] = iqr
|
1325
|
-
result[:factor] = factor
|
1326
|
-
result
|
1327
|
-
end
|
1328
|
-
end
|
1329
|
-
|
1330
|
-
# Returns the LinearRegression object for the equation a * x + b which
|
1331
|
-
# represents the line computed by the linear regression algorithm.
|
1332
|
-
def linear_regression
|
1333
|
-
@linear_regression ||= LinearRegression.new @measurements
|
1334
|
-
end
|
1335
|
-
|
1336
|
-
# Returns a Histogram instance with +bins+ as the number of bins for this
|
1337
|
-
# analysis' measurements.
|
1338
|
-
def histogram(bins)
|
1339
|
-
Histogram.new(self, bins)
|
1340
|
-
end
|
1341
|
-
end
|
1342
|
-
|
1343
410
|
CaseMethod = Struct.new(:name, :case, :clock)
|
1344
411
|
|
1345
412
|
# This class' instance represents a method to be benchmarked.
|