Hintergrund

Siehe den Blogeintrag zu Idee und Hintergrund dieses Notebooks

Benötigte Pakete laden

In [1]:
import pandas as pd
import numpy as np
import folium
import json

Daten zusammentragen

Bevölkerungsdaten

Daten zur Bevölkerung in Niederösterreich, hier beschrieben. Konkret wird diese CSV Datein verwendet.

In [2]:
FILE_BEVOELKERUNG = 'noe_pop_age_sex_2012_2015_lau2.csv'
In [3]:
bev = pd.read_csv(FILE_BEVOELKERUNG, encoding='latin-1', delimiter=';', decimal=',', skiprows=1)
bev[:3]
Out[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.

In [4]:
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]
Out[4]:
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.

In [5]:
iso_gemeinde = bev.iso_gemeinde.unique().astype('str').tolist()
iso_bezirk = bev.iso_bezirk.unique().astype('str').tolist()

GeoJSON Daten

Daten zur graphischen Darstellung von Bezirken und Gemeinden finden sich hier. Konkret verwende ich aus dieser Datei die Dateien gemeinden.json und bezirke.json.

In [6]:
FILE_GEOJSON_GEMEINDE = 'gemeinden.json'
FILE_GEOJSON_BEZIRK = 'bezirke.json'
In [7]:
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.

In [8]:
geo_json_data_gemeinde['type']
Out[8]:
'FeatureCollection'
In [9]:
geo_gemeinde = dict()
geo_bezirk = dict()
geo_gemeinde['type'] = geo_json_data_gemeinde['type']
geo_bezirk['type'] = geo_json_data_bezirk['type']
In [10]:
geo_json_data_gemeinde['features'][1]['properties']
Out[10]:
{'iso': '80214', 'iso_alt': None, 'name': 'Gaissau'}
In [11]:
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.

In [12]:
map_iso_bezirk = {data['properties']['iso']: data['properties']['name'] for data in geo_bezirk['features']}
map_iso_bezirk
Out[12]:
{'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'}
In [13]:
bev['name_bezirk'] = bev.iso_bezirk.map(map_iso_bezirk)
bev[:3]
Out[13]:
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)

Karten erstellen

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, ...

In [14]:
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
Out[14]:

Daten hinzufügen

Einwohner pro Bezirk

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.

In [15]:
df = bev[bev.YEAR == 2015].groupby(['iso_bezirk', 'name_bezirk'])[['POP_TOTAL']].sum().reset_index()
df = df.sort_values(by='POP_TOTAL')
df[:3]
Out[15]:
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
In [16]:
df[-3:]
Out[16]:
iso_bezirk name_bezirk POP_TOTAL
16 317 Moedling 116878
23 324 Wien-Umgebung 118691
5 306 Baden 141750
In [17]:
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.
Out[17]:

Frauen vs Männer

In welchem Bezirk wohnen prozentuell am meisten Frauen, wo am wenigsten?

In [18]:
bev[:3]
Out[18]:
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)
In [19]:
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]
Out[19]:
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
In [20]:
df[-3:]
Out[20]:
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.

In [21]:
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.
Out[21]: