Siehe den Blogeintrag zu Idee und Hintergrund dieses Notebooks
import pandas as pd
import numpy as np
import folium
import json
Daten zur Bevölkerung in Niederösterreich, hier beschrieben. Konkret wird diese CSV Datein verwendet.
FILE_BEVOELKERUNG = 'noe_pop_age_sex_2012_2015_lau2.csv'
bev = pd.read_csv(FILE_BEVOELKERUNG, encoding='latin-1', delimiter=';', decimal=',', skiprows=1)
bev[:3]
NUTS1 | NUTS2 | NUTS3 | LAU2_CODE | LAU2_NAME | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | |
---|---|---|---|---|---|---|---|---|---|---|
0 | AT1 | AT12 | AT124 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 |
1 | AT1 | AT12 | AT124 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 |
2 | AT1 | AT12 | AT124 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 |
Formatieren des DatenFrames: Löschen von unbenötigten Spalten, Umbenennen von Spalten, Umformatieren und Berechnen des durchschnittlichen Alters pro Altersgruppe.
bev = bev.drop(['NUTS1', 'NUTS2', 'NUTS3'], axis=1)
bev = bev.rename(columns={'LAU2_CODE':'iso_gemeinde', 'LAU2_NAME':'name_gemeinde'})
bev['iso_gemeinde'] = bev.iso_gemeinde.astype('str')
bev['iso_bezirk'] = bev.iso_gemeinde.str[0:3]
bev['alter_durchschnitt'] = bev.AGE_GROUP.str.split('_', expand=True).replace('', np.nan).astype('float').mean(axis=1)
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | |
---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 |
Erzeugen einer Liste aller Gemeinden und Bezirke (jeweils ISO code), für die Daten vorhanden sind.
iso_gemeinde = bev.iso_gemeinde.unique().astype('str').tolist()
iso_bezirk = bev.iso_bezirk.unique().astype('str').tolist()
Daten zur graphischen Darstellung von Bezirken und Gemeinden finden sich hier. Konkret verwende ich aus dieser Datei die Dateien gemeinden.json
und bezirke.json
.
FILE_GEOJSON_GEMEINDE = 'gemeinden.json'
FILE_GEOJSON_BEZIRK = 'bezirke.json'
geo_json_data_gemeinde = json.load(open(FILE_GEOJSON_GEMEINDE, encoding='latin-1'))
geo_json_data_bezirk = json.load(open(FILE_GEOJSON_BEZIRK, encoding='latin-1'))
Die GeoJSON Files enthalten Informationen zu allen Gemeinden und Bezirken in Österreich. Da ich mich hier nur Niederösterreich interessiere, erstelle ich neue Geojson Files, die nur diese Information enthalten.
Zuerst wird der Eintrag type
kopiert, dann die Einträge features
auf Niederösterreich eingeschränkt.
geo_json_data_gemeinde['type']
'FeatureCollection'
geo_gemeinde = dict()
geo_bezirk = dict()
geo_gemeinde['type'] = geo_json_data_gemeinde['type']
geo_bezirk['type'] = geo_json_data_bezirk['type']
geo_json_data_gemeinde['features'][1]['properties']
{'iso': '80214', 'iso_alt': None, 'name': 'Gaissau'}
geo_gemeinde['features'] = [data for data in geo_json_data_gemeinde['features']
if data['properties']['iso'] in iso_gemeinde]
geo_bezirk['features'] = [data for data in geo_json_data_bezirk['features']
if data['properties']['iso'] in iso_bezirk]
Um in unserem DataFrame bev
auch den Namen des Bezirks zu haben, wird zuerst, basierend auf der Information in geo_bezirk
ein Mapping zwischen Iso-Code und Bezirksname erstellt. Dieses wird dann auf den Iso-Code des Bezirks in bev
angewendet.
map_iso_bezirk = {data['properties']['iso']: data['properties']['name'] for data in geo_bezirk['features']}
map_iso_bezirk
{'301': 'Krems an der Donau (Stadt)', '302': 'St.Poelten (Stadt)', '303': 'Waidhofen an der Ybbs (Stadt)', '304': 'Wiener Neustadt (Stadt)', '305': 'Amstetten', '306': 'Baden', '307': 'Bruck an der Leitha', '308': 'Gaenserndorf', '309': 'Gmuend', '310': 'Hollabrunn', '311': 'Horn', '312': 'Korneuburg', '313': 'Krems (Land)', '314': 'Lilienfeld', '315': 'Melk', '316': 'Mistelbach', '317': 'Moedling', '318': 'Neunkirchen', '319': 'Sankt Poelten (Land)', '320': 'Scheibbs', '321': 'Tulln', '322': 'Waidhofen an der Thaya', '323': 'Wiener Neustadt (Land)', '324': 'Wien-Umgebung', '325': 'Zwettl'}
bev['name_bezirk'] = bev.iso_bezirk.map(map_iso_bezirk)
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | name_bezirk | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 | Krems an der Donau (Stadt) |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 | Krems an der Donau (Stadt) |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 | Krems an der Donau (Stadt) |
Dank folium
ist das recht einfach. Zuerst wird eine Karte erstellt, hier muss man die Koordinaten und den Zoomlevel angeben. Dann wird die Bezirksinformation aus dem GeoJSON File hinzugefügt. Und dann noch die Gemeindeinformation, wobei dort noch eine Formatierung (Farbe, Strichbreite, ..) angegeben wird. Zum Schluss zeige die Karte im Notebook an. Die Karte ist interaktiv, sprich man kann darin zoomen, verschieben, ...
m = folium.Map(location=[48.2,15.8], zoom_start=8)
folium.GeoJson(geo_bezirk).add_to(m)
folium.GeoJson(
geo_gemeinde,
style_function=lambda feature:{
'fillColor': 'red',
'color' : 'black',
'weight' : 2,
'dashArray' : '5, 5'
}
).add_to(m)
m
Nun sollen Daten zu den Karten hinzugefügt werden. Als erstes Beispiel sollen die Bezirke basierend auf der Einwohneranzahl eingefärbt werden. Auch das ist mit folium
recht einfach. Zuerst erzeuge ich einen Datenframe mit der entsprechenden Information pro Bezirk. Mit dem Befehl choropleth
lassen sich die Daten dann mit der Karte verbinden. Am Schluss speichere ich die Karte noch in einer extra html-Datei für meinen Blog.
df = bev[bev.YEAR == 2015].groupby(['iso_bezirk', 'name_bezirk'])[['POP_TOTAL']].sum().reset_index()
df = df.sort_values(by='POP_TOTAL')
df[:3]
iso_bezirk | name_bezirk | POP_TOTAL | |
---|---|---|---|
2 | 303 | Waidhofen an der Ybbs (Stadt) | 11306 |
0 | 301 | Krems an der Donau (Stadt) | 24011 |
13 | 314 | Lilienfeld | 26074 |
df[-3:]
iso_bezirk | name_bezirk | POP_TOTAL | |
---|---|---|---|
16 | 317 | Moedling | 116878 |
23 | 324 | Wien-Umgebung | 118691 |
5 | 306 | Baden | 141750 |
m = folium.Map(location=[48.2, 15.8], zoom_start=8)
m.choropleth(geo_str=geo_bezirk,
data=df,
columns=['iso_bezirk', 'POP_TOTAL'],
key_on='feature.properties.iso',
fill_color='YlOrRd', fill_opacity=0.7, line_opacity=0.3
)
m.save('01.html')
m
C:\Users\uniqu\Anaconda3\lib\site-packages\ipykernel\__main__.py:6: FutureWarning: 'threshold_scale' default behavior has changed. Now you get a linear scale between the 'min' and the 'max' of your data. To get former behavior, use folium.utilities.split_six.
In welchem Bezirk wohnen prozentuell am meisten Frauen, wo am wenigsten?
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | name_bezirk | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 | Krems an der Donau (Stadt) |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 | Krems an der Donau (Stadt) |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 | Krems an der Donau (Stadt) |
df = bev[bev.YEAR == 2015].groupby(['iso_bezirk', 'name_bezirk'])[['POP_MALE', 'POP_FEMALE']].sum().reset_index()
df['FEMALE_RATIO'] = df.POP_FEMALE / df.POP_MALE * 100
df = df.sort_values('FEMALE_RATIO')
df[:3]
iso_bezirk | name_bezirk | POP_MALE | POP_FEMALE | FEMALE_RATIO | |
---|---|---|---|---|---|
24 | 325 | Zwettl | 21524 | 21418 | 99.507526 |
19 | 320 | Scheibbs | 20518 | 20552 | 100.165708 |
4 | 305 | Amstetten | 56455 | 57050 | 101.053937 |
df[-3:]
iso_bezirk | name_bezirk | POP_MALE | POP_FEMALE | FEMALE_RATIO | |
---|---|---|---|---|---|
23 | 324 | Wien-Umgebung | 57638 | 61053 | 105.924911 |
0 | 301 | Krems an der Donau (Stadt) | 11644 | 12367 | 106.209206 |
16 | 317 | Moedling | 56153 | 60725 | 108.142040 |
Man sieht also, dass in Zwettl auf 100 Männer nur 99.5 Frauen kommen, während das in Mödling 108 sind. Auf einer Landkarte sieht das wie folgt aus.
m = folium.Map(location=[48.2, 15.8], zoom_start=8)
m.choropleth(geo_str=geo_bezirk,
data=df,
columns=['iso_bezirk', 'FEMALE_RATIO'],
key_on='feature.properties.iso',
fill_color='YlOrRd', fill_opacity=0.7, line_opacity=0.3)
m.save('02.html')
m
C:\Users\uniqu\Anaconda3\lib\site-packages\ipykernel\__main__.py:6: FutureWarning: 'threshold_scale' default behavior has changed. Now you get a linear scale between the 'min' and the 'max' of your data. To get former behavior, use folium.utilities.split_six.
In welcher Gemeinde ist das durchschnittliche Alter am höchsten, wo leben die durchschnittlich jüngsten Leute?
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | name_bezirk | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 | Krems an der Donau (Stadt) |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 | Krems an der Donau (Stadt) |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 | Krems an der Donau (Stadt) |
df = bev[bev.YEAR == 2015].groupby(['iso_gemeinde', 'name_gemeinde'])[['POP_TOTAL', 'alter_durchschnitt']].\
apply(lambda x: sum(x['POP_TOTAL'] * x['alter_durchschnitt']) / sum(x['POP_TOTAL']))
df = pd.DataFrame(df, columns=['alter_durchschnitt']).reset_index().sort_values('alter_durchschnitt')
df[:3]
iso_gemeinde | name_gemeinde | alter_durchschnitt | |
---|---|---|---|
448 | 32010 | Reinsberg | 37.579310 |
21 | 30522 | Oed-Oehling | 37.845361 |
282 | 31528 | Nöchling | 38.101289 |
df[-3:]
iso_gemeinde | name_gemeinde | alter_durchschnitt | |
---|---|---|---|
146 | 30925 | Litschau | 49.480440 |
360 | 31805 | Breitenstein | 49.651515 |
187 | 31113 | Langau | 50.958021 |
m = folium.Map(location=[48.2, 15.8], zoom_start=8)
m.choropleth(geo_str=geo_gemeinde,
data=df,
columns=['iso_gemeinde', 'alter_durchschnitt'],
key_on='feature.properties.iso',
fill_color='YlOrRd', fill_opacity=0.7, line_opacity=0.3)
m.save('03.html')
m
C:\Users\uniqu\Anaconda3\lib\site-packages\ipykernel\__main__.py:6: FutureWarning: 'threshold_scale' default behavior has changed. Now you get a linear scale between the 'min' and the 'max' of your data. To get former behavior, use folium.utilities.split_six.
In welchem Bezirk leben prozentuell die meisten Kinder (jünger als 10 Jahre)?
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | name_bezirk | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 | Krems an der Donau (Stadt) |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 | Krems an der Donau (Stadt) |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 | Krems an der Donau (Stadt) |
# Die Altersgruppe 5-9 hat das durchschnittliche Alter 7. Daher filtere ich für die Anzahl der Kinder,
# nach dem durchschnittlichen Alter <= Variable jung = 7
jung = 7
df = bev[bev.YEAR == 2015].groupby(['iso_bezirk', 'name_bezirk'])[['POP_TOTAL', 'alter_durchschnitt']].\
apply(lambda x: sum(x['POP_TOTAL'] * (x['alter_durchschnitt'] <= jung)) / sum(x['POP_TOTAL']) * 100)
df = pd.DataFrame(df, columns=['prozent_jung']).reset_index().sort_values('prozent_jung')
m = folium.Map(location=[48.2, 15.8], zoom_start=8)
m.choropleth(geo_str=geo_bezirk,
data=df,
columns=['iso_bezirk', 'prozent_jung'],
key_on='feature.properties.iso',
fill_color='YlOrRd', fill_opacity=0.7, line_opacity=0.3)
m.save('04.html')
m
C:\Users\uniqu\Anaconda3\lib\site-packages\ipykernel\__main__.py:6: FutureWarning: 'threshold_scale' default behavior has changed. Now you get a linear scale between the 'min' and the 'max' of your data. To get former behavior, use folium.utilities.split_six.
Welche Gemeinden sind am meisten gewachsten bzw. geschrumpft?
bev[:3]
iso_gemeinde | name_gemeinde | AGE_GROUP | POP_TOTAL | POP_MALE | POP_FEMALE | YEAR | iso_bezirk | alter_durchschnitt | name_bezirk | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 30101 | Krems an der Donau | 5_9 | 950 | 486 | 464 | 2015 | 301 | 7.0 | Krems an der Donau (Stadt) |
1 | 30101 | Krems an der Donau | 10_14 | 971 | 484 | 487 | 2015 | 301 | 12.0 | Krems an der Donau (Stadt) |
2 | 30101 | Krems an der Donau | 15_19 | 1184 | 591 | 593 | 2015 | 301 | 17.0 | Krems an der Donau (Stadt) |
df = bev.groupby(['iso_gemeinde', 'name_gemeinde'])[['POP_TOTAL', 'YEAR']].\
apply(lambda x:
(sum(x['POP_TOTAL'] * (x['YEAR'] == 2015)) / sum(x['POP_TOTAL'] * (x['YEAR'] == 2012)) - 1 ) * 100)
df = pd.DataFrame(df, columns=['delta_pop_total']).reset_index().sort_values('delta_pop_total')
df[:3]
iso_gemeinde | name_gemeinde | delta_pop_total | |
---|---|---|---|
170 | 31038 | Retzbach | -6.945766 |
254 | 31409 | Ramsau | -6.888634 |
190 | 31119 | Röhrenbach | -6.842105 |
df[-3:]
iso_gemeinde | name_gemeinde | delta_pop_total | |
---|---|---|---|
336 | 31701 | Achau | 12.881916 |
89 | 30802 | Andlersdorf | 13.953488 |
127 | 30858 | Untersiebenbrunn | 14.356436 |
Mit folium
kann man die Farbpalette genau angeben. Hier verwenden wir einen Verlauf von rot (starker Rückgang) über weiß hin zu grün (starkes Wachstum).
colormap = folium.colormap.LinearColormap(('red', 'white', 'green'),
index=(df.delta_pop_total.min(), 0, df.delta_pop_total.max()),
vmin = df.delta_pop_total.min(), vmax=df.delta_pop_total.max())
colormap.caption = 'Bevölkerungsentwicklung in %, 2012 vs 2015'
colormap
Um die Karte zu erstellen, benötigt man eine Funktion, die basierend auf dem ISO-Code der Gemeinde die entsprechende Farbe ausgibt.
def colormap_iso(iso):
delta_pop_total = float(df.loc[df.iso_gemeinde == iso, 'delta_pop_total'])
return colormap(delta_pop_total)
colormap_iso('30506')
'#fff5f5'
geo_gemeinde['features'][0]['properties']['iso']
'30506'
Damit sich das Grün der Farbpalette nicht mit dem Grün in der Karte verwende ich diesmal einen anderen Kartentyp.
m = folium.Map(location=[48.2, 15.8], zoom_start=8, tiles='cartodbpositron')
folium.GeoJson(geo_gemeinde,
style_function=lambda feature: {
'fillColor': colormap_iso(feature['properties']['iso']),
'color' : 'black',
'weight' : 2,
'dashArray' : '5, 5'
}).add_to(m)
m.add_children(colormap)
m.save('05.html')
m