The key in toplogical coloring is the definition of adjacency, to understand which features are neighboring and could not share the same color. greedy comes with several methods of defining it. Binary spatial weights denoting adjacency are then stored as libpysal’s weight objects.

import geopandas as gpd
import pandas as pd
import numpy as np
from time import time
import seaborn as sns
import matplotlib.pyplot as plt

from shapely.geometry import Point

from greedy import greedy

sns.set()

For illustration purposes, let’s generate a 10x10 mesh of squared polygons:

polys = []
for x in range(10):
for y in range(10):
polys.append(Point(x, y).buffer(0.5, cap_style=3))
gdf = gpd.GeoDataFrame(geometry=polys)
ax = gdf.plot(edgecolor='w')
ax.set_axis_off() The most performant way of generating spatial weights is using libpysal contiguity weights. As they are based on the shared nodes or edges, dataset needs to be topologically correct. Neighboring polygons needs to share vertices and edges, otherwise their relationship will not be captured.

Rook¶

There are two ways how to define contiguity weights - rook and queen. Rook identifies two objects as neighboring only if they share at least on edge - line between two shared points. Use rook if you do not mind two polygons touching by hteir corners having the same color:

gdf['rook'] = greedy(gdf, sw='rook', min_colors=2)
ax = gdf.plot('rook', edgecolor='w', categorical=True, cmap='Set3')
ax.set_axis_off() Queen¶

The default option in greedy is queen adjacency. That identifies two objects as neighboring if they share at least one point. It ensures that even poygons sharing only one corner will not share a color:

gdf['queen'] = greedy(gdf, sw='queen', min_colors=2)
ax = gdf.plot('queen', edgecolor='w', categorical=True, cmap='Set3')
ax.set_axis_off() As noted above, if the topology of the dataset is not ideal, libpysal might not identify two visually neighboring features as neighbors. greedy can use intersection-based algorithm using GEOS intersection to define if two features intersects in any way. They do not have to share any points. Naturally, such an approach is significantly slower (details below), but it can provide correct adjacency when libpysal fails.

To make greedy to use this algorithm, one just needs to define min_distance. If it is set to 0, it behaves similarly to queen contiguity, just capturing all intersections:

gdf['geos'] = greedy(gdf, min_distance=0, min_colors=2)
ax = gdf.plot('geos', edgecolor='w', categorical=True, cmap='Set3')
ax.set_axis_off() min_distance also sets the minimal distance between colors. To do that, all features within such a distance are identified as neighbors, hence no two features wihtin set distance can share the same color:

gdf['dist1'] = greedy(gdf, min_distance=1, min_colors=2)
ax = gdf.plot('dist1', edgecolor='w', categorical=True, cmap='Set3')
ax.set_axis_off() Reusing spatial weights¶

Passing libpysal.weights.W object to sw, will skip generating spatial weights and use the passed object instead. That will improve the performance if one intends to repeate the coloring multiple times. In that case, weights should be denoted using GeodataFrame’s index.

Performance¶

The difference in performance of libpysal and GEOS-based method is large, so it is recommended to use libpysal if possible. Details of comparison between all methods are below:

times = pd.DataFrame(index=['rook', 'queen', 'geos', 'dist1'])
for number in range(10, 110, 10):
print(number)
polys = []
for x in range(number):
for y in range(number):
polys.append(Point(x, y).buffer(0.5, cap_style=3))
gdf = gpd.GeoDataFrame(geometry=polys)
timer = []
for run in range(5):
s = time()
colors = greedy(gdf, sw='rook', min_colors=2)
e = time() - s
timer.append(e)
times.loc['rook', number] = np.mean(timer)
print('rook: ', np.mean(timer), 's; ', np.max(colors) + 1, 'colors')
timer = []
for run in range(5):
s = time()
colors = greedy(gdf, sw='queen', min_colors=2)
e = time() - s
timer.append(e)
times.loc['queen', number] = np.mean(timer)
print('queen: ', np.mean(timer), 's; ', np.max(colors) + 1, 'colors')
timer = []
for run in range(5):
s = time()
colors = greedy(gdf, min_distance=0, min_colors=2)
e = time() - s
timer.append(e)
times.loc['geos', number] = np.mean(timer)
print('geos: ', np.mean(timer), 's; ', np.max(colors) + 1, 'colors')
timer = []
for run in range(5):
s = time()
colors = greedy(gdf, min_distance=1, min_colors=2)
e = time() - s
timer.append(e)
times.loc['dist1', number] = np.mean(timer)
print('dist1: ', np.mean(timer), 's; ', np.max(colors) + 1, 'colors')
10
rook:  0.008332633972167968 s;  2 colors
queen:  0.0067865848541259766 s;  4 colors
geos:  0.18540759086608888 s;  4 colors
dist1:  0.3216702938079834 s;  10 colors
20
rook:  0.023499011993408203 s;  2 colors
queen:  0.02506265640258789 s;  4 colors
geos:  0.7820498943328857 s;  4 colors
dist1:  1.1883579730987548 s;  10 colors
30
rook:  0.09070181846618652 s;  2 colors
queen:  0.05646052360534668 s;  4 colors
geos:  1.4737992763519288 s;  4 colors
dist1:  2.313761281967163 s;  10 colors
40
rook:  0.1293130874633789 s;  2 colors
queen:  0.08214569091796875 s;  4 colors
geos:  2.6077350616455077 s;  4 colors
dist1:  4.236755275726319 s;  10 colors
50
rook:  0.1469170093536377 s;  2 colors
queen:  0.16109180450439453 s;  4 colors
geos:  4.08953275680542 s;  4 colors
dist1:  6.589194393157959 s;  10 colors
60
rook:  0.25732903480529784 s;  2 colors
queen:  0.25914812088012695 s;  4 colors
geos:  7.026346206665039 s;  4 colors
dist1:  10.011457777023315 s;  10 colors
70
rook:  0.29179821014404295 s;  2 colors
queen:  0.3061429500579834 s;  4 colors
geos:  8.105659198760986 s;  4 colors
dist1:  12.690713024139404 s;  10 colors
80
rook:  0.40609278678894045 s;  2 colors
queen:  0.37023115158081055 s;  4 colors
geos:  10.018446493148804 s;  4 colors
dist1:  16.996356391906737 s;  10 colors
90
rook:  0.5523331642150879 s;  2 colors
queen:  0.5231795310974121 s;  4 colors
geos:  13.362245750427245 s;  4 colors
dist1:  19.79914846420288 s;  10 colors
100
rook:  0.632891035079956 s;  2 colors
queen:  0.6089587688446045 s;  4 colors
geos:  15.45732364654541 s;  4 colors
dist1:  24.436386251449584 s;  10 colors

Comparison of time needed to generate greedy coloring using each of the methods above shows the big difference between GEOS and libpysal:

ax = times.T.plot()
ax.set_ylabel('time (s)')
ax.set_xlabel('# of polygons')
locs, labels = plt.xticks()
plt.xticks(locs, (times.columns ** 2), rotation='vertical') Plotting without the GEOS methods, the difference between queen and rook is minimal:

ax = times.loc[['rook', 'queen']].T.plot()
ax.set_ylabel('time (s)')
ax.set_xlabel('# of polygons')
locs, labels = plt.xticks()
plt.xticks(locs, (times.columns ** 2), rotation='vertical') 