ENH: Add GeoPlot accessor (#1465)

Co-authored-by: Martin Fleischmann <martin@martinfleischmann.net>
Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
This commit is contained in:
sangarshanan
2021-02-27 09:34:34 +01:00
committed by GitHub
co-authored by Martin Fleischmann Joris Van den Bossche
parent 6e8f6f91cb
commit e0981ab14e
9 changed files with 185 additions and 21 deletions
+1
View File
@@ -34,6 +34,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
result_images
# Sphinx documentation
doc/_build/
+1
View File
@@ -18,6 +18,7 @@ dependencies:
- rtree
- matplotlib
- mapclassify
- scipy
- geopy
- SQLalchemy
- libspatialite
+1
View File
@@ -18,6 +18,7 @@ dependencies:
- rtree
- matplotlib
- mapclassify
- scipy
- geopy
# installed in tests.yaml, because not available on windows
# - postgis
+1
View File
@@ -19,6 +19,7 @@ dependencies:
- matplotlib
- descartes
- mapclassify
- scipy
- geopy
# installed in tests.yaml, because not available on windows
# - postgis
+1 -1
View File
@@ -13,7 +13,7 @@ Advanced topics can be found in the :doc:`Advanced Guide <advanced_guide>` and f
Data Structures <user_guide/data_structures>
Reading and Writing Files <user_guide/io>
Indexing and Selecting Data <user_guide/indexing>
Making Maps <user_guide/mapping>
Making Maps and plots <user_guide/mapping>
Managing Projections <user_guide/projections>
Geometric Manipulations <user_guide/geometric_manipulations>
Set Operations with overlay <user_guide/set_operations>
+36 -1
View File
@@ -11,7 +11,7 @@
plt.close('all')
Mapping Tools
Mapping and Plotting Tools
=========================================
@@ -222,6 +222,41 @@ We can set the ``zorder`` for cities higher than for world to move it of top.
@savefig zorder_set.png
world.plot(ax=ax, zorder=1);
Pandas Plots
-----------------
Plotting methods also allow for different plot styles from pandas
along with the default ``geo`` plot. These methods can be accessed using
the ``kind`` keyword argument in :meth:`~GeoDataFrame.plot`, and include:
* ``geo`` for mapping
* ``line`` for line plots
* ``bar`` or ``barh`` for bar plots
* ``hist`` for histogram
* ``box`` for boxplot
* ``kde`` or ``density`` for density plots
* ``area`` for area plots
* ``scatter`` for scatter plots
* ``hexbin`` for hexagonal bin plots
* ``pie`` for pie plots
.. ipython:: python
gdf = world.head(10)
@savefig pandas_line_plot.png
gdf.plot(kind='scatter', x="pop_est", y="gdp_md_est")
You can also create these other plots using the ``GeoDataFrame.plot.<kind>`` accessor methods instead of providing the ``kind`` keyword argument.
.. ipython:: python
@savefig pandas_bar_plot.png
gdf.plot.bar()
For more information check out the `pandas documentation <https://pandas.pydata.org/pandas-docs/stable/user_guide/visualization.html>`_.
Other Resources
-----------------
Links to jupyter Notebooks for different mapping tasks:
+18 -14
View File
@@ -1312,20 +1312,6 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
return self
def plot(self, *args, **kwargs):
"""Generate a plot of the geometries in the ``GeoDataFrame``.
If the ``column`` parameter is given, colors plot according to values
in that column, otherwise calls ``GeoSeries.plot()`` on the
``geometry`` column.
Wraps the ``plot_dataframe()`` function, and documentation is copied
from there.
"""
return plot_dataframe(self, *args, **kwargs)
plot.__doc__ = plot_dataframe.__doc__
def dissolve(self, by=None, aggfunc="first", as_index=True):
"""
Dissolve geometries within `groupby` into single observation.
@@ -1617,6 +1603,24 @@ box': (2.0, 1.0, 2.0, 1.0)}], 'bbox': (1.0, 1.0, 2.0, 2.0)}
)
return self.geometry.difference(other)
if compat.PANDAS_GE_025:
from pandas.core.accessor import CachedAccessor
plot = CachedAccessor("plot", geopandas.plotting.GeoplotAccessor)
else:
def plot(self, *args, **kwargs):
"""Generate a plot of the geometries in the ``GeoDataFrame``.
If the ``column`` parameter is given, colors plot according to values
in that column, otherwise calls ``GeoSeries.plot()`` on the
``geometry`` column.
Wraps the ``plot_dataframe()`` function, and documentation is copied
from there.
"""
return plot_dataframe(self, *args, **kwargs)
plot.__doc__ = plot_dataframe.__doc__
def _dataframe_set_geometry(self, col, drop=False, inplace=False, crs=None):
if inplace:
+44 -5
View File
@@ -115,7 +115,7 @@ def _PolygonPatch(polygon, **kwargs):
path = Path.make_compound_path(
Path(np.asarray(polygon.exterior.coords)[:, :2]),
*[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors]
*[Path(np.asarray(ring.coords)[:, :2]) for ring in polygon.interiors],
)
return PathPatch(path, **kwargs)
@@ -254,7 +254,7 @@ def _plot_point_collection(
vmax=None,
marker="o",
markersize=None,
**kwargs
**kwargs,
):
"""
Plots a collection of Point and MultiPoint geometries to `ax`
@@ -488,7 +488,7 @@ def plot_dataframe(
classification_kwds=None,
missing_kwds=None,
aspect="auto",
**style_kwds
**style_kwds,
):
"""
Plot a GeoDataFrame.
@@ -508,6 +508,21 @@ def plot_dataframe(
If np.array or pd.Series are used then it must have same length as
dataframe. Values are used to color the plot. Ignored if `color` is
also set.
kind: str
The kind of plots to produce:
- 'geo': Map (default)
Pandas Kinds
- 'line' : line plot
- 'bar' : vertical bar plot
- 'barh' : horizontal bar plot
- 'hist' : histogram
- 'box' : BoxPlot
- 'kde' : Kernel Density Estimation plot
- 'density' : same as 'kde'
- 'area' : area plot
- 'pie' : pie plot
- 'scatter' : scatter plot
- 'hexbin' : hexbin plot.
cmap : str (default None)
The name of a colormap recognized by matplotlib.
color : str (default None)
@@ -683,7 +698,7 @@ GON (((-122.84000 49.00000, -120.0000...
figsize=figsize,
markersize=markersize,
aspect=aspect,
**style_kwds
**style_kwds,
)
# To accept pd.Series and np.arrays as column
@@ -820,7 +835,7 @@ GON (((-122.84000 49.00000, -120.0000...
vmax=mx,
markersize=markersize,
cmap=cmap,
**style_kwds
**style_kwds,
)
if missing_kwds is not None and not expl_series[nan_idx].empty:
@@ -899,6 +914,30 @@ GON (((-122.84000 49.00000, -120.0000...
return ax
if geopandas._compat.PANDAS_GE_025:
from pandas.plotting import PlotAccessor
class GeoplotAccessor(PlotAccessor):
__doc__ = plot_dataframe.__doc__
_pandas_kinds = PlotAccessor._all_kinds
def __call__(self, *args, **kwargs):
data = self._parent.copy()
kind = kwargs.pop("kind", "geo")
if kind == "geo":
return plot_dataframe(data, *args, **kwargs)
if kind in self._pandas_kinds:
# Access pandas plots
return PlotAccessor(data)(kind=kind, **kwargs)
else:
# raise error
raise ValueError(f"{kind} is not a valid plot kind")
def geo(self, *args, **kwargs):
return self(kind="geo", *args, **kwargs)
def _mapclassify_choro(values, scheme, **classification_kwds):
"""
Wrapper for choropleth schemes from mapclassify for use with plot_dataframe
+82
View File
@@ -29,6 +29,13 @@ matplotlib = pytest.importorskip("matplotlib")
matplotlib.use("Agg")
import matplotlib.pyplot as plt # noqa
try: # skipif and importorskip do not work for decorators
from matplotlib.testing.decorators import check_figures_equal
MPL_DECORATORS = True
except ImportError:
MPL_DECORATORS = False
@pytest.fixture(autouse=True)
def close_figures(request):
@@ -42,6 +49,8 @@ try:
except KeyError:
MPL_DFT_COLOR = matplotlib.rcParams["axes.color_cycle"][0]
plt.rcParams.update({"figure.max_open_warning": 0})
class TestPointPlotting:
def setup_method(self):
@@ -1466,6 +1475,79 @@ class TestPlotCollections:
ax.cla()
@pytest.mark.skipif(not compat.PANDAS_GE_025, reason="requires pandas > 0.24")
class TestGeoplotAccessor:
def setup_method(self):
geometries = [Polygon([(0, 0), (1, 0), (1, 1)]), Point(1, 3)]
x = [1, 2]
y = [10, 20]
self.gdf = GeoDataFrame({"geometry": geometries, "x": x, "y": y})
self.df = pd.DataFrame({"x": x, "y": y})
def compare_figures(self, kind, fig_test, fig_ref, kwargs):
"""Compare Figures."""
ax_pandas_1 = fig_test.subplots()
self.df.plot(kind=kind, ax=ax_pandas_1, **kwargs)
ax_geopandas_1 = fig_ref.subplots()
self.gdf.plot(kind=kind, ax=ax_geopandas_1, **kwargs)
ax_pandas_2 = fig_test.subplots()
getattr(self.df.plot, kind)(ax=ax_pandas_2, **kwargs)
ax_geopandas_2 = fig_ref.subplots()
getattr(self.gdf.plot, kind)(ax=ax_geopandas_2, **kwargs)
_pandas_kinds = []
if compat.PANDAS_GE_025:
from geopandas.plotting import GeoplotAccessor
_pandas_kinds = GeoplotAccessor._pandas_kinds
if MPL_DECORATORS:
@pytest.mark.parametrize("kind", _pandas_kinds)
@check_figures_equal(extensions=["png", "pdf"])
def test_pandas_kind(self, kind, fig_test, fig_ref):
"""Test Pandas kind."""
import importlib
_scipy_dependent_kinds = ["kde", "density"] # Needs scipy
_y_kinds = ["pie"] # Needs y
_xy_kinds = ["scatter", "hexbin"] # Needs x & y
kwargs = {}
if kind in _scipy_dependent_kinds:
if not importlib.util.find_spec("scipy"):
with pytest.raises(
ModuleNotFoundError, match="No module named 'scipy'"
):
self.gdf.plot(kind=kind)
elif kind in _y_kinds:
kwargs = {"y": "y"}
elif kind in _xy_kinds:
kwargs = {"x": "x", "y": "y"}
self.compare_figures(kind, fig_test, fig_ref, kwargs)
plt.close("all")
@check_figures_equal(extensions=["png", "pdf"])
def test_geo_kind(self, fig_test, fig_ref):
"""Test Geo kind."""
ax1 = fig_test.subplots()
self.gdf.plot(ax=ax1)
ax2 = fig_ref.subplots()
getattr(self.gdf.plot, "geo")(ax=ax2)
plt.close("all")
def test_invalid_kind(self):
"""Test invalid kinds."""
with pytest.raises(ValueError, match="error is not a valid plot kind"):
self.gdf.plot(kind="error")
with pytest.raises(
AttributeError,
match="'GeoplotAccessor' object has no attribute 'error'",
):
self.gdf.plot.error()
def test_column_values():
"""
Check that the dataframe plot method returns same values with an