Hvorfor er datavisualisering så afgørende i Python?
Du kan have det reneste datasæt i verden og den mest elegante pandas-pipeline – men hvis du ikke kan vise hvad dataene fortæller, er det hele lidt ligegyldigt. Ærligt talt, tabeller med tusindvis af rækker siger sjældent noget til nogen. Et godt diagram? Det siger alt.
Python har to biblioteker, der tilsammen dækker stort set alle behov for datavisualisering: matplotlib og seaborn. Matplotlib giver dig total kontrol over hvert eneste pixel i din graf. Seaborn bygger ovenpå og gør det nemt at skabe statistisk informative og visuelt flotte diagrammer med minimal kode. De supplerer hinanden rigtig godt.
I denne guide gennemgår vi begge biblioteker fra bunden med rigtige pandas DataFrames. Alt kode kan kopieres direkte ind i din Jupyter Notebook eller Python-fil – ingen halvfærdige eksempler her.
Opsætning: Installation og import
Først skal vi have de nødvendige pakker installeret. Kør dette i din terminal:
pip install matplotlib seaborn pandas numpy
Derefter importerer vi standardpakkerne. Denne importblok er stort set universel i Python-datavidenskab, så du kommer til at se den overalt:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Sæt seaborns standardstil – forbedrer alle plots markant
sns.set_theme(style="whitegrid")
print(f"matplotlib version: {plt.matplotlib.__version__}")
print(f"seaborn version: {sns.__version__}")
Vi bruger sns.set_theme() med det samme, fordi det automatisk giver pænere akser, skrifttyper og farver. Uden det ser matplotlib-plots ret spartanske ud (for at sige det mildt).
Opret testdata som pandas DataFrame
Lad os oprette et realistisk datasæt, vi kan bruge gennem hele guiden. Vi simulerer salgsdata for en dansk webshop:
np.random.seed(42)
n = 365
datoer = pd.date_range(start="2025-01-01", periods=n, freq="D")
salgsdata = pd.DataFrame({
"dato": datoer,
"omsaetning": np.cumsum(np.random.normal(5000, 2000, n)).clip(min=0),
"antal_ordrer": np.random.poisson(lam=45, size=n),
"kategori": np.random.choice(
["Elektronik", "Tøj", "Bolig", "Sport", "Bøger"], size=n
),
"region": np.random.choice(
["København", "Aarhus", "Odense", "Aalborg"], size=n,
p=[0.4, 0.25, 0.2, 0.15]
),
"kundetilfredshed": np.random.uniform(3.0, 5.0, size=n).round(1)
})
salgsdata["maaned"] = salgsdata["dato"].dt.month_name()
salgsdata["ugedag"] = salgsdata["dato"].dt.day_name()
print(salgsdata.head(10))
print(f"\nShape: {salgsdata.shape}")
print(f"Kolonner: {list(salgsdata.columns)}")
Nu har vi et DataFrame med 365 rækker daglige salgsdata – inklusiv omsætning, ordreantal, kategorier, regioner og kundetilfredshed. Det er nok til at dække alle de plottyper, vi skal igennem.
Matplotlib: Det grundlæggende du skal kende
Matplotlib kan virke lidt overvældende i starten. Der er nemlig to måder at bruge det på: den pyplot-baserede tilgang (hurtig og nem) og den objektorienterede tilgang (mere kontrol). Vi bruger primært den objektorienterede, fordi den er langt bedre til alt ud over de allermest simple plots.
Lad mig vise dig hvad jeg mener.
Linjediagram: Omsætning over tid
fig, ax = plt.subplots(figsize=(12, 5))
ax.plot(salgsdata["dato"], salgsdata["omsaetning"],
color="#2563eb", linewidth=0.8, alpha=0.6, label="Daglig omsætning")
# Tilføj 30-dages glidende gennemsnit
salgsdata["oms_30d"] = salgsdata["omsaetning"].rolling(window=30).mean()
ax.plot(salgsdata["dato"], salgsdata["oms_30d"],
color="#dc2626", linewidth=2, label="30-dages gennemsnit")
ax.set_title("Daglig omsætning med glidende gennemsnit", fontsize=14, fontweight="bold")
ax.set_xlabel("Dato")
ax.set_ylabel("Omsætning (DKK)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Nøglen her er fig, ax = plt.subplots(). Det opretter en figur og et akseobjekt, som du derefter kan tilpasse frit. Meget mere fleksibelt end bare at kalde plt.plot() direkte – og du vænner dig hurtigt til syntaksen.
Søjlediagram: Omsætning per kategori
kategori_sum = salgsdata.groupby("kategori")["omsaetning"].sum().sort_values(ascending=True)
fig, ax = plt.subplots(figsize=(10, 5))
farver = ["#3b82f6", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6"]
ax.barh(kategori_sum.index, kategori_sum.values, color=farver)
# Tilføj værdier på søjlerne
for i, (vaerdi, navn) in enumerate(zip(kategori_sum.values, kategori_sum.index)):
ax.text(vaerdi + 5000, i, f"{vaerdi:,.0f} DKK", va="center", fontsize=10)
ax.set_title("Samlet omsætning per kategori (2025)", fontsize=14, fontweight="bold")
ax.set_xlabel("Omsætning (DKK)")
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
plt.tight_layout()
plt.show()
Horisontale søjlediagrammer er næsten altid bedre end vertikale, når du har kategorinavne. De er simpelthen nemmere at læse, fordi teksten ikke skal roteres. En lille detalje, men den gør en overraskende stor forskel for læsbarheden.
Scatterplot: Ordrer vs. omsætning
fig, ax = plt.subplots(figsize=(8, 6))
scatter = ax.scatter(
salgsdata["antal_ordrer"],
salgsdata["omsaetning"],
c=salgsdata["kundetilfredshed"],
cmap="RdYlGn",
alpha=0.6,
s=30
)
plt.colorbar(scatter, ax=ax, label="Kundetilfredshed")
ax.set_title("Sammenhæng mellem ordrer og omsætning", fontsize=14, fontweight="bold")
ax.set_xlabel("Antal ordrer")
ax.set_ylabel("Omsætning (DKK)")
plt.tight_layout()
plt.show()
Her bruger vi c-parameteren til at farvelægge punkterne efter kundetilfredshed og cmap="RdYlGn" til at vise en rød-gul-grøn farveskala. Resultatet? Tre dimensioner af information i ét enkelt plot. Det er ret effektivt.
Subplots: Flere grafer i én figur
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Plot 1: Linjediagram
axes[0, 0].plot(salgsdata["dato"], salgsdata["antal_ordrer"], color="#2563eb", linewidth=0.5)
axes[0, 0].set_title("Daglige ordrer")
axes[0, 0].set_ylabel("Antal")
# Plot 2: Histogram
axes[0, 1].hist(salgsdata["kundetilfredshed"], bins=20, color="#10b981", edgecolor="white")
axes[0, 1].set_title("Fordeling af kundetilfredshed")
axes[0, 1].set_xlabel("Score")
# Plot 3: Boxplot per region
regioner = salgsdata.groupby("region")["omsaetning"].apply(list)
axes[1, 0].boxplot(regioner.values, labels=regioner.index)
axes[1, 0].set_title("Omsætning per region")
axes[1, 0].set_ylabel("DKK")
# Plot 4: Cirkeldiagram
kategori_fordeling = salgsdata["kategori"].value_counts()
axes[1, 1].pie(kategori_fordeling, labels=kategori_fordeling.index,
autopct="%1.0f%%", startangle=90)
axes[1, 1].set_title("Kategorifordeling")
fig.suptitle("Salgsoverblik 2025", fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()
Med plt.subplots(2, 2) opretter vi et 2×2-gitter af plots. Det er uvurderligt til dashboards og rapporter, hvor du skal vise flere perspektiver samtidig. Husk altid plt.tight_layout() til sidst – det forhindrer, at tekst og akser overlapper hinanden. Jeg har glemt det nok gange til at vide, at det aldrig er sjovt at debugge.
Seaborn: Statistisk visualisering gjort nemt
Så, lad os snakke om seaborn. Det er bygget ovenpå matplotlib og er designet til at arbejde direkte med pandas DataFrames. I stedet for at sende arrays sender du bare kolonnenavne som strenge og peger på dit DataFrame med data=-parameteren.
Og ærligt? Det er fantastisk.
Distributionsplot med histplot og kdeplot
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Histogram med KDE-kurve
sns.histplot(data=salgsdata, x="kundetilfredshed", bins=25,
kde=True, color="#6366f1", ax=axes[0])
axes[0].set_title("Fordeling af kundetilfredshed")
# KDE-plot opdelt per kategori
sns.kdeplot(data=salgsdata, x="omsaetning", hue="kategori",
fill=True, alpha=0.3, ax=axes[1])
axes[1].set_title("Omsætningsfordeling per kategori")
plt.tight_layout()
plt.show()
Seaborns histplot() med kde=True giver dig både et histogram og en blød fordelingskurve i ét kald. Med hue-parameteren opdeler du automatisk efter en kategorisk kolonne – det ville kræve mindst ti linjer ekstra kode i ren matplotlib. Tro mig, jeg har prøvet.
Boxplot og violinplot: Sammenlign fordelinger
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Boxplot: omsætning per region
sns.boxplot(data=salgsdata, x="region", y="omsaetning",
palette="Set2", ax=axes[0])
axes[0].set_title("Omsætning per region (boxplot)")
axes[0].set_ylabel("Omsætning (DKK)")
# Violinplot: kundetilfredshed per kategori
sns.violinplot(data=salgsdata, x="kategori", y="kundetilfredshed",
palette="muted", inner="quart", ax=axes[1])
axes[1].set_title("Kundetilfredshed per kategori (violin)")
axes[1].set_ylabel("Score")
axes[1].tick_params(axis="x", rotation=30)
plt.tight_layout()
plt.show()
Violinplots viser mere end boxplots: du kan se hele fordelingens form, ikke bare kvartilerne. Parameteren inner="quart" tilføjer kvartillinjer inden i violinen, så du får det bedste fra begge verdener. Personligt foretrækker jeg dem til eksplorativ analyse – de afslører bare mere.
Heatmap: Korrelationer og mønstre
# Beregn korrelationsmatrix for numeriske kolonner
numerisk = salgsdata[["omsaetning", "antal_ordrer", "kundetilfredshed"]]
korrelation = numerisk.corr()
fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(korrelation, annot=True, fmt=".2f", cmap="coolwarm",
center=0, square=True, linewidths=1,
cbar_kws={"shrink": 0.8}, ax=ax)
ax.set_title("Korrelationsmatrix", fontsize=14, fontweight="bold")
plt.tight_layout()
plt.show()
En korrelations-heatmap er noget af det første, du bør lave, når du udforsker et nyt datasæt. Den afslører sammenhænge mellem variable med det samme – og nogle gange overraskende sammenhænge. annot=True skriver korrelationsværdierne ind i cellerne, og center=0 sørger for, at farveaksen er symmetrisk omkring nul.
Countplot og barplot: Kategoriske data
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Countplot: antal observationer per kategori og region
sns.countplot(data=salgsdata, x="kategori", hue="region",
palette="viridis", ax=axes[0])
axes[0].set_title("Ordrer per kategori og region")
axes[0].legend(title="Region", fontsize=8)
axes[0].tick_params(axis="x", rotation=30)
# Barplot: gennemsnitlig tilfredshed per kategori
sns.barplot(data=salgsdata, x="kategori", y="kundetilfredshed",
palette="rocket", ci=95, ax=axes[1])
axes[1].set_title("Gennemsnitlig kundetilfredshed (95% CI)")
axes[1].set_ylabel("Score")
axes[1].tick_params(axis="x", rotation=30)
plt.tight_layout()
plt.show()
Seaborns barplot() beregner automatisk gennemsnittet og viser et konfidensinterval som de sorte fejllinjer. Det er en vigtig statistisk detalje, som ren matplotlib bare ikke giver dig uden en del ekstra arbejde.
Pairplot: Overblik over alle variable på én gang
# Pairplot er perfekt til eksplorativ analyse
g = sns.pairplot(
salgsdata[["omsaetning", "antal_ordrer", "kundetilfredshed", "kategori"]],
hue="kategori",
palette="husl",
diag_kind="kde",
plot_kws={"alpha": 0.5, "s": 20}
)
g.fig.suptitle("Pairplot: Alle numeriske variable", y=1.02, fontsize=14)
plt.show()
Med pairplot() ser du alle mulige kombinationer af scatterplots mellem dine numeriske variable – plus fordelingerne på diagonalen. Det er en eksplorativ guldgrube. Jeg bruger det næsten hver gang jeg starter med et nyt datasæt, fordi det kan afsløre mønstre, du aldrig ville have fundet ved bare at stirre på tal.
Avancerede teknikker: FacetGrid og relplot
Når du vil opdele dine visualiseringer efter en eller flere kategoriske variable, er seaborns FacetGrid-system virkelig svært at slå. I stedet for manuelt at oprette subplots genererer seaborn dem automatisk for dig.
relplot: Relationelle plots opdelt per kategori
# Scatterplots opdelt per region
g = sns.relplot(
data=salgsdata,
x="antal_ordrer",
y="omsaetning",
col="region",
col_wrap=2,
hue="kategori",
palette="deep",
alpha=0.6,
height=4,
aspect=1.2
)
g.fig.suptitle("Ordrer vs. omsætning per region", y=1.02, fontsize=14)
g.set_axis_labels("Antal ordrer", "Omsætning (DKK)")
plt.show()
col="region" opretter automatisk et separat plot for hver region, og col_wrap=2 sørger for, at de vises i et 2-kolonne gitter. Prøv at gøre det samme i ren matplotlib – det tager mindst fire gange så mange linjer kode. Ikke en overdrivelse.
catplot: Kategoriske plots med facets
g = sns.catplot(
data=salgsdata,
x="kategori",
y="kundetilfredshed",
col="region",
kind="box",
col_wrap=2,
height=4,
aspect=1.2,
palette="Set2"
)
g.set_xticklabels(rotation=45)
g.fig.suptitle("Tilfredshed per kategori og region", y=1.02, fontsize=14)
plt.show()
Med catplot() kan du skifte mellem boxplot, violinplot, stripplot og swarmplot bare ved at ændre kind-parameteren. Det gør det utroligt hurtigt at eksperimentere med forskellige visninger af de samme data – og det er jo dét, eksplorativ analyse handler om.
Tilpasning og styling: Gør dine grafer professionelle
Standard matplotlib-plots ser, ja... ikke altid helt præsentable ud. Her er de vigtigste teknikker til at få dem til at se publikationsklare ud.
Seaborn temaer og kontekster
# Forskellige temaer
fig, axes = plt.subplots(1, 4, figsize=(20, 4))
temaer = ["whitegrid", "darkgrid", "white", "dark"]
for ax, tema in zip(axes, temaer):
with sns.axes_style(tema):
sns.histplot(salgsdata["kundetilfredshed"], bins=15, ax=ax, color="#6366f1")
ax.set_title(f"Tema: {tema}")
plt.tight_layout()
plt.show()
# Kontekster styrer skriftstørrelser
# "paper" < "notebook" (standard) < "talk" < "poster"
sns.set_theme(context="talk", style="whitegrid")
Brug context="talk" til præsentationer og context="poster" til store print. Det skalerer automatisk alle skriftstørrelser og linjetykkelser op, så dine plots er læselige på afstand. En lille ting, men det sparer dig for en masse manuel justering.
Brugerdefineret farvepalette
# Definer en brugerdefineret palette
firma_farver = ["#1a56db", "#057a55", "#ff5a1f", "#9061f9", "#e74694"]
sns.set_palette(firma_farver)
fig, ax = plt.subplots(figsize=(10, 5))
sns.barplot(data=salgsdata, x="kategori", y="omsaetning",
ci=None, ax=ax)
ax.set_title("Omsætning per kategori (firmafarver)")
ax.set_ylabel("Omsætning (DKK)")
ax.tick_params(axis="x", rotation=30)
plt.tight_layout()
plt.show()
Gem plots i høj kvalitet
# Gem som PNG i høj opløsning
fig.savefig("omsaetning_per_kategori.png", dpi=300, bbox_inches="tight",
facecolor="white", transparent=False)
# Gem som SVG til web
fig.savefig("omsaetning_per_kategori.svg", format="svg", bbox_inches="tight")
# Gem som PDF til print
fig.savefig("omsaetning_per_kategori.pdf", format="pdf", bbox_inches="tight")
Brug altid dpi=300 for print og bbox_inches="tight" for at undgå afklippet tekst. SVG-formatet er ideelt til webapplikationer, fordi det skalerer uden kvalitetstab. PDF er selvfølgelig standarden til trykte rapporter.
Praktisk eksempel: Komplet salgsdashboard
Nu samler vi alt det, vi har gennemgået, i ét samlet dashboard. Det her er den slags figur, du faktisk ville præsentere for din chef eller inkludere i en rapport:
fig = plt.figure(figsize=(16, 12))
# Layout med GridSpec for fleksibelt gitter
gs = fig.add_gridspec(3, 2, hspace=0.35, wspace=0.3)
# 1. Omsætning over tid (fuld bredde)
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(salgsdata["dato"], salgsdata["omsaetning"], alpha=0.4, color="#94a3b8")
ax1.plot(salgsdata["dato"], salgsdata["oms_30d"], color="#1a56db", linewidth=2)
ax1.set_title("Omsætningsudvikling 2025", fontsize=13, fontweight="bold")
ax1.set_ylabel("DKK")
ax1.legend(["Daglig", "30-dages snit"], loc="upper left")
# 2. Omsætning per kategori
ax2 = fig.add_subplot(gs[1, 0])
kat_sum = salgsdata.groupby("kategori")["omsaetning"].sum().sort_values()
ax2.barh(kat_sum.index, kat_sum.values, color=firma_farver)
ax2.set_title("Omsætning per kategori", fontsize=12, fontweight="bold")
ax2.set_xlabel("DKK")
# 3. Fordeling af kundetilfredshed
ax3 = fig.add_subplot(gs[1, 1])
sns.histplot(data=salgsdata, x="kundetilfredshed", bins=20,
kde=True, color="#057a55", ax=ax3)
ax3.set_title("Kundetilfredshed", fontsize=12, fontweight="bold")
ax3.axvline(salgsdata["kundetilfredshed"].mean(), color="#dc2626",
linestyle="--", label=f'Gns: {salgsdata["kundetilfredshed"].mean():.1f}')
ax3.legend()
# 4. Boxplot per region
ax4 = fig.add_subplot(gs[2, 0])
sns.boxplot(data=salgsdata, x="region", y="omsaetning",
palette="Set2", ax=ax4)
ax4.set_title("Omsætning per region", fontsize=12, fontweight="bold")
ax4.set_ylabel("DKK")
# 5. Heatmap: ordrer per ugedag og kategori
pivot = salgsdata.pivot_table(index="ugedag", columns="kategori",
values="antal_ordrer", aggfunc="mean")
dag_orden = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
pivot = pivot.reindex(dag_orden)
ax5 = fig.add_subplot(gs[2, 1])
sns.heatmap(pivot, annot=True, fmt=".0f", cmap="YlOrRd",
linewidths=0.5, ax=ax5)
ax5.set_title("Gns. ordrer per dag og kategori", fontsize=12, fontweight="bold")
ax5.set_ylabel("")
fig.suptitle("Salgsdashboard – Dansk Webshop 2025",
fontsize=16, fontweight="bold", y=0.98)
plt.show()
Dette dashboard kombinerer fem forskellige plottyper i én figur med GridSpec. Det øverste plot spænder over begge kolonner, mens de fire resterende plots deler to rækker. Det er denne slags sammensatte figurer, der gør datapræsentationer overbevisende – og som får folk til at lytte.
Typiske fejl og hvordan du undgår dem
Jeg har lavet mere end min rimelige andel af plotfejl gennem årene. Her er de mest almindelige – og den hurtige løsning på hver:
- Overlapende tekst: Kald altid
plt.tight_layout()eller brugbbox_inches="tight"ved gem. For x-akselabels, brugrotation=45ogha="right". - For mange farver: Hold dig til 5-7 farver maksimalt i ét plot. Brug seaborns paletter som "Set2" eller "husl" frem for at vælge tilfældige farver.
- Manglende plt.show(): I scripts (ikke Jupyter) skal du altid kalde
plt.show()for at vise figuren. I Jupyter kan du tilføje%matplotlib inlinei toppen af din notebook. - Hukommelseslæk: Matplotlib gemmer alle figurer i hukommelsen. Kald
plt.close("all")efter at have gemt en figur, især i løkker. Det her har bidt mig mere end én gang. - Forkert figur-størrelse: Standard
figsizeer ofte for lille. Start med(10, 6)for enkeltplots og(14, 10)for subplots.
Ofte stillede spørgsmål
Hvad er forskellen mellem matplotlib og seaborn?
Matplotlib er det grundlæggende plotbibliotek i Python, der giver dig fuld kontrol over hvert element i en figur. Seaborn er bygget ovenpå matplotlib og tilbyder et højere abstraktionsniveau med fokus på statistisk visualisering. Brug matplotlib, når du har brug for detaljeret tilpasning, og seaborn, når du hurtigt vil udforske data eller skabe statistiske plots. I praksis bruger de fleste begge biblioteker sammen – og det giver også bedst mening.
Kan jeg bruge seaborn uden at kende matplotlib?
Du kan komme langt med seaborn alene, men du vil før eller siden have brug for matplotlib til finpudsning. Ting som aksetitler, figur-størrelse, annotations og layout-justering kræver matplotlib-kald. Så ja, lær begge dele – men du kan roligt starte med seaborn og tilføje matplotlib-viden undervejs.
Hvordan vælger jeg den rigtige plottype til mine data?
Som tommelfingerregel: brug linjediagrammer til tidsserier, søjlediagrammer til kategoriske sammenligninger, scatterplots til relationer mellem to numeriske variable, histogrammer eller KDE-plots til fordelinger, og heatmaps til korrelationer eller matrixer. Hvis du er i tvivl, start med et pairplot – det giver overblik over alle kombinationer på én gang.
Hvordan gør jeg mine matplotlib-plots klar til præsentation?
Brug sns.set_theme(context="talk") for automatisk skalering af tekst. Gem altid med dpi=300 og bbox_inches="tight". Fjern unødvendige akselinjer med ax.spines["top"].set_visible(False). Og brug ensartede farver – begræns antallet af dataserier per plot til 5-7 styk.
Hvad er det bedste alternativ til matplotlib til interaktive grafer?
Til interaktive visualiseringer i browseren er Plotly det mest populære valg i Python. Det understøtter zoom, panorering og hover-tooltips ud af boksen. For dashboards er Streamlit eller Dash gode valg, der begge integrerer med Plotly. Matplotlib har dog også interaktive funktioner via %matplotlib widget i Jupyter-notebooks, men Plotly er nok det mere naturlige valg til web.