Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add Moran scatterplot ported from splot #356

Merged
merged 7 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 177 additions & 1 deletion esda/moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"Levi John Wolf <[email protected]>"
)

from warnings import simplefilter
from warnings import simplefilter, warn

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -321,6 +321,37 @@ def by_col(
**stat_kws,
)

def plot_scatterplot(
self,
ax=None,
scatter_kwds=None,
fitline_kwds=None,
):
"""
Plot a Moran scatterplot with optional coloring for significant points.

Parameters
----------
ax : matplotlib.axes.Axes, optional
Pre-existing axes for the plot, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
scatter_kwds : dict, optional
Additional keyword arguments for scatter plot, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
fitline_kwds : dict, optional
Additional keyword arguments for fit line, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
matplotlib.axes.Axes
Axes object with the Moran scatterplot
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
"""
return _scatterplot(
self,
crit_value=None,
ax=ax,
scatter_kwds=scatter_kwds,
fitline_kwds=fitline_kwds,
)


class Moran_BV: # noqa: N801
"""
Expand Down Expand Up @@ -1272,6 +1303,40 @@ def plot(self, gdf, crit_value=0.05, **kwargs):
gdf["Moran Cluster"] = self.get_cluster_labels(crit_value)
return _viz_local_moran(self, gdf, crit_value, "plot", **kwargs)

def plot_scatterplot(
self,
crit_value=0.05,
ax=None,
scatter_kwds=None,
fitline_kwds=None,
):
"""
Plot a Moran scatterplot with optional coloring for significant points.

Parameters
----------
crit_value : float, optional
Critical value to determine statistical significance, by default 0.05
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
ax : matplotlib.axes.Axes, optional
Pre-existing axes for the plot, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
scatter_kwds : dict, optional
Additional keyword arguments for scatter plot, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
fitline_kwds : dict, optional
Additional keyword arguments for fit line, by default None
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
matplotlib.axes.Axes
Axes object with the Moran scatterplot
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
"""
return _scatterplot(
self,
crit_value=crit_value,
ax=ax,
scatter_kwds=scatter_kwds,
fitline_kwds=fitline_kwds,
)


class Moran_Local_BV: # noqa: N801
"""Bivariate Local Moran Statistics.
Expand Down Expand Up @@ -1847,6 +1912,47 @@ def _viz_local_moran(moran_local, gdf, crit_value, method, **kwargs):
)


def _moran_loc_scatterplot(
moran_loc,
crit_value=None,
ax=None,
scatter_kwds=None,
fitline_kwds=None,
):
"""
Moran Scatterplot with option of coloring of Local Moran Statistics
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
moran_loc : esda.moran.Moran_Local instance
Values of Moran's I Local Autocorrelation Statistics
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
p : float, optional
If given, the p-value threshold for significance. Points will
be colored by significance. By default it will not be colored.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Default =None.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
aspect_equal : bool, optional
If True, Axes of Moran Scatterplot will show the same
aspect or visual proportions.
ax : Matplotlib Axes instance, optional
If given, the Moran plot will be created inside this axis.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Default =None.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
scatter_kwds : keyword arguments, optional
Keywords used for creating and designing the scatter points.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Default =None.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
fitline_kwds : keyword arguments, optional
Keywords used for creating and designing the moran fitline.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Default =None.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
fig : Matplotlib Figure instance
Moran Local scatterplot figure
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
ax : matplotlib Axes instance
Axes in which the figure is plotted
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

martinfleis marked this conversation as resolved.
Show resolved Hide resolved
"""


def _get_cluster_labels(moran_local, crit_value):
gdf = pd.DataFrame()
gdf["q"] = moran_local.q
Expand All @@ -1863,6 +1969,76 @@ def _get_cluster_labels(moran_local, crit_value):
return gdf["Moran Cluster"].values


def _scatterplot(
moran,
crit_value=0.05,
ax=None,
scatter_kwds=None,
fitline_kwds=None,
):
try:
from matplotlib import pyplot as plt
except ImportError as err:
raise ImportError(
"matplotlib library must be installed to use the scatterplot feature"
) from err

# to set default as an empty dictionary that is later filled with defaults
if scatter_kwds is None:
scatter_kwds = dict()
if fitline_kwds is None:
fitline_kwds = dict()

if crit_value is not None:
labels = moran.get_cluster_labels(crit_value)
# TODO: allow customization of colors in here and in plot and explore
# TODO: in a way to keep them easily synced
colors5_mpl = {
"High-High": "#d7191c",
"Low-High": "#89cff0",
"Low-Low": "#2c7bb6",
"High-Low": "#fdae61",
"Insignificant": "lightgrey",
}
colors5 = [colors5_mpl[i] for i in labels] # for mpl

# define customization
scatter_kwds.setdefault("alpha", 0.6)
fitline_kwds.setdefault("alpha", 0.9)

if ax is None:
_, ax = plt.subplots()

# set labels
ax.set_xlabel("Attribute")
ax.set_ylabel("Spatial Lag")
ax.set_title("Moran Local Scatterplot")

# plot and set standards
lag = lag_spatial(moran.w, moran.z)
fit = stats.linregress(
moran.z,
lag,
)
# v- and hlines
ax.axvline(0, alpha=0.5, color="k", linestyle="--")
ax.axhline(0, alpha=0.5, color="k", linestyle="--")
if crit_value is not None:
fitline_kwds.setdefault("color", "k")
scatter_kwds.setdefault("c", colors5)
ax.plot(moran.z, fit.intercept + fit.slope * moran.z, **fitline_kwds)
ax.scatter(moran.z, lag, **scatter_kwds)
else:
scatter_kwds.setdefault("color", "#bababa")
fitline_kwds.setdefault("color", "#d6604d")
ax.plot(moran.z, fit.intercept + fit.slope * moran.z, **fitline_kwds)
ax.scatter(moran.z, lag, **scatter_kwds)

ax.set_aspect("equal")

return ax


# --------------------------------------------------------------
# Conditional Randomization Moment Estimators
# --------------------------------------------------------------
Expand Down
153 changes: 144 additions & 9 deletions esda/tests/test_moran.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,58 @@ def test_by_col(self):
np.testing.assert_allclose(sidr, 0.24772519320480135, atol=ATOL, rtol=RTOL)
np.testing.assert_allclose(pval, 0.001)

@parametrize_sac
def test_Moran_plot_scatterplot(self, w):
import matplotlib

matplotlib.use("Agg")

m = moran.Moran(
sac1.WHITE,
w,
)

ax = m.plot_scatterplot()

# test scatter
np.testing.assert_array_almost_equal(
ax.collections[0].get_facecolors(),
np.array([[0.729412, 0.729412, 0.729412, 0.6]]),
)

# test fitline
l = ax.lines[2]
x, y = l.get_data()
np.testing.assert_almost_equal(x.min(), -1.8236414387225368)
np.testing.assert_almost_equal(x.max(), 3.893056527659032)
np.testing.assert_almost_equal(y.min(), -0.7371749399524187)
np.testing.assert_almost_equal(y.max(), 1.634939204358587)
assert l.get_color() == "#d6604d"

@parametrize_sac
def test_Moran_plot_scatterplot_args(self, w):
import matplotlib

matplotlib.use("Agg")

m = moran.Moran(
sac1.WHITE,
w,
)

ax = m.plot_scatterplot(scatter_kwds=dict(color='blue'), fitline_kwds=dict(color='pink'))

# test scatter
np.testing.assert_array_almost_equal(
ax.collections[0].get_facecolors(),
np.array([[0, 0, 1, 0.6]]),
)

# test fitline
l = ax.lines[2]
assert l.get_color() == "pink"



class TestMoranRate:
def setup_method(self):
Expand Down Expand Up @@ -251,15 +303,98 @@ def test_Moran_Local_plot(self, w):
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]))
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],
[0.5372549, 0.81176471, 0.94117647, 1.0],
[0.82745098, 0.82745098, 0.82745098, 1.0],
[0.84313725, 0.09803922, 0.10980392, 1.0],
[0.99215686, 0.68235294, 0.38039216, 1.0],
]
),
)
np.testing.assert_array_equal(counts, np.array([86, 3, 298, 38, 3]))

@parametrize_sac
def test_Moran_Local_plot_scatterplot(self, w):
import matplotlib

matplotlib.use("Agg")

lm = moran.Moran_Local(
sac1.WHITE,
w,
transformation="r",
permutations=99,
keep_simulations=True,
seed=SEED,
)

ax = lm.plot_scatterplot()

# test scatter
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, 0.6],
[0.5372549, 0.81176471, 0.94117647, 0.6],
[0.82745098, 0.82745098, 0.82745098, 0.6],
[0.84313725, 0.09803922, 0.10980392, 0.6],
[0.99215686, 0.68235294, 0.38039216, 0.6],
]
),
)
np.testing.assert_array_equal(counts, np.array([73, 12, 261, 52, 5]))

# test fitline
l = ax.lines[2]
x, y = l.get_data()
np.testing.assert_almost_equal(x.min(), -1.8236414387225368)
np.testing.assert_almost_equal(x.max(), 3.893056527659032)
np.testing.assert_almost_equal(y.min(), -0.7371749399524187)
np.testing.assert_almost_equal(y.max(), 1.634939204358587)
assert l.get_color() == "k"

@parametrize_sac
def test_Moran_Local_plot_scatterplot_args(self, w):
import matplotlib

matplotlib.use("Agg")

lm = moran.Moran_Local(
sac1.WHITE,
w,
transformation="r",
permutations=99,
keep_simulations=True,
seed=SEED,
)

ax = lm.plot_scatterplot(
crit_value=None,
scatter_kwds={"s": 10},
fitline_kwds={"linewidth": 4},
)
# test scatter
np.testing.assert_array_almost_equal(
ax.collections[0].get_facecolors(),
np.array([[0.729412, 0.729412, 0.729412, 0.6]]),
)
assert ax.collections[0].get_sizes()[0] == 10

# test fitline
l = ax.lines[2]
assert l.get_color() == "#d6604d"
assert l.get_linewidth() == 4.0

@parametrize_desmith
def test_Moran_Local_parallel(self, w):
Expand Down
Loading