Rich Data     About/Imprint     CV     Publications     Blog Archive     Blog Feed

Using geographical heat maps to visualise cultural influence

100 lines of code (Python)

Streets, squares and places in general tend to be named after influential people. As a consequence, place names offer a glimpse into who shaped local cultures. In this post, I use open street map data in order to visualise the influence of different historical figures in Germany.

Konrad Adenauer place names heat map

Querying open street map data

In order to find out where geographical entities are named after certain historical figures, we need a data base of place names and coordinates. In this post, we will use the volunteer project open street map for that.

One can query the open street map data base with the Python module overpy. Before we write the function to do that, let’s first import the modules we’ll need for this project.

import overpy
import time
import matplotlib.pyplot as plt
import pandas as pd
import geopandas as gpd
import numpy as np
from scipy import ndimage

The API of open street map is thankfully explained here. We follow this guide and write a query_str which defines which text we want to look for in the place names of which country. All the place names which match the text are saved in the local variable df together with their latitude and longitude. The function run_overpy_query() does all that.

API_WAIT_TIME = 60  # seconds
def run_overpy_query(search_term, country_code='DE'):

    api=overpy.Overpass()
    query_str = '''
    area["ISO3166-1"="{0}"][admin_level=2];
    (node["name"~"{1}", i](area);
    way["name"~"{1}", i](area);
    rel["name"~"{1}", i](area);
    );
    out center;
    '''.format(country_code, search_term)
    try:
        r = api.query(query_str)
    except:
        print('Query unsuccessful. Wait {} seconds and try again.'.format(API_WAIT_TIME))
        time.sleep(API_WAIT_TIME)
        r = api.query(query_str)

    # extract coordinates from query result
    coords  = []
    coords += [(float(node.lon), float(node.lat)) for node in r.nodes]
    coords += [(float(way.center_lon), float(way.center_lat)) for way in r.ways]
    coords += [(float(rel.center_lon), float(rel.center_lat)) for rel in r.relations]
    df = pd.DataFrame(coords)
    df.columns = ['lon', 'lat']

    return df

Please note that, as with many APIs, we can run into a rate limitation if we query too often. Waiting 60 seconds (API_WAIT_TIME) should fix that.

Let’s try out the function to see where Germany’s first modern chancellor, Konrad Adenauer, gets honored.

df_ka = run_overpy_query(search_term='adenauer')
df_ka.head()

The result looks like this:

lat long
9.988323 48.388831
8.692704 49.407714
6.273176 50.887146
13.307399 52.500242
8.929486 48.466880

No, I also don’t know where these places are. But we can turn to a map of Germany to find out.

Visualising open street map data

Before we turn to the heat map, it is worth plotting the results first with a simple scatter plot.

def visualise_osm_scatter(search_term, title_name, country_code='DE', author_line=True):
    df = run_overpy_query(search_term=search_term, country_code=country_code)
    world = gpd.read_file('PATH\\TO\\SHAPE\\FILE\\FOLDER\\ne_10m_admin_1_states_provinces\\ne_10m_admin_1_states_provinces.shp')
    fig, ax = plt.subplots(figsize=[8, 9.5])
    ax.axis('off')
    world[world['iso_a2']=='DE'].plot(color='w', edgecolor='black', linewidth=0.5, ax=ax)
    ax.scatter(df['lon'], df['lat'], c='red')
    ax.set_title('Location of the {} places\nnamed after {}\n'.format(len(df), title_name),
                 color=(0.41, 0.41, 0.41),
                 size=20)
    #author line
    if author_line:
        fig.text(0.99, 0.01, '@rikunert', color='lightgrey', style='italic',
             horizontalalignment='right')

    return fig, ax

Note how we first query the API using our new function run_overpy_query() and then visualise the results with ax.scatter(). For reference, I use a map of Germany with federal state borders (world) which I got from here. Otherwise, the function sets a title on top and an author line in the bottom right hand corner.

The result looks like this:

fig, ax = visualise_osm_scatter('adenauer', 'Konrad Adenauer')

Konrad Adenauer place names scatter plot

Apparently, the first political leader of West Germany gets honored in place names all over, well, West Germany. Thirty years after the reunification of Germany he hardly has any place named after him in the former East.

We can get a better overall picture by combining nearby dots on the map into areas. These appear “hot” on a heat map. We adjust the plotting function accordingly.

def visualise_osm(search_term, title_name, country_code='DE', plot='heatmap', author_line=True):

    df = run_overpy_query(search_term=search_term, country_code=country_code)

    # plot query results on map of Germany
    world = gpd.read_file('PATH\\TO\\SHAPE\\FILE\\FOLDER\\ne_10m_admin_1_states_provinces\\ne_10m_admin_1_states_provinces.shp')
    fig, ax = plt.subplots(figsize=[8, 9.5])
    ax.axis('off')
    if plot=='scatter':
        world[world['iso_a2']=='DE'].plot(color='w', edgecolor='black', linewidth=0.5, ax=ax)
        ax.scatter(df['lon'], df['lat'], c='red')
    elif plot=='heatmap':
        # see https://nbviewer.jupyter.org/gist/perrygeo/c426355e40037c452434
        smoothing = 30  # smoothing factor
        bins=1000  # "pixel count"
        cmap='hot'  # color scale
        xlim = [5.7, 15.2]  # East-West extent of Germany
        ylim = [47.1, 55.1]  # North-South extent of Germany
        heatmap, xedges, yedges = np.histogram2d(df['lat'], df['lon'],
                                                 bins=bins, range=[ylim, xlim])
        extent = [yedges[0], yedges[-1],
                  xedges[-1], xedges[0]]
        logheatmap = np.log(heatmap)
        logheatmap[np.isneginf(logheatmap)] = 0
        logheatmap = ndimage.filters.gaussian_filter(logheatmap, smoothing, mode='nearest')  # smooth out peaks
        logheatmap[logheatmap < 0.0001] = 0  # clearly show areas without occurence
        logheatmap = np.max(logheatmap) - logheatmap  # reverse color scale

        world[world['iso_a2']=='DE']['geometry'].boundary.plot(color='k', edgecolor='black', linewidth=0.2, ax=ax)
        ax.imshow(logheatmap, cmap=cmap, extent=extent)
        ax.invert_yaxis()
        ax.set(xlim=xlim, ylim=ylim)

    ax.set_title('Location of the {} places\nnamed after {}\n'.format(len(df), title_name),
                    color=(0.41, 0.41, 0.41),
                    size=20)
    #author line
    if author_line:
        fig.text(0.99, 0.01, '@rikunert', color='lightgrey', style='italic',
             horizontalalignment='right')

    return fig, ax

Under the hood the heat map is a 2D histogram (np.histogram2d()). It’s as if we layed a grid onto the map of Germany and counted how many red dots appear in each cell. This 2D histogram then gets smoothed (ndimage.filters.gaussian_filter()), i.e. the values of nearby cells are averaged. I plot the result using ax.imshow().

Let’s try it out.

fig, ax = visualise_osm_scatter('adenauer', 'Konrad Adenauer')

Konrad Adenauer place names heat map

Three hot spots of Adenauer commemoration are obvious: Berlin, Frankfurt, and the Ruhr region. This was not as obvious when using a scatter plot.

East-west differences

While the first West German chancellor gets commemorated nearly exclusively in West Germany, East Germany also features historical figures which are only honored there. Two examples are famous socialist leaders of the 1918 revolution.

Karl Liebknecht:

fig, ax = name_locator('karl-liebknecht', 'Karl Liebknecht')

Karl Liebknecht place names heat map

Rosa Luxemburg:

fig, ax = visualise_osm('rosa-luxemburg', 'Rosa Luxemburg', plot='heatmap')

Rosa Luxemburg place names heat map

Note how these two figures are nearly exclusively honored in East German place names. The socialist past of this part of Germany is still very much visible.

Another interesting figure is Paul von Hindenburg. Second president of the Weimar Republic who appointed Hitler as chancellor. Where can we find this controversial historical person in place names?

fig, ax = visualise_osm('hindenburg', 'Paul von Hindenburg', plot='heatmap')

Paul von Hindenburg place names heat map

Turns out Hindenburg is nearly only found in West German place names. Germany’s cultural east-west divide is still clearly visible in how geographical entities are named.

Local influence

We can even go further back in time and reveal an enduring influence of historical personalities. Take Martin Luther, a 16th century religious figure who founded the protestant church.

fig, ax = visualise_osm('luther', 'Martin Luther', plot='heatmap')

Martin Luther place names heat map

That Eastern cluster of hot spots are exactly the places where Martin Luther lived. That influence is still visible today. Also interestingly, the protestant north of Bavaria honors Luther in place names while the catholic south does not.

Conclusion

Heatmaps of place names today can be used to track cultural differences between different regions of Germany. However, the approach used in this post has some caveats:

  • Densely populated regions, like the Ruhr region, can appear hot by chance because the denser the population of a region, the more place names there are there in general in this region.
  • The open street map data is based on volunteer submissions and, thus, does not offer a complete picture of place names in Germany.

Nonetheless, as the examples in this post show, a Germany wide scan of place names is possible using open street map. It reveals the local history of Germany.

The complete code to recreate the analyses and plots of this blog post can, as always, be found on github here.

Like this post? Share it with your followers or follow me on Twitter!