Interaktive Karten mit Python

Veröffentlicht von Ramon Voges am 30.07.2019 9 Minuten zum Lesen

In diesem Post zeige ich, wie sich mithilfe von Python, plotly und einigen anderen Bibliotheken interaktive, web-basierte Karten erstellen lassen. Ich erklären, wie man auf der Grundlage von wenigen Angaben programmatisch Karten erstellt, die jeder gängige Browser anzeigen kann. Das ist besonders praktisch, wenn man beispielsweise historische Vorgänge geographisch visualisieren möchte.

Als Beispiel dient mir ein Verzeichnis der Erstausgaben und Übersetzungen von Miguel de Cervantes Don Quixote. Die Angaben zu den Jahren, Orten und der Anzahl der Veröffentlichungen liegen in einer enfachen Excel-Tabelle vor.

Vorbereitungen

Um die interaktive Karte automatisch zu generieren, nutze ich Python. Python ist nicht nur quelloffen und nicht-kommerziell. Es ist auch das Schweizer Taschenmesser unter den Programmiersprachen.

Eine der einfachsten und besten Möglichkeiten, Python zu installieren, bietet die Distribution von Anaconda. Sie ist nicht nur erste Wahl, wenn es um die wissenschaftliche Analyse von Daten aller Art geht. Sie stellt auch alle Erweiterungen und Bibliotheken zur Verfügung, die benötigten werden, wenn man mit Python im Bereich der Geoinformationssysteme (GIS) arbeiten möchte. Auf die Installation von Anaconda gehe ich an dieser Stelle nicht weiter ein. Dafür gibt es eine ganze Reihe von Anlaufstellen im Netz.

Zunächst schaffen wir uns eine neue virtuelle Umgebung, in der wir die nötigen Pakete installieren. Eine virtuelle Umgebung bringt den Vorzug mit, dass wir die Basis-Umgebung nicht verändern müssen. Die einzelnen Pakete und Bibliotheke weisen nämlich mitunter ziemlich komplexe Abhängigkeiten auf und erwarten ganz konkrete andere Pakete. Damit es zu keinen Konflikten unter den Paketen kommt, installiert man sie am besten getrennt für jedes einzelne Projekt in eine virtuelle Umgebung.

Wir nutzen dafür conda create -n geo. Hiermit erstellen wir eine neue virtuelle Umgebung namens ‘geo’. Um sie zu aktivieren, schreiben wir conda activate geo in unser Terminal. Im Prompt sollte darauf etwas wie “(geo) ~$” bzw. “(geo) C:\Windows\System32>” stehen, je nachdem ob unter einem unixoiden Betriebssystem oder Windows arbeiten. Anschließend installieren wir die benötigten Bibliotheken:

(geo) $ conda install notebook numpy pandas plotly
(geo) $ conda install -c plotly plotly-geo
(geo) $ conda install -c conda-forge geopy

Die erste Zeile installiert Bibliotheken, die Anaconda standardmäßig zur Verfügung stellt. Für plotly-geo und geopy müssen wir auf den Kanal plotly bzw. conda-forge zurückgreifen, zwei weiteren Software-Repositorium für Anaconda. Die Rückfragen von Anaconda können wir getrost bejahen.

Daraufhin wechseln wir mit cd Pfad/zu/unseren/Skripten in das Verzeichnis unserer Wahl, wo wir unsere Notebooks und Daten abspeichen wollen. Von hier aus können wir mit jupyter notebook einen neuen Notebook-Server starten und mit der Arbeit beginnen.

Daten einlesen

Als nächstes lesen wir die Daten ein. Dafür gehen wir davon aus, dass sich die Daten in einer Excel-Tabelle mit den Spalten Datum, Ort und Ereignis in dem Verzeichnis data befinden.

Zunächst importieren wir pandas, um die Tabelle einzulesen und die Daten in einem data frame abzulegen. Damit wir mit ihm weiterarbeiten können, weisen wir ihm den Namen df zu.

import pandas as pd

df = pd.read_excel('data/itinerar.xlsx', parse_dates=True)

Mit dem optionalen Argument parse_dates=True stellen wir sicher, dass Pandas die enthaltenen Jahresangaben nicht als Zeichenkette oder Ganzzahl, sondern als Datum interpretiert.

Daten verarbeiten

Nachdem wir unsere Daten eingelesen haben, nutzen wir geopy, um die geographischen Koordinaten der einzelnen Stadtnamen abzufragen. Wir bedienen uns dafür dem Lokalisierungsdienst Nominatim. Die API erwartet einen ‘user_agent’, den wir angeben müssen. Außerdem setzen wir die Zeitdauer etwas hoch, um ein timeout zu verhindern.

import geopy.geocoders as geo

geolocator = geo.Nominatim(user_agent='Itinerar')
geo.options.default_timeout = 10

Da wir mithilfe von Pandas gleich eine ganze Reihe von Lokalisierungsanfragen stellen, ist es eine Frage der Höflichkeit, dem Server kleine Pausen zwischen den einzelnen Anfragen einzuräumen. Das erreichen wir folgendermaßen:

from geopy.extra.rate_limiter import RateLimiter
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)

Jetzt erstellen wir eine neue Spalte namens ‘GeoData’, in der wir die geographischen Informationen der Lokalisierungsabfrage ablegen. Dafür greifen wir der Reihe nach auf die Informationen in der Spalte ‘Ort’ zurück und übergeben sie der Funktion zur Georeferenzierung. Den Rückgabewert speichern wir in der Spalte ‘GeoData’.

df['GeoData'] = df['Ort'].apply(geolocator.geocode)

Anschließend ergänzen wir zwei weitere Spalte, in denen wir nacheinander die Länge und Breite der jeweiligen Orte eintragen. Die Daten entnehmen wir den gerade gewonnenen Geo-Informationen.

df['Latitude'] = df['GeoData'].apply(lambda x: x.latitude if x else None)
df['Longitude'] = df['GeoData'].apply(lambda x: x.longitude if x else None)

Die Informationen in ‘GeoData’ übergeben wir eine anonymen Lambda-Funktion als x. Von diesem x übernehmen wir das Attribut latitude bzw. longitude, wenn für x ein Wert vorhanden ist, ansonsten lassen wir den Wert leer.

Um nicht immer wieder dieselbe Abfrage ausführen zu müssen, speichern wir die Daten in einer CSV-Tabelle mit df.to_csv('data/Cervantes.csv'). Folgende Daten sind in ihr enthalten:

Erstausgaben von Don Quixote
Jahr Anzahl Ort Details GeoData Latitude Longitude
0 1605 2 Madrid Madrid, Área metropolitana de Madrid y Corredor del Henares, Comunidad de Madrid, 28001, España 40.4167047 -3.7035825
1 1605 2 Lissabon Lisboa, LSB, Lisboa, Grande Lisboa, Área Metropolitana de Lisboa, 1100-148, Portugal 38.7077507 -9.1365919
2 1605 1 Valencia València, Comarca de València, València / Valencia, Comunitat Valenciana, España 39.4699014 -0.3759513
3 1607 1 Mailand Milano, MI, LOM, Italia 45.4667971 9.1904984
4 1607 1 Brüssel BXL, Brussel-Hoofdstad - Bruxelles-Capitale, Région de Bruxelles-Capitale - Brussels Hoofdstedelijk Gewest, 1000, België / Belgique / Belgien 50.8465573 4.351697
5 1608 1 Madrid Madrid, Área metropolitana de Madrid y Corredor del Henares, Comunidad de Madrid, 28001, España 40.4167047 -3.7035825
6 1611 1 Brüssel BXL, Brussel-Hoofdstad - Bruxelles-Capitale, Région de Bruxelles-Capitale - Brussels Hoofdstedelijk Gewest, 1000, België / Belgique / Belgien 50.8465573 4.351697
7 1612 1 London Englische Übersetzung von Thomas Shelton London, Greater London, England, SW1A 2DX, United Kingdom 51.5073219 -0.1276474
8 1614 1 Paris Französische Übersetzung von César Oudin Paris, Île-de-France, France métropolitaine, France 48.8566101 2.3514992
9 1615 1 Madrid Band 2 erscheint Madrid, Área metropolitana de Madrid y Corredor del Henares, Comunidad de Madrid, 28001, España 40.4167047 -3.7035825
10 1622 1 Venedig Italienische Übersetzung Venezia, VE, VEN, Italia 45.4371908 12.3345898
11 1648 1 Frankfurt Deutsche Übersetzung Frankfurt, Regierungsbezirk Darmstadt, Hessen, 60311, Deutschland 50.1106444 8.6820917

Daten darstellen

Jetzt können wir damit beginnen, die Daten auf einer Karte darzustellen. Wir greifen dafür auf die Bibliothek plotly-express zurück.

import plotly.express as px

Zwei Zeilen genügen bereits, um die Ausgaben auf einer Karte darzustellen:

fig = px.line_geo(df, lat='Latitude', lon='Longitude',
                  scope='europe', text='Ort',
                  hover_data=['Jahr'])
fig.show()

Wir rufen von plotly.express die Funktion line_geo() auf und weisen dem zurückgegebenen Objekt den Namen ‘fig’ für figure zu. Diese erwartet eine Reihe von obligatorischen und optionalen Argumenten, um ein Liniendiagramm auf einer Karte darzustellen. Als erstes muss immer der data frame, das Dictionary oder die Liste stehen, aus denen die Daten bezogen werden sollen. In diesem Fall ist es der data frame df. Anschließend übergeben wir die Angaben für Länge und Breite, die aus den Spalten ‘Latitude’ bzw. ‘Longitude’ von df bezogen werden können. Die folgenden Argumente sind optional: Mit scope='europe' legen wir den Ausschnitt der darzustellenden Karte fest. text='Ort' weist die Funktion an, die Beschriftung der geographischen Punkte aus der Spalte ‘Ort’ des data frame zu beziehen. Das letzte Argument erlaubt es, die Spalten anzugeben, die angezeigt werden sollen, wenn sich die Maus über die entsprechenden Punkte bewegt.

Das Ergebnis sieht bereits ganz passabel aus. Nur fällt auf, dass einige Orte, z.B. Madrid und Brüssel, im Laufe der Jahre mehrfach auftauchen. Wäre es also nicht praktischer, auf der Karte auch den zeitlichen Verlauf deutlicher darzustellen. Kein Problem!

Dafür erstellen wir ein Streudigramm auf einer Karte und lassen das Ganze als Animation abspielen:

fig = px.scatter_geo(df, lat='Latitude', lon='Longitude',
                  scope='europe', text='Ort',
                  hover_data=['Jahr'], size='Anzahl',
                  opacity=0.4, animation_frame='Jahr')
fig.show()

Mit dem Argument animation_frame='Datum' geben wir der Funktion zu verstehen, dass sie die Jahresangaben nutzen soll, um die geographischen Punkte nacheinander darzustellen. Um zusätzlich die unterschiedlichen Anzahlen der Ausgaben optisch hervortreten lassen, geben wir über das Argument size die auszulesenden Werte angeben. Ziemlich cool. Und sehr einfach.

Für weitere Anpassungen verweise ich auf die ausführliche Dokumentation von plotly. Um die interaktiven Karten zu speichern, benutzt man am besten die mitgelieferten Funktionen wie write_html():

fig.write_html('itinerar.html')

Fazit

Plotly, Geopy und Pandas geben ein hervorragendes Team ab, wenn es darum geht, interaktive und ansprechende Karten zu erstellen. Das gilt insbesondere, seitdem Plotly in der Version 4 vorliegt und damit zahlreiche Einzelschritte massiv vereinfacht hat. Zwei kleine Skripte, die den Vorgang weiter automatisieren, können bei Github heruntergeladen werden.

Viel Spaß beim Plotten!