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 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 another point as input.
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 written seems accord on the point
8
- that with more than 10 dimensions, brute force approach might give better results.
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, :coord => [6.3299934, 52.32444]},
22
- {:id => 2, :coord => [3.34444, 53.23259]},
23
- {:id => 3, :coord => [4.22452, 53.243982]},
24
- {:id => 4, :coord => [4.2333424, 51.239994]},
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, :coord=>[3.34444, 53.23259]}
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* must is given using an array of hashes.
38
+ *data* is given using an array of hashes.
36
39
  The only requirement of an Hash instance is
37
- to have a :coord keys containing an array of coordinate.
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.5'
8
- s.date = '2011-05-23'
7
+ s.version = '0.0.6'
8
+ s.date = '2011-09-09'
9
9
  s.rubyforge_project = 'knnball'
10
10
 
11
- s.summary = "K-Nearest Neighbor queries using a KDTree"
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?(:coord))
26
- raise ArgumentError.new("value must contains :coord key but has only #{value.keys.inspect}")
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[:coord]
35
+ value[:point]
36
36
  end
37
37
 
38
38
  def nearest(target, min)
39
39
  result = nil
40
- d = [distance(target), min[0]].min
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,
@@ -16,30 +16,100 @@ module KnnBall
16
16
  def initialize(root = nil)
17
17
  @root = root
18
18
  end
19
-
20
- def nearest(coord, &cmp_block)
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
- # Find the parent to which this coord should belongs to
25
- # This will be our best first try
26
- result = parent(coord)
27
- smallest = result.distance(coord)
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
- # Starting back from the root, we check all rectangle that
30
- # might overlap the smallest one.
31
- best = [smallest]
32
- better_one = root.nearest(coord, best)
33
- return (better_one || result).value
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 parent(coord)
107
+ def parent_ball(coord)
38
108
  current = root
39
- idx = current.dimension-1
109
+ d_idx = current.dimension-1
40
110
  result = nil
41
111
  while(result.nil?)
42
- if(coord[idx] <= current.center[idx])
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
- idx = current.dimension-1
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 :coord, an array of position (one per dimension)
22
- # [ {:value => 1, :coord => [1.23, 2.34, -1.23, -22.3]},
23
- # {:value => 2, :coord => [-2.33, 4.2, 1.23, 332.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 :coord key
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[:coord].size
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[:coord][actual_dimension-1] <=> v2[:coord][actual_dimension-1]}
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)
@@ -17,7 +17,7 @@ module KnnBall
17
17
 
18
18
  describe "Leaf balls" do
19
19
  before :each do
20
- @value = {:id => 1, :coord => [1,2,3]}
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[:coord]
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, :coord => [1,2,3]}
44
- @ball = Ball.new(@value, 1, Ball.new({:id => 3, :coord => [-1, -2, -3]}), Ball.new({:id => 2, :coord => [2, 3, 4]}))
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[:coord]
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, :coord => [-1, -2, -3]}, nil, nil],
59
- [{:id => 2, :coord => [2, 3, 4]}, nil, nil]
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, :coord => [-1, -2, -3]}, :left => nil, :right => nil},
66
- :right => {:value => {:id => 2, :coord => [2, 3, 4]}, :left => nil, :right => nil}
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, :coord => [1,2,3]}
73
+ @value = {:id => 1, :point => [1,2,3]}
74
74
  @leaf_1 = Ball.new(@value)
75
- @leaf_2 = Ball.new({:id => 2, :coord => [2, 3, 4]})
76
- @leaf_3 = Ball.new({:id => 3, :coord => [-1, -2 , -5]})
77
- @leaf_4 = Ball.new({:id => 4, :coord => [-3, -2, -2]})
78
- @sub_ball_1 = Ball.new({:id => 5, :coord => [1.4, 2, 2.5]}, 1, @leaf_1, @leaf_2)
79
- @sub_ball_2 = Ball.new({:id => 6, :coord => [-2, -1.9, -3]}, 1, @leaf_3, @leaf_4)
80
- @ball = Ball.new({:id => 7, :coord => [0, 0, 0]}, 1, @sub_ball_1, @sub_ball_2)
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, :coord => [2, 3, 4]})
96
- b2 = Ball.new({:id => 3, :coord => [-1, -2 , -5]})
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","coord":[47.0495221,8.3079993]},{"id":"Dresse Achermann-Bieri Ursula","coord":[47.0525778,8.3052475]},{"id":"M. Ackermann Christian","coord":[46.9406862,7.3991925]},{"id":"Dr Ackermann Roland","coord":[47.3538085,7.9035746]},{"id":"Dr Adank-Sailer Gabrielle","coord":[47.1359809,7.2447156]},{"id":"Dr Aebersold Christian","coord":[47.1262539,7.2763632]},{"id":"Dresse Aebersold Gaby","coord":[47.1262539,7.2763632]},{"id":"M. Aellen Jean-Marc","coord":[46.1980464,6.1483353]},{"id":"M. Aerni Christian","coord":[46.18612,6.118075]},{"id":"Dr Akermann Felix","coord":[47.1697675,9.4766358]},{"id":"Dr Albrecht Silvia","coord":[47.1655638,7.5936764]},{"id":"M. Alder Ernst","coord":[47.6833333,8.75]},{"id":"Dr Althaus Marc-André","coord":[46.4810917,6.4571243]},{"id":"Dr Amati Francesca","coord":[46.5230847,6.640415]},{"id":"Anklin Bernard","coord":[47.3920158,8.5392456]},{"id":"Dr Bachelin Pierre","coord":[46.6668682,6.5107242]},{"id":"Dr Backes Hans-Ulrich","coord":[47.4238477,9.3686547]},{"id":"Badorff Cornel","coord":[47.4977665,8.7270143]},{"id":"Dr Bagutti Carlo","coord":[46.516054,6.608998]},{"id":"Dr Ballmer Peter Matthias","coord":[46.7617201,7.6279035]},{"id":"Dr Bandi-Ott Elisabeth","coord":[47.3839285,8.54832]},{"id":"Dr Barandun Jürg","coord":[47.3519079,8.5762373]},{"id":"Dr Barras Bernard","coord":[46.2270208,7.3533351]},{"id":"Dr Beck Thomas","coord":[46.7571286,7.6305141]},{"id":"Dr Bedat Bernard","coord":[46.2785274,6.1684023]},{"id":"M. Bekkering Anton","coord":[47.4226784,9.3184235]},{"id":"Dresse Benz Gabrielle","coord":[46.2304522,7.363867]},{"id":"M. Benz Martin","coord":[46.2304522,7.363867]},{"id":"M. Berchten Anthony","coord":[46.3873762,6.2208354]},{"id":"M. Berdoz Jean-Marc","coord":[46.2124628,6.131694]},{"id":"Mme Berdoz Nicole","coord":[46.2124628,6.131694]},{"id":"Dr Berger Hanspeter","coord":[46.684936,7.8499151]},{"id":"Dr Berghoff Ueli","coord":[47.2217408,8.6722444]},{"id":"Dr Bernasconi Cristiano","coord":[46.3601201,8.971514]},{"id":"Dr Bernhart Felix","coord":[46.9286035,7.447995]},{"id":"Dr Beuing Markus","coord":[46.4965828,9.8382621]},{"id":"Dr Beyeler Jürg","coord":[47.3805257,8.5428647]},{"id":"Dr Bianchi Michele","coord":[46.0056877,8.9407073]},{"id":"Dr Bickel Andreas","coord":[47.2287339,8.8268529]},{"id":"Dr Biedert Roland","coord":[47.1333363,7.2612937]}]
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]}]
@@ -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, :coord => [1]})).wont_be_empty
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({:id => 4, :coord => [5]}, 1,
31
- Ball.new({:id => 2, :coord => [2]}, 1, Ball.new({:id => 1, :coord => [1]}), Ball.new({:id => 3, :coord => [3]})),
32
- Ball.new({:id => 6, :coord => [13]}, 1, Ball.new({:id => 5, :coord => [8]}),
33
- Ball.new({:id => 7, :coord => [21]}, 1, Ball.new({:id => 8, :coord => [34]})))
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, :coord => [5, 7]}, 1,
46
- Ball.new({:id => 2, :coord => [3, 4]}, 1, Ball.new({:id => 1, :coord => [2, 2]}), Ball.new({:id => 3, :coord => [4, 8]})),
47
- Ball.new({:id => 6, :coord => [13, 4]}, 1, Ball.new({:id => 5, :coord => [8, 1]}),
48
- Ball.new({:id => 7, :coord => [21, 6]}, 1, Ball.new({:id => 8, :coord => [34, 5]})))
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.parent([13.2, 4.5]).value[:id].must_equal(8)
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, :coord => [1, 2, 3]})).to_a.must_equal [{:id => 1, :coord => [1,2,3]}, nil, nil]
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
@@ -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, :coord => [1.0,1.0]},
23
- {:id => 2, :coord => [2.0, 3.0]}
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, :coord => [2]},
30
- {:id => 3, :coord => [3]},
31
- {:id => 1, :coord => [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, :coord => [2]})
34
- tree.root.left.value.must_equal({:id => 1, :coord => [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, :coord => [3]})
41
+ tree.root.right.value.must_equal({:id => 3, :point => [3]})
39
42
 
40
43
  KnnBall.build([
41
- {:id => 1, :coord => [1]},
42
- {:id => 2, :coord => [2]},
43
- {:id => 3, :coord => [3]},
44
- {:id => 4, :coord => [5]},
45
- {:id => 5, :coord => [8]},
46
- {:id => 6, :coord => [13]},
47
- {:id => 7, :coord => [21]},
48
- {:id => 8, :coord => [34]}
49
- ]).root.value.must_equal({:id => 4, :coord => [5]})
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
- json = File.open(File.join(File.dirname(__FILE__), 'data.json'), 'r:utf-8').read
73
- data = JSON.parse(json)
74
- data = data.map do |l|
75
- h = {}
76
- l.each {|k,v| h[k.to_sym] = v}
77
- h
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
- tree = KnnBall.build(data)
81
- errors = []
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
- data.each do |p|
101
+
102
+ tree.nearest(@data.first[:point])
103
+
104
+ @data.each do |p|
84
105
  t0 = Time.now
85
- res = data.map do |p2|
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
- p[:coord].must_equal(brute_force_result[:coord])
108
+
93
109
  t2 = Time.now
94
- nn_result = tree.nearest(p[:coord])
110
+ nn_result = tree.nearest(p[:point])
95
111
  t3 = Time.now
96
- if(nn_result[:coord] != brute_force_result[:coord])
97
- errors << [p, nn_result, brute_force_result]
98
- end
99
- if(t1-t0 < t3-t2)
100
- msgs << "For #{p}, efficiency was before with bruteforce than with kdtree search."
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
- msgs = errors.map do |e|
105
- if(e[0] == e[1])
106
- "For #{e[0]}, OK, but brute force retrieved #{e[2]}"
107
- elsif(e[0] == e[2])
108
- "For #{e[0]}, #{e[1]} retrieved instead of #{e[2]}"
109
- else
110
- "For #{e[0]}, both brute force and nn search are wrong"
111
- end
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 errors
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
@@ -50,8 +50,8 @@ module KnnBall
50
50
  end
51
51
 
52
52
  def test_median_with_hash
53
- data = [{:coord => [1]}, {:coord => [2]}, {:coord => [3]}]
54
- assert_equal({:coord => [2]}, Stat.median!(data){|a,b| a[:coord] <=> b[:coord]}, data.inspect)
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.5
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-05-23 00:00:00.000000000 +02:00
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: K-Nearest Neighbor queries using a KDTree
65
+ summary: Multi-dimensional nearest neighbor search
62
66
  test_files: []