observance 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7dfc295009d53d249c379697a871e38abd6f3ec1
4
- data.tar.gz: 1abc3656a6c554a43402e77f204e643d3bbe5369
3
+ metadata.gz: 553f83a148f0ff71cd4f7e0172afc68fc359d5ab
4
+ data.tar.gz: c56c3391312d58cf455d69c1121cf079aaa5ef8b
5
5
  SHA512:
6
- metadata.gz: d5cb644af3b4d4173d01d7ec6f0bf5fdc1368b54f10fc12707f9723f9e4247978f9a367cf5eff62c987d03d5d3daed30299fac4f57716f1e2550a7504a182136
7
- data.tar.gz: b76a12662b91601b5b0e41a2811b2ea50ce5c3e6d924af4d97a9279691c4a491deebddbca01c034c4451e88449de873749a06e262195ffbf3688a603ab6ff7a2
6
+ metadata.gz: 3083ccf959eec5e7bb8f1e4e84b25f2e18308c0066abd1e79eaf447ec0cb33b780f48b5b896e04e160353c98625c6cc914891acfb5758ccf4040ae72ee4f636c
7
+ data.tar.gz: 09894761a0ca773960edb616d7d03b19be6ccef4032c9c19cc7f9b09310682c17066fed76022024e784eac43ca08b507bb64d6a3c463439fffe2add4d8cf9050
data/README.md CHANGED
@@ -1,61 +1,124 @@
1
1
  # Observance
2
2
 
3
- Given multiple observations returns the most likely.
3
+ Given a collection of observations it returns the most likely. An observation is anything that responds to `to_a` Array or that responds to `to_h`.
4
4
 
5
- Imagine a person flashing at a distance a card
6
- displaying a number from 1 to 10. Let us say five people
7
- are observing this remote scene and note their observations during 4 rounds.
5
+ As an example imagine a person flashing at a distance a card
6
+ displaying a number from 1 to 10. If five people are observing
7
+ this remote scene and note their observations during 4 rounds we have
8
8
 
9
+ ```ruby
9
10
  observer_1 = {first: 8, second: 6, third: 1, fourth: 4}
10
11
  observer_2 = {first: 8, second: 6, third: 7, fourth: 4}
11
12
  observer_3 = {first: 8, second: 9, third: 7, fourth: 4}
12
13
  observer_4 = {first: 8, second: 9, third: 2, fourth: 8}
13
14
  observer_5 = {first: 0, second: 2, third: 0, fourth: 1}
15
+ ```
16
+
17
+ The probable outcome seems to be `8-(6 or 9)-7-4`. So the best observation
18
+ would either be `8-6-7-4` or `8-9-7-4`.
19
+
20
+ Using `Observance` we determine the most likely observation(s) by doing:
21
+
22
+ ```ruby
23
+ observations = Observance.run(observer_1, observer_2, observer_3,
24
+ observer_4, observer_5)
14
25
 
15
- The probable outcome seems to be 8-(6 or 9)-7-4. So the probable observation
16
- would either be 8-6-7-4 or 8-9-7-4.
26
+ observations[0].rating #=> 0.55
27
+ observations[0].object #=> observer_2
28
+
29
+ observations[1].rating #=> 0.55
30
+ observations[1].object #=> observer_3
31
+
32
+ observations[2].rating #=> 0.5
33
+ observations[3].rating #=> 0.4
34
+ observations[4].rating #=> 0.2
35
+ ```
36
+
37
+ As expected it gives us that `observer_2` and `observer_3` are equally the most likely to have happened.
17
38
 
18
39
  ## Installation
19
40
 
20
41
  Add `gem 'observance'` to your Gemfile or run `gem install observance`
21
42
 
43
+ ## Testing
44
+
45
+ This gem uses minitest. Just run `bundle exec rake` to test.
46
+
47
+ ## Features
48
+
49
+ * Handle observations of **different sizes** (although makes less sense)
50
+ * Observations can be **JSON documents** since `Observance` handles nested hashes
51
+ * Filter which set of keys to run `Observance` on
52
+
22
53
  ## Usage
23
54
 
24
- This gem runs on given sets of observations. An observation is anything
25
- that responds to `to_h` or `to_hash`.
55
+ ### Observance input
56
+
57
+ `Observance` needs as input collections of observations. An observation is anything
58
+ that responds to `to_a` or `to_h`.
59
+
60
+ For instance Hashes or Arrays:
26
61
 
27
62
  ```ruby
28
63
  o1 = {first_toss: 'head', second_toss: 'tail', third_toss: 'tail'}
29
64
  o2 = {first_toss: 'head', second_toss: 'tail', third_toss: 'tail'}
30
65
  o3 = {first_toss: 'tail', second_toss: 'tail', third_toss: 'tail'}
31
66
 
32
- Observance.observed(o1, o2, o3)
67
+ Observance.run(o1, o2, o3)
68
+
69
+ o1 = ['head', 'tail', 'tail']
70
+ o2 = ['head', 'tail', 'tail']
71
+ o3 = ['tail', 'tail', 'tail']
72
+
73
+ Observance.run(o1, o2, o3)
74
+
33
75
  ```
34
76
 
35
- Since a observation object is anything that responds to `to_h` or `to_hash`, observations
36
- can be a Hash, a parsed JSON, or a slightly modified Struct
77
+ Since a observation object is anything that responds to `to_a` or `to_h`, it
78
+ can also be a parsed JSON, or a slightly modified Struct as follows:
37
79
 
38
80
  ```ruby
39
81
  # Observation as a slightly modified Struct
40
- Scores = Struct.new(:period_1, :period_2, :period_3, period_4) do
82
+ Scores = Struct.new(:period_1, :period_2, :period_3, :period_4) do
41
83
  def to_h
42
84
  Hash[members.zip(to_a)]
43
85
  end
44
86
  end
45
87
 
46
88
  tom_observations = Scores.new(5, 6, 7, 4)
47
- jane_observations = Scores.new(5, 6, 7, 4)
89
+ jane_observations = Scores.new(5, 5, 7, 4)
90
+ bill_observations = Scores.new(5, 6, 6, 4)
48
91
 
49
- Observance.observed(tom_observations, jane_observations)
92
+ results = Observance.run(tom_observations, jane_observations, bill_observations)
93
+ results.first #=> Tom sample is the most likely with a rating of 0.8333
50
94
  ```
51
95
 
52
- The result returned is an ordered collection of Observance::Observed objects
96
+ ### Observance output
53
97
 
54
- ```ruby
55
- results = Observance.observed(o1, o2, o3, o4, o5)
56
- observed_1, observed_2, _ = results
98
+ Running Observance, it returns a sorted Array of `Observance::Observation` objects. Each `Observance::Observation` contains the original observation object (usually a hash) along with a **rating** (a float number between 0 and 1).
99
+
100
+ **The higher the rating the better the observation.**
57
101
 
58
- observed_1.factor = 0.7515
59
- observed_1.object == 01
102
+ The results **are sorted by rating** - with the highest rating (most likely) being the first.
60
103
 
104
+ ## Simple example
105
+
106
+ 5 people observe a coin being tossed 5 times. Each sends their observations as an Array
107
+
108
+ ```ruby
109
+ o1 = ['head', 'tail', 'tail', 'head', 'tail']
110
+ o2 = ['tail', 'tail', 'tail', 'head', 'tail']
111
+ o3 = ['head', 'head', 'tail', 'head', 'tail']
112
+ o4 = ['head', 'head', 'head', 'head', 'tail']
113
+ o5 = ['head', 'tail', 'tail', 'tail', 'head']
114
+
115
+ results = Observance.run(o1, o2, o3, o4, o5)
116
+ results.class # => Array
117
+
118
+ puts results
119
+ #<struct Observance::Observation rating=0.76, object=["head", "tail", "tail", "head", "tail"], index=0>
120
+ #<struct Observance::Observation rating=0.72, object=["head", "head", "tail", "head", "tail"], index=2>
121
+ #<struct Observance::Observation rating=0.64, object=["tail", "tail", "tail", "head", "tail"], index=1>
122
+ #<struct Observance::Observation rating=0.6, object=["head", "head", "head", "head", "tail"], index=3>
123
+ #<struct Observance::Observation rating=0.52, object=["head", "tail", "tail", "tail", "head"], index=4>
61
124
  ```
data/Rakefile CHANGED
@@ -2,6 +2,7 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
+ t.pattern = "test/**/*_test.rb"
5
6
  t.libs << "test"
6
7
  end
7
8
 
data/lib/observance.rb CHANGED
@@ -1,5 +1,68 @@
1
- require "observance/version"
1
+ require 'observance/version'
2
2
 
3
3
  module Observance
4
- # Your code goes here...
4
+
5
+ Observation = Struct.new(:rating, :object, :index) do
6
+ include Comparable
7
+
8
+ def <=>(other)
9
+ return nil unless other.is_a? self.class
10
+ if other.rating == self.rating
11
+ self.index <=> other.index
12
+ else
13
+ other.rating <=> self.rating
14
+ end
15
+ end
16
+ end
17
+
18
+ def self.run(*observations)
19
+ obs = if observations.first.is_a? Array
20
+ wrapped_in_hashes(observations)
21
+ elsif observations.first.respond_to? :to_h
22
+ observations.map(&:to_h)
23
+ else
24
+ raise "Observations need to be Array or respond to 'to_h'"
25
+ end
26
+ obs.each_with_index.map do |c, index|
27
+ rating = (obs.inject(0) do |acc, o|
28
+ acc = acc + c.similarity_to(o)
29
+ end).fdiv(obs.size)
30
+ Observation.new(rating.round(4), observations[index], index)
31
+ end.sort
32
+ end
33
+
34
+ private
35
+
36
+ def self.wrapped_in_hashes(observations)
37
+ observations.map do |o|
38
+ Hash[(0...o.size).zip(o)]
39
+ end
40
+ end
5
41
  end
42
+
43
+ class Hash
44
+ # Returns a rating f between 0 and 1 that indicates
45
+ # the similarity rating of self to other
46
+ #
47
+ # Calculates the number of keys with the same values
48
+ # then divide the result by the number of keys
49
+ def similarity_to(other)
50
+ smaller_one, bigger_one = [self, other].sort_by(&:size)
51
+ diff_size = bigger_one.size - smaller_one.size
52
+
53
+ ratio = bigger_one.size + diff_size
54
+
55
+ sum = 0
56
+ bigger_one.each_pair do |k, v|
57
+ (sum = sum + 1) if v == smaller_one[k]
58
+ end
59
+ r1 = sum.fdiv(bigger_one.size)
60
+
61
+ sum2 = 0
62
+ smaller_one.each_pair do |k, v|
63
+ (sum2= sum2 + 1) if v == bigger_one[k]
64
+ end
65
+ r2 = (sum2).fdiv(ratio)
66
+ (r1 + r2).fdiv(2)
67
+ end
68
+ end
@@ -1,3 +1,3 @@
1
1
  module Observance
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/observance.gemspec CHANGED
@@ -21,4 +21,5 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "bundler", "~> 1.7"
22
22
  spec.add_development_dependency "rake", "~> 10.0"
23
23
  spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "pry"
24
25
  end
@@ -0,0 +1,56 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestHashExtension < MiniTest::Test
4
+
5
+ def test_similarity_in_same_size_hashes
6
+ h = {one: 1, two: 2, three: 3, four: 4, five: 5,
7
+ six: 6, seven: 7, eight: 8, nine: 9, ten: 10}
8
+
9
+ assert_equal 1, h.similarity_to(h)
10
+ assert_equal 0.9, h.similarity_to(h.merge(one: 0))
11
+ assert_equal 0.8, h.similarity_to(h.merge(one: 0, two: 0))
12
+ assert_equal 0.7, h.similarity_to(h.merge(one: 0, two: 0, three: 0))
13
+ assert_equal 0.6, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0))
14
+ assert_equal 0.5, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0))
15
+ assert_equal 0.4, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0,
16
+ six: 0))
17
+ assert_equal 0.3, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0,
18
+ six: 0, seven: 0))
19
+ assert_equal 0.2, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0,
20
+ six: 0, seven: 0, eight: 0))
21
+ assert_equal 0.1, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0,
22
+ six: 0, seven: 0, eight: 0, nine: 0))
23
+ assert_equal 0.0, h.similarity_to(h.merge(one: 0, two: 0, three: 0, four: 0, five: 0,
24
+ six: 0, seven: 0, eight: 0, nine: 0, ten: 0))
25
+ end
26
+
27
+ def test_similarity_in_different_size_hashes
28
+ h = {one: 1, two: 2, three: 3, four: 4}
29
+
30
+ assert_in_delta 0.733, h.similarity_to({one: 1, two: 2, three: 3, four: 4, five: 5})
31
+ assert_equal 0.675, h.similarity_to({one: 1, two: 2, three: 3})
32
+ assert_in_delta 0.416, h.similarity_to({one: 1, two: 2})
33
+ assert_in_delta 0.208, h.similarity_to({one: 0, two: 2})
34
+ assert_in_delta 0.183, h.similarity_to({one: 0, two: 0, three: 0, four: 4, five: 5})
35
+ assert_equal 0.0, h.similarity_to({one: 0, two: 0, three: 0, four: 0, five: 5})
36
+ end
37
+
38
+ def test_same_size_has_better_similarity_than_different_size
39
+ h = {one: 1, two: 2, three: 3, four: 4}
40
+
41
+ assert h.similarity_to({one: 1, two: 2, three: 3}) <
42
+ h.similarity_to(one: 1, two: 2, three: 3, four: 0)
43
+ end
44
+
45
+ def test_symmetry_of_similarity_operation
46
+ h = {one: 1, two: 2, three: 3, four: 4}
47
+ o = {one: 2, two: 1, three: 3, four: 5}
48
+
49
+ assert_equal h.similarity_to(o), o.similarity_to(h)
50
+
51
+ h = {one: 1, two: 2, three: 3, four: 4}
52
+ o = {one: 1, two: 2, three: 3, four: 4, five: 5}
53
+
54
+ assert_equal h.similarity_to(o), o.similarity_to(h)
55
+ end
56
+ end
@@ -0,0 +1,85 @@
1
+ require 'minitest_helper'
2
+
3
+ class TestObservance < MiniTest::Test
4
+ def test_returns_ordered_observation_from_same_size_hashes
5
+ o1 = {one: 1, two: 2, three: 3, four: 4}
6
+ o2 = {one: 1, two: 2, three: 3, four: 4}
7
+ o3 = {one: 1, two: 2, three: 3, four: 0}
8
+ o4 = {one: 1, two: 2, three: 0, four: 4}
9
+ o5 = {one: 1, two: 2, three: 0, four: 0}
10
+ o6 = {one: 1, two: 0, three: 0, four: 0}
11
+ o7 = {one: 0, two: 0, three: 0, four: 0}
12
+
13
+ observations = Observance.run(o1, o2, o3, o4, o5, o6, o7)
14
+
15
+ assert_equal o5, observations[0].object
16
+ assert_equal o3, observations[1].object
17
+ assert_equal o4, observations[2].object
18
+ assert_equal o1, observations[3].object
19
+ assert_equal o2, observations[4].object
20
+ assert_equal o6, observations[5].object
21
+ assert_equal o7, observations[6].object
22
+
23
+ assert_equal 0.6786, observations[0].rating
24
+ assert_equal 0.6429, observations[1].rating
25
+ assert_equal 0.6429, observations[2].rating
26
+ assert_equal 0.6071, observations[3].rating
27
+ assert_equal 0.6071, observations[4].rating
28
+ assert_equal 0.5714, observations[5].rating
29
+ assert_equal 0.3929, observations[6].rating
30
+ end
31
+
32
+ def test_returns_ordered_observation_from_different_size_hashes
33
+ o1 = {one: 1, two: 2, three: 3, four: 4, five: 5}
34
+ o2 = {one: 1, two: 2, three: 3, four: 4}
35
+ o3 = {zero: 0, one: 1, two: 2, three: 3, four: 0}
36
+ o4 = {one: 1, two: 2, three: 0, four: 4}
37
+ o5 = {one: 1, two: 2, three: 0, four: 0, ten: 10, eleven: 11}
38
+ o6 = {one: 1, two: 0}
39
+ o7 = {one: 0, two: 0, three: 0}
40
+
41
+ observations = Observance.run(o1, o2, o3, o4, o5, o6, o7)
42
+
43
+ assert_equal o4, observations[0].object
44
+ assert_equal o2, observations[1].object
45
+ assert_equal o1, observations[2].object
46
+ assert_equal o3, observations[3].object
47
+ assert_equal o5, observations[4].object
48
+ assert_equal o6, observations[5].object
49
+ assert_equal o7, observations[6].object
50
+
51
+ assert_equal 0.5054, observations[0].rating
52
+ assert_equal 0.5048, observations[1].rating
53
+ assert_equal 0.4793, observations[2].rating
54
+ assert_equal 0.4491, observations[3].rating
55
+ assert_equal 0.3965, observations[4].rating
56
+ assert_equal 0.3095, observations[5].rating
57
+ end
58
+
59
+ def test_returns_ordered_observation_from_same_size_arrays
60
+ o1 = [1, 2, 3, 4]
61
+ o2 = [1, 2, 3, 4]
62
+ o3 = [1, 2, 3, 0]
63
+ o4 = [1, 2, 0, 4]
64
+ o5 = [1, 2, 0, 0]
65
+ o6 = [1, 0, 0, 0]
66
+ o7 = [0, 0, 0, 0]
67
+
68
+ observations = Observance.run(o1, o2, o3, o4, o5, o6, o7)
69
+
70
+ assert_equal o5, observations[0].object
71
+ assert_equal o3, observations[1].object
72
+ assert_equal o4, observations[2].object
73
+ assert_equal o1, observations[3].object
74
+ assert_equal o2, observations[4].object
75
+ assert_equal o6, observations[5].object
76
+ assert_equal o7, observations[6].object
77
+
78
+ assert_equal 0.6786, observations[0].rating
79
+ assert_equal 0.6429, observations[1].rating
80
+ assert_equal 0.6429, observations[2].rating
81
+ assert_equal 0.6071, observations[3].rating
82
+ assert_equal 0.6071, observations[4].rating
83
+ assert_equal 0.5714, observations[5].rating
84
+ end
85
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: observance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - simcap
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Returns the most likely observation given multiple observations
56
70
  email:
57
71
  - simcap@fastmail.com
@@ -60,7 +74,6 @@ extensions: []
60
74
  extra_rdoc_files: []
61
75
  files:
62
76
  - ".gitignore"
63
- - ".travis.yml"
64
77
  - Gemfile
65
78
  - LICENSE.txt
66
79
  - README.md
@@ -68,8 +81,9 @@ files:
68
81
  - lib/observance.rb
69
82
  - lib/observance/version.rb
70
83
  - observance.gemspec
84
+ - test/hash_extensions_test.rb
71
85
  - test/minitest_helper.rb
72
- - test/test_observance.rb
86
+ - test/observance_test.rb
73
87
  homepage: ''
74
88
  licenses:
75
89
  - MIT
@@ -95,5 +109,6 @@ signing_key:
95
109
  specification_version: 4
96
110
  summary: Returns the most likely observation given multiple observations
97
111
  test_files:
112
+ - test/hash_extensions_test.rb
98
113
  - test/minitest_helper.rb
99
- - test/test_observance.rb
114
+ - test/observance_test.rb
data/.travis.yml DELETED
@@ -1,3 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.1.5
@@ -1,11 +0,0 @@
1
- require 'minitest_helper'
2
-
3
- class TestObservance < MiniTest::Unit::TestCase
4
- def test_that_it_has_a_version_number
5
- refute_nil ::Observance::VERSION
6
- end
7
-
8
- def test_it_does_something_useful
9
- assert false
10
- end
11
- end