spatial_stats 0.1.1 → 0.2.1

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.
@@ -3,7 +3,19 @@
3
3
  require 'numo/narray'
4
4
 
5
5
  module Numo
6
+ ##
7
+ # Extension to Numo::NArray base class.
6
8
  class NArray
9
+ ##
10
+ # For a 2-D NArray, transform the non-zero values so that the sum of each
11
+ # row is 1.
12
+ #
13
+ # @ example
14
+ #
15
+ # Numo::DFloat [[0, 1, 1], [1, 1, 1]].row_standardized
16
+ # Numo::DFloat [[0, 0.5, 0.5], [0.33333, 0.33333, 0.33333]]
17
+ #
18
+ # @return [Numo::NArray]
7
19
  def row_standardized
8
20
  # every row will sum up to 1, or if they are all 0, do nothing
9
21
  standardized = each_over_axis.map do |row|
@@ -20,6 +32,21 @@ module Numo
20
32
  self.class.cast(standardized)
21
33
  end
22
34
 
35
+ ##
36
+ # For a 2-D, n x n NArray, if the trace is 0, add an n x n eye matrix to the matrix
37
+ # and return the result.
38
+ #
39
+ # @ example
40
+ #
41
+ # Numo::DFloat [[0, 1, 0], [1, 0, 1], [0, 1, 0]].windowed
42
+ # Numo::DFloat [[1, 1, 0], [1, 1, 1], [0, 1, 1]]
43
+ #
44
+ # @ example
45
+ # # Input will be equivalent to output in this case
46
+ # Numo::DFloat [[1, 1, 0], [1, 0, 1], [0, 1, 0]].windowed
47
+ # Numo::DFloat [[1, 1, 0], [1, 0, 1], [0, 1, 0]]
48
+ #
49
+ # @return [Numo::NArray]
23
50
  def windowed
24
51
  # in windowed calculations, the diagonal is set to 1
25
52
  # if trace (sum of diag) is 0, add it, else return input
@@ -2,3 +2,9 @@
2
2
 
3
3
  require 'spatial_stats/queries/variables'
4
4
  require 'spatial_stats/queries/weights'
5
+
6
+ module SpatialStats
7
+ # The Queries module contains the ActiveRecord/PostGIS interface for the gem.
8
+ module Queries
9
+ end
10
+ end
@@ -2,10 +2,23 @@
2
2
 
3
3
  module SpatialStats
4
4
  module Queries
5
+ ##
6
+ # Variables includes a method to query a field from a given scope and
7
+ # keep it in a consistent order with how weights are queried.
5
8
  module Variables
6
- # Module to query for the desired variable from the given scope
7
- # and include the primary keys so that the weights matrix
8
- # will know that its keys will match up with the variables.
9
+ ##
10
+ # Query the given field for a scope and order by primary key
11
+ #
12
+ # @example
13
+ # scope = County.all
14
+ # field = :avg_income
15
+ # SpatialStats::Queries::Variables.query_field(scope, field)
16
+ # # => [30023, 23400, 57800, ...]
17
+ #
18
+ # @param [ActiveRecord::Relation] scope you want to query
19
+ # @param [Symbol, String] field you want to query from the scope
20
+ #
21
+ # @return [Array]
9
22
  def self.query_field(scope, field)
10
23
  klass = scope.klass
11
24
  column = ActiveRecord::Base.connection.quote_column_name(field)
@@ -2,14 +2,31 @@
2
2
 
3
3
  module SpatialStats
4
4
  module Queries
5
- # This provides PostGIS queries for calculating weights/neighbors
6
- # of spatial data sets
5
+ ##
6
+ # Weights includes methods for querying a scope using PostGIS sql methods
7
+ # to determine neighbors and weights based on different weighting
8
+ # schemes/formulas.
7
9
  module Weights
8
- def self.idw_knn(scope, column, n, alpha)
10
+ ##
11
+ # Compute inverse distance weighted, k nearest neighbors weights
12
+ # for a given scope and geometry.
13
+ #
14
+ # Combines knn and idw weightings. Each observation will have
15
+ # k neighbors, but the weights will be calculated by 1/(d**alpha).
16
+ #
17
+ # Only works for geometry types that implement ST_Distance.
18
+ #
19
+ # @param [ActiveRecord::Relation] scope you want to query
20
+ # @param [Symbol, String] column that contains the geometry
21
+ # @param [Integer] k neighbors to find
22
+ # @param [Integer] alpha number used in inverse calculations (usually 1 or 2)
23
+ #
24
+ # @return [Hash]
25
+ def self.idw_knn(scope, column, k, alpha = 1)
9
26
  klass = scope.klass
10
27
  column = ActiveRecord::Base.connection.quote_column_name(column)
11
28
  primary_key = klass.quoted_primary_key
12
- neighbors = klass.find_by_sql([<<-SQL, scope: scope, n: n])
29
+ neighbors = klass.find_by_sql([<<-SQL, scope: scope, k: k])
13
30
  WITH scope as (:scope)
14
31
  SELECT neighbors.*
15
32
  FROM scope AS a
@@ -19,7 +36,7 @@ module SpatialStats
19
36
  FROM scope as b
20
37
  WHERE a.#{primary_key} <> b.#{primary_key}
21
38
  ORDER BY a.#{column} <-> b.#{column}
22
- LIMIT :n
39
+ LIMIT :k
23
40
  ) AS neighbors
24
41
  SQL
25
42
 
@@ -41,6 +58,21 @@ module SpatialStats
41
58
  end
42
59
  end
43
60
 
61
+ ##
62
+ # Compute inverse distance weighted, band limited weights
63
+ # for a given scope and geometry.
64
+ #
65
+ # Combines distance_band and idw weightings. Each observation will have
66
+ # neighbers in the bandwidth, but the weights will be calculated by 1/(d**alpha).
67
+ #
68
+ # Only works for geometry types that implement ST_Distance and ST_DWithin.
69
+ #
70
+ # @param [ActiveRecord::Relation] scope you want to query
71
+ # @param [Symbol, String] column that contains the geometry
72
+ # @param [Numeric] bandwidth to find neighbors in
73
+ # @param [Integer] alpha number used in inverse calculations (usually 1 or 2)
74
+ #
75
+ # @return [Hash]
44
76
  def self.idw_band(scope, column, bandwidth, alpha = 1)
45
77
  klass = scope.klass
46
78
  column = ActiveRecord::Base.connection.quote_column_name(column)
@@ -75,11 +107,19 @@ module SpatialStats
75
107
  end
76
108
  end
77
109
 
78
- def self.knn(scope, column, n)
110
+ ##
111
+ # Compute k nearest neighbor weights for a given scope.
112
+ #
113
+ # @param [ActiveRecord::Relation] scope you want to query
114
+ # @param [Symbol, String] column that contains the geometry
115
+ # @param [Integer] k neighbors to find
116
+ #
117
+ # @return [Hash]
118
+ def self.knn(scope, column, k)
79
119
  klass = scope.klass
80
120
  column = ActiveRecord::Base.connection.quote_column_name(column)
81
121
  primary_key = klass.quoted_primary_key
82
- klass.find_by_sql([<<-SQL, scope: scope, n: n])
122
+ klass.find_by_sql([<<-SQL, scope: scope, k: k])
83
123
  WITH scope as (:scope)
84
124
  SELECT neighbors.*
85
125
  FROM scope AS a
@@ -88,11 +128,21 @@ module SpatialStats
88
128
  FROM scope as b
89
129
  WHERE a.#{primary_key} <> b.#{primary_key}
90
130
  ORDER BY a.#{column} <-> b.#{column}
91
- LIMIT :n
131
+ LIMIT :k
92
132
  ) AS neighbors
93
133
  SQL
94
134
  end
95
135
 
136
+ ##
137
+ # Compute distance band weights for a given scope. Identifies neighbors
138
+ # as other observations in scope that are within the distance band
139
+ # from the observation.
140
+ #
141
+ # @param [ActiveRecord::Relation] scope you want to query
142
+ # @param [Symbol, String] column that contains the geometry
143
+ # @param [Numeric] bandwidth to find neighbors in
144
+ #
145
+ # @return [Hash]
96
146
  def self.distance_band_neighbors(scope, column, bandwidth)
97
147
  klass = scope.klass
98
148
  column = ActiveRecord::Base.connection.quote_column_name(column)
@@ -109,15 +159,47 @@ module SpatialStats
109
159
  SQL
110
160
  end
111
161
 
112
- # DE-9IM queen contiguiety = F***T****
162
+ ##
163
+ # Compute queen contiguity weights for a given scope. Queen
164
+ # contiguity weights are defined by geometries sharing an edge
165
+ # or vertex.
166
+ #
167
+ # DE-9IM pattern = +F***T****+
168
+ #
169
+ # @param [ActiveRecord::Relation] scope you want to query
170
+ # @param [Symbol, String] column that contains the geometry
171
+ #
172
+ # @return [Hash]
113
173
  def self.queen_contiguity_neighbors(scope, column)
114
174
  _contiguity_neighbors(scope, column, 'F***T****')
115
175
  end
116
176
 
177
+ ##
178
+ # Compute rook contiguity weights for a given scope. Rook
179
+ # contiguity weights are defined by geometries sharing an edge.
180
+ #
181
+ # DE-9IM pattern = +'F***1****'+
182
+ #
183
+ # @param [ActiveRecord::Relation] scope you want to query
184
+ # @param [Symbol, String] column that contains the geometry
185
+ #
186
+ # @return [Hash]
117
187
  def self.rook_contiguity_neighbors(scope, column)
118
188
  _contiguity_neighbors(scope, column, 'F***1****')
119
189
  end
120
190
 
191
+ ##
192
+ # Generic function to compute contiguity neighbor weights for a
193
+ # given scope. Takes any valid DE-9IM pattern and computes the
194
+ # neighbors based off of that.
195
+ #
196
+ # @see https://en.wikipedia.org/wiki/DE-9IM
197
+ #
198
+ # @param [ActiveRecord::Relation] scope you want to query
199
+ # @param [Symbol, String] column that contains the geometry
200
+ # @param [String] pattern to describe neighbor relation
201
+ #
202
+ # @return [Hash]
121
203
  def self._contiguity_neighbors(scope, column, pattern)
122
204
  klass = scope.klass
123
205
  column = ActiveRecord::Base.connection.quote_column_name(column)
@@ -1,3 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'spatial_stats/utils/lag'
4
+
5
+ module SpatialStats
6
+ ##
7
+ # The Utils module contains various utilities used in the gem.
8
+ module Utils
9
+ end
10
+ end
@@ -3,23 +3,55 @@
3
3
  require 'numo/narray'
4
4
  module SpatialStats
5
5
  module Utils
6
+ ##
7
+ # Lag includes methdos for computing spatially lagged variables under
8
+ # different contexts.
6
9
  module Lag
7
- # module for computing spatially lagged variables
8
- # from a weights matrix and variable array
10
+ ##
11
+ # Dot product of the row_standardized input matrix
12
+ # by the input vector, variables.
13
+ #
14
+ # @param [Numo::NArray] matrix 2-D square matrix.
15
+ # @param [Array] variables vector multiplying the matrix
16
+ #
17
+ # @return [Array] resultant vector
9
18
  def self.neighbor_average(matrix, variables)
10
19
  matrix = matrix.row_standardized
11
20
  neighbor_sum(matrix, variables)
12
21
  end
13
22
 
23
+ ##
24
+ # Dot product of the input matrix by the input vector, variables.
25
+ #
26
+ # @param [Numo::NArray] matrix 2-D square matrix.
27
+ # @param [Array] variables vector multiplying the matrix
28
+ #
29
+ # @return [Array] resultant vector
14
30
  def self.neighbor_sum(matrix, variables)
15
31
  matrix.dot(variables).to_a
16
32
  end
17
33
 
34
+ ##
35
+ # Dot product of the input windowed, row standardizd matrix by
36
+ # the input vector, variables.
37
+ #
38
+ # @param [Numo::NArray] matrix 2-D square matrix.
39
+ # @param [Array] variables vector multiplying the matrix
40
+ #
41
+ # @return [Array] resultant vector
18
42
  def self.window_average(matrix, variables)
19
43
  matrix = matrix.windowed.row_standardized
20
44
  window_sum(matrix, variables)
21
45
  end
22
46
 
47
+ ##
48
+ # Dot product of the input windowed matrix by
49
+ # the input vector, variables.
50
+ #
51
+ # @param [Numo::NArray] matrix 2-D square matrix.
52
+ # @param [Array] variables vector multiplying the matrix
53
+ #
54
+ # @return [Array] resultant vector
23
55
  def self.window_sum(matrix, variables)
24
56
  matrix = matrix.windowed
25
57
  matrix.dot(variables).to_a
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpatialStats
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.1'
5
5
  end
@@ -3,3 +3,12 @@
3
3
  require 'spatial_stats/weights/contiguous'
4
4
  require 'spatial_stats/weights/distant'
5
5
  require 'spatial_stats/weights/weights_matrix'
6
+
7
+ module SpatialStats
8
+ ##
9
+ # The Weights module contains the classes for computing different types of
10
+ # weight matrices and a weights matrix class to work with the results of
11
+ # queries.
12
+ module Weights
13
+ end
14
+ end
@@ -2,7 +2,18 @@
2
2
 
3
3
  module SpatialStats
4
4
  module Weights
5
+ ##
6
+ # Contiguous weights module includes methods that provide an interface to
7
+ # coniguous weights queries and formats the result properly to utilize
8
+ # a weights matrix.
5
9
  module Contiguous
10
+ ##
11
+ # Compute rook weights matrix for a scope.
12
+ #
13
+ # @param [ActiveRecord::Relation] scope to query
14
+ # @param [Symbol, String] field with geometry in it
15
+ #
16
+ # @return [WeightsMatrix]
6
17
  def self.rook(scope, field)
7
18
  p_key = scope.primary_key
8
19
  keys = scope.pluck(p_key).sort
@@ -21,6 +32,13 @@ module SpatialStats
21
32
  SpatialStats::Weights::WeightsMatrix.new(keys, weights)
22
33
  end
23
34
 
35
+ ##
36
+ # Compute queen weights matrix for a scope.
37
+ #
38
+ # @param [ActiveRecord::Relation] scope to query
39
+ # @param [Symbol, String] field with geometry in it
40
+ #
41
+ # @return [WeightsMatrix]
24
42
  def self.queen(scope, field)
25
43
  p_key = scope.primary_key
26
44
  keys = scope.pluck(p_key).sort
@@ -2,7 +2,18 @@
2
2
 
3
3
  module SpatialStats
4
4
  module Weights
5
+ # Distant weights module includes methods that provide an interface to
6
+ # distance-based weights queries and formats the result properly to utilize
7
+ # a weights matrix.
5
8
  module Distant
9
+ ##
10
+ # Compute distance band weights matrix for a scope.
11
+ #
12
+ # @param [ActiveRecord::Relation] scope to query
13
+ # @param [Symbol, String] field with geometry in it
14
+ # @param [Numeric] bandwidth of distance band
15
+ #
16
+ # @return [WeightsMatrix]
6
17
  def self.distance_band(scope, field, bandwidth)
7
18
  p_key = scope.primary_key
8
19
  keys = scope.pluck(p_key).sort
@@ -21,12 +32,20 @@ module SpatialStats
21
32
  SpatialStats::Weights::WeightsMatrix.new(keys, weights)
22
33
  end
23
34
 
24
- def self.knn(scope, field, n)
35
+ ##
36
+ # Compute distance band weights matrix for a scope.
37
+ #
38
+ # @param [ActiveRecord::Relation] scope to query
39
+ # @param [Symbol, String] field with geometry in it
40
+ # @param [Integer] k neighbors to find
41
+ #
42
+ # @return [WeightsMatrix]
43
+ def self.knn(scope, field, k)
25
44
  p_key = scope.primary_key
26
45
  keys = scope.pluck(p_key).sort
27
46
 
28
47
  neighbors = SpatialStats::Queries::Weights
29
- .knn(scope, field, n)
48
+ .knn(scope, field, k)
30
49
 
31
50
  neighbors = neighbors.group_by(&:i_id)
32
51
  weights = neighbors.transform_values do |value|
@@ -39,6 +58,15 @@ module SpatialStats
39
58
  SpatialStats::Weights::WeightsMatrix.new(keys, weights)
40
59
  end
41
60
 
61
+ ##
62
+ # Compute idw, distance band weights matrix for a scope.
63
+ #
64
+ # @param [ActiveRecord::Relation] scope to query
65
+ # @param [Symbol, String] field with geometry in it
66
+ # @param [Numeric] bandwidth of distance band
67
+ # @param [Numeric] alpha used in weighting calculation (usually 1 or 2)
68
+ #
69
+ # @return [WeightsMatrix]
42
70
  def self.idw_band(scope, field, bandwidth, alpha = 1)
43
71
  p_key = scope.primary_key
44
72
  keys = scope.pluck(p_key).sort
@@ -56,12 +84,21 @@ module SpatialStats
56
84
  SpatialStats::Weights::WeightsMatrix.new(keys, weights)
57
85
  end
58
86
 
59
- def self.idw_knn(scope, field, n, alpha = 1)
87
+ ##
88
+ # Compute idw, knn weights matrix for a scope.
89
+ #
90
+ # @param [ActiveRecord::Relation] scope to query
91
+ # @param [Symbol, String] field with geometry in it
92
+ # @param [Integer] k neighbors to find
93
+ # @param [Numeric] alpha used in weighting calculation (usually 1 or 2)
94
+ #
95
+ # @return [WeightsMatrix]
96
+ def self.idw_knn(scope, field, k, alpha = 1)
60
97
  p_key = scope.primary_key
61
98
  keys = scope.pluck(p_key).sort
62
99
 
63
100
  neighbors = SpatialStats::Queries::Weights
64
- .idw_knn(scope, field, n, alpha)
101
+ .idw_knn(scope, field, k, alpha)
65
102
  neighbors = neighbors.group_by { |pair| pair[:i_id] }
66
103
 
67
104
  # only keep j_id and weight
@@ -4,7 +4,17 @@ require 'numo/narray'
4
4
 
5
5
  module SpatialStats
6
6
  module Weights
7
+ ##
8
+ # WeightsMatrix class is used to store spatial weights and related
9
+ # information in various formats.
7
10
  class WeightsMatrix
11
+ ##
12
+ # A new instance of WeightsMatrix
13
+ #
14
+ # @param [Array] keys ordered list of keys used in weights
15
+ # @param [Hash] weights hash of format +{key: [{j_id: neighbor_key, weight: 1}]}+ that describe the relations between neighbors
16
+ #
17
+ # @return [WeightsMatrix]
8
18
  def initialize(keys, weights)
9
19
  @keys = keys
10
20
  @weights = weights
@@ -12,6 +22,17 @@ module SpatialStats
12
22
  end
13
23
  attr_accessor :keys, :weights, :n
14
24
 
25
+ ##
26
+ # Compute the n x n Numo::Narray of the weights hash.
27
+ #
28
+ # @example
29
+ # hash = {1 => [{j_id: 2, weight: 1}], 2 => [{j_id: 1, weight: 1},
30
+ # {j_id: 3, weight: 1}], 3 => [{j_id: 2, weight: 1}]}
31
+ # wm = WeightsMatrix.new(hash.keys, hash)
32
+ # wm.full
33
+ # # => Numo::DFloat[[0, 1, 0], [1, 0, 1], [0, 1, 0]]
34
+ #
35
+ # @return [Numo::DFloat]
15
36
  def full
16
37
  # returns a square matrix Wij using @keys as the order of items
17
38
  @full ||= begin
@@ -34,6 +55,10 @@ module SpatialStats
34
55
  end
35
56
  end
36
57
 
58
+ ##
59
+ # Row standardized version of +#full+
60
+ #
61
+ # @return [Numo::DFloat]
37
62
  def standardized
38
63
  @standardized ||= full.row_standardized
39
64
  end