'Creating custom colourmap for geopandas.explore plot

all code:

def rgb2hex(r,g,b):
    return '#{:02x}{:02x}{:02x}'.format(r,g,b)

def rg(num):
    num = int(np.round((num / 100) * 124))
    r = (124 - num)
    g = (124 + num)
    b = (0)
    x = rgb2hex(r,g,b)
    return x

def colourmap(value):
    if math.isnan(value) == False:
        y = rg(value)
    else:
        y = '#808080'
    return y

m = homes.explore(
     column="Percentage",
     cmap=lambda value: colourmap(value),#map to custom colour scheme
     marker_kwds=dict(radius=15, fill=True), # make marker radius 15px with fill
     tooltip=labels, # show labels in the tooltip
     legend=False, 
     name="Homes" # name of the layer in the map
)
m #plot

Hi, I'm relatively new to stackoverflow and using geopandas so any comments are appreciated.

I'm trying to create a custom colour scheme that handles NaN values.

Green being high percentages, yellow being low but gray meaning NaN.

However I get this error:


ValueError                                Traceback (most recent call last)
_______________________________________.ipynb Cell 33' in <module>
      3 labels = ["Worker Type","Postcode","Name","Percentage"]
---> 16 m = homes.explore(
     17      column="Percentage",
     18      cmap=lambda value: colourmap(value)
     19      marker_kwds=dict(radius=15, fill=True)
     20      tooltip=labels
     22      legend=False, # do not show column label in the tooltip
     23      name="Homes" # name of the layer in the map

File ____________________\ref_env\lib\site-packages\geopandas\geodataframe.py:1858, in GeoDataFrame.explore(self, *args, **kwargs)
   1855 @doc(_explore)
   1856 def explore(self, *args, **kwargs):
   1857     """Interactive map based on folium/leaflet.js"""
-> 1858     return _explore(self, *args, **kwargs)

File ________________\ref_env\lib\site-packages\geopandas\explore.py:457, in _explore(df, column, cmap, color, m, tiles, attr, tooltip, popup, highlight, categorical, legend, scheme, k, vmin, vmax, width, height, categories, classification_kwds, control_scale, marker_type, marker_kwds, style_kwds, highlight_kwds, missing_kwds, tooltip_kwds, popup_kwds, legend_kwds, **kwargs)
    454     nan_color = missing_kwds.pop("color", None)
    456     gdf["__folium_color"] = nan_color
--> 457     gdf.loc[~nan_idx, "__folium_color"] = color
    458 else:
    459     gdf["__folium_color"] = color

File _______________\ref_env\lib\site-packages\pandas\core\indexing.py:723, in _LocationIndexer.__setitem__(self, key, value)
    720 self._has_valid_setitem_indexer(key)
    722 iloc = self if self.name == "iloc" else self.obj.iloc
--> 723 iloc._setitem_with_indexer(indexer, value, self.name)

File _________________\ref_env\lib\site-packages\pandas\core\indexing.py:1730, in _iLocIndexer._setitem_with_indexer(self, indexer, value, name)
   1727 # align and set the values
   1728 if take_split_path:
   1729     # We have to operate column-wise
-> 1730     self._setitem_with_indexer_split_path(indexer, value, name)
   1731 else:
   1732     self._setitem_single_block(indexer, value, name)

File ______________\ref_env\lib\site-packages\pandas\core\indexing.py:1785, in _iLocIndexer._setitem_with_indexer_split_path(self, indexer, value, name)
   1780     if len(value) == 1 and not is_integer(info_axis):
   1781         # This is a case like df.iloc[:3, [1]] = [0]
   1782         #  where we treat as df.iloc[:3, 1] = 0
   1783         return self._setitem_with_indexer((pi, info_axis[0]), value[0])
-> 1785     raise ValueError(
   1786         "Must have equal len keys and value "
   1787         "when setting with an iterable"
   1788     )
   1790 elif lplane_indexer == 0 and len(value) == len(self.obj.index):
   1791     # We get here in one case via .loc with a all-False mask
   1792     pass

ValueError: Must have equal len keys and value when setting with an iterable

Can someone describe to me this error and direct me on where to look at next?

EDIT: I should have included that I am using Point Geometry



Solution 1:[1]

  • have used standard geometry to simulate data for this question
  • what I have found appears to be a bug when cmap is a callable and their are NaN values in column
  • have worked around by
    1. fillna(-99)
    2. adapted your colormap() function to treat -99 same as NaN
  • have extended answer based on comment around JSON serialisation. Created a column that is functions. Test columns are JSON serialisable and then exclude columns that are not I have contributed enhancements to explore() in geopandas. I'm working through permutations of parameters that leads to this issue / bug. Will raise an issue against Geopandas and possibly commit a PR. For meantime I recommend using work around noted above.

Update have created an issue https://github.com/geopandas/geopandas/issues/2408

import geopandas as gpd
import numpy as np
import math
import json


def rgb2hex(r, g, b):
    return "#{:02x}{:02x}{:02x}".format(r, g, b)


def rg(num):
    num = int(np.round((num / 100) * 124))
    r = 124 - num
    g = 124 + num
    b = 0
    x = rgb2hex(r, g, b)
    return x


def colourmap(value):
    if value != -99 and math.isnan(value) == False:  # changed to treat -99 as NaN :-(
        y = rg(value)
    else:
        y = "#808080"

    return y


# some geometry to demonstrate
def foo():
    return True


homes = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
homes["Percentage"] = np.random.randint(1, 100, len(homes))
homes["func_col"] = foo
homes.loc[homes.sample(20).index, "Percentage"] = np.nan
# miss this line and error is generated...
homes["Percentage"] = homes["Percentage"].fillna(-99)

excl = {}
for c in homes.columns:
    try:
        json.dumps(homes[c].to_dict())
    except TypeError as e:
        if c != "geometry":
            excl[c] = str(e)

m = homes.loc[:, [c for c in homes.columns if c not in excl.keys()]].explore(
    column="Percentage",
    cmap=lambda value: colourmap(value),  # map to custom colour scheme
    marker_kwds=dict(radius=15, fill=True),  # make marker radius 15px with fill
    # tooltip=labels, # show labels in the tooltip
    legend=False,
    name="Homes",  # name of the layer in the map
)
m  # plot

Solution 2:[2]

Combining from Rob's answer and discussion in comments, here was the final solution that is pretty much what I ended up using:

import geopandas as gpd
import numpy as np
import math
import json
import random
import pandas as pd
import random

def rgb2hex(r, g, b):
    return "#{:02x}{:02x}{:02x}".format(r, g, b)
    
def rg(p):
    d = [255,0,0]
    d[1] = int((510*p)/100)
    if d[1]>255:
        d[0] -= d[1]-255
        d[1] = 255
    x = rgb2hex(d[0],d[1],d[2])
    return x

def colourmap(value):
    if value != -99 and math.isnan(value) == False:  # changed to treat -99 as NaN :-(
        y = rg(value)
    else:
        y = "#808080"

    return y

def foo(df):
    x = np.random.randint(-180, 180, len(df))
    y = np.random.randint(-90, 90, len(df))
    s = gpd.GeoSeries.from_xy(x, y, crs="EPSG:4326")
    return s

homes = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
homes["Percentage"] = np.random.randint(1, 100, len(homes))
homes["func_col"] = foo(homes)
homes.loc[homes.sample(20).index, "Percentage"] = np.nan
# miss this line and error is generated...
homes["Percentage"] = homes["Percentage"].fillna(-99)

homes = homes.set_geometry('func_col')

excl = {}
for c in homes.columns:
    try:
        json.dumps(homes[c].to_dict())
    except TypeError as e:
        if c != "geometry":
            excl[c] = str(e)

m = homes.explore(
     column="Percentage",
     cmap=lambda value: colourmap(value),
     marker_kwds=dict(radius=15, fill=True), # make marker radius 15px with fill
     tooltip=["name"], # show "name" column in the tooltip
     legend_kwds=dict(caption="Percentage",colorbar=True), # do not use colorbar
     legend=False, # do not show column label in the tooltip
     name="homes" # name of the layer in the map
)

m

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Ross Perry