Masz przed sobą sześć miesięcy obserwacji z rezerwatu, @name: 8, 12, 15, 10, 18, 22. Każda liczba jest poprawna, każda opisana, wszystkie leżą w jednej kolumnie DataFrame. I co z tego wynika? Czy populacja lwów rośnie? Czy maj to wyjątek, czy początek trendu? Przy sześciu wartościach jeszcze to policzysz w głowie. Przy trzech tysiącach wierszy nie policzysz już nic, a właśnie tyle przywozi się z jednego sezonu.
Mój pierwszy sezon w rezerwacie skończył się notesem pełnym wpisów, z których nie wynikało nic. Dopiero kiedy przypiąłem szpilki do mapy i połączyłem je sznurkiem, zobaczyłem to, czego notes nie pokazał przez cztery miesiące: stado wędrowało za deszczem. Wykres to ta sama mapa ze szpilkami - te same dane, ułożone tak, żeby oko wyłapało wzór, zanim rozum zdąży cokolwiek policzyć.
Dostajesz w tej lekcji dwa narzędzia. Matplotlib to warsztat, w którym budujesz wykres krok po kroku: linia, opis osi, tytuł, legenda. Seaborn to nadbudówka nad Matplotlibem, która zna gotowe wykresy statystyczne i rysuje je jednym poleceniem. Zaczynamy od warsztatu, bo Seaborn i tak opiera się na nim całym ciężarem.
pltMatplotlib to pakiet, czyli spory zbiór modułów. Rysowaniem zajmuje się w nim jeden konkretny moduł:
pyplot. Nie jest on osobną biblioteką - mieszka w środku pakietu matplotlib, więc trzeba do niego dojść ścieżką z kropką: matplotlib.pyplot. Pisanie tego przy każdej linii byłoby męczarnią, więc przyjęło się nadawać mu skrót plt słowem kluczowym as. To nie wymóg języka, tylko konwencja, ale konwencja tak silna, że każdy przykład w dokumentacji i każda odpowiedź w sieci zakłada właśnie plt.Wokół tej jednej linii krążą trzy zapisy, które nie zadziałają, i warto je znać, żeby nie tracić wieczoru na literówkę. Zapis
import pyplot as plt szuka samodzielnie zainstalowanego pakietu o nazwie pyplot - takiego nie ma, więc Python kończy błędem ModuleNotFoundError: No module named 'pyplot'. Zapis from matplotlib import plot sięga po nazwę plot, ale moduł nazywa się pyplot, nie plot, więc dostajesz ImportError: cannot import name 'plot' from 'matplotlib'. Najbardziej podstępny jest import matplotlib as plt: ta linia przechodzi bez błędu, bo pakiet matplotlib naprawdę istnieje. Dopiero pierwsze wywołanie wykłada się na AttributeError: module 'matplotlib' has no attribute 'plot', bo funkcje rysujące siedzą w module pyplot, a nie w korzeniu pakietu.1import matplotlib.pyplot as pltPo tej linii nie otwiera się żadne okno i nie powstaje żaden plik. Import tylko wciąga moduł do pamięci i wiesza na nim etykietę
plt. Rysować zaczniesz dopiero następnym poleceniem, więc ta jedna linia nigdy niczego nie zepsuje - a jest jedyną poprawną wersją z całej czwórki.Zacznijmy od najprostszego pytania, jakie zadaje się danym z rezerwatu: jak liczba obserwacji zmienia się w czasie? Do zmian w czasie służy wykres liniowy, bo linia łącząca punkty pokazuje kierunek, a nie tylko wartości. Rysuje go funkcja
plot, która przyjmuje dwie listy: pierwsza trafia na oś poziomą, druga na pionową. Na końcu wywołujemy show, czyli polecenie "pokaż mi to, co narysowałeś".1months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
2lion_sightings = [8, 12, 15, 10, 18, 22]
3
4plt.plot(months, lion_sightings)
5plt.show()Sześć liczb z notesu zamieniło się w linię, która wyraźnie pnie się w górę, z jednym dołkiem w kwietniu. Zapamiętaj podział ról:
plot rysuje w pamięci, a show wyświetla. To dwie osobne czynności i pominięcie show w zwykłym skrypcie kończy się tym, że program się wykonuje, kończy bez błędu i nie pokazuje nic. Sama funkcja show niczego nie liczy i nie zmienia danych - lista lion_sightings po jej wywołaniu jest dokładnie taka sama jak przed. Warto zapamiętać ją znak po znaku, bo będziesz ją pisał setki razy: plt, ., show, (, ).Poprzedni wykres ma jedną wadę: nikt poza Tobą nie wie, co przedstawia. Osie nie mają nazw, nie ma tytułu, a jeśli dorzucisz drugi gatunek, nie da się odróżnić linii. Naprawiają to cztery funkcje o mówiących nazwach:
title ustawia tytuł, xlabel opisuje oś poziomą, ylabel oś pionową, a legend rysuje legendę na podstawie etykiet podanych w plot jako label. Dochodzi do tego figure z parametrem figsize, który podaje rozmiar kartki w calach jako parę liczb, oraz grid, który dokłada siatkę pomocniczą.1elephant_sightings = [5, 7, 6, 9, 11, 8]
2
3plt.figure(figsize=(10, 6))
4plt.plot(months, lion_sightings, marker="o", label="Lwy", color="gold")
5plt.plot(months, elephant_sightings, marker="s", label="Slonie", color="gray")
6
7plt.title("Obserwacje w rezerwacie - pierwsze polrocze")
8plt.xlabel("Miesiac")
9plt.ylabel("Liczba obserwacji")
10plt.legend()
11plt.grid(True, alpha=0.3)
12plt.show()Dwa wywołania
plot pod rząd nie tworzą dwóch wykresów - dokładają dwie linie do tego samego rysunku. Nowa kartka powstaje dopiero przy plt.figure(...) i dlatego polecam zaczynać każdy wykres właśnie od tej linii: bez niej drugi wykres w skrypcie dorysuje się na pierwszym i zobaczysz sieczkę. Parametr marker zaznacza pojedyncze pomiary kółkiem lub kwadratem, color ustala barwę, a alpha to przezroczystość w skali od 0 do 1. Wszystkie te funkcje zmieniają wyłącznie wygląd rysunku. Liczby w listach pozostają nietknięte, a linia lwów przebiega dokładnie tam, gdzie przebiegała bez tytułu i legendy.savefig przed showWykres oglądany na ekranie ginie razem z zamknięciem okna. Do raportu z ekspedycji potrzebujesz pliku, a od tego jest funkcja
savefig, która przyjmuje nazwę pliku i zapisuje rysunek na dysk. Rozszerzenie w nazwie decyduje o formacie, a parametr dpi o rozdzielczości - im wyższy, tym ostrzejszy obrazek i tym cięższy plik.1plt.figure(figsize=(10, 6))
2plt.plot(months, lion_sightings, marker="o", label="Lwy")
3plt.legend()
4
5plt.savefig("obserwacje.png", dpi=150)
6plt.show()Kolejność dwóch ostatnich linii nie jest przypadkowa i to najczęstsza pułapka tej lekcji:
musi stać przed savefig
. Po zamknięciu okna podglądu część backendów sprząta rysunek, więc show
savefig wywołane później zapisuje pusty, biały obrazek - bez błędu, bez ostrzeżenia, po prostu pusty. Zapis do pliku nie zamyka przy tym okna i nie zwalnia Cię z wywołania show, jeśli chcesz wykres jeszcze obejrzeć.plt.bar, nie plt.barsLinia jest dobra do czasu, ale nie do porównań. Gdy masz cztery gatunki i ich liczebności, chcesz zobaczyć, który słupek jest wyższy - a nie łączyć lwa z żyrafą sznurkiem, bo między nimi nie ma żadnego "pomiędzy". Do porównywania kategorii służy wykres słupkowy i rysuje go funkcja o krótkiej, jednoznacznej nazwie
bar. Pierwszym argumentem jest lista podpisów kategorii, drugim lista wartości; opcjonalny color przyjmuje jedną barwę albo listę barw, po jednej na słupek.Nazwę trzeba zapamiętać dokładnie, bo Matplotlib nie wybacza kreatywności. Funkcja nazywa się
bar, w liczbie pojedynczej, a nie bars. Nie ma też funkcji column - w Matplotlibie kolumny nazywają się słupkami i tyle. Nie ma również funkcji histogram - wykres rozkładu rysuje hist, o czym za chwilę, a np.histogram z NumPy to zupełnie co innego, bo zwraca same liczby zamiast rysunku. Każda z tych trzech pomyłek kończy się tym samym komunikatem: AttributeError: module 'matplotlib.pyplot' has no attribute ....1species = ["Lion", "Elephant", "Cheetah", "Giraffe"]
2populations = [120, 450, 85, 200]
3
4plt.figure(figsize=(10, 6))
5plt.bar(species, populations, color=["gold", "gray", "orange", "brown"])
6plt.title("Populacje gatunkow w rezerwacie")
7plt.xlabel("Gatunek")
8plt.ylabel("Populacja")
9plt.show()Cztery słupki i jedno spojrzenie wystarczy, żeby wiedzieć, że słoni jest ponad pięć razy więcej niż gepardów. Kolejność słupków bierze się wprost z kolejności listy
species - bar niczego nie sortuje i nie sumuje za Ciebie, po prostu stawia obok siebie tyle prostokątów, ile podałeś wartości. Jeśli chcesz słupki poziome, jest na to osobna funkcja barh, ale przy nazwach gatunków pionowe czytają się lepiej i to je polecam.Załóżmy, że przez sezon zważyłeś dwadzieścia cztery lwy. Wykres słupkowy dałby tu dwadzieścia cztery słupki, po jednym na zwierzę - kompletnie nieczytelne i do niczego. Pytanie brzmi przecież inaczej: jakie wagi są typowe, a jakie rzadkie. Do tego służy histogram: dzieli zakres wartości na przedziały, zwane koszykami, i pokazuje, ile pomiarów wpadło do każdego. Rysuje go funkcja
hist, w której parametr bins ustala liczbę koszyków, a edgecolor rysuje obwódkę, żeby sąsiednie słupki się nie zlewały.1lion_weights = [168, 175, 190, 182, 177, 195, 160, 188,
2 172, 181, 199, 165, 178, 186, 173, 191,
3 169, 184, 176, 193, 180, 187, 171, 179]
4
5plt.figure(figsize=(10, 6))
6plt.hist(lion_weights, bins=6, edgecolor="black", alpha=0.7)
7plt.title("Rozklad wagi lwow")
8plt.xlabel("Waga [kg]")
9plt.ylabel("Liczba osobnikow")
10plt.show()Różnica względem
bar jest zasadnicza, choć oba wykresy mają słupki. Do bar podajesz dwie listy: kategorie i gotowe wartości. Do hist podajesz jedną listę surowych pomiarów, a zliczanie robi za Ciebie funkcja. Oś pionowa histogramu zawsze pokazuje liczebność, nawet jeśli tego nie napiszesz. Zmiana bins zmienia wyłącznie sposób pokrojenia zakresu - dane pozostają te same, a przy zbyt małej liczbie koszyków rozkład wygląda gładko, przy zbyt dużej rozsypuje się na pojedyncze pomiary.plt.pieCzasem nie interesuje Cię bezwzględna liczba zwierząt, tylko to, jaką część rezerwatu stanowi każdy gatunek. Odpowiedzi na pytanie "ile procent" udziela wykres kołowy, rysowany funkcją
pie. Podajesz jej listę wartości, w parametrze labels listę podpisów, a w autopct wzorzec formatowania procentów - zapis "%1.1f%%" znaczy "jedna cyfra po przecinku, a na końcu znak procenta".1plt.figure(figsize=(8, 8))
2plt.pie(populations, labels=species, autopct="%1.1f%%",
3 colors=["gold", "gray", "orange", "brown"])
4plt.title("Udzial gatunkow w populacji rezerwatu")
5plt.show()Procenty na wycinkach policzył sam Matplotlib, dzieląc każdą wartość przez sumę wszystkich - lista
populations nadal trzyma surowe sztuki, nie procenty. Dlatego wykres kołowy ma sens tylko wtedy, gdy Twoje wartości naprawdę składają się na jedną całość. Dla czterech gatunków z jednego rezerwatu to prawda, dla czterech niezależnych pomiarów już nie, a przy kilkunastu kategoriach wycinki robią się tak cienkie, że lepiej wrócić do bar.plt.scatterDo tej pory każdy wykres miał kategorię i liczbę. A co, jeśli chcesz sprawdzić, czy dwie liczby chodzą razem - na przykład czy w miesiącach z większymi opadami widuje się więcej lwów? Do tego służy wykres punktowy, rysowany funkcją
scatter, która stawia jedną kropkę na każdą parę wartości. Parametr s ustala wielkość kropek, a znane już alpha ich przezroczystość, dzięki czemu widać, gdzie punkty się nakładają.1rainfall_mm = [12, 18, 45, 80, 130, 165]
2
3plt.figure(figsize=(10, 6))
4plt.scatter(rainfall_mm, lion_sightings, s=100, alpha=0.7, color="gold")
5plt.title("Opady a liczba obserwacji")
6plt.xlabel("Opady [mm]")
7plt.ylabel("Liczba obserwacji lwow")
8plt.show()Sześć kropek układa się z grubsza wzdłuż linii biegnącej w górę: im więcej deszczu, tym więcej obserwacji. To pierwszy trop, nie dowód -
scatter niczego nie liczy i nie dopasowuje żadnej linii, po prostu stawia punkty tam, gdzie każesz. Kolejność argumentów ma znaczenie: pierwsza lista trafia na oś poziomą, druga na pionową, a zamiana ich miejscami da poprawny wykres o zupełnie innej wymowie.Matplotlib rysuje wszystko, ale każdy wykres statystyczny musisz w nim złożyć samodzielnie. Seaborn idzie o krok dalej: zna typowe wykresy używane w analizie danych, przyjmuje wprost DataFrame z Pandas i sam dobiera kolory oraz opisy osi na podstawie nazw kolumn. Nie zastępuje Matplotliba, tylko go opakowuje, dlatego
plt.title i plt.show działają na wykresach Seaborna tak samo jak na własnych. Bibliotekę importuje się pod skrótem sns, a funkcja set_style ustawia wspólny styl dla wszystkich kolejnych rysunków.Żeby pokazać rozkład wagi w podziale na gatunki, potrzebujemy danych w układzie długim: jedna kolumna z nazwą gatunku i jedna z pomiarem, po jednym wierszu na zwierzę.
1import seaborn as sns
2import pandas as pd
3
4sns.set_style("whitegrid")
5
6observations = pd.DataFrame({
7 "species": ["Lion"] * 6 + ["Cheetah"] * 6 + ["Giraffe"] * 6,
8 "weight_kg": [168, 175, 190, 182, 177, 195,
9 52, 58, 47, 61, 55, 49,
10 810, 950, 880, 1020, 905, 870]
11})
12
13print(observations.shape) # (18, 2)Osiemnaście wierszy i dwie kolumny, dokładnie jak przy DataFrame z poprzedniej lekcji - Seaborn nie wprowadza własnej struktury danych, tylko czyta tę, którą już znasz. Wywołanie
set_style nie narysowało niczego; zmieniło jedynie domyślny wygląd wykresów, które dopiero powstaną. Wcześniejsze rysunki pozostają takie, jakie były.Histogram pokazał rozkład jednej grupy. A jak porównać rozkłady trzech gatunków obok siebie, nie rysując trzech osobnych histogramów? Do tego służy wykres pudełkowy, po angielsku boxplot, rysowany funkcją
boxplot. Streszcza on rozkład do pięciu liczb: dolnej i górnej krawędzi pudełka, czyli kwartyli dzielących dane na cztery równe części, kreski mediany w środku pudełka, oraz wąsów sięgających do wartości typowych. Punkty leżące poza wąsami rysowane są osobno jako wartości odstające, czyli outliery.1plt.figure(figsize=(10, 6))
2sns.boxplot(data=observations, x="species", y="weight_kg")
3plt.title("Rozklad wagi wedlug gatunku")
4plt.show()Trzy pudełka na jednym rysunku i od razu widać, że gepardy mieszczą się w wąskim zakresie, a żyrafy rozstrzeliwują skalę. Dla lwów dolny kwartyl wypada na 175,5 kg, mediana na 179,5 kg, a górny kwartyl na 188 kg - te trzy liczby to właśnie krawędzie i kreska pudełka. Argument
data przyjmuje cały DataFrame, a x i y to nazwy kolumn, nie same dane; podpisy osi Seaborn pobiera z tych nazw automatycznie.Warto od razu odsiać trzy nieporozumienia, bo nazwa "pudełko" myli. Boxplot nie służy do rysowania ramek wokół tekstu - od obramowania podpisu jest parametr
bbox w funkcjach tekstowych. Nie jest też narzędziem do rysowania prostokątów jako takich; owszem, rysuje prostokąt, ale ten prostokąt jest wykresem pięciu policzonych statystyk, a nie kształtem, któremu podajesz wymiary. I nie łączy wykresów w ramki obok siebie - od układania kilku wykresów na jednej kartce jest plt.subplots, który poznasz na końcu lekcji. Boxplot ma dokładnie jedno zadanie: pokazać rozkład danych z kwartylami i wartościami odstającymi. Bliskim krewnym jest sns.violinplot, który zamiast pudełka rysuje kształt rozkładu, oraz sns.countplot, który zlicza wiersze w każdej kategorii i sam robi z tego słupki.Wykres punktowy pokazał związek opadów z obserwacjami lwów, ale przy trzech czy trzydziestu kolumnach nie będziesz rysował każdej pary osobno. Pandas potrafi policzyć wszystkie związki naraz metodą
corr, która zwraca macierz korelacji: tabelę, gdzie w każdej komórce stoi siła związku między dwiema kolumnami, w skali od minus jeden do jeden. Jedynka oznacza, że wartości rosną idealnie razem, zero - że nie mają ze sobą nic wspólnego, a minus jeden, że jedna rośnie, gdy druga maleje. Wynik przypiszemy do zmiennej o nazwie corr_matrix.1monthly = pd.DataFrame({
2 "rainfall_mm": rainfall_mm,
3 "lion_sightings": lion_sightings,
4 "elephant_sightings": elephant_sightings
5})
6
7corr_matrix = monthly.corr()
8print(corr_matrix.round(2))
9# rainfall_mm lion_sightings elephant_sightings
10# rainfall_mm 1.00 0.85 0.71
11# lion_sightings 0.85 1.00 0.47
12# elephant_sightings 0.71 0.47 1.00Trzy kolumny liczbowe dały tabelę trzy na trzy. Na przekątnej stoją same jedynki, bo każda kolumna jest w stu procentach zgodna sama ze sobą, a cała tabela jest symetryczna względem tej przekątnej. Zwróć uwagę na warunek, o którym łatwo zapomnieć:
corr działa wyłącznie na liczbach. Gdyby w monthly została choć jedna kolumna tekstowa, wywołanie przerwałoby się błędem ValueError: could not convert string to float, a kolumny liczbowe trzeba by wcześniej odsiać parametrem numeric_only=True albo metodą select_dtypes znaną z poprzedniej lekcji. Tam czai się druga pułapka: jeśli po odsianiu zostanie Ci jedna kolumna liczbowa, dostaniesz macierz jeden na jeden z pojedynczą jedynką - formalnie poprawną, informacyjnie bezwartościową. Sensowna macierz korelacji potrzebuje co najmniej dwóch kolumn liczbowych.sns.heatmap, czyli macierz w kolorachTrzy na trzy jeszcze przeczytasz wzrokiem, ale dwadzieścia na dwadzieścia to już ściana cyfr. Mapa cieplna zamienia każdą liczbę na kolor, dzięki czemu silne związki wyskakują z tabeli same. Rysuje ją funkcja
heatmap z Seaborna. Parametr annot dopisuje liczby na kafelkach, cmap wybiera paletę - "coolwarm" maluje wartości ujemne na niebiesko, a dodatnie na czerwono - a center wskazuje, która wartość ma wypaść w środku skali kolorów, czyli tutaj zero.W najprostszej postaci to wywołanie składa się z sześciu części i warto je znać na pamięć:
sns, ., heatmap, (, corr_matrix, ). Najpierw skrót biblioteki, potem kropka, nazwa funkcji, nawias otwierający, przekazywana macierz i nawias zamykający. Dodatkowe parametry dopisujesz dopiero po nazwie macierzy, przed nawiasem zamykającym.1plt.figure(figsize=(8, 6))
2sns.heatmap(corr_matrix, annot=True, cmap="coolwarm", center=0)
3plt.title("Korelacje obserwacji z rezerwatu")
4plt.show()Najgorętszy kafelek poza przekątną łączy opady z obserwacjami lwów: 0,85, czyli bardzo silny związek. Słonie reagują na deszcz słabiej, 0,71, a między lwami a słoniami zostaje ledwie 0,47. Mapa cieplna niczego nie policzyła - wszystkie te liczby były już w
corr_matrix, ona tylko pomalowała je tak, żeby dało się je porównać jednym spojrzeniem. I pamiętaj o klasycznej pułapce: silna korelacja mówi, że dwie rzeczy chodzą razem, a nie że jedna powoduje drugą. Deszcz nie tworzy lwów, tylko ściąga zwierzynę do wodopoju, gdzie łatwiej ją zobaczyć.Raport z ekspedycji rzadko składa się z jednego rysunku. Zamiast zapisywać cztery osobne pliki, możesz podzielić kartkę na siatkę funkcją
subplots, której podajesz liczbę wierszy i kolumn. Zwraca ona dwie rzeczy naraz: obiekt całej kartki, tradycyjnie nazywany fig, oraz tablicę pól do rysowania, nazywaną axes. Do konkretnego pola sięgasz jak do komórki macierzy z NumPy, czyli axes[wiersz, kolumna], licząc od zera. Uwaga na nazwy metod: pole nie ma title, tylko set_title, bo jest obiektem, a nie modułem plt.1fig, axes = plt.subplots(2, 2, figsize=(12, 10))
2
3axes[0, 0].bar(species, populations)
4axes[0, 0].set_title("Populacje")
5
6axes[0, 1].pie(populations, labels=species, autopct="%1.1f%%")
7axes[0, 1].set_title("Udzial")
8
9axes[1, 0].hist(lion_weights, bins=6)
10axes[1, 0].set_title("Rozklad wagi")
11
12axes[1, 1].scatter(rainfall_mm, lion_sightings)
13axes[1, 1].set_title("Opady a obserwacje")
14
15plt.tight_layout()
16plt.show()Cztery wykresy z całej lekcji wylądowały na jednej kartce w układzie dwa na dwa. Nazwy funkcji rysujących nie zmieniły się ani o literę - to wciąż
bar, pie, hist i scatter, tyle że wywołane na polu zamiast na plt. Wywołanie tight_layout rozsuwa wykresy tak, żeby tytuły i opisy osi na siebie nie nachodziły, i to jedyne, co robi. Na koniec i tak wołasz jedno plt.show(), bo cała siatka to jeden rysunek.import matplotlib.pyplot as plt. Zapisy import pyplot as plt, from matplotlib import plot i import matplotlib as plt prowadzą do błędu, ten ostatni dopiero przy pierwszym rysowaniu.plt.bar(). Nie istnieją plt.bars(), plt.column() ani plt.histogram().plt.show(), znak po znaku: plt, ., show, (, ).plt.plot to zmiany w czasie, plt.bar to porównanie kategorii, plt.hist to rozkład jednej zmiennej, plt.pie to udziały w całości, plt.scatter to związek dwóch liczb.plt.savefig("nazwa.png") i zawsze przed plt.show().sns, przyjmuje DataFrame i dokłada wykresy statystyczne.sns.boxplot pokazuje rozkład danych z kwartylami i wartościami odstającymi - nie rysuje ramek wokół tekstu, nie służy do rysowania prostokątów i nie łączy wykresów w ramki.sns, ., heatmap, (, corr_matrix, ).plt.subplots(wiersze, kolumny), a pola opisujesz metodą set_title.W następnej lekcji użyjemy tych wykresów w zwiadzie zwanym EDA, gdzie histogram i boxplot posłużą do wytropienia braków i wartości odstających w świeżym zbiorze. Na razie zapamiętaj jedno, @name: wykres nie dodaje do danych ani jednej informacji, tylko układa je tak, żeby oko zauważyło trop, którego kolumna liczb nigdy Ci nie pokaże.