1
1
from math import sqrt , pi , degrees
2
2
from typing import Optional , Tuple
3
+ from collections import namedtuple
3
4
import numpy
4
5
import scipy .stats
5
6
from pyssage .classes import Connections
6
7
from pyssage .utils import create_output_table , check_for_square_matrix
7
8
import pyssage .mantel
8
9
10
+ __all__ = ["bearing_correlogram" , "correlogram" , "windrose_correlogram" ]
11
+
9
12
10
13
def check_variance_assumption (x : Optional [str ]) -> None :
11
14
"""
@@ -21,21 +24,35 @@ def check_variance_assumption(x: Optional[str]) -> None:
21
24
22
25
23
26
def morans_i (y : numpy .ndarray , weights : Connections , alt_weights : Optional [numpy .ndarray ] = None ,
24
- variance : Optional [str ] = "random" ):
27
+ variance : Optional [str ] = "random" , permutations : int = 0 ):
25
28
check_variance_assumption (variance )
26
29
n = len (y )
27
- # mean_y = numpy.average(y)
28
30
mean_y = numpy .mean (y )
29
31
dev_y = y - mean_y # deviations from mean
30
32
w = weights .as_binary ()
31
33
if alt_weights is not None : # multiply to create non-binary weights, if necessary
32
34
w = w * alt_weights
33
- sumyij = numpy .sum (numpy .outer (dev_y , dev_y ) * w , dtype = numpy .float64 )
34
35
sumy2 = numpy .sum (numpy .square (dev_y ), dtype = numpy .float64 ) # sum of squared deviations from mean
35
36
sumw = numpy .sum (w , dtype = numpy .float64 ) # sum of weight matrix
36
37
sumw2 = sumw ** 2
37
- moran = n * sumyij / (sumw * sumy2 )
38
38
expected = - 1 / (n - 1 )
39
+ sumyij = numpy .sum (numpy .outer (dev_y , dev_y ) * w , dtype = numpy .float64 )
40
+ moran = n * sumyij / (sumw * sumy2 )
41
+
42
+ # permutations
43
+ permuted_i_list = [moran ]
44
+ perm_p = 1
45
+ if permutations > 0 :
46
+ rand_y = numpy .copy (dev_y )
47
+ for k in range (permutations - 1 ):
48
+ numpy .random .shuffle (rand_y )
49
+ perm_sumyij = numpy .sum (numpy .outer (rand_y , rand_y ) * w , dtype = numpy .float64 )
50
+ perm_moran = n * perm_sumyij / (sumw * sumy2 )
51
+ permuted_i_list .append (perm_moran )
52
+ if abs (perm_moran - expected ) >= abs (moran - expected ):
53
+ perm_p += 1
54
+ perm_p /= permutations
55
+
39
56
if variance is None :
40
57
sd , z , p = None , None , None
41
58
else :
@@ -51,24 +68,38 @@ def morans_i(y: numpy.ndarray, weights: Connections, alt_weights: Optional[numpy
51
68
z = abs (moran - expected ) / sd
52
69
p = scipy .stats .norm .sf (z )* 2 # two-tailed test
53
70
54
- return weights .min_scale , weights .max_scale , weights .n_pairs (), expected , moran , sd , z , p
71
+ return weights .min_scale , weights .max_scale , weights .n_pairs (), expected , moran , sd , z , p , perm_p , permuted_i_list
55
72
56
73
57
74
def gearys_c (y : numpy .ndarray , weights : Connections , alt_weights : Optional [numpy .ndarray ] = None ,
58
- variance : Optional [str ] = "random" ):
75
+ variance : Optional [str ] = "random" , permutations : int = 0 ):
59
76
check_variance_assumption (variance )
60
77
n = len (y )
61
- # mean_y = numpy.average(y)
62
78
mean_y = numpy .mean (y )
63
79
dev_y = y - mean_y # deviations from mean
64
80
w = weights .as_binary ()
65
81
if alt_weights is not None : # multiply to create non-binary weights, if necessary
66
82
w *= alt_weights
67
- sumdif2 = numpy .sum (numpy .square (w * (dev_y [:, numpy .newaxis ] - dev_y )), dtype = numpy .float64 )
68
83
sumy2 = numpy .sum (numpy .square (dev_y ), dtype = numpy .float64 ) # sum of squared deviations from mean
69
84
sumw = numpy .sum (w , dtype = numpy .float64 ) # sum of weight matrix
70
85
sumw2 = sumw ** 2
86
+ sumdif2 = numpy .sum (numpy .square (w * (dev_y [:, numpy .newaxis ] - dev_y )), dtype = numpy .float64 )
71
87
geary = (n - 1 ) * sumdif2 / (2 * sumw * sumy2 )
88
+
89
+ # permutations
90
+ permuted_c_list = [geary ]
91
+ perm_p = 1
92
+ if permutations > 0 :
93
+ rand_y = numpy .copy (dev_y )
94
+ for k in range (permutations - 1 ):
95
+ numpy .random .shuffle (rand_y )
96
+ perm_sumdif2 = numpy .sum (numpy .square (w * (rand_y [:, numpy .newaxis ] - rand_y )), dtype = numpy .float64 )
97
+ perm_geary = (n - 1 ) * perm_sumdif2 / (2 * sumw * sumy2 )
98
+ permuted_c_list .append (perm_geary )
99
+ if abs (perm_geary - 1 ) >= abs (geary - 1 ):
100
+ perm_p += 1
101
+ perm_p /= permutations
102
+
72
103
if variance is None :
73
104
sd , z , p = None , None , None
74
105
else :
@@ -86,11 +117,11 @@ def gearys_c(y: numpy.ndarray, weights: Connections, alt_weights: Optional[numpy
86
117
z = abs (geary - 1 ) / sd
87
118
p = scipy .stats .norm .sf (z )* 2 # two-tailed test
88
119
89
- return weights .min_scale , weights .max_scale , weights .n_pairs (), 1 , geary , sd , z , p
120
+ return weights .min_scale , weights .max_scale , weights .n_pairs (), 1 , geary , sd , z , p , perm_p , permuted_c_list
90
121
91
122
92
123
def mantel_correl (y : numpy .ndarray , weights : Connections , alt_weights : Optional [numpy .ndarray ] = None ,
93
- variance : Optional [ str ] = "random" ):
124
+ variance = None , permutations : int = 0 ):
94
125
"""
95
126
in order to get the bearing version to work right, we have to use normal binary weights, then reverse the sign
96
127
of the resulting Mantel correlation. if we use reverse binary weighting we end up multiplying the 'out of
@@ -99,12 +130,12 @@ def mantel_correl(y: numpy.ndarray, weights: Connections, alt_weights: Optional[
99
130
w = weights .as_binary ()
100
131
if alt_weights is not None : # multiply to create non-binary weights, if necessary
101
132
w *= alt_weights
102
- r , p_value , _ , _ , _ , _ , z = pyssage .mantel .mantel (y , w , [])
103
- return weights .min_scale , weights .max_scale , weights .n_pairs (), 0 , - r , - z , p_value
133
+ r , p_value , _ , _ , _ , permuted_two_p , permuted_rs , z = pyssage .mantel .mantel (y , w , [], permutations = permutations )
134
+ return weights .min_scale , weights .max_scale , weights .n_pairs (), 0 , - r , - z , p_value , permuted_two_p , permuted_rs
104
135
105
136
106
137
def correlogram (data : numpy .ndarray , dist_class_connections : list , metric : morans_i ,
107
- variance : Optional [str ] = "random" ) :
138
+ variance : Optional [str ] = "random" , permutations : int = 0 ) -> Tuple [ list , list , list ] :
108
139
if metric == morans_i :
109
140
metric_title = "Moran's I"
110
141
exp_format = "f"
@@ -118,8 +149,14 @@ def correlogram(data: numpy.ndarray, dist_class_connections: list, metric: moran
118
149
metric_title = ""
119
150
exp_format = ""
120
151
output = []
152
+ all_permuted_values = []
121
153
for dc in dist_class_connections :
122
- output .append (metric (data , dc , variance = variance ))
154
+ * tmp_out , permuted_values = metric (data , dc , variance = variance , permutations = permutations )
155
+ if permutations > 0 :
156
+ output .append (tmp_out )
157
+ all_permuted_values .append (permuted_values )
158
+ else :
159
+ output .append (tmp_out [:len (tmp_out )- 1 ])
123
160
124
161
# create basic output text
125
162
output_text = list ()
@@ -128,20 +165,27 @@ def correlogram(data: numpy.ndarray, dist_class_connections: list, metric: moran
128
165
output_text .append ("# of data points = {}" .format (len (data )))
129
166
if variance is not None :
130
167
output_text .append ("Distribution assumption = {}" .format (variance ))
168
+ if permutations > 0 :
169
+ output_text .append ("Permutation probability calculated from {} permutations" .format (permutations ))
131
170
output_text .append ("" )
132
- if metric == mantel_correl :
133
- col_headers = ("Min dist" , "Max dist" , "# pairs" , "Expected" , metric_title , "Z" , "Prob" )
134
- col_formats = ("f" , "f" , "d" , exp_format , "f" , "f" , "f" )
135
- else :
136
- col_headers = ("Min dist" , "Max dist" , "# pairs" , "Expected" , metric_title , "SD" , "Z" , "Prob" )
137
- col_formats = ("f" , "f" , "d" , exp_format , "f" , "f" , "f" , "f" )
171
+ col_headers = ["Min dist" , "Max dist" , "# pairs" , "Expected" , metric_title , "Z" , "Prob" ]
172
+ col_formats = ["f" , "f" , "d" , exp_format , "f" , "f" , "f" ]
173
+ if metric != mantel_correl :
174
+ col_headers .insert (5 , "SD" )
175
+ col_formats .insert (5 , "f" )
176
+ if permutations > 0 :
177
+ col_headers .append ("PermProb" )
178
+ col_formats .append ("f" )
179
+
138
180
create_output_table (output_text , output , col_headers , col_formats )
139
181
140
- return output , output_text
182
+ correlogram_output = namedtuple ("correlogram_output" , ["output_values" , "output_text" , "permuted_values" ])
183
+ return correlogram_output (output , output_text , all_permuted_values )
141
184
142
185
143
186
def bearing_correlogram (data : numpy .ndarray , dist_class_connections : list , angles : numpy .ndarray , n_bearings : int = 18 ,
144
- metric = morans_i , variance : Optional [str ] = "random" ):
187
+ metric = morans_i , variance : Optional [str ] = "random" ,
188
+ permutations : int = 0 ) -> Tuple [list , list , list ]:
145
189
if metric == morans_i :
146
190
metric_title = "Moran's I"
147
191
exp_format = "f"
@@ -164,11 +208,17 @@ def bearing_correlogram(data: numpy.ndarray, dist_class_connections: list, angle
164
208
bearing_weights .append (numpy .square (numpy .cos (angles - a )))
165
209
166
210
output = []
211
+ all_permuted_values = []
167
212
for i , b in enumerate (bearing_weights ):
168
213
for dc in dist_class_connections :
169
- tmp_out = list (metric (data , dc , alt_weights = b , variance = variance ))
214
+ * tmp_out , permuted_values = metric (data , dc , alt_weights = b , variance = variance , permutations = permutations )
215
+ tmp_out = list (tmp_out )
170
216
tmp_out .insert (2 , degrees (bearings [i ]))
171
- output .append (tmp_out )
217
+ if permutations > 0 :
218
+ output .append (tmp_out )
219
+ all_permuted_values .append (permuted_values )
220
+ else :
221
+ output .append (tmp_out [:len (tmp_out )- 1 ])
172
222
173
223
# create basic output text
174
224
output_text = list ()
@@ -177,16 +227,21 @@ def bearing_correlogram(data: numpy.ndarray, dist_class_connections: list, angle
177
227
output_text .append ("# of data points = {}" .format (len (data )))
178
228
if variance is not None :
179
229
output_text .append ("Distribution assumption = {}" .format (variance ))
230
+ if permutations > 0 :
231
+ output_text .append ("Permutation probability calculated from {} permutations" .format (permutations ))
180
232
output_text .append ("" )
181
- if metric == mantel_correl :
182
- col_headers = ("Min dist" , "Max dist" , "Bearing" , "# pairs" , "Expected" , metric_title , "Z" , "Prob" )
183
- col_formats = ("f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" )
184
- else :
185
- col_headers = ("Min dist" , "Max dist" , "Bearing" , "# pairs" , "Expected" , metric_title , "SD" , "Z" , "Prob" )
186
- col_formats = ("f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" , "f" )
233
+ col_headers = ["Min dist" , "Max dist" , "Bearing" , "# pairs" , "Expected" , metric_title , "Z" , "Prob" ]
234
+ col_formats = ["f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" ]
235
+ if metric != mantel_correl :
236
+ col_headers .insert (6 , "SD" )
237
+ col_formats .insert (6 , "f" )
238
+ if permutations > 0 :
239
+ col_headers .append ("PermProb" )
240
+ col_formats .append ("f" )
187
241
create_output_table (output_text , output , col_headers , col_formats )
188
242
189
- return output , output_text
243
+ correlogram_output = namedtuple ("correlogram_output" , ["output_values" , "output_text" , "permuted_values" ])
244
+ return correlogram_output (output , output_text , all_permuted_values )
190
245
191
246
192
247
def windrose_sectors_per_annulus (segment_param : int , annulus : int ) -> int :
@@ -211,7 +266,8 @@ def create_windrose_connections(distances: numpy.ndarray, angles: numpy.ndarray,
211
266
212
267
def windrose_correlogram (data : numpy .ndarray , distances : numpy .ndarray , angles : numpy .ndarray ,
213
268
radius_c : float , radius_d : float , radius_e : float , segment_param : int = 4 ,
214
- min_pairs : int = 21 , metric = morans_i , variance : Optional [str ] = "random" ):
269
+ min_pairs : int = 21 , metric = morans_i , variance : Optional [str ] = "random" ,
270
+ permutations : int = 0 ) -> Tuple [list , list , list , list ]:
215
271
if metric == morans_i :
216
272
metric_title = "Moran's I"
217
273
exp_format = "f"
@@ -237,18 +293,25 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
237
293
all_output = []
238
294
# all_output is needed for graphing the output *if* we want to include those sectors with too few pairs, but
239
295
# still more than zero
296
+ all_permuted_values = []
240
297
for annulus in range (n_annuli ):
241
298
for sector in range (windrose_sectors_per_annulus (segment_param , annulus )):
242
299
connection , min_ang , max_ang = create_windrose_connections (distances , angles , annulus , sector ,
243
300
segment_param , radius_c , radius_d , radius_e )
244
301
np = connection .n_pairs ()
245
302
if np >= min_pairs :
246
- tmp_out = list (metric (data , connection , variance = variance ))
303
+ * tmp_out , permuted_values = metric (data , connection , variance = variance , permutations = permutations )
304
+ tmp_out = list (tmp_out )
247
305
# add sector angles to output
248
306
tmp_out .insert (2 , degrees (min_ang ))
249
307
tmp_out .insert (3 , degrees (max_ang ))
250
- output .append (tmp_out )
251
- all_output .append (tmp_out )
308
+ if permutations > 0 :
309
+ output .append (tmp_out )
310
+ all_output .append (tmp_out )
311
+ all_permuted_values .append (permuted_values )
312
+ else :
313
+ output .append (tmp_out [:len (tmp_out ) - 1 ])
314
+ all_output .append (tmp_out [:len (tmp_out ) - 1 ])
252
315
else :
253
316
# using -1 for the probability as an indicator that nothing was calculated
254
317
if metric == mantel_correl :
@@ -257,6 +320,8 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
257
320
else :
258
321
tmp_out = [connection .min_scale , connection .max_scale , degrees (min_ang ), degrees (max_ang ),
259
322
np , 0 , 0 , 0 , 0 , - 1 ]
323
+ if permutations > 0 :
324
+ tmp_out .append (- 1 )
260
325
all_output .append (tmp_out )
261
326
262
327
# create basic output text
@@ -270,16 +335,21 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
270
335
output_text .append ("" )
271
336
if variance is not None :
272
337
output_text .append ("Distribution assumption = {}" .format (variance ))
338
+ if permutations > 0 :
339
+ output_text .append ("Permutation probability calculated from {} permutations" .format (permutations ))
273
340
274
341
output_text .append ("" )
275
- if metric == mantel_correl :
276
- col_headers = ("Min dist" , "Max dist" , "Min angle" , "Max angle" , "# pairs" , "Expected" , metric_title ,
277
- "Z" , "Prob" )
278
- col_formats = ("f" , "f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" )
279
- else :
280
- col_headers = ("Min dist" , "Max dist" , "Min angle" , "Max angle" , "# pairs" , "Expected" , metric_title , "SD" ,
281
- "Z" , "Prob" )
282
- col_formats = ("f" , "f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" , "f" )
342
+ col_headers = ["Min dist" , "Max dist" , "Min angle" , "Max angle" , "# pairs" , "Expected" , metric_title ,
343
+ "Z" , "Prob" ]
344
+ col_formats = ["f" , "f" , "f" , "f" , "d" , exp_format , "f" , "f" , "f" ]
345
+ if metric != mantel_correl :
346
+ col_headers .insert (7 , "SD" )
347
+ col_formats .insert (7 , "f" )
348
+ if permutations > 0 :
349
+ col_headers .append ("PermProb" )
350
+ col_formats .append ("f" )
283
351
create_output_table (output_text , output , col_headers , col_formats )
284
352
285
- return output , output_text , all_output
353
+ windrose_correlogram_output = namedtuple ("windrose_correlogram_output" , ["output_values" , "output_text" ,
354
+ "all_output" , "permuted_values" ])
355
+ return windrose_correlogram_output (output , output_text , all_output , all_permuted_values )
0 commit comments