Skip to content

Commit fdeb6d0

Browse files
authored
Merge pull request #37 from msrosenberg/MSR-develop
permutation tests for correlograms
2 parents a80d010 + 1d2af09 commit fdeb6d0

File tree

8 files changed

+322
-119
lines changed

8 files changed

+322
-119
lines changed

pyssage/anisotropy.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def bearing_analysis(data: numpy.ndarray, distances: numpy.ndarray, angles: nump
3434
for a in range(nbearings):
3535
test_angle = a * angle_width
3636
b_matrix = distances * numpy.square(numpy.cos(angles - test_angle))
37-
r, p_value, _, _, _, rand_p, _ = pyssage.mantel.mantel(data, b_matrix, [], npermutations)
37+
r, p_value, _, _, _, rand_p,_, _ = pyssage.mantel.mantel(data, b_matrix, [], npermutations)
3838
if npermutations > 0:
3939
output.append([a*180/nbearings, r, p_value, rand_p])
4040
else:
@@ -47,11 +47,11 @@ def bearing_analysis(data: numpy.ndarray, distances: numpy.ndarray, angles: nump
4747
output_text.append("Tested {} vectors".format(nbearings))
4848
output_text.append("")
4949
if npermutations > 0:
50-
col_headers = ("Bearing", "Correlation", "Prob", "RandProb")
51-
col_formats = ("f", "f", "f", "f")
50+
col_headers = ["Bearing", "Correlation", "Prob", "RandProb"]
51+
col_formats = ["f", "f", "f", "f"]
5252
else:
53-
col_headers = ("Bearing", "Correlation", "Prob")
54-
col_formats = ("f", "f", "f")
53+
col_headers = ["Bearing", "Correlation", "Prob"]
54+
col_formats = ["f", "f", "f"]
5555
create_output_table(output_text, output, col_headers, col_formats)
5656
bearing_output = namedtuple("bearing_output", ["output_values", "output_text"])
5757
return bearing_output(output, output_text)

pyssage/connections.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
from pyssage.classes import Point, Triangle, VoronoiEdge, VoronoiTessellation, VoronoiPolygon, Connections
66
from pyssage.utils import euclidean_angle, check_for_square_matrix
77

8-
__all__ = ["delaunay_tessellation", "relative_neighborhood_network", "gabriel_network",
9-
"minimum_spanning_tree", "connect_distance_range", "least_diagonal_network", "nearest_neighbor_connections"]
8+
__all__ = ["connect_distance_range", "delaunay_tessellation", "distance_classes_to_connections",
9+
"gabriel_network", "least_diagonal_network", "minimum_spanning_tree", "nearest_neighbor_connections",
10+
"relative_neighborhood_network"]
1011

1112

1213
def create_point_list(x: numpy.ndarray, y: numpy.ndarray) -> list:

pyssage/correlogram.py

Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from math import sqrt, pi, degrees
22
from typing import Optional, Tuple
3+
from collections import namedtuple
34
import numpy
45
import scipy.stats
56
from pyssage.classes import Connections
67
from pyssage.utils import create_output_table, check_for_square_matrix
78
import pyssage.mantel
89

10+
__all__ = ["bearing_correlogram", "correlogram", "windrose_correlogram"]
11+
912

1013
def check_variance_assumption(x: Optional[str]) -> None:
1114
"""
@@ -21,21 +24,35 @@ def check_variance_assumption(x: Optional[str]) -> None:
2124

2225

2326
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):
2528
check_variance_assumption(variance)
2629
n = len(y)
27-
# mean_y = numpy.average(y)
2830
mean_y = numpy.mean(y)
2931
dev_y = y - mean_y # deviations from mean
3032
w = weights.as_binary()
3133
if alt_weights is not None: # multiply to create non-binary weights, if necessary
3234
w = w * alt_weights
33-
sumyij = numpy.sum(numpy.outer(dev_y, dev_y) * w, dtype=numpy.float64)
3435
sumy2 = numpy.sum(numpy.square(dev_y), dtype=numpy.float64) # sum of squared deviations from mean
3536
sumw = numpy.sum(w, dtype=numpy.float64) # sum of weight matrix
3637
sumw2 = sumw**2
37-
moran = n * sumyij / (sumw * sumy2)
3838
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+
3956
if variance is None:
4057
sd, z, p = None, None, None
4158
else:
@@ -51,24 +68,38 @@ def morans_i(y: numpy.ndarray, weights: Connections, alt_weights: Optional[numpy
5168
z = abs(moran - expected) / sd
5269
p = scipy.stats.norm.sf(z)*2 # two-tailed test
5370

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
5572

5673

5774
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):
5976
check_variance_assumption(variance)
6077
n = len(y)
61-
# mean_y = numpy.average(y)
6278
mean_y = numpy.mean(y)
6379
dev_y = y - mean_y # deviations from mean
6480
w = weights.as_binary()
6581
if alt_weights is not None: # multiply to create non-binary weights, if necessary
6682
w *= alt_weights
67-
sumdif2 = numpy.sum(numpy.square(w * (dev_y[:, numpy.newaxis] - dev_y)), dtype=numpy.float64)
6883
sumy2 = numpy.sum(numpy.square(dev_y), dtype=numpy.float64) # sum of squared deviations from mean
6984
sumw = numpy.sum(w, dtype=numpy.float64) # sum of weight matrix
7085
sumw2 = sumw**2
86+
sumdif2 = numpy.sum(numpy.square(w * (dev_y[:, numpy.newaxis] - dev_y)), dtype=numpy.float64)
7187
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+
72103
if variance is None:
73104
sd, z, p = None, None, None
74105
else:
@@ -86,11 +117,11 @@ def gearys_c(y: numpy.ndarray, weights: Connections, alt_weights: Optional[numpy
86117
z = abs(geary - 1) / sd
87118
p = scipy.stats.norm.sf(z)*2 # two-tailed test
88119

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
90121

91122

92123
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):
94125
"""
95126
in order to get the bearing version to work right, we have to use normal binary weights, then reverse the sign
96127
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[
99130
w = weights.as_binary()
100131
if alt_weights is not None: # multiply to create non-binary weights, if necessary
101132
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
104135

105136

106137
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]:
108139
if metric == morans_i:
109140
metric_title = "Moran's I"
110141
exp_format = "f"
@@ -118,8 +149,14 @@ def correlogram(data: numpy.ndarray, dist_class_connections: list, metric: moran
118149
metric_title = ""
119150
exp_format = ""
120151
output = []
152+
all_permuted_values = []
121153
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])
123160

124161
# create basic output text
125162
output_text = list()
@@ -128,20 +165,27 @@ def correlogram(data: numpy.ndarray, dist_class_connections: list, metric: moran
128165
output_text.append("# of data points = {}".format(len(data)))
129166
if variance is not None:
130167
output_text.append("Distribution assumption = {}".format(variance))
168+
if permutations > 0:
169+
output_text.append("Permutation probability calculated from {} permutations".format(permutations))
131170
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+
138180
create_output_table(output_text, output, col_headers, col_formats)
139181

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)
141184

142185

143186
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]:
145189
if metric == morans_i:
146190
metric_title = "Moran's I"
147191
exp_format = "f"
@@ -164,11 +208,17 @@ def bearing_correlogram(data: numpy.ndarray, dist_class_connections: list, angle
164208
bearing_weights.append(numpy.square(numpy.cos(angles - a)))
165209

166210
output = []
211+
all_permuted_values = []
167212
for i, b in enumerate(bearing_weights):
168213
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)
170216
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])
172222

173223
# create basic output text
174224
output_text = list()
@@ -177,16 +227,21 @@ def bearing_correlogram(data: numpy.ndarray, dist_class_connections: list, angle
177227
output_text.append("# of data points = {}".format(len(data)))
178228
if variance is not None:
179229
output_text.append("Distribution assumption = {}".format(variance))
230+
if permutations > 0:
231+
output_text.append("Permutation probability calculated from {} permutations".format(permutations))
180232
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")
187241
create_output_table(output_text, output, col_headers, col_formats)
188242

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)
190245

191246

192247
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,
211266

212267
def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles: numpy.ndarray,
213268
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]:
215271
if metric == morans_i:
216272
metric_title = "Moran's I"
217273
exp_format = "f"
@@ -237,18 +293,25 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
237293
all_output = []
238294
# all_output is needed for graphing the output *if* we want to include those sectors with too few pairs, but
239295
# still more than zero
296+
all_permuted_values = []
240297
for annulus in range(n_annuli):
241298
for sector in range(windrose_sectors_per_annulus(segment_param, annulus)):
242299
connection, min_ang, max_ang = create_windrose_connections(distances, angles, annulus, sector,
243300
segment_param, radius_c, radius_d, radius_e)
244301
np = connection.n_pairs()
245302
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)
247305
# add sector angles to output
248306
tmp_out.insert(2, degrees(min_ang))
249307
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])
252315
else:
253316
# using -1 for the probability as an indicator that nothing was calculated
254317
if metric == mantel_correl:
@@ -257,6 +320,8 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
257320
else:
258321
tmp_out = [connection.min_scale, connection.max_scale, degrees(min_ang), degrees(max_ang),
259322
np, 0, 0, 0, 0, -1]
323+
if permutations > 0:
324+
tmp_out.append(-1)
260325
all_output.append(tmp_out)
261326

262327
# create basic output text
@@ -270,16 +335,21 @@ def windrose_correlogram(data: numpy.ndarray, distances: numpy.ndarray, angles:
270335
output_text.append("")
271336
if variance is not None:
272337
output_text.append("Distribution assumption = {}".format(variance))
338+
if permutations > 0:
339+
output_text.append("Permutation probability calculated from {} permutations".format(permutations))
273340

274341
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")
283351
create_output_table(output_text, output, col_headers, col_formats)
284352

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

Comments
 (0)