observance 0.0.1 → 0.0.2
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.
- 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