Dieses Notebook ist ein Skript (Drehbuch) zur Vorstellung grundlegender Funktionen von Jupyter, Python, Pandas und matplotlib, um ein Gefühl für die Analyse von Softwaredaten mit diesen Biblotheken zu bekommen. Daher ist das gewählte Beispiel so gewählt, dass wir typische Aufgaben während einer Datenanalyse bearbeiten. Inhaltlich ist diese Analyse allerdings nicht repräsentativ, da sie lediglich einfache Statistiken über ein Git-Repository darstellt.
Zuallerst sehen wir uns Jupyter genauer an. Das hier ist Jupyter, die interaktive Notebook-Umgebung zum Programmieren. Wir sehen nachfolgend eine Zelle, in der wir Python-Code eingeben können. Tippen wir hier einfach einmal einen String namens "Hello World"
ein. Mit der Tastenkombination Strg
+ Enter
können wir diese Zelle ausführen.
In [1]:
"Hello World"
Out[1]:
Das Ergebnis ist sofort unter der Zelle sichtbar. Legen wir nun eine weitere Zelle an. Dies funktioniert mit dem Drücken der Taste ESC
und einem darauffolgendem Buchstaben b
. Alternativ können wir am Ende eines Notebooks eine Zelle mit Shift
+ Enter
ausführen und gleich eine neue Zelle erstellen.
Hier sehen wir gleich eine wichtige Eigenheit von Jupyter: Die Unterscheidung zwischen Befehlsmodus (erreichbar über Taste Esc
) und dem Eingabemodus (erreichbar über die Taste Enter
). Im Befehlsmodus ist die Umrahmung der aktuellen Zelle blau. Im Eingabemodus wird die Umrahmung grün. Gehen wir in den Befehlsmodus und drücken m
. Dies ändert den Zelltyp zu einer Markdown-Zelle. Markdown ist eine einfache Markup-Sprache, mit der Texte geschrieben und formatiert werden können. Damit lassen sich unsere durchgeführten Schritte direkt mit dokumentieren.
Sehen wir uns ein paar grundlegende Python-Programmierkonstrukte an, die wir später in der Arbeit mit Pandas benötigen.
Zuerst legen wir unseren Text "Hello World" per Zuweisung =
in einer Variable namens text
ab. Wir schreiben in die darunterliegenden Zeile noch einmal text
, führen die Zelle aus und sehen dann das Ergebnis unter der Zelle dargestellt.
In [2]:
text = "Hello World!"
text
Out[2]:
Über die Array-Schreibweise mit den eckigen Klammern [
und ]
können wir mittels einen 0-basierten Index auf den ersten Buchstaben unseres Texts zugreifen (dies funktioniert auch für anders geartet Listen).
In [3]:
text[0]
Out[3]:
Den letzten Buchstaben können wir mit [-1]
ausgeben lassen. Negative Zahlen in den Klammern stellen also die Indexierung von hinten dar.
In [4]:
text[-1]
Out[4]:
Wir können zudem mit sog. "Slices" arbeiten. Damit können wir beliebige Wertebereiche aus unserem text
ausgeben lassen.
In [5]:
text[2:5]
Out[5]:
Im Folgenden ist noch die Kurzschreibweise [:-1]
zu sehen, die eine Abkürzung für einen 0-basierten Slice darstellt.
In [6]:
text[:-1]
Out[6]:
Zuletzt sehen wir uns noch an, wie wir einen Text (bzw. auch eine Liste) umkehren können. Dies funktioniert mit der ::
-Schreibweise und der Angabe von -1
.
In [7]:
text[::-1]
Out[7]:
Die weitere Funktionalität einer Bibliothek (bzw. eines in einer Variable abgelegten Objekts) können wir erkunden, indem wir die Methoden und Attribute einer Klasse oder eines Objekts ansehen. Dazu schreiben wir in unserem String-Beispiel text.
und nutzen die integrierte Autovervollständigung von Jupyter mittels der Tabulatortaste Tab
, um zu sehen, welche Methoden uns aktuell verwendetes Objekt bietet. Gehen wir dann mit der Pfeiltaste unten
oder drücken z. B. die ersten Buchstaben von upper
, drücken Enter
und schließend Shift
+ Tab
, dann erscheint die Signatur des entsprechenden Funktionalität und der Ausschnitt der Hilfedokumentation. Bei zweimaligem Drücken von Shift
+ Tab
erscheint die Hilfe vollständig. Mit dem Aufruf von upper()
auf unsere text
-Variable können wir unseren Text in Großbuchstaben schreiben lassen.
In [8]:
text.upper
Out[8]:
Die interaktive Quellcode-Dokumentation hilft uns auch herauszufinden, welche Argumente wir in einer Methode zusätzlich zu normale Übergabeparametern hinzufügen können. Beim Schreiben des unteren Texts mit Nutzung der integrierten Hilfefunktion kann dies gut beobachtet werden.
In [9]:
text.split(maxsplit=2, sep=" ")
Out[9]:
In diesem Notebook wollen wir uns die Entwicklungsgeschichte des Open-Source-Projekts "Linux" anhand der Historie des dazugehörigen GitHub-Mirror-Repositories ein wenig genauer ansehen.
Das GitHub-Repository https://github.com/torvalds/linux/ wurde dafür über den Befehl
git clone https://github.com/torvalds/linux.git
auf die lokale Festplatte geklont.
Die für diese Auswertung relevanten Teile der Historie wurde mittels
git log --pretty="%ad,%aN" --no-merges > git_demo_timestamp_linux.csv
exportiert. Dieser Befehl liefert pro Commit des Git-Repositories den Zeitstempel des Commits (%ad
) sowie den Namen des Autors (%aN
). Die jeweiligen Werte sind kommasepariert. Wir geben zusätzlich mit an, dass wir reine Merge-Commits nicht erhalten wollen (über --no-merges
). Das Ergebnis der Ausgabe speichern wir in die Datei git_demo_timestamp_linux.csv
.
Hinweis: Für eine optimierte Demo wurden im bereitgestellten Dataset manuell noch Header und das Trennzeichen geändert, um schneller durch die Analyse zu kommen. Die Unterschiede sind unter https://www.feststelltaste.de/developers-habits-linux-edition/ zu sehen, welcher mit dem Original-Datensatz durchgeführt wurde.
Pandas ist ein in Python (und teils in C) geschriebenes Datenanalysewerkzeug, welches durch die Nutzung effektiver Datenstrukturen sowie eingebauter Statistikfunktionen hervorragend zur Auswertung von tabellarischen Daten geeignet ist.
Nun können wir diese Daten mit Hilfe des Datenanalyse-Frameworks Pandas einlesen. Wir importieren dazu pandas
mit der gängigen Abkürzung pd
mittels der import ... as ...
Syntax von Pyhton.
In [10]:
import pandas as pd
Ob das Importieren des Moduls auch wirklich funktioniert hat, können wir prüfen, in dem wir mit dem pd
-Modul arbeiten. Dazu hängen wir an die pd
-Variable den ?
Operator an und führen die Zelle aus. Es erscheint die Dokumentation des Moduls im unteren Bereich des Notebooks. Diesen Bereich können wir durchlesen und mit der Taste ESC
auch wieder verschwinden lassen.
In [11]:
pd?
Danach lesen wir die oben beschriebene und gepackte CSV-Datei git_demo_timestamp_linux.gz
von einer URL
ein. Da es sich um eine gzip
-gepackte Datei handelt und wir diese Datei über das Web beziehen, müssen wir hier zusätzlich den verwendeten Kompressionsalgorithmus mittels compression='gzip'
mit angeben.
Das Ergebnis des Ladens speichern wir in der Variable git_log
. Hierin haben wir nun die Daten in
DataFrame
(so etwas ähnliches wie ein programmierbares Excel-Arbeitsblatt) geladen,Series
(in etwa Spalten) besteht. Auf den DataFrame
können wir nun Operationen ausführen. Z. B. können wir uns mittels head()
die fünf ersten Einträge anzeigen lassen.
In [12]:
#URL = "https://raw.githubusercontent.com/feststelltaste/software-analytics/master/demos/dataset/git_demo_timestamp_linux.gz"
URL = "../dataset/git_demo_timestamp_linux.gz"
git_log = pd.read_csv(URL, compression="gzip")
git_log.head()
Out[12]:
Als nächstes rufen wir info()
auf den DataFrame
auf, um einige Eckdaten über die eingelesenen Daten zu erhalten.
In [13]:
git_log.info()
Den Zugriff auf die einzelnen Series können wir mittels der Schreibeweise ['<spaltenname>']
oder (in den meisten Fällen, d. h. solange sich die Spaltennamen nicht mit Methodenname überschneiden, die ein DataFrame
selbst anbietet) per direkter Nutzung des Namens der Series
selbst erreichen.
In [14]:
git_log.author.head()
Out[14]:
Auch auf einer Series
selbst können wir verschiedene Operationen ausführen. Z. B. können wir mit value_counts()
die in einer Series
enthaltenen Werte zählen und nach gleichzeitig nach ihrer Häufigkeit sortieren lassen. Das Ergebnis ist wieder eine Series
, diesmal aber mit den zusammengezählten und sortieren Werten. Auf diese Series
können wir zusätzlich ein head(10)
aufrufen. So erhalten wir eine schnelle Möglichkeit, die TOP-10-Werte einer Series
anzeigen zu lassen. Das Ergebnis können wir dann in einer Variable top10
festhalten und ausgeben lassen, in dem wir die Variable in die nächste Zellenzeile schreiben.
In [15]:
top10 = git_log.author.value_counts().head(10)
top10
Out[15]:
Als nächstes wollen wir das Ergebnis visualisieren bzw. plotten. Um das Plot-Ergebnis der intern verwendeten Plotting-Bibliothek matplotlib
direkt im Notebook anzuzeigen, müssen wir Jupyter dies mit dem Magic-Kommando
%matplotlib inline
vor dem Aufruf der plot()
Methode mitteilen.
Standardmäßig wird beim Aufruf von plot()
auf einen DataFrame
oder einer Series
ein Liniendiagramm erstellt.
In [16]:
%matplotlib inline
top10.plot()
Out[16]:
Das ergibt hier wenig Sinn, weshalb wir mittels einer Untermethode von plot
namens bar()
ein Balkendiagramm erzeugen lassen.
In [17]:
top10.plot.bar()
Out[17]:
Für diese Daten bietet sich auch eine Visualisierung als Tortendiagramm an. Hierfür rufen wir statt bar()
die Methode pie()
auf.
In [18]:
top10.plot.pie()
Out[18]:
Das Diagramm sieht hier jedoch nicht sehr schön aus.
Mit den optionalen Styling-Parametern können wir erreichen, dass wir eine schönere Grafik angezeigt bekommen. Wir verwenden dazu
figsize=[7,7]
als Größenangabetitle="Top 10 Autoren"
als Titellabels=None
, um die überflüssige Beschriftung nicht anzuzeigen.
In [19]:
top10.plot.pie(
figsize=[5,5],
title="Top 10 Autoren",
label="")
Out[19]:
In [20]:
git_log.timestamp.head()
Out[20]:
Bevor wir in die Welt der Zeitreihenverarbeitung einsteigen können, müssen wir unsere Spalte mit den Datumsangabe zuerst in den passenden Datentyp umwandeln. Zurzeit ist unsere Spalte timestamp
noch ein String, also von textueller Natur. Wir können dies sehen, in dem wir uns mittels der Helferfunktion type(<object>)
den ersten Eintrag der timestamp
-Spalte anzeigen lassen:
In [21]:
type(git_log.timestamp[0])
Out[21]:
Beim Umwandeln von Datentypen hilft uns Pandas natürlich ebenfalls. Die Funktion pd.to_datetime
nimmt als ersten Parameter eine Series
mit Datumsangaben entgegen und wandelt diese um. Als Rückgabewert erhalten wir entsprechend eine Series
vom Datentyp Timestamp
. Die Umwandlung funktioniert für die meisten textuellen Datumsangaben auch meistens automagisch [sic!], da Pandas mit unterschiedlichsten Datumsformaten umgehen kann. Das Ergebnis schreiben wir auch gleich in die gleiche Spalte zurück.
In [22]:
git_log.timestamp = pd.to_datetime(git_log.timestamp)
git_log.head()
Out[22]:
Ob die Umwandlung erfolgreich war, können wir mit einem nochmaligen Aufruf von type()
auf den ersten Wert unserer umgewandelten Spalte timestamp_local
überprüfen.
In [23]:
type(git_log.timestamp[0])
Out[23]:
Wir können nun auch auf einzelne Bestandteile der Datumsangaben zugreifen. Dazu verwenden wir das dt
-Objekt ("datetime") und können auf dessen Eigenschaften wie etwa hour
zurückgreifen.
In [24]:
git_log.timestamp.dt.hour.head()
Out[24]:
Zusammen mit der bereits oben vorgestellten value_counts()
-Methode können wir nun wieder Werte nach ihrem Auftreten zählen lassen. Wichtig ist hier jedoch, dass wir zusätzlich den Parameter sort=False
setzen, um die Sortierung nach Mengenangaben zu vermeiden.
In [25]:
commits_je_stunde = git_log.timestamp.dt.hour.value_counts(sort=False)
commits_je_stunde.head()
Out[25]:
Das Ergebnis können wir entsprechend mittels eines Balkendiagramms ausgeben und erhalten so eine Übersicht, zu welcher Tageszeit Quellcode committet wird.
In [26]:
commits_je_stunde.plot.bar()
Out[26]:
Wir beschriften nun zusätzlich die Grafik. Dazu speichern wir uns das Rückgabeobjekt der bar()
-Funktion in der Variable ax
. Hierbei handelt es sich um ein Axes
-Objekt der darunterliegenden Plotting-Bibliothek matplotlib
, durch das wir zusätzliche Eigenschaften des Plots beliebig anpassen können. Wir setzen hier
set_title("<titelname>")
set_xlabel("<x_achsenname>")
undset_ylabel<"y_achsenname>")
Als Ergebnis erhalten wir nun ein ausagekräftiges, beschriftetes Balkendiagramm.
In [27]:
ax = commits_je_stunde.plot.bar()
ax.set_title("Commits pro Stunde")
ax.set_xlabel("Tagesstunde")
ax.set_ylabel("Commits")
Out[27]:
Wir können auch nach Wochentagen auswerten. Dazu verwenden wir das weekday
-Attribut auf dem DateTime
-Attribut dt
. Die Werte sind hier 0-basiert mit Montag als ersten Wochentag. Wie üblich, lassen wir hier die Werte über value_counts
zählen, lassen die Werte aber nicht der Größe nach sortieren.
In [35]:
commits_je_wochentag = git_log.timestamp.dt.weekday.value_counts(sort=False)
commits_je_wochentag
Out[35]:
Das Ergebnis in commits_je_wochentag
lassen wir als ein Balkendiagramm mittels plot.bar()
ausgeben.
In [36]:
commits_je_wochentag.plot.bar()
Out[36]:
Nachfolgend wollen wir den Verlauf aller Commits über die letzten Jahre aufzeichnen lassen. Dazu setzen wir die timestamp
Spalte als Index mittels set_index('<spaltenname>')
. Zudem wählen wir lediglich die author
-Spalte. Dadurch arbeiten wir fortlaufend auf einer reinen Series
statt eines DataFrame
. Randnotiz: Die Verarbeitung mittels Series
folgt im Hinblick auf Statistikfunktionen fast analog wie bei einem DataFrame
. Eine Series
wird jedoch nicht so schön in einer Tabelle formatiert angezeigt, weshalb ich persönlich die Bearbeitung mittels DataFrame
bevorzuge.
In [30]:
git_timed = git_log.set_index('timestamp').author
git_timed.head()
Out[30]:
Über die resample("<zeiteinheit>")
-Funktion des DataFrame
s können wir nun Werte nach bestimmten Zeiteinheiten gruppieren wie z. B. nach Tage (D
), Monate (M
), Quartale (Q
) oder Jahre (A
). Wir verwenden hier ein resample("D")
für tageweises zählen. Zudem geben wir noch an, wie die Einzelwerte pro Zeiteinheit zusammengeführt werden sollen. Hierzu wählen wir die count()
-Funktion, um die Anzahl der Commits für jeden einzelnen Tag zu zählen.
In [31]:
commits_per_day = git_timed.resample("D").count()
commits_per_day.head()
Out[31]:
Um den Commit-Verlauf über die Jahre hinweg aufzuzeigen, bilden wir die kumulative Summe über alle Tageseinträge mittels cumsum()
. Damit werden alle Werte nacheinander aufsummiert.
In [32]:
commits_pro_tag_kumulativ = commits_per_day.cumsum()
commits_pro_tag_kumulativ.head()
Out[32]:
Das Ergebnis plotten wir nun als Liniendiagramm und erhalten somit die Anzahl der Commits über die Jahre hinweg aufgezeichnet.
In [33]:
commits_pro_tag_kumulativ.plot()
Out[33]:
Wir haben jetzt einige Grundlagen zu Pandas kennengelernt. Damit kommen wir schon sehr weit in der täglichen Arbeit. Die anderen wichtigen Themenbereiche, die nun noch fehlen, sind:
groupby
DataFrame
s mittels pivot_table
Ich hoffe, dass ich Dir mit diesem Mini-Tutorial das Potenzial von Datenanalysen mittels Python und Pandas näherbringen konnte. Über Anmerkungen und Feedback freue ich mich!