eenie_meenie 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,21 +1,19 @@
1
1
  EenieMeenie
2
2
  ========
3
3
 
4
- This tool was written in order to play with some simple methods for assigning members of a population to one of two groups. The goal was initially to provide an algorithm that would limit the user's ability to predict to which group a member will be assigned.
4
+ Decides to which experimental group a member of a population should be assigned.
5
5
 
6
6
  Installation
7
7
  ------------
8
8
 
9
- EenieMeenie is a Ruby gem, and can be installed using `gem install eenie_meenie`
9
+ EenieMeenie is a Ruby gem, and can be installed using `gem install eenie_meenie` or by adding the following to your application's Gemfile:
10
+
11
+ gem 'eenie_meenie', '0.1.0'
10
12
 
11
13
  Usage
12
14
  -----
13
15
 
14
- EenieMeenie currently provides two algorithms for assigning members to experimental groups.
15
-
16
- `EenieMeenie::Assignment` was the first algorithm, and was intended to handle situations where members of a population are to be assigned to ONE of TWO groups. As I started working with a greater variety of research studies, I found the algorithm to be both overcomplicated and inadequate.
17
-
18
- `EeenieMeenie::PolyAssignment` is a new algorithm. With this algorithm you'll be able to:
16
+ Attention! The usage of `EeenieMeenie::Assignment` changed with the release of version 0.1.0. With the new algorithm you'll be able to:
19
17
 
20
18
  1. Assign a threshold (a `Float` to represent percentage) to each experimental group.
21
19
  2. Assign a threshold of "DO NOT CARE" to any group by passing `false` instead of a `Float`
@@ -30,13 +28,13 @@ Example Configurations
30
28
  # Experimental A: %50 (randomly assign)
31
29
  # Experimental B: %50 (randomly assign)
32
30
 
33
- EenieMeenie::PolyAssignment.new({
31
+ EenieMeenie::Assignment.new({
34
32
  groups: ["Experimental A", "Experimental B"], # EenieMeenie's assignment options
35
33
  member: @obj, # Member of population
36
34
  group_rules: {
37
35
  "Control" => { threshold: false }, # Don't care
38
36
  "Experimental A" => { threshold: 0.5 }, # No more than 50%
39
- "Experimental B" => { threshold: 0.5 } # No more than 50%
37
+ "Experimental B" => { threshold: 0.5 } # No more than 50%
40
38
  },
41
39
  class_rules: { organization_id: 1} # Only consider members belonging to Organization 1
42
40
  }).execute!
@@ -47,7 +45,7 @@ EenieMeenie::PolyAssignment.new({
47
45
  # Experimental A: %33.3 (randomly assign)
48
46
  # Experimental B: %33.3 (randomly assign)
49
47
 
50
- EenieMeenie::PolyAssignment.new({
48
+ EenieMeenie::Assignment.new({
51
49
  groups: ["Control", "Experimental A", "Experimental B"], # EenieMeenie's assignment options
52
50
  member: @obj, # Member of population
53
51
  group_rules: {
@@ -61,7 +59,7 @@ EenieMeenie::PolyAssignment.new({
61
59
 
62
60
  ```ruby
63
61
  # Control: %50 (randomly assign)
64
- # Experimental: Do not care (randomly assign)
62
+ # Experimental: %50 (randomly assign)
65
63
  # Experimental A: Do not care (manually assign)
66
64
  # Experimental B: Do not care (manually assign)
67
65
 
@@ -69,12 +67,12 @@ EenieMeenie::PolyAssignment.new({
69
67
  # Later someone will choose whether they're in "Experimental A" ...
70
68
  # or in "Experimental B"
71
69
 
72
- EenieMeenie::PolyAssignment.new({
70
+ EenieMeenie::Assignment.new({
73
71
  groups: ["Control", "Experimental"], # EenieMeenie's assignment options
74
72
  member: @obj, # Member of population
75
73
  group_rules: {
76
- "Control" => { threshold: 0.5 }, # No more than one-third
77
- "Experimental" => { threshold: false }, # Don't care
74
+ "Control" => { threshold: 0.5 }, # No more than one half
75
+ "Experimental" => { threshold: 0.5 }, # No more than one half
78
76
  "Experimental A" => { threshold: false } # Don't care
79
77
  "Experimental B" => { threshold: false } # Don't care
80
78
  }
@@ -83,4 +81,4 @@ EenieMeenie::PolyAssignment.new({
83
81
 
84
82
  ### Pull requests/issues
85
83
 
86
- Please submit any useful pull requests through GitHub. Please report any bugs using Github's issue tracker
84
+ Please submit any useful pull requests through GitHub. Please report any bugs using Github's issue tracker.
data/eenie_meenie.gemspec CHANGED
@@ -19,6 +19,8 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
+ s.add_runtime_dependency "pay_dirt", "~> 0.0.5"
23
+
22
24
  s.add_development_dependency "minitest"
23
25
 
24
26
  s.add_development_dependency "rake"
@@ -1,62 +1,43 @@
1
1
  module EenieMeenie
2
- class Assignment < ::EenieMeenie::Base
2
+ class Assignment < PayDirt::Base
3
3
  def initialize(options)
4
- load_options(:groups, :member_class, options)
5
- #raise ArgumentError unless @member_class.respond_to?(:experimental_group)
4
+ options = {
5
+ class_rules: {}
6
+ }.merge(options)
7
+
8
+ load_options(:member, :class_rules, :groups, :group_rules, options)
6
9
  end
7
10
 
8
11
  def execute!
9
- group = assign
12
+ set_group_counts
13
+ return random_group
10
14
  end
11
15
 
12
16
  private
13
17
 
14
- def assign
15
- group = coercion_threshold_reached? ? assign_with_coercion : assign_without_coercion
16
-
17
- group
18
- end
19
-
20
- def count_for_group(group)
21
- @member_class.where(experimental_group: group).count
18
+ # Total population
19
+ def population
20
+ @members ||= @member.class.where(@class_rules)
22
21
  end
23
22
 
24
- def assign_without_coercion
25
- selected_group = @groups.sample
26
- if rand(expected_population) >= (expected_population / 2)
27
- group = selected_group
28
- else
29
- group = the_other_group(selected_group)
23
+ # Current population in each group
24
+ def set_group_counts
25
+ @group_rules.each do |k,v|
26
+ v[:count] = population.where(experimental_group: k.to_s).count
30
27
  end
31
28
  end
32
29
 
33
- def assign_with_coercion
34
- group = @groups.sample
35
- group_tally = count_for_group(group)
36
- other_group = (@groups - [group]).first
37
- group = other_group if group_tally > count_for_group(other_group) + rand(leeway)
38
-
39
- group
40
- end
41
-
42
- def expected_population
43
- 60000.to_f
44
- end
45
-
46
- def current_population
47
- @member_class.count.to_f
48
- end
49
-
50
- def leeway
51
- ((current_population / 2 ) * 0.01).round
52
- end
30
+ # Groups not over threshold
31
+ def group_candidates
32
+ @group_candidates ||= @group_rules.reject { |k,v|
33
+ v[:threshold] && (v[:count] / @members.count.to_f) >= v[:threshold]
34
+ }.keys.map(&:to_s)
53
35
 
54
- def the_other_group group
55
- (@groups - [group]).first
36
+ @group_candidates.select {|cand| @groups.include?(cand) }
56
37
  end
57
38
 
58
- def coercion_threshold_reached?
59
- (current_population / expected_population) >= 0.9
39
+ def random_group
40
+ group_candidates.sample || @groups.sample
60
41
  end
61
42
  end
62
43
  end
@@ -1,3 +1,3 @@
1
1
  module EenieMeenie
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/eenie_meenie.rb CHANGED
@@ -1,11 +1,2 @@
1
- require "eenie_meenie/base"
2
- #require "eenie_meenie/miny_moe"
3
- #require "eenie_meenie/sorters/round_robin"
4
- #require "eenie_meenie/sorters/pure_random"
5
- #require "eenie_meenie/sorters/pick_a_group"
6
- #require "eenie_meenie/version"
7
- #require "eenie_meenie/result"
8
-
9
- Dir.glob(File.join(File.dirname(__FILE__), '/**/*.rb')) do |c|
10
- require(c)
11
- end
1
+ require "pay_dirt"
2
+ require "eenie_meenie/assignment"
@@ -0,0 +1,57 @@
1
+ require "minitest_helper"
2
+
3
+ describe EenieMeenie::Assignment do
4
+ describe ".execute!" do
5
+ before do
6
+ # Mock dependencies
7
+ @member = MiniTest::Mock.new
8
+ @member_class = MiniTest::Mock.new
9
+ @relation = MiniTest::Mock.new
10
+ @groups = ["Experimental", "Control"]
11
+ @group_rules = {
12
+ "Experimental" => { threshold: 0.5 },
13
+ "Control" => { threshold: 0.5 }
14
+ }
15
+
16
+ @minimum_dependencies = {
17
+ member: @member,
18
+ groups: @groups,
19
+ group_rules: @group_rules
20
+ }
21
+
22
+ @member.expect :class, @member_class
23
+ @member_class.expect :where, @relation, [{}]
24
+ @relation.expect :where, @relation, [{:experimental_group=>"Control"}]
25
+ @relation.expect :count, 42
26
+ @relation.expect :count, 42
27
+ @subject = EenieMeenie::Assignment
28
+ end
29
+
30
+ it "initializes when minimum dependencies are provided" do
31
+ proc {
32
+ @subject.new(@minimum_dependencies)
33
+ }.must_be_silent
34
+ end
35
+
36
+ it "fails to initialize without required options" do
37
+ @minimum_dependencies.each_key do |key|
38
+ begin
39
+ @subject.new(@minimum_dependencies.reject {|k| k == key })
40
+ rescue => e
41
+ e.must_be_kind_of RuntimeError
42
+ end
43
+ end
44
+ end
45
+
46
+ it "produces an expected result" do
47
+ result = @subject.new(@minimum_dependencies.merge({
48
+ groups: ["Control"],
49
+ group_rules: {
50
+ "Control" => { threshold: 1.0 }
51
+ }
52
+ })).execute!
53
+
54
+ assert_equal result, "Control"
55
+ end
56
+ end
57
+ end
metadata CHANGED
@@ -1,16 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eenie_meenie
3
3
  version: !ruby/object:Gem::Version
4
+ version: 0.1.0
4
5
  prerelease:
5
- version: 0.0.5
6
6
  platform: ruby
7
7
  authors:
8
8
  - Tad Hosford
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-23 00:00:00.000000000 Z
12
+ date: 2013-05-27 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: pay_dirt
16
+ version_requirements: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: 0.0.5
21
+ none: false
22
+ requirement: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.0.5
27
+ none: false
28
+ prerelease: false
29
+ type: :runtime
14
30
  - !ruby/object:Gem::Dependency
15
31
  name: minitest
16
32
  version_requirements: !ruby/object:Gem::Requirement
@@ -81,17 +97,8 @@ files:
81
97
  - eenie_meenie.gemspec
82
98
  - lib/eenie_meenie.rb
83
99
  - lib/eenie_meenie/assignment.rb
84
- - lib/eenie_meenie/base.rb
85
- - lib/eenie_meenie/miny_moe.rb
86
- - lib/eenie_meenie/poly_assignment.rb
87
- - lib/eenie_meenie/result.rb
88
- - lib/eenie_meenie/sorters/bucket_shuffle.rb
89
- - lib/eenie_meenie/sorters/late_coercion.rb
90
- - lib/eenie_meenie/sorters/pick_a_group.rb
91
- - lib/eenie_meenie/sorters/pure_random.rb
92
- - lib/eenie_meenie/sorters/round_robin.rb
93
100
  - lib/eenie_meenie/version.rb
94
- - test/eenie_meenie/miny_moe_test.rb
101
+ - test/eenie_meenie/assignment_test.rb
95
102
  - test/minitest_helper.rb
96
103
  homepage: http://github.com/rthbound/eenie_meenie
97
104
  licenses: []
@@ -105,9 +112,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
105
112
  - !ruby/object:Gem::Version
106
113
  segments:
107
114
  - 0
108
- hash: 2
109
115
  version: !binary |-
110
116
  MA==
117
+ hash: 2
111
118
  none: false
112
119
  required_rubygems_version: !ruby/object:Gem::Requirement
113
120
  requirements:
@@ -115,9 +122,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
122
  - !ruby/object:Gem::Version
116
123
  segments:
117
124
  - 0
118
- hash: 2
119
125
  version: !binary |-
120
126
  MA==
127
+ hash: 2
121
128
  none: false
122
129
  requirements: []
123
130
  rubyforge_project:
@@ -1,13 +0,0 @@
1
- module EenieMeenie
2
- class Base
3
- def load_option(option, options)
4
- instance_variable_set("@#{option}", options.fetch(option.to_sym) { raise "Missing required option: #{option}" } )
5
- end
6
-
7
- def load_options(*option_names, options)
8
- option_names.each{|o| load_option(o, options) }
9
-
10
- option_names << instance_variable_set("@tracked", options[:tracked]) if options[:tracked]
11
- end
12
- end
13
- end
@@ -1,14 +0,0 @@
1
- module EenieMeenie
2
- class MinyMoe < ::EenieMeenie::Base
3
- def initialize(options)
4
- load_options(:groups, :population, :sorter, options)
5
- end
6
-
7
- def execute!
8
- groups = @sorter.new(groups: @groups, population: @population).sort
9
- return EenieMeenie::Result.new(groups: groups, population: @population)
10
- end
11
-
12
- private
13
- end
14
- end
@@ -1,43 +0,0 @@
1
- module EenieMeenie
2
- class PolyAssignment < ::EenieMeenie::Base
3
- def initialize(options)
4
- options = {
5
- class_rules: {}
6
- }.merge(options)
7
-
8
- load_options(:group_rules, :class_rules, :groups, :member, options)
9
- end
10
-
11
- def execute!
12
- set_group_counts
13
- return random_group
14
- end
15
-
16
- private
17
-
18
- # Total population
19
- def population
20
- @members ||= @member.class.where(@class_rules)
21
- end
22
-
23
- # Current population in each group
24
- def set_group_counts
25
- @group_rules.each do |k,v|
26
- v[:count] = population.where(experimental_group: k.to_s).count
27
- end
28
- end
29
-
30
- # Groups not over threshold
31
- def group_candidates
32
- @group_candidates ||= @group_rules.reject { |k,v|
33
- v[:threshold] && (v[:count] / @members.count.to_f) >= v[:threshold]
34
- }.keys.map(&:to_s)
35
-
36
- @group_candidates.select {|cand| @groups.include?(cand) }
37
- end
38
-
39
- def random_group
40
- group_candidates.sample || @groups.sample
41
- end
42
- end
43
- end
@@ -1,22 +0,0 @@
1
- module EenieMeenie
2
- class Result
3
- def initialize(options)
4
- @groups = options[:groups]
5
- @population = options[:population]
6
- @imbalance = @groups.values.inject(:-).abs
7
- @relative_imbalance = (@imbalance / @population).to_f
8
- end
9
-
10
- def groups
11
- @groups
12
- end
13
-
14
- def imbalance
15
- @imbalance
16
- end
17
-
18
- def relative_imbalance
19
- @relative_imbalance
20
- end
21
- end
22
- end
@@ -1,20 +0,0 @@
1
- module EenieMeenie
2
- module Sorters
3
- class PureRandom < EenieMeenie::Base
4
- def initialize(*args, options)
5
- load_options(:groups, :population, options)
6
- end
7
-
8
- def sort
9
- results = {}
10
- @groups.each { |group| results.merge!(group => 0) }
11
-
12
- @population.times do |i|
13
- results[(rand(@population) >= (@population / 2) ? @groups.first : @groups.last)] += 1
14
- end
15
- groups = results
16
- end
17
- end
18
- end
19
- end
20
-
@@ -1,61 +0,0 @@
1
- module EenieMeenie
2
- module Sorters
3
- class LateCoercion < EenieMeenie::Base
4
- def initialize(*args, options)
5
- load_options(:groups, :population, options)
6
- end
7
-
8
- def sort
9
- @results = {}
10
- @groups.each { |group| @results.merge!(group => 0) }
11
-
12
- @population.times do |i|
13
- @results[assign] += 1
14
- end
15
- groups = @results
16
- end
17
-
18
- def assign
19
- coercion_threshold_reached? ? assign_with_coercion : assign_without_coercion
20
- end
21
-
22
- def assign_without_coercion
23
- selected_group = @groups.sample
24
- if rand(@population) >= (@population / 2)
25
- group = selected_group
26
- else
27
- group = the_other_group(selected_group)
28
- end
29
- end
30
-
31
- def the_other_group group
32
- (@groups - [group]).first
33
- end
34
-
35
- def assign_with_coercion
36
- group = @groups.sample
37
- group_tally = count_for_group(group)
38
- other_group = (@groups - [group]).first
39
- group = other_group if group_tally > count_for_group(other_group) + rand(leeway)
40
-
41
- group
42
- end
43
-
44
- def count_for_group(group)
45
- @results[group].to_f
46
- end
47
-
48
- def leeway
49
- ((current_population / 2 ) * 0.01).round
50
- end
51
-
52
- def current_population
53
- @results.values.inject(&:+).to_f
54
- end
55
-
56
- def coercion_threshold_reached?
57
- (current_population / @population) >= 0.9
58
- end
59
- end
60
- end
61
- end
@@ -1,23 +0,0 @@
1
- module EenieMeenie
2
- module Sorters
3
- class PickAGroup < EenieMeenie::Base
4
- def initialize(*args, options)
5
- load_options(:groups, :population, options)
6
- end
7
-
8
- def sort
9
- results = {}
10
- @groups.each { |group| results.merge!(group => 0) }
11
-
12
- @population.times do |i|
13
- group = @groups.sample
14
- other_group = (@groups - [group]).first
15
- group = other_group if results.values.any? {|v| results[group] > v + rand(10) }
16
- results[group] += 1
17
- end
18
- groups = results
19
- end
20
- end
21
- end
22
- end
23
-
@@ -1,20 +0,0 @@
1
- module EenieMeenie
2
- module Sorters
3
- class PureRandom < EenieMeenie::Base
4
- def initialize(*args, options)
5
- load_options(:groups, :population, options)
6
- end
7
-
8
- def sort
9
- results = {}
10
- @groups.each { |group| results.merge!(group => 0) }
11
-
12
- @population.times do |i|
13
- results[(rand(@population) > (@population / 2) ? @groups.first : @groups.last)] += 1
14
- end
15
- groups = results
16
- end
17
- end
18
- end
19
- end
20
-
@@ -1,20 +0,0 @@
1
- module EenieMeenie
2
- module Sorters
3
- class RoundRobin < EenieMeenie::Base
4
- def initialize(*args, options)
5
- load_options(:groups, :population, options)
6
- end
7
-
8
- def sort
9
- results = {}
10
- @groups.each { |group| results.merge!(group => 0) }
11
- @population.times do |i|
12
- results[@groups[i % @groups.length]] += 1
13
- end
14
-
15
- groups = results
16
- end
17
- end
18
- end
19
- end
20
-
@@ -1,13 +0,0 @@
1
- require "minitest_helper"
2
-
3
- describe EenieMeenie::MinyMoe do
4
- describe ".execute!" do
5
- before do
6
- @options = { population: 100, groups: ["Experimental", "Control"] }
7
- @subject = EenieMeenie::MinyMoe
8
- end
9
- it "returns a result" do
10
- @subject.new(@options.merge(sorter: EenieMeenie::Sorters::RoundRobin)).execute!.must_be_kind_of(EenieMeenie::Result)
11
- end
12
- end
13
- end