Adapted from Marvin Ronsdorf on Unsplash

Bundesliga in Zahlen: Vor und nach dem Corona-Restart [Projekt]

Dies ist das Projekt zu meinem Artikel ‘Bundesliga in Zahlen: Vor und nach dem Corona-Restart’. In diesem habe ich beschrieben, wie sich bestimmte Leistungsstatistiken nach dem Restart der Bundesliga nach der Corona-Unterbrechung verändert haben. Es konnte gezeigt werden, dass einige Mannschaften klar bessere Leistungen abrufen konnten, andere nach der Pause wesentlich schlechter abschnitten. Allgemein ging außerdem der Heimvorteil verloren.

Da es sich dabei um eine statistische Auswertung handelt, zu der es technisch nicht viel zu erklären gibt, möchte ich hier vorrangig vorstellen, wie die Visualisierungen angefertigt wurden. Dabei handelt es sich um zwei kleine Tricks und Kniffe, die die Darstellungen und gezeigten Grafiken deutlich aufwerten. Hierbei liegt der Fokus auf dem Annotieren von Punkten und dem Plotten von Bildern, bzw. Logos, als Punkte in einem Graphen. Vor allem Letzteres ist äußerst hilfreich bei Präsentationen, in denen beispielsweise Firmen miteinander verglichen werden. Nicht nur wird der Plot grafisch ansprechender, ein schnelle Aufnahme der Informationen aus dem Plot wird extrem vereinfacht. Genutzt habe ich die populären Python-Plotting-Bibliothek Matplotlib, Pythons älteste und verbreitetste Plotting-Bibliothek, und Seaborn, eine Visualisierungsbibliothek welche auf Matplotlib beruht.

Die Daten

Zuerst möchte ich ein paar Sätze zu den Daten verlieren, auch wenn ich hier nicht im Detail darauf eingehen werde. Ursprünglich habe ich die Daten für ein anderes Projekt gesammelt, über das ich sicherlich später mal einen Artikel verfassen werde. Im Grunde wurden die Daten hauptsächlich von API-Football bezogen. Ergänzt wurden sie teilweise von den Daten, welche auf football-data.co.uk zur Verfügung gestellt wurden.

Annotieren von Punkten

Für die Bilder aus dem Artikel habe ich Seaborns pointplot-Funktion genutzt. Durch diese Funktion erhalten wir direkt die Mittelwerte der Datenpunkte, ohne sie in einem vorherigen Schritt berechnen zu müssen.

import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
ax = sns.pointplot(x=x_column, y=y_column, data=data, ci=None)
sns.despine()

Wir nutzen ci=None, um die Fehlerbalken auszublenden, der ax.despine()-Befehl entfernt die obere und die rechte Achse. Durch die pointplot-Funktion erhalten wir ein Matplotlib Axes Objekt, aus welchem wir die Koordinaten der Punkte extrahieren können.
An dieser Stelle möchte ich erwähnen, dass man das Ganze auch hätte andersherum aufziehen können. Also zuerst die Mittelwerte der Daten berechnen und dann plotten. So hätte man sich erspart, die entsprechende Werte aus dem Plot extrahieren zu müssen. Der Vorteil ist jedoch, dass weder die Ausgangsdaten verändert werden müssen, noch ein neuer DataFrame erstellt werden muss. So halten wir unser Skript bezüglich der Daten und Kopien davon sauberer. Ein weiterer Vorteil ist, dass diese Methode auch für andere Plottypen eingesetzt werden kann. Manchmal ist es einfacher, die Koordinaten im Nachhinein aus dem Plot zu holen als vorher selbst die nötigen Berechnungen durchzuführen. Die Besonderheit an diesem Plot: wir wollen nicht jeden Punkt annotieren, sondern nur die rechten. Dafür holen wir uns die y-Koordinaten aller Punkte einer Linie über line.get_ydata(). Dabei handelt es sich um eine Liste, die zusammen mit einem entsprechenden Eintrag aus der Liste der x-Koordinaten einen Punkt im Plot angibt. Da unsere Linien von 0 bis 1 auf der x-Achse gehen, sind wir an dem Eintrag bei Index 1 interessiert.

right_points = [line.get_ydata()[1] for line in ax.lines if np.array_equal(line.get_xdata(), np.array([0, 1]))]
colors = [line.get_color() for line in ax.lines if np.array_equal(line.get_xdata(), np.array([0, 1]))]

Wieso ist hier nun noch eine if-Bedingung vorhanden? Das liegt daran, dass ich zusätzlich noch einen einzelnen schwarzen Punkt geplottet habe, der im Ursprung des Koordinatensystems liegt und die anderen Farben überdecken soll. Wir wollen die Punkte natürlich nicht einfach in schwarzer Schrift annotieren, sondern es soll die Farbe der Punkte gewählt werden. Diese bekommen wir ganz ähnlich, wie oben zu sehen. Um die Punkte nun zu annotieren benutzen wir schlicht die Einträge aus der Legende. Die Legende selbst wird danach dann ausgeblendet. Eine Funktion, die hierfür sehr nützlich ist, aber von vielen nur sehr selten benutzt wird, ist enumerate. So erhalten wir nicht nur den Eintrag aus der Liste sonder auch seinen Index. Dies ermöglicht uns, dem Text die entsprechenden Koordinaten und die passende Farbe zuzuordnen. Den x-Wert setzten wir jeweils fest, dabei achten wir darauf, dass dieser leicht größer als 1 ist, damit der Text etwas nach rechts vom Punkt versetzt beginnt.

for i, label in enumerate(ax.legend().get_texts()):
    annotation = label.get_text()
    ax.text(1.07, right_points[i], annotation, color=colors[i])

Bilder als Punkte im Plot

Eine Frage, die ich mir häufig gestellt habe ist, wie ich Logos oder allgemein Bilder als Punkte plotten kann. Dies ist in Präsentationen sehr nützlich, wenn man beispielsweise die Werte von Firmen, oder wie in diesem Fall von Bundesligisten, vergleichen möchte. So ist man nicht ausschließlich auf die Farbe der Linie angewiesen. Der Betrachter kann also viel schneller die Informationen des Plots aufnehmen, da die Zuordnung der gezeigten Werte leichter und intuitiver wird. Die Funktion hierfür sieht folgendermaßen aus:

def imscatter(x, y, label, image_dict, ax=None):
    if ax is None:
        ax = plt.gca()
    x, y = np.atleast_1d(x, y)
    artists = []
    for x0, y0, l0 in zip(x, y, label):
        image = plt.imread(image_dict.get(l0, some_default_image_path))
        height, width = image.shape[0], image.shape[1]
        if 2*height > width:
            zoom = 20 / height
        else:
            zoom = 40 / width

        im = OffsetImage(image, zoom=zoom)
        ab = AnnotationBbox(im, (x0, y0), xycoords='data', frameon=False)
        artists.append(ax.add_artist(ab))
    ax.update_datalim(np.column_stack([x, y]))
    ax.autoscale()
    return artists

Gehen wir das einmal Zeile für Zeile durch. Zuerst wird geprüft, ob ein Matplotlib Axes Element an die Funktion übergeben wurde, anderenfalls wird aktuelle Instanz genommen. Danach durchlaufen wir die Koordinaten und dazugehörigen Label, die jeweils als einzelne Listen an die Funktion übergeben wurden. Das Label benutzen wir, um das entsprechende Bild einzulesen. Die Zuweisung zwischen Label und Datei-Pfad ist dabei in einem Dictionary gespeichert, welches der Funktion übergeben werden muss. Danach wird festgelegt, wie groß das Bild sein soll. Anstatt mit Hilfe von festen Größen diesen Zoom-Faktor auszurechnen, könnte alternativ dieser als Argument an die Funktion übergeben werden. Mit Hilfe der Klasse OffsetImage wird das Bild anschließend skaliert und dann durch eine Annotation dem Plot hinzugefügt. Dies geschieht durch den add_artists()-Befehl, Artists sind Objekte, die im Plot gerendert werden sollen. Schlussendlich fügen wir die neuen Koordinaten dem Axes-Objekt hinzu und skalieren anschließend, sodass alle relevanten Punkte im Plot zu sehen sind.

Zusammenfassung

Heute habe ich zwei kleine Kniffe gezeigt, die eure Darstellung erheblich aufwerten können. Zum Einen habe ich erklärt, wie man Punkte eines Matplotlib Axes Objekt annotiert und ich habe vorgestellt, wie sich Bilder in Plots als Punkte einbinden lassen. Ich hoffe, ihr findet es nützlich.
Den zugehörigen Artikel findet ihr hier.

Marian Biermann
Data Scientist

Blog

Wie haben sich die Corona-Unterbrechung und die leeren Stadien auf Leistungsstatistiken in der Bundesliga ausgewirkt? Welche Teams haben profitiert, welche haben sich schwerer getan?