18. Pandas#
Numpy ist das Python-Tool wenn es um den Umgang mit großen Zahlenmengen geht. Aber für viele Data Science-artige Aufgaben fehlen bestimmte Dinge. Und genau darum gibt es Pandas (das intern u.a. Numpy Arrays benutzt):
Heterogene Datentypen innerhalb einer Tabelle
Labels
Mehr Möglichkeiten zum Umgang mit fehlenden Werten
Mehr Datentypen
Pandas (der Name steht für Panel Data) wird üblicherweise wie folgt importiert:
import pandas as pd
In Numpy haben wir uns v.a. mit den Numpy Arrays beschäftigt, der zentrale Datentyp in Numpy.
In Pandas gibt es zwei Datentypen mit denen wir arbeiten werden: DataFrame und Series. Wir starten mit ersterem.
import pandas as pd
import numpy as np
my_dict = {
"city": ["Essen", "Bochum", "Dortmund"],
"inhabitants": [591032, 364454, 587696],
"xyz": np.random.random(3)
}
df = pd.DataFrame(my_dict, index=["a", "b", "c"])
18.1. Jupyter Notebook#
Bisher haben wir unseren Python Code entweder in einer Konsole geschrieben (eigentlich nur test-weise), oder eben hauptsächlich in einer Entwicklungsumgebung wie Spyder. Letzteres ist und bleibt auch der bevorzugte Weg um umfangreicheren Code zu entwickeln.
Allerdings gibt es noch eine alternative Umgebung in der sehr gut mit Python gearbeitet werden kann und die sich besonders für Data Science Aufgaben, bzw. erste Erkundungen von Datensets eignet: Jupyter Notebook. Die ganzen folgenden Code-Beispiele werden darum auch in solchen Notebooks dargestellt.
Jupyter Notebook ist Teil der Anaconda-Installation und erzeugt Notebook-Dateien die mit .ipynb enden. Jupyter Notebook kann über das Startmenü (unter Anaconda) gestartet werden, oder über den Befehl jupyter notebook aus dem Terminal.
In der Vorlesung wird die Verwendung von Jupyter notebooks vorgeführt und wir werden uns in der nächsten Übung auch damit beschäftigen. Ansonsten gibt es im Internet viel Material zur Benutzung der notebooks, z.B. hier: https://realpython.com/jupyter-notebook-introduction/.
import pandas as pd
import numpy as np
my_list = [
["Essen", 591032, np.random.random(1)[0]],
["Bochum", 364454, np.random.random(1)[0]],
["Dortmund", 587696, np.random.random(1)[0]]
]
df = pd.DataFrame(
my_list,
columns=["city", "inhabitants", "xyz"],
index=["a", "b", "c"]
)
18.2. Slicing und Indexing#
Slicing und Indexing, also das gezielte „zerschneiden“ (slicing) und Adressieren (indexing) von Tabellen um nur bestimmte Daten wiederzugeben ist eines der zentralen Elemente bei der Arbeit mit größeren Datenmengen. Bei Numpy haben wir schon gesehen, wie mächtig, aber auch wie schwierig das sein kann. In Pandas gibt es sogar oft noch mehr Optionen und es kann schnell etwas unübersichtlich werden. Allerdings: Gute Slicing/Indexing-Skills in Pandas werden ziemlich schnell zu echten Superkräften wenn es um Data Science geht!
OK, aber starten wir erstmal einfach.
Wir haben gerade gesehen, dass ein Dictionary in ein DataFrame umgesetzt werden kann. Und die Inhalte können auch auf eine Ähnliche Art abgerufen werden, nämlich über die Spaltennamen (entweder einzeln oder als Liste):
df["inhabitants"]
cols = ["city", "inhabitants"]
df[cols]
| city | inhabitants | |
|---|---|---|
| a | Essen | 591032 |
| b | Bochum | 364454 |
| c | Dortmund | 587696 |
Um bestimmte Zeilen auszuwählen ist es möglich .loc zu nutzen, also .loc["a"] für eine Zeile oder eine Liste oder Range für mehrere Zeilen
df = pd.DataFrame({
"city": ["Essen", "Bochum", "Dortmund"],
"inhabitants": [591032, 364454, 587696],
"xyz": np.random.random(3)},
index=["a", "b", "c"])
df.loc["a"]
# oder über liste
rows = ["a", "c"]
df.loc[rows]
# oder über range mit :
df.loc["a":"b"]
# Das kann auch mit Spalten kombiniert werden
df.loc["a", ["city", "inhabitants"]]
city Essen
inhabitants 591032
Name: a, dtype: object
Alternativ gibt es noch einen Weg über die Indices und zwar mit .iloc und das sieht dann plötzlich wieder aus wie bei Numpy:
df.iloc[0:2]
| city | inhabitants | xyz | |
|---|---|---|---|
| a | Essen | 591032 | 0.711240 |
| b | Bochum | 364454 | 0.136874 |
# oder auch Zeilen und Spalten
df.iloc[0:2, 1:]
| inhabitants | xyz | |
|---|---|---|
| a | 591032 | 0.711240 |
| b | 364454 | 0.136874 |
Zusammenfassung:
[ ]um Spalten zu wählen.loc[row_labels, column_labels]um über die Labels (Zeilen und Spaltennamen) Elemente auszuwählen.iloc[row_positions, column_positions]um über die Indices Elemente auszuwählen
18.2.1. Elemente hinzufügen/entfernen#
Einem DataFrame können weitere Spalten genau wie bei Dictionaries hinzugefügt werden:
df["height"] = [116, 100, 86]
df
| city | inhabitants | xyz | height | |
|---|---|---|---|---|
| a | Essen | 591032 | 0.711240 | 116 |
| b | Bochum | 364454 | 0.136874 | 100 |
| c | Dortmund | 587696 | 0.996618 | 86 |
Der umgekehrte Schritt wäre, eine Spalte komplett zu entfernen, das geht mit del df["xyz"].
18.2.2. Now: go Data Science#
Bisher haben wir uns nur einfache Beispiele angeschaut um die Methoden von Pandas ein wenig kennenzulernen. Interessant wird das Ganze aber eigentlich erst wenn wir anfangen mit größeren Datensets zu spielen. Wann ein Datenset als „groß“ gilt ist natürlich relativ, hier meine ich damit v.a. Daten die zumindest so groß sind, dass wir sie nicht mit einem kurzem Blick auf eine Tabelle vollständig verstehen können.
Für diesen Zweck fangen wir hier an mit dem „Pokemon Dataset“ zu arbeiten das man auf „Kaggle“ herunterladen kann: https://www.kaggle.com/rounakbanik/pokemon
18.2.3. Daten importieren#
Das Laden von Daten aus Textdateien geht in der Regel recht einfach mit Pandas, da Pandas entsprechende Funktionen dafür bereitstellt. U.a. zum importieren von CSV oder Excel-Dateien.
import os
root = os.getcwd()
filename = os.path.join(root, "../data", 'pokemon.csv') # je nach eigenem Pfad anpassen!
data = pd.read_csv(filename, delimiter=",")
# hier geht auch: data = pd.read_csv(filename) da delimiter="," der "default"-Parameter ist
data.head()
| abilities | against_bug | against_dark | against_dragon | against_electric | against_fairy | against_fight | against_fire | against_flying | against_ghost | ... | percentage_male | pokedex_number | sp_attack | sp_defense | speed | type1 | type2 | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | ['Overgrow', 'Chlorophyll'] | 1.0 | 1.0 | 1.0 | 0.5 | 0.5 | 0.5 | 2.0 | 2.0 | 1.0 | ... | 88.1 | 1 | 65 | 65 | 45 | grass | poison | 6.9 | 1 | 0 |
| 1 | ['Overgrow', 'Chlorophyll'] | 1.0 | 1.0 | 1.0 | 0.5 | 0.5 | 0.5 | 2.0 | 2.0 | 1.0 | ... | 88.1 | 2 | 80 | 80 | 60 | grass | poison | 13.0 | 1 | 0 |
| 2 | ['Overgrow', 'Chlorophyll'] | 1.0 | 1.0 | 1.0 | 0.5 | 0.5 | 0.5 | 2.0 | 2.0 | 1.0 | ... | 88.1 | 3 | 122 | 120 | 80 | grass | poison | 100.0 | 1 | 0 |
| 3 | ['Blaze', 'Solar Power'] | 0.5 | 1.0 | 1.0 | 1.0 | 0.5 | 1.0 | 0.5 | 1.0 | 1.0 | ... | 88.1 | 4 | 60 | 50 | 65 | fire | NaN | 8.5 | 1 | 0 |
| 4 | ['Blaze', 'Solar Power'] | 0.5 | 1.0 | 1.0 | 1.0 | 0.5 | 1.0 | 0.5 | 1.0 | 1.0 | ... | 88.1 | 5 | 80 | 65 | 80 | fire | NaN | 19.0 | 1 | 0 |
5 rows × 41 columns
# oder wir schauen die letzten Zeilen zuerst an
data.tail()
| abilities | against_bug | against_dark | against_dragon | against_electric | against_fairy | against_fight | against_fire | against_flying | against_ghost | ... | percentage_male | pokedex_number | sp_attack | sp_defense | speed | type1 | type2 | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 796 | ['Beast Boost'] | 0.25 | 1.0 | 0.5 | 2.0 | 0.5 | 1.0 | 2.0 | 0.5 | 1.0 | ... | NaN | 797 | 107 | 101 | 61 | steel | flying | 999.9 | 7 | 1 |
| 797 | ['Beast Boost'] | 1.00 | 1.0 | 0.5 | 0.5 | 0.5 | 2.0 | 4.0 | 1.0 | 1.0 | ... | NaN | 798 | 59 | 31 | 109 | grass | steel | 0.1 | 7 | 1 |
| 798 | ['Beast Boost'] | 2.00 | 0.5 | 2.0 | 0.5 | 4.0 | 2.0 | 0.5 | 1.0 | 0.5 | ... | NaN | 799 | 97 | 53 | 43 | dark | dragon | 888.0 | 7 | 1 |
| 799 | ['Prism Armor'] | 2.00 | 2.0 | 1.0 | 1.0 | 1.0 | 0.5 | 1.0 | 1.0 | 2.0 | ... | NaN | 800 | 127 | 89 | 79 | psychic | NaN | 230.0 | 7 | 1 |
| 800 | ['Soul-Heart'] | 0.25 | 0.5 | 0.0 | 1.0 | 0.5 | 1.0 | 2.0 | 0.5 | 1.0 | ... | NaN | 801 | 130 | 115 | 65 | steel | fairy | 80.5 | 7 | 1 |
5 rows × 41 columns
Die Tabelle ist hier erstmal zu groß, darum können wir uns erst die Spaltennamen anschauen:
data.columns
Index(['abilities', 'against_bug', 'against_dark', 'against_dragon',
'against_electric', 'against_fairy', 'against_fight', 'against_fire',
'against_flying', 'against_ghost', 'against_grass', 'against_ground',
'against_ice', 'against_normal', 'against_poison', 'against_psychic',
'against_rock', 'against_steel', 'against_water', 'attack',
'base_egg_steps', 'base_happiness', 'base_total', 'capture_rate',
'classfication', 'defense', 'experience_growth', 'height_m', 'hp',
'japanese_name', 'name', 'percentage_male', 'pokedex_number',
'sp_attack', 'sp_defense', 'speed', 'type1', 'type2', 'weight_kg',
'generation', 'is_legendary'],
dtype='object')
18.2.3.1. Neues, kleineres DataFrame erzeugen#
Das sind ziemlich viele Spalten! Wir nehmen darum erstmal nur ein paar davon für die nächsten Schritte.
cols = [
'name', 'type1', 'type2',
'speed', 'abilities', 'attack',
'defense', 'height_m', 'hp', 'weight_kg',
'generation', 'is_legendary'
]
data_selected = data[cols]
data_selected.head()
| name | type1 | type2 | speed | abilities | attack | defense | height_m | hp | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Bulbasaur | grass | poison | 45 | ['Overgrow', 'Chlorophyll'] | 49 | 49 | 0.7 | 45 | 6.9 | 1 | 0 |
| 1 | Ivysaur | grass | poison | 60 | ['Overgrow', 'Chlorophyll'] | 62 | 63 | 1.0 | 60 | 13.0 | 1 | 0 |
| 2 | Venusaur | grass | poison | 80 | ['Overgrow', 'Chlorophyll'] | 100 | 123 | 2.0 | 80 | 100.0 | 1 | 0 |
| 3 | Charmander | fire | NaN | 65 | ['Blaze', 'Solar Power'] | 52 | 43 | 0.6 | 39 | 8.5 | 1 | 0 |
| 4 | Charmeleon | fire | NaN | 80 | ['Blaze', 'Solar Power'] | 64 | 58 | 1.1 | 58 | 19.0 | 1 | 0 |
Einen ersten Eindruck der Daten (zumindest der numerischen Einträge), können wir uns verschaffen über:
data_selected.describe()
| speed | attack | defense | height_m | hp | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|
| count | 801.000000 | 801.000000 | 801.000000 | 781.000000 | 801.000000 | 781.000000 | 801.000000 | 801.000000 |
| mean | 66.334582 | 77.857678 | 73.008739 | 1.163892 | 68.958801 | 61.378105 | 3.690387 | 0.087391 |
| std | 28.907662 | 32.158820 | 30.769159 | 1.080326 | 26.576015 | 109.354766 | 1.930420 | 0.282583 |
| min | 5.000000 | 5.000000 | 5.000000 | 0.100000 | 1.000000 | 0.100000 | 1.000000 | 0.000000 |
| 25% | 45.000000 | 55.000000 | 50.000000 | 0.600000 | 50.000000 | 9.000000 | 2.000000 | 0.000000 |
| 50% | 65.000000 | 75.000000 | 70.000000 | 1.000000 | 65.000000 | 27.300000 | 4.000000 | 0.000000 |
| 75% | 85.000000 | 100.000000 | 90.000000 | 1.500000 | 80.000000 | 64.800000 | 5.000000 | 0.000000 |
| max | 180.000000 | 185.000000 | 230.000000 | 14.500000 | 255.000000 | 999.900000 | 7.000000 | 1.000000 |
Danach kommen wir zum eigentlichen Kern der Pandas Analyse. Durch das Kombinieren von Slicing/Indexing und weiteren Pandas-Methoden können verschiedene Aspekte der Daten schnell und gut erkundet werden.
Z.B um zu schauen welche Typen von Pokemon es gibt:
data_selected["type1"].unique()
array(['grass', 'fire', 'water', 'bug', 'normal', 'poison', 'electric',
'ground', 'fairy', 'fighting', 'psychic', 'rock', 'ghost', 'ice',
'dragon', 'dark', 'steel', 'flying'], dtype=object)
Oder um zu schauen, wie viele es von jedem Typ gibt:
data_selected["type1"].value_counts()
type1
water 114
normal 105
grass 78
bug 72
psychic 53
fire 52
rock 45
electric 39
poison 32
ground 32
dark 29
fighting 28
dragon 27
ghost 27
steel 24
ice 23
fairy 18
flying 3
Name: count, dtype: int64
18.2.4. Wie bekomme ich nun die Werte#
Pandas gibt in den meisten Fällen entweder ein DataFrame (Tabelle) zurück, oder eine Series (eine einzelne Spalte mit Zeilennamen). In manchen Fällen wollen wir aber nur die eigentlichen Werte aus der Spalte (oder aus der Tabelle) haben. Dafür gibt es in Pandas die Möglichkeit ein Numpy Array auszugeben mit .values.
values = data_selected["type1"].value_counts().values
print(values)
print(type(values))
[114 105 78 72 53 52 45 39 32 32 29 28 27 27 24 23 18 3]
<class 'numpy.ndarray'>
18.3. Sortieren#
Anders als by Numpy und Listen gibt es bei Pandas keine Methode .sort(), sondern zwei verschiedene Methoden:
.sort_index()- Sortiert nach dem Index.sort_values()- Sortiert nach Spaltenwerten
Anders als bei Listen gibt es kein Parameter „reverse“ sondern dafür ascending, das zur Umkehrung der Sortierung auf False gesetzte werden kann.
data_selected.sort_values(["speed", "name"])
| name | type1 | type2 | speed | abilities | attack | defense | height_m | hp | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 445 | Munchlax | normal | NaN | 5 | ['Pickup', 'Thick Fat', 'Gluttony'] | 85 | 40 | 0.6 | 135 | 105.0 | 4 | 0 |
| 770 | Pyukumuku | water | NaN | 5 | ['Innards Out', 'Unaware'] | 60 | 130 | 0.3 | 55 | 1.2 | 7 | 0 |
| 212 | Shuckle | bug | rock | 5 | ['Sturdy', 'Gluttony', 'Contrary'] | 10 | 230 | 0.6 | 20 | 20.5 | 2 | 0 |
| 437 | Bonsly | rock | NaN | 10 | ['Sturdy', 'Rock Head', 'Rattled'] | 80 | 95 | 0.5 | 50 | 15.0 | 4 | 0 |
| 596 | Ferroseed | grass | steel | 10 | ['Iron Barbs'] | 50 | 91 | 0.6 | 44 | 18.8 | 5 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 64 | Alakazam | psychic | NaN | 150 | ['Synchronize', 'Inner Focus', 'Magic Guard'] | 50 | 65 | 1.5 | 55 | 48.0 | 1 | 0 |
| 100 | Electrode | electric | NaN | 150 | ['Soundproof', 'Static', 'Aftermath'] | 50 | 70 | 1.2 | 60 | 66.6 | 1 | 0 |
| 794 | Pheromosa | bug | fighting | 151 | ['Beast Boost'] | 137 | 37 | 1.8 | 71 | 25.0 | 7 | 1 |
| 290 | Ninjask | bug | flying | 160 | ['Speed Boost', 'Infiltrator'] | 90 | 45 | 0.8 | 61 | 12.0 | 3 | 0 |
| 385 | Deoxys | psychic | NaN | 180 | ['Pressure'] | 95 | 90 | 1.7 | 50 | 60.8 | 3 | 1 |
801 rows × 12 columns
data_selected.sort_values(["speed", "name"], ascending=False)
| name | type1 | type2 | speed | abilities | attack | defense | height_m | hp | weight_kg | generation | is_legendary | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 385 | Deoxys | psychic | NaN | 180 | ['Pressure'] | 95 | 90 | 1.7 | 50 | 60.8 | 3 | 1 |
| 290 | Ninjask | bug | flying | 160 | ['Speed Boost', 'Infiltrator'] | 90 | 45 | 0.8 | 61 | 12.0 | 3 | 0 |
| 794 | Pheromosa | bug | fighting | 151 | ['Beast Boost'] | 137 | 37 | 1.8 | 71 | 25.0 | 7 | 1 |
| 100 | Electrode | electric | NaN | 150 | ['Soundproof', 'Static', 'Aftermath'] | 50 | 70 | 1.2 | 60 | 66.6 | 1 | 0 |
| 64 | Alakazam | psychic | NaN | 150 | ['Synchronize', 'Inner Focus', 'Magic Guard'] | 50 | 65 | 1.5 | 55 | 48.0 | 1 | 0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 596 | Ferroseed | grass | steel | 10 | ['Iron Barbs'] | 50 | 91 | 0.6 | 44 | 18.8 | 5 | 0 |
| 437 | Bonsly | rock | NaN | 10 | ['Sturdy', 'Rock Head', 'Rattled'] | 80 | 95 | 0.5 | 50 | 15.0 | 4 | 0 |
| 212 | Shuckle | bug | rock | 5 | ['Sturdy', 'Gluttony', 'Contrary'] | 10 | 230 | 0.6 | 20 | 20.5 | 2 | 0 |
| 770 | Pyukumuku | water | NaN | 5 | ['Innards Out', 'Unaware'] | 60 | 130 | 0.3 | 55 | 1.2 | 7 | 0 |
| 445 | Munchlax | normal | NaN | 5 | ['Pickup', 'Thick Fat', 'Gluttony'] | 85 | 40 | 0.6 | 135 | 105.0 | 4 | 0 |
801 rows × 12 columns