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
INFO

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 in boolean_series vorkommen und dort den Wert True haben

  • df.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-Positionen

  • df.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 zwischen start und ende

    • wenn start weggelassen wird, dann wird der erste Wert eingesetzt

    • wenn ende weggelassen wird, dann wird der letzte Wert eingesetzt

    • wenn 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
ÜBUNG: DataFrames and Series

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

#
Hide 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
ÜBUNG: DataFrames Werte zuweisen

Fügen Sie drei neue Spalten zum cities DataFrame hinzu:

  1. 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
  2. length_km: Länge des Liniennetzes in km statt in Metern. Nutzen Sie danach die drop Methode um die Spalte length zu löschen
  3. status: 'planning', wenn die Länge 0 ist, sonst abhängig vom Startjahr 'new' (>= 2000), 'medium' (>= 1950), 'old' (ansonsten)
#
Hide 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

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

ÜBUNG: DataFrames kombinieren

  1. Speichern Sie das Ergebnis der obigen Operation mit den extrahierten lat und long Spalten in eine neue DataFrame-Variable
  2. Wandeln Sie Datentypen der lat und long Spalten auf float
  3. Nutzen Sie pd.concat, um dieses DataFrame "rechts" an das cities DataFrame zu hängen
  4. Speichern Sie das zusammengebaute DataFrame wieder in der cities Variable

#
Hide 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.

ÜBUNG: DataFrame abspeichern

Speichern Sie Ihre Daten in eine Datei 'cities_non_zero.csv'. Es sollen folgende Daten abgespeichert werden:

  1. Nur Cities mit einer Netzlänge > 0km. Dazu können Sie die status Spalte nutzen
  2. Nur die Spalten name, country, lat, long, start_year, age, length_km - in dieser Reihenfolge

#
Hide 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.