From e095b217b2ebfa76d43e5d05178dd3662dac9653 Mon Sep 17 00:00:00 2001 From: Martin Fleischmann Date: Thu, 2 Jan 2025 00:59:03 +0100 Subject: [PATCH] ENH: add Moran_Local.plot (#355) * static plot * simplify * add tests * fix docstrings --- esda/moran.py | 54 +++++++++++++++++++++++++++++----------- esda/tests/test_moran.py | 25 +++++++++++++++++++ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/esda/moran.py b/esda/moran.py index 750710b5..0dddc41e 100644 --- a/esda/moran.py +++ b/esda/moran.py @@ -83,7 +83,7 @@ class Moran: w : W | Graph original w object z : array - zero-mean, unit standard deviation normalized y + zero-mean, unit standard deviation normalized y permutations : int number of permutations I : float @@ -673,7 +673,7 @@ class Moran_Rate(Moran): # noqa: N801 if adjusted is True, y is standardized rates otherwise, y is raw rates z : array - zero-mean, unit standard deviation normalized y + zero-mean, unit standard deviation normalized y w : W | Graph original w object permutations : int @@ -928,7 +928,7 @@ class Moran_Local: # noqa: N801 w : W | Graph original w object z : array - zero-mean, unit standard deviation normalized y + zero-mean, unit standard deviation normalized y permutations : int number of random permutations for calculation of pseudo p_values Is : array @@ -1249,7 +1249,28 @@ def explore(self, gdf, crit_value=0.05, **kwargs): """ gdf = gdf.copy() gdf["Moran Cluster"] = self.get_cluster_labels(crit_value) - return _explore_local_moran(self, gdf, crit_value, **kwargs) + return _viz_local_moran(self, gdf, crit_value, "explore", **kwargs) + + def plot(self, gdf, crit_value=0.05, **kwargs): + """Create static map of LISA indicators + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + geodataframe used to conduct the local Moran analysis + crit_value : float, optional + critical value to determine statistical significance, by default 0.05 + kwargs : dict, optional + additional keyword arguments passed to the geopandas `explore` method + + Returns + ------- + ax + matplotlib axis + """ + gdf = gdf.copy() + gdf["Moran Cluster"] = self.get_cluster_labels(crit_value) + return _viz_local_moran(self, gdf, crit_value, "plot", **kwargs) class Moran_Local_BV: # noqa: N801 @@ -1571,7 +1592,7 @@ class Moran_Local_Rate(Moran_Local): # noqa: N801 if adjusted is True, y is standardized rates otherwise, y is raw rates z : array - zero-mean, unit standard deviation normalized y + zero-mean, unit standard deviation normalized y w : W | Graph original w object permutations : int @@ -1773,8 +1794,8 @@ def by_col( df[col] = rate_df[col] -def _explore_local_moran(moran_local, gdf, crit_value, **kwargs): - """Plot local Moran values as an interactive map +def _viz_local_moran(moran_local, gdf, crit_value, method, **kwargs): + """Common helper for local Moran's I vizualization Parameters ---------- @@ -1784,20 +1805,24 @@ def _explore_local_moran(moran_local, gdf, crit_value, **kwargs): geodataframe used to create the Moran_Local class crit_value : float, optional critical value for determining statistical significance, by default 0.05 + method : str {"explore", "plot"} + GeoDataFrame method to be used kwargs : dict, optional additional keyword arguments are passed directly - to geopandas.explore, by default None + to the plotting method, by default None Returns ------- - m - folium.Map + m | ax + either folium.Map or maptlotlib.Axes """ try: from matplotlib import colors - except ImportError: - raise ImportError("matplotlib library must be installed to use the explore feature") from None + except ImportError as err: + raise ImportError( + "matplotlib library must be installed to use the vizualization feature" + ) from err gdf = gdf.copy() gdf["Moran Cluster"] = moran_local.get_cluster_labels(crit_value) @@ -1817,8 +1842,9 @@ def _explore_local_moran(moran_local, gdf, crit_value, **kwargs): if "cmap" not in kwargs: kwargs["cmap"] = hmap - m = gdf[["Moran Cluster", "p-value", "geometry"]].explore("Moran Cluster", **kwargs) - return m + return getattr(gdf[["Moran Cluster", "p-value", "geometry"]], method)( + "Moran Cluster", **kwargs + ) def _get_cluster_labels(moran_local, crit_value): diff --git a/esda/tests/test_moran.py b/esda/tests/test_moran.py index c35d4009..949809ac 100644 --- a/esda/tests/test_moran.py +++ b/esda/tests/test_moran.py @@ -236,6 +236,31 @@ def test_Moran_Local_explore(self, w): assert out_str.count("#fdae61") == 6 assert out_str.count("#d3d3d3") == 280 + @parametrize_sac + def test_Moran_Local_plot(self, w): + import matplotlib + + matplotlib.use("Agg") + + lm = moran.Moran_Local( + sac1.HSG_VAL.values, + w, + transformation="r", + permutations=99, + keep_simulations=True, + seed=SEED, + ) + ax = lm.plot(sac1) + unique, counts = np.unique(ax.collections[0].get_facecolors(), axis=0, return_counts=True) + np.testing.assert_array_almost_equal(unique, np.array([ + [0.17254902, 0.48235294, 0.71372549, 1.], + [0.5372549 , 0.81176471, 0.94117647, 1.], + [0.82745098, 0.82745098, 0.82745098, 1.], + [0.84313725, 0.09803922, 0.10980392, 1.], + [0.99215686, 0.68235294, 0.38039216, 1.]] + )) + np.testing.assert_array_equal(counts, np.array([86,3, 298,38, 3])) + @parametrize_desmith def test_Moran_Local_parallel(self, w): lm = moran.Moran_Local(