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.
- checksums.yaml +4 -4
- data/README.md +83 -11
- data/Rakefile +7 -0
- data/ext/spatial_stats/csr_matrix.c +365 -0
- data/ext/spatial_stats/csr_matrix.h +35 -0
- data/ext/spatial_stats/extconf.rb +6 -0
- data/ext/spatial_stats/spatial_stats.c +32 -0
- data/lib/spatial_stats/global/bivariate_moran.rb +43 -21
- data/lib/spatial_stats/global/moran.rb +43 -35
- data/lib/spatial_stats/global/stat.rb +31 -27
- data/lib/spatial_stats/local/bivariate_moran.rb +67 -2
- data/lib/spatial_stats/local/geary.rb +36 -5
- data/lib/spatial_stats/local/getis_ord.rb +45 -17
- data/lib/spatial_stats/local/moran.rb +40 -9
- data/lib/spatial_stats/local/multivariate_geary.rb +29 -10
- data/lib/spatial_stats/local/stat.rb +72 -36
- data/lib/spatial_stats/narray_ext.rb +5 -5
- data/lib/spatial_stats/spatial_stats.so +0 -0
- data/lib/spatial_stats/utils/lag.rb +10 -10
- data/lib/spatial_stats/version.rb +1 -1
- data/lib/spatial_stats/weights/contiguous.rb +20 -10
- data/lib/spatial_stats/weights/distant.rb +38 -20
- data/lib/spatial_stats/weights/weights_matrix.rb +60 -26
- data/lib/spatial_stats.rb +1 -0
- metadata +25 -3
@@ -19,7 +19,7 @@ module SpatialStats
|
|
19
19
|
@scope = scope
|
20
20
|
@x_field = x_field
|
21
21
|
@y_field = y_field
|
22
|
-
@weights = weights
|
22
|
+
@weights = weights.standardize
|
23
23
|
end
|
24
24
|
attr_writer :x, :y
|
25
25
|
|
@@ -29,8 +29,7 @@ module SpatialStats
|
|
29
29
|
#
|
30
30
|
# @return [Float]
|
31
31
|
def stat
|
32
|
-
|
33
|
-
y_lag = SpatialStats::Utils::Lag.neighbor_sum(w, y)
|
32
|
+
y_lag = SpatialStats::Utils::Lag.neighbor_sum(weights, y)
|
34
33
|
numerator = 0
|
35
34
|
x.each_with_index do |xi, idx|
|
36
35
|
numerator += xi * y_lag[idx]
|
@@ -55,19 +54,20 @@ module SpatialStats
|
|
55
54
|
#
|
56
55
|
# @return [Float]
|
57
56
|
def variance
|
58
|
-
n =
|
59
|
-
|
60
|
-
w = wij.sum
|
57
|
+
n = weights.n
|
58
|
+
w_sum = n.to_f
|
61
59
|
e = expectation
|
62
60
|
|
63
|
-
|
64
|
-
|
61
|
+
wij = weights.sparse.coordinates
|
62
|
+
|
63
|
+
s1 = s1_calc(wij)
|
64
|
+
s2 = s2_calc(n, wij, weights.sparse.row_index)
|
65
65
|
s3 = s3_calc(n, x)
|
66
66
|
|
67
|
-
s4 = (n**2 - 3 * n + 3) * s1 - n * s2 + 3 * (
|
68
|
-
s5 = (n**2 - n) * s1 - 2 * n * s2 + 6 * (
|
67
|
+
s4 = (n**2 - 3 * n + 3) * s1 - n * s2 + 3 * (w_sum**2)
|
68
|
+
s5 = (n**2 - n) * s1 - 2 * n * s2 + 6 * (w_sum**2)
|
69
69
|
|
70
|
-
var_left = (n * s4 - s3 * s5) / ((n - 1) * (n - 2) * (n - 3) *
|
70
|
+
var_left = (n * s4 - s3 * s5) / ((n - 1) * (n - 2) * (n - 3) * w_sum**2)
|
71
71
|
var_right = e**2
|
72
72
|
var_left - var_right
|
73
73
|
end
|
@@ -88,6 +88,19 @@ module SpatialStats
|
|
88
88
|
mc_bv(permutations, seed)
|
89
89
|
end
|
90
90
|
|
91
|
+
##
|
92
|
+
# Summary of the statistic. Computes +stat+ and +mc+ and returns the values
|
93
|
+
# in a hash.
|
94
|
+
#
|
95
|
+
# @param [Integer] permutations to run. Last digit should be 9 to produce round numbers.
|
96
|
+
# @param [Integer] seed used in random number generator for shuffles.
|
97
|
+
#
|
98
|
+
# @return [Hash]
|
99
|
+
def summary(permutations = 99, seed = nil)
|
100
|
+
p_val = mc(permutations, seed)
|
101
|
+
{ stat: stat, p: p_val }
|
102
|
+
end
|
103
|
+
|
91
104
|
##
|
92
105
|
# Standardized variables queried from +x_field+.
|
93
106
|
#
|
@@ -108,32 +121,41 @@ module SpatialStats
|
|
108
121
|
|
109
122
|
private
|
110
123
|
|
124
|
+
def stat_mc(perms)
|
125
|
+
x_arr = Numo::DFloat.cast(x)
|
126
|
+
lag = w.dot(perms.transpose)
|
127
|
+
x_arr.dot(lag) / (x_arr**2).sum
|
128
|
+
end
|
129
|
+
|
111
130
|
def s3_calc(n, zs)
|
112
131
|
numerator = (1.0 / n) * zs.sum { |v| v**4 }
|
113
132
|
denominator = ((1.0 / n) * zs.sum { |v| v**2 })**2
|
114
133
|
numerator / denominator
|
115
134
|
end
|
116
135
|
|
117
|
-
def s2_calc(n, wij)
|
136
|
+
def s2_calc(n, wij, row_index)
|
118
137
|
s2 = 0
|
119
|
-
|
138
|
+
wij_arr = wij.to_a # for row slicing
|
139
|
+
(0..n - 1).each do |idx|
|
140
|
+
row = wij_arr[row_index[idx]..(row_index[idx + 1] - 1)]
|
120
141
|
left_term = 0
|
121
142
|
right_term = 0
|
122
|
-
|
123
|
-
|
124
|
-
|
143
|
+
|
144
|
+
row.each do |coords, val|
|
145
|
+
left_term += val
|
146
|
+
right_term += wij[coords.reverse] || 0
|
125
147
|
end
|
126
148
|
s2 += (left_term + right_term)**2
|
127
149
|
end
|
128
150
|
s2
|
129
151
|
end
|
130
152
|
|
131
|
-
def s1_calc(
|
153
|
+
def s1_calc(wij)
|
132
154
|
s1 = 0
|
133
|
-
|
134
|
-
(
|
135
|
-
|
136
|
-
|
155
|
+
wij.each do |coords, val|
|
156
|
+
# (wij + wji)**2
|
157
|
+
wji = wij[coords.reverse] || 0
|
158
|
+
s1 += (val + wji)**2
|
137
159
|
end
|
138
160
|
s1 / 2
|
139
161
|
end
|
@@ -29,8 +29,7 @@ module SpatialStats
|
|
29
29
|
# compute's Moran's I. numerator is sum of zi * spatial lag of zi
|
30
30
|
# denominator is sum of zi**2.
|
31
31
|
# have to use row-standardized weights
|
32
|
-
|
33
|
-
z_lag = SpatialStats::Utils::Lag.neighbor_sum(w, z)
|
32
|
+
z_lag = SpatialStats::Utils::Lag.neighbor_sum(weights, z)
|
34
33
|
numerator = 0
|
35
34
|
z.each_with_index do |zi, j|
|
36
35
|
row_sum = zi * z_lag[j]
|
@@ -49,7 +48,7 @@ module SpatialStats
|
|
49
48
|
# @return [Float]
|
50
49
|
def expectation
|
51
50
|
# -1/(n-1)
|
52
|
-
-1.0 / (
|
51
|
+
-1.0 / (weights.n - 1)
|
53
52
|
end
|
54
53
|
|
55
54
|
##
|
@@ -58,19 +57,20 @@ module SpatialStats
|
|
58
57
|
#
|
59
58
|
# @return [Float]
|
60
59
|
def variance
|
61
|
-
n =
|
62
|
-
|
63
|
-
w = wij.sum
|
60
|
+
n = weights.n
|
61
|
+
w_sum = n # standardized weights
|
64
62
|
e = expectation
|
65
63
|
|
66
|
-
|
67
|
-
|
64
|
+
wij = weights.sparse.coordinates
|
65
|
+
|
66
|
+
s1 = s1_calc(wij)
|
67
|
+
s2 = s2_calc(n, wij, weights.sparse.row_index)
|
68
68
|
s3 = s3_calc(n, z)
|
69
69
|
|
70
|
-
s4 = (n**2 - 3 * n + 3) * s1 - n * s2 + 3 * (
|
71
|
-
s5 = (n**2 - n) * s1 - 2 * n * s2 + 6 * (
|
70
|
+
s4 = (n**2 - 3 * n + 3) * s1 - n * s2 + 3 * (w_sum**2)
|
71
|
+
s5 = (n**2 - n) * s1 - 2 * n * s2 + 6 * (w_sum**2)
|
72
72
|
|
73
|
-
var_left = (n * s4 - s3 * s5) / ((n - 1) * (n - 2) * (n - 3) *
|
73
|
+
var_left = (n * s4 - s3 * s5) / ((n - 1) * (n - 2) * (n - 3) * w_sum**2)
|
74
74
|
var_right = e**2
|
75
75
|
var_left - var_right
|
76
76
|
end
|
@@ -92,58 +92,66 @@ module SpatialStats
|
|
92
92
|
end
|
93
93
|
|
94
94
|
##
|
95
|
-
#
|
95
|
+
# Summary of the statistic. Computes +stat+ and +mc+ and returns the values
|
96
|
+
# in a hash.
|
96
97
|
#
|
97
|
-
# @
|
98
|
-
|
99
|
-
@x ||= SpatialStats::Queries::Variables.query_field(@scope, @field)
|
100
|
-
end
|
101
|
-
|
102
|
-
# TODO: remove these last 2 methods and just standardize x.
|
103
|
-
##
|
104
|
-
# Mean of x
|
98
|
+
# @param [Integer] permutations to run. Last digit should be 9 to produce round numbers.
|
99
|
+
# @param [Integer] seed used in random number generator for shuffles.
|
105
100
|
#
|
106
|
-
# @return [
|
107
|
-
def
|
108
|
-
|
101
|
+
# @return [Hash]
|
102
|
+
def summary(permutations = 99, seed = nil)
|
103
|
+
p_val = mc(permutations, seed)
|
104
|
+
{ stat: stat, p: p_val }
|
109
105
|
end
|
110
106
|
|
111
107
|
##
|
112
|
-
#
|
108
|
+
# Values of the +field+ queried from the +scope+
|
113
109
|
#
|
114
110
|
# @return [Array]
|
115
|
-
def
|
116
|
-
x
|
111
|
+
def x
|
112
|
+
@x ||= SpatialStats::Queries::Variables.query_field(@scope, @field)
|
113
|
+
.standardize
|
117
114
|
end
|
115
|
+
alias z x
|
118
116
|
|
119
117
|
private
|
120
118
|
|
119
|
+
def stat_mc(perms)
|
120
|
+
z_arr = Numo::DFloat.cast(z)
|
121
|
+
lag = w.dot(perms.transpose)
|
122
|
+
z_arr.dot(lag) / (z_arr**2).sum
|
123
|
+
end
|
124
|
+
|
121
125
|
def s3_calc(n, zs)
|
122
126
|
numerator = (1.0 / n) * zs.sum { |v| v**4 }
|
123
127
|
denominator = ((1.0 / n) * zs.sum { |v| v**2 })**2
|
124
128
|
numerator / denominator
|
125
129
|
end
|
126
130
|
|
127
|
-
|
131
|
+
# use row_index to take slices of wij
|
132
|
+
def s2_calc(n, wij, row_index)
|
128
133
|
s2 = 0
|
134
|
+
wij_arr = wij.to_a # for row slicing
|
129
135
|
(0..n - 1).each do |idx|
|
136
|
+
row = wij_arr[row_index[idx]..(row_index[idx + 1] - 1)]
|
130
137
|
left_term = 0
|
131
138
|
right_term = 0
|
132
|
-
|
133
|
-
|
134
|
-
|
139
|
+
|
140
|
+
row.each do |coords, val|
|
141
|
+
left_term += val
|
142
|
+
right_term += wij[coords.reverse] || 0
|
135
143
|
end
|
136
144
|
s2 += (left_term + right_term)**2
|
137
145
|
end
|
138
146
|
s2
|
139
147
|
end
|
140
148
|
|
141
|
-
def s1_calc(
|
149
|
+
def s1_calc(wij)
|
142
150
|
s1 = 0
|
143
|
-
|
144
|
-
(
|
145
|
-
|
146
|
-
|
151
|
+
wij.each do |coords, val|
|
152
|
+
# (wij + wji)**2
|
153
|
+
wji = wij[coords.reverse] || 0
|
154
|
+
s1 += (val + wji)**2
|
147
155
|
end
|
148
156
|
s1 / 2
|
149
157
|
end
|
@@ -11,7 +11,7 @@ module SpatialStats
|
|
11
11
|
def initialize(scope, field, weights)
|
12
12
|
@scope = scope
|
13
13
|
@field = field
|
14
|
-
@weights = weights
|
14
|
+
@weights = weights.standardize
|
15
15
|
end
|
16
16
|
attr_accessor :scope, :field, :weights
|
17
17
|
|
@@ -45,21 +45,21 @@ module SpatialStats
|
|
45
45
|
permutations.times do
|
46
46
|
shuffles << x.shuffle(random: rng)
|
47
47
|
end
|
48
|
+
shuffles = Numo::DFloat.cast(shuffles)
|
49
|
+
|
48
50
|
# r is the number of equal to or more extreme samples
|
49
51
|
# one sided
|
50
|
-
stat_orig = stat
|
51
|
-
r = 0
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
end
|
62
|
-
end
|
52
|
+
stat_orig = stat.round(5)
|
53
|
+
# r = 0
|
54
|
+
|
55
|
+
# compute new stat values
|
56
|
+
stat_new = stat_mc(shuffles)
|
57
|
+
|
58
|
+
r = if stat_orig.positive?
|
59
|
+
(stat_new >= stat_orig).count
|
60
|
+
else
|
61
|
+
(stat_new <= stat_orig).count
|
62
|
+
end
|
63
63
|
|
64
64
|
(r + 1.0) / (permutations + 1.0)
|
65
65
|
end
|
@@ -71,27 +71,31 @@ module SpatialStats
|
|
71
71
|
permutations.times do
|
72
72
|
shuffles << y.shuffle(random: rng)
|
73
73
|
end
|
74
|
+
shuffles = Numo::DFloat.cast(shuffles)
|
74
75
|
|
75
76
|
# r is the number of equal to or more extreme samples
|
76
|
-
stat_orig = stat
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
r += 1 if klass.stat >= stat_orig
|
85
|
-
else
|
86
|
-
r += 1 if klass.stat <= stat_orig
|
87
|
-
end
|
88
|
-
end
|
77
|
+
stat_orig = stat.round(5)
|
78
|
+
stat_new = stat_mc(shuffles)
|
79
|
+
|
80
|
+
r = if stat_orig.positive?
|
81
|
+
(stat_new >= stat_orig).count
|
82
|
+
else
|
83
|
+
(stat_new <= stat_orig).count
|
84
|
+
end
|
89
85
|
|
90
86
|
(r + 1.0) / (permutations + 1.0)
|
91
87
|
end
|
92
88
|
|
93
89
|
private
|
94
90
|
|
91
|
+
def stat_mc(_shuffles)
|
92
|
+
raise NotImplementedError, 'private method stat_mc not defined'
|
93
|
+
end
|
94
|
+
|
95
|
+
def w
|
96
|
+
@w ||= weights.dense
|
97
|
+
end
|
98
|
+
|
95
99
|
def gen_rng(seed)
|
96
100
|
if seed
|
97
101
|
Random.new(seed)
|
@@ -19,9 +19,10 @@ module SpatialStats
|
|
19
19
|
@scope = scope
|
20
20
|
@x_field = x_field
|
21
21
|
@y_field = y_field
|
22
|
-
@weights = weights
|
22
|
+
@weights = weights.standardize
|
23
23
|
end
|
24
24
|
attr_accessor :scope, :x_field, :y_field, :weights
|
25
|
+
attr_writer :x, :y
|
25
26
|
|
26
27
|
##
|
27
28
|
# Computes the local indicator of spatial correlation for
|
@@ -62,6 +63,61 @@ module SpatialStats
|
|
62
63
|
mc_bv(permutations, seed)
|
63
64
|
end
|
64
65
|
|
66
|
+
##
|
67
|
+
# Determines what quadrant an observation is in. Based on its value
|
68
|
+
# compared to its neighbors. This does not work for all stats, since
|
69
|
+
# it requires that values be negative.
|
70
|
+
#
|
71
|
+
# In a standardized array of z, high values are values greater than 0
|
72
|
+
# and it's neighbors are determined by the spatial lag and if that is
|
73
|
+
# positive then it's neighbors would be high, low otherwise.
|
74
|
+
#
|
75
|
+
# Quadrants are:
|
76
|
+
# [HH] a high value surrounded by other high values
|
77
|
+
# [LH] a low value surrounded by high values
|
78
|
+
# [LL] a low value surrounded by low values
|
79
|
+
# [HL] a high value surrounded by low values
|
80
|
+
#
|
81
|
+
# @return [Array] of labels
|
82
|
+
def quads
|
83
|
+
# https://github.com/pysal/esda/blob/master/esda/moran.py#L925
|
84
|
+
z_lag = SpatialStats::Utils::Lag.neighbor_average(weights, y)
|
85
|
+
zp = x.map(&:positive?)
|
86
|
+
lp = z_lag.map(&:positive?)
|
87
|
+
|
88
|
+
# hh = zp & lp
|
89
|
+
# lh = zp ^ true & lp
|
90
|
+
# ll = zp ^ true & lp ^ true
|
91
|
+
# hl = zp next to lp ^ true
|
92
|
+
hh = zp.each_with_index.map { |v, idx| v & lp[idx] }
|
93
|
+
lh = zp.each_with_index.map { |v, idx| (v ^ true) & lp[idx] }
|
94
|
+
ll = zp.each_with_index.map { |v, idx| (v ^ true) & (lp[idx] ^ true) }
|
95
|
+
hl = zp.each_with_index.map { |v, idx| v & (lp[idx] ^ true) }
|
96
|
+
|
97
|
+
# now zip lists and map them to proper terms
|
98
|
+
quad_terms = %w[HH LH LL HL]
|
99
|
+
hh.zip(lh, ll, hl).map do |feature|
|
100
|
+
quad_terms[feature.index(true)]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
alias groups quads
|
104
|
+
|
105
|
+
##
|
106
|
+
# Summary of the statistic. Computes +stat+, +mc+, and +groups+ then returns the values
|
107
|
+
# in a hash array.
|
108
|
+
#
|
109
|
+
# @param [Integer] permutations to run. Last digit should be 9 to produce round numbers.
|
110
|
+
# @param [Integer] seed used in random number generator for shuffles.
|
111
|
+
#
|
112
|
+
# @return [Array]
|
113
|
+
def summary(permutations = 99, seed = nil)
|
114
|
+
p_vals = mc(permutations, seed)
|
115
|
+
data = weights.keys.zip(stat, p_vals, groups)
|
116
|
+
data.map do |row|
|
117
|
+
{ key: row[0], stat: row[1], p: row[2], group: row[3] }
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
65
121
|
def x
|
66
122
|
@x ||= SpatialStats::Queries::Variables.query_field(@scope, @x_field)
|
67
123
|
.standardize
|
@@ -79,8 +135,17 @@ module SpatialStats
|
|
79
135
|
x[idx] * y_lag_i
|
80
136
|
end
|
81
137
|
|
138
|
+
def mc_observation_calc(stat_i_orig, stat_i_new, _permutations)
|
139
|
+
# Since moran can be positive or negative, go by this definition
|
140
|
+
if stat_i_orig.positive?
|
141
|
+
(stat_i_new >= stat_i_orig).count
|
142
|
+
else
|
143
|
+
(stat_i_new <= stat_i_orig).count
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
82
147
|
def y_lag
|
83
|
-
@y_lag ||= SpatialStats::Utils::Lag.neighbor_sum(
|
148
|
+
@y_lag ||= SpatialStats::Utils::Lag.neighbor_sum(weights, y)
|
84
149
|
end
|
85
150
|
end
|
86
151
|
end
|
@@ -18,6 +18,7 @@ module SpatialStats
|
|
18
18
|
def initialize(scope, field, weights)
|
19
19
|
super(scope, field, weights)
|
20
20
|
end
|
21
|
+
attr_writer :x
|
21
22
|
|
22
23
|
##
|
23
24
|
# Computes Geary's C for every observation in the +scoe+.
|
@@ -32,6 +33,25 @@ module SpatialStats
|
|
32
33
|
end
|
33
34
|
alias c stat
|
34
35
|
|
36
|
+
##
|
37
|
+
# Computes the groups each observation belongs to.
|
38
|
+
# Potential groups for Geary's C are:
|
39
|
+
# [HH] High-High
|
40
|
+
# [LL] Low-Low
|
41
|
+
# [N] Negative - Group traditionally for HL and LH, but since the difference is squared they are in the same group.
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# @return [Array] groups for each observation
|
45
|
+
def groups
|
46
|
+
quads.map do |quad|
|
47
|
+
if %w[HL LH].include?(quad)
|
48
|
+
'N'
|
49
|
+
else
|
50
|
+
quad
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
35
55
|
##
|
36
56
|
# Values of the +field+ queried from the +scope+
|
37
57
|
#
|
@@ -45,9 +65,11 @@ module SpatialStats
|
|
45
65
|
private
|
46
66
|
|
47
67
|
def stat_i(idx)
|
48
|
-
|
49
|
-
|
50
|
-
|
68
|
+
# TODO: maybe don't even use stat_i
|
69
|
+
# just form all of the modified zs and then
|
70
|
+
# pass it to a loop of mulvec all implemented in c ext
|
71
|
+
zi = z.map { |val| (z[idx] - val)**2 }
|
72
|
+
weights.sparse.dot_row(zi, idx)
|
51
73
|
end
|
52
74
|
|
53
75
|
def mc_i(wi, perms, idx)
|
@@ -55,8 +77,17 @@ module SpatialStats
|
|
55
77
|
(wi * zi).sum(1)
|
56
78
|
end
|
57
79
|
|
58
|
-
def
|
59
|
-
|
80
|
+
def mc_observation_calc(stat_i_orig, stat_i_new, _permutations)
|
81
|
+
# Geary cannot be negative, so we have to use this technique from
|
82
|
+
# GeoDa to determine p values. Note I slightly modified it to be inclusive
|
83
|
+
# on both tails not just the lower tail.
|
84
|
+
# https://github.com/GeoDaCenter/geoda/blob/master/Explore/LocalGearyCoordinator.cpp#L981 mean = stat_i_new.mean
|
85
|
+
mean = stat_i_new.mean
|
86
|
+
if stat_i_orig <= mean
|
87
|
+
(stat_i_new <= stat_i_orig).count
|
88
|
+
else
|
89
|
+
(stat_i_new >= stat_i_orig).count
|
90
|
+
end
|
60
91
|
end
|
61
92
|
end
|
62
93
|
end
|
@@ -14,13 +14,18 @@ module SpatialStats
|
|
14
14
|
# @param [ActiveRecord::Relation] scope
|
15
15
|
# @param [Symbol, String] field to query from scope
|
16
16
|
# @param [WeightsMatrix] weights to define relationship between observations in scope
|
17
|
+
# @param [Boolean] star to preset if star will be true or false. Will be calculated otherwise.
|
17
18
|
#
|
18
19
|
# @return [GetisOrd]
|
19
20
|
def initialize(scope, field, weights, star = nil)
|
20
|
-
|
21
|
+
@scope = scope
|
22
|
+
@field = field
|
23
|
+
@weights = weights
|
21
24
|
@star = star
|
25
|
+
calc_weights
|
22
26
|
end
|
23
27
|
attr_accessor :star
|
28
|
+
attr_writer :x
|
24
29
|
|
25
30
|
##
|
26
31
|
# Computes the G or G* statistic for every observation in x.
|
@@ -33,6 +38,25 @@ module SpatialStats
|
|
33
38
|
end
|
34
39
|
alias g stat
|
35
40
|
|
41
|
+
##
|
42
|
+
# Computes the groups each observation belongs to.
|
43
|
+
# Potential groups for G are:
|
44
|
+
# [H] High
|
45
|
+
# [L] Low
|
46
|
+
#
|
47
|
+
# Group is high when standardized z is positive, low otherwise.
|
48
|
+
#
|
49
|
+
# @return [Array] groups for each observation
|
50
|
+
def groups
|
51
|
+
z.standardize.map do |val|
|
52
|
+
if val.positive?
|
53
|
+
'H'
|
54
|
+
else
|
55
|
+
'L'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
36
60
|
##
|
37
61
|
# Values of the +field+ queried from the +scope+
|
38
62
|
#
|
@@ -50,7 +74,7 @@ module SpatialStats
|
|
50
74
|
# @return [Boolean] of star
|
51
75
|
def star?
|
52
76
|
if @star.nil?
|
53
|
-
@star = weights.
|
77
|
+
@star = weights.dense.trace.positive?
|
54
78
|
else
|
55
79
|
@star
|
56
80
|
end
|
@@ -67,25 +91,29 @@ module SpatialStats
|
|
67
91
|
x_lag_i / denominators[idx]
|
68
92
|
end
|
69
93
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
94
|
+
def mc_observation_calc(stat_i_orig, stat_i_new, permutations)
|
95
|
+
# GetisOrd cannot be negative, so we have to use this technique from
|
96
|
+
# ESDA to determine if we should select p or 1-p.
|
97
|
+
# https://github.com/pysal/esda/blob/master/esda/getisord.py#L388
|
98
|
+
num_larger = (stat_i_new >= stat_i_orig).count
|
99
|
+
is_low = (permutations - num_larger) < num_larger
|
100
|
+
if is_low
|
101
|
+
permutations - num_larger
|
102
|
+
else
|
103
|
+
num_larger
|
77
104
|
end
|
78
105
|
end
|
79
106
|
|
107
|
+
def calc_weights
|
108
|
+
@weights = if star?
|
109
|
+
weights.window.standardize
|
110
|
+
else
|
111
|
+
weights.standardize
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
80
115
|
def z_lag
|
81
|
-
|
82
|
-
@z_lag ||= begin
|
83
|
-
if star?
|
84
|
-
SpatialStats::Utils::Lag.window_sum(w, x)
|
85
|
-
else
|
86
|
-
SpatialStats::Utils::Lag.neighbor_sum(w, x)
|
87
|
-
end
|
88
|
-
end
|
116
|
+
@z_lag ||= SpatialStats::Utils::Lag.neighbor_sum(weights, x)
|
89
117
|
end
|
90
118
|
alias x_lag z_lag
|
91
119
|
|