spatial_stats 0.2.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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