knnball 0.0.5 → 0.0.6
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.
- data/README.md +15 -12
- data/knnball.gemspec +7 -4
- data/lib/knnball/ball.rb +21 -4
- data/lib/knnball/kdtree.rb +110 -16
- data/lib/knnball/result_set.rb +57 -0
- data/lib/knnball.rb +7 -6
- data/test/specs/ball_spec.rb +18 -18
- data/test/specs/data.json +1 -1
- data/test/specs/kdtree_spec.rb +86 -12
- data/test/specs/knnball_spec.rb +105 -49
- data/test/specs/result_set_spec.rb +158 -0
- data/test/specs/spec_helpers.rb +18 -0
- data/test/units/stat_test.rb +2 -2
- metadata +8 -4
data/README.md
CHANGED
@@ -2,10 +2,10 @@ KnnBall Instruction
|
|
2
2
|
===================
|
3
3
|
|
4
4
|
KnnBall is a Ruby library that implements *Querying neareast neighbor algorithm*.
|
5
|
-
This algorithm optimize the search of the nearest point given
|
5
|
+
This algorithm optimize the search of the nearest point given a point as input.
|
6
6
|
|
7
|
-
It works with any number of dimension but
|
8
|
-
that with more than 10 dimensions, brute force approach
|
7
|
+
It works with any number of dimension but essays seems to accord on the fact
|
8
|
+
that with more than 10 dimensions, brute force approach will give better results.
|
9
9
|
|
10
10
|
In this library, each point is associated to a value,
|
11
11
|
this way the library acts as an index for multidimensional data like
|
@@ -18,23 +18,26 @@ Usage
|
|
18
18
|
require 'knnball'
|
19
19
|
|
20
20
|
data = [
|
21
|
-
{:id => 1, :
|
22
|
-
{:id => 2, :
|
23
|
-
{:id => 3, :
|
24
|
-
{:id => 4, :
|
21
|
+
{:id => 1, :point => [6.3299934, 52.32444]},
|
22
|
+
{:id => 2, :point => [3.34444, 53.23259]},
|
23
|
+
{:id => 3, :point => [4.22452, 53.243982]},
|
24
|
+
{:id => 4, :point => [4.2333424, 51.239994]},
|
25
25
|
# ...
|
26
26
|
]
|
27
27
|
|
28
28
|
index = KnnBall.build(data)
|
29
29
|
|
30
30
|
result = index.nearest([3.43353, 52.34355])
|
31
|
-
puts result # --> {:id=>2, :
|
31
|
+
puts result # --> {:id=>2, :point=>[3.34444, 53.23259]}
|
32
|
+
|
33
|
+
restults = index.nearest([3.43353, 52.34355], :limit => 3)
|
34
|
+
puts result # --> [{...}, {...}, {...}]
|
32
35
|
|
33
36
|
Some notes about the above:
|
34
37
|
|
35
|
-
*data*
|
38
|
+
*data* is given using an array of hashes.
|
36
39
|
The only requirement of an Hash instance is
|
37
|
-
to have a :
|
40
|
+
to have a :point keys containing an array of coordinate.
|
38
41
|
in the documentation one of this Hash instance will be
|
39
42
|
called a *value* and the array of coordinates a *point*.
|
40
43
|
Sticking to built-in data-type will allow you to easily
|
@@ -49,8 +52,8 @@ tree to store and retrieve the values. The nodes of the KDTree are Ball instance
|
|
49
52
|
whoose class name refer to the theory of having ball containing smaller ball and so
|
50
53
|
on. In practice, this class does not behave like a ball, but by metaphore, it may help.
|
51
54
|
|
52
|
-
*KDTree#nearest* retrieve the nearest *value* of the given *point
|
53
|
-
|
55
|
+
*KDTree#nearest* retrieve the nearest *value* of the given *point* by default or
|
56
|
+
the k nearest value if ':limit' optional argument is greater than 1.
|
54
57
|
|
55
58
|
Roadmap
|
56
59
|
-------
|
data/knnball.gemspec
CHANGED
@@ -4,12 +4,12 @@ Gem::Specification.new do |s|
|
|
4
4
|
s.rubygems_version = '1.3.5'
|
5
5
|
|
6
6
|
s.name = 'knnball'
|
7
|
-
s.version = '0.0.
|
8
|
-
s.date = '2011-
|
7
|
+
s.version = '0.0.6'
|
8
|
+
s.date = '2011-09-09'
|
9
9
|
s.rubyforge_project = 'knnball'
|
10
10
|
|
11
|
-
s.summary = "
|
12
|
-
s.description = "Implements K-Nearest Neighbor algorithm using a KDTree in Ruby."
|
11
|
+
s.summary = "Multi-dimensional nearest neighbor search"
|
12
|
+
s.description = "Implements K-Nearest Neighbor algorithm using a KDTree in Ruby. Usefull for sorting geolocation or any other multi-dimensional data."
|
13
13
|
|
14
14
|
s.authors = ["Olivier Amblet"]
|
15
15
|
s.email = 'olivier@amblet.net'
|
@@ -30,10 +30,13 @@ lib/knnball.rb
|
|
30
30
|
lib/knnball/ball.rb
|
31
31
|
lib/knnball/stat.rb
|
32
32
|
lib/knnball/kdtree.rb
|
33
|
+
lib/knnball/result_set.rb
|
33
34
|
test/specs/ball_spec.rb
|
34
35
|
test/specs/data.json
|
35
36
|
test/specs/kdtree_spec.rb
|
36
37
|
test/specs/knnball_spec.rb
|
38
|
+
test/specs/result_set_spec.rb
|
39
|
+
test/specs/spec_helpers.rb
|
37
40
|
test/units/stat_test.rb
|
38
41
|
]
|
39
42
|
# = MANIFEST =
|
data/lib/knnball/ball.rb
CHANGED
@@ -22,8 +22,8 @@ module KnnBall
|
|
22
22
|
unless (value.respond_to?(:include?) && value.respond_to?(:[]))
|
23
23
|
raise ArgumentError.new("Value must at least respond to methods include? and [].")
|
24
24
|
end
|
25
|
-
unless (value.include?(:
|
26
|
-
raise ArgumentError.new("value must contains :
|
25
|
+
unless (value.include?(:point))
|
26
|
+
raise ArgumentError.new("value must contains :point key but has only #{value.keys.inspect}")
|
27
27
|
end
|
28
28
|
@value = value
|
29
29
|
@right = right
|
@@ -32,12 +32,12 @@ module KnnBall
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def center
|
35
|
-
value[:
|
35
|
+
value[:point]
|
36
36
|
end
|
37
37
|
|
38
38
|
def nearest(target, min)
|
39
39
|
result = nil
|
40
|
-
d = [
|
40
|
+
d = [quick_distance(target), min[0]].min
|
41
41
|
if d < min[0]
|
42
42
|
min[0] = d
|
43
43
|
result = self
|
@@ -80,6 +80,18 @@ module KnnBall
|
|
80
80
|
Math.sqrt([center, coordinates].transpose.map {|a,b| (b - a)**2}.reduce {|d1,d2| d1 + d2})
|
81
81
|
end
|
82
82
|
|
83
|
+
# Quickly compute a distance using Manhattan
|
84
|
+
def quick_distance(coordinates)
|
85
|
+
distance(coordinates)
|
86
|
+
# coordinates = coordinates.center if coordinates.respond_to?(:center)
|
87
|
+
# [center, coordinates].transpose.map {|a,b| (b - a)**2}.reduce {|d1,d2| d1 + d2}
|
88
|
+
# [center, coordinates].transpose.map {|a,b| (b - a).abs}.reduce {|d1,d2| d1 + d2}
|
89
|
+
end
|
90
|
+
|
91
|
+
def count
|
92
|
+
1 + (left.nil? ? 0 : left.count) + (right.nil? ? 0 : right.count)
|
93
|
+
end
|
94
|
+
|
83
95
|
# Retrieve true if this is a leaf ball.
|
84
96
|
#
|
85
97
|
# A leaf ball has no sub_balls.
|
@@ -87,6 +99,11 @@ module KnnBall
|
|
87
99
|
@left.nil? && @right.nil?
|
88
100
|
end
|
89
101
|
|
102
|
+
# Return true if this ball has a left and a right ball
|
103
|
+
def complete?
|
104
|
+
! (@left.nil? || @right.nil?)
|
105
|
+
end
|
106
|
+
|
90
107
|
# Generate an Array from this Ball.
|
91
108
|
#
|
92
109
|
# index 0 contains the value object,
|
data/lib/knnball/kdtree.rb
CHANGED
@@ -16,30 +16,100 @@ module KnnBall
|
|
16
16
|
def initialize(root = nil)
|
17
17
|
@root = root
|
18
18
|
end
|
19
|
-
|
20
|
-
|
19
|
+
|
20
|
+
# Retrieve the nearest point from the given coord array.
|
21
|
+
#
|
22
|
+
# available keys for options are :root and :limit
|
23
|
+
#
|
24
|
+
# Wikipedia tell us (excerpt from url http://en.wikipedia.org/wiki/Kd%5Ftree#Nearest%5Fneighbor%5Fsearch)
|
25
|
+
#
|
26
|
+
# Searching for a nearest neighbour in a k-d tree proceeds as follows:
|
27
|
+
# 1. Starting with the root node, the algorithm moves down the tree recursively,
|
28
|
+
# in the same way that it would if the search point were being inserted
|
29
|
+
# (i.e. it goes left or right depending on whether the point is less than or
|
30
|
+
# greater than the current node in the split dimension).
|
31
|
+
# 2. Once the algorithm reaches a leaf node, it saves that node point as the "current best"
|
32
|
+
# 3. The algorithm unwinds the recursion of the tree, performing the following steps at each node:
|
33
|
+
# 1. If the current node is closer than the current best, then it becomes the current best.
|
34
|
+
# 2. The algorithm checks whether there could be any points on the other side of the splitting
|
35
|
+
# plane that are closer to the search point than the current best. In concept, this is done
|
36
|
+
# by intersecting the splitting hyperplane with a hypersphere around the search point that
|
37
|
+
# has a radius equal to the current nearest distance. Since the hyperplanes are all axis-aligned
|
38
|
+
# this is implemented as a simple comparison to see whether the difference between the splitting
|
39
|
+
# coordinate of the search point and current node is less than the distance (overall coordinates) from
|
40
|
+
# the search point to the current best.
|
41
|
+
# 1. If the hypersphere crosses the plane, there could be nearer points on the other side of the plane,
|
42
|
+
# so the algorithm must move down the other branch of the tree from the current node looking for
|
43
|
+
# closer points, following the same recursive process as the entire search.
|
44
|
+
# 2. If the hypersphere doesn't intersect the splitting plane, then the algorithm continues walking
|
45
|
+
# up the tree, and the entire branch on the other side of that node is eliminated.
|
46
|
+
# 4. When the algorithm finishes this process for the root node, then the search is complete.
|
47
|
+
#
|
48
|
+
# Generally the algorithm uses squared distances for comparison to avoid computing square roots. Additionally,
|
49
|
+
# it can save computation by holding the squared current best distance in a variable for comparison.
|
50
|
+
def nearest(coord, options = {})
|
21
51
|
return nil if root.nil?
|
22
52
|
return nil if coord.nil?
|
23
53
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
54
|
+
results = (options[:results] ? options[:results] : ResultSet.new({limit: options[:limit] || 1}))
|
55
|
+
root_ball = options[:root] || root
|
56
|
+
|
57
|
+
# keep the stack while finding the leaf best match.
|
58
|
+
parents = []
|
59
|
+
|
60
|
+
best_balls = []
|
61
|
+
in_target = []
|
62
|
+
|
63
|
+
# Move down to best match
|
64
|
+
current_best = nil
|
65
|
+
current = root_ball
|
66
|
+
while current_best.nil?
|
67
|
+
dim = current.dimension-1
|
68
|
+
if(current.complete?)
|
69
|
+
next_ball = (coord[dim] <= current.center[dim] ? current.left : current.right)
|
70
|
+
elsif(current.leaf?)
|
71
|
+
next_ball = nil
|
72
|
+
else
|
73
|
+
next_ball = (current.left.nil? ? current.right : current.left)
|
74
|
+
end
|
75
|
+
if ( next_ball.nil? )
|
76
|
+
current_best = current
|
77
|
+
else
|
78
|
+
parents.push current
|
79
|
+
current = next_ball
|
80
|
+
end
|
81
|
+
end
|
28
82
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
83
|
+
# Move up to check split
|
84
|
+
parents.reverse!
|
85
|
+
results.add(current_best.quick_distance(coord), current_best.value)
|
86
|
+
parents.each do |current_node|
|
87
|
+
dist = current_node.quick_distance(coord)
|
88
|
+
if results.eligible?( dist )
|
89
|
+
results.add(dist, current_node.value)
|
90
|
+
end
|
91
|
+
|
92
|
+
dim = current_node.dimension-1
|
93
|
+
if current_node.complete?
|
94
|
+
# retrieve the splitting node.
|
95
|
+
split_node = (coord[dim] <= current_node.center[dim] ? current_node.right : current_node.left)
|
96
|
+
best_dist = results.barrier_value
|
97
|
+
if( (coord[dim] - current_node.center[dim]).abs < best_dist)
|
98
|
+
# potential match, need to investigate subtree
|
99
|
+
nearest(coord, root: split_node, results: results)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
return results.limit == 1 ? results.items.first : results.items
|
34
104
|
end
|
35
105
|
|
36
106
|
# Retrieve the parent to which this coord should belongs to
|
37
|
-
def
|
107
|
+
def parent_ball(coord)
|
38
108
|
current = root
|
39
|
-
|
109
|
+
d_idx = current.dimension-1
|
40
110
|
result = nil
|
41
111
|
while(result.nil?)
|
42
|
-
if(coord[
|
112
|
+
if(coord[d_idx] <= current.center[d_idx])
|
43
113
|
if current.left.nil?
|
44
114
|
result = current
|
45
115
|
else
|
@@ -52,7 +122,7 @@ module KnnBall
|
|
52
122
|
current = current.right
|
53
123
|
end
|
54
124
|
end
|
55
|
-
|
125
|
+
d_idx = current.dimension-1
|
56
126
|
end
|
57
127
|
return result
|
58
128
|
end
|
@@ -76,6 +146,11 @@ module KnnBall
|
|
76
146
|
return res
|
77
147
|
end
|
78
148
|
|
149
|
+
# naive implementation
|
150
|
+
def count
|
151
|
+
root.count
|
152
|
+
end
|
153
|
+
|
79
154
|
private
|
80
155
|
|
81
156
|
def each_ball(b, &proc)
|
@@ -87,4 +162,23 @@ module KnnBall
|
|
87
162
|
return
|
88
163
|
end
|
89
164
|
end
|
90
|
-
end
|
165
|
+
end
|
166
|
+
|
167
|
+
|
168
|
+
__END__
|
169
|
+
|
170
|
+
,x,
|
171
|
+
/ \
|
172
|
+
x x
|
173
|
+
/ o `x
|
174
|
+
| x´ `--x
|
175
|
+
x
|
176
|
+
|
177
|
+
,x,
|
178
|
+
/ | \
|
179
|
+
x | x\
|
180
|
+
/ |---`x----->
|
181
|
+
| |o | |
|
182
|
+
x | `x´
|
183
|
+
| |
|
184
|
+
v v
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Copyright (C) 2011 Olivier Amblet <http://olivier.amblet.net>
|
4
|
+
#
|
5
|
+
# knnball is freely distributable under the terms of an MIT license.
|
6
|
+
# See LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
7
|
+
|
8
|
+
module KnnBall
|
9
|
+
# This class represents a ball in the tree.
|
10
|
+
#
|
11
|
+
# The value of this ball will be its center
|
12
|
+
# while its radius is the distance between the center and
|
13
|
+
# the most far sub-ball.
|
14
|
+
class ResultSet
|
15
|
+
attr_reader :limit, :barrier_value
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@limit = options[:limit] || 10
|
19
|
+
@items = []
|
20
|
+
@barrier_value = options[:barrier_value]
|
21
|
+
end
|
22
|
+
|
23
|
+
def eligible?(value)
|
24
|
+
@barrier_value.nil? || @items.count < limit || value < @barrier_value
|
25
|
+
end
|
26
|
+
|
27
|
+
def add(value, item)
|
28
|
+
return false unless(eligible?(value))
|
29
|
+
|
30
|
+
if @barrier_value.nil? || value > @barrier_value || @items.empty?
|
31
|
+
@barrier_value = value
|
32
|
+
@items.push [value, item]
|
33
|
+
else
|
34
|
+
idx = 0
|
35
|
+
begin
|
36
|
+
while(value > @items[idx][0])
|
37
|
+
idx = idx + 1
|
38
|
+
end
|
39
|
+
rescue
|
40
|
+
raise "ArrayOutOfBound for #{value} at index #{idx} for a limit of #{limit}"
|
41
|
+
end
|
42
|
+
@items.insert idx, [value, item]
|
43
|
+
end
|
44
|
+
|
45
|
+
if @items.count > limit
|
46
|
+
@items.pop
|
47
|
+
end
|
48
|
+
|
49
|
+
@barrier_value = @items.last[0]
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
def items
|
54
|
+
@items.map {|i| i[1]}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/knnball.rb
CHANGED
@@ -14,22 +14,23 @@ module KnnBall
|
|
14
14
|
autoload :Ball, 'knnball/ball'
|
15
15
|
autoload :Stat, 'knnball/stat'
|
16
16
|
autoload :KDTree, 'knnball/kdtree'
|
17
|
+
autoload :ResultSet, 'knnball/result_set'
|
17
18
|
|
18
19
|
# Retrieve a new BallTree given an array of input values.
|
19
20
|
#
|
20
21
|
# Each data entry in the array is a Hash containing
|
21
|
-
# keys :value and :
|
22
|
-
# [ {:value => 1, :
|
23
|
-
# {:value => 2, :
|
22
|
+
# keys :value and :point, an array of position (one per dimension)
|
23
|
+
# [ {:value => 1, :point => [1.23, 2.34, -1.23, -22.3]},
|
24
|
+
# {:value => 2, :point => [-2.33, 4.2, 1.23, 332.2]} ]
|
24
25
|
#
|
25
|
-
# @param data an array of Hash containing :value and :
|
26
|
+
# @param data an array of Hash containing :value and :point key
|
26
27
|
#
|
27
28
|
# @see KnnBall::KDTree#initialize
|
28
29
|
def self.build(data)
|
29
30
|
if(data.nil? || data.empty?)
|
30
31
|
raise ArgumentError.new("data argument must be a not empty Array")
|
31
32
|
end
|
32
|
-
max_dimension = data.first[:
|
33
|
+
max_dimension = data.first[:point].size
|
33
34
|
kdtree = KDTree.new(max_dimension)
|
34
35
|
kdtree.root = generate(data, max_dimension)
|
35
36
|
return kdtree
|
@@ -51,7 +52,7 @@ module KnnBall
|
|
51
52
|
# and that every point on the right are of greater value
|
52
53
|
# than the median. They are not more sorted than that.
|
53
54
|
median_idx = Stat.median_index(data)
|
54
|
-
value = Stat.median!(data) {|v1, v2| v1[:
|
55
|
+
value = Stat.median!(data) {|v1, v2| v1[:point][actual_dimension-1] <=> v2[:point][actual_dimension-1]}
|
55
56
|
ball = Ball.new(value)
|
56
57
|
|
57
58
|
actual_dimension = (max_dimension == actual_dimension ? 1 : actual_dimension)
|
data/test/specs/ball_spec.rb
CHANGED
@@ -17,7 +17,7 @@ module KnnBall
|
|
17
17
|
|
18
18
|
describe "Leaf balls" do
|
19
19
|
before :each do
|
20
|
-
@value = {:id => 1, :
|
20
|
+
@value = {:id => 1, :point => [1,2,3]}
|
21
21
|
@ball = Ball.new(@value)
|
22
22
|
end
|
23
23
|
|
@@ -26,7 +26,7 @@ module KnnBall
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it "must have a center equals to the value location" do
|
29
|
-
@ball.center.must_equal @value[:
|
29
|
+
@ball.center.must_equal @value[:point]
|
30
30
|
end
|
31
31
|
|
32
32
|
it "must convert itself to an Array instance" do
|
@@ -40,8 +40,8 @@ module KnnBall
|
|
40
40
|
|
41
41
|
describe "Standard Balls" do
|
42
42
|
before :each do
|
43
|
-
@value = {:id => 1, :
|
44
|
-
@ball = Ball.new(@value, 1, Ball.new({:id => 3, :
|
43
|
+
@value = {:id => 1, :point => [1,2,3]}
|
44
|
+
@ball = Ball.new(@value, 1, Ball.new({:id => 3, :point => [-1, -2, -3]}), Ball.new({:id => 2, :point => [2, 3, 4]}))
|
45
45
|
end
|
46
46
|
|
47
47
|
it "wont be a leaf" do
|
@@ -49,35 +49,35 @@ module KnnBall
|
|
49
49
|
end
|
50
50
|
|
51
51
|
it "must_be_centered_at_the_ball_value_location" do
|
52
|
-
@ball.center.must_equal @value[:
|
52
|
+
@ball.center.must_equal @value[:point]
|
53
53
|
end
|
54
54
|
|
55
55
|
it "must convert itself to an Array instance" do
|
56
56
|
@ball.to_a.must_equal([
|
57
57
|
@value,
|
58
|
-
[{:id => 3, :
|
59
|
-
[{:id => 2, :
|
58
|
+
[{:id => 3, :point => [-1, -2, -3]}, nil, nil],
|
59
|
+
[{:id => 2, :point => [2, 3, 4]}, nil, nil]
|
60
60
|
])
|
61
61
|
end
|
62
62
|
|
63
63
|
it "must convert itself to a Hash instance" do
|
64
64
|
@ball.to_h.must_equal({:value => @value,
|
65
|
-
:left => {:value => {:id => 3, :
|
66
|
-
:right => {:value => {:id => 2, :
|
65
|
+
:left => {:value => {:id => 3, :point => [-1, -2, -3]}, :left => nil, :right => nil},
|
66
|
+
:right => {:value => {:id => 2, :point => [2, 3, 4]}, :left => nil, :right => nil}
|
67
67
|
})
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
71
|
describe "Ball with sub-balls" do
|
72
72
|
before :each do
|
73
|
-
@value = {:id => 1, :
|
73
|
+
@value = {:id => 1, :point => [1,2,3]}
|
74
74
|
@leaf_1 = Ball.new(@value)
|
75
|
-
@leaf_2 = Ball.new({:id => 2, :
|
76
|
-
@leaf_3 = Ball.new({:id => 3, :
|
77
|
-
@leaf_4 = Ball.new({:id => 4, :
|
78
|
-
@sub_ball_1 = Ball.new({:id => 5, :
|
79
|
-
@sub_ball_2 = Ball.new({:id => 6, :
|
80
|
-
@ball = Ball.new({:id => 7, :
|
75
|
+
@leaf_2 = Ball.new({:id => 2, :point => [2, 3, 4]})
|
76
|
+
@leaf_3 = Ball.new({:id => 3, :point => [-1, -2 , -5]})
|
77
|
+
@leaf_4 = Ball.new({:id => 4, :point => [-3, -2, -2]})
|
78
|
+
@sub_ball_1 = Ball.new({:id => 5, :point => [1.4, 2, 2.5]}, 1, @leaf_1, @leaf_2)
|
79
|
+
@sub_ball_2 = Ball.new({:id => 6, :point => [-2, -1.9, -3]}, 1, @leaf_3, @leaf_4)
|
80
|
+
@ball = Ball.new({:id => 7, :point => [0, 0, 0]}, 1, @sub_ball_1, @sub_ball_2)
|
81
81
|
end
|
82
82
|
|
83
83
|
it "must be centered at (0,0,0)" do
|
@@ -92,8 +92,8 @@ module KnnBall
|
|
92
92
|
end
|
93
93
|
|
94
94
|
it "retrieve the correct distance" do
|
95
|
-
b1 = Ball.new({:id => 2, :
|
96
|
-
b2 = Ball.new({:id => 3, :
|
95
|
+
b1 = Ball.new({:id => 2, :point => [2, 3, 4]})
|
96
|
+
b2 = Ball.new({:id => 3, :point => [-1, -2 , -5]})
|
97
97
|
b1.distance(b2).must_equal(Math.sqrt(115))
|
98
98
|
end
|
99
99
|
end
|
data/test/specs/data.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
[{"id":"Dr Achermann Romeo","
|
1
|
+
[{"id":"Dr Achermann Romeo","point":[47.0495221,8.3079993]},{"id":"Dresse Achermann-Bieri Ursula","point":[47.0525778,8.3052475]},{"id":"M. Ackermann Christian","point":[46.9406862,7.3991925]},{"id":"Dr Ackermann Roland","point":[47.3538085,7.9035746]},{"id":"Dr Adank-Sailer Gabrielle","point":[47.1359809,7.2447156]},{"id":"Dr Aebersold Christian","point":[47.1262539,7.2763632]},{"id":"Dresse Aebersold Gaby","point":[47.1262539,7.2763632]},{"id":"M. Aellen Jean-Marc","point":[46.1980464,6.1483353]},{"id":"M. Aerni Christian","point":[46.18612,6.118075]},{"id":"Dr Akermann Felix","point":[47.1697675,9.4766358]},{"id":"Dr Albrecht Silvia","point":[47.1655638,7.5936764]},{"id":"M. Alder Ernst","point":[47.6833333,8.75]},{"id":"Dr Althaus Marc-André","point":[46.4810917,6.4571243]},{"id":"Dr Amati Francesca","point":[46.5230847,6.640415]},{"id":"Anklin Bernard","point":[47.3920158,8.5392456]},{"id":"Dr Bachelin Pierre","point":[46.6668682,6.5107242]},{"id":"Dr Backes Hans-Ulrich","point":[47.4238477,9.3686547]},{"id":"Badorff Cornel","point":[47.4977665,8.7270143]},{"id":"Dr Bagutti Carlo","point":[46.516054,6.608998]},{"id":"Dr Ballmer Peter Matthias","point":[46.7617201,7.6279035]},{"id":"Dr Bandi-Ott Elisabeth","point":[47.3839285,8.54832]},{"id":"Dr Barandun Jürg","point":[47.3519079,8.5762373]},{"id":"Dr Barras Bernard","point":[46.2270208,7.3533351]},{"id":"Dr Beck Thomas","point":[46.7571286,7.6305141]},{"id":"Dr Bedat Bernard","point":[46.2785274,6.1684023]},{"id":"M. Bekkering Anton","point":[47.4226784,9.3184235]},{"id":"Dresse Benz Gabrielle","point":[46.2304522,7.363867]},{"id":"M. Benz Martin","point":[46.2304522,7.363867]},{"id":"M. Berchten Anthony","point":[46.3873762,6.2208354]},{"id":"M. Berdoz Jean-Marc","point":[46.2124628,6.131694]},{"id":"Mme Berdoz Nicole","point":[46.2124628,6.131694]},{"id":"Dr Berger Hanspeter","point":[46.684936,7.8499151]},{"id":"Dr Berghoff Ueli","point":[47.2217408,8.6722444]},{"id":"Dr Bernasconi Cristiano","point":[46.3601201,8.971514]},{"id":"Dr Bernhart Felix","point":[46.9286035,7.447995]},{"id":"Dr Beuing Markus","point":[46.4965828,9.8382621]},{"id":"Dr Beyeler Jürg","point":[47.3805257,8.5428647]},{"id":"Dr Bianchi Michele","point":[46.0056877,8.9407073]},{"id":"Dr Bickel Andreas","point":[47.2287339,8.8268529]},{"id":"Dr Biedert Roland","point":[47.1333363,7.2612937]}]
|
data/test/specs/kdtree_spec.rb
CHANGED
@@ -9,49 +9,123 @@
|
|
9
9
|
|
10
10
|
require 'minitest/autorun'
|
11
11
|
require 'knnball'
|
12
|
-
|
12
|
+
require_relative 'spec_helpers'
|
13
13
|
|
14
14
|
module KnnBall
|
15
15
|
|
16
16
|
describe KDTree do
|
17
17
|
|
18
|
+
include KnnBall::SpecHelpers
|
19
|
+
|
18
20
|
describe "building the tree" do
|
19
21
|
it "must be an empty tree without params" do
|
20
22
|
KDTree.new.must_be_empty
|
21
23
|
end
|
22
24
|
|
23
25
|
it "wont be an empty tree with data" do
|
24
|
-
KDTree.new(Ball.new({:id => 1, :
|
26
|
+
KDTree.new(Ball.new({:id => 1, :point => [1]})).wont_be_empty
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
28
30
|
describe "find the nearest ball" do
|
29
31
|
before :each do
|
30
|
-
root = Ball.new(
|
31
|
-
|
32
|
-
Ball.new({:id =>
|
33
|
-
Ball.new({:id =>
|
32
|
+
root = Ball.new(
|
33
|
+
{:id => 4, :point => [5]}, 1,
|
34
|
+
Ball.new({:id => 2, :point => [2]}, 1,
|
35
|
+
Ball.new({:id => 1, :point => [1]}), Ball.new({:id => 3, :point => [3]})
|
36
|
+
),
|
37
|
+
Ball.new({:id => 6, :point => [13]}, 1,
|
38
|
+
Ball.new({:id => 5, :point => [8]}),
|
39
|
+
Ball.new({:id => 7, :point => [21]}, 1, nil, Ball.new({:id => 8, :point => [34]}))
|
40
|
+
)
|
34
41
|
)
|
35
42
|
@ball_tree = KDTree.new(root)
|
36
43
|
end
|
37
44
|
|
38
45
|
it "return a matching location" do
|
39
46
|
@ball_tree.nearest([3])[:id].must_equal(3)
|
47
|
+
@ball_tree.nearest([35])[:id].must_equal(8)
|
48
|
+
@ball_tree.nearest([5])[:id].must_equal(4)
|
49
|
+
@ball_tree.nearest([2])[:id].must_equal(2)
|
40
50
|
end
|
41
51
|
end
|
42
52
|
|
43
53
|
describe "find the parent for coordinates" do
|
44
54
|
before :each do
|
45
|
-
@root = Ball.new({:id => 4, :
|
46
|
-
Ball.new({:id => 2, :
|
47
|
-
Ball.new({:id => 6, :
|
48
|
-
Ball.new({:id => 7, :
|
55
|
+
@root = Ball.new({:id => 4, :point => [5, 7]}, 1,
|
56
|
+
Ball.new({:id => 2, :point => [3, 4]}, 1, Ball.new({:id => 1, :point => [2, 2]}), Ball.new({:id => 3, :point => [4, 8]})),
|
57
|
+
Ball.new({:id => 6, :point => [13, 4]}, 1, Ball.new({:id => 5, :point => [8, 1]}),
|
58
|
+
Ball.new({:id => 7, :point => [21, 6]}, 1, Ball.new({:id => 8, :point => [34, 5]})))
|
49
59
|
)
|
50
60
|
@ball_tree = KDTree.new(@root)
|
51
61
|
end
|
52
62
|
|
53
63
|
it "return the nearest parent" do
|
54
|
-
@ball_tree.
|
64
|
+
@ball_tree.parent_ball([13.2, 4.5]).value[:id].must_equal(8)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "find the best nearest ball" do
|
69
|
+
points = [
|
70
|
+
{:id => 1, :point => [2, 2]},
|
71
|
+
{:id => 2, :point => [3, 4]},
|
72
|
+
{:id => 3, :point => [4, 8]},
|
73
|
+
{:id => 4, :point => [5, 7]},
|
74
|
+
{:id => 5, :point => [8, 1]},
|
75
|
+
{:id => 6, :point => [13, 4]},
|
76
|
+
{:id => 7, :point => [21, 6]},
|
77
|
+
{:id => 8, :point => [34, 5]},
|
78
|
+
]
|
79
|
+
|
80
|
+
before do
|
81
|
+
@root = Ball.new({:id => 4, :point => [5, 7]}, 1,
|
82
|
+
Ball.new({:id => 2, :point => [3, 4]}, 1,
|
83
|
+
Ball.new({:id => 1, :point => [2, 2]}), Ball.new({:id => 3, :point => [4, 8]})
|
84
|
+
),
|
85
|
+
Ball.new({:id => 6, :point => [13, 4]}, 1,
|
86
|
+
Ball.new({:id => 5, :point => [8, 1]}),
|
87
|
+
Ball.new({:id => 7, :point => [21, 6]}, 1,
|
88
|
+
nil,
|
89
|
+
Ball.new({:id => 8, :point => [34, 5]})
|
90
|
+
)
|
91
|
+
)
|
92
|
+
)
|
93
|
+
@tree = KDTree.new(@root)
|
94
|
+
end
|
95
|
+
|
96
|
+
points.each do |p|
|
97
|
+
it "Should retrieve point #{p} if the exact location is given" do
|
98
|
+
assert_equal p, @tree.nearest(p[:point])
|
99
|
+
end
|
100
|
+
|
101
|
+
p_near = p[:point].map {|c| c-0.1}
|
102
|
+
it "Should retrieve point #{p} if #{p_near} is given" do
|
103
|
+
assert_equal p, @tree.nearest(p_near)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "Should retrieve 2 nearest points in the correct order for point #{p}" do
|
107
|
+
# Points might be different as long as distance are equals
|
108
|
+
expected = brute_force(p, points, 2).map do |r|
|
109
|
+
Math.sqrt( (p[:point][0]-r[:point][0])**2 + (p[:point][1]-r[:point][1])**2 )
|
110
|
+
end
|
111
|
+
results = @tree.nearest(p[:point], :limit => 2).map do |r|
|
112
|
+
Math.sqrt( (p[:point][0]-r[:point][0])**2 + (p[:point][1]-r[:point][1])**2 )
|
113
|
+
end
|
114
|
+
assert_equal expected, results
|
115
|
+
end
|
116
|
+
|
117
|
+
it "Should retrieve 5 nearest points in the correct order for point #{p}" do
|
118
|
+
# Points might be different as long as distance are equals
|
119
|
+
bf = brute_force(p, points, 5)
|
120
|
+
expected = bf.map do |r|
|
121
|
+
Math.sqrt( (p[:point][0]-r[:point][0])**2 + (p[:point][1]-r[:point][1])**2 )
|
122
|
+
end
|
123
|
+
nn = @tree.nearest(p[:point], :limit => 5)
|
124
|
+
results = nn.map do |r|
|
125
|
+
Math.sqrt( (p[:point][0]-r[:point][0])**2 + (p[:point][1]-r[:point][1])**2 )
|
126
|
+
end
|
127
|
+
assert_equal expected, results, "bf: #{bf.inspect} -- nn: #{nn.inspect}"
|
128
|
+
end
|
55
129
|
end
|
56
130
|
end
|
57
131
|
|
@@ -61,7 +135,7 @@ module KnnBall
|
|
61
135
|
end
|
62
136
|
|
63
137
|
it "Should return a tree array if not nil" do
|
64
|
-
KDTree.new(Ball.new({:id => 1, :
|
138
|
+
KDTree.new(Ball.new({:id => 1, :point => [1, 2, 3]})).to_a.must_equal [{:id => 1, :point => [1,2,3]}, nil, nil]
|
65
139
|
end
|
66
140
|
end
|
67
141
|
end
|
data/test/specs/knnball_spec.rb
CHANGED
@@ -10,8 +10,11 @@
|
|
10
10
|
require 'minitest/autorun'
|
11
11
|
require 'knnball'
|
12
12
|
require 'json'
|
13
|
+
require_relative 'spec_helpers'
|
13
14
|
|
14
15
|
describe KnnBall do
|
16
|
+
include KnnBall::SpecHelpers
|
17
|
+
|
15
18
|
before do
|
16
19
|
@ball_tree = MiniTest::Mock.new
|
17
20
|
end
|
@@ -19,34 +22,34 @@ describe KnnBall do
|
|
19
22
|
describe "when asked to build the tree" do
|
20
23
|
it "must retrieve a KDTree instance" do
|
21
24
|
KnnBall.build([
|
22
|
-
{:id => 1, :
|
23
|
-
{:id => 2, :
|
25
|
+
{:id => 1, :point => [1.0,1.0]},
|
26
|
+
{:id => 2, :point => [2.0, 3.0]}
|
24
27
|
]).must_be :kind_of?, KnnBall::KDTree
|
25
28
|
end
|
26
29
|
|
27
30
|
it "must build a one dimension tree correctly" do
|
28
31
|
tree = KnnBall.build(
|
29
|
-
[{:id => 2, :
|
30
|
-
{:id => 3, :
|
31
|
-
{:id => 1, :
|
32
|
+
[{:id => 2, :point => [2]},
|
33
|
+
{:id => 3, :point => [3]},
|
34
|
+
{:id => 1, :point => [1]}]
|
32
35
|
)
|
33
|
-
tree.root.value.must_equal({:id => 2, :
|
34
|
-
tree.root.left.value.must_equal({:id => 1, :
|
36
|
+
tree.root.value.must_equal({:id => 2, :point => [2]})
|
37
|
+
tree.root.left.value.must_equal({:id => 1, :point => [1]})
|
35
38
|
tree.root.left.left.must_be_nil
|
36
39
|
tree.root.left.right.must_be_nil
|
37
40
|
tree.root.right.wont_be_nil
|
38
|
-
tree.root.right.value.must_equal({:id => 3, :
|
41
|
+
tree.root.right.value.must_equal({:id => 3, :point => [3]})
|
39
42
|
|
40
43
|
KnnBall.build([
|
41
|
-
{:id => 1, :
|
42
|
-
{:id => 2, :
|
43
|
-
{:id => 3, :
|
44
|
-
{:id => 4, :
|
45
|
-
{:id => 5, :
|
46
|
-
{:id => 6, :
|
47
|
-
{:id => 7, :
|
48
|
-
{:id => 8, :
|
49
|
-
]).root.value.must_equal({:id => 4, :
|
44
|
+
{:id => 1, :point => [1]},
|
45
|
+
{:id => 2, :point => [2]},
|
46
|
+
{:id => 3, :point => [3]},
|
47
|
+
{:id => 4, :point => [5]},
|
48
|
+
{:id => 5, :point => [8]},
|
49
|
+
{:id => 6, :point => [13]},
|
50
|
+
{:id => 7, :point => [21]},
|
51
|
+
{:id => 8, :point => [34]}
|
52
|
+
]).root.value.must_equal({:id => 4, :point => [5]})
|
50
53
|
end
|
51
54
|
end
|
52
55
|
|
@@ -63,54 +66,107 @@ describe KnnBall do
|
|
63
66
|
end
|
64
67
|
|
65
68
|
describe "when asked to find the neareast location" do
|
69
|
+
before :each do
|
70
|
+
json = File.open(File.join(File.dirname(__FILE__), 'data.json'), 'r:utf-8').read
|
71
|
+
@data = JSON.parse(json)
|
72
|
+
@data = @data.map do |l|
|
73
|
+
h = {}
|
74
|
+
l.each {|k,v| h[k.to_sym] = v}
|
75
|
+
h
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
66
79
|
it "retrieve the nearest location" do
|
67
80
|
result = KnnBall.find_knn(@ball_tree, [1, 1, 1, 1])
|
68
81
|
result.must_be :kind_of?, Array
|
69
82
|
end
|
70
83
|
|
71
84
|
it "retrieve the same results as a brute force approach" do
|
72
|
-
|
73
|
-
|
74
|
-
data
|
75
|
-
|
76
|
-
|
77
|
-
|
85
|
+
tree = KnnBall.build(@data)
|
86
|
+
msgs = []
|
87
|
+
@data.each do |p|
|
88
|
+
brute_force_result = brute_force(p, @data)
|
89
|
+
p[:point].must_equal(brute_force_result[:point])
|
90
|
+
nn_result = tree.nearest(p[:point])
|
91
|
+
if(nn_result[:point] != brute_force_result[:point])
|
92
|
+
msgs << "For #{p}, #{nn_result} retrieved amongs those 2 first results #{tree.nearest(p[:point], :limit => 2)}"
|
93
|
+
end
|
78
94
|
end
|
79
|
-
|
80
|
-
|
81
|
-
|
95
|
+
must_be_empty msgs
|
96
|
+
end
|
97
|
+
|
98
|
+
it "is more efficient than the brute force approach" do
|
99
|
+
tree = KnnBall.build(@data)
|
82
100
|
msgs = []
|
83
|
-
|
101
|
+
|
102
|
+
tree.nearest(@data.first[:point])
|
103
|
+
|
104
|
+
@data.each do |p|
|
84
105
|
t0 = Time.now
|
85
|
-
|
86
|
-
euc = Math.sqrt((p2[:coord][0] - p[:coord][0])**2.0 + (p2[:coord][1] - p[:coord][1])**2.0)
|
87
|
-
[p2, euc]
|
88
|
-
end
|
89
|
-
best = res.min {|a, b| a.last <=> b.last}
|
90
|
-
brute_force_result = best.first
|
106
|
+
brute_force_result = brute_force(p, @data)
|
91
107
|
t1 = Time.now
|
92
|
-
|
108
|
+
|
93
109
|
t2 = Time.now
|
94
|
-
nn_result = tree.nearest(p[:
|
110
|
+
nn_result = tree.nearest(p[:point])
|
95
111
|
t3 = Time.now
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
112
|
+
|
113
|
+
dt_bf = t1-t0
|
114
|
+
dt_kdtree = t3-t2
|
115
|
+
|
116
|
+
if(dt_bf < dt_kdtree)
|
117
|
+
msgs << "For #{p}, efficiency is better with brute force than with kdtree search. #{((dt_bf - dt_kdtree)/dt_bf * 100).to_i}%"
|
101
118
|
end
|
102
119
|
end
|
120
|
+
assert( (msgs.count / @data.count) * 100 < 5, msgs.inspect )
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "When asked to retrieve the k nearest neighbors" do
|
125
|
+
before :each do
|
126
|
+
json = File.open(File.join(File.dirname(__FILE__), 'data.json'), 'r:utf-8').read
|
127
|
+
@data = JSON.parse(json)
|
128
|
+
@data = @data.map do |l|
|
129
|
+
h = {}
|
130
|
+
l.each {|k,v| h[k.to_sym] = v}
|
131
|
+
h
|
132
|
+
end
|
103
133
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
134
|
+
assert @data.count > 10
|
135
|
+
|
136
|
+
@index = KnnBall.build(@data)
|
137
|
+
assert @index.count > 10
|
138
|
+
end
|
139
|
+
|
140
|
+
it "retrieve an array of results" do
|
141
|
+
result = @index.nearest([46.23, 5.46], :limit => 10)
|
142
|
+
result.kind_of?(Array).must_equal true
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should contains 10 results" do
|
146
|
+
result = @index.nearest([46.23, 5.46], :limit => 10)
|
147
|
+
result.size.must_equal 10
|
148
|
+
end
|
149
|
+
|
150
|
+
it "should contains results from the nearer to the farest" do
|
151
|
+
target = [46.18612, 6.118075]
|
152
|
+
results = @index.nearest(target, :limit => 10)
|
153
|
+
results.reduce do |r0, r1|
|
154
|
+
p0, p1 = r0[:point], r1[:point]
|
155
|
+
|
156
|
+
d0, d1 = Math.sqrt((p0[0] - target[0]) ** 2 + (p0[1] - target[1]) ** 2), Math.sqrt((p1[0] - target[0]) ** 2 + (p1[1] - target[1]) ** 2)
|
157
|
+
(d0 <= d1).must_equal(true, "#{d0} <= #{d1} should have been true")
|
158
|
+
r1
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it "retrieve the same results as a brute force approach" do
|
163
|
+
msgs = []
|
164
|
+
@data.each do |p|
|
165
|
+
brute_force_result = brute_force(p, @data, 10)
|
166
|
+
nn_result = @index.nearest(p[:point], :limit => 10)
|
167
|
+
(nn_result.map{|r| r[:point]}).must_equal(brute_force_result.map{|r| r[:point]})
|
112
168
|
end
|
113
|
-
must_be_empty
|
169
|
+
must_be_empty(msgs)
|
114
170
|
end
|
115
171
|
end
|
116
172
|
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Copyright (C) 2011 Olivier Amblet <http://olivier.amblet.net>
|
4
|
+
#
|
5
|
+
# knnball is freely distributable under the terms of an MIT license.
|
6
|
+
# See LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
7
|
+
|
8
|
+
require 'minitest/autorun'
|
9
|
+
require 'knnball'
|
10
|
+
|
11
|
+
module KnnBall
|
12
|
+
|
13
|
+
describe ResultSet do
|
14
|
+
|
15
|
+
describe "default state" do
|
16
|
+
|
17
|
+
before :each do
|
18
|
+
@rs = ResultSet.new
|
19
|
+
end
|
20
|
+
|
21
|
+
it "has a 10 items limit" do
|
22
|
+
assert_equal 10, @rs.limit
|
23
|
+
end
|
24
|
+
|
25
|
+
it "has no barrier value" do
|
26
|
+
assert_nil @rs.barrier_value
|
27
|
+
end
|
28
|
+
|
29
|
+
it "has an empty items array" do
|
30
|
+
assert_equal [], @rs.items
|
31
|
+
end
|
32
|
+
|
33
|
+
it "accept any value" do
|
34
|
+
@rs.eligible? 12
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "fix the limit" do
|
39
|
+
|
40
|
+
before :each do
|
41
|
+
@rs = ResultSet.new :limit => 1
|
42
|
+
end
|
43
|
+
|
44
|
+
it "has a 1 items limit" do
|
45
|
+
assert_equal 1, @rs.limit
|
46
|
+
end
|
47
|
+
|
48
|
+
it "has no barrier value" do
|
49
|
+
assert_nil @rs.barrier_value
|
50
|
+
end
|
51
|
+
|
52
|
+
it "has an empty items array" do
|
53
|
+
assert_equal [], @rs.items
|
54
|
+
end
|
55
|
+
|
56
|
+
it "accept any value" do
|
57
|
+
assert @rs.eligible?(12)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "accept only one value" do
|
61
|
+
assert @rs.add(12, '12')
|
62
|
+
assert ! @rs.eligible?(13)
|
63
|
+
assert @rs.eligible?(2)
|
64
|
+
assert @rs.add(2, '2')
|
65
|
+
assert_equal ['2'], @rs.items
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
describe "first value" do
|
71
|
+
before :each do
|
72
|
+
@rs = ResultSet.new
|
73
|
+
@rs.add(2, 'AA')
|
74
|
+
end
|
75
|
+
|
76
|
+
it "set its barrier value to the first added value" do
|
77
|
+
assert_equal 2, @rs.barrier_value
|
78
|
+
end
|
79
|
+
|
80
|
+
it "add the item to the item list" do
|
81
|
+
assert_equal ['AA'], @rs.items
|
82
|
+
end
|
83
|
+
|
84
|
+
it "accept any value" do
|
85
|
+
@rs.eligible? 12
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "up to the limit" do
|
90
|
+
before :each do
|
91
|
+
@rs = ResultSet.new
|
92
|
+
@rs.add(5, '5')
|
93
|
+
@rs.add(2, '2')
|
94
|
+
@rs.add(10, '10')
|
95
|
+
@rs.add(5, '5')
|
96
|
+
@rs.add(3, '3')
|
97
|
+
@rs.add(4, '4')
|
98
|
+
@rs.add(5, '5')
|
99
|
+
@rs.add(1, '1')
|
100
|
+
@rs.add(5, '5')
|
101
|
+
@rs.add(5, '5')
|
102
|
+
end
|
103
|
+
|
104
|
+
it "set its barrier value to the first added value" do
|
105
|
+
assert_equal 10, @rs.barrier_value
|
106
|
+
end
|
107
|
+
|
108
|
+
it "add the item to the item list" do
|
109
|
+
assert_equal ['1', '2', '3', '4', '5', '5', '5', '5', '5', '10'], @rs.items
|
110
|
+
end
|
111
|
+
|
112
|
+
it "successfuly add an item that should replace the last one" do
|
113
|
+
@rs.add(9, '9')
|
114
|
+
assert_equal ['1', '2', '3', '4', '5', '5', '5', '5', '5', '9'], @rs.items
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "break the limit" do
|
119
|
+
before :each do
|
120
|
+
@rs = ResultSet.new
|
121
|
+
@rs.add(5, '5')
|
122
|
+
@rs.add(2, '2')
|
123
|
+
@rs.add(10, '10')
|
124
|
+
@rs.add(5, '5')
|
125
|
+
@rs.add(3, '3')
|
126
|
+
@rs.add(4, '4')
|
127
|
+
@rs.add(5, '5')
|
128
|
+
@rs.add(1, '1')
|
129
|
+
@rs.add(5, '5')
|
130
|
+
@rs.add(5, '5')
|
131
|
+
|
132
|
+
@rs.add(11, '11')
|
133
|
+
@rs.add(-1, '-1')
|
134
|
+
@rs.add(6, '6')
|
135
|
+
@rs.add(4, '4')
|
136
|
+
@rs.add(0, '0')
|
137
|
+
end
|
138
|
+
|
139
|
+
it "set its barrier value to the first added value" do
|
140
|
+
assert_equal 5, @rs.barrier_value
|
141
|
+
end
|
142
|
+
|
143
|
+
it "add the item below the limit to the item list" do
|
144
|
+
assert_equal ['-1', '0', '1', '2', '3', '4', '4', '5', '5', '5'], @rs.items
|
145
|
+
end
|
146
|
+
|
147
|
+
it "refuse big values" do
|
148
|
+
assert ! @rs.eligible?(12)
|
149
|
+
assert ! @rs.eligible?(5)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "accept smaller value" do
|
153
|
+
assert @rs.eligible? -2
|
154
|
+
assert @rs.eligible? 4
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module KnnBall
|
2
|
+
module SpecHelpers
|
3
|
+
|
4
|
+
def brute_force(point, data, count=1)
|
5
|
+
res = data.map do |p2|
|
6
|
+
euc = Math.sqrt((p2[:point][0] - point[:point][0])**2.0 + (p2[:point][1] - point[:point][1])**2.0)
|
7
|
+
[p2, euc]
|
8
|
+
end
|
9
|
+
results = res.sort {|a, b| a.last <=> b.last}[0..9].map{|r| r.first}
|
10
|
+
|
11
|
+
if(count == 1)
|
12
|
+
results.first
|
13
|
+
else
|
14
|
+
results[0..count-1]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/test/units/stat_test.rb
CHANGED
@@ -50,8 +50,8 @@ module KnnBall
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def test_median_with_hash
|
53
|
-
data = [{:
|
54
|
-
assert_equal({:
|
53
|
+
data = [{:point => [1]}, {:point => [2]}, {:point => [3]}]
|
54
|
+
assert_equal({:point => [2]}, Stat.median!(data){|a,b| a[:point] <=> b[:point]}, data.inspect)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: knnball
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,10 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-
|
12
|
+
date: 2011-09-09 00:00:00.000000000 +02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
|
-
description: Implements K-Nearest Neighbor algorithm using a KDTree in Ruby.
|
15
|
+
description: Implements K-Nearest Neighbor algorithm using a KDTree in Ruby. Usefull
|
16
|
+
for sorting geolocation or any other multi-dimensional data.
|
16
17
|
email: olivier@amblet.net
|
17
18
|
executables: []
|
18
19
|
extensions: []
|
@@ -28,10 +29,13 @@ files:
|
|
28
29
|
- lib/knnball/ball.rb
|
29
30
|
- lib/knnball/stat.rb
|
30
31
|
- lib/knnball/kdtree.rb
|
32
|
+
- lib/knnball/result_set.rb
|
31
33
|
- test/specs/ball_spec.rb
|
32
34
|
- test/specs/data.json
|
33
35
|
- test/specs/kdtree_spec.rb
|
34
36
|
- test/specs/knnball_spec.rb
|
37
|
+
- test/specs/result_set_spec.rb
|
38
|
+
- test/specs/spec_helpers.rb
|
35
39
|
- test/units/stat_test.rb
|
36
40
|
has_rdoc: true
|
37
41
|
homepage: http://github.com/oliamb/knnball
|
@@ -58,5 +62,5 @@ rubyforge_project: knnball
|
|
58
62
|
rubygems_version: 1.6.2
|
59
63
|
signing_key:
|
60
64
|
specification_version: 1
|
61
|
-
summary:
|
65
|
+
summary: Multi-dimensional nearest neighbor search
|
62
66
|
test_files: []
|