Wprowadzenie — dlaczego wizualizacja danych jest fundamentem data science?
Kto nie szukał kiedyś odpowiedzi w surowych tabelach z tysiącami wierszy, scrollując w nieskończoność, żeby wychwycić jakiś wzorzec? Ja szukałem — i to nie raz. Powiem Ci szczerze, to jest droga donikąd. Ludzki mózg nie jest zoptymalizowany do czytania kolumn z liczbami. Jest za to świetnie przystosowany do rozpoznawania wzorców wizualnych. Jeden dobrze zrobiony wykres potrafi powiedzieć więcej niż dwadzieścia tabel.
Wizualizacja danych to nie jest "ładny dodatek" do analizy. To fundament. Jest obecna na każdym etapie pracy z danymi: od wstępnej eksploracji (EDA), przez diagnostykę modeli ML, aż po prezentację wyników klientowi czy zarządowi. Bez niej pracujesz na ślepo. (A przynajmniej marnujesz czas, który mógłbyś spędzić produktywniej.)
W tym przewodniku pokażę Ci, jak efektywnie tworzyć wizualizacje w Pythonie, korzystając z matplotlib 3.10 i seaborn 0.13 — dwóch bibliotek, które w 2026 roku nadal stanowią absolutny rdzeń ekosystemu wizualizacji w Pythonie. Omówimy wszystko: od podstaw, przez nowości w najnowszych wersjach, po praktyczne wzorce, które stosuję na co dzień w prawdziwych projektach.
Jeśli pracujesz już z pandas do czyszczenia danych albo budujesz potoki ML w scikit-learn — wizualizacja jest naturalnym uzupełnieniem tych umiejętności. Dane trzeba zobaczyć, zanim zacznie się je modelować. Zaufaj mi w tym.
Instalacja i konfiguracja środowiska
Instalacja bibliotek
Zaczynamy od podstaw. Potrzebujesz czterech bibliotek — matplotlib, seaborn, pandas i numpy.
Seaborn korzysta z matplotlib pod spodem, więc technicznie instalując seaborn, dostajesz też matplotlib jako zależność. Mimo to wolę instalować jawnie — lepiej wiedzieć, co masz:
pip install matplotlib seaborn pandas numpy
Po instalacji warto zweryfikować wersje. W kontekście tego przewodnika pracujemy z matplotlib 3.10.x (seria 3.10 wystartowała 13 grudnia 2024, najnowsza stabilna to 3.10.8), seaborn 0.13.2, a w tle NumPy 2.4 i pandas 3.0:
import matplotlib
import seaborn as sns
import pandas as pd
import numpy as np
print(f"matplotlib: {matplotlib.__version__}") # 3.10.8
print(f"seaborn: {sns.__version__}") # 0.13.2
print(f"pandas: {pd.__version__}") # 3.0.x
print(f"numpy: {np.__version__}") # 2.4.x
Konfiguracja dla Jupyter Notebook
Jeśli pracujesz w Jupyter Notebook (a większość z nas pracuje, przynajmniej na etapie eksploracji), kilka magicznych komend na starcie oszczędzi Ci mnóstwo frustracji:
%matplotlib inline
# Wykresy w wyższej rozdzielczości (retina) — ogromna różnica na ekranach HiDPI
%config InlineBackend.figure_format = 'retina'
import matplotlib.pyplot as plt
# Domyślny rozmiar figury — 10x6 cali to mój standard
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['figure.dpi'] = 100
# Polskie znaki w etykietach bez problemów
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['axes.unicode_minus'] = False
Ta konfiguracja retina to coś, co odkryłem parę lat temu i od tamtej pory nie wyobrażam sobie pracy bez niej. Domyślne wykresy w Jupyter wyglądają rozmazanie — z retina są ostre i czytelne.
Matplotlib od podstaw — kluczowe koncepcje
Anatomia wykresu — Figure, Axes i Artists
Zanim zaczniesz rysować wykresy, musisz zrozumieć trzy fundamentalne pojęcia matplotlib. Bez tego będziesz stale walczył z biblioteką zamiast z nią współpracować.
- Figure — to cały "płótno". Kontener najwyższego poziomu, który może zawierać jeden lub wiele wykresów. Pomyśl o nim jak o kartce papieru.
- Axes — to pojedynczy wykres wewnątrz Figure. I tu uwaga, bo nazwa jest myląca: to nie jest "osie" (axes w sensie x, y), a cały obszar wykresu z danymi, etykietami i legendą. Tak, wiem, naming w matplotlib potrafi frustrować.
- Artists — to dosłownie wszystko, co widzisz na wykresie: linie, punkty, tekst, etykiety osi, tytuły. Każdy element wizualny to Artist.
import matplotlib.pyplot as plt
import numpy as np
# Tworzenie Figure z jednym Axes
fig, ax = plt.subplots(figsize=(10, 6))
# Dane
miesiace = np.arange(1, 13)
sprzedaz = [12500, 14200, 13800, 16100, 17500, 19200, 18400, 20100, 21500, 19800, 22300, 25000]
# Rysowanie na obiekcie Axes
ax.plot(miesiace, sprzedaz, marker='o', linewidth=2, color='#2196F3')
ax.set_title('Sprzedaż miesięczna w 2025 roku', fontsize=14, fontweight='bold')
ax.set_xlabel('Miesiąc')
ax.set_ylabel('Sprzedaż (PLN)')
ax.set_xticks(miesiace)
ax.set_xticklabels(['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze',
'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'])
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Kluczowy wzorzec to fig, ax = plt.subplots(). Ta jedna linijka tworzy Figure i Axes jednocześnie, dając Ci pełną kontrolę nad obydwoma obiektami.
Dwa podejścia — pyplot vs obiektowe API
Matplotlib oferuje dwa style pisania kodu. Pierwszy to pyplot — proceduralny interfejs inspirowany MATLAB-em. Drugi to obiektowe API — jawna praca z obiektami Figure i Axes. Oba działają, ale — nie będę owijał w bawełnę — moja rekomendacja jest jednoznaczna.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
# ---- Podejście 1: pyplot (szybkie, ale ograniczone) ----
plt.figure(figsize=(8, 4))
plt.plot(x, np.sin(x), label='sin(x)')
plt.plot(x, np.cos(x), label='cos(x)')
plt.title('Podejście pyplot')
plt.legend()
plt.show()
# ---- Podejście 2: obiektowe API (zalecane) ----
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, np.sin(x), label='sin(x)')
ax.plot(x, np.cos(x), label='cos(x)')
ax.set_title('Podejście obiektowe')
ax.legend()
plt.show()
Pyplot jest OK do szybkiego podglądu jednego wykresu w notebooku. Ale do czegokolwiek poważniejszego — wiele paneli, precyzyjne formatowanie, wykresy do publikacji — używaj obiektowego API. Zawsze wiesz, na którym obiekcie Axes operujesz. Koniec z tajemniczymi efektami ubocznymi, kiedy plt.title() zmienia tytuł nie tego wykresu, co trzeba.
Podstawowe typy wykresów
Przejdźmy przez pięć najczęściej używanych typów wykresów. Każdy z przykładem, który możesz skopiować i uruchomić od razu — bo taka jest idea.
Wykres liniowy — idealny do danych czasowych i trendów:
import matplotlib.pyplot as plt
dni = list(range(1, 31))
temperatura = [2, 3, 1, -1, 0, 2, 4, 5, 7, 6, 8, 10, 9, 11, 12, 10, 8, 9, 11, 13, 14, 12, 10, 9, 7, 6, 8, 10, 11, 12]
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(dni, temperatura, color='#E53935', linewidth=2, marker='.')
ax.set_title('Temperatura w Warszawie — marzec 2026', fontsize=13)
ax.set_xlabel('Dzień miesiąca')
ax.set_ylabel('Temperatura (°C)')
ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Wykres punktowy (scatter) — do badania zależności między dwiema zmiennymi:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
doswiadczenie = np.random.uniform(1, 15, 50)
wynagrodzenie = 4000 + 800 * doswiadczenie + np.random.normal(0, 1500, 50)
fig, ax = plt.subplots(figsize=(9, 6))
scatter = ax.scatter(doswiadczenie, wynagrodzenie, c=doswiadczenie,
cmap='viridis', s=60, alpha=0.8, edgecolors='white')
ax.set_title('Doświadczenie vs wynagrodzenie', fontsize=13)
ax.set_xlabel('Lata doświadczenia')
ax.set_ylabel('Wynagrodzenie brutto (PLN)')
fig.colorbar(scatter, ax=ax, label='Lata doświadczenia')
plt.tight_layout()
plt.show()
Wykres słupkowy — do porównywania kategorii:
import matplotlib.pyplot as plt
miasta = ['Warszawa', 'Kraków', 'Wrocław', 'Gdańsk', 'Poznań', 'Łódź']
mediana_wynagrodzen = [9500, 8200, 8000, 7800, 7600, 6800]
fig, ax = plt.subplots(figsize=(9, 5))
kolory = ['#1976D2', '#388E3C', '#F57C00', '#7B1FA2', '#C62828', '#00838F']
ax.bar(miasta, mediana_wynagrodzen, color=kolory, edgecolor='white', linewidth=0.8)
ax.set_title('Mediana wynagrodzeń IT w polskich miastach (2026)', fontsize=13)
ax.set_ylabel('Wynagrodzenie brutto (PLN)')
for i, v in enumerate(mediana_wynagrodzen):
ax.text(i, v + 100, f'{v:,}', ha='center', fontsize=10)
plt.tight_layout()
plt.show()
Histogram — do badania rozkładu zmiennej:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
wiek = np.concatenate([
np.random.normal(28, 4, 300),
np.random.normal(40, 6, 200)
])
fig, ax = plt.subplots(figsize=(9, 5))
ax.hist(wiek, bins=30, color='#26A69A', edgecolor='white', alpha=0.85)
ax.set_title('Rozkład wieku uczestników ankiety', fontsize=13)
ax.set_xlabel('Wiek')
ax.set_ylabel('Liczba osób')
ax.axvline(np.median(wiek), color='red', linestyle='--', label=f'Mediana: {np.median(wiek):.1f}')
ax.legend()
plt.tight_layout()
plt.show()
Wykres pudełkowy (boxplot) — do porównywania rozkładów z uwzględnieniem outlierów:
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
dane = {
'Warszawa': np.random.normal(9500, 2000, 100),
'Kraków': np.random.normal(8200, 1800, 100),
'Wrocław': np.random.normal(8000, 1700, 100),
'Gdańsk': np.random.normal(7800, 1600, 100),
}
fig, ax = plt.subplots(figsize=(9, 5))
bp = ax.boxplot(dane.values(), labels=dane.keys(), patch_artist=True,
boxprops=dict(facecolor='#B3E5FC', edgecolor='#0288D1'),
medianprops=dict(color='#D32F2F', linewidth=2))
ax.set_title('Rozkład wynagrodzeń IT wg miast', fontsize=13)
ax.set_ylabel('Wynagrodzenie brutto (PLN)')
ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
Nowości w matplotlib 3.10 — co warto znać w 2026 roku?
No dobra, matplotlib 3.10 to nie jest rewolucja, ale kilka zmian jest naprawdę istotnych. Seria 3.10 wystartowała 13 grudnia 2024 i od tego czasu doczekała się serii poprawek — najnowsza stabilna wersja to 3.10.8. Przejdźmy przez najważniejsze nowości.
Nowy cykl kolorów petroff10 — dostępność przede wszystkim
To jedna z moich ulubionych zmian w tej wersji. Domyślny cykl kolorów w matplotlib (tab10) jest z nami od lat i — powiedzmy sobie szczerze — nie jest idealny pod kątem dostępności dla osób z zaburzeniami widzenia barw. Nowy cykl petroff10 został zaprojektowany z myślą o maksymalnej rozróżnialności kolorów.
Co ciekawe, petroff10 powstawał dwuetapowo: najpierw algorytm zoptymalizował kolory pod kątem dostępności dla osób z daltonizmem, a następnie model ML wytrenowany na danych z crowdsourcingu ocenił ich estetykę. Efekt? Paleta, która jest zarówno czytelna, jak i ładna.
import matplotlib.pyplot as plt
import numpy as np
# Przełączenie na nowy cykl kolorów petroff10
plt.rcParams['axes.prop_cycle'] = plt.cycler(color=plt.cm.colors.TABLEAU_COLORS.values())
# Albo bezpośrednio
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
x = np.linspace(0, 10, 100)
# Domyślny cykl (tab10)
ax1.set_prop_cycle(plt.cycler(color=plt.colormaps['tab10'].colors))
for i in range(6):
ax1.plot(x, np.sin(x + i * 0.5), linewidth=2, label=f'Seria {i+1}')
ax1.set_title('Domyślny cykl: tab10')
ax1.legend(fontsize=9)
# Nowy cykl petroff10
ax2.set_prop_cycle(plt.cycler(color=plt.colormaps['petroff10'].colors))
for i in range(6):
ax2.plot(x, np.sin(x + i * 0.5), linewidth=2, label=f'Seria {i+1}')
ax2.set_title('Nowy cykl: petroff10')
ax2.legend(fontsize=9)
plt.tight_layout()
plt.show()
Szczerze? Jeszcze nie zdecydowałem, czy przełączam się na petroff10 jako domyślny we wszystkich projektach, ale do raportów i prezentacji — zdecydowanie tak. Dostępność to nie jest "nice to have", to wymóg profesjonalnej wizualizacji.
Ciemne mapy kolorów — berlin, managua, vanimo
Matplotlib 3.10 wprowadził nowe dywergentne mapy kolorów z kolekcji Fabio Crameri (Scientific colour maps v8.0.1): berlin, managua i vanimo. Czym się wyróżniają? Są zaprojektowane tak, że punkt neutralny (środek skali) jest ciemny, a nie jasny — co świetnie sprawdza się w trybach ciemnych i na ciemnych tłach.
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
dane = np.random.randn(20, 20)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, nazwa in zip(axes, ['berlin', 'managua', 'vanimo']):
im = ax.imshow(dane, cmap=nazwa, aspect='auto')
ax.set_title(f'cmap: {nazwa}', fontsize=12)
fig.colorbar(im, ax=ax, shrink=0.8)
plt.suptitle('Nowe ciemne mapy kolorów w matplotlib 3.10', fontsize=14, y=1.02)
plt.tight_layout()
plt.show()
Te mapy są perceptualnie jednolite i przyjazne dla daltonistów. Jeśli robisz heatmapy, wykresy konturowe albo obrazy naukowe — przetestuj je.
Parametr orientation zamiast vert
Drobna, ale ważna zmiana ergonomiczna. W funkcjach takich jak boxplot() i violinplot() dotychczasowy parametr vert=False został zastąpiony przez orientation='horizontal'. Stary parametr jest oznaczony jako deprecated.
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(42)
dane = [np.random.normal(loc, 2, 100) for loc in [5, 7, 6, 8]]
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# Stary sposób (deprecated)
# ax1.boxplot(dane, vert=False)
# Nowy sposób — orientation
ax1.boxplot(dane, orientation='horizontal')
ax1.set_title('boxplot z orientation="horizontal"')
ax1.set_yticklabels(['Q1', 'Q2', 'Q3', 'Q4'])
ax2.violinplot(dane, orientation='horizontal')
ax2.set_title('violinplot z orientation="horizontal"')
ax2.set_yticklabels(['Q1', 'Q2', 'Q3', 'Q4'])
plt.tight_layout()
plt.show()
Inne ulepszenia
Matplotlib 3.10 przyniósł też kilka mniejszych, ale użytecznych zmian:
- Wsparcie DataFrame w
ax.table()— teraz możesz przekazać pandas DataFrame bezpośrednio do metodytable(), bez ręcznej konwersji. Niby drobiazg, ale oszczędza kilka linijek kodu w każdym raporcie. FillBetweenPolyCollectionzset_data()— kolekcje zfill_between()mają teraz metodęset_data(), co ułatwia aktualizację danych w animacjach i wykresach interaktywnych.- Tworzenie Axes szybsze o 20–25% — jeśli generujesz dużo małych paneli (np. siatka 10x10 subplotów), poczujesz różnicę.
- Zwiększone limity renderera Agg — z 2^16 na 2^23 pikseli. Oznacza to, że możesz teraz generować ogromne obrazy (np. do druku plakatów naukowych) bez obcinania.
Seaborn — wizualizacja statystyczna na wyższym poziomie
Dlaczego seaborn, skoro jest matplotlib?
Słyszę to pytanie regularnie — i jest zasadne. Ale odpowiedź jest prosta: seaborn nie zastępuje matplotlib — on go uzupełnia. Seaborn jest zbudowany na matplotlib i oferuje API wyższego poziomu, zaprojektowane specjalnie do wizualizacji statystycznej z danymi tabelarycznymi.
Co konkretnie daje Ci seaborn ponad matplotlib?
- Automatyczne stylowanie — wykresy od razu wyglądają profesjonalnie, bez godzin konfiguracji
rcParams. - Natywna praca z DataFrame — przekazujesz nazwę kolumny jako string, a seaborn sam wyciąga dane z DataFrame. Zero ręcznego indeksowania.
- Wykresy statystyczne out of the box — rozkłady, regresja, korelacje, porównania kategorii — wszystko w jednej linijce kodu.
- Inteligentny hue/style/size — grupowanie danych po dodatkowych zmiennych jednym parametrem.
Podstawowe wykresy w seaborn
Zobaczmy seaborn w akcji. Przygotujmy najpierw realistyczny zbiór danych:
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
n = 200
df = pd.DataFrame({
'wiek': np.random.randint(22, 60, n),
'doswiadczenie': np.random.randint(1, 25, n),
'wynagrodzenie': np.random.normal(9000, 3000, n).clip(4000, 25000).round(0),
'miasto': np.random.choice(['Warszawa', 'Kraków', 'Wrocław', 'Gdańsk'], n),
'stanowisko': np.random.choice(['Junior', 'Mid', 'Senior', 'Lead'], n, p=[0.25, 0.35, 0.3, 0.1]),
})
# Korekta wynagrodzenia — wyższe dla wyższych stanowisk
mnozniki = {'Junior': 0.7, 'Mid': 1.0, 'Senior': 1.4, 'Lead': 1.8}
df['wynagrodzenie'] = (df['wynagrodzenie'] * df['stanowisko'].map(mnozniki)).round(0)
Histogram z krzywą gęstości — sns.histplot():
fig, ax = plt.subplots(figsize=(9, 5))
sns.histplot(data=df, x='wynagrodzenie', hue='stanowisko', kde=True,
bins=25, alpha=0.6, ax=ax)
ax.set_title('Rozkład wynagrodzeń wg stanowiska', fontsize=13)
ax.set_xlabel('Wynagrodzenie brutto (PLN)')
ax.set_ylabel('Liczba osób')
plt.tight_layout()
plt.show()
Wykres punktowy z grupowaniem — sns.scatterplot():
fig, ax = plt.subplots(figsize=(9, 6))
sns.scatterplot(data=df, x='doswiadczenie', y='wynagrodzenie',
hue='stanowisko', style='miasto', s=60, alpha=0.7, ax=ax)
ax.set_title('Doświadczenie vs wynagrodzenie', fontsize=13)
ax.set_xlabel('Lata doświadczenia')
ax.set_ylabel('Wynagrodzenie brutto (PLN)')
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
Wykres pudełkowy — sns.boxplot():
fig, ax = plt.subplots(figsize=(10, 5))
sns.boxplot(data=df, x='stanowisko', y='wynagrodzenie', hue='miasto',
order=['Junior', 'Mid', 'Senior', 'Lead'],
palette='Set2', ax=ax)
ax.set_title('Wynagrodzenia wg stanowiska i miasta', fontsize=13)
ax.set_xlabel('Stanowisko')
ax.set_ylabel('Wynagrodzenie brutto (PLN)')
plt.tight_layout()
plt.show()
Heatmapa korelacji — sns.heatmap():
fig, ax = plt.subplots(figsize=(7, 5))
kolumny_numeryczne = df.select_dtypes(include=[np.number])
korelacja = kolumny_numeryczne.corr()
sns.heatmap(korelacja, annot=True, fmt='.2f', cmap='coolwarm',
center=0, linewidths=0.5, ax=ax)
ax.set_title('Macierz korelacji', fontsize=13)
plt.tight_layout()
plt.show()
Nowości w seaborn 0.13 — native_scale i wsparcie dla Polars
Seaborn 0.13 wprowadził kilka zmian, które warto znać. To nie jest ogromna aktualizacja, ale parę rzeczy naprawdę poprawia codzienną pracę.
Parametr native_scale=True — to zmiana, na którą czekałem. Jeśli masz kolumnę numeryczną używaną jako kategorię (np. liczba pokoi: 1, 2, 3, 4), seaborn domyślnie traktował ją jak etykiety tekstowe i rozmieszczał równomiernie. Z native_scale=True pozycje na osi odpowiadają rzeczywistym wartościom liczbowym:
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
df_mieszkania = pd.DataFrame({
'liczba_pokoi': np.random.choice([1, 2, 3, 4, 6], 150),
'cena_m2': np.random.normal(12000, 3000, 150).clip(5000, 25000),
})
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
sns.stripplot(data=df_mieszkania, x='liczba_pokoi', y='cena_m2',
native_scale=False, alpha=0.5, ax=ax1)
ax1.set_title('native_scale=False (domyślnie)')
sns.stripplot(data=df_mieszkania, x='liczba_pokoi', y='cena_m2',
native_scale=True, alpha=0.5, ax=ax2)
ax2.set_title('native_scale=True')
plt.tight_layout()
plt.show()
Zwróć uwagę na drugą oś — odstęp między 4 a 6 pokojami jest teraz proporcjonalny do rzeczywistej różnicy.
Wsparcie dla Polars — seaborn 0.13 obsługuje teraz DataFrame z biblioteki Polars dzięki protokołowi wymiany danych (dataframe exchange protocol). Jeśli pracujesz z Polars (a w 2026 roku coraz więcej osób tak robi), możesz przekazywać ramki Polars bezpośrednio do funkcji seaborn bez ręcznej konwersji na pandas.
Pozostałe istotne zmiany w seaborn 0.13:
- Przepisane funkcje kategoryczne —
boxplot(),violinplot(),stripplot(),swarmplot()zostały zrefaktoryzowane, co poprawia wydajność i spójność zachowania. BoxPlotContainer— nowy obiekt zwracany przezboxplot(), który ułatwia programowy dostęp do elementów wykresu po jego stworzeniu (mediany, wąsy, outliery).
Interfejs seaborn.objects — deklaratywna przyszłość
Moduł seaborn.objects to eksperymentalny, ale coraz stabilniejszy interfejs inspirowany Grammar of Graphics (to samo podejście co ggplot2 w R). Zamiast wywoływać gotowe funkcje jak sns.scatterplot(), budujesz wykres deklaratywnie — warstwa po warstwie.
Podstawowy wzorzec: so.Plot(data, x=..., y=...).add(so.Mark())
import seaborn.objects as so
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'doswiadczenie': np.random.randint(1, 20, 100),
'wynagrodzenie': np.random.normal(10000, 3000, 100).clip(4000, 22000),
'stanowisko': np.random.choice(['Junior', 'Mid', 'Senior'], 100),
})
# Prosty scatter plot
(
so.Plot(df, x='doswiadczenie', y='wynagrodzenie', color='stanowisko')
.add(so.Dot(alpha=0.7))
.label(x='Lata doświadczenia', y='Wynagrodzenie (PLN)', color='Stanowisko')
.layout(size=(9, 5))
.show()
)
Siła tego interfejsu ujawnia się przy bardziej złożonych wizualizacjach — agregacja, grupowanie i faceting w jednym łańcuchu:
import seaborn.objects as so
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'miasto': np.random.choice(['Warszawa', 'Kraków', 'Wrocław', 'Gdańsk'], 200),
'stanowisko': np.random.choice(['Junior', 'Mid', 'Senior'], 200),
'wynagrodzenie': np.random.normal(9000, 3000, 200).clip(4000, 20000),
})
# Średnie wynagrodzenie — słupki pogrupowane (dodged) z facetami
(
so.Plot(df, x='stanowisko', y='wynagrodzenie', color='miasto')
.add(so.Bar(), so.Agg('mean'), so.Dodge())
.facet('miasto', wrap=2)
.label(x='Stanowisko', y='Średnie wynagrodzenie (PLN)', color='Miasto')
.layout(size=(10, 8))
.show()
)
Interfejs seaborn.objects jest nadal oznaczony jako eksperymentalny, ale jest już na tyle stabilny, że korzystam z niego w projektach eksploracyjnych. Dla raportów produkcyjnych trzymam się jeszcze klasycznego API — ale kierunek rozwoju seaborn jest jasny.
Praktyczne wzorce wizualizacji dla data science
Szybka eksploracja danych (EDA) z seaborn
EDA to moment, kiedy seaborn naprawdę błyszczy. Bez przesady.
Trzy wykresy, które robię prawie w każdym projekcie na samym początku:
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Przygotowanie danych
np.random.seed(42)
n = 300
df = pd.DataFrame({
'wiek': np.random.randint(22, 60, n),
'doswiadczenie': np.random.randint(1, 25, n),
'wynagrodzenie': np.random.normal(10000, 3500, n).clip(4000, 28000).round(0),
'godziny_tygodniowo': np.random.normal(42, 5, n).clip(30, 60).round(1),
'stanowisko': np.random.choice(['Junior', 'Mid', 'Senior', 'Lead'], n, p=[0.25, 0.35, 0.3, 0.1]),
})
mnozniki = {'Junior': 0.7, 'Mid': 1.0, 'Senior': 1.35, 'Lead': 1.75}
df['wynagrodzenie'] = (df['wynagrodzenie'] * df['stanowisko'].map(mnozniki)).round(0)
# 1. Pairplot — przegląd wszystkich zależności naraz
sns.pairplot(df, hue='stanowisko', diag_kind='kde',
plot_kws={'alpha': 0.5, 's': 30},
height=2.5, aspect=1.1)
plt.suptitle('Pairplot — przegląd zależności', y=1.01, fontsize=14)
plt.show()
# 2. Heatmapa korelacji
fig, ax = plt.subplots(figsize=(7, 5))
korelacja = df.select_dtypes(include=[np.number]).corr()
maska = np.triu(np.ones_like(korelacja, dtype=bool))
sns.heatmap(korelacja, mask=maska, annot=True, fmt='.2f',
cmap='RdBu_r', center=0, vmin=-1, vmax=1,
linewidths=0.5, ax=ax)
ax.set_title('Macierz korelacji (trójkąt dolny)', fontsize=13)
plt.tight_layout()
plt.show()
# 3. Rozkłady zmiennych numerycznych
kolumny_num = ['wiek', 'doswiadczenie', 'wynagrodzenie', 'godziny_tygodniowo']
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
for ax, kol in zip(axes.flat, kolumny_num):
sns.histplot(data=df, x=kol, kde=True, ax=ax, color='#5C6BC0')
ax.set_title(f'Rozkład: {kol}')
plt.suptitle('Rozkłady zmiennych numerycznych', fontsize=14, y=1.01)
plt.tight_layout()
plt.show()
Te trzy wykresy — pairplot, heatmapa korelacji, histogramy rozkładów — dają Ci solidny przegląd danych w mniej niż minutę. To jest mój standardowy "rytuał startowy" w każdym nowym projekcie.
Wykresy gotowe do publikacji z matplotlib
Eksploracja to jedno, ale prędzej czy później musisz przygotować wykres, który trafi do raportu, prezentacji albo publikacji naukowej. Tutaj liczy się każdy detal: odpowiednie fonty, czytelne etykiety, właściwe DPI.
import matplotlib.pyplot as plt
import numpy as np
# Dane
kwartaly = ['Q1 2025', 'Q2 2025', 'Q3 2025', 'Q4 2025', 'Q1 2026']
przychod = [2.4, 2.8, 3.1, 3.5, 3.9]
koszty = [1.8, 2.0, 2.1, 2.3, 2.4]
zysk = [p - k for p, k in zip(przychod, koszty)]
# Konfiguracja dla publikacji
plt.rcParams.update({
'font.size': 11,
'axes.titlesize': 14,
'axes.labelsize': 12,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'legend.fontsize': 10,
'figure.titlesize': 16,
})
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(kwartaly))
szerokosc = 0.25
slupki_przychod = ax.bar(x - szerokosc, przychod, szerokosc, label='Przychód',
color='#1565C0', edgecolor='white')
slupki_koszty = ax.bar(x, koszty, szerokosc, label='Koszty',
color='#EF6C00', edgecolor='white')
slupki_zysk = ax.bar(x + szerokosc, zysk, szerokosc, label='Zysk netto',
color='#2E7D32', edgecolor='white')
# Etykiety na słupkach
for slupki in [slupki_przychod, slupki_koszty, slupki_zysk]:
for slupek in slupki:
wysokosc = slupek.get_height()
ax.annotate(f'{wysokosc:.1f}',
xy=(slupek.get_x() + slupek.get_width() / 2, wysokosc),
xytext=(0, 4), textcoords='offset points',
ha='center', va='bottom', fontsize=9)
ax.set_title('Wyniki finansowe — przychód, koszty, zysk', fontweight='bold')
ax.set_xlabel('Kwartał')
ax.set_ylabel('Wartość (mln PLN)')
ax.set_xticks(x)
ax.set_xticklabels(kwartaly)
ax.legend(loc='upper left')
ax.grid(True, axis='y', alpha=0.3)
ax.set_ylim(0, max(przychod) * 1.25)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
# Zapis w wysokiej rozdzielczości
fig.savefig('wyniki_finansowe.png', dpi=300, bbox_inches='tight',
facecolor='white', edgecolor='none')
fig.savefig('wyniki_finansowe.pdf', bbox_inches='tight') # PDF dla LaTeX
print("Wykresy zapisane: wyniki_finansowe.png (300 DPI) i .pdf")
plt.show()
Kluczowe szczegóły: dpi=300 dla druku (72 wystarczy na ekran), bbox_inches='tight' żeby nie obcinać etykiet, ukryte górna i prawa ramka (spines) dla czystszego wyglądu. Te detale robią różnicę między wykresem "z notebooka" a wykresem do publikacji.
Method chaining — wykresy bezpośrednio z pandas
Mało kto o tym pamięta (albo nie wie), ale pandas ma wbudowane metody do tworzenia wykresów. Pod spodem to i tak matplotlib, ale składnia jest niesamowicie zwięzła — idealna do szybkiego podglądu danych w jednej linijce:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(42)
df = pd.DataFrame({
'miesiac': pd.date_range('2025-01', periods=12, freq='MS'),
'sprzedaz': np.random.randint(50000, 120000, 12),
'zwroty': np.random.randint(2000, 8000, 12),
})
df = df.set_index('miesiac')
# Jedna linijka — wykres liniowy
df.plot(figsize=(10, 5), title='Sprzedaż i zwroty w 2025', grid=True)
plt.ylabel('Wartość (PLN)')
plt.tight_layout()
plt.show()
# Jedna linijka — wykres słupkowy
df['sprzedaz'].plot.bar(figsize=(10, 5), color='#42A5F5', edgecolor='white',
title='Sprzedaż miesięczna')
plt.ylabel('Wartość (PLN)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Method chaining z operacjami na danych
(
df
.assign(marza=lambda x: x['sprzedaz'] - x['zwroty'])
.loc[:, ['marza']]
.plot(figsize=(10, 4), title='Marża miesięczna', color='#66BB6A', grid=True)
)
plt.ylabel('Marża (PLN)')
plt.tight_layout()
plt.show()
To podejście jest genialne do EDA — żadnego importowania, żadnych dodatkowych linijek. Po prostu df.plot() i widzisz dane.
Style i personalizacja wykresów
Gotowe arkusze stylów matplotlib
Matplotlib ma wbudowane arkusze stylów, które zmieniają wygląd wszystkich wykresów jedną linijką. Serio, jedną linijką. Wielu początkujących o tym nie wie — a szkoda, bo to ogromna oszczędność czasu.
import matplotlib.pyplot as plt
import numpy as np
# Zobaczenie wszystkich dostępnych stylów
print(plt.style.available)
# ['Solarize_Light2', '_classic_test_patch', '_mpl-gallery', ...,
# 'bmh', 'classic', 'dark_background', 'fivethirtyeight',
# 'ggplot', 'seaborn-v0_8', 'tableau-colorblind10', ...]
# Ustawienie stylu globalnie
plt.style.use('ggplot')
# Albo tymczasowo — tylko dla jednego wykresu
x = np.linspace(0, 10, 100)
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
style_lista = ['ggplot', 'seaborn-v0_8-whitegrid', 'fivethirtyeight', 'bmh']
for ax, styl in zip(axes.flat, style_lista):
with plt.style.context(styl):
ax.plot(x, np.sin(x), linewidth=2)
ax.plot(x, np.cos(x), linewidth=2)
ax.set_title(f'Styl: {styl}', fontsize=11)
ax.grid(True)
plt.suptitle('Porównanie stylów matplotlib', fontsize=14, y=1.01)
plt.tight_layout()
plt.show()
Moje ulubione style na co dzień: seaborn-v0_8-whitegrid do prezentacji, bmh do raportów, ggplot kiedy chcę estetykę znaną z R. Metoda plt.style.context() jest genialna, bo pozwala na tymczasowe przełączenie stylu bez wpływu na resztę notebooka.
Palety kolorów w seaborn
Seaborn oferuje bogaty zestaw palet kolorów, podzielonych na trzy kategorie: jakościowe (qualitative — dla kategorii), sekwencyjne (sequential — dla wartości ciągłych) i dywergentne (diverging — dla wartości z naturalnym punktem środkowym).
import seaborn as sns
import matplotlib.pyplot as plt
# Przegląd wbudowanych palet
fig, axes = plt.subplots(3, 1, figsize=(10, 6))
# Jakościowa — dla kategorii
paleta_kat = sns.color_palette('Set2', 8)
sns.palplot(paleta_kat)
plt.title('Jakościowa: Set2')
# Sekwencyjna — dla wartości ciągłych
paleta_seq = sns.color_palette('YlOrRd', 10)
# Dywergentna — dla wartości z centrum
paleta_div = sns.color_palette('RdBu_r', 11)
# Tworzenie własnej palety
moja_paleta = sns.color_palette(['#264653', '#2A9D8F', '#E9C46A',
'#F4A261', '#E76F51'])
# Ustawienie palety globalnie
sns.set_palette(moja_paleta)
# Użycie w praktyce
import numpy as np
import pandas as pd
np.random.seed(42)
df = pd.DataFrame({
'kategoria': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100),
'wartosc': np.random.normal(50, 15, 100),
})
fig, ax = plt.subplots(figsize=(9, 5))
sns.boxplot(data=df, x='kategoria', y='wartosc', palette=moja_paleta, ax=ax)
ax.set_title('Boxplot z własną paletą kolorów', fontsize=13)
plt.tight_layout()
plt.show()
# Reset do domyślnej palety
sns.set_palette('deep')
Moja rada (z doświadczenia): unikaj domyślnych palet w raportach, które idą do klienta. Zdefiniuj firmową paletę kolorów raz, zapisz ją jako listę hexów i używaj konsekwentnie w całym projekcie. To niewielki wysiłek, a wykres wygląda o klasę lepiej.
Najczęściej zadawane pytania (FAQ)
Jaka jest różnica między matplotlib a seaborn?
Matplotlib to niskopoziomowa biblioteka do tworzenia wykresów — daje pełną kontrolę nad każdym elementem wizualizacji, ale wymaga więcej kodu. Seaborn jest zbudowany na matplotlib i oferuje API wyższego poziomu, zoptymalizowane pod wizualizację statystyczną z danymi tabelarycznymi (DataFrame). W praktyce: użyj seaborn do szybkiej eksploracji i wykresów statystycznych, a matplotlib kiedy potrzebujesz pełnej kontroli nad layoutem, subplotami albo niestandardowymi elementami. Obie biblioteki świetnie współpracują — seaborn do treści, matplotlib do precyzyjnego dopracowania.
Jak zapisać wykres do pliku w matplotlib?
Użyj metody fig.savefig() lub plt.savefig(). Najważniejsze parametry to dpi (rozdzielczość — 300 do druku, 150 na ekran), bbox_inches='tight' (żeby nie obcinać etykiet) i format pliku (PNG, PDF, SVG, EPS). Przykład:
fig.savefig('wykres.png', dpi=300, bbox_inches='tight', facecolor='white')
fig.savefig('wykres.pdf', bbox_inches='tight') # wektorowy PDF
fig.savefig('wykres.svg', bbox_inches='tight') # wektorowy SVG
I protip: zawsze wywołuj savefig() przed plt.show(), bo show() w niektórych backendach czyści figurę. Sam się na tym złapałem więcej razy, niż chciałbym przyznać.
Jak zmienić rozmiar wykresu w matplotlib?
Trzy sposoby, w zależności od kontekstu:
# 1. Przy tworzeniu (najczęściej)
fig, ax = plt.subplots(figsize=(12, 6)) # szerokość x wysokość w calach
# 2. Globalnie dla wszystkich wykresów
plt.rcParams['figure.figsize'] = (10, 6)
# 3. Po utworzeniu figury
fig.set_size_inches(12, 6)
Pamiętaj, że rozmiar jest w calach, a rzeczywisty rozmiar w pikselach to figsize * dpi. Domyślne DPI to 100, więc figsize=(10, 6) daje obraz 1000x600 pikseli.
Czy seaborn działa z Polars?
Tak. Od wersji 0.13 seaborn obsługuje DataFrame z Polars dzięki protokołowi wymiany danych (dataframe exchange protocol, PEP 3401). Możesz przekazywać ramki Polars bezpośrednio do funkcji seaborn — pod spodem dane są konwertowane automatycznie. W praktyce wygląda to identycznie jak praca z pandas:
import polars as pl
import seaborn as sns
df_polars = pl.DataFrame({
'miasto': ['Warszawa', 'Kraków', 'Wrocław', 'Gdańsk'] * 25,
'wynagrodzenie': [9500, 8200, 8000, 7800] * 25,
})
# Działa bezpośrednio — bez konwersji na pandas
sns.boxplot(data=df_polars, x='miasto', y='wynagrodzenie')
Który interfejs seaborn wybrać — klasyczny czy objects?
Zależy od kontekstu. Klasyczny interfejs (sns.scatterplot(), sns.boxplot() itd.) jest dojrzały, dobrze udokumentowany i sprawdzony w produkcji — to bezpieczny wybór do raportów i potoków automatycznych. Interfejs seaborn.objects (so.Plot().add()) jest nowszy, inspirowany Grammar of Graphics i oferuje bardziej deklaratywną, elastyczną składnię — świetny do interaktywnej eksploracji i złożonych wizualizacji z facetingiem. Jest nadal oznaczony jako eksperymentalny, ale jest już stabilny w codziennym użyciu. Moja rekomendacja? Ucz się obu. Klasyczne API do produkcji, objects do eksploracji i projektów, gdzie chcesz maksymalnej elastyczności. W końcu i tak będziesz potrzebował jednego i drugiego.