spatial_stats 0.2.2 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -20,6 +20,7 @@ module SpatialStats
20
20
  def initialize(scope, field, weights)
21
21
  super(scope, field, weights)
22
22
  end
23
+ attr_writer :x
23
24
 
24
25
  ##
25
26
  # Computes the local indicator of spatial autocorrelation (lisa) for
@@ -55,12 +56,11 @@ module SpatialStats
55
56
  # @return [Array] of variances for each observation
56
57
  def variance
57
58
  # formula is A - B - (E[I])**2
58
- wt = w.row_standardized
59
59
  exp = expectation
60
60
 
61
61
  vars = []
62
- a_terms = a_calc(wt)
63
- b_terms = b_calc(wt)
62
+ a_terms = a_calc
63
+ b_terms = b_calc
64
64
 
65
65
  a_terms.each_with_index do |a_term, idx|
66
66
  vars << (a_term - b_terms[idx] - (exp**2))
@@ -68,6 +68,21 @@ module SpatialStats
68
68
  vars
69
69
  end
70
70
 
71
+ ##
72
+ # Computes the groups each observation belongs to.
73
+ # Potential groups for Moran's I are:
74
+ # [HH] High-High
75
+ # [HL] High-Low
76
+ # [LH] Low-High
77
+ # [LL] Low-Low
78
+ #
79
+ # This is the same as the +#quads+ method in the +Stat+ class.
80
+ #
81
+ # @return [Array] groups for each observation
82
+ def groups
83
+ quads
84
+ end
85
+
71
86
  ##
72
87
  # Values of the +field+ queried from the +scope+
73
88
  #
@@ -85,7 +100,7 @@ module SpatialStats
85
100
  def z_lag
86
101
  # w is already row_standardized, so we are using
87
102
  # neighbor sum instead of neighbor_average to save cost
88
- @z_lag ||= SpatialStats::Utils::Lag.neighbor_sum(w, z)
103
+ @z_lag ||= SpatialStats::Utils::Lag.neighbor_sum(weights, z)
89
104
  end
90
105
 
91
106
  private
@@ -102,6 +117,15 @@ module SpatialStats
102
117
  z[idx] * z_lag_i
103
118
  end
104
119
 
120
+ def mc_observation_calc(stat_i_orig, stat_i_new, _permutations)
121
+ # Since moran can be positive or negative, go by this definition
122
+ if stat_i_orig.positive?
123
+ (stat_i_new >= stat_i_orig).count
124
+ else
125
+ (stat_i_new <= stat_i_orig).count
126
+ end
127
+ end
128
+
105
129
  def si2
106
130
  # @si2 ||= z.sample_variance
107
131
  # we standardize so sample_variance is 1
@@ -109,20 +133,27 @@ module SpatialStats
109
133
  end
110
134
 
111
135
  # https://pro.arcgis.com/en/pro-app/tool-reference/spatial-statistics/h-local-morans-i-additional-math.htm
112
- def a_calc(wt)
113
- n = wt.shape[0]
136
+ # TODO: sparse
137
+ def a_calc
138
+ n = weights.n
114
139
  b2i = b2i_calc
140
+
141
+ wts = weights.sparse.values
142
+ row_index = weights.sparse.row_index
143
+
115
144
  a_terms = []
116
145
 
117
146
  (0..n - 1).each do |idx|
118
- sigma_term = wt[idx, true].to_a.sum { |v| v**2 }
147
+ row_range = row_index[idx]..(row_index[idx + 1] - 1)
148
+ wt = wts[row_range]
149
+ sigma_term = wt.sum { |v| v**2 }
119
150
  a_terms << (n - b2i) * sigma_term / (n - 1)
120
151
  end
121
152
  a_terms
122
153
  end
123
154
 
124
- def b_calc(wt)
125
- n = wt.shape[0]
155
+ def b_calc
156
+ n = weights.n
126
157
  b2i = b2i_calc
127
158
  b_terms = []
128
159
 
@@ -23,7 +23,7 @@ module SpatialStats
23
23
  def initialize(scope, fields, weights)
24
24
  @scope = scope
25
25
  @fields = fields
26
- @weights = weights
26
+ @weights = weights.standardize
27
27
  end
28
28
  attr_accessor :scope, :fields, :weights
29
29
 
@@ -67,22 +67,24 @@ module SpatialStats
67
67
  stat_orig = stat
68
68
  rs = [0] * n
69
69
 
70
- ws = neighbor_weights
70
+ row_index = weights.sparse.row_index
71
+ ws = weights.sparse.values
71
72
 
72
73
  idx = 0
73
74
  while idx < n
74
75
  stat_i_orig = stat_orig[idx]
75
- wi = Numo::DFloat.cast(ws[idx])
76
+
77
+ row_range = row_index[idx]..(row_index[idx + 1] - 1)
78
+ if row_range.size.zero?
79
+ rs[idx] = permutations
80
+ idx += 1
81
+ next
82
+ end
83
+ wi = Numo::DFloat.cast(ws[row_range])
76
84
 
77
85
  # for each field, compute the C value at that index.
78
86
  stat_i_new = mc_i(wi, shuffles[idx], idx)
79
-
80
- rs[idx] = if stat_i_orig.positive?
81
- (stat_i_new >= stat_i_orig).count
82
- else
83
- (stat_i_new <= stat_i_orig).count
84
- end
85
-
87
+ rs[idx] = mc_observation_calc(stat_i_orig, stat_i_new, permutations)
86
88
  idx += 1
87
89
  end
88
90
 
@@ -91,6 +93,10 @@ module SpatialStats
91
93
  end
92
94
  end
93
95
 
96
+ def groups
97
+ raise NotImplementedError, 'groups not implemented'
98
+ end
99
+
94
100
  private
95
101
 
96
102
  def mc_i(wi, perms, idx)
@@ -108,6 +114,19 @@ module SpatialStats
108
114
  cs.mean(0)
109
115
  end
110
116
 
117
+ def mc_observation_calc(stat_i_orig, stat_i_new, _permutations)
118
+ # Geary cannot be negative, so we have to use this technique from
119
+ # GeoDa to determine p values. Note I slightly modified it to be inclusive
120
+ # on both tails not just the lower tail.
121
+ # https://github.com/GeoDaCenter/geoda/blob/master/Explore/LocalGearyCoordinator.cpp#L981 mean = stat_i_new.mean
122
+ mean = stat_i_new.mean
123
+ if stat_i_orig <= mean
124
+ (stat_i_new <= stat_i_orig).count
125
+ else
126
+ (stat_i_new >= stat_i_orig).count
127
+ end
128
+ end
129
+
111
130
  def field_data
112
131
  @field_data ||= fields.map do |field|
113
132
  SpatialStats::Queries::Variables.query_field(@scope, field)
@@ -12,7 +12,7 @@ module SpatialStats
12
12
  def initialize(scope, field, weights)
13
13
  @scope = scope
14
14
  @field = field
15
- @weights = weights
15
+ @weights = weights.standardize
16
16
  end
17
17
  attr_accessor :scope, :field, :weights
18
18
 
@@ -62,17 +62,20 @@ module SpatialStats
62
62
  # need to get k for max_neighbors
63
63
  # and wc for cardinalities of each item
64
64
  # this returns an array of length n with
65
- # (permutations x neighborz) Numo Arrays.
65
+ # (permutations x neighbors) Numo Arrays.
66
66
  # This helps reduce computation time because
67
67
  # we are only dealing with neighbors for each
68
68
  # entry not the entire list of permutations for each entry.
69
69
  n_1 = weights.n - 1
70
70
 
71
+ sparse = weights.sparse
72
+ row_index = sparse.row_index
73
+
71
74
  # weight counts
72
- wc = [0] * weights.n
75
+ wc = Array.new(weights.n)
73
76
  k = 0
74
77
  (0..n_1).each do |idx|
75
- wc[idx] = (w[idx, true] > 0).count
78
+ wc[idx] = row_index[idx + 1] - row_index[idx]
76
79
  end
77
80
 
78
81
  k = wc.max + 1
@@ -112,26 +115,43 @@ module SpatialStats
112
115
  # of the entire set. This will be done for each item.
113
116
  rng = gen_rng(seed)
114
117
  shuffles = crand(x, permutations, rng)
118
+
115
119
  n = weights.n
116
120
  # r is the number of equal to or more extreme samples
117
121
  stat_orig = stat
118
122
  rs = [0] * n
119
123
 
120
- ws = neighbor_weights
124
+ row_index = weights.sparse.row_index
125
+ ws = weights.sparse.values
121
126
 
122
127
  idx = 0
123
128
  while idx < n
129
+ # need to truncate because floats from
130
+ # c in sparse matrix are inconsistent with
131
+ # dfloats
124
132
  stat_i_orig = stat_orig[idx]
125
133
 
126
- wi = Numo::DFloat.cast(ws[idx])
134
+ # account for case where there are no neighbors
135
+ # the way Numo handles negative ranges, it returns the max
136
+ # so there will be a len 0 z array being multiplied by a
137
+ # max_neighbor width permutation matrix.
138
+ # Need to skip.
139
+ row_range = row_index[idx]..(row_index[idx + 1] - 1)
140
+ if row_range.size.zero?
141
+ rs[idx] = permutations
142
+ idx += 1
143
+ next
144
+ end
145
+ wi = Numo::DFloat.cast(ws[row_range])
127
146
  stat_i_new = mc_i(wi, shuffles[idx], idx)
128
147
 
129
- rs[idx] = if stat_i_orig.positive?
130
- (stat_i_new >= stat_i_orig).count
131
- else
132
- (stat_i_new <= stat_i_orig).count
133
- end
134
-
148
+ rs[idx] = mc_observation_calc(stat_i_orig, stat_i_new,
149
+ permutations)
150
+ # rs[idx] = if stat_i_orig.positive?
151
+ # (stat_i_new >= stat_i_orig).count
152
+ # else
153
+ # (stat_i_new <= stat_i_orig).count
154
+ # end
135
155
  idx += 1
136
156
  end
137
157
 
@@ -160,19 +180,30 @@ module SpatialStats
160
180
  stat_orig = stat
161
181
  rs = [0] * n
162
182
 
163
- ws = neighbor_weights
183
+ row_index = weights.sparse.row_index
184
+ ws = weights.sparse.values
164
185
 
165
186
  idx = 0
166
187
  while idx < n
167
188
  stat_i_orig = stat_orig[idx]
168
- wi = Numo::DFloat.cast(ws[idx])
189
+
190
+ row_range = row_index[idx]..(row_index[idx + 1] - 1)
191
+ if row_range.size.zero?
192
+ rs[idx] = permutations
193
+ idx += 1
194
+ next
195
+ end
196
+ wi = Numo::DFloat.cast(ws[row_range])
197
+
169
198
  stat_i_new = mc_i(wi, shuffles[idx], idx)
170
199
 
171
- rs[idx] = if stat_i_orig.positive?
172
- (stat_i_new >= stat_i_orig).count
173
- else
174
- (stat_i_new <= stat_i_orig).count
175
- end
200
+ rs[idx] = mc_observation_calc(stat_i_orig, stat_i_new,
201
+ permutations)
202
+ # if stat_i_orig.positive?
203
+ # (stat_i_new >= stat_i_orig).count
204
+ # else
205
+ # (stat_i_new <= stat_i_orig).count
206
+ # end
176
207
 
177
208
  idx += 1
178
209
  end
@@ -200,8 +231,7 @@ module SpatialStats
200
231
  # @return [Array] of labels
201
232
  def quads
202
233
  # https://github.com/pysal/esda/blob/master/esda/moran.py#L925
203
- w = @weights.full
204
- z_lag = SpatialStats::Utils::Lag.neighbor_average(w, z)
234
+ z_lag = SpatialStats::Utils::Lag.neighbor_average(weights, z)
205
235
  zp = z.map(&:positive?)
206
236
  lp = z_lag.map(&:positive?)
207
237
 
@@ -221,6 +251,22 @@ module SpatialStats
221
251
  end
222
252
  end
223
253
 
254
+ ##
255
+ # Summary of the statistic. Computes +stat+, +mc+, and +groups+ then returns the values
256
+ # in a hash array.
257
+ #
258
+ # @param [Integer] permutations to run. Last digit should be 9 to produce round numbers.
259
+ # @param [Integer] seed used in random number generator for shuffles.
260
+ #
261
+ # @return [Array]
262
+ def summary(permutations = 99, seed = nil)
263
+ p_vals = mc(permutations, seed)
264
+ data = weights.keys.zip(stat, p_vals, groups)
265
+ data.map do |row|
266
+ { key: row[0], stat: row[1], p: row[2], group: row[3] }
267
+ end
268
+ end
269
+
224
270
  private
225
271
 
226
272
  def stat_i
@@ -231,8 +277,12 @@ module SpatialStats
231
277
  raise NotImplementedError, 'method mc_i not defined'
232
278
  end
233
279
 
280
+ def mc_observation_calc(_stat_i_orig, _stat_i_new, _permutations)
281
+ raise NotImplementedError, 'method mc_observation_calc not defined'
282
+ end
283
+
234
284
  def w
235
- weights.standardized
285
+ @w ||= weights.dense
236
286
  end
237
287
 
238
288
  def gen_rng(seed = nil)
@@ -242,20 +292,6 @@ module SpatialStats
242
292
  Random.new
243
293
  end
244
294
  end
245
-
246
- def neighbor_weights
247
- # record the non-zero weights in variable length arrays for each
248
- # row in the weights table
249
- ws = [[]] * weights.n
250
- (0..weights.n - 1).each do |idx|
251
- neighbors = []
252
- w[idx, true].each do |wij|
253
- neighbors << wij if wij != 0
254
- end
255
- ws[idx] = neighbors
256
- end
257
- ws
258
- end
259
295
  end
260
296
  end
261
297
  end
@@ -12,11 +12,11 @@ module Numo
12
12
  #
13
13
  # @ example
14
14
  #
15
- # Numo::DFloat [[0, 1, 1], [1, 1, 1]].row_standardized
15
+ # Numo::DFloat [[0, 1, 1], [1, 1, 1]].row_standardize
16
16
  # Numo::DFloat [[0, 0.5, 0.5], [0.33333, 0.33333, 0.33333]]
17
17
  #
18
18
  # @return [Numo::NArray]
19
- def row_standardized
19
+ def row_standardize
20
20
  # every row will sum up to 1, or if they are all 0, do nothing
21
21
  standardized = each_over_axis.map do |row|
22
22
  sum = row.sum
@@ -38,16 +38,16 @@ module Numo
38
38
  #
39
39
  # @ example
40
40
  #
41
- # Numo::DFloat [[0, 1, 0], [1, 0, 1], [0, 1, 0]].windowed
41
+ # Numo::DFloat [[0, 1, 0], [1, 0, 1], [0, 1, 0]].window
42
42
  # Numo::DFloat [[1, 1, 0], [1, 1, 1], [0, 1, 1]]
43
43
  #
44
44
  # @ example
45
45
  # # Input will be equivalent to output in this case
46
- # Numo::DFloat [[1, 1, 0], [1, 0, 1], [0, 1, 0]].windowed
46
+ # Numo::DFloat [[1, 1, 0], [1, 0, 1], [0, 1, 0]].window
47
47
  # Numo::DFloat [[1, 1, 0], [1, 0, 1], [0, 1, 0]]
48
48
  #
49
49
  # @return [Numo::NArray]
50
- def windowed
50
+ def window
51
51
  # in windowed calculations, the diagonal is set to 1
52
52
  # if trace (sum of diag) is 0, add it, else return input
53
53
  if trace.zero?
Binary file
@@ -11,36 +11,36 @@ module SpatialStats
11
11
  # Dot product of the row_standardized input matrix
12
12
  # by the input vector, variables.
13
13
  #
14
- # @param [Numo::NArray] matrix 2-D square matrix.
14
+ # @param [WeightsMatrix] matrix holding target weights.
15
15
  # @param [Array] variables vector multiplying the matrix
16
16
  #
17
17
  # @return [Array] resultant vector
18
18
  def self.neighbor_average(matrix, variables)
19
- matrix = matrix.row_standardized
19
+ matrix = matrix.standardize
20
20
  neighbor_sum(matrix, variables)
21
21
  end
22
22
 
23
23
  ##
24
24
  # Dot product of the input matrix by the input vector, variables.
25
25
  #
26
- # @param [Numo::NArray] matrix 2-D square matrix.
26
+ # @param [WeightsMatrix] matrix holding target weights.
27
27
  # @param [Array] variables vector multiplying the matrix
28
28
  #
29
29
  # @return [Array] resultant vector
30
30
  def self.neighbor_sum(matrix, variables)
31
- matrix.dot(variables).to_a
31
+ matrix.sparse.mulvec(variables)
32
32
  end
33
33
 
34
34
  ##
35
- # Dot product of the input windowed, row standardizd matrix by
35
+ # Dot product of the input windowed, row standardized matrix by
36
36
  # the input vector, variables.
37
37
  #
38
- # @param [Numo::NArray] matrix 2-D square matrix.
38
+ # @param [WeightsMatrix] matrix holding target weights.
39
39
  # @param [Array] variables vector multiplying the matrix
40
40
  #
41
41
  # @return [Array] resultant vector
42
42
  def self.window_average(matrix, variables)
43
- matrix = matrix.windowed.row_standardized
43
+ matrix = matrix.window.standardize
44
44
  window_sum(matrix, variables)
45
45
  end
46
46
 
@@ -48,13 +48,13 @@ module SpatialStats
48
48
  # Dot product of the input windowed matrix by
49
49
  # the input vector, variables.
50
50
  #
51
- # @param [Numo::NArray] matrix 2-D square matrix.
51
+ # @param [WeightsMatrix] matrix holding target weights.
52
52
  # @param [Array] variables vector multiplying the matrix
53
53
  #
54
54
  # @return [Array] resultant vector
55
55
  def self.window_sum(matrix, variables)
56
- matrix = matrix.windowed
57
- matrix.dot(variables).to_a
56
+ matrix = matrix.window
57
+ matrix.sparse.mulvec(variables)
58
58
  end
59
59
  end
60
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpatialStats
4
- VERSION = '0.2.2'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -15,21 +15,26 @@ module SpatialStats
15
15
  #
16
16
  # @return [WeightsMatrix]
17
17
  def self.rook(scope, field)
18
- p_key = scope.primary_key
19
- keys = scope.pluck(p_key).sort
20
-
21
18
  neighbors = SpatialStats::Queries::Weights
22
19
  .rook_contiguity_neighbors(scope, field)
23
20
 
21
+ # get keys to make sure we have consistent dimensions when
22
+ # some entries don't have neighbors.
23
+ # define a new hash that has all the keys from scope
24
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
25
+
24
26
  neighbors = neighbors.group_by(&:i_id)
27
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
28
+ neighbors = neighbors.merge(missing_neighbors)
29
+
25
30
  weights = neighbors.transform_values do |value|
26
31
  value.map do |neighbor|
27
- hash = neighbor.as_json(only: [:j_id]).symbolize_keys
32
+ hash = { id: neighbor[:j_id] }
28
33
  hash[:weight] = 1
29
34
  hash
30
35
  end
31
36
  end
32
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
37
+ SpatialStats::Weights::WeightsMatrix.new(weights)
33
38
  end
34
39
 
35
40
  ##
@@ -40,21 +45,26 @@ module SpatialStats
40
45
  #
41
46
  # @return [WeightsMatrix]
42
47
  def self.queen(scope, field)
43
- p_key = scope.primary_key
44
- keys = scope.pluck(p_key).sort
45
-
46
48
  neighbors = SpatialStats::Queries::Weights
47
49
  .queen_contiguity_neighbors(scope, field)
48
50
 
51
+ # get keys to make sure we have consistent dimensions when
52
+ # some entries don't have neighbors.
53
+ # define a new hash that has all the keys from scope
54
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
55
+
49
56
  neighbors = neighbors.group_by(&:i_id)
57
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
58
+ neighbors = neighbors.merge(missing_neighbors)
59
+
50
60
  weights = neighbors.transform_values do |value|
51
61
  value.map do |neighbor|
52
- hash = neighbor.as_json(only: [:j_id]).symbolize_keys
62
+ hash = { id: neighbor[:j_id] }
53
63
  hash[:weight] = 1
54
64
  hash
55
65
  end
56
66
  end
57
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
67
+ SpatialStats::Weights::WeightsMatrix.new(weights)
58
68
  end
59
69
  end
60
70
  end
@@ -15,21 +15,26 @@ module SpatialStats
15
15
  #
16
16
  # @return [WeightsMatrix]
17
17
  def self.distance_band(scope, field, bandwidth)
18
- p_key = scope.primary_key
19
- keys = scope.pluck(p_key).sort
20
-
21
18
  neighbors = SpatialStats::Queries::Weights
22
19
  .distance_band_neighbors(scope, field, bandwidth)
23
20
 
21
+ # get keys to make sure we have consistent dimensions when
22
+ # some entries don't have neighbors.
23
+ # define a new hash that has all the keys from scope
24
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
25
+
24
26
  neighbors = neighbors.group_by(&:i_id)
27
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
28
+ neighbors = neighbors.merge(missing_neighbors)
29
+
25
30
  weights = neighbors.transform_values do |value|
26
31
  value.map do |neighbor|
27
- hash = neighbor.as_json(only: [:j_id]).symbolize_keys
32
+ hash = { id: neighbor[:j_id] }
28
33
  hash[:weight] = 1
29
34
  hash
30
35
  end
31
36
  end
32
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
37
+ SpatialStats::Weights::WeightsMatrix.new(weights)
33
38
  end
34
39
 
35
40
  ##
@@ -41,21 +46,26 @@ module SpatialStats
41
46
  #
42
47
  # @return [WeightsMatrix]
43
48
  def self.knn(scope, field, k)
44
- p_key = scope.primary_key
45
- keys = scope.pluck(p_key).sort
46
-
47
49
  neighbors = SpatialStats::Queries::Weights
48
50
  .knn(scope, field, k)
49
51
 
52
+ # get keys to make sure we have consistent dimensions when
53
+ # some entries don't have neighbors.
54
+ # define a new hash that has all the keys from scope
55
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
56
+
50
57
  neighbors = neighbors.group_by(&:i_id)
58
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
59
+ neighbors = neighbors.merge(missing_neighbors)
60
+
51
61
  weights = neighbors.transform_values do |value|
52
62
  value.map do |neighbor|
53
- hash = neighbor.as_json(only: [:j_id]).symbolize_keys
63
+ hash = { id: neighbor[:j_id] }
54
64
  hash[:weight] = 1
55
65
  hash
56
66
  end
57
67
  end
58
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
68
+ SpatialStats::Weights::WeightsMatrix.new(weights)
59
69
  end
60
70
 
61
71
  ##
@@ -68,20 +78,24 @@ module SpatialStats
68
78
  #
69
79
  # @return [WeightsMatrix]
70
80
  def self.idw_band(scope, field, bandwidth, alpha = 1)
71
- p_key = scope.primary_key
72
- keys = scope.pluck(p_key).sort
73
-
74
81
  neighbors = SpatialStats::Queries::Weights
75
82
  .idw_band(scope, field, bandwidth, alpha)
83
+
84
+ # get keys to make sure we have consistent dimensions when
85
+ # some entries don't have neighbors.
86
+ # define a new hash that has all the keys from scope
87
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
76
88
  neighbors = neighbors.group_by { |pair| pair[:i_id] }
89
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
90
+ neighbors = neighbors.merge(missing_neighbors)
77
91
 
78
92
  # only keep j_id and weight
79
93
  weights = neighbors.transform_values do |value|
80
94
  value.map do |neighbor|
81
- { weight: neighbor[:weight], j_id: neighbor[:j_id] }
95
+ { weight: neighbor[:weight], id: neighbor[:j_id] }
82
96
  end
83
97
  end
84
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
98
+ SpatialStats::Weights::WeightsMatrix.new(weights)
85
99
  end
86
100
 
87
101
  ##
@@ -94,20 +108,24 @@ module SpatialStats
94
108
  #
95
109
  # @return [WeightsMatrix]
96
110
  def self.idw_knn(scope, field, k, alpha = 1)
97
- p_key = scope.primary_key
98
- keys = scope.pluck(p_key).sort
99
-
100
111
  neighbors = SpatialStats::Queries::Weights
101
112
  .idw_knn(scope, field, k, alpha)
113
+
114
+ # get keys to make sure we have consistent dimensions when
115
+ # some entries don't have neighbors.
116
+ # define a new hash that has all the keys from scope
117
+ keys = SpatialStats::Queries::Variables.query_field(scope, scope.klass.primary_key)
102
118
  neighbors = neighbors.group_by { |pair| pair[:i_id] }
119
+ missing_neighbors = Hash[(keys - neighbors.keys).map { |key| [key, []] }]
120
+ neighbors = neighbors.merge(missing_neighbors)
103
121
 
104
122
  # only keep j_id and weight
105
123
  weights = neighbors.transform_values do |value|
106
124
  value.map do |neighbor|
107
- { weight: neighbor[:weight], j_id: neighbor[:j_id] }
125
+ { weight: neighbor[:weight], id: neighbor[:j_id] }
108
126
  end
109
127
  end
110
- SpatialStats::Weights::WeightsMatrix.new(keys, weights)
128
+ SpatialStats::Weights::WeightsMatrix.new(weights)
111
129
  end
112
130
  end
113
131
  end