Zum Inhalt

Datenvisualisierung & Statistisches Storytelling: Objektorientiertes Matplotlib & Seaborn unter Linux

Erstellt: 2026-11-08  ·  Zuletzt aktualisiert: 2026-11-08  ·  Nejat Hakan

Management Summary: Eine statistische Analyse oder ein Machine-Learning-Modell ist nur so wirkungsvoll wie die Klarheit, mit der Unsicherheiten, Verteilungen und Anomalien visuell vermittelt werden. Die veraltete, zustandsbehaftete MATLAB-Syntax (plt.plot()) erzeugt unübersichtliche, fehleranfällige Diagramme, die in automatisierten Pipelines versagen. Dieser Leitfaden etabliert professionelle Visualisierungsstandards auf Debian Linux mittels Matplotlibs objektorientierter (Figure/Axes) API, subplot_mosaic-Layouts, Seaborn-Verteilungsdiagrammen, wahrnehmungsgetreuen, farbenblindenfreundlichen Farbpaletten und Headless-Vektor-Rendering (SVG/PDF).

  • Objektorientierte Figure / Axes API


    Zustandsbehaftete plt.*-Nebeneffekte eliminieren und Achsen, Ticks, Rahmenlinien und Annotationen explizit via fig, ax = plt.subplots() steuern.

  • Echte Verteilungen statt Balkendiagramme


    Bimodale Verteilungen oder Ausreißer niemals hinter reinen Mittelwert-Balken verstecken – nutzen Sie ECDFs, Violin-Plots und gebootstrapte 95%-Konfidenzintervalle.

  • Wahrnehmungsgetreue Farbpaletten


    Irreführende Regenbogen-Farbskalen (jet) durch helligkeitsmonotone Farbräume wie viridis, cividis und die kategoriale Okabe-Ito-Palette ersetzen.

  • Headless-Rendering auf Debian-Servern


    Gestochen scharfe 300-DPI-PNGs und unbegrenzt skalierbare SVG/PDF-Berichte auf Debian-Servern ohne X11/Wayland-Display über das C++ Agg-Backend erzeugen.


1. Die Matplotlib-Objekthierarchie (Figure → Axes → Artist)

Matplotlib bietet zwei grundlegend verschiedene Schnittstellen: 1. Das zustandsbehaftete pyplot-Interface (plt.plot(), plt.title()): Verwaltet im Hintergrund eine globale State-Machine. In Produktionsskripten strikt vermeiden. 2. Das objektorientierte (OO) Interface (fig, ax = plt.subplots()): Erzeugt explizit ein Top-Level-Figure-Canvas mit autonomen Axes-Koordinatensystemen.

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    FIG["Figure Canvas Container<br/>fig = plt.figure()"]

    subgraph Mosaic["subplot_mosaic Layout"]
        direction TB
        AX1["Axes 'top_left'<br/>XAxis / YAxis / Spines"]
        AX2["Axes 'top_right'<br/>ECDF / Legend"]
        AX3["Axes 'bottom'<br/>Volle Breite Heatmap"]
        AX1 --> AX2 --> AX3
    end

    AGG["Headless Agg Backend<br/>fig.savefig('report.svg')"]

    FIG --> Mosaic
    Mosaic --> AGG

2. Die richtige visuelle Codierung wählen

Die Wahl des falschen Diagrammtyps verschleiert fundamentale statistische Fehler (berühmt demonstriert durch das Anscombe-Quartett, bei dem völlig unterschiedliche Daten identische Mittelwerte aufweisen).

Analytische Frage ❌ Irreführende Wahl ✅ Zuverlässiges statistisches Diagramm Seaborn / Matplotlib Primitiv
Verteilung über 3–8 Kategorien vergleichen Balkendiagramm der Mittelwerte mit Fehlerbalken Violin-Plot + Quartile oder Boxen/Strip-Plot sns.violinplot(inner="quart")
Tail-Latenzen vergleichen (p95, p99, p99.9) Histogramm (Extrem abhängig von der gewählten Bin-Breite) Empirische Verteilungsfunktion (ECDF) sns.ecdfplot() (Zeigt exakte Quantile ohne Bins!)
Bivariate Korrelation prüfen (N > 10.000) Standard-Streudiagramm (Leidet unter massivem Overplotting) 2D-Hexbin / KDE-Kontur + Randverteilungen sns.jointplot(kind="hex") oder ax.hexbin()
Metriken im Zeitverlauf mit Unsicherheit darstellen Spaghetti-Plot aus 20 verrauschten Rohkurven Linienplot mit 95%-Konfidenzintervall-Band sns.lineplot(errorbar=("ci", 95)) + ax.axhline()
Multi-Variablen-Korrelationsmatrix visualisieren Unsortierte quadratische Regenbogentabelle Maskierte Heatmap der unteren Dreiecksmatrix sns.heatmap(mask=np.triu(...), vmin=-1, vmax=1, center=0)

3. Headless-Rendering & Typografie unter Debian Linux

Für automatisierte tägliche Berichte innerhalb eines systemd-Timers oder rootless Podman-Containers ohne X11/Wayland-Server wird vor dem Import von pyplot das Agg-Backend aktiviert und freie Systemschriftarten installiert:

sudo apt update && sudo apt install -y \
    fonts-liberation2 \
    fonts-dejavu-core \
    fontconfig
fc-cache -fv

4. Multi-Panel Dashboard Workshop (subplot_mosaic)

Das moderne plt.subplot_mosaic() ersetzt unübersichtliche GridSpec-Berechnungen durch einen intuitiven ASCII-/Listen-Bauplan. Folgendes Skript erzeugt einen 3-teiligen Infrastruktur- und SLO-Report:

#!/usr/bin/env python3
# Datei: /opt/ds-ops/generate_slo_dashboard.py
import matplotlib

# Headless C++ Backend erzwingen vor pyplot-Import
matplotlib.use("Agg")

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns

# Farbenblindenfreundliche Okabe-Ito-Palette
OKABE_ITO = ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#F0E442", "#56B4E9"]


def configure_publication_style() -> None:
    sns.set_theme(style="whitegrid", palette=OKABE_ITO)
    plt.rcParams.update(
        {
            "figure.dpi": 150,
            "savefig.dpi": 300,
            "savefig.bbox": "tight",
            "font.family": "sans-serif",
            "axes.titlesize": 12,
            "axes.titleweight": "bold",
            "axes.labelsize": 10,
            "axes.spines.top": False,
            "axes.spines.right": False,
        }
    )


def synthesize_telemetry() -> pd.DataFrame:
    rng = np.random.default_rng(seed=2026)
    n = 3_000
    clusters = rng.choice(["PostgreSQL-Primary", "PgBouncer-Pool", "Rust-Axum-API"], size=n)
    base_latency = np.where(
        clusters == "PostgreSQL-Primary",
        rng.lognormal(mean=2.4, sigma=0.55, size=n),
        np.where(
            clusters == "PgBouncer-Pool",
            rng.lognormal(mean=1.2, sigma=0.35, size=n),
            rng.lognormal(mean=1.8, sigma=0.45, size=n),
        ),
    )
    return pd.DataFrame(
        {
            "timestamp": pd.date_range("2026-11-08 00:00", periods=n, freq="1min", tz="UTC"),
            "cluster": clusters,
            "latency_ms": base_latency,
            "cpu_pct": np.clip(20.0 + base_latency * 1.4 + rng.normal(0, 6, size=n), 5.0, 100.0),
        }
    )


def render_dashboard() -> None:
    configure_publication_style()
    df = synthesize_telemetry()

    # 2-Zeilen-Mosaik: Obere Zeile geteilt in [dist, ecdf], untere Zeile volle Breite [timeline, timeline]
    fig, axd = plt.subplot_mosaic(
        [
            ["dist", "ecdf"],
            ["timeline", "timeline"],
        ],
        figsize=(13, 8),
        constrained_layout=True,
    )

    # Panel 1 (Oben links): Violin-Verteilung mit Quartilen
    sns.violinplot(
        data=df,
        x="cluster",
        y="latency_ms",
        hue="cluster",
        inner="quart",
        cut=0,
        legend=False,
        ax=axd["dist"],
    )
    axd["dist"].set_title("A. Latenzverteilung & Quartile nach Cluster-Ebene")
    axd["dist"].set_xlabel("")
    axd["dist"].set_ylabel("Antwort-Latenz (ms)")

    # Panel 2 (Oben rechts): Exakte empirische Verteilungsfunktion (ECDF) mit SLO-Ziel
    sns.ecdfplot(data=df, x="latency_ms", hue="cluster", linewidth=2.0, ax=axd["ecdf"])
    axd["ecdf"].axhline(0.95, color="#D55E00", linestyle="--", linewidth=1.2, label="p95 SLO-Ziel")
    axd["ecdf"].axvline(30.0, color="#333333", linestyle=":", linewidth=1.2, label="30 ms Budget")
    axd["ecdf"].set_xlim(0, 60)
    axd["ecdf"].set_title("B. Empirische CDF (Exakte Tail-Quantile)")
    axd["ecdf"].set_xlabel("Antwort-Latenz (ms)")
    axd["ecdf"].set_ylabel("Kumulative Wahrscheinlichkeit")

    # Panel 3 (Unten volle Breite): Stündlich aggregierte Zeitreihe mit 95% KI-Band
    df_hourly = df.assign(hour=df["timestamp"].dt.floor("h"))
    sns.lineplot(
        data=df_hourly,
        x="hour",
        y="latency_ms",
        hue="cluster",
        errorbar=("ci", 95),
        linewidth=2.0,
        ax=axd["timeline"],
    )
    axd["timeline"].set_title("C. 48-Stunden Latenzverlauf (Mittelwert ± 95% Bootstrapped KI)")
    axd["timeline"].set_xlabel("UTC Zeitstempel")
    axd["timeline"].set_ylabel("Latenz (ms)")

    fig.suptitle("Infrastruktur-Telemetrie-Bericht — srv001.nejat-hakan.de", fontsize=14, fontweight="bold")
    fig.savefig("/var/reports/infrastructure_slo_dashboard.svg")
    fig.savefig("/var/reports/infrastructure_slo_dashboard.png")
    plt.close(fig)  # Essenziell: Speicherfreigabe des C++ Agg-Puffers!
    print("[OK] Vektor-SVG und 300-DPI PNG-Dashboards exportiert nach /var/reports/")


if __name__ == "__main__":
    render_dashboard()

In automatisierten Daemons immer plt.close(fig) aufrufen!

Selbst bei Nutzung des objektorientierten Interfaces hält Matplotlib Referenzen auf erstellte Figures im Speicher. Fehlt plt.close(fig) in periodischen Hintergrund-Tasks, entsteht ein permanenter Memory-Leak, der den Linux-OOM-Killer auslöst.


5. Farbpaletten-Leitfaden für Publikationen

  1. Sequenzielle Daten (0 → 100% CPU-Last, Latenz): Nutzen Sie helligkeitsmonotone Farbskalen (viridis, magma, plasma, cividis, crest). Diese bleiben auch in Schwarz-Weiß-Drucken oder bei Farbenblindheit lesbar.
  2. Divergierende Daten (-50% → 0% → +50% Delta): Verwenden Sie coolwarm, vlag oder icefire mit festem Mittelpunkt center=0.
  3. Direkte Canvas-Annotationen: Bei weniger als 4 Linien oder kritischen Ereignissen (z. B. "Kernel 6.12 Upgrade") annotieren Sie den Punkt direkt auf der Zeichenfläche (ax.annotate), um ständige Augenbewegungen zur Legende zu vermeiden.