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 +4 -4
- data/README.md +83 -20
- data/Rakefile +1 -0
- data/lib/observance.rb +65 -2
- data/lib/observance/version.rb +1 -1
- data/observance.gemspec +1 -0
- data/test/hash_extensions_test.rb +56 -0
- data/test/observance_test.rb +85 -0
- metadata +19 -4
- data/.travis.yml +0 -3
- data/test/test_observance.rb +0 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 553f83a148f0ff71cd4f7e0172afc68fc359d5ab
|
4
|
+
data.tar.gz: c56c3391312d58cf455d69c1121cf079aaa5ef8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3083ccf959eec5e7bb8f1e4e84b25f2e18308c0066abd1e79eaf447ec0cb33b780f48b5b896e04e160353c98625c6cc914891acfb5758ccf4040ae72ee4f636c
|
7
|
+
data.tar.gz: 09894761a0ca773960edb616d7d03b19be6ccef4032c9c19cc7f9b09310682c17066fed76022024e784eac43ca08b507bb64d6a3c463439fffe2add4d8cf9050
|
data/README.md
CHANGED
@@ -1,61 +1,124 @@
|
|
1
1
|
# Observance
|
2
2
|
|
3
|
-
Given
|
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
|
-
|
6
|
-
displaying a number from 1 to 10.
|
7
|
-
|
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
|
-
|
16
|
-
|
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
|
-
|
25
|
-
|
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.
|
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 `
|
36
|
-
can be a
|
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,
|
89
|
+
jane_observations = Scores.new(5, 5, 7, 4)
|
90
|
+
bill_observations = Scores.new(5, 6, 6, 4)
|
48
91
|
|
49
|
-
Observance.
|
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
|
-
|
96
|
+
### Observance output
|
53
97
|
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
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
data/lib/observance.rb
CHANGED
@@ -1,5 +1,68 @@
|
|
1
|
-
require
|
1
|
+
require 'observance/version'
|
2
2
|
|
3
3
|
module Observance
|
4
|
-
|
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
|
data/lib/observance/version.rb
CHANGED
data/observance.gemspec
CHANGED
@@ -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.
|
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/
|
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/
|
114
|
+
- test/observance_test.rb
|
data/.travis.yml
DELETED
data/test/test_observance.rb
DELETED