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.
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.
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.
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:
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 |
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')
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!