experiment 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|