Ehrliche Diagramme
Kurz gesagt: Ein irreführendes Diagramm zeigt eine wahre Tabelle mit einem falschen Eindruck (abgeschnittene Achse, zwei Achsen, 3D); ein ehrliches beginnt Balken bei null und nennt die Schlussfolgerung im Titel.
Problem
„Der September ist eingebrochen“
Ein Kollege schickt ein Diagramm für das Treffen mit dem Chef: der Umsatz der letzten vier Monate. Der Balken des Septembers ist fast weg. Es sieht aus wie eine Katastrophe.
Der September ist wirklich schwächer: 7.128,80 € gegen 8.812,70 € im August. Warum, finden wir im Projekt am Ende des Kurses heraus. Hier ist die Frage eine andere: Wie viel schwächer sieht er im Diagramm aus, und wie viel schwächer ist er wirklich?
Die Idee
Balken beginnen bei null
Das Auge liest einen Balken an seiner Länge: Ein doppelt so langer Balken bedeutet doppelt so viel. Also muss die Länge die Zahl selbst sein. Das Diagramm des Kollegen beginnt bei 7.000 (set_ylim(7000, 10000)), und das heißt abgeschnittene Achse. Hier dieselben Zahlen mit einer Achse ab 7.000 und ab 0:
Der wirkliche Rückgang von August auf September ist 19,1 %. Links ist der Balken des Septembers 92,9 % kürzer als der des Augusts: Der Rückgang sieht 4,9-mal größer aus. Die abgeschnittene Achse entfernt die unteren 7.000 €, die sich die zwei Monate teilen, und lässt nur den Unterschied übrig.
matplotlib beginnt Balken von selbst bei 0. Eine abgeschnittene Achse kommt von set_ylim oder von Werkzeugen, die die Achse von allein „heranzoomen“. Wenn man den Unterschied zwischen den Balken ab 0 nicht sieht, ist er klein. Auch das ist eine Antwort.
Genauer, für Neugierige
Eine Linie ist anders: Sie liest man an ihrer Steigung und daran, wann sie steigt und fällt, nicht an der Länge. Die Achse einer Linie darf woanders als bei 0 beginnen, wenn sie beschriftet ist. Eine Linie, die sich um ein paar Prozent ändert, sieht auf einer Achse ab 0 fast flach aus, und die Veränderung ist nicht zu sehen.
Probier es aus
Das Diagramm korrigieren
Jemand hat den Umsatz nach Monat mit einer Achse ab 7.000 gezeichnet. Korrigiere das Diagramm: 12 Balken mit dem Umsatz jedes Monats (monthly, ohne die stornierten Bestellungen) und die y-Achse beginnend bei 0 und über den Dezember hinaus reichend.
Lösung
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Der Umsatz lässt die stornierten Bestellungen weg.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly)
ax.set_ylim(0, 17000)
ax.set_title('Umsatz nach Monat, €')
ax.tick_params(axis='x', rotation=90)
print(monthly.min().round(2), monthly.max().round(2))ax.bar(months, monthly) mit ax.set_ylim(0, 17000) zeigt den schwächsten Monat (7.128,80 €) knapp unter der Hälfte des stärksten (15.915,90 €), wie es ist. Mit einer Achse ab 7.000 verschwinden die schwachen Monate fast. Wenn du den Umsatz aus orders zeichnest, kommen auch die stornierten Bestellungen in die Balken.
Die Idee
Zwei y-Achsen
Der Umsatz liegt in Tausenden Euro, und die Bestellungen liegen in Hunderten. Um sie in ein Diagramm zu bringen, geben Leute der zweiten Linie oft eine eigene Achse rechts: ax.twinx() macht eine neue y-Achse, die sich die x-Achse mit der ersten teilt. Aber die Skala jeder Achse wird getrennt gewählt, und jede Linie dehnt sich, um ihre eigene zu füllen. So können zwei Linien verschmolzen oder auseinander aussehen, wie der Autor es will.
Die ehrliche Alternative ist ein Index: jeden Monat durch den ersten teilen und mit 100 multiplizieren. by_month / by_month.iloc[0] * 100 macht das für beide Spalten auf einmal (Broadcasting über die Spalten). Dann liegen beide Linien auf einer Achse und beginnen bei 100:
Links fallen die Linien fast zusammen, und Umsatz und Bestellungen scheinen sich gemeinsam zu bewegen. Rechts siehst du den Unterschied: Im Dezember ist der Umsatz 171,2 und die Bestellungen 136,7. Der Umsatz ist viel stärker gewachsen als die Bestellungen, weil die Bestellungen im Dezember auch größer sind. Das hast du auch in den Boxplots der vorigen Lektion gesehen.
Ein anderer ehrlicher Weg sind zwei Diagramme untereinander, jedes mit eigener Achse und Beschriftung: plt.subplots(2, 1, sharex=True).
Die Idee
3D, Kreisdiagramme und Bilder
Drei Dinge, die beeindruckend aussehen und in die Irre führen:
- 3D-Balken und 3D-Kreisdiagramme. Die Perspektive macht das Nahe größer als das Ferne. Die dritte Dimension trägt keine Daten; sie verzerrt nur.
- Kreisdiagramme. Das Auge vergleicht Winkel und Flächen viel schlechter als Längen. Du hast es in der Lektion über die Wahl des Diagramms gesehen: Sortierte Balken sagen dasselbe klarer.
- Ein Wert in der Größe eines Bildes. Wenn das Bild in Höhe und Breite wächst, wächst seine Fläche mit dem Quadrat: Doppelt so viel sieht aus wie viermal so viel.
Die Idee
Farbe für die Antwort
Farbe ist keine Dekoration: Sie sagt dir, wohin du sehen sollst. Eine Farbe für das, was zählt, und Grau für den Rest ist am klarsten. Hier der Umsatz nach Monat zweimal: links sind alle Balken gleich, rechts ist der Dezember gefärbt:
np.where(monthly.index.month == 12, '#E69F00', 'lightgray') liefert eine Farbe pro Monat: Orange für den Dezember, Grau für die anderen. color=colors legt sie auf die Balken.
Auch der Titel rechts wirkt: nicht „Umsatz nach Monat“, sondern die Schlussfolgerung: „Der Dezember bringt 14 % des Jahresumsatzes“ (genau 14,3 %). Der Leser versteht es, bevor er auf die Achsen schaut.
Das Orange #E69F00 stammt aus der Okabe-Ito-Palette: acht Farben, die auch Menschen unterscheiden können, die manche Farben nicht sehen. Viele Menschen können Rot nicht von Grün unterscheiden. Trenne also nie zwei Dinge nur durch Rot und Grün. Wenn du mehr Farben brauchst, ist auch die Palette tab10 von matplotlib (die Standardfarben, C0 bis C9) ein guter Anfang.
Die Idee
Beschriftungen statt Legende
Sechs Linien in sechs Farben und eine Legende in der Ecke: Das Auge springt zwischen Linie und Legende, um herauszufinden, welche welche ist. Eine Beschriftung direkt an der Linie spart das. Hier der Umsatz nach Kategorie, links mit einer Legende und rechts mit Beschriftungen:
ax.text(x, y, 'Küche') schreibt Text an den Punkt (x, y) der Daten. Die Monate sind Text, und matplotlib setzt sie auf die Positionen 0, 1, 2…, also liegt len(table) - 0.8 knapp rechts vom letzten Monat und 1.3 neben dem Dezember. Die Beschriftung der Küche hat das Blau ihrer Linie, und die Küche wird zuletzt und dicker gezeichnet, also liegt sie oben.
Im Dezember verkauft die Küche für 3.240,60 €, 4,4-mal so viel wie in ihrem durchschnittlichen Monat im Rest des Jahres. Meist verkaufen sich Pfannen und Messer, wahrscheinlich als Geschenke. Im linken Diagramm ist das eine von sechs Linien, im rechten ist es das Erste, was du siehst. Die Geschenkkarte hat keine Kategorie, deshalb ist sie nicht dabei.
Probier es aus
Farben
Übung
Das Diagramm für den Jahresbericht
Der Chef will ein Diagramm für den Jahresbericht: den Umsatz nach Monat (monthly), den Dezember in Farbe, die anderen Monate grau, die Achse ab 0 und einen Titel, der sagt, wie viel vom Jahresumsatz der Dezember bringt. Leg den Anteil in share ab und die Farben in colors.
Lösung
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Der Umsatz lässt die stornierten Bestellungen weg.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
share = monthly['2025-12-01'] / monthly.sum() * 100
colors = np.where(monthly.index.month == 12, '#E69F00', 'lightgray')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly, color=colors)
ax.set_ylim(0, 17000)
ax.set_title(f'Der Dezember bringt {share:.0f} % des Jahresumsatzes')
ax.tick_params(axis='x', rotation=90)Der Dezember bringt 14,3 % des Jahresumsatzes. Wenn jeder Monat gleich wäre, wäre es 1/12, etwa 8,3 %. np.where(monthly.index.month == 12, "#E69F00", "lightgray") färbt nur den Dezember, und f"Der Dezember bringt {share:.0f} % des Jahresumsatzes" setzt die Antwort in den Titel. Ein Titel wie „Umsatz nach Monat“ sagt nur, was auf den Achsen steht, und Balken, die alle gleich aussehen, lassen den Leser allein suchen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Breit und lang: pivot_table und melt · Datumswerte und Zeitreihen · Das erste Diagramm · Welches Diagramm für welche Frage
- Geht weiter in: Warum ist der Umsatz gefallen?
- Aufgaben zum Thema: Diagramme