Kundendaten visualisieren in Python mit matplotlib

Wenn es um Daten geht sind Diagramme häufig unverzichtbar für den Überblick, aber auch für das Verständnis. Im letzten Post haben wir einige hilfreiche Diagramme für Kundendaten vorgestellt. Hier wollen wir nun einen Einblick in den verwendeten Code geben.

Am schnellsten plottet man mit dem Submodul Pyplot, welches matplotlib bereitstellt. In der Dokumentation heißt es dazu:

matplotlib.pyplot is a state-based interface to matplotlib. It provides a MATLAB-like way of plotting. pyplot is mainly intended for interactive plots and simple cases of programmatic plot generation.

https://matplotlib.org/api/_as_gen/matplotlib.pyplot.html

Alternativ kann man mit matplotlib alles „from scratch“ machen, das heißt näher am internen Aufbau des Moduls zu arbeiten. Das bietet mehr Kontrolle, ist aber auch aufwändiger. Für unsere Anwendung wollen wir möglichst schnell brauchbare Diagramme erstellen, daher verwenden wir Pyplot.

Los geht’s mit dem Pareto Plot

Wir benötigen die folgenden Module:

import matplotlib.pyplot as plt # zum Plotten
import numpy as np
import pandas as pd

Unsere Daten sollen nach dem Pareto-Prinzip verteilt werden. In unserem Fall werden ca. 80% des Umsatzes von ca. 20% der Kunden generiert. Die Kurve, die wir mit dem Diagramm erhalten nennt man auch Lorenz-Kurve. Sie zeigt uns wie ungleich die Verteilung des Umsatzes auf die Kunden ist. Würden alle Kunden den gleichen Anteil zum Gesamtumsatz beitragen, dann würde man eine diagonale Gerade sehen (x=y). Je mehr sich die Kurve nach oben wölbt, desto ungleicher ist die Verteilung.

Mit numpy generieren wir nun Beispieldaten nach der Pareto-Verteilung, die wir dann als Umsatz pro Kunde interpretieren. Wir haben dann 1000 Kunden, die verschieden viel Umsatz generieren. Um die Daten für den Plot vorzubereiten sortieren wir die generierten Zahlen, so dass vorne die „größten Kunden“ stehen. Dann rechnen wir für jeden Wert den Anteil am Gesamtumsatz aus und bilden ein Array, in dem die Werte aufsummiert werden (numpy.cumsum). Die x-Achse soll den Kundenanteil wiedergeben, daher muss auch hier aufsummiert werden, und zwar einfach über 1/1000, da wir insgesamt 1000 Kunden haben.

n = 1000 # Für 1000 Datenpunkte
sales = np.random.pareto(1, n)  # Datengenerierung Pareto Verteilung

df = pd.DataFrame(sales ,columns=['sales']) # Erstellt pandas dataframe

df.sort_values(by='sales', ascending=False, inplace=True)

## In Prozent umrechnen und kummulieren, sodass die Daten bis 100% gehen
sales_cum = np.cumsum(100 * df.sales / sum(df.sales))

## Passende x-Werte, die ebenfalls bis zu 100% aufsummiert werden
x = 100*(np.cumsum(np.repeat(1/n,n))).round(decimals=4) 

sales_cum.index = x # x-Wert als Index im dataframe setzen

Am Ende haben wir eine Tabelle in Form eines pandas-dataframes, in dem die x-Werte im Index und die y-Werte in der einzigen Spalte stehen.

Nun zum Erstellen der Grafik

Der plot-Befehl erstellt aus den x,y-Paaren eine Kurve. Danach kommen die Beschriftungen, das Füllen der Fläche unter der Kurve und die gestrichelte Linie, die das 80-20 Verhältnis verdeutlicht. Genau genommen holen wir uns die Position der 20% Marke des Kunden-Arrays, um damit den entsprechenden Wert aus dem Umsatz-Array zu holen.

plt.plot(sales_cum, color='#FF9933') # Dieser Befehl reicht schon aus um einen einfachen Plot zu erhalten

# Nun bearbeiten wir den Plot, sodass er aussagekräftiger ist

plt.xlabel('Kunden in %') # Achsenbeschriftungen
plt.ylabel('Kumultativer Umsatz in %')
plt.suptitle('Pareto-Kurve') # Überschrift

# Die Fläche unter der Kurve und die gestrichelte Linie

plt.fill_between(x, sales_cum, color='#ffcfa0') # färbt die (horizontale) Fläche zwischen x-Achse und y-Werten

y_20 = np.where(x==20) # Speichert den Index vom x-Wert bei 20 % der Kunden
a, w = 0.7, 1 # a für die Transparenz, w für die Stärke der Linien

plt.vlines(20, 0, sales_cum.iloc[y_20], linestyles='dashed', Alpha=a, linewidth=w) # Vertikale Linie bei (x, ymin, ymax)
plt.hlines(sales_cum.iloc[y_20], 0, 20, linestyles='dashed', Alpha=a, linewidth=w) # Horizontale Linie bei (y, xmin, xmax)

plt.savefig('pareto.png',dpi=600)
plt.show()

Next: Balkendiagramm mit Monatsumsatz

Zunächst unsere Ausgangslage: Wir haben eine Tabelle, die die Verkäufe der letzten 12 Monate enthält, wo jeder Verkauf mit dem Datum in einer Zeile steht. Mit dem groupby-Befehl von pandas können wir die Verkaufsbeträge für jeden Monat aufsummieren. Dazu brauchen wir eine Spalte, die jeweils nur den Monat des Verkaufs enthält. Diese muss gegebenenfalls erst erstellt werden.

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("sales_data.csv") # Einlesen der CSV als Pandas-Dataframe; enthält bereits die Spalte "Month"
df_month_sum = df.groupby(['Month']).sum() # Summiert alle Werte für den jeweiligen Monat
y = df_month_sum['Sales']/1000 # Die y-Werte zum Plotten
months = range(1,13) # x-Werte brauchen wir auch

Nach der Vorverarbeitung der Daten, können diese jetzt einfach geplottet werden.

plt.bar(months, y, color='#FF9933') # gibt uns das Balkendiagramm in Rohform
plt.xticks(months) # damit alle Ticks beschriftet werden
plt.ylabel('Umsatz in 1000 €')
plt.xlabel('Monat')
plt.suptitle('Umsatz einer Produktgruppe innerhalb eines Jahres')
plt.savefig('umsatz.png',dpi=300)
plt.show()

Geografische Daten darstellen

Etwas aufwändiger ist es, wenn man Daten auf Karten abbilden will. Die geografischen Informationen haben wir von folgender Website bezogen: http://opendatalab.de/projects/geojson-utilities/# .

import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np

Zum Plotten der Karte aus der „.geojson“ Datei benutzen wir geopandas. Um die gewohnte Form der Bundesrepublik zu bekommen müssen wir den epsg-Wert des „coordinate reference system“ (crs) ändern. Dieser Wert beschreibt quasi die Stauchung der abzubildenden Länderformen. Der Grund für dieses Problem ist, dass es sich stets um eine (nicht eindeutige) Projektion handelt, wenn man Teile der kugelförmigen Erde als 2-D Grafik darstellen will. Verschiedene Projektionen geben uns verschiedene 2-D Versionen der Weltkugel. In der folgenden Abbildung kann man die Auswirkungen des epsg-Wertes gut nachvollziehen.

# Kartendaten einlesen
de = gpd.read_file('GEOJson/bundeslaender_simplify20.geojson')

# für die gewohnte "schlanke" Ansicht:
de = de.to_crs(epsg=3395)
Links mit epsg = 4326 , rechts mit epsg = 3395

Doch erstmal einen Schritt zurück. Beim Anschauen der geojson fällt auf, dass es manche Bundesländer mehrfach gibt. Ein Subset mit den ersten 16 Zeilen löst das Problem. Der iloc Befehl (pandas/geopandas) erlaubt es uns die Zeilen und Spalten auszuwählen, die wir behalten wollen. Der alleinstehende Doppelpunkt wählt alle Spalten-Werte aus. „:16“ wird als „von Anfang bis 16“ interpretiert, was dazu führt, dass wir die ersten 16 Zeilen erhalten.

Nun müssen wir jedem Bundesland den darzustellenden y-Wert zuordnen, indem wir eine neue Spalte zum dataframe hinzufügen. Die y-Werte können zum Beispiel für den Grad der Zielerreichung in % stehen. In unserem Beispiel weisen wir einfach jedem Bundesland einen Wert zu.

# Subset mit den ersten 16 Zeilen
de = de.iloc[:16,:]

# y-Werte für jedes Bundesland
bdl_scores = [80,105,90,85,95,65,80,105,85,120,110,106,97,115,92,104]

# Spalte mit den Scores erstellen
de['bdl_scores'] = bdl_scores

Der Plot-Befehl wird über die Geopandas API aufgerufen. Neben der Spalte mit den Scores wollen wir noch einen passenden Farbverlauf (Colormap) übergeben. Diesen haben wir auf https://matplotlib.org/3.2.1/tutorials/colors/colormaps.html gefunden. Um die Legende zu platzieren sind wir folgendermaßen vorgegangen:

Mit dem subplot2grid-Befehl konnten wir ein Grid erstellen, in das wir die Grafiken (Diagramm und Legende) dann eingefügt haben, was uns viel Kontrolle über die Position und Größe der beiden Grafiken gibt.

Die Form (shape) des Grids und die Position (loc) der aktuellen Grafik (genauer: des Axes-Objekts) werden über ein Tupel (Zeile, Spalte) angegeben. Mit colspan und rowspan können Zellen verbunden werden. Wenn wir wollen, dass ein Element die erste Zeile komplett ausfüllt, müssen wir loc = (0,0) und colspan = Breite setzen.

fig = plt.figure(figsize=(8,8)) # Öffnet eine Figure

b = 5 # Breite
h = 30 # Höhe

# Für die Karte (colspan und rowspan füllen fast das ganze Grid)
ax1 = plt.subplot2grid(
    shape=(h, b), 
    loc=(0, 0), 
    colspan=b, 
    rowspan=h-1
)

# für die Legende (sitzt in der Mitte der letzten Zeile des Grids)
ax2 = plt.subplot2grid(
    shape=(h, b), 
    loc=(h-1, 1), 
    colspan=3
)

# Achsen verstecken (das wäre ein Rahmen um die Karte)
ax1.axis(False)

# Legende bearbeiten (wird unten eingesetzt)
legend_kwds = {
    'label': "Zielerreichung in %", 
    'orientation': "horizontal",
}

# Die Karte plotten (geopandas hat ein eigenes Interface zu matplotlib)
de.plot(
    column = 'bdl_scores', 
    cmap = 'RdYlGn', # Colormap (Farbverlauf)
    ax = ax1, # Karte
    legend = True,
    cax = ax2, # Legende
    legend_kwds = legend_kwds
)

# eine Kontur an den Bundeslandgrenzen hinzufügen
de.geometry.boundary.plot(color=None,edgecolor='0',linewidth = .1,ax=ax1)

plt.suptitle('Grad der Zielerreichung in den Bundesländern')
plt.savefig('schland.png',dpi=800)
plt.show()

Damit haben wir die letzte Grafik erfolgreich programmiert.