Explorative Datenanalyse#
Know your data! Bei der Arbeit mit Daten ist es wichtig, diese gut zu kennen. Die explorative Datenanalyse (EDA) ist ein unverzichtbarer Schritt im Datenanalyseprozess, der bedeutende Einblicke und Verständnis in Datensätze bringt, bevor komplexere Analysen angewendet werden. Zuächst ermöglicht EDA die Identifizierung von Mustern, Anomalien und Beziehungen in den Daten, die nicht sofort offensichtlich sind. Außerdem trägt sie zur Qualitätssicherung bei, indem sie es ermöglicht, fehlende Werte, Ausreißer oder fehlerhafte Daten zu erkennen und zu korrigieren, was die Genauigkeit der nachfolgenden Analysen verbessert. Ferner unterstützt EDA die Auswahl geeigneter Analysemethoden und Modelle, indem sie Einblicke in die Datenstruktur und Verteilung bietet. Zusätzlich fördert sie ein tiefes Verständnis der Daten, was für die effektive Kommunikation der Analyseergebnisse entscheidend ist. Schließlich ermöglicht EDA eine effizientere und zielgerichtetere Datenanalyse, indem sie die Richtung vorgibt, in der detailliertere Analysen am vielversprechendsten sind.
Pandas ist sehr hilfreich, um mit wenig Code erste Einsichten in seine Daten zu bekommen. Daher werden im folgenden Abschnitt einige erste Herangehensweisen vorgestellt, wie man in einfachen Schritten mit pandas Daten erkunden kann.
import pandas as pd
Am Anfang einer explorativen Datenanalyse steht wie immer das Einlesen der Daten - und die Umwandlung der Datentypen in das richtige Format. Wir lesen diesmal eine JSON-Datei ein und wandeln den in der Spalte date enthaltenen unix-Timestamp in ein Datetime-Objekt um. Dies erreichen wir mit der Methode pd.to_datetime(). Das Argument unit= müssen wir bei dem vorliegenden Beispiel auf ms für Millisekunden setzen.
df = pd.read_json('data/AvH-letters-with-tokens.json')
df.loc[:, 'date'] = pd.to_datetime(df.loc[:, 'date'], unit='ms')
print(df.shape)
df.head()
Infos zum Dataframe#
Schauen wir uns zunächst die Benennung der Spalten an:
df.columns
Dann geben wir Infos zu allen Spalten aus, um Angaben zu den Datentypen zu erhalten. Bis auf zwei Spalten ist der Datentyp als ‘object’ ausgewiesen. D.h. hier sind strings in den Spalten vorhanden. Bei date haben wir ein Datetime-Objekt erstellt, bei nr_token handelt es sich um integers. Auch die Angabe über fehlende Werte ist ersichtlich, die wir aber in der darauffolgenden Zelle nochmal auf einem anderen Weg darstellen können.
df.info()
df.isna().sum()
df.describe()
Zu den Spalten mit numerischen Werten können wir Informationen über die Verteilung der Daten aufrufen. In unserem Beispiel haben wir allerdings nur eine Spalte mit numerischen Werten, die Auskunft über die Anzahl der Token in den Briefen gibt. Im Durchschnitt (mean) besteht ein Brief aus 388 Token, während der längste Brief 4347 Token umfasst. Im Median (50 %) enthält ein Brief 224 Token; auch die Quartile (25%, 75%) werden ausgegeben. Die Standardabweichung beträgt 474,31 Token.
count: Die Anzahl der Nicht-Null-Einträge. Dies hilft, die Menge der vorhandenen Daten zu verstehen und fehlende Werte zu identifizieren.
mean: Der Durchschnittswert der Einträge in der Spalte.
std: Die Standardabweichung, die misst, wie weit die Werte im Durchschnitt vom Mittelwert (mean) entfernt sind.
min: Der kleinste Wert in der Spalte.
25% (unteres Quartil): Der Wert, unterhalb dessen 25% der Daten liegen. Dies ist der “mittlere” Wert des ersten Quartils.
50% (Median): Der mittlere Wert der Daten. 50% der Daten liegen unter diesem Wert und 50% darüber. Dieser Wert teilt den Datensatz in zwei Hälften.
75% (oberes Quartil): Der Wert, unterhalb dessen 75% der Daten liegen. Dies ist der “mittlere” Wert des dritten Quartils.
max: Der größte Wert in der Spalte
Infos zu einzelnen Spalten#
Mit nunique() können wir die Anzahl der einmalig auftretenden Werte ausgeben, mit unique() können wir diese Werte selbst aufrufen. Dies führen wir für die Spalten edition_id, sender, receiver und place mit Hilfe einer for-Schleife durch.
for col in ['edition_id', 'sender', 'receiver', 'place']:
print(col)
print(df.loc[:, col].nunique())
print(df.loc[:, col].unique())
print('###\n')
Ermitteln der kürzesten und längsten Briefe#
Die Methode idxmax() gibt die Indexposition des größten Wertes zurück. Diese Rückgabe können wir als Argument nutzen, um die Zeile genauer zu inspizieren. Gleiches erreichen wir mit idxmin() für den kürzesten Brief.
df.loc[df.loc[:, 'nr_token'].idxmax(), :]
df.loc[df.loc[:, 'nr_token'].idxmax(), 'text']
df.loc[df.loc[:, 'nr_token'].idxmin(), :]
Hier stellen wir fest, dass bei diesem Eintrag anscheinend der Textes des Briefes nicht vorhanden ist.
Schnelle Visualisierungen mit pandas#
Mit pandas können wir schnell auch erste Visualisierungen plotten. Allerdings: Die Darstellung geht natürlich noch besser - das werden wir im Kapitel zur Datenvisualisierung ausführlicher besprechen. Bspw. ist bei den folgenden ‘schnellen’ Visualisierungen zur ersten Exploration der Daten die Reihenfolge der horizontalen Balken verkehrt herum. Auch die Abstände zwischen den Jahreszahlen, die als Label auf der x-Achse stehen, sind viel zu eng. Vieles lässt sich anpassen, das werden wir - wie gesagt - im nächsten Kapitel vorstellen.
Balkendiagramm#
In den folgenden drei Balkendiagrammen zeigen wir die 5 häufigsten Orte, von denen Briefe verschickt wurden, sowie die 5 häufigsten Verfasser und Empfänger der Briefe. Dazu nutzen wir die Methode value_counts() auf die jeweilige Spalte und weisen die Rückgabe einer neuen Variablen zu, die wir wiederum nutzen, um das Ergebnis zu plotten. Wir nutzen mit head() nur die ersten 5 Ergebnisse. Mit dem Argument kind='barh' geben wir an, dass wir die Darstellung in einem horizontalen Balkendiagramm ausgeben wollen. Mit title= legen wir fest, welche Überschrift das Diagramm erhalten soll.
df_top_places = df.loc[:, 'place'].value_counts()
df_top_places.head(5).plot(kind='barh', title='Top 5 Orte');
df_top_sender = df.loc[:, 'sender'].value_counts()
df_top_sender.head(5).plot(kind='barh', title='Top 5 Schreiber');
df_top_receiver = df.loc[:, 'receiver'].value_counts()
df_top_receiver.head(5).plot(kind='barh', title='Top 5 Empfänger');
Histogramm#
Histogramme sind grafische Darstellungen, die zeigen, wie oft Daten innerhalb bestimmter Intervalle oder bins vorkommen, um die Verteilung eines Datensatzes zu visualisieren. Jedes bin repräsentiert einen Wertebereich, und die Höhe der Balken im Diagramm zeigt die Anzahl der Beobachtungen (Häufigkeit), die in jedes bin fallen. Durch die Analyse eines Histogramms können wir die Zentralität, Streuung und Schiefe der Daten verstehen, sowie mögliche Anomalien wie Ausreißer erkennen.
Mit nur einer Codezeile können wir ein Histogramm plotten, das Auskunft über die Verteilung der Länge der Briefe gibt. Wir wählen dazu die Spalte mit den Anzahl der Token aus und nutzen plot() mit dem Parameter hist und bins=30, um die Werte der Textlängen in 30 gleichgroße Intervalle einteilen zu lassen. Wir erkennen, dass viele Briefe weniger als 1000 Token umfassen, der größte Teil weniger als 500 Token. Einige wenige Briefe haben auch mehr als eine Länge von 1000 Token.
df.loc[:, 'nr_token' ].plot(kind='hist', bins=30, title='Verteilung der Länge der Briefe');
Gruppieren#
Wenn wir wissen wollen, wie viele Briefe (aus unserem in Auswahl zusammengestellten Korpus) in den jeweiligen Jahren verfasst wurden, können wir diese mit der Methode groupby() entsprechend gruppieren. Die Gruppierung führen wir auf der Datums-Spalte aus. Wir nutzen dann die Methode dt.to_period('Y'), um auf der Datumsangabe nach einer ‘Zeitperiode’, in diesem Fall das Jahr, das wir mit ‘Y’ angeben, zu gruppieren. Die Abkürzung dt gibt dabei an, dass wir mit Datetime-Objekten arbeiten. Neben ‘Y’ könnten wir auch ‘M’ für die Monate nutzen. Schließlich nutzen wir size(), um dann die Anzahl pro Jahr zu erhalten. In der zweiten Zeile plotten wir die Werte nach Jahren als Balkendiagramm.
df_grouped_sum = df.groupby(df.loc[:, 'date'].dt.to_period('Y')).size()
df_grouped_sum.plot(kind='bar', title='Anzahl der Briefe pro Jahr');
Im nächsten Beispiel gruppieren wir unsere Daten ebenfalls nach der Datumspalte. Diesmal nutzen wir zusätzlich die Spalte mit der Anzahl der Token und wenden darauf die mean()-Methode an, um die durchschnittliche Anzahl der Token für das jeweilige Jahr zu erhalten. Den Code für das Balkendiagramm wenden wir wie bei den vorherigen Diagrammen an.
df_grouped_token = df.groupby(df.loc[:, 'date'].dt.to_period('Y'))['nr_token'].mean()
df_grouped_token.plot(kind='bar', title='Durchschnittliche Brieflänge in Token nach Jahren');
Abfrage mit boolescher Maske#
Ein weiterer Schritt bei der Exploration der Daten können Abfragen nach bestimmten Wörtern in den Textdaten sein. Hier hilft eine Reihe von Methoden, die in pandas speziell für die Arbeit mit Strings existieren und mit dem Kürzel str eingeleitet werden (siehe auch die Dokumentation hierzu). Die Methode str.contains() prüft, ob eine Zeichenkette in einem String enthalten ist. Nachfolgend haben wir die Methode auf die Spalte mit den Texten der Briefe angewandt und suchen hier nach dem String ‘Vulkan’.
Diese Suchfunktionalität ermöglicht auch den Einsatz von Regulären Ausdrücken, die wir in einem kleinen Exkurs genauer vorstellen werden. Hier ermöglicht die Syntax ‘[V|v]’, dass sowohl nach dem Wort in Groß- und Kleinschreibung gesucht wird. In der weiteren Variante wird dann sogar nach den Schreibvarianten mit k bzw. c gesucht. Wir erhalten hier 17 Treffer des Wortes ‘Vulkan’ und dessen Varianten, die in den Brieftexten vorkommen. Diese Briefe filtern wir mit Hilfe der booleschen Maskierung.
query_1 = df.loc[:, 'text'].str.contains('Vulkan').sum()
query_1
query_2 = df.loc[:, 'text'].str.contains('[V|v]ulkan').sum()
query_2
query_3 = df.loc[:, 'text'].str.contains('[V|v]ul[c|k]an').sum()
query_3
mask = df.loc[:, 'text'].str.contains('[V|v]ul[c|k]an')
df_vulkan = df.loc[mask, :]
print(df_vulkan.shape)
df_vulkan.head(3)
In dieser Auswahl können wir nun bspw. weitergehend erkunden, wer die Empfänger der Briefe sind, in denen das Wort Vulkan vorkommt. Mit zwei weiteren Zeilen Code können wir auch das Ergebnis schnell visualisieren.
print(df_vulkan.loc[:, 'receiver'].nunique())
df_vulkan.loc[:, 'receiver'].unique()
top_receiver = df_vulkan.loc[:, 'receiver'].value_counts()
top_receiver.plot(kind='barh', title='Top Empfänger "Vulkan"');
Auch die pd.crosstab()-Methode kann ähnlich eingesetzt werden wie groupby(). Hier setzen wir den Index eines neuen Dataframes auf die Datumsangaben und fügen dann die Spalte count hinzu, in der die Häufigkeit der Vorkommnisse für die Jahre gezählt werden. Da nicht in jedem Jahr das Wort ‘Vulkan’ Erwähnung findet, sind im anschließenden Diagramme nur die Jahre aufgeführt, in denen mindestens einmal das Wort in einem der Briefe, die in diesem Jahr verfasst wurden, zu finden ist. Das ist für eine Visualisierung unvorteilhaft. Aber dies kann alles angepasst werden - genauso wie fehlende Beschriftungen wie auch die Kommawerte in den Labeln der y-Achse. Wie die Visualisierung der Daten verbessert werden, kann werden wir im nächsten Kapitel ausführen. Hier werden eigene Python-Bibliotheken zum Plotten zum Einsatz kommen.
df_vulkan_year = pd.crosstab(index=df_vulkan.loc[:,'date'].dt.to_period('Y'), columns='count')
df_vulkan_year.plot(kind='bar', legend=False);