Jeśli jest jakaś biblioteka w Pythonie, którą można nazwać Ojcem Chrzestnym wizualizacji, byłby to Matplotlib. Za myśl przewodnią całego wpisu niech posłuży nam poniższy cytat.

Metod może być milion i więcej, lecz zasad jest tylko kilka. Człowiek, który rozumie zasady, może z powodzeniem wybierać metody. Człowiek, który próbuje metod, ignorując zasady, wpędza się w tarapaty.

Ralph Waldo Emerson

Nawiązanie bliskich relacji z kimś wiąże się z poznaniem zasad i reguł jakimiś ktoś się kieruje. Zanim do tego dojdziemy, małe intro naszego Ojca Chrzestnego.

Stworzony przez Johna Huntera by, odtworzyć w Pythonie to, co potrafi MatLab w zakresie wykresów. Biblioteka grafiki 2D i 3D do prezentacji danych naukowych.

Główne zalety to:

  • łatwy start z prostymi wykresami bez wchodzenia w szczegóły (podejście funkcyjne — wyjaśnię to później),
  • obsługa niestandardowych etykiet i tekstów,
  • kontrola każdego elementu (podejście obiektowe),
  • wiele formatów wyjściowych w wysokiej jakości,
  • nastawiony na wysoką konfigurowalność (podejście obiektowe)

Próbka możliwości na oficjalnej stronie projektu, gdzie jest dosyć pokaźna galeria.

W tym wpisie postaram się pokazać jak pracować z naszym bohaterem. Zacznijmy więc od zasad, jakim rządzi się Matplotlib.

Architektura Matplotlib i Dwa Podejścia

Kluczem do poznania Matplolib-a jest poznanie jego architektury. Swoją budową przypomina… Matrioszkę.

Struktura Matplotlib niczym Matrioszka
Architektura Matplotlib niczym Matrioszka

Precyzyjniej wygląda to tak.

Warstwy Matplotlib
Matplotlib na warstwy

Istnieją więc 3 warstwy:

  1. Najniższa warstwa: Backend Layer. W Jupyterze, gdy wprowadzamy %matplotlib inline, to tak naprawdę informujemy Backend Layer, aby renderował wykresy bez wywoływania plt.show(). Możemy także użyć tej warstwy, jeśli chcielibyśmy wybrać silnik do renderowania wykresów np. hvplot, jshtml. Są to najprawdopodobniej, jedyne przypadki, w których wchodzimy w interakcję z tą warstwą.
  2. Poziom drugi: Artist Layer. Poziom ten umożliwia największą kontrolę nad całym wykresem poprzez zmianę niemal każdego jego elementu. Dostęp do tej warstwy uzyskujemy np. poprzez ax1.plot(). Korzystając z tej warstwy, używamy podejścia obiektowego.
  3. Najwyższy poziom: Scripting Layer. Przydatny, gdy zależy nam na szybkiej wizualizacji, nie wchodząc zbyt głęboko w budowę wykresów. Korzystając z tego poziomu, używamy podejścia funkcyjnego.

Matplotlib Funkcyjnie Czy Obiektowo?

Od razu pojawia się pytanie. Które podejście jest najlepsze?

Najlepsza odpowiedź na tego typu pytania? TO ZALEŻY.

Najważniejszy jest cel, w jakim używamy danego narzędzia. Jeśli chcemy szybko zobaczyć wyniki, nie chcemy dużo zmieniać i nie mamy potrzeby kontroli każdego aspektu wykresu, podejście funkcyjne nam wystarczy.

Gdy chcemy wykres „dopieścić” albo pokazać coś, czego w standardowy sposób nie możemy, trzeba będzie przejść na poziom obiektowy.

Zaawansowani użytkownicy Matplolib-a wychodzą z założenia, że lepiej całkowicie przejść na podejście obiektowe. Jest kilka przesłanek ku temu:

  • jawnie definiujemy to, co Matplolib i tak robi za nas, kiedy korzystamy z podejścia funkcyjnego (o tym za chwilę),
  • mamy pełną kontrolę nad elementami wykresu,
  • kiedy wszystko jest jawnie zdefiniowane, kod staje się czytelniejszy i łatwiejszy do ponownego użycia,
  • znajomość podejścia obiektowego ułatwia pracę z innymi bibliotekami do wizualizacji np. seaborn.

Podsumowując, w Matplotlib wiele rzeczy możemy zrobić na kilka sposobów. Może to spowodować małe zamieszanie, ale jeśli rozumiemy strukturę tej biblioteki i dwa oferowane podejścia wszystko staje się prostsze. Korzystając z podejścia funkcyjnego, tak naprawdę przerzucamy na Matplotlib-a ciężar wywoływania obiektów.

FunkcyjnieCo robi Matplotlib?Obiektowo
plt.subplot()Matplotlib wywołuje ->fig = plt.figure()
fig.add_subplot()
plt.axes()Matplotlib wywołuje ->fig = plt.figure()
fig.add_axes()

Skoro struktura jest omówiona, zobaczmy te dwa podejścia w praktyce. Zacznijmy od instalacji i importu biblioteki.

Instalacja i Import Matplotlib

Osoby korzystające z Anacondy mają Matplotlib w gratisie.

Zawsze możemy doinstalować go za pomocą polecenia w wierszu poleceń lub terminalu.

conda install matplotlib

lub

pip install matplotlib

Po instalacji robimy import w naszym edytorze:

import matplotlib.pyplot as plt

Gdy korzystamy z Jupytera, możemy dodać także:

%matplotlib inline

Powyższa linijka sprawia, że nie będziemy musieli za każdym razem wydawać polecenie plt.show(), aby wygenerować wykres. W przypadku edytora innego niż Jupyter Notebook wydajemy na końcu polecenie plt.show().

Podejście Funkcyjne w Praktyce

Pierwszy Wykres Matplotlib

Wykorzystamy w naszym wykresie dwie tablice x i y wygenerowane dzięki numpy.

import numpy as np
x = np.linspace(0, 10, 11)
y = x ** 2

Wykorzystując powyższe dane, możemy stworzyć bardzo prosty wykres używając jedynie kilka linijek kodu.

# do funkcji plt.plot() wstawiamy nasze dane x i y
plt.plot(x, y, 'blue') # 'blue' ustawiamy kolor linii
plt.xlabel('Nazwa osi X')
plt.ylabel('Nazwa osi Y')
plt.title('Tutaj mamy tytuł')
plt.show()
Pierwszy wykres Matplotlib

Wiele Wykresów Na Jednym „Płótnie”

Wprowadźmy umowne pojęcie „płótna”, na którym będziemy umieszczać nasze wykresy. Dalej wyjaśnię, czym operujemy przy okazji podejścia obiektowego.

Aby na jednym „płótnie” umieścić więcej niż jeden wykres robimy tak jak poniżej.

# funkcja plt.subplot(liczba_wierszy, liczba_kolumn, numer_wykresu)
plt.subplot(1,2,1) # jeden wiersz, dwie kolumny, wykres pierwszy
plt.plot(x, y, 'b--') # więcej o stylach linii później
plt.subplot(1,2,2) # jeden wiersz, dwie kolumny, wykres drugi
plt.plot(y, x, 'r*-')
plt.show()
Dwa wykresy na jednym "płótnie"

Powyższe podejście to wierzchołek „góry lodowej”. Więcej możliwości daje przejście na podejście obiektowe i praca na drugim poziomie Artist Layer.

Podejście Obiektowe

Widzieliśmy już podejście funkcyjne, pora zejść niżej do podejścia obiektowego. Tutaj zaczniemy tworzyć obiekty, by wywoływać metody lub korzystać z atrybutów tych obiektów. Aby lepiej wiedzieć, na czym operujemy, zacznijmy od budowy wykresu.Matplotlib

Budowa Wykresu w Matplotlib

By zrozumieć, jak Matplotlib buduje wykresy, wprowadźmy kilka pojęć:

  • Figure — obiekt, który porównajmy do „płótna”, na którym będziemy „malować” nasze wykresy,
  • Axes — przestrzeń naszego wykresu, gdzie mamy osie, wykres, tytuł, etykiety itd.,
  • Axis — inaczej osie wykresu, dla 2D (x,y) dla 3D (x,y,z).
Rysunek poglądowy: Figure, Axes, Axis
Rysunek poglądowy: Figure, Axes, Axis

Główna Idea

Główną ideą korzystania z bardziej formalnej obiektowego podejścia jest tworzenie obiektów, a następnie wywoływanie metod na tym obiekcie lub atrybutów tego obiektu. Takie podejście zyskuje już w momencie, kiedy na jednym „płótnie” mamy więcej wykresów.

Na początek tworzymy „płótno” czyli obiekt Figure, a następnie dodajemy wykres, czyli obiekt Axes.

# tworzymy obiekt Figure, czyli nasze "płótno"
fig = plt.figure()

# dodajemy nasz wykres, w nawiasach kwadratowych mamy podaną pozycję wykresu
# 0.1 start z lewej, 0.1 start od dołu, 0.8 szerokość wykresu, 0.8 wysokość wykresu
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # wartości z zakresu (0 - 1 )

# dodanie naszych danych do wykresu, ustawiwnie etykiet i tytułu
axes.plot(x, y, 'b--')
axes.set_xlabel('Nazwa osi X') # Notice the use of set_ to begin methods
axes.set_ylabel('Nazwa osi Y')
axes.set_title('Tutaj mamy tutuł')
plt.show()
Wykres - obiektowe podejście

Kod jest nieco bardziej skomplikowany, ale zaletą jest to, że teraz mamy pełną kontrolę nad tym, gdzie umieszczony jest wykres. Możemy też swobodnie dodawać kolejne wykresy do tego samego obiektu Figure.

Mniejszy i Większy

Przykład poniżej pokazuje, jak możemy kontrolować dodawanie, położenie i wielkość kolejnych wykresów.

# tworzymy obiekt Figure, czyli nasze "płótno"
fig = plt.figure()

# pamiętamy że oznaczenia w nawiasach kwadratowych to
# start z lewej, start od dołu, szerokość i wysokość (skala 0-1)
axes1 = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # większy wykres
axes2 = fig.add_axes([0.2, 0.5, 0.4, 0.3]) # mniejszy wykres

# Większy wykres
axes1.plot(x, y, 'b--')
axes1.set_xlabel('Oś X większego wykresu')
axes1.set_ylabel('Oś Y większego wykresu')
axes1.set_title('Tytuł większego wykresu')

# Mniejszy wykres
axes2.plot(y, x, 'r*-')
axes2.set_xlabel('Oś X mniejszego wykresu')
axes2.set_ylabel('Oś Y mniejszego wykresu')
axes2.set_title('Tytuł mniejszego wykresu')

plt.show()
Wykres mniejszy w większym

Menedżer Wykresów Matplotlib

Korzystając z plt.subplots(), przekazujemy informację, że Matplotlib ma zadbać o układ naszych wykresów. Działa to trochę tak jak automatyczny menadżer wykresów.

# za pomocą krotki fig, axes przechwytujemy obiekty wywołane przez 
# plt.subplots()
fig, axes = plt.subplots()

# wykorzystujemy obiekt axes by dodać nasze dane do wykresu
axes.plot(x, y, 'r')
axes.set_xlabel('oś x')
axes.set_ylabel('oś y')
axes.set_title('tytuł')
plt.show()
Wykres menadżer wykresów

Możemy zrobić jednak coś więcej. Możemy podać liczbę wierszy i kolumn przy wywoływaniu metody plt.subplots().

# Puste "płótno" z dwoma wykresami czekającymi na dane
fig, axes = plt.subplots(nrows=1, ncols=2)
plt.show()
Dwa puste wykresy

Nasz obiekt axes jest tak naprawdę listą wykresów, w naszym wypadku dwóch pustych wykresów. Możemy przejść przez naszą listę, stosując pętle for.

for ax in axes:
    ax.plot(x, x, 'b')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('tytuł')

# wyświetlenie "płótna", czyli obiektu Figure  
fig
Nachodzące na siebie wykresy

Częstym problemem związanym z Matplolib są nakładające się na siebie wykresy. Jest prosty sposób na naprawienie tego. Wykorzystujemy metody fig.tight_layout() lub plt.tight_layout(), które automatycznie dostosowują pozycje obu wykresów tak, aby nie nakładały się na siebie.

fig, axes = plt.subplots(nrows=1, ncols=2)

for ax in axes:
    ax.plot(x, y, 'g')
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('title')

fig    
plt.tight_layout()
Wykresy wyrównane plt.tight_layout()

Wielkość Płótna i DPI

Matplotlib w momencie tworzenia daje możliwość ustalenia wielkości naszego „płótna” oraz ustalić dpi (przydatna funkcja, gdy będziemy tworzyć wykresu do druku).

Służą do tego dwa atrybuty:

  • figsize — krotka, w której podajemy szerokość i wysokość w calach,
  • dpi inaczej dots-per-inch czyli liczba pikseli na cal.

Możemy to zrobić tak:

fig = plt.figure(figsize=(8,4), dpi=300)

lub w menadżerze wykresów:

fig, axes = plt.subplots(figsize=(8,4),dpi=300)

axes.plot(x, y, 'r')
axes.set_xlabel('x')
axes.set_ylabel('y')
axes.set_title('tytuł')
plt.show()
Wykres dpi=300

Zapisywanie „Płótna”

Matplotlib może generować wysokiej jakości wykresy w wielu formatach, w tym: PNG, JPG, EPS, SVG, PGF i PDF.

Wykresy zapisujemy poprzez wywołanie metody plt.savefig() z podaniem ścieżki naszego pliku.

fig.savefig("nazwa_pliku.png")

Ewentualnie podajemy jeszcze dpi.

fig.savefig("nazwa_pliku.png", dpi=200)

Tytuły, Nazwy Osi i Legenda

Z poprzednich przykładów wiemy jak nadawać tytułu naszym wykresom:

ax.set_title("tytuł")

oraz nazywać osie:

ax.set_xlabel("x")
ax.set_ylabel("y")

Przydałoby się mieć legendę dla wykresów. W tym celu używamy argumentu label = "etykieta dla danych", gdy dodajemy dane do wykresu. Wszytko, po to, aby na końcu wygenerować legendę dla wykresu.

Pokażemy na przykładzie sinusa i cosinusa i danych wygenerowanych z numpy.

# wygenerujmy sobie z numpy dane dla sinusa i cosinusa
X = np.linspace(-np.pi, np.pi, 256, endpoint=True)
C, S = np.cos(X), np.sin(X)

fig = plt.figure()

ax = fig.add_axes([0.1,0.1,0.8,0.8])

ax.plot(S, label="sinus")
ax.plot(C, label="cosinus")
ax.legend()
plt.show()
Wykres z domyślną legendą

Domyślne ustawienie legendy jest takie, aby znaleźć optymalne miejsce dla siebie.Używamy atrybutu loc aby określić miejsce legendy.

ax.legend(loc=0) # domyślne najbardziej optymalne miejsce
ax.legend(loc=1) # górny prawy róg
ax.legend(loc=2) # górny lewy róg
ax.legend(loc=3) # dolny lewy róg
ax.legend(loc=4) # dolny prawy róg

Kolory, Style i Grubość Linii

Matplotlib ma wiele możliwości dostosowania wykresu do własnych preferencji. Można wykorzystać składnię z MatLab-a, stosując uproszczoną notację (rezygnując tym samym z czytelności kodu).

# wygenerujmy sobie z numpy dane dla sinusa i cosinusa
X = np.linspace(-np.pi, np.pi, 256, endpoint=True)
C, S = np.cos(X), np.sin(X)

fig = plt.figure()

ax = fig.add_axes([0.1,0.1,0.8,0.8])

ax.plot(S, 'b--', label="sinus") # niebieski kolor i linia kreskowana
ax.plot(C, 'g:', label="cosinus") # zielony kolor i linia kropkowana
ax.legend()
Wykres ze zmienionymi stylami

Podając odpowiednie argumenty, możemy ustawić wszystkie opcje jawnie (zyskując na czytelności kodu):

  • color — w postaci kodu szesnastkowego lub nazwy (lista nazw kolorów tutaj),
  • alpha — poziom przezroczystości koloru w przedziale 0-1,
  • linewidth lub lw — grubość linii,
  • linestyle lub ls — style linii,
  • marker — ustawiamy symbol znacznika,
  • markersize — wielkość znacznika,
  • markerfacecolor — kolor główny znacznika,
  • markeredgewidth — grubość obramowania znacznika,
  • markeredgecolor — kolor obramowania znacznika.

Zbiorcze zestawienie i porównanie atrybutów.

fig, ax = plt.subplots(figsize=(12,8))

# kolor szestnaskowy z 50% przezroczystością
ax.plot(x, x, color="#8B008B", alpha=0.5)

# różne grubości linii# wygenerujmy sobie z numpy dane dla sinusa i cosinusa
X = np.linspace(-np.pi, np.pi, 256, endpoint=True)
C, S = np.cos(X), np.sin(X)

fig = plt.figure()

ax = fig.add_axes([0.1,0.1,0.8,0.8])

ax.plot(S, label="sinus")
ax.plot(C, label="cosinus")
ax.legend()
plt.show()
ax.plot(x, x+1, color="red", linewidth=0.25)
ax.plot(x, x+2, color="red", linewidth=0.50)
ax.plot(x, x+3, color="red", linewidth=1.00)
ax.plot(x, x+4, color="red", linewidth=2.00)

# style linii możliwe opcje: ‘-‘, ‘–’, ‘-.’, ‘:’,
ax.plot(x, x+5, color="green", lw=3, linestyle='-')
ax.plot(x, x+6, color="green", lw=3, ls='-.')
ax.plot(x, x+7, color="green", lw=3, ls=':')

# możliwe style markerów: '+', 'o', '*', 's', ',', '.', '1', '2', '3', '4', ...
ax.plot(x, x+ 9, color="blue", lw=3,Jeśli jest jakaś biblioteka w Pythonie, którą można nazwać Ojcem Chrzestnym wizualizacji, byłby to Matplotlib. Niech poniższy cytat będzie myślą przewodnią całego wpisu. ls='-', marker='+')
ax.plot(x, x+10, color="blue", lw=3, ls='--', marker='o')
ax.plot(x, x+11, color="blue", lw=3, ls='-', marker='s')
ax.plot(x, x+12, color="blue", lw=3, ls='--', marker='1')

# wielkość i kolory markerów
ax.plot(x, x+13, color="purple", lw=1, ls='-', marker='o', markersize=2)
ax.plot(x, x+14, color="purple", lw=1, ls='-', marker='o', markersize=4)
ax.plot(x, x+15, color="purple", lw=1, ls='-', marker='o', markersize=8, markerfacecolor="red")
ax.plot(x, x+16, color="purple", lw=1, ls='-', marker='s', markersize=8, 
        markerfacecolor="yellow", markeredgewidth=3, markeredgecolor="green")
plt.show()
Porównanie styli

Granice Wykresu

Możemy skonfigurować granice osi wykresów przy użyciu metod set_ylim i set_xlim. Możemy także wydać polecenie aby, Matplotlib sam zawężał wykres za pomocą polecenia axis('tight').

fig, axes = plt.subplots(1, 3, figsize=(12, 4))

axes[0].plot(C)
axes[0].plot(S)
axes[0].set_title("domyślne granice wykresu")

axes[1].plot(C)
axes[1].plot(S)
axes[1].axis('tight')
axes[1].set_title("zawężony_automatycznie")

axes[2].plot(C)
axes[2].plot(S)
axes[2].set_ylim([0,1])
axes[2].set_xlim([50, 200])
axes[2].set_title("ręczne_ograniczenie")

plt.show()
Opcje ustalania granic wykresu

Typy Wykresów Matplotlib

Do tej pory korzystaliśmy z wykresów liniowych plot(). Pokażę kilka kluczowych, które już niedługo przydadzą się nam przy okazji eksploracji danych.

Histogram

Do sprawdzenia rozkładu wartości numerycznych.

import numpy as np
srednia, odchylenie = 500, 100 
rozklad = np.random.normal(srednia,odchylenie,1000)
plt.hist(rozklad)
plt.show()
Histogram

Bar Plot

Do obserwacji kategorii przyda się Bar Plot.

sprzedaz = (100, 20, 70, 150, 45)
grupy = ['G1', 'G2', 'G3', 'G4', 'G5']

fig, axes = plt.subplots()

axes.bar(labels,sprzedaz)
axes.set_xlabel('Grupy')
axes.set_ylabel('Sprzedaż')
axes.set_title('Sprzedaż z podziałem na grupy')

plt.show()
Bar Plot

Box Plot

Do obserwacji segmentacji, czyli relacji pomiędzy wartościami numerycznymi a kategoriami zastosujemy Box Plot.

dane = [np.random.normal(0, odchylenie, 100) for odchylenie in range(1, 4)]

plt.boxplot(dane)
plt.show()
Box Plot

Heatmap

Do badania korelacji miedzy danymi numerycznymi będzie pomocny Heatmap.

a = np.random.random((16, 16))
plt.imshow(a)
Heatmap

Źródła, Linki, Inspiracje

Lista adresów, z których korzystałem w tym wpisie, i które warto odwiedzić:

Kategorie: Narzędzia