knnball 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|