planout 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 81d07087a7e4796827f84836ff455d897bb2cc9d
4
+ data.tar.gz: e859012c622ca1377b99005011209b9086216fda
5
+ SHA512:
6
+ metadata.gz: 6b9c798846c42b70a9e58b34817f0637086e06c0327e17e337faf5296ecf19b72fdc6682e622ac4fb1c4ad37f3245346e978839ce2de255cb5e44c68e53a63d7
7
+ data.tar.gz: f53ebe3dd097c406ecb793a70b82f8f22e7c51b525c8e2386e021d023e4ab1719535607386334382e52c98e8800378c082cbbe0c7ca8a3377808cb3a145f1306
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /backup/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
16
+ .DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in planout.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ BSD License
2
+
3
+ For PlanOut software
4
+
5
+ Copyright (c) 2014, Facebook, Inc. All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without modification,
8
+ are permitted provided that the following conditions are met:
9
+
10
+ * Redistributions of source code must retain the above copyright notice, this
11
+ list of conditions and the following disclaimer.
12
+
13
+ * Redistributions in binary form must reproduce the above copyright notice,
14
+ this list of conditions and the following disclaimer in the documentation
15
+ and/or other materials provided with the distribution.
16
+
17
+ * Neither the name Facebook nor the names of its contributors may be used to
18
+ endorse or promote products derived from this software without specific
19
+ prior written permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
22
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
23
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
25
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
26
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ ## Overview
2
+ This is a rough implementation of the Experiment / logging infrasture for running PlanOut experiments, with all the random assignment operators available in Python. This port is nearly a line-by-line port, and produces assignments that are completely consistent with those produced by the Python reference implementation.
3
+
4
+ ## Installation
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'planout'
9
+ ```
10
+
11
+ And then execute:
12
+ ```bash
13
+ $ bundle
14
+ ```
15
+
16
+ Or install it yourself as:
17
+ ```bash
18
+ $ gem install planout
19
+ ```
20
+
21
+ ## How it works
22
+
23
+ This defines a simple experiment that randomly assigns three variables, foo, bar, and baz.
24
+ `foo` and `baz` use `userid` as input, while `bar` uses a pair, namely `userid` combined with the value of `foo` from the prior step.
25
+ ```Ruby
26
+ class VotingExperiment < SimpleExperiment
27
+ # Experiment#assign takes params and an input array
28
+ def assign(params, **inputs)
29
+ userid = inputs[:userid]
30
+
31
+ params[:button_color] = UniformChoice.new({
32
+ choices: ['ff0000', '#00ff00'],
33
+ unit: userid
34
+ })
35
+
36
+ params[:button_text] = UniformChoice.new({
37
+ choices: ["I'm voting", "I'm a voter"],
38
+ unit: userid,
39
+ salt:'x'
40
+ })
41
+ end
42
+ end
43
+ ```
44
+
45
+ Then, we can examine the assignments produced for a few input userids. Note that since exposure logging is enabled by default, all of the experiments' inputs, configuration information, timestamp, and parameter assignments are pooped out via the Logger class.
46
+
47
+ ```ruby
48
+ my_exp = VotingExperiment.new(userid: 14)
49
+ my_button_color = my_exp.get(:button_color)
50
+ button_text = my_exp.get(:button_text)
51
+ puts "button color is #{my_button_color} and button text is #{button_text}."
52
+ ```
53
+
54
+ The output of the Ruby script looks something like this:
55
+
56
+ ```ruby
57
+ logged data: {"name":"VotingExperiment","time":1404944726,"salt":"VotingExperiment","inputs":{"userid":14},"params":{"button_color":"ff0000","button_text":"I'm a voter"},"event":"exposure"}
58
+
59
+ button color is ff0000 and button text is I'm a voter.
60
+ ```
61
+ ## Examples
62
+
63
+ For examples please refer to the `examples` directory.
64
+
65
+ ## Running the tests
66
+ Make sure you're in the ruby implementation directory of PlanOut and run
67
+
68
+ `rake` or `rake test`
69
+
70
+ to run the entire test suite.
71
+
72
+ If you wish to run a specific test, run
73
+
74
+ `rake test TEST=test/testname.rb` or even better `ruby test/testname.rb`
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ end
7
+
8
+ task default: :test
@@ -0,0 +1,31 @@
1
+ require_relative '../../lib/planout/simple_experiment'
2
+
3
+ module Planout
4
+ class VotingExperiment < SimpleExperiment
5
+ def setup; end
6
+
7
+ def assign(params, **inputs)
8
+ userid = inputs[:userid]
9
+ params[:button_color] = UniformChoice.new({
10
+ choices: ['ff0000', '00ff00'],
11
+ unit: userid
12
+ })
13
+
14
+ params[:button_text] = UniformChoice.new({
15
+ choices: ["I'm voting", "I'm a voter"],
16
+ unit: userid,
17
+ salt:'x'
18
+ })
19
+ end
20
+ end
21
+
22
+ if __FILE__ == $0
23
+ (14..16).each do |i|
24
+ my_exp = VotingExperiment.new(userid:i)
25
+ # toggling the above disables or re-enables auto-logging
26
+ #my_exp.auto_exposure_log = false
27
+ puts "\ngetting assignment for user #{i} note: first time triggers a log event"
28
+ puts "button color is #{my_exp.get(:button_color)} and button text is #{my_exp.get(:button_text)}"
29
+ end
30
+ end
31
+ end
data/lib/planout.rb ADDED
@@ -0,0 +1,16 @@
1
+ require_relative 'planout/operator'
2
+ require_relative 'planout/op_simple'
3
+ require_relative 'planout/op_random'
4
+ require_relative 'planout/random_float'
5
+ require_relative 'planout/random_integer'
6
+ require_relative 'planout/bernoulli_trial'
7
+ require_relative 'planout/weighted_choice'
8
+ require_relative 'planout/uniform_choice'
9
+ require_relative 'planout/assignment'
10
+ require_relative 'planout/experiment'
11
+ require_relative 'planout/simple_experiment'
12
+ require_relative 'planout/version'
13
+
14
+ module Planout
15
+
16
+ end
@@ -0,0 +1,41 @@
1
+ module Planout
2
+ class Assignment
3
+ attr_accessor :experiment_salt, :data
4
+
5
+ def initialize(experiment_salt)
6
+ @experiment_salt = experiment_salt
7
+ @data = {}
8
+ end
9
+
10
+ def evaluate(data)
11
+ data
12
+ end
13
+
14
+ def get(var, default = nil)
15
+ @data[var.to_sym] || default
16
+ end
17
+
18
+ # in python this would be defined as __setattr__ or __setitem__
19
+ # not sure how to do this in Ruby.
20
+ def set(name, value)
21
+ if value.is_a? Operator
22
+ value.args[:salt] = name if !value.args.has_key?(:salt)
23
+ @data[name.to_sym] = value.execute(self)
24
+ else
25
+ @data[name.to_sym] = value
26
+ end
27
+ end
28
+
29
+ def [](x)
30
+ get(x)
31
+ end
32
+
33
+ def []=(x,y)
34
+ set(x,y)
35
+ end
36
+
37
+ def get_params
38
+ @data
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'op_random'
2
+
3
+ module Planout
4
+ class BernoulliTrial < OpRandom
5
+ def simple_execute
6
+ p = @parameters[:p]
7
+ rand_val = get_uniform(0.0, 1.0)
8
+ (rand_val <= p) ? 1 : 0
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,112 @@
1
+ require 'logger'
2
+ require 'json'
3
+
4
+ module Planout
5
+ class Experiment
6
+ attr_accessor :auto_exposure_log
7
+
8
+ def initialize(**inputs)
9
+ @inputs = inputs
10
+ @exposure_logged = false
11
+ @_salt = nil
12
+ @in_experiment = true
13
+ @name = self.class.name
14
+ @auto_exposure_log = true
15
+
16
+ setup # sets name, salt, etc.
17
+
18
+ @assignment = Assignment.new(salt)
19
+ @assigned = false
20
+
21
+ @logger = nil
22
+ setup
23
+ end
24
+
25
+ def _assign
26
+ configure_logger
27
+ assign(@assignment, **@inputs)
28
+ @in_experiment = @assignment.get(:in_experiment, @in_experiment)
29
+ @assigned = true
30
+ end
31
+
32
+ def setup
33
+ nil
34
+ end
35
+
36
+ def salt=(value)
37
+ @_salt = value
38
+ end
39
+
40
+ def salt
41
+ @_salt || @name
42
+ end
43
+
44
+ def auto_exposure_log=(value)
45
+ @auto_exposure_log = value
46
+ end
47
+
48
+ def configure_logger
49
+ nil
50
+ end
51
+
52
+ def requires_assignment
53
+ _assign if !@assigned
54
+ end
55
+
56
+ def is_logged?
57
+ @logged
58
+ end
59
+
60
+ def requires_exposure_logging
61
+ log_exposure if @auto_exposure_log && @in_experiment && !@exposure_logged
62
+ end
63
+
64
+ def get_params
65
+ requires_assignment
66
+ requires_exposure_logging
67
+ @assignment.get_params
68
+ end
69
+
70
+ def get(name, default = nil)
71
+ requires_assignment
72
+ requires_exposure_logging
73
+ @assignment.get(name, default)
74
+ end
75
+
76
+ def assign(params, *inputs)
77
+ # up to child class to implement
78
+ nil
79
+ end
80
+
81
+ def log_event(event_type, extras = nil)
82
+ if extras.nil?
83
+ extra_payload = {event: event_type}
84
+ else
85
+ extra_payload = {
86
+ event: event_type,
87
+ extra_data: extras.clone
88
+ }
89
+ end
90
+
91
+ log(as_blob(extra_payload))
92
+ end
93
+
94
+ def log_exposure(extras = nil)
95
+ @exposure_logged = true
96
+ log_event(:exposure, extras)
97
+ end
98
+
99
+ def as_blob(extras = {})
100
+ d = {
101
+ name: @name,
102
+ time: Time.now.to_i,
103
+ salt: salt,
104
+ inputs: @inputs,
105
+ params: @assignment.data
106
+ }
107
+
108
+ d.merge!(extras)
109
+ end
110
+ end
111
+
112
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'op_simple'
2
+
3
+ module Planout
4
+ class OpRandom < OpSimple
5
+ LongScale = Float(0xFFFFFFFFFFFFFFF)
6
+
7
+ def get_unit(appended_unit = nil)
8
+ unit = @parameters[:unit]
9
+ unit = [unit] if !unit.is_a? Array
10
+ unit += appended_unit if appended_unit != nil
11
+ unit
12
+ end
13
+
14
+ def get_hash(appended_unit = nil)
15
+ salt = @parameters[:salt]
16
+ salty = "#{@mapper.experiment_salt}.#{salt}"
17
+ unit_str = get_unit(appended_unit).join('.')
18
+ x = "#{salty}.#{unit_str}"
19
+ last_hex = (Digest::SHA1.hexdigest(x))[0..14]
20
+ last_hex.to_i(16)
21
+ end
22
+
23
+ def get_uniform(min_val = 0.0, max_val = 1.0, appended_unit = nil)
24
+ zero_to_one = self.get_hash(appended_unit)/LongScale
25
+ min_val + (max_val-min_val) * zero_to_one
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'operator'
2
+
3
+ module Planout
4
+ class OpSimple < Operator
5
+ def execute(mapper)
6
+ @mapper = mapper
7
+ @parameters = {}
8
+
9
+ @args.each do |key, value|
10
+ @parameters[key] = mapper.evaluate(value)
11
+ end
12
+
13
+ simple_execute
14
+ end
15
+
16
+ def simple_execute
17
+ -1
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require 'digest/sha1'
2
+
3
+ module Planout
4
+ class Operator
5
+ attr_accessor :args
6
+ def initialize(parameters)
7
+ @args = parameters
8
+ end
9
+
10
+ def execute(mapper)
11
+ mapper.experiment_salt
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'op_random'
2
+
3
+ module Planout
4
+ class RandomFloat < OpRandom
5
+ def simple_execute
6
+ min_val = @parameters.fetch(:min, 0)
7
+ max_val = @parameters.fetch(:max, 1)
8
+ get_uniform(min_val, max_val)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'op_random'
2
+
3
+ module Planout
4
+ class RandomInteger < OpRandom
5
+ def simple_execute
6
+ min_val = @parameters.fetch(:min, 0)
7
+ max_val = @parameters.fetch(:max, 1)
8
+ min_val + get_hash() % (max_val - min_val + 1)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ require_relative 'experiment'
2
+
3
+ module Planout
4
+ class SimpleExperiment < Experiment
5
+ def configure_logger
6
+ @logger = Logger.new(STDOUT)
7
+ #@loger.level = Logger::WARN
8
+ @logger.formatter = proc do |severity, datetime, progname, msg|
9
+ "logged data: #{msg}\n"
10
+ end
11
+ end
12
+
13
+ def log(data)
14
+ @logger.info(JSON.dump(data))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ require_relative 'op_random'
2
+
3
+ module Planout
4
+ class UniformChoice < OpRandom
5
+ def simple_execute
6
+ choices = @parameters[:choices]
7
+ return [] if choices.length() == 0
8
+ rand_index = get_hash() % choices.length()
9
+ choices[rand_index]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Planout
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'op_random'
2
+
3
+ module Planout
4
+ class WeightedChoice < OpRandom
5
+ def simple_execute
6
+ choices = @parameters[:choices]
7
+ weights = @parameters[:weights]
8
+
9
+ return [] if choices.length() == 0
10
+
11
+ cum_weights = choices.zip(weights)
12
+ cum_sum = 0.0
13
+
14
+ cum_weights.each do |choice, weight|
15
+ cum_sum += weight
16
+ cum_weights[choice] = cum_sum
17
+ end
18
+
19
+ stop_value = get_uniform(0.0, cum_sum)
20
+
21
+ cum_weights.each do |choice, cum_weight|
22
+ choice if stop_value <= cum_weight
23
+ end
24
+ end
25
+ end
26
+ end
data/planout.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'planout/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "planout"
8
+ spec.version = Planout::VERSION
9
+ spec.authors = ["Mohnish Thallavajhula"]
10
+ spec.email = ["i@mohni.sh"]
11
+ spec.summary = %q{PlanOut is a framework and programming language for online field experimentation.}
12
+ spec.description = %q{PlanOut is a framework and programming language for online field experimentation. PlanOut was created to make it easy to run and iterate on sophisticated experiments, while satisfying the constraints of deployed Internet services with many users.}
13
+ spec.homepage = "https://facebook.github.io/planout"
14
+ spec.license = "BSD"
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_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "minitest", "~> 5.5"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ end
@@ -0,0 +1,42 @@
1
+ require_relative '../test_helper'
2
+
3
+ module Planout
4
+ class AssignmentTest < Minitest::Test
5
+ def setup
6
+ @assignment = Assignment.new('mtsalt')
7
+ end
8
+
9
+ def test_salt
10
+ assert_equal('mtsalt', @assignment.experiment_salt)
11
+ end
12
+
13
+ def test_evaluate
14
+ assert_equal(1, @assignment.evaluate(1))
15
+ assert_equal(2, @assignment.evaluate(2))
16
+ end
17
+
18
+ def test_set
19
+ @assignment.set(:color, 'green')
20
+ @assignment.set('platform', 'ios')
21
+ @assignment.set('foo', UniformChoice.new({ unit: 1, choices: ['x', 'y'] }))
22
+ assert_equal('green', @assignment.data[:color])
23
+ assert_equal('ios', @assignment.data[:platform])
24
+ assert_equal('y', @assignment.data[:foo])
25
+ end
26
+
27
+ def test_get
28
+ @assignment.set(:button_text, 'Click Me!')
29
+ @assignment.set(:gender, 'f')
30
+ assert_equal('Click Me!', @assignment.get('button_text'))
31
+ assert_equal('f', @assignment.get('gender'))
32
+ assert_equal(10, @assignment.get('missing_key', 10))
33
+ assert_nil(@assignment.get('missing_key'))
34
+ end
35
+
36
+ def test_get_params
37
+ @assignment.set('foo', 'bar')
38
+ @assignment.set(:baz, 'qux')
39
+ assert_equal({ foo: 'bar', baz: 'qux' }, @assignment.get_params)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ require_relative '../test_helper'
2
+ require_relative '../../examples/planout/voting_experiment'
3
+
4
+ module Planout
5
+ class ExperimentTest < Minitest::Test
6
+ def setup
7
+ @voting_experiment = VotingExperiment.new(userid: 14)
8
+ @voting_experiment2 = VotingExperiment.new(userid: 15)
9
+ @voting_experiment.auto_exposure_log = false
10
+ @voting_experiment2.auto_exposure_log = false
11
+ end
12
+
13
+ def test_get_attributes
14
+ assert_equal('ff0000', @voting_experiment.get(:button_color))
15
+ assert_equal(1, @voting_experiment.get(:missing_key, 1))
16
+ assert_equal('ff0000', @voting_experiment2.get(:button_color))
17
+ assert_equal("I'm voting", @voting_experiment.get(:button_text))
18
+ assert_equal("I'm a voter", @voting_experiment2.get(:button_text))
19
+ end
20
+
21
+ def test_get_params
22
+ assert_equal({ button_color: 'ff0000', button_text: "I'm voting" }, @voting_experiment.get_params)
23
+ assert_equal({ button_color: 'ff0000', button_text: "I'm a voter" }, @voting_experiment2.get_params)
24
+ end
25
+
26
+ def test_as_blob
27
+ result = @voting_experiment.as_blob
28
+ assert_equal('Planout::VotingExperiment', result[:name])
29
+ assert_equal('Planout::VotingExperiment', result[:salt])
30
+ assert_equal({ userid: 14 }, result[:inputs])
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ require_relative '../test_helper'
2
+
3
+ module Planout
4
+ class OperatorTest < Minitest::Test
5
+ def setup
6
+ @operator = Operator.new({ foo: 'bar' })
7
+ @op_simple = OpSimple.new({ bar: 'qux' })
8
+ end
9
+
10
+ def test_execute
11
+ a = Assignment.new('mtsalt')
12
+ assert_equal('mtsalt', @operator.execute(a))
13
+ assert_equal(-1, @op_simple.execute(a))
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,2 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../lib/planout'
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: planout
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Mohnish Thallavajhula
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ description: PlanOut is a framework and programming language for online field experimentation.
56
+ PlanOut was created to make it easy to run and iterate on sophisticated experiments,
57
+ while satisfying the constraints of deployed Internet services with many users.
58
+ email:
59
+ - i@mohni.sh
60
+ executables: []
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - examples/planout/voting_experiment.rb
70
+ - lib/planout.rb
71
+ - lib/planout/assignment.rb
72
+ - lib/planout/bernoulli_trial.rb
73
+ - lib/planout/experiment.rb
74
+ - lib/planout/op_random.rb
75
+ - lib/planout/op_simple.rb
76
+ - lib/planout/operator.rb
77
+ - lib/planout/random_float.rb
78
+ - lib/planout/random_integer.rb
79
+ - lib/planout/simple_experiment.rb
80
+ - lib/planout/uniform_choice.rb
81
+ - lib/planout/version.rb
82
+ - lib/planout/weighted_choice.rb
83
+ - planout.gemspec
84
+ - test/planout/assignment_test.rb
85
+ - test/planout/experiment_test.rb
86
+ - test/planout/operator_test.rb
87
+ - test/test_helper.rb
88
+ homepage: https://facebook.github.io/planout
89
+ licenses:
90
+ - BSD
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.2.2
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: PlanOut is a framework and programming language for online field experimentation.
112
+ test_files:
113
+ - test/planout/assignment_test.rb
114
+ - test/planout/experiment_test.rb
115
+ - test/planout/operator_test.rb
116
+ - test/test_helper.rb