Geospatial adventures. Step 4. The Colour of Magic or If I Don’t See It — It Doesn’t Exist. | by Dmitry Selemir | Towards Data Science

Continuing on from Step 3. Now that we’ve got a dataset for all (known to OSM) buildings in the glorious expanses of East Riding of Yorkshire, let’s look at ways of visualising our data.
Just to give you a very quick recap — in the previous post we looked at the local authority polygons in England and combined them with the OSM building polygons. We then discussed a way of attributing all of the building polygons to the corresponding local authority polygons and concentrated on East Riding of Yorkshire, motivated by the beauty of its shape and proximity to Hull. We then proceeded to create a highly inapt visualisation of the result, which we are about to rectify. This post is all about different ways of looking at geometric/geographic data on a map.
We do need a couple of small steps before we can get started properly.
First, let’s load all the libraries we used in the last post:
import geopandas as gpdimport swifterimport numpy as npfrom shapely.geometry import Polygon, GeometryCollection
On top of this we still have our DataFrame with attributed buildings, which we called “bd_ery”. Let’s take a quick peek at it:
bd_ery.head()
We are going to proceed in small steps, so first, let’s see what we can do with GeoDataFrame itself as it already has some extra plotting capabilities, on top of displaying the geometry objects. If you are new to GeoPandas — have a look at the second post of this series)
We will still need to use MatPlotLib, so let’s go ahead and load it as well.
import matplotlib.pyplot as plt%matplotlib inline
One more thing — plotting the entire local authority is going to make all our beautiful building shapes really small and that would be just unforgivable. So, instead, we are going to take advantage of the tiles we created to attribute them in the first place (see previous post for details). We don’t want to deal with their polygons, however, all we need is a simple way of selecting the buildings sitting in a particular tile. To do that, we are going to convert the polygons into ids. The easiest way of doing that is grabbing the coordinates of the bottom left corner (in integer form) and merging them into one 12-digit id (you might also consider hashing the string representation of the polygon, we don’t need to go that far, however). You can keep our 12-digit id as a string or an integer (the latter will take up less memory). I’m going to keep them as strings as they will have to stay inside lists anyway, so the DataFrame will end up seeing them as objects.Transformation can be done in just one line, using some basic list comprehension:
bd_ery['ids']=bd_ery['grid'].swifter.apply(    lambda x: [        ''.join(np.array(a.exterior)[0].astype(int).astype(str)) for a in x    ])
I’m going to use tile ‘499323426360’ for no specific reason, other than it has quite a few buildings in it, so it suits our purposes quite well.
fig, ax = plt.subplots(1, figsize=(15, 15))ax.axes.get_xaxis().set_visible(False)ax.axes.get_yaxis().set_visible(False)bd_ery[bd_ery['ids'].apply(lambda x: 1 if '499323426360' in x else 0) == 1].plot(ax=ax)
OK, this is still somewhat underwhelming, isn’t it? You can definitely see a bunch of building shapes, but that’s about it. We can do a bit more, of course.
Let’s make the axes visible this time and let’s also assign colours to each building according to its type and let’s throw in a colormap whilst we are at it.Gridlines, anyone? Oh, OK, gridlines too... And the legend? Yes, yes, OK...
fig, ax = plt.subplots(1, figsize=(15, 15))colormap="RdYlBu"ax.grid()bd_ery[bd_ery['ids'].apply(lambda x: 1 if '499323426360' in x else 0) == 1].plot(    column='type',    ax=ax,     cmap=colormap,     legend=True)
That’s better... This can actually be useful in the right hands, especially considering how easy it is to make one of these. Stick it into your webapp or interactive dashboard or at least paste the screenshot into a medium blog and pretend that you know what you are talking about.
And yet, we are not quite there. We can do better.
Enter...
The great thing about folium is, while it is still controlled entirely by your application and the output can still be used for webapps and dashboards, the output is an html file, which gives the user quite a lot of control over the resulting map. And yes, it is a map, we are going to plot our lovely polygons on the actual map now!
Before we can get to our polygons though let’s see if we can create the map on its own. Run
!pip install folium
if you haven’t already and let’s load the library and the base map.
import foliumstate_geo = 'http://geoportal1-ons.opendata.arcgis.com/datasets/01fd6b2d7600446d8af768005992f76a_4.geojson'
We also need to pass a starting point, so Folium knows which part of the map we are after. This needs to be done in lat/long format, so I am going to create a mini GeoDataFrame with the centre of our tile as the only point and then use crs conversion method to get lat/long. A somewhat convoluted way of doing that, but it works. There used to be an excellent blog post by Hannah Fry (of BBC4 science podcast fame — it’s awesome, check it out) with the code for conversion and you can still find versions of it written in Python on GitHub, but I believe the blog itself is no longer there. I felt it’s worth mentioning as I still use the code occasionally and know you can still find versions of it out there.
from shapely.geometry import Pointtile = gpd.GeoDataFrame(    {'geometry':[Point(499323+1000,426360+1000)]})tile.crs = 'epsg:27700'tile = tile.to_crs(epsg=4326)
This takes care of the conversion for us and now all we have to do is plot:
m = folium.Map(location=[tile.geometry.iloc[0].y, tile.geometry.iloc[0].x], zoom_start=15)m
Looks somewhat familiar?
Now let’s draw some polygons.
First we’ll turn our tile’s contents into a separate DataFrame and convert it to epsg:4326 as well to make the whole thing just a little bit more comprehensible:
bd_tile = bd_ery[    bd_ery['ids'].apply(lambda x: 1 if '499323426360' in x else 0) == 1].to_crs('epsg:4326').reset_index(drop=True)
And now we can plot away:
folium.GeoJson(bd_tile['geometry'].to_json()).add_to(m)m
Aha! We are almost there! Note, that we just made an addition to the earlier map, we are not redrawing the whole thing.
Let’s add some styling to this. I’m going to steal the colour palette from Seaborn, turn it into a dictionary and pass it into folium.
import seaborn as snstypes = list(bd_tile['type'].unique())color_dict = dict(    zip(        types,        sns.color_palette("Paired", len(types)).as_hex()    ))m = folium.Map(    location=[        tile.geometry.iloc[0].y,        tile.geometry.iloc[0].x    ],    zoom_start=15)folium.GeoJson(    bd_tile[['osm_id', 'type', 'geometry']].to_json(),    style_function = lambda feature: {        'fillColor': color_dict[feature['properties']['type']],        'color': 'black',        'weight': 0.2,        'fillOpacity': 0.7    }).add_to(m)m
And then you can save it as html as easy as:
m.save_to('map.html')
It’s not too bad, but there are still some drawbacks. Unless you are prepared to create a custom html legend like the one here, you are stuck without one. The standard method only allows to add an autogenerated colormap, which isn’t fun for categorical variables at all. There is also another way of creating the same map using choropleth method, which you can check here, but again no customisable legend and no tooltips/popups. The latter you can hack by adding Circle objects to polygon centroids and blending them in. These guys can have both the popups and tooltips, so it makes your life a little bit easier. Still, you can probably catch my drift — there is another option, which I think might be the best way of doing this.
Enter
You can read about it here. It can be used in a jupyter notebook, here’s a very detailed article on that (including installation instructions right at the end). It’s not without some drawbacks, as it can not be fully controlled via code from the notebook itself, the map needs some manual customisation. It can still be saved as html though.
Let’s look at our example in Kepler.
from keplergl import KeplerGlmap_1 = KeplerGl(height=500)
Height parameter refers to the size of the window in jupyter.
Now all we have to do is add our GeoDataFrame to the map object. Make sure you don’t pass any other columns with geometry objects as this will confuse it.
map_1.add_data(    data=bd_tile[['osm_id', 'type', 'geometry']],     name='Building types')map_1
Looks familiar and on the first glance not all that impressive. The key is — from here on it is very easy to customise and get it where it needs to be.
All you need to do is click on that little square with an arrow in the top left corner to get the following menu:
In the first (layers) menu you can adjust the colours and boundaries of your objects, the colours can be driven by columns of the DataFrame you uploaded:
Interactions menu allows you to control the content of the tooltips as well. Menus on the right, among other things, allow you to switch to 3D mode, add legend, etc. Again, the height of your 3D representations can be driven by your DataFrame columns or can be set to a specific value.
You can also zoom in and out, move to other locations, change the view angle, etc.
Here’s an example of what it looks like in 3D, colour coded according to type and displaying a tooltip with osm_id and type:
Final step — save it to html and share away...
map_1.save_to_html(file_name='kepler_example.html')
Not bad... the only major drawback is having to manually massage the final images into the right state, so this is only good for one off presentations or static data representations in your webapp. Still, watch this space — these drawbacks might get rectified relatively quickly.
Oh Yes... Almost forgot — the title image is all the buildings of Westminster, created with the very same KeplerGL. This actually points to its other advantage — it can handle quite a lot of data and so plotting over 20000 polygon objects on a map is not a problem at all.
That’s it for today. Next time — we take a look at Raster files and their analysis/interpretation.
Geospatial adventures. Step 1: Shapely.
Geospatial adventures. Step 2: Pandas vs. GeoPandas
Geospatial adventures. Step 3. Polygons grow on R-Trees
Geospatial adventures. Step 5. Leaving the flatlands or flying over the sea of polygons