Repository: geopandas/geopandas

Issue:
ENH: give missing data distinct pattern in choropleth plots
From http://ramiro.org/notebook/geopandas-choropleth/, it would be nice if geopandas could give the missing values in your column a distinct pattern from the used color map (eg just blank, or shaded).

cc @yaph


Discussion:
I did some research to see how a pattern can be added in matplotlib and updated the notebook. There is a ``hatch`` option that can be used. Thought I'd share because I wasn't aware of it.
Thanks, similar notebook with an example here: http://nbviewer.jupyter.org/gist/jorisvandenbossche/bb1cc71f94aa3e8f2832f18dd12f6174 

I think the main question will be how to expose this to the user. We could have a `missing_style_kwargs` keyword (or shorter name), where you can specify the style for the missing records. 
Other question is if we want a default behaviour for this.

I think the default should stay as it is, this way the user gets a warning about missing data when trying to plot it. An additional dict with options for missing data is probably a good choice. I suggest using ``color`` and ``hatch`` as keys to be consistent with matplotlib's parameter names.

Resolving patch:
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -376,6 +376,7 @@ def plot_dataframe(
     figsize=None,
     legend_kwds=None,
     classification_kwds=None,
+    missing_kwds=None,
     **style_kwds
 ):
     """
@@ -441,6 +442,11 @@ def plot_dataframe(
         matplotlib.pyplot.colorbar().
     classification_kwds : dict (default None)
         Keyword arguments to pass to mapclassify
+    missing_kwds : dict (default None)
+        Keyword arguments specifying color options (as style_kwds)
+        to be passed on to geometries with missing values in addition to
+        or overwriting other style kwds. If None, geometries with missing
+        values are not plotted.
 
     **style_kwds : dict
         Color options to be passed on to the actual plot function, such
@@ -516,14 +522,16 @@ def plot_dataframe(
     if values.dtype is np.dtype("O"):
         categorical = True
 
+    nan_idx = pd.isna(values)
+
     # Define `values` as a Series
     if categorical:
         if cmap is None:
             cmap = "tab10"
-        categories = list(set(values))
+        categories = list(set(values[~nan_idx]))
         categories.sort()
         valuemap = dict((k, v) for (v, k) in enumerate(categories))
-        values = np.array([valuemap[k] for k in values])
+        values = np.array([valuemap[k] for k in values[~nan_idx]])
 
     if scheme is not None:
         if classification_kwds is None:
@@ -531,16 +539,22 @@ def plot_dataframe(
         if "k" not in classification_kwds:
             classification_kwds["k"] = k
 
-        binning = _mapclassify_choro(values, scheme, **classification_kwds)
+        binning = _mapclassify_choro(values[~nan_idx], scheme, **classification_kwds)
         # set categorical to True for creating the legend
         categorical = True
-        binedges = [values.min()] + binning.bins.tolist()
+        binedges = [values[~nan_idx].min()] + binning.bins.tolist()
         categories = [
             "{0:.2f} - {1:.2f}".format(binedges[i], binedges[i + 1])
             for i in range(len(binedges) - 1)
         ]
         values = np.array(binning.yb)
 
+    # fill values with placeholder where were NaNs originally to map them properly
+    # (after removing them in categorical or scheme)
+    if categorical:
+        for n in np.where(nan_idx)[0]:
+            values = np.insert(values, n, values[0])
+
     mn = values[~np.isnan(values)].min() if vmin is None else vmin
     mx = values[~np.isnan(values)].max() if vmax is None else vmax
 
@@ -552,28 +566,31 @@ def plot_dataframe(
     point_idx = np.asarray((geom_types == "Point") | (geom_types == "MultiPoint"))
 
     # plot all Polygons and all MultiPolygon components in the same collection
-    polys = df.geometry[poly_idx]
+    polys = df.geometry[poly_idx & np.invert(nan_idx)]
+    subset = values[poly_idx & np.invert(nan_idx)]
     if not polys.empty:
         plot_polygon_collection(
-            ax, polys, values[poly_idx], vmin=mn, vmax=mx, cmap=cmap, **style_kwds
+            ax, polys, subset, vmin=mn, vmax=mx, cmap=cmap, **style_kwds
         )
 
     # plot all LineStrings and MultiLineString components in same collection
-    lines = df.geometry[line_idx]
+    lines = df.geometry[line_idx & np.invert(nan_idx)]
+    subset = values[line_idx & np.invert(nan_idx)]
     if not lines.empty:
         plot_linestring_collection(
-            ax, lines, values[line_idx], vmin=mn, vmax=mx, cmap=cmap, **style_kwds
+            ax, lines, subset, vmin=mn, vmax=mx, cmap=cmap, **style_kwds
         )
 
     # plot all Points in the same collection
-    points = df.geometry[point_idx]
+    points = df.geometry[point_idx & np.invert(nan_idx)]
+    subset = values[point_idx & np.invert(nan_idx)]
     if not points.empty:
         if isinstance(markersize, np.ndarray):
-            markersize = markersize[point_idx]
+            markersize = markersize[point_idx & np.invert(nan_idx)]
         plot_point_collection(
             ax,
             points,
-            values[point_idx],
+            subset,
             vmin=mn,
             vmax=mx,
             markersize=markersize,
@@ -581,6 +598,16 @@ def plot_dataframe(
             **style_kwds
         )
 
+    if missing_kwds is not None:
+        if color:
+            if "color" not in missing_kwds:
+                missing_kwds["color"] = color
+
+        merged_kwds = style_kwds.copy()
+        merged_kwds.update(missing_kwds)
+
+        plot_series(df.geometry[nan_idx], ax=ax, **merged_kwds)
+
     if legend and not color:
 
         if legend_kwds is None:
@@ -609,7 +636,25 @@ def plot_dataframe(
                         markeredgewidth=0,
                     )
                 )
-
+            if missing_kwds is not None:
+                if "color" in merged_kwds:
+                    merged_kwds["facecolor"] = merged_kwds["color"]
+                patches.append(
+                    Line2D(
+                        [0],
+                        [0],
+                        linestyle="none",
+                        marker="o",
+                        alpha=merged_kwds.get("alpha", 1),
+                        markersize=10,
+                        markerfacecolor=merged_kwds.get("facecolor", None),
+                        markeredgecolor=merged_kwds.get("edgecolor", None),
+                        markeredgewidth=merged_kwds.get(
+                            "linewidth", 1 if merged_kwds.get("edgecolor", False) else 0
+                        ),
+                    )
+                )
+                categories.append(merged_kwds.get("label", "NaN"))
             legend_kwds.setdefault("numpoints", 1)
             legend_kwds.setdefault("loc", "best")
             ax.legend(patches, categories, **legend_kwds)


Test patch:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -231,6 +231,30 @@ def test_multipoints(self):
         # colors are repeated for all components within a MultiPolygon
         _check_colors(2, ax.collections[0].get_facecolors(), ["r"] * 10 + ["b"] * 10)
 
+    def test_misssing(self):
+        self.df.loc[0, "values"] = np.nan
+        ax = self.df.plot("values")
+        cmap = plt.get_cmap()
+        expected_colors = cmap(np.arange(self.N - 1) / (self.N - 2))
+        _check_colors(self.N - 1, ax.collections[0].get_facecolors(), expected_colors)
+
+        ax = self.df.plot("values", missing_kwds={"color": "r"})
+        cmap = plt.get_cmap()
+        expected_colors = cmap(np.arange(self.N - 1) / (self.N - 2))
+        _check_colors(1, ax.collections[1].get_facecolors(), ["r"])
+        _check_colors(self.N - 1, ax.collections[0].get_facecolors(), expected_colors)
+
+        ax = self.df.plot(
+            "values", missing_kwds={"color": "r"}, categorical=True, legend=True
+        )
+        _check_colors(1, ax.collections[1].get_facecolors(), ["r"])
+        point_colors = ax.collections[0].get_facecolors()
+        nan_color = ax.collections[1].get_facecolors()
+        leg_colors = ax.get_legend().axes.collections[0].get_facecolors()
+        leg_colors1 = ax.get_legend().axes.collections[1].get_facecolors()
+        np.testing.assert_array_equal(point_colors[0], leg_colors[0])
+        np.testing.assert_array_equal(nan_color[0], leg_colors1[0])
+
 
 class TestPointZPlotting:
     def setup_method(self):