experiment 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +5 -3
- data/{README.rdoc → README.md} +26 -57
- data/Rakefile +3 -1
- data/bin/experiment +28 -9
- data/lib/experiment/base.rb +165 -65
- data/lib/experiment/config.rb +140 -11
- data/lib/experiment/distributed.rb +51 -20
- data/lib/experiment/factorial.rb +227 -0
- data/lib/experiment/generator/{experiment_template.rb → experiment_template.rb.txt} +10 -10
- data/lib/experiment/generator/readme_template.txt +2 -2
- data/lib/experiment/notify.rb +150 -146
- data/lib/experiment/params.rb +18 -0
- data/lib/experiment/runner.rb +50 -8
- data/lib/experiment/stats/descriptive.rb +58 -0
- data/lib/experiment/work_server.rb +20 -5
- data/lib/experiment.rb +1 -1
- data/test/test_stats.rb +9 -3
- metadata +12 -9
- data/lib/experiment/stats.rb +0 -43
data/lib/experiment/notify.rb
CHANGED
@@ -1,180 +1,184 @@
|
|
1
|
-
# This class is responsible for UI goodness in letting you know
|
2
|
-
# about the progress of your experiments
|
3
1
|
require "drb/drb"
|
4
|
-
|
2
|
+
module Experiment
|
3
|
+
# This class is responsible for UI goodness in letting you know
|
4
|
+
# about the progress of your experiments
|
5
|
+
# @private
|
6
|
+
class Notify
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
8
|
+
class << self
|
9
|
+
include DRb::DRbUndumped
|
10
|
+
|
11
|
+
# initialize display
|
12
|
+
def init(total, out = STDERR, growl = true, mode = :normal)
|
13
|
+
@curent_experiment = ""
|
14
|
+
@current_cv = 0
|
15
|
+
@cv_prog = {}
|
16
|
+
@total = total
|
17
|
+
@out = out
|
18
|
+
@terminal_width = 80
|
19
|
+
@bar_mark = "o"
|
20
|
+
@current = 0
|
21
|
+
@previous = 0
|
22
|
+
@finished_p = false
|
23
|
+
@start_time = Time.now
|
24
|
+
@previous_time = @start_time
|
25
|
+
@growl = growl
|
26
|
+
@mode = mode
|
27
|
+
show if @mode == :normal
|
28
|
+
end
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
# Called when starting work on a particular experiment
|
31
|
+
def started(experiment)
|
32
|
+
@curent_experiment = experiment
|
33
|
+
@current_cv = 1
|
34
|
+
@cv_prog[experiment] = []
|
35
|
+
show_if_needed
|
36
|
+
end
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
38
|
+
# Called when experiment completed.
|
39
|
+
# Shows a Growl notification on OSX.
|
40
|
+
# The message can be expanded by overriding the result_line
|
41
|
+
# method in the experiment class
|
42
|
+
def completed(experiment, msg = "")
|
43
|
+
if @growl
|
44
|
+
begin
|
45
|
+
`G_TITLE="Experiment Complete" #{File.dirname(__FILE__)}/../../bin/growl.sh -nosticky "Experimental condition #{experiment} complete. #{msg}"`
|
46
|
+
rescue
|
47
|
+
# probably not on OSX
|
48
|
+
end
|
45
49
|
end
|
50
|
+
m = "Condition #{experiment} complete. #{msg}"
|
51
|
+
puts m + " " * @terminal_width
|
52
|
+
@curent_experiment = nil
|
46
53
|
end
|
47
|
-
m = "Condition #{experiment} complete. #{msg}"
|
48
|
-
puts m + " " * @terminal_width
|
49
|
-
@curent_experiment = nil
|
50
|
-
end
|
51
54
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
# called after a crossvalidation has completed
|
56
|
+
def cv_done(experiment, num)
|
57
|
+
@cv_prog[experiment][num] ||= 0
|
58
|
+
inc(1 - @cv_prog[experiment][num])
|
59
|
+
#@cv_prog = 0
|
60
|
+
end
|
58
61
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
62
|
+
# Wrap up
|
63
|
+
def done
|
64
|
+
@current = @total
|
65
|
+
@finished_p = true
|
66
|
+
#show
|
67
|
+
end
|
65
68
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
69
|
+
# Use this in experiment after each (potentially time consuming) task
|
70
|
+
# The argument should be a fraction (0 < num < 1) which tells
|
71
|
+
# how big a portion the task was of the complete run (eg. your
|
72
|
+
# calls should sum up to 1).
|
73
|
+
def step(experiment, cv, num)
|
74
|
+
if @mode == :normal
|
75
|
+
if num > 1
|
76
|
+
num = num / 100
|
77
|
+
end
|
78
|
+
inc(num)
|
79
|
+
@cv_prog[experiment][cv] ||= 0
|
80
|
+
@cv_prog[experiment][cv] += num
|
81
|
+
else
|
82
|
+
@mode.notify.step(experiment, cv, num)
|
74
83
|
end
|
75
|
-
inc(num)
|
76
|
-
@cv_prog[experiment][cv] ||= 0
|
77
|
-
@cv_prog[experiment][cv] += num
|
78
|
-
else
|
79
|
-
@mode.notify.step(experiment, cv, num)
|
80
84
|
end
|
81
|
-
end
|
82
85
|
|
83
|
-
end
|
84
|
-
|
85
|
-
# a big part of this module is copied/inspired by Satoru Takabayashi's <satoru@namazu.org> ProgressBar class at http://0xcc.net/ruby-progressbar/index.html.en
|
86
|
-
module ProgressBar #:nodoc
|
87
|
-
def inc(step = 1)
|
88
|
-
@current += step
|
89
|
-
@current = @total if @current > @total
|
90
|
-
show_if_needed
|
91
|
-
@previous = @current
|
92
86
|
end
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
87
|
+
|
88
|
+
# a big part of this module is copied/inspired by Satoru Takabayashi's <satoru@namazu.org> ProgressBar class at http://0xcc.net/ruby-progressbar/index.html.en
|
89
|
+
module ProgressBar #:nodoc
|
90
|
+
def inc(step = 1)
|
91
|
+
@current += step
|
92
|
+
@current = @total if @current > @total
|
93
|
+
show_if_needed
|
94
|
+
@previous = @current
|
101
95
|
end
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
96
|
+
|
97
|
+
def show_if_needed
|
98
|
+
if @total.zero?
|
99
|
+
cur_percentage = 100
|
100
|
+
prev_percentage = 0
|
101
|
+
else
|
102
|
+
cur_percentage = (@current * 100 / @total).to_i
|
103
|
+
prev_percentage = (@previous * 100 / @total).to_i
|
104
|
+
end
|
105
|
+
@finished_p = cur_percentage == 100
|
106
|
+
# Use "!=" instead of ">" to support negative changes
|
107
|
+
if cur_percentage != prev_percentage ||
|
108
|
+
Time.now - @previous_time >= 1 || @finished_p
|
109
|
+
show
|
110
|
+
end
|
107
111
|
end
|
108
|
-
end
|
109
112
|
|
110
113
|
|
111
114
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
115
|
+
def show
|
116
|
+
percent = @current * 100 / @total
|
117
|
+
bar_width = percent * @terminal_width / 100
|
118
|
+
line = sprintf "%3d%% |%s%s| %s", percent, "=" * bar_width, "-" * (@terminal_width - bar_width), stat
|
116
119
|
|
117
120
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
121
|
+
width = get_width
|
122
|
+
if line.length == width - 1
|
123
|
+
@out.print(line + (@finished_p ? "\n" : "\r"))
|
124
|
+
@out.flush
|
125
|
+
elsif line.length >= width
|
126
|
+
@terminal_width = [@terminal_width - (line.length - width + 1), 0].max
|
127
|
+
if @terminal_width == 0 then @out.print(line + eol) else show end
|
128
|
+
else # line.length < width - 1
|
129
|
+
@terminal_width += width - line.length + 1
|
130
|
+
show
|
131
|
+
end
|
132
|
+
@previous_time = Time.now
|
128
133
|
end
|
129
|
-
@previous_time = Time.now
|
130
|
-
end
|
131
134
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
+
def stat
|
136
|
+
if @finished_p then elapsed else eta end
|
137
|
+
end
|
135
138
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
139
|
+
def eta
|
140
|
+
if @current == 0
|
141
|
+
"ETA: --:--:--"
|
142
|
+
else
|
143
|
+
elapsed = Time.now - @start_time
|
144
|
+
eta = elapsed * @total / @current - elapsed;
|
145
|
+
sprintf("ETA: %s", format_time(eta))
|
146
|
+
end
|
143
147
|
end
|
144
|
-
end
|
145
148
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
149
|
+
def elapsed
|
150
|
+
elapsed = Time.now - @start_time
|
151
|
+
sprintf("Time: %s", format_time(elapsed))
|
152
|
+
end
|
150
153
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
154
|
+
def format_time (t)
|
155
|
+
t = t.to_i
|
156
|
+
sec = t % 60
|
157
|
+
min = (t / 60) % 60
|
158
|
+
hour = t / 3600
|
159
|
+
sprintf("%02d:%02d:%02d", hour, min, sec);
|
160
|
+
end
|
158
161
|
|
159
162
|
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
163
|
+
def get_width
|
164
|
+
# FIXME: I don't know how portable it is.
|
165
|
+
default_width = 80
|
166
|
+
begin
|
167
|
+
tiocgwinsz = 0x5413
|
168
|
+
data = [0, 0, 0, 0].pack("SSSS")
|
169
|
+
if @out.ioctl(tiocgwinsz, data) >= 0 then
|
170
|
+
rows, cols, xpixels, ypixels = data.unpack("SSSS")
|
171
|
+
if cols >= 0 then cols else default_width end
|
172
|
+
else
|
173
|
+
default_width
|
174
|
+
end
|
175
|
+
rescue Exception
|
170
176
|
default_width
|
171
177
|
end
|
172
|
-
rescue Exception
|
173
|
-
default_width
|
174
178
|
end
|
175
|
-
|
176
|
-
end
|
179
|
+
end
|
177
180
|
|
178
|
-
|
181
|
+
extend ProgressBar
|
179
182
|
|
183
|
+
end
|
180
184
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Experiment
|
2
|
+
class Params
|
3
|
+
|
4
|
+
# Return if set the value of the current param.
|
5
|
+
#
|
6
|
+
# If it is not defined fallback to {Experiment::Config#[]}.
|
7
|
+
def self.[](h)
|
8
|
+
@@params[h] || Config[h]
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# @private
|
13
|
+
def self.set(a) # :nodoc:
|
14
|
+
@@params = a
|
15
|
+
end
|
16
|
+
end
|
17
|
+
Params.set({})
|
18
|
+
end
|
data/lib/experiment/runner.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/config"
|
1
2
|
module Experiment
|
2
3
|
|
3
|
-
# This is the class behind the command line magic
|
4
|
+
# This is the class behind the command line magic.
|
5
|
+
# It is possible to use it programatically, though.
|
6
|
+
# @see https://github.com/gampleman/Experiment/wiki/Command-Line-Interface
|
7
|
+
# @example For documentation on the CLI run
|
8
|
+
# experiment -h
|
4
9
|
class Runner
|
5
10
|
|
6
11
|
attr_reader :options
|
7
12
|
|
13
|
+
# If you are using this programmatically you need to set these params correctly:
|
14
|
+
# @param [Array<String>] arg Typically the name of the experiment the operation
|
15
|
+
# needs to operate on.
|
16
|
+
# @param [Struct, OpenStruct] opt an options object that should respond according
|
17
|
+
# to the CLI.
|
8
18
|
def initialize(arg, opt)
|
9
19
|
@arguments, @options = arg, opt
|
10
20
|
end
|
@@ -51,13 +61,13 @@ module Experiment
|
|
51
61
|
end
|
52
62
|
FileUtils::cp File.join(basedir, "generator/readme_template.txt"), File.join(dir, "README")
|
53
63
|
FileUtils::cp File.join(basedir, "generator/Rakefile"), File.join(dir, "Rakefile")
|
54
|
-
FileUtils::cp File.join(basedir, "generator/experiment_template.rb"), File.join(dir, "experiments", "experiment.rb")
|
64
|
+
FileUtils::cp File.join(basedir, "generator/experiment_template.rb.txt"), File.join(dir, "experiments", "experiment.rb")
|
55
65
|
end
|
56
66
|
|
57
67
|
# Lists available experiments
|
58
68
|
def list
|
59
69
|
puts "Available experiments:"
|
60
|
-
puts " " + Dir["./experiments/*"].map{|a| File.
|
70
|
+
puts " " + Dir["./experiments/*"].map{|a| File.dirname(a) }.join(", ")
|
61
71
|
end
|
62
72
|
|
63
73
|
# Generates 2 files in the report directory
|
@@ -109,14 +119,14 @@ module Experiment
|
|
109
119
|
end
|
110
120
|
|
111
121
|
|
112
|
-
# runs experiments passed
|
122
|
+
# runs experiments passed as arguments
|
113
123
|
# use the -o option to override configuration
|
114
124
|
def run
|
115
125
|
require File.dirname(__FILE__) + "/base"
|
116
126
|
|
117
127
|
require "./experiments/experiment"
|
118
128
|
Experiment::Config::init @options.env
|
119
|
-
|
129
|
+
@options.cv = Experiment::Config.get :cross_validations, 5 if @options.cv.nil?
|
120
130
|
if @options.distributed
|
121
131
|
require "drb/drb"
|
122
132
|
require File.dirname(__FILE__) + "/work_server"
|
@@ -130,15 +140,45 @@ module Experiment
|
|
130
140
|
@arguments.each do |exp|
|
131
141
|
require "./experiments/#{exp}/#{exp}"
|
132
142
|
cla = eval(as_class_name(exp))
|
133
|
-
experiment = cla.new :normal, exp, @options
|
143
|
+
experiment = cla.new :normal, exp, @options
|
134
144
|
experiment.normal_run! @options.cv
|
135
145
|
end
|
136
146
|
Notify::done
|
137
147
|
end
|
138
148
|
end
|
139
149
|
|
150
|
+
# Creates an IRB console useful for debugging experiments
|
151
|
+
# Loads up the environment for the condition passed
|
152
|
+
def console
|
153
|
+
cla = as_class_name(@arguments.first) if @arguments.length == 1
|
154
|
+
File.open("./tmp/irb-setup.rb", 'w') do |f|
|
155
|
+
f.puts "Experiment::Config::init #{@options.env.inspect}"
|
156
|
+
f.puts "def reload!"
|
157
|
+
f.puts " "
|
158
|
+
f.puts "end"
|
159
|
+
if @arguments.length == 1
|
160
|
+
f.puts "def experiment"
|
161
|
+
f.puts " @experiment ||= #{cla}.new :normal, #{@arguments.first.inspect}, OpenStruct.new(#{@options.marshal_dump})"
|
162
|
+
f.puts "end"
|
163
|
+
f.puts "experiment #load up the configs"
|
164
|
+
else
|
165
|
+
f.puts 'Dir["./app/*.rb"].each{|e| require e }'
|
166
|
+
f.puts "Experiment::Config::load '', #{options.opts.inspect}"
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
|
171
|
+
libs = " -r irb/completion"
|
172
|
+
libs << " -r #{File.dirname(__FILE__) + "/base"}"
|
173
|
+
libs << " -r./experiments/experiment"
|
174
|
+
libs << " -r ./experiments/#{@arguments.first}/#{@arguments.first}" if @arguments.length == 1
|
175
|
+
libs << " -r ./tmp/irb-setup.rb"
|
176
|
+
puts "Loading #{@options.env} environment..."
|
177
|
+
exec "#{irb} #{libs} --simple-prompt"
|
178
|
+
end
|
179
|
+
|
140
180
|
|
141
|
-
#
|
181
|
+
# Starts a Worker implementation. It requires an --address option
|
142
182
|
# of it's master server and will recieve tasks (experiments and
|
143
183
|
# cross-validations) and compute them.
|
144
184
|
def worker
|
@@ -155,13 +195,15 @@ module Experiment
|
|
155
195
|
require "./experiments/experiment"
|
156
196
|
require "./experiments/#{exp}/#{exp}"
|
157
197
|
cla = eval(as_class_name(exp))
|
158
|
-
experiment = cla.new :slave, exp, @options
|
198
|
+
experiment = cla.new :slave, exp, @options
|
159
199
|
experiment.master = @master.instance item
|
160
200
|
experiment.slave_run!
|
161
201
|
end
|
162
202
|
end
|
163
203
|
end
|
164
204
|
|
205
|
+
|
206
|
+
|
165
207
|
private
|
166
208
|
|
167
209
|
require 'socket'
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Experiment
|
2
|
+
module Stats
|
3
|
+
module Descriptive
|
4
|
+
|
5
|
+
def sum(ar = self, &block)
|
6
|
+
ar.reduce(0.0) {|asum, a| (block_given? ? yield(a) : a) + asum}
|
7
|
+
end
|
8
|
+
|
9
|
+
def variance(ar = self)
|
10
|
+
v = sum(ar) {|x| (mean(ar) - x)**2.0 }
|
11
|
+
v/(ar.count - 1.0)
|
12
|
+
end
|
13
|
+
|
14
|
+
def standard_deviation(ar = self)
|
15
|
+
Math.sqrt(variance(ar))
|
16
|
+
end
|
17
|
+
|
18
|
+
def z_scores(ar = self)
|
19
|
+
ar.map {|x| z_score(ar, x)}
|
20
|
+
end
|
21
|
+
|
22
|
+
def z_score(ar = self, x)
|
23
|
+
(x - mean(ar)) / standard_deviation(ar)
|
24
|
+
end
|
25
|
+
|
26
|
+
def range(ar = self)
|
27
|
+
ar.max - ar.min
|
28
|
+
end
|
29
|
+
|
30
|
+
def mean(ar = self)
|
31
|
+
sum(ar) / ar.count
|
32
|
+
end
|
33
|
+
|
34
|
+
def median(ar = self)
|
35
|
+
a = ar.sort
|
36
|
+
if ar.count.odd?
|
37
|
+
a[(ar.count-1)/2]
|
38
|
+
else
|
39
|
+
(a[ar.count/2 - 1] + a[ar.count/2]) / 2.0
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
# Monkey pathces the Array class to accept the methods in this class
|
47
|
+
# as it's own - so instead of `Stats::variance([1, 2, 3])`
|
48
|
+
# you can call [1, 2, 3].variance
|
49
|
+
def monkey_patch!
|
50
|
+
Array.send :include, Descriptive
|
51
|
+
end
|
52
|
+
|
53
|
+
include Descriptive
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -1,4 +1,8 @@
|
|
1
1
|
module Experiment
|
2
|
+
# This class is responsible for disrtibuting work
|
3
|
+
# and instantiating experimental conditions according
|
4
|
+
# to available resources
|
5
|
+
# @private
|
2
6
|
class WorkServer
|
3
7
|
def initialize(experiments, options, ip = "localhost")
|
4
8
|
uri="druby://#{ip}:8787"
|
@@ -15,20 +19,29 @@ module Experiment
|
|
15
19
|
DRb.thread.join
|
16
20
|
end
|
17
21
|
|
18
|
-
|
22
|
+
# @deprecated
|
23
|
+
def ready? # TODO: get rid of this
|
19
24
|
true
|
20
25
|
end
|
21
26
|
|
27
|
+
# Workers call this method and recieve a new work object incrementally
|
22
28
|
def new_item
|
23
29
|
@experiments.each_with_index do |e, i|
|
24
30
|
if @experiment_instances[i].nil?
|
25
31
|
exp = @experiments[i]
|
26
32
|
require "./experiments/#{exp}/#{exp}"
|
27
33
|
cla = eval(as_class_name(exp))
|
28
|
-
experiment = cla.new :master, exp, @options
|
29
|
-
experiment.
|
30
|
-
|
31
|
-
|
34
|
+
experiment = cla.new :master, exp, @options
|
35
|
+
if experiment.respond_to? :master_sub_experiments
|
36
|
+
subs = experiment.master_sub_experiments @options.cv
|
37
|
+
@experiments += subs.map { exp }
|
38
|
+
@experiment_instances += subs
|
39
|
+
return i + 1
|
40
|
+
else
|
41
|
+
experiment.master_run! @options.cv
|
42
|
+
@experiment_instances[i] = experiment
|
43
|
+
return i
|
44
|
+
end
|
32
45
|
elsif !@experiment_instances[i].distribution_done?
|
33
46
|
return i
|
34
47
|
end
|
@@ -38,10 +51,12 @@ module Experiment
|
|
38
51
|
false
|
39
52
|
end
|
40
53
|
|
54
|
+
# accessor for the remote notification service
|
41
55
|
def notify
|
42
56
|
Notify
|
43
57
|
end
|
44
58
|
|
59
|
+
|
45
60
|
def experiment(num)
|
46
61
|
@experiments[num]
|
47
62
|
end
|
data/lib/experiment.rb
CHANGED
data/test/test_stats.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
require File.dirname(__FILE__) + "/../lib/experiment/stats"
|
2
|
-
#require "wrong"
|
1
|
+
require File.dirname(__FILE__) + "/../lib/experiment/stats/descriptive"
|
2
|
+
#require "wrong"require "../lib/experiment/stats/descriptive"
|
3
|
+
|
3
4
|
class TestStats < Test::Unit::TestCase
|
4
5
|
#include Wrong
|
5
|
-
|
6
|
+
include Experiment
|
6
7
|
def setup
|
7
8
|
@data = [1, 2, 3, 4]
|
8
9
|
end
|
@@ -31,4 +32,9 @@ class TestStats < Test::Unit::TestCase
|
|
31
32
|
def test_median
|
32
33
|
assert_equal 2.5, Stats::median(@data)
|
33
34
|
end
|
35
|
+
|
36
|
+
def test_monkey_patch
|
37
|
+
Stats::monkey_patch!
|
38
|
+
assert_equal 1.6666666666666667, [1, 2, 3, 4].variance
|
39
|
+
end
|
34
40
|
end
|