4. Einführung in pandas#
pandas ist eine mächtige Bibliothek für die Arbeit mit heterogenen Daten in tabellenartiger Form. In pandas können wir Daten laden, säubern, transformieren, aggregieren und analysieren. Diese Schritte kommen vor der weiteren Verarbeitung in Machine Learning Algorithmen oder der weitergehenden Visualisierung. Das Ergebnis eines pandas-Programms ist häufig ein homogenes NumPy-Array mit numerischen Werten. Gemeinsam mit NumPy ist der array-basierte Ansatz - eine For-Schleife über die Reihen eines Datensatzes verstößt mit hoher Wahrscheinlichkeit gegen Best Practices.
Um pandas in Python zu nutzen bietet es sich an der Standard-Konvention zu folgen und es als pd
zu importieren:
import pandas as pd
Die Übungen und Beispiele basieren auf Daten über weltweite Systeme des öffentlichen Nahverkehrs von https://www.citylines.co
4.1. pandas Series#
Die grundlegende Datenstruktur in pandas ist die Series: eine eindimensionale Datenstruktur ähnlich eines eindimensionalen Arrays:
countries = pd.Series(['Sweden', 'Australia', 'Singpaore', 'Austria'])
countries
0 Sweden
1 Australia
2 Singpaore
3 Austria
dtype: object
Links neben den Einträgen sehen wir den Index - da wir nichts anderes angegeben haben, werden ähnlich wie beim Array die ganzen Zahlen aufsteigend nummeriert von 0 an genommen. Mit .values
können wir das NumPy-Array einer Series abfragen:
countries.values
array(['Sweden', 'Australia', 'Singpaore', 'Austria'], dtype=object)
type(countries.values)
numpy.ndarray
Mit .index
können wir auf den Index der Series zugreifen. Mit []
können wir per Index auf einzelne Elemente der Series zugreifen.
countries.index
RangeIndex(start=0, stop=4, step=1)
countries.index.values
array([0, 1, 2, 3])
countries[0]
'Sweden'
Anders als die Indizes eines Arrays bleiben die Indizes einer Series auch bei Selektionen erhalten:
countries[2:3]
2 Singpaore
dtype: object
countries[[0,2]]
0 Sweden
2 Singpaore
dtype: object
Ebenso können ähnlich wie beim dict
andere Index-Elemente gesetzt werden, z.B.
countries = pd.Series(['Sweden', 'Australia', 'Singapore', 'Austria'],
index=['Stockholm', 'Sydney', 'Singapore', 'Vienna'])
countries
Stockholm Sweden
Sydney Australia
Singapore Singapore
Vienna Austria
dtype: object
Bei nicht-numerischen Index ist die Range-Selection über :
sowohl bei Start als auch Ende inklusive:
countries['Stockholm':'Singapore']
Stockholm Sweden
Sydney Australia
Singapore Singapore
dtype: object
Series können wie NumPy-Arrays miteinander kombiniert werden, dabei werden einzelne Werte über die Indizes gematcht, z.B.:
# Erstelle Series mit anderer Reihenfolge und einem Datensatz weniger
continents = pd.Series(['Europe', 'Europe', 'Asia'],
index=['Vienna', 'Stockholm', 'Singapore'])
# Kombiniere Series countries mit continents per String-Konkatenation
countries + ', ' + continents
Singapore Singapore, Asia
Stockholm Sweden, Europe
Sydney NaN
Vienna Austria, Europe
dtype: object
Series bieten neben den aus NumPy bekannten Funktionen (z.B. max
, min
) noch viele weitere: https://pandas.pydata.org/docs/reference/series.html
Zum Beispiel kann mit dem str
-Accessor auf viele String-Funktionen zurückgegriffen werden:
countries.str.startswith('S')
Stockholm True
Sydney False
Singapore True
Vienna False
dtype: bool
Das Ergebnis der vorherigen Operation ist wiederum eine Series mit dem gleichen Index und Boolean-Werten. Diese Series kann nun wiederum als Index für ein Array mit gleichen Indizes verwendet werden:
countries[countries.str.startswith('S')]
Stockholm Sweden
Singapore Singapore
dtype: object
filt_s = countries.str.startswith('S')
continents[filt_s]
Stockholm Europe
Singapore Asia
dtype: object
4.2. pandas DataFrame#
Ein DataFrame
bündelt mehrere Series
in eine tabellarische Datenstruktur zusammen. Jede Series
wird unter einem Label (Spaltenname) abgelegt und beinhaltet die Wert einer Spalte. Die Series haben im Normalfall den gleichen Index (oder zumindest überschneidend). Die Werte aus den unterschiedlichen Series mit dem gleichen Index bilden eine Reihe.
Ein DataFrame kann z.B. über ein dict erstellt werden - key ist der Spaltenname, value die Series mit den Werten:
df = pd.DataFrame({"country": countries, "continent": continents})
df
country | continent | |
---|---|---|
Singapore | Singapore | Asia |
Stockholm | Sweden | Europe |
Sydney | Australia | NaN |
Vienna | Austria | Europe |
Es gibt zahlreiche weitere Konstruktoren für DataFrames, z.B. basierend auf Arrays, Dicts. Siehe https://pandas.pydata.org/docs/user_guide/dsintro.html#dataframe
Wir werden meist DataFrames aus Dateien laden. Diese können in unterschiedlichen Formaten sein (z.B. csv, Excel, JSON) und sowohl auf dem lokalen Dateisystem als auch auf einem Webserver liegen. Siehe https://pandas.pydata.org/docs/user_guide/io.html
# Read city data direkt von citylines.co
# cities = pd.read_csv("https://data.heroku.com/dataclips/wmeilvvkgqrderovlbhfbktsnxlm.csv")
# Wir lesen aus einer lokal gecachten Version, um den gleichen
# Datenstand für Skript und Übungen festzuhalten
cities = pd.read_csv("data/cities.csv")
# Jupyer Notebook stellt DataFrames in einer gewohnten tabellarischen Ansicht dar
cities
id | name | coords | start_year | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|---|
0 | 5 | Aberdeen | POINT(-2.15 57.15) | 2017 | aberdeen | Scotland | NaN | 0 |
1 | 6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | adelaide | Australia | NaN | 0 |
2 | 7 | Algiers | POINT(3 36.83333333) | 2017 | algiers | Algeria | NaN | 0 |
3 | 9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | ankara | Turkey | NaN | 0 |
4 | 16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | belem | Brazil | NaN | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
397 | 360 | Santa Fe | POINT(-60.7 -31.633333) | 2008 | santa-fe-argentina | Argentina | Santa Fe | 9939 |
398 | 361 | Tegal | POINT(109.133333 -6.866667) | 2020 | tegal | Indonesia | Central Java | 51496 |
399 | 342 | Istanbul | POINT(28.955 41.013611) | 1989 | istanbul | Turkey | NaN | 38096 |
400 | 356 | Seoul | POINT(126.977966 37.566536) | 1971 | seoul | South Korea | NaN | 1418234 |
401 | 114 | Tokyo | POINT(139.75 35.66666667) | 1872 | tokyo | Japan | NaN | 4770864 |
402 rows × 8 columns
# Da DataFrames oftmals sehr viele Zeilen haben bietet sich die head-Methode an
# Sie zeigt die ersten n Zeilen an - Default-Wert ist n=5
cities.head(10)
id | name | coords | start_year | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|---|
0 | 5 | Aberdeen | POINT(-2.15 57.15) | 2017 | aberdeen | Scotland | NaN | 0 |
1 | 6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | adelaide | Australia | NaN | 0 |
2 | 7 | Algiers | POINT(3 36.83333333) | 2017 | algiers | Algeria | NaN | 0 |
3 | 9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | ankara | Turkey | NaN | 0 |
4 | 16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | belem | Brazil | NaN | 0 |
5 | 10 | Asunción | POINT(-57.66666667 -25.25) | 2017 | asuncion | Paraguay | NaN | 0 |
6 | 395 | Málaga | POINT(-4.416667 36.716667) | 2020 | malaga | Spain | Andalucía | 0 |
7 | 12 | Auckland | POINT(174.75 -36.86666667) | 2017 | auckland | New Zealand | NaN | 0 |
8 | 407 | Tel Aviv | POINT(34.783333 32.066667) | 2021 | tel-aviv | Israel | NaN | 0 |
9 | 17 | Belfast | POINT(-5.933333333 54.61666667) | 2017 | belfast | Northern Ireland | NaN | 0 |
pandas hat beim Einlesen automatisch einen numerischen Index vergeben (ganz linke Spalte ohne Spaltenname). Im Datensatz ist jedoch eine schon eine Spalte “id” vorhanden, die als Index dienen kann. Im Prinzip könnten wir auch den Namen der Städte als Index setzen, aber wenn wir die Stadttabelle mit weiteren Tabellen verknüpfen wollen wird dort die city id als Schlüssel verwendet. Mit set_index können wir eine Spalte des DataFrames als Index setzen. Wie die meisten anderen transformativen Methoden auf einem DataFrame verändert die set_index Methode standardmäßig das DataFrame nicht, sondern generiert ein neues. Dies können wir übernehmen, indem wir den inplace=True Parameter setzen oder das neue generierte DataFrame wiederum in der ursprünglichen Variable speichern.
# Erzeugt ein neues DataFrame mit der Spalte id als Index
cities.set_index('id').head()
name | coords | start_year | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|
id | |||||||
5 | Aberdeen | POINT(-2.15 57.15) | 2017 | aberdeen | Scotland | NaN | 0 |
6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | adelaide | Australia | NaN | 0 |
7 | Algiers | POINT(3 36.83333333) | 2017 | algiers | Algeria | NaN | 0 |
9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | ankara | Turkey | NaN | 0 |
16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | belem | Brazil | NaN | 0 |
# Das DataFrame cities ist unverändert:
cities.head()
id | name | coords | start_year | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|---|
0 | 5 | Aberdeen | POINT(-2.15 57.15) | 2017 | aberdeen | Scotland | NaN | 0 |
1 | 6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | adelaide | Australia | NaN | 0 |
2 | 7 | Algiers | POINT(3 36.83333333) | 2017 | algiers | Algeria | NaN | 0 |
3 | 9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | ankara | Turkey | NaN | 0 |
4 | 16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | belem | Brazil | NaN | 0 |
# Die Änderung übernehmen können wir entweder (nur eins funktioniert - danach
# ist der Index schon gesetzt und die Spalte id nicht mehr vorhanden, daher
# ist Option 1 auskommentiert
# Option 1: Abspeichern des transformierten DataFrames in der ursprünglichen Variable
# cities = cities.set_index('id')
# Option 2: inplace=True Parameter bestimmt, dass das DataFrame verändert werden soll
cities.set_index('id', inplace=True)
Beachten Sie, dass Operationen mit inplace=True keinen Rückgabewert haben. Das sehen Sie daran, dass im Jupyter Notebook nichts angezeigt wird.
4.3. Selektionen#
Auf einzelne Spalten (Series) kann per [spalten_name]
zugegriffen werden.
cities['name']
id
5 Aberdeen
6 Adelaide
7 Algiers
9 Ankara
16 Belém
...
360 Santa Fe
361 Tegal
342 Istanbul
356 Seoul
114 Tokyo
Name: name, Length: 402, dtype: object
Alternativ für Namen ohne Leer- und Sonderzeichen kann auch per .spalten_name
zugegriffen werden - wir vermeiden das im Folgenden aber Sie werden es häufig in Beispiel-Code finden.
cities.name
id
5 Aberdeen
6 Adelaide
7 Algiers
9 Ankara
16 Belém
...
360 Santa Fe
361 Tegal
342 Istanbul
356 Seoul
114 Tokyo
Name: name, Length: 402, dtype: object
Ähnlich wie bei Series können Selektionen mit []
durchgeführt werden. Da pandas hier gemäß einer Logik erschließt, ob Selektionen auf Spaltennamen, Zeilen-Labels (aus dem Index) oder Zeilen-Positionen gewünscht sind, kommt es manchmal zu unerwünschten Ergebnissen, insbesondere bei Spaltennamen die Zahlen sind. Beispiel:
# DataFrame mit Zahlen als Index-Labels und Spaltennamen
df = pd.DataFrame({0: [1, 2, 3], 1: [4, 5, 6], 2: [7, 8, 9]}, index=[1, 2, 3])
df
0 | 1 | 2 | |
---|---|---|---|
1 | 1 | 4 | 7 |
2 | 2 | 5 | 8 |
3 | 3 | 6 | 9 |
# Zugriff auf eine Spalte mit []
df[0]
1 1
2 2
3 3
Name: 0, dtype: int64
# Zugriff auf Zeile per Label-Slice mit []
df[0:1]
0 | 1 | 2 | |
---|---|---|---|
1 | 1 | 4 | 7 |
Um Missverständnisse zu vermeiden, verwenden wir folgende Zugriffe auf ein DataFrame df:
df[spalten]
Zugriff auf einzelne Spalte (Series) oder Spalten (DataFrame) per Spaltenname(n)df[boolean_series]
um auf alle Zeilen zuzugreifen, deren Index inboolean_series
vorkommen und dort den WertTrue
habendf.loc[zeilen]
: Zugriff auf einzelne Zeile oder Zeilen per Label(s)df.loc[zeilen,spalten]
: Zugriff auf Zeilen und Spalten per Label(s) und Spaltenname(n)df.iloc[zeilen]
: Zugriff auf einzelne Zeile oder Zeilen per Integer-Positionendf.iloc[zeilen,spalten]
: Zugriff auf Zeilen und Spalten per Integer-Positionen
Um Spalten, Zeilen auszuwählen gibt es 3 Möglichkeiten:
Einzelner Wert (Spaltenname, Index-Label, Integer-Position): selektiert genau diesen Wert
Liste von Spaltennamen, Index-Labeln, Integer-Positionen: selektiert, wenn Wert in Liste
Slices mit
start:ende
: selektiert zusammenhängenden Bereich zwischenstart
undende
wenn
start
weggelassen wird, dann wird der erste Wert eingesetztwenn
ende
weggelassen wird, dann wird der letzte Wert eingesetztwenn beides weggelassen wird, wird der gesamte Bereich selektiert
Beispiele:
# Einzelne Spalte als Series
cities['name']
id
5 Aberdeen
6 Adelaide
7 Algiers
9 Ankara
16 Belém
...
360 Santa Fe
361 Tegal
342 Istanbul
356 Seoul
114 Tokyo
Name: name, Length: 402, dtype: object
# DataFrame mit den Spalten in der Liste (in der angegeben Reihenfolge)
cities[['country', 'name']].head()
country | name | |
---|---|---|
id | ||
5 | Scotland | Aberdeen |
6 | Australia | Adelaide |
7 | Algeria | Algiers |
9 | Turkey | Ankara |
16 | Brazil | Belém |
# Boolean-Series (Ergebnis einer Series Operation)
cities[cities['start_year'] < 1821]
name | coords | start_year | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|
id | |||||||
139 | Boston | POINT(-71.08333333 42.35) | 1806 | boston | United States | Mass. | 615505 |
206 | New York | POINT(-73.96666667 40.78333333) | 1817 | new-york | United States | N.Y. | 1089854 |
# Auswahl per Label-Ranges sowohl bei Zeilen als auch Spalten
cities.loc[300:305, 'start_year':'country']
start_year | url_name | country | |
---|---|---|---|
id | |||
300 | 1996 | montpellier | France |
228 | 1997 | salt-lake-city | United States |
261 | 1908 | concepcion | Chile |
256 | 1988 | valencia | Spain |
305 | 2009 | dijon | France |
# Selektiere die letzten beiden Zeilen und geraden Spaltennummern
cities.iloc[-3:-1,[0, 2, 4, 6]]
name | start_year | country | length | |
---|---|---|---|---|
id | ||||
342 | Istanbul | 1989 | Turkey | 38096 |
356 | Seoul | 1971 | South Korea | 1418234 |
Geben Sie alle Städte aus, die (a) eine Netzlänge von über 500km haben und (b) nicht in Japan sind. Beachten Sie, dass die Spalten des DataFrames wiederum Series sind. Dabei sollen neben dem Stadtnamen auch die Start-Jahr, das Land und die Länge ausgegeben werden
#
Show code cell content
# Build filters on individual columns/series
filt_len = cities['length'] > 500000
filt_JPN = cities['country'] == 'Japan'
# Combine filters
filt = filt_len & ~filt_JPN
# Oder greife auf ganzen DataFrame mit boolean-Series zu:
cities.loc[filt, ['name', 'start_year', 'country', 'length']]
name | start_year | country | length | |
---|---|---|---|---|
id | ||||
139 | Boston | 1806 | United States | 615505 |
206 | New York | 1817 | United States | 1089854 |
364 | Chengdu | 2006 | China | 553993 |
32 | Guangzhou | 1997 | China | 681074 |
48 | Glasgow | 1891 | Scotland | 528652 |
22 | Mumbai | 1910 | India | 601802 |
1 | Buenos Aires | 1854 | Argentina | 1141979 |
27 | Brussels | 2017 | Belgium | 530739 |
71 | Madrid | 1869 | Spain | 577574 |
69 | London | 1833 | England | 1663529 |
59 | Jakarta | 1871 | Indonesia | 515337 |
15 | Beijing | 1961 | China | 1092665 |
79 | Milan | 1840 | Italy | 523871 |
14 | Barcelona | 1848 | Spain | 775034 |
106 | São Paulo | 1860 | Brazil | 749495 |
107 | Shanghai | 1990 | China | 954685 |
356 | Seoul | 1971 | South Korea | 1418234 |
4.4. Werte zuweisen#
Sämtliche Zugriffsarten liefern eine View
auf das originale DataFrame. Das heißt, dass Änderungen auf der View auch im DatenFrame wiederzufinden sind. Eine Zuweisung funktioniert auch, wenn die Selektion bisher nicht im DataFrame vorhanden ist.
Beispiele:
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6], 'c': [7, 8, 9]})
df
a | b | c | |
---|---|---|---|
0 | 1 | 4 | 7 |
1 | 2 | 5 | 8 |
2 | 3 | 6 | 9 |
# Neue Spalte
df['d'] = [10, 11, 12]
df
a | b | c | d | |
---|---|---|---|---|
0 | 1 | 4 | 7 | 10 |
1 | 2 | 5 | 8 | 11 |
2 | 3 | 6 | 9 | 12 |
# Zuweisung für zweidimensional selektierten Bereich
df.loc[0:1, 'a':'b'] = [[100, 101], [102, 103]]
df
a | b | c | d | |
---|---|---|---|---|
0 | 100 | 101 | 7 | 10 |
1 | 102 | 103 | 8 | 11 |
2 | 3 | 6 | 9 | 12 |
Wenn einer bestimmten Selektion ein Skalar oder ein Array von kleinerer Dimension zugewiesen wird, dann versucht pandas per Broadcasting
die entsprechende Zuordnung vorzunehmen. Wir verwenden das vor allem, wenn wir einer Spalte einen konstanten Wert zuweisen wollen, z.B.
df['e'] = 1001
df
a | b | c | d | e | |
---|---|---|---|---|---|
0 | 100 | 101 | 7 | 10 | 1001 |
1 | 102 | 103 | 8 | 11 | 1001 |
2 | 3 | 6 | 9 | 12 | 1001 |
Fügen Sie drei neue Spalten zum cities DataFrame hinzu:
- age: Alter des Schienennetzes (aktuelles Jahr - start_year). Die Spalte soll direkt nach der start_year Spalte eingefügt werden. Nutzen Sie dazu die insert Methode des DataFrames
- length_km: Länge des Liniennetzes in km statt in Metern. Nutzen Sie danach die drop Methode um die Spalte length zu löschen
- status: 'planning', wenn die Länge 0 ist, sonst abhängig vom Startjahr 'new' (>= 2000), 'medium' (>= 1950), 'old' (ansonsten)
#
Show code cell content
# Spalte age:
# - Aktuells Jahr
from datetime import date
year = date.today().year
# - Position von start_year + 1
pos = cities.columns.get_loc('start_year') + 1
# - Berechnen der Daten
age = year - cities['start_year']
# - Einfügen
cities.insert(pos, 'age', age)
cities
name | coords | start_year | age | url_name | country | country_state | length | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
5 | Aberdeen | POINT(-2.15 57.15) | 2017 | 7 | aberdeen | Scotland | NaN | 0 |
6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | 7 | adelaide | Australia | NaN | 0 |
7 | Algiers | POINT(3 36.83333333) | 2017 | 7 | algiers | Algeria | NaN | 0 |
9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | 7 | ankara | Turkey | NaN | 0 |
16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | 7 | belem | Brazil | NaN | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... |
360 | Santa Fe | POINT(-60.7 -31.633333) | 2008 | 16 | santa-fe-argentina | Argentina | Santa Fe | 9939 |
361 | Tegal | POINT(109.133333 -6.866667) | 2020 | 4 | tegal | Indonesia | Central Java | 51496 |
342 | Istanbul | POINT(28.955 41.013611) | 1989 | 35 | istanbul | Turkey | NaN | 38096 |
356 | Seoul | POINT(126.977966 37.566536) | 1971 | 53 | seoul | South Korea | NaN | 1418234 |
114 | Tokyo | POINT(139.75 35.66666667) | 1872 | 152 | tokyo | Japan | NaN | 4770864 |
402 rows × 8 columns
Show code cell content
# Spalte length_km
cities['length_km'] = cities['length'] / 1000.0
cities.drop(columns=['length'], inplace=True)
cities.tail()
name | coords | start_year | age | url_name | country | country_state | length_km | |
---|---|---|---|---|---|---|---|---|
id | ||||||||
360 | Santa Fe | POINT(-60.7 -31.633333) | 2008 | 16 | santa-fe-argentina | Argentina | Santa Fe | 9.939 |
361 | Tegal | POINT(109.133333 -6.866667) | 2020 | 4 | tegal | Indonesia | Central Java | 51.496 |
342 | Istanbul | POINT(28.955 41.013611) | 1989 | 35 | istanbul | Turkey | NaN | 38.096 |
356 | Seoul | POINT(126.977966 37.566536) | 1971 | 53 | seoul | South Korea | NaN | 1418.234 |
114 | Tokyo | POINT(139.75 35.66666667) | 1872 | 152 | tokyo | Japan | NaN | 4770.864 |
Show code cell content
# Spalte status
# Starten mit Standardwert
cities['status'] = 'old'
# Dann immer spezifischer überschreiben
cities.loc[cities['start_year'] >= 1950, 'status'] = 'medium'
cities.loc[cities['start_year'] >= 2000, 'status'] = 'new'
cities.loc[cities['length_km'] == 0, 'status'] = 'planning'
cities
name | coords | start_year | age | url_name | country | country_state | length_km | status | |
---|---|---|---|---|---|---|---|---|---|
id | |||||||||
5 | Aberdeen | POINT(-2.15 57.15) | 2017 | 7 | aberdeen | Scotland | NaN | 0.000 | planning |
6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | 7 | adelaide | Australia | NaN | 0.000 | planning |
7 | Algiers | POINT(3 36.83333333) | 2017 | 7 | algiers | Algeria | NaN | 0.000 | planning |
9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | 7 | ankara | Turkey | NaN | 0.000 | planning |
16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | 7 | belem | Brazil | NaN | 0.000 | planning |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
360 | Santa Fe | POINT(-60.7 -31.633333) | 2008 | 16 | santa-fe-argentina | Argentina | Santa Fe | 9.939 | new |
361 | Tegal | POINT(109.133333 -6.866667) | 2020 | 4 | tegal | Indonesia | Central Java | 51.496 | new |
342 | Istanbul | POINT(28.955 41.013611) | 1989 | 35 | istanbul | Turkey | NaN | 38.096 | medium |
356 | Seoul | POINT(126.977966 37.566536) | 1971 | 53 | seoul | South Korea | NaN | 1418.234 | medium |
114 | Tokyo | POINT(139.75 35.66666667) | 1872 | 152 | tokyo | Japan | NaN | 4770.864 | old |
402 rows × 9 columns
4.5. Beispiel Data Wrangling#
Eine Hauptaufgabe von pandas ist die Daten aus diversen Input-Formaten in das passende Output-Format zu bringen. Unser bisheriger Datensatz ist recht wohlgeformt aber in der Praxis sind die Input-Formate häufig recht schwer auslesbar und es muss mit den Daten gerangelt/gestritten werden, um sie ins richtige Format zu bringen - daher der Begriff Data Wrangling.
In unserem Datensatz müssen wir uns um die Spalte coords kümmern, die lattitude und longitude zusammengepackt in String-Form enthält. Wir werden schrittweise vorgehen:
# Entfernen von POINT und den Klammern
# Der str-Accessor der coords-Series bietet entsprechende Funktionen
# Per Slice mit Integer-Positionen
cities['coords'].str.slice(6,-2)
id
5 -2.15 57.1
6 138.6 -34.9166666
7 3 36.8333333
9 32.91666667 39.9166666
16 -48.48333333 -1.46666666
...
360 -60.7 -31.63333
361 109.133333 -6.86666
342 28.955 41.01361
356 126.977966 37.56653
114 139.75 35.6666666
Name: coords, Length: 402, dtype: object
# Per Ersetzen
cities['coords'].str.replace(pat='POINT(',repl='',regex=False).str.replace(pat=')',repl='', regex=False)
id
5 -2.15 57.15
6 138.6 -34.91666667
7 3 36.83333333
9 32.91666667 39.91666667
16 -48.48333333 -1.466666667
...
360 -60.7 -31.633333
361 109.133333 -6.866667
342 28.955 41.013611
356 126.977966 37.566536
114 139.75 35.66666667
Name: coords, Length: 402, dtype: object
# Beide Ergebnisse beinhalten nun beide Koordinaten per Leerzeichen getrennt
# Hier hilft uns die str.split Methode weiter:
cities['coords'].str.slice(6,-2).str.split(' ', expand=True)
0 | 1 | |
---|---|---|
id | ||
5 | -2.15 | 57.1 |
6 | 138.6 | -34.9166666 |
7 | 3 | 36.8333333 |
9 | 32.91666667 | 39.9166666 |
16 | -48.48333333 | -1.46666666 |
... | ... | ... |
360 | -60.7 | -31.63333 |
361 | 109.133333 | -6.86666 |
342 | 28.955 | 41.01361 |
356 | 126.977966 | 37.56653 |
114 | 139.75 | 35.6666666 |
402 rows × 2 columns
# Beide Schritte inklusive Benennung der Spalten, können auch
# per Regular-Expression Matching durchgeführt werden
cities['coords'].str.extract(r'(?P<long>[-+0-9.]+) (?P<lat>[-+0-9.]+)')
long | lat | |
---|---|---|
id | ||
5 | -2.15 | 57.15 |
6 | 138.6 | -34.91666667 |
7 | 3 | 36.83333333 |
9 | 32.91666667 | 39.91666667 |
16 | -48.48333333 | -1.466666667 |
... | ... | ... |
360 | -60.7 | -31.633333 |
361 | 109.133333 | -6.866667 |
342 | 28.955 | 41.013611 |
356 | 126.977966 | 37.566536 |
114 | 139.75 | 35.66666667 |
402 rows × 2 columns
Das Ergebnis der letzten Operation ist ein DataFrame mit den zwei neuen Spalten lat und long mit dem passenden Index zu unserem cities DataFrame. Um nun beide DataFrames in eins zu packen verwenden wir die pd.concat Funktion. Die Funktion hat viele Möglichkeiten, wir nutzen Sie, um zwei DataFrames “nebeneinander” zu setzen, so dass das neue DataFrame die Spalten beider DataFrames besitzt. Die Funktion verändert die übergebenen DataFrames nicht, sondern liefert ein neues DataFrame zurück.
- Speichern Sie das Ergebnis der obigen Operation mit den extrahierten lat und long Spalten in eine neue DataFrame-Variable
- Wandeln Sie Datentypen der lat und long Spalten auf float
- Nutzen Sie pd.concat, um dieses DataFrame "rechts" an das cities DataFrame zu hängen
- Speichern Sie das zusammengebaute DataFrame wieder in der cities Variable
#
Show code cell content
# Abspeichern in DataFrame-Variable
coords = cities['coords'].str.extract(r'(?P<long>[-+0-9.]+) (?P<lat>[-+0-9.]+)')
# Umwandeln in Float-Zahlen
coords = coords.astype('float')
# Zusammenfügen und in cities speichern
cities = pd.concat([cities, coords], axis=1)
cities.head()
name | coords | start_year | age | url_name | country | country_state | length_km | status | long | lat | |
---|---|---|---|---|---|---|---|---|---|---|---|
id | |||||||||||
5 | Aberdeen | POINT(-2.15 57.15) | 2017 | 7 | aberdeen | Scotland | NaN | 0.0 | planning | -2.150000 | 57.150000 |
6 | Adelaide | POINT(138.6 -34.91666667) | 2017 | 7 | adelaide | Australia | NaN | 0.0 | planning | 138.600000 | -34.916667 |
7 | Algiers | POINT(3 36.83333333) | 2017 | 7 | algiers | Algeria | NaN | 0.0 | planning | 3.000000 | 36.833333 |
9 | Ankara | POINT(32.91666667 39.91666667) | 2017 | 7 | ankara | Turkey | NaN | 0.0 | planning | 32.916667 | 39.916667 |
16 | Belém | POINT(-48.48333333 -1.466666667) | 2017 | 7 | belem | Brazil | NaN | 0.0 | planning | -48.483333 | -1.466667 |
4.6. Daten speichern#
pandas kann nicht nur Daten einlesen, sondern auch wieder in diverse Format abspeichern, siehe https://pandas.pydata.org/docs/reference/io.html
Beim Abspeichern in eine Datei sollten Sie beachten, dass existierende Dateien ohne Nachfrage überschrieben werden. Es ist immer eine gute Idee, die original Daten zu behalten, um Analysen nachvollziehbar zu machen und bei Bedarf verbessern zu können.
Speichern Sie Ihre Daten in eine Datei 'cities_non_zero.csv'. Es sollen folgende Daten abgespeichert werden:
- Nur Cities mit einer Netzlänge > 0km. Dazu können Sie die status Spalte nutzen
- Nur die Spalten name, country, lat, long, start_year, age, length_km - in dieser Reihenfolge
#
Show code cell content
cities_non_zero = cities[cities['status'] != 'planning']
cities_non_zero[['name', 'country', 'lat', 'long', 'start_year', 'age', 'length_km']].to_csv('data/cities_non_zero.csv')
4.7. Abschluss#
Wir haben nun erste Transformationen und Filterungen mit pandas durchgeführt und das Ergebnis in einer Datei abgespeichert. Nachdem wir die Grundlagen von pandas kennengelernt haben, werden wir uns im Folgenden eher aus Richtung der Fragestellung zu den benötigten pandas-Funktionen annähern, anstatt diese systematisch durchzugehen.