10.1. Daten bereinigen mit Pandas#
Bisher haben wir uns nur der Beschaffung von Daten beschäftigt. Die extrahierten Daten haben wir meist als Pandas Dataframe dargestellt und gespeichert; oder wir haben extrahierte Texte direkt als Plaintextdateien gespeichert. In diesem Kapitel werden wir etwas tiefer in Pandas einsteigen und anhand eines Beispiels einige typische Datenbereinigungs- und -transformationsschritte kennenlernen.
10.1.1. Einstieg Pandas#
Lest euch zunächst die beiden Anleitungen “What kind of data does pandas handle?” und “How do I select a subset of a DataFrame?” auf der Seite Getting Started Tutorials durch.
Ruft anschließend dieses Pandas Cheatsheet auf und beantwortet mithilfe der Tutorial-Seiten und des Cheat Sheets die folgenden Fragen:
Wie hängen Pandas Dataframe- und Series-Objekte zusammen?
Was ist der Unterschied zwischen den Methoden
.loc()
,.iloc()
,.at()
und.iat()
?
10.1.2. Daten extrahieren und bereinigen#
In der Praxisaufgabe auf dem Übungsblatt 12 solltet ihr die Links zu Tierfotos von der Seite https://www.pinterest.com/ideas/animals/925056443165/ extrahieren. In diesem Beispiel scrapen wir nicht die Links zu den Fotos, sondern Kommentare zu den einzelnen Fotos zusammen mit den Usernamen von Kommentator:innen. Anschließend werden wir den Pandas Dataframe mit den extrahierten Daten bereinigen und bearbeiten, und zuletzt erstellen wir zwei beispielhafte Grafiken zur Visualisierung der Daten.
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import pandas as pd
Als Beispiel verwenden wir zwei Pins, für die zunächst die Kommentare sowie die Usernamen der Kommentator:innen extrahiert werden. Diese Daten speichern wir zusammen mit der URL zum Tierfotopin zunächst als Python Dictionary, das wir in einen Pandas Dataframe umwandeln:
comments_dict = {"link":[], "commentator":[], "comment":[]}
tierlinks = [
"https://www.pinterest.de/pin/206321226666310819/",
"https://www.pinterest.de/pin/1108730001991298438/",
"https://de.pinterest.com/pin/314759461475004846/"
]
driver = webdriver.Chrome()
for link in tierlinks:
driver.get(link)
wait = WebDriverWait(driver, 20)
comment_elems = wait.until(
EC.visibility_of_all_elements_located((By.XPATH, "//div[@data-test-id='author-and-comment-container']/div/div/span/span[3]/span"))
)
commentator_elems = wait.until(
EC.visibility_of_all_elements_located((By.XPATH, "//div[@data-test-id='author-and-comment-container']/div/div/span/span[2]/a"))
)
if len(comment_elems) > 0: # oder len(commentator_elems); das ist egal
comments = [comment.text for comment in comment_elems]
commentators = [commentator.get_attribute("href") for commentator in commentator_elems]
comments_dict["link"].extend([link]*len(comments)) # [link] erstellt eine Liste mit einem Element
comments_dict["comment"].extend(comments)
comments_dict["commentator"].extend(commentators)
driver.quit()
comments_df = pd.DataFrame.from_dict(comments_dict)
comments_df
Eine kurze Durchsicht der extrahierten Daten zeigt, dass nicht alle Kommentare extrahiert wurden, sondern nur die Kommentare, die beim Aufruf einer Tierfotoseite sichtbar sind. Für unsere Zwecke reichen uns aber diese Kommentare. Wir haben außerdem nur Kommentare von zwei Tierfotoseiten extrahiert, sodass der Dataframe überschaubar ist. Bei sehr großen Dataframes kann es sinnvoll sein, sich nicht den gesamten Dataframe ausgeben zu lassen, sondern nur eine bestimmte Anzahl von Zeilen. Dazu können die Pandas Dataframe-Methoden .tail()
und .head()
verwendet werden, oder eine Slicing-Operation:
comments_df.head(10) # erste 10 Zeilen
comments_df.tail(5) # erste 10 Zeilen
comments_df.iloc[4:12] # Zeilen 4-11
Zeilen können auch mithilfe einer logischen Abfrage ausgewählt werden, zum Beispiel:
# Die Spalte commentator benennen wir allerdings erst später in commentator_id um (s.u.)
comments_df.loc[comments_df["commentator_id"] == 5] # Zeilen mit commentator_id == 5
Es können auch einzelne Spalten oder nur bestimmte Spalten ausgewählt werden:
# Spalten commentator bis comment
comments_df.loc[:, "commentator":"comment"]
# Zugriff auf einzelne Spalte
comments_df["comment"]
comments_df.comment
comments_df.loc[:, "comment"]
Die Durchsicht des Dataframes zeigt, dass für einige Zellen in der Spalte comment leer zu sein scheinen. Die Kommentare sind immer dann leer, wenn ein:e User:in ein Bild als Kommentar geposted hat anstelle eines Textkommentars. Tatsächlich sind diese Zellen in unserem Dataframe aber nicht leer:
type(comments_df.at[10, "comment"])
Um das Fehlen der Werte in unserem Dataframe zu kennzeichnen, können wir den speziellen Wert NA
einsetzen:
# Leere Zeichenketten durch NA Werte ersetzen
df = comments_df.replace('', pd.NA, inplace=True)
Der Wert NA markiert das Fehlen von Werten. In Pandas können Zellen, die fehlende Werte enthalten, mithilfe spezieller Methoden abgefragt und bearbeitet werden, so zum Beispiel .isna()
oder .fillna()
. Hier könnt ihr nachlesen, wie fehlende Werte in Pandas-Datenobjekten allgemein behandelt werden.
Als nächstes überprüfen wir eine Zelle mit einem Kommentar und überprüfen, ob sich am Anfang oder Ende der Zeichenkette überflüssige Leerzeichen befinden. Das ist bei der Extraktion von Textinhalt häufig der Fall und diesem Problem sind wir im Laufe des Semesters schon einige Male begegnet (z.B. beim Scrapen der Tags auf der Quotes to Scrape-Seite).
comments_df.at[2, "comment"]
Tatsächlich befindet sich am Anfang des ausgewählten Kommentars überflüssige Leerzeichen. Leerzeichen am Anfang und Ende einer einzelnen Zeichenkette können mithilfe der Methode .strip()
entfernt werden; die Methode .str.strip()
entfernt Leerzeichen für jedes Element in einer Spalte eines Pandas-Dataframes (bzw. in einem Pandas Series-Objekt, denn das ist ja dasselbe):
# Leading und trailing Whitespace entfernen mit strip()
comments_df.at[2, "comment"].strip()
# Leading und trailing whitespace für eine gesamte Spalte entfernen mit .str.strip()
comments_df['comment'] = comments_df['comment'].str.strip()
comments_df.at[2, "comment"] # überprüfen: hat es geklappt?
Ein weiterer Verarbeitungsschritt ist die Anonymisierung der Kommentator:innen. Je nach Forschungsfrage interessiert nicht unbedingt, welche:r Nutzer:in welchen Kommentar verfasst hat, sondern beispielsweise nur, ob dieselben Kommentator:innen ähnliche Bilder kommentieren oder wie viele Kommentare jede:r Nutzer:in hinterlassen hat. Dazu müssen wir die konkreten Nutzernamen nicht kennen; es reicht aus, wenn wir jeder Kommentator:in eine einzigartige ID zuteilen und in unserer Analyse nur die IDs betrachten. Durch das Anonymisieren der Nutzernamen gehen wir außerdem sicher, dass unser Datensatz nicht die Auflagen zur Speicherung und Nutzung personenbezogener Daten laut DSGVO verletzt (siehe Abschnitt 6.1).
# Die factorize()-Methode ordnet jedem einzigartigen Wert eine eindeutige ID zu und gibt ein Tupel zurück, das aus einem Array von Labels und einem Index mit den einzigartigen Werten besteht.
labels, unique = pd.factorize(comments_df['commentator'])
labels
unique
comments_df['commentator'] = labels
comments_df # überprüfen
Nachdem wir die Nutzernamen durch IDs ersetzt haben, passt der Spaltenname commentator nicht mehr so ganz und wir werden die Spalte umbenennen. Das Argument inplace=True bewirkt dabei, dass die Änderung direkt im bestehenden Dataframe vorgenommen wird, ohne dass eine Kopie des Objekts erstellt wird.
# Spalte commentator in commentator_id umbenennen
comments_df.rename(columns={"commentator": "commentator_id"}, inplace=True)
comments_df
# Alternative
comments_df.columns = ["link", "commentator_id", "comment"]
10.1.3. Daten zwischenspeichern#
Häufig erfolgt die Analyse der extrahierten Daten nicht unmittelbar nach der Datenextraktion, sondern die Daten werden zwischengespeichert und später wieder eingelesen. Den bereinigten und anonymisierten Dataframe speichern wir deswegen im Folgenden auf dem Computer. Neben der bereits bekannten Pandas Dataframe-Methode .to_csv()
gibt es eine Vielzahl anderer Methoden zum Schreiben von Pandas-Objekten. Einen Überblick über alle Datenformate und Methoden zum Schreiben von Daten findet ihr unter https://pandas.pydata.org/docs/user_guide/io.html.
CSV-Dateien sind nicht immer die beste Wahl. Wenn Daten nur zwischengespeichert und später wieder eingelesen und weiterverarbeitet werden sollen, dann eignet sich zum Beispiel das Python-spezifische Datenformat pickle. Für größere Dataframes und wenn der Dataframe in einer anderen Programmiersprache wie R weiter bearbeitet werden soll, eignet sich dagegen das Datenformat feather. Um die Methode .to_feather()
verwenden zu können, muss jedoch zuvor das Modul pyarrow installiert werden. Es muss ausnahmsweise nicht importiert werden, weil die Methode unter der Motorhaube automatisch auf das Modul zurückgreift. Zum Speichern besonders großer Datenobjekte wird häufig das Datenformat HDF5 empfohlen (zum Beispiel in diesem Blogartikel.
# CSV
comments_df.to_csv("comments_df.csv", index=False, encoding="utf-8")
# Pickle
comments_df.to_pickle("comments_df.pkl")
# Feather
import sys
!conda install --yes --prefix {sys.prefix} pyarrow
comments_df.to_feather("comments_df.feather")
Bei der Arbeit mit sehr großen Datenmengen lohnt sich außerdem gegebenenfalls der Umstieg von Pandas auf Polars, eine neue und deutlich effizientere Bibliothek zur Arbeit mit Dataframes. Wer bereits Pandas und/oder R und insbesondere R dplyr kennt, sollte beim Umstieg jedoch keine Probleme haben, denn die Polars-Syntax ist sehr ähnlich. Einen Vergleich zwischen der Polars und Pandas Syntax findet ihr hier. Für einen Vergleich zwischen Polars und R empfehle ich diese Seite.
10.1.4. Quellen#
Liam Brannigan. Cheatsheet for Pandas to Polars. 2024. URL: https://www.rhosignal.com/posts/polars-pandas-cheatsheet/.
Jodie Burchell. Polars vs. Pandas: What's the Difference? 2023. URL: https://blog.jetbrains.com/dataspell/2023/08/polars-vs-pandas-what-s-the-difference/.
Damien Dotta. Cookbook Polars for R. 2023. URL: https://ddotta.github.io/cookbook-rpolars/.
Wes McKinney. Data Wrangling with Pandas Cheat Sheet. 2024. URL: https://pandas.pydata.org/Pandas_Cheat_Sheet.pdf.
Wes McKinney. Pandas 2.2 Documentation: DataFrame. 2024. URL: https://pandas.pydata.org/docs/reference/frame.html.
Wes McKinney. Pandas 2.2 Documentation: Getting Started Tutorials. 2024. URL: https://pandas.pydata.org/docs/getting_started/intro_tutorials/index.html.
Wes McKinney. Pandas 2.2 Documentation: IO Tools. 2024. URL: https://pandas.pydata.org/docs/user_guide/io.html.
Wes McKinney. Pandas 2.2 Documentation: Series. 2024. URL: https://pandas.pydata.org/docs/reference/series.html.
Wes McKinney. Pandas 2.2 Documentation: Working with Missing Data. 2024. URL: https://pandas.pydata.org/docs/user_guide/missing_data.html.