Skip to content

Commit 9bb87ca

Browse files
287 add local morans i (#305)
* Added Local Moran's I --------- Co-authored-by: lehtonenp <[email protected]>
1 parent 1ffab10 commit 9bb87ca

File tree

4 files changed

+357
-0
lines changed

4 files changed

+357
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Local Moran's I
2+
3+
::: eis_toolkit.exploratory_analyses.local_morans_i
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import geopandas as gpd
2+
import libpysal
3+
import numpy as np
4+
from beartype import beartype
5+
from beartype.typing import Literal
6+
from esda.moran import Moran_Local
7+
8+
from eis_toolkit import exceptions
9+
from eis_toolkit.exceptions import InvalidParameterValueException
10+
11+
12+
@beartype
13+
def _local_morans_i(
14+
gdf: gpd.GeoDataFrame, column: str, weight_type: Literal["queen", "knn"], k: int, permutations: int
15+
) -> gpd.GeoDataFrame:
16+
17+
if weight_type == "queen":
18+
w = libpysal.weights.Queen.from_dataframe(gdf)
19+
elif weight_type == "knn":
20+
w = libpysal.weights.KNN.from_dataframe(gdf, k=k)
21+
else:
22+
raise InvalidParameterValueException("Invalid weight_type. Use 'queen' or 'knn'.")
23+
24+
w.transform = "R"
25+
26+
if len(gdf[column]) != len(w.weights):
27+
raise InvalidParameterValueException("Dimension mismatch between data and weights matrix.")
28+
29+
moran_loc = Moran_Local(gdf[column], w, permutations=permutations)
30+
31+
gdf[f"{column}_local_moran_I"] = moran_loc.Is
32+
gdf[f"{column}_local_moran_I_p_value"] = moran_loc.p_sim
33+
34+
gdf[f"{column}_local_moran_I_p_value"].fillna(value=np.nan, inplace=True)
35+
36+
return gdf
37+
38+
39+
@beartype
40+
def local_morans_i(
41+
gdf: gpd.GeoDataFrame,
42+
column: str,
43+
weight_type: Literal["queen", "knn"] = "queen",
44+
k: int = 4,
45+
permutations: int = 999,
46+
) -> gpd.GeoDataFrame:
47+
"""Execute Local Moran's I calculation for the data.
48+
49+
Args:
50+
gdf: The geodataframe that contains the data to be examined with local morans I.
51+
column: The column to be used in the analysis.
52+
weight_type: The type of spatial weights matrix to be used. Defaults to "queen".
53+
k: Number of nearest neighbors for the KNN weights matrix. Defaults to 4.
54+
permutations: Number of permutations for significance testing. Defaults to 999.
55+
56+
Returns:
57+
Geodataframe appended with two new columns: one with Local Moran's I
58+
statistic and one with p-value for the statistic.
59+
60+
Raises:
61+
EmptyDataFrameException: The input geodataframe is empty.
62+
"""
63+
if gdf.shape[0] == 0:
64+
raise exceptions.EmptyDataFrameException("Geodataframe is empty.")
65+
66+
if column not in gdf.columns:
67+
raise exceptions.InvalidParameterValueException(f"Column '{column}' not found in the GeoDataFrame.")
68+
69+
if k < 1:
70+
raise exceptions.InvalidParameterValueException("k must be > 0.")
71+
72+
if permutations < 100:
73+
raise exceptions.InvalidParameterValueException("permutations must be > 99.")
74+
75+
calculations = _local_morans_i(gdf, column, weight_type, k, permutations)
76+
77+
return calculations

notebooks/testing_local_morans_i.ipynb

+109
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import geopandas as gpd
2+
import numpy as np
3+
import pytest
4+
5+
from eis_toolkit import exceptions
6+
from eis_toolkit.exploratory_analyses.local_morans_i import local_morans_i
7+
8+
9+
def test_local_morans_i_queen_correctness():
10+
"""Test Local Moran's I Queen correctness."""
11+
12+
permutations = 999
13+
14+
column = "gdp_md_est"
15+
data = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
16+
gdf = gpd.GeoDataFrame(data)
17+
gdf20 = gdf.head(20)
18+
19+
Is = [
20+
-0.0,
21+
0.07475982388811646,
22+
-0.0,
23+
0.32707308155951376,
24+
0.3270730815595137,
25+
0.02567026668185298,
26+
0.06875988330128255,
27+
0.014784750951796788,
28+
0.014784750951796785,
29+
0.04431219450688388,
30+
0.04431219450688389,
31+
0.07606167537417435,
32+
0.07674902657188765,
33+
0.07510349948697309,
34+
0.08027860346365057,
35+
0.08027860346365055,
36+
0.0766008275457467,
37+
0.0766008275457467,
38+
-0.017419349937576573,
39+
-0.0,
40+
]
41+
42+
p_sims = [
43+
0.001,
44+
0.365,
45+
0.001,
46+
0.056,
47+
0.056,
48+
0.191,
49+
0.355,
50+
0.206,
51+
0.38,
52+
0.201,
53+
0.256,
54+
0.464,
55+
0.351,
56+
0.24,
57+
0.224,
58+
0.386,
59+
0.397,
60+
0.338,
61+
0.299,
62+
0.001,
63+
]
64+
65+
result = local_morans_i(gdf=gdf20, column=column, weight_type="queen", permutations=permutations)
66+
67+
np.testing.assert_allclose(result[f"{column}_local_moran_I"], Is, rtol=0.1, atol=0.1)
68+
np.testing.assert_allclose(result[f"{column}_local_moran_I_p_value"], p_sims, rtol=0.1, atol=0.1)
69+
70+
71+
def test_local_morans_i_knn_correctness():
72+
"""Test Local Moran's I KNN correctness."""
73+
74+
k = 4
75+
permutations = 999
76+
77+
column = "gdp_md_est"
78+
data = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
79+
gdf = gpd.GeoDataFrame(data)
80+
gdf20 = gdf.head(20)
81+
82+
Is = [
83+
0.03686709680905326,
84+
0.07635702591252187,
85+
0.07990131850915279,
86+
0.06552029316560676,
87+
-0.8031053484182811,
88+
0.048272793989863144,
89+
0.051515005797283464,
90+
0.03846954477931574,
91+
0.010137393086155687,
92+
0.051762074257733624,
93+
0.05895594777225281,
94+
0.0768224164382028,
95+
0.07889650044641662,
96+
0.07492029251731681,
97+
0.07855252515119235,
98+
0.07851482805880286,
99+
-0.26650904930879504,
100+
-0.25076447340691294,
101+
-0.015081612933344679,
102+
-0.2666687928014803,
103+
]
104+
105+
p_sims = [
106+
0.223,
107+
0.094,
108+
0.082,
109+
0.148,
110+
0.257,
111+
0.465,
112+
0.396,
113+
0.27,
114+
0.469,
115+
0.319,
116+
0.319,
117+
0.115,
118+
0.126,
119+
0.095,
120+
0.087,
121+
0.133,
122+
0.035,
123+
0.054,
124+
0.416,
125+
0.033,
126+
]
127+
128+
result = local_morans_i(gdf20, column, "knn", k=k, permutations=permutations)
129+
130+
np.testing.assert_allclose(result[f"{column}_local_moran_I"], Is, rtol=0.1, atol=0.1)
131+
np.testing.assert_allclose(result[f"{column}_local_moran_I_p_value"], p_sims, rtol=0.1, atol=0.1)
132+
133+
134+
def test_empty_geodataframe():
135+
"""Test Local Moran's I raises EmptyDataFrameException."""
136+
137+
empty_gdf = gpd.GeoDataFrame()
138+
139+
# Use pytest.raises to check the expected exception
140+
with pytest.raises(exceptions.EmptyDataFrameException):
141+
local_morans_i(empty_gdf, column="value", weight_type="queen", k=2, permutations=999)
142+
143+
144+
def test_geodataframe_missing_column():
145+
"""Test Local Moran's I raises InvalidParameterValueException for missing column."""
146+
147+
gdf = gpd.GeoDataFrame({"test_col": [1, 2, 3]})
148+
149+
with pytest.raises(exceptions.InvalidParameterValueException):
150+
local_morans_i(gdf, column="value", weight_type="queen", k=4, permutations=999)
151+
152+
153+
def test_invalid_k_value():
154+
"""Test Local Moran's I raises InvalidParameterValueException for k value under 1."""
155+
156+
gdf = gpd.GeoDataFrame({"value": [1, 2, 3]})
157+
158+
with pytest.raises(exceptions.InvalidParameterValueException):
159+
local_morans_i(gdf, column="value", weight_type="queen", k=0, permutations=999)
160+
161+
162+
def test_invalid_permutations_value():
163+
"""Test Local Moran's I raises InvalidParameterValueException for permutations value under 100."""
164+
165+
gdf = gpd.GeoDataFrame({"value": [1, 2, 3]})
166+
167+
with pytest.raises(exceptions.InvalidParameterValueException):
168+
local_morans_i(gdf, column="value", weight_type="queen", k=4, permutations=99)

0 commit comments

Comments
 (0)