Daten aus OpenStreetMap (OSM) und WikiData zusammen mit städtischen Daten nutzen
# Python Pakete installieren (auf JupyterHub nicht nötig!)
# pip install -r requirements.txt
import os
import json
from pprint import pprint
import requests
import folium
import geopandas
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import matplotlib.dates as mdates
from IPython.display import HTML, JSON, display, display_javascript
import utils
# Inhalt von utils anschauen
#%pycat utils/__init__.py
lv95 = 'EPSG:2056'
wgs84 = 'EPSG:4326'
Im ersten Teil schauen wir uns an, wie wir städtische Daten mit Daten aus OpenStreetMap (OSM) anreichern können. Dazu laden wir uns zuerst den Datensatz «Kunst im öffentlichen Raum» via WFS.
Auf dem Open-Data-Katalog finden wir den Datensatz Kunst im Stadtraum. Wenn wir den Datensatz als GeoJSON herunterladen wollen, werden wir entsprechend zum Geoportal weitergeleitet.
# Variablen setzen
kioer_geojson_url = 'https://www.ogd.stadt-zuerich.ch/wfs/geoportal/Kunst_im_Stadtraum?service=WFS&version=1.1.0&request=GetFeature&outputFormat=GeoJSON&typename=view_kioer'
kioer_layer = 'view_kioer'
wfs_url = 'https://www.ogd.stadt-zuerich.ch/wfs/geoportal/Kunst_im_Stadtraum'
layers = utils.get_layers_from_wfs(wfs_url) # GetCapabilitites
layers
r = requests.get(wfs_url, params={
'service': 'WFS',
'version': '1.0.0',
'request': 'GetFeature',
'typename': kioer_layer,
'outputFormat': 'GeoJSON'
})
kioer_geo = r.json()
display(kioer_geo)
Wir suchen thematisch passende Daten aus OpenStreetMap. Um diese zu finden, lohnt es sich das OpenStreetMap Wiki zu durchsuchen und passende Tags zu finden.
ACHTUNG: Daten aus OpenStreetMap stehen unter der Open Data Commons Open Database License (ODbL). D.h. die Verwendung der Daten ist erlaubt wenn:
Daten von OpenStreetMap (OSM) können u.a. via Overpass API geladen werden. Overpass hat eine eigene Abfragesprache (Overpass QL), mit der Objekte (Nodes, Ways, Relations) abgefragt werden können.
[Bildquelle: © Itinero](https://docs.itinero.tech/docs/osmsharp/osm.html)
artwork_zh = """
/*
Alle Kunstwerke (tourism=artwork) in der Stadt Zürich
*/
[out:json];
area["name"="Zürich"]["wikipedia"="de:Zürich"]->.perimeter;
(
nwr[tourism=artwork](area.perimeter);
);
out center;
"""
osm_result = utils.overpass_query(artwork_zh)
osm_result
# Basiskarte
m = utils.base_map()
# KiöR-Daten hinzufügen
kioer_layer = folium.FeatureGroup(name='KiöR', show=True)
utils.style_layer(kioer_geo, kioer_layer, icon_color='#031cff', icon='certificate', prefix='fa')
kioer_layer.add_to(m)
# OSM-Daten hinzufügen
osm_layer = folium.FeatureGroup(name='OSM: tourism=artwork', show=True)
utils.style_layer(osm_result, osm_layer, icon_color='#ff0000', icon='fire', prefix='fa')
osm_layer.add_to(m)
# Add controls for layers
folium.LayerControl().add_to(m)
m
Um die beiden Quellen zu matchen gibt es grundsätzlich zwei Möglichkeiten:
Oder eine Kombination der beiden Ansätze.
Im folgenden schauen wir uns den Räumlichen Join («Spatial Join») genauer an.
kioer_df = geopandas.GeoDataFrame.from_features(kioer_geo, crs=wgs84)
kioer_df.head()
osm_df = geopandas.GeoDataFrame.from_features(osm_result, crs=wgs84)
osm_df.head()
# Einträge mit leerer Geometrie
kioer_df[(pd.isna(kioer_df.geometry))].head()
# Alle Einträge mit leerer Geometrie entfernen
kior_buf = kioer_df.dropna(subset=['geometry']).reset_index(drop=True)
osm_buf = osm_df.dropna(subset=['geometry']).reset_index(drop=True)
# CRS zu LV95 re-projezieren
kior_buf = kior_buf.to_crs(lv95) # Konvertieren zu 2D Koordinatensystem
osm_buf = osm_buf.to_crs(lv95) # Konvertieren zu 2D Koordinatensystem
# Buffer um die Geometrien hinzufügen (10 Meter)
kior_buf['geometry'] = kior_buf.geometry.buffer(10)
osm_buf['geometry'] = osm_buf.geometry.buffer(10)
# Basiskarte
bm = utils.base_map()
folium.features.GeoJson(
kior_buf.to_crs(wgs84).to_json(),
tooltip=folium.features.GeoJsonTooltip(
fields=['titel', 'autoren', 'datierung', 'material'],
aliases=['Titel:', 'Künstler:', 'Datierung:', 'Material:'],
)
).add_to(bm)
folium.features.GeoJson(
osm_buf.to_crs(wgs84).to_json(),
style_function=lambda x: {'fillColor': '#FF0000', 'color': '#FF0000'},
tooltip=folium.features.GeoJsonTooltip(
fields=['name', 'artist_name', 'wikidata'],
aliases=['Titel:', 'Künstler:', "Wikidata:"],
)
).add_to(bm)
bm
# spatial join über die beiden Geometrien
sjoin_kunst = geopandas.sjoin(kior_buf, osm_buf, how='left', predicate='intersects', lsuffix='kior', rsuffix='osm').reset_index()
# Wie zurück zu WGS84
sjoin_kunst = sjoin_kunst.to_crs(wgs84)
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
display(sjoin_kunst[['titel', 'name', 'autoren', 'artist_name', 'material_kior', 'material_osm', 'datierung']])
folium.features.GeoJson(
sjoin_kunst.to_json(),
style_function=lambda x: {'fillColor': '#09bd63', 'color': '#09bd63'},
tooltip=folium.features.GeoJsonTooltip(
fields=['name', 'artist_name', 'wikidata'],
aliases=['Titel:', 'Künstler:', "Wikidata:"],
)
).add_to(bm)
bm
# OSM-Einträge ohne Match
osm_no_match = osm_df[(~osm_df.id.isin(sjoin_kunst.id))].reset_index()
osm_no_match.head()
Das Result lässt sich sehr einfach als Geopackage exportieren.
Im Beispiel ein Layer für die gematchten und ein Layer für die nicht-gematchten Einträge.
Das Geopackage lässt sich anschliessend z.B. in QGIS weiterverarbeiten
osm_no_match.to_file(os.path.join('..', 'kunst_package.gpkg'), layer='osm_no_match', driver="GPKG")
sjoin_kunst.to_file(os.path.join('..', 'kunst_package.gpkg'), layer='kioer_osm_match', driver="GPKG")
WikiData ist ein Schwesterprojekt von Wikipedia. Es beinhaltet strukturierte Daten, welche via API abgefragt werden können (REST oder SPARQL).
Alle Daten aus WikiData sind CreativeCommons-Zero (CC0) lizenziert, können also bedenkenlos weiterverwendet werden.
# WikiData Verweise von OpenStreetMap
osm_kunst_wd = osm_df[(~pd.isna(osm_df.wikidata))] # alle Einträge, bei denen "wikidata" nicht leer ist
osm_kunst_wd = osm_kunst_wd[['geometry', 'type', 'id', 'artist_name', 'artist:wikidata', 'name', 'wikidata']].reset_index(drop=True)
osm_kunst_wd.head()
wikidata_item = utils.wikidata_item(osm_kunst_wd['wikidata'].iloc[0])
pprint(wikidata_item['sitelinks'])
Bilder aus Wikimedia Commons laden:
# Bilder laden
category = wikidata_item['sitelinks']['commonswiki']['title']
urls = utils.images_from_commons_category(category, image_size=500)
urls
#Bilder anzeigen
display(HTML(''.join([utils.img_html(url) for url in urls])))
Um Daten aus WikiData zu laden, können Abfragen mit SPARQL gemacht werden. SPARQL ist eine SQL-ähnliche Abfragesprache für Linked Data.
Die Idee von Linked Data ist es, einen Informations-Graphen in Form von sogenannten «Triples» abzubilden:
Triple = Subjekt (z.B. Marie) -> Prädikat (z.B. birthPlace) -> Objekt (z.B. Italy)
[Bildquelle: © WordLift](https://wordlift.io/blog/en/entity/linked-data/)
# direkte SPARQL-Query auf WikiData
wd_kunstwerke_query = """
SELECT DISTINCT ?artwork ?artworkLabel ?creator ?creatorLabel ?creatorBirthday ?createDateOfDeath ?geo
WHERE
{
?artwork wdt:P136 wd:Q557141 . # Genre "Kunst im öffentlichen Raum"
?artwork wdt:P131 wd:Q72 . # liegt in Zürich
OPTIONAL {
?artwork wdt:P170 ?creator . # Urheber des Werks
?creator wdt:P569 ?creatorBirthday . # Geburtsdatum des Urhebers
?creator wdt:P570 ?createDateOfDeath . # Todesdatum des Urhebers
}
OPTIONAL {
?artwork wdt:P625 ?geo . # Koordinaten für Kartenansicht
}
SERVICE wikibase:label {
bd:serviceParam wikibase:language "[AUTO_LANGUAGE],de,en"
}
}
ORDER BY ?artworkLabel
"""
wd_result = utils.wikidata_query(wd_kunstwerke_query)
wikidata_df = pd.DataFrame(wd_result)
wikidata_df.head()
wikidata_df[(pd.isna(wikidata_df.geo))]
# remove entries with empty coordinates
wikidata_df = wikidata_df.dropna(subset=['geo']).reset_index(drop=True)
wikidata_df.head()
# Erstelle GeoDataFrame
#wikidata_gdf = geopandas.GeoDataFrame(wikidata_df, geometry=geopandas.points_from_xy(wikidata_df.lon, wikidata_df.lat))
wikidata_gdf = geopandas.GeoDataFrame(wikidata_df, geometry=geopandas.GeoSeries.from_wkt(wikidata_df.geo))
wikidata_gdf = wikidata_gdf.drop(columns=['geo'])
wikidata_gdf = wikidata_gdf.set_crs(wgs84)
wikidata_gdf = wikidata_gdf.dropna(subset=['geometry']).reset_index(drop=True)
wikidata_gdf.head()
# Basiskarte
wikimap = utils.base_map()
wd_layer = folium.FeatureGroup(name='WikiData: public art', show=True)
utils.style_layer(wikidata_gdf, wd_layer, color='green', icon='eye', prefix='fa')
wd_layer.add_to(wikimap)
wikimap
Nachdem wir oben gesehen haben, dass wir (Geo-)Daten via SPARQL beziehen können, gibt der folgende Exkurs einen kleinen Einblick in die Geodaten von geo.admin.ch, welche als Linked Data zur Verfügung gestellt werden.
Diese umfassen u.a.:
Wir wollen nun am Beispiel von swissBOUNDARIES zeigen, wie wir eine (stets aktuelle) Liste von Schweizer Gemeinden beziehen können.
SPARQL Query auf ld.geo.admin.ch ausführen))%0A++FILTER+NOT+EXISTS+%7B+%0A++++++%3FAdminArea+dct%3AhasVersion%2Fdct%3Aissued+%3FAusstellDatum%0A++++++filter+(%3FAusstellDatum+%3E+%3FDatum)%0A++%7D%0A%7D%0AORDER+BY+ASC(%3FGemeindeName)%0ALIMIT+3000&contentTypeConstruct=application%2Frdf%2Bjson&contentTypeSelect=application%2Fsparql-results%2Bjson&endpoint=https%3A%2F%2Fld.geo.admin.ch%2Fquery&requestMethod=POST&tabTitle=Query&headers=%7B%7D&outputFormat=table)
gemeinde_query = """
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX schema: <http://schema.org/>
PREFIX gn: <http://www.geonames.org/ontology#>
PREFIX ch: <https://ld.geo.admin.ch/def/>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX geosparql: <http://www.opengis.net/ont/geosparql#>
SELECT ?bfsNummer ?GemeindeName ?RegionName ?KantonName ?Bevoelkerung ?geoWKT
WHERE
{
?AdminArea rdf:type schema:AdministrativeArea .
?AdminArea dct:hasVersion ?Gemeinde .
?Gemeinde gn:featureCode gn:A.ADM3 .
?Gemeinde schema:name ?GemeindeName .
?Gemeinde ch:bfsNumber ?bfsNummer .
?Gemeinde gn:parentADM1 ?Kanton .
?Kanton schema:name ?KantonName .
?Gemeinde dct:issued ?Datum .
?Gemeinde schema:validUntil ?ValidDatum .
?Gemeinde gn:population ?Bevoelkerung .
?Gemeinde geosparql:hasGeometry/geosparql:asWKT ?geoWKT .
OPTIONAL {
?Gemeinde gn:parentADM2 ?Region .
?Region schema:name ?RegionName .
}
# Filter auf die aktuellste, gültige Version
FILTER (?ValidDatum >= xsd:date(NOW()))
FILTER NOT EXISTS {
?AdminArea dct:hasVersion/dct:issued ?AusstellDatum
filter (?AusstellDatum > ?Datum)
}
}
ORDER BY ASC(?GemeindeName)
LIMIT 3000
"""
gde_result = utils.geoadmin_query(gemeinde_query)
gde_df = pd.DataFrame(gde_result)
gde_df.bfsNummer = pd.to_numeric(gde_df.bfsNummer)
gde_df.Bevoelkerung = pd.to_numeric(gde_df.Bevoelkerung)
gde_df.head()
gde_gdf = geopandas.GeoDataFrame(gde_df, geometry=geopandas.GeoSeries.from_wkt(gde_df.geoWKT))
gde_gdf = gde_gdf.drop(columns=['geoWKT'])
gde_gdf = gde_gdf.set_crs(wgs84)
gde_gdf = gde_gdf.dropna(subset=['geometry']).reset_index(drop=True)
gde_gdf.head()
Im folgenden noch ein paar Beispiele, wie diese Daten visualisiert werden können.
gemeinde_map = utils.base_map(location=[46.25, 8.53], zoom=8)
folium.features.GeoJson(
gde_gdf.to_json(),
style_function=lambda x: {'fillColor': '#17a0c2', 'color': '#17a0c2'},
tooltip=folium.features.GeoJsonTooltip(
fields=['GemeindeName', 'Bevoelkerung', 'bfsNummer'],
aliases=['Gemeinde:', 'Bevölkerung:', "BFS Nummer:"],
)
).add_to(gemeinde_map)
gemeinde_map
utils.use_style('fivethirtyeight')
fig, ax = plt.subplots()
top10 = gde_gdf.nlargest(10, ['Bevoelkerung'])
top10.plot(kind='barh', y='Bevoelkerung', x="GemeindeName", label="Bevölkerung", ax=ax)
ax.legend().set_visible(False)
ax.set_ylabel('Bevölkerung')
ax.set_xlabel('Gemeinde')
ax.invert_yaxis()
ax.xaxis.set_major_formatter(ticker.EngFormatter())
plt.show()
Um das Bevölkerungswachstum zu visualisieren, holen wir zuerst die Bevölkerungszahlen (gemäss BFS-Definition) von Zürich%0ALIMIT+100&contentTypeConstruct=text%2Fturtle&contentTypeSelect=application%2Fsparql-results%2Bjson&endpoint=https%3A%2F%2Fld.geo.admin.ch%2Fquery&requestMethod=POST&tabTitle=Query+3&headers=%7B%7D&outputFormat=table).
bev_zurich_query = """
PREFIX schema: <http://schema.org/>
PREFIX gn: <http://www.geonames.org/ontology#>
PREFIX dct: <http://purl.org/dc/terms/>
SELECT ?GemeindeName ?ValidDatum ?Bevoelkerung
WHERE
{
VALUES ?AdminArea { <https://ld.geo.admin.ch/boundaries/municipality/261> } # Zürich
?AdminArea dct:hasVersion ?Gemeinde .
?Gemeinde schema:name ?GemeindeName .
?Gemeinde schema:validUntil ?ValidDatum .
?Gemeinde gn:population ?Bevoelkerung .
}
ORDER BY ASC(?ValidDatum)
LIMIT 100
"""
zh_result = utils.geoadmin_query(bev_zurich_query)
zh_df = pd.DataFrame(zh_result)
zh_df.Bevoelkerung = pd.to_numeric(zh_df.Bevoelkerung)
zh_df.ValidDatum = pd.to_datetime(zh_df.ValidDatum, format='%Y-%m-%d')
zh_df
utils.use_style('fivethirtyeight')
fig, ax = plt.subplots()
zh_df.plot(kind='line', y='Bevoelkerung', x="ValidDatum", label="Bevölkerung", ax=ax)
ax.legend().set_visible(False)
ax.set_ylim(bottom=0, top=450_000) # immer bei 0 starten
ax.set_ylabel('Bevölkerung')
ax.set_xlabel('Jahr')
plt.setp(ax.get_yticklabels()[0], visible=False)
plt.show()