Celem jest poznanie podstaw koniecznych do tworzenia wizualizacji.
Wizualizacja jest bardzo ważnym tematem chociażby dlatego, że jest to obszar, w którym człowiek potrafi wyciągnąć błyskawiczne wnioski. Pamiętasz to przysłowie - “Lepiej raz zobaczyć niż sto razy usłyszeć”? To właśnie za pomocą zrobionych wykresów można "zobaczyć" rozwiązanie.
Z drugiej strony, warto zrozumieć, że wykresy są tworzone dla ludzi, maszyna ich nie potrzebuje. Innymi słowy, dla modelu wszystko wygląda jak wykres, on używa metryki sukcesu jako kierunku optymalizacji.

Temat wizualizacji jest obszerny - dlatego my tutaj zajmiemy się tylko jego wycinkiem, ale warto od czegoś zacząć.
Jeśli dotykasz tematu wizualizacji po raz pierwszy, to nie musisz rozumieć wszystkiego. Poznaj przynajmniej nazwy i jak te wykresy powstają. Daruj sobie na początek badanie, czym są wykresy .kdeplot(). Oczywiście to może kiedyś się przyda, ale celem tego notebooka jest coś innego.


Krok po kroku
Jeśli jesteś osobą początkującą możesz skorzystać z nagrania poniżej. Omawiam tam krok po kroku tę lekcję. Możesz to pominąć i skupić się na samym tekście pisanym poniżej i kodzie, a możesz dodatkowo posłuchać i zobaczyć nagranie. Decyzja należy do Ciebie :)
Wizualizacja
Plan minimum jest taki, że masz poznać:
  • .plot() czyli wykres liniowy
  • .hist(), czyli histogram
  • .scatterplot(), czyli scatterplot
Коd

import numpy as np
import pandas as pd

import sklearn.datasets as d

import seaborn as sns
import matplotlib.pyplot as plt
from pandas.plotting import scatter_matrix
from matplotlib.ticker import NullLocator, FixedLocator, LogLocator, MultipleLocator

%matplotlib inline
Linia
Jeden z najprostszych wykresów to linia łącząca dwa lub więcej punktów.
Коd

plt.plot([3, 7, 8]);
Wynik:

Funkcja .plot() oczekuje na dwa parametry x i y, natomiast jeśli został podany tylko jeden (tak jak powyżej), czyli został podany w formie jednej listy, to traktuje to jako y, a wszystkie x wypełnia inkrementalnie. Możesz o tym myśleć w ten sposób, że wartości x to są indeksy y.

Na przykład: y[0]=3 => x=0, y=3.
Ten sam wynik, ale już jawnie podając x.

Коd

plt.plot([0, 1, 2], [3, 7, 8]);
Wynik:

Znak ; na końcu powoduje usunięcie opisu z biblioteki matplotlib. Możesz usunąć ; i zobaczyć, co się pojawi.
Warto zwrócić uwagę, że domyślny kolor linii to niebieski. W dalszej części pokażę, jak można zmienić kolor.

Zróbmy teraz bardziej złożony wykres. Funkcja f(x) będzie nam generować wartości na osi x od 0 do 10 i będzie tych punktów 100.


Коd

#jakaś "ładna" funkcja
def f(x):
    return np.pi*np.sin(x) + np.pi*np.cos(np.pi * x) + np.sin(x + np.pi)


x = np.linspace(0, 10, 100) # generuje 100 punktów między 0 i 10
y = f(x)

plt.plot(x, y);
Wynik:

Trochę więcej kontroli nad wykresem

Kolor linii

Коd

for color in ['r', 'g', 'b', 'c', 'm']: 
    plt.plot(x, y, color=color) #parametr "color" lub skrócona wersja "c". Możesz sprawdzić, podając `c=color`.
    plt.show()
Wynik:

Rodzaj linii


Коd

for line_style in ['-', '-.', '--', ':', '']: #pusty string oznacza "żaden"
    plt.plot(x, y, linestyle=line_style) #parameter "linestyle" lub skrócona wersja "ls"
    plt.show()
Wynik:

Punkty (marker w terminologii matplotlib)

Коd

all_markers = {'.': 'point',',': 'pixel','o': 'circle','v': 'triangle_down','^': 'triangle_up','<': 'triangle_left','>': 'triangle_right','1': 'tri_down','2': 'tri_up','3': 'tri_left','4': 'tri_right','8': 'octagon','s': 'square','p': 'pentagon','*': 'star','h': 'hexagon1','H': 'hexagon2','+': 'plus','x': 'x','D': 'diamond','d': 'thin_diamond','|': 'vline','_': 'hline'}

markers = {'.': 'point', 'o': 'circle', 's': 'square', '*': 'star','h': 'hexagon1'}



for marker, marker_name in markers.items():
    print("{0}\t{1}".format(marker, marker_name))
    
    plt.figure(figsize=(12, 7))
    plt.plot(x, y, markersize=7, marker=marker)
    plt.show()
Wynik:

Kontrola nad kreskami (ang. ticks) na osiach.

Domyślnie wartości na osiach (x czy y) są wypisywane w sposób liniowy, ale możesz to zmienić, zaczynając od tego, że masz opcję je schować lub wprowadzić jawnie, które punkty chcesz mieć widoczne, a nawet zrobić skalę logarytmiczną.
Więcej informacji w dokumentacji Tick locating and formatting.


Коd

locators = [
    NullLocator(), #brak
    FixedLocator([0, 1, 8]), #zafiksowany, będzie tylko dla 0, 1 i 8
    LogLocator(2, [1.]), #skala logarytmiczna
    MultipleLocator(3.) #co trzeci
]

for locator in locators:
    print(str(locator))
    plt.figure(figsize=(12, 7))
    
    ax = plt.gca()
    ax.xaxis.set_major_locator(locator)
    plt.plot(x, y)
    plt.show()
Wynik:

Wiele linii

Więcej niż jedna linia na jednym wykresie.
Коd

z = y / 4.0

plt.figure(figsize=(12, 7))
plt.plot(x, y, label="high")
plt.plot(x, z, label="low")

#dodanie legendy
plt.legend(loc='best'); #opcja "best" (można też wpisać po prostu 0) oznacza, że biblioteka samodzielnie poszukuje najlepszej możliwej opcji
Wynik:

Komentarz / adnotacja (ang. annotation)

Bardziej skomplikowanym przypadkiem jest, kiedy chcemy zaznaczyć wybrany punkt na wykresie i dodać komentarz.
Jako dodatek na wykresie poniżej dodajemy dwie linie: poziomą i pionową.
Swoją drogą, "biznes" (ludzie z biznesu) bardzo to lubi. Kiedy wskazujemy miejsce warte uwagi wykres nagle staje się zrozumiały. Pokazanie wyniku to ważna umiejętność - zwykle osoby techniczne mają z tym problem.

Коd

plt.figure(figsize=(12, 7))
plt.plot(x, y, label="high")
plt.plot(x, z, label="low")

plt.ylim(-6, 7)

plt.annotate(r'$\pi sin(x) + \pi cos(\pi x) + \pi sin(x + \pi )$',
            xy=(2, 5), xycoords='data',
            xytext=(+3, +5.5), textcoords='data', fontsize=15,
            arrowprops=dict(arrowstyle="->",connectionstyle="arc3,rad=.2"),
            )

plt.plot([0, 10], [0, 0], color='grey', linestyle='-.')
plt.plot([2, 2],[0, 10], color='blue', linewidth=1, linestyle="--");
Wynik:

Anatomia matplotlib


Powyżej jest obraz, który opisuje poszczególne elementy wykresu.

Teraz dodajmy:
  1. Tytuł
  2. Agendę
  3. Bardziej zaawansowany podpis na osi x ( 0, +П, +2П, +3П)
  4. Jawnie punkty na dwóch liniach (używając różnych markerów).
Коd

plt.figure(figsize=(12, 7))
plt.plot(x, y, label="high", marker='*')
plt.plot(x, z, label="low", marker='+')

plt.ylim(-6, 7)

plt.annotate(r'$\pi sin(x) + \pi cos(\pi x) + \pi sin(x + \pi )$',
            xy=(2, 5), xycoords='data',
            xytext=(+3, +5.5), textcoords='data', fontsize=15,
            arrowprops=dict(arrowstyle="->",connectionstyle="arc3,rad=.2"),
            )

plt.plot([0, 10], [0, 0], color='grey', linestyle='-.')
plt.plot([2, 2],[0, 10], color='blue', linewidth=1, linestyle="--")

plt.title("Magic Sinusoida", fontsize=20)
plt.xticks([0, np.pi, 2*np.pi, 3*np.pi],
          [r'$0$', r'$+\pi$', r'$+2\pi$', r'$+3\pi$'])

plt.yticks([-6, -5, -4, -3, -2, -1, 0, +1, +2, +3, +4, +5, +6], [r'$-6$', r'$-5$', r'$-4$', r'$-3$', r'$-2$', r'$-1$', r'$0$', r'$+1$', r'$+2$', r'$+3$', r'$+4$', r'$+5$', r'$+6$'])

plt.legend(loc='best');
Wynik:

Zadanie 2.2.1

Przykład z życia wzięty... Zadanie wymaga więcej czasu, aby je wyjaśnić, niż zrealizować. Natomiast celem jest pokazanie mniej standardowego zastosowania, żeby przyzwyczaić się do myślenia, że nawet logi można wizualizować.
Załóżmy że jest system, którego zadaniem jest wykonanie pewnej pracy. Załóżmy, że praca ta polega na renderowaniu plików pdf (z jakąś tam zawartością).

Proces zajmujący się wyrenderowaniem nowego pliku będziemy nazywać job. Istnieje kilka workerów, które mogą tworzyć nowe joby. W tym samym czasie tylko jeden job (wśród wszystkich workerów) może renderować, reszta musi oczekiwać... Działa to w następujący sposób. Workery budzą się w pewnym odstępie czasowym (żeby nie przeszkadzać sobie nawzajem), uwzględniając ile czasu zajmuje średni rendering.
Po "obudzeniu" worker tworzy nowego joba z czasem begin. Następnie jest sprawdzane - czy można zacząć render, jeśli nie (czyli zajęte) - to job idzie spać na losowy okres czasu - jak obudzi się, znowu sprawdza czy render jest wolny, i tak aż uda się wykonać zadanie.
Tak wygląda przykładowy log, w którym są pewne zdarzenia. Żeby przyspieszyć to zadanie, nie musisz parsować logów - zrobiłem to już za Ciebie. Log zawiera następujące informacje.

  • begin - czas, kiedy worker uruchomił job i zaczął sprawdzać, czy może zacząć render
  • finish - czas, kiedy worker skończył job (włączając redner pdf)
  • start_processing - czas, kiedy job zaczął render
  • offset_submit - opóźnienie w sekundach, kiedy job narodził się (begin) w porównaniu do pierwszego (“joba”)
  • offset_processing - opóźnienie w sekundach od momentu, kiedy job narodził się (begin) i faktycznie rozpoczął rendering (innymi słowy, jak długo musiał czekać na rozpoczęcie renderingu)
  • total_seconds - ile czasu sumaryczne zajęło wykonanie joba

Logi
Oczekiwany wynik

Twoim zadaniem jest zwizualizować wszystkie joby, żeby upewnić się, że wykonują się one w uczciwej kolejności. To oznacza, że nie ma wiszących jobów, bo ktoś inny ciągle zajmuje miejsce dla renderingu.
Odpowiedź 2.2.1
Коd

df = pd.read_csv('../input/load_test.csv') #wczytujemy dane

df.head()

## YOUR CODE IS HERE

#może przyda się, jeśli ciężko będzie poradzić z odstępami :)
#plt.ylim(0, 100)
#plt.xlim(0, 3000)
Dodakowe objaśnienie do zadania :)

Jak wcześniej wspominałem, plt.plot() może przyjąć 2 argumenty: x i y, a następnie na ich podstawie rysuje linie, które łączą te punkty. x i y mogą być również listami zawierającymi współrzędne dla kilku punktów (wtedy obie listy muszą być takiej samej długości, aby każdy punkt w liście podanej do x miał swój odpowiednik w liście podanej do y).

Przykładowo plt.plot([1, 2], [3, 4]) oznacza, że rysujemy 2 punkty. Jeden będzie miał współrzędne x=1, y=3, a drugi x=2, y=4. W powyższych linijkach kodu jako argumenty również są podane 2 listy z 2 wartościami (linijka jest wprawdzie długa, ale przyjrzyj się uważnie, gdzie się otwierają i kończą nawiasy []). Żeby 2 punkty uformowały poziomą (bądź pionową) linię, to muszę mieć taką samą wartość na osi y (bądź x, jeśli linia ma być pionowa). Stąd te konstrukty [0, 0], [1, 1] itd. Mówią one, że te 2 punkty podane w liście x mają być na tej samej wysokości.
Jeśli nadal nie do końca czujesz, dlaczego musimy podać [0, 0], [1, 1] itd., aby mieć poziomą (lub pionową) linię, to polecam wziąć kartkę papieru - narysuj osie x oraz y, następnie narysuj pionową lub poziomą linię i zobacz, jak zachowują się współrzędne na tych liniach :).

Histogram, scatter plot, boxplot

Przejdźmy teraz do kształtów zwanych wykresami analitycznymi.
Poznaj kilka innych wykresów, które mogą być dla nas przydatne
  • histogram
  • boxplot
  • scatterplot
Każdy z nich ma swoje wady i zalety. Histogram jest jednym z wykresów, którego używam najczęściej, więc od niego zaczniemy.

Histogram
Jak należy go odczytać? Histogram nakłada się na konkretną zmienną, np. wiek. W histogramie określamy, ile chcemy mieć koszyków, np. 10 czy 100. Załóżmy, że mamy 10 i wiek mamy od 0 do 100 (podobno można żyć dłużej, ale na razie zostawmy to). To wtedy mamy 10 koszyków od 0 do 10, od 10 do 20, do 20 do 30 itd.
Teraz naszym zadaniem jest poukładać dane w tych koszykach, np. jeśli mamy Age=28, to trafi do koszyka pomiędzy 20 a 30, jeśli Age=42, to trafia do koszyka pomiędzy 40 a 50. Innymi słowy, wszystkie nasze dane rozkładamy po koszykach i na końcu liczymy (czyli to jest oś Y), ile mamy danych w poszczególnych koszykach.
Poniżej jest przykład histogramu, akurat zrobiony na danych losowych (dlatego jest zmienny).

Коd

plt.figure(figsize=(12, 7))
plt.hist(np.random.randn(1000), bins=40);
Wynik:

Boxplot
Ten wykres działa trochę inaczej niż histogram. Zawiera mniej informacji, bo tak naprawdę pokazuje tylko wartość minimalną, maksymalną, średnią i medianę.
W tym artykule znajdziesz więcej szczegółów na temat boxplota.

Zróbmy teraz wykres.

Коd

plt.figure(figsize=(12, 7))
plt.boxplot(np.random.randn(50));
Wynik:

Boxplot czasem jest szczególnie lubiany przez pewne grupy ludzi, bo jest wygodny przez to, że kompresuje wiedzę tylko do 4-5 liczb, czyli nie pokazuje prawdziwego rozkładu danych jak histogram. Chociaż czym mniej jest koszyków w histogramie, tym bardziej ma podobny problem jak boxplot. Na skutek tego naprawdę potrafi być mylący.

Scatter plot
Poznajmy jeszcze jeden dość przydatny sposób na wizualizację danych.
To podejście także ma swoje wady, np. jeśli mamy dane, które nakładają się na siebie, to nadal widzimy jedną "kropkę", ale za tą jedną może stać 10 kropek czy nawet 1000 kropek, po prostu wizualnie nałożyły się na siebie. Da się to częściowo rozwiązać wprowadzając np. przezroczystość lub tak zwany trick "jitter", kiedy "rozrzucamy" kropki w sposób losowy z delikatnym krokiem, w wyniku czego próbujmy zminimalizować ryzyko wystąpienia sytuacji, że kropki nałożą się na siebie i przysłonią się.
Коd

n = 100
plt.scatter(range(n), np.random.randn(n));
Wynik:

Zadanie 2.2.2*

Teraz trochę bardziej zaawansowane pytania, ale spróbuj na nie odpowiedzieć. Jeśli są za trudne, możesz je na początku pominąć. Potem do nich wrócisz:
  1. Kiedy warto stosować histogram (zalety i wady)?
  2. Kiedy warto stosować scatter plot (zalety i wady)?
  3. Kiedy warto stosować box plot (zalety i wady)?

Odpowiedzi 2.2.2
1.
2.
3.
Iris

Przykład wizualizacji na podstawie zbioru iris.

Коd

iris = d.load_iris()

print(iris.DESCR)
.. _iris_dataset:

Iris plants dataset
--------------------

**Data Set Characteristics:**

:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
- sepal length in cm
- sepal width in cm
- petal length in cm
- petal width in cm
- class:
- Iris-Setosa
- Iris-Versicolour
- Iris-Virginica

:Summary Statistics:

============== ==== ==== ======= ===== ====================
Min Max Mean SD Class Correlation
============== ==== ==== ======= ===== ====================
sepal length: 4.3 7.9 5.84 0.83 0.7826
sepal width: 2.0 4.4 3.05 0.43 -0.4194
petal length: 1.0 6.9 3.76 1.76 0.9490 (high!)
petal width: 0.1 2.5 1.20 0.76 0.9565 (high!)
============== ==== ==== ======= ===== ====================

:Missing Attribute Values: None
:Class Distribution: 33.3% for each of 3 classes.
:Creator: R.A. Fisher
:Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
:Date: July, 1988

The famous Iris database, first used by Sir R.A. Fisher. The dataset is taken
from Fisher's paper. Note that it's the same as in R, but not as in the UCI
Machine Learning Repository, which has two wrong data points.

This is perhaps the best known database to be found in the
pattern recognition literature. Fisher's paper is a classic in the field and
is referenced frequently to this day. (See Duda & Hart, for example.) The
data set contains 3 classes of 50 instances each, where each class refers to a
type of iris plant. One class is linearly separable from the other 2; the
latter are NOT linearly separable from each other.

.. topic:: References

- Fisher, R.A. "The use of multiple measurements in taxonomic problems"
Annual Eugenics, 7, Part II, 179-188 (1936); also in "Contributions to
Mathematical Statistics" (John Wiley, NY, 1950).
- Duda, R.O., & Hart, P.E. (1973) Pattern Classification and Scene Analysis.
(Q327.D83) John Wiley & Sons. ISBN 0-471-22361-1. See page 218.
- Dasarathy, B.V. (1980) "Nosing Around the Neighborhood: A New System
Structure and Classification Rule for Recognition in Partially Exposed
Environments". IEEE Transactions on Pattern Analysis and Machine
Intelligence, Vol. PAMI-2, No. 1, 67-71.
- Gates, G.W. (1972) "The Reduced Nearest Neighbor Rule". IEEE Transactions
on Information Theory, May 1972, 431-433.
- See also: 1988 MLC Proceedings, 54-64. Cheeseman et al"s AUTOCLASS II
conceptual clustering system finds 3 classes in the data.
- Many, many more ...

Коd

plt.figure(figsize=(12, 7))
plt.boxplot(iris.data)

plt.xticks(range(1, len(iris.data[0]) + 1), ['sepal length', 'sepal width', 'petal length', 'petal width'] )
plt.ylim(-1, 9);
Wynik:

Przedtem używaliśmy matplotlib. To jest niskopoziomowa biblioteka. Spójrzmy teraz na Seaborn, która jest bardziej deklaratywną biblioteką (gdzie bardziej mówimy "co zrobić", a mniej "jak to zrobić").

Seaborn
To jest biblioteka, która pod spodem również używa matplotlib, ale pozwala robić bardziej złożone wykresy w prostszy sposób. Chociaż jak to zwykle bywa - coś za coś ;). Jeśli akurat chcesz zrobić coś innego, niż zaplanował w swoich szablonach autor seaborn, to już będzie trudno i trzeba będzie cofnąć się do matplotliba.
Dlatego warto poznać co najmniej te dwie biblioteki.
Najpierw wykres displot().

Коd

sns.distplot(np.random.randn(1000));
Wynik:

Na tym wykresie masz kilka rzeczy. Pierwsza, to histogram i też możesz zmieniać liczbę koszyków. Spróbujmy to zmienić.
Коd

sns.distplot(np.random.randn(1000), bins=100);
Wynik:

Również histogram można wyłączyć w taki sposób:
Коd

sns.distplot(np.random.randn(1000), hist=False);
Wynik:

Zostaje nam wtedy estymator gęstości, więcej o tym można poczytać tutaj. Dodatkowo można włączyć rug plot.
Uwaga! Jeśli słyszysz o tym po raz pierwszy, to nie próbuj zrozumieć wszystkiego naraz. Będzie fajnie, jeśli zobaczysz i będziesz wiedzieć, że takie coś jest (tym samym zmniejsza się twoje "nie wiem, czego nie wiem").

Коd

sns.distplot(np.random.randn(1000), hist=False, rug=True);
Wynik:

Iris
Wróćmy do zbioru iris. Zróbmy scatter plot, ale trochę rozbudowany, czyli na bokach jeszcze będą histogramy, czyli jointplot().


Коd

iris = sns.load_dataset("iris")

sns.jointplot(x="sepal_length", y="sepal_width", data=iris, height=5);
Wynik:

Można "kropki" pokolorować w zależności od klasy, do której należą (w naszym przypadku mamy 3 klasy).

Коd

sns.FacetGrid(iris, hue="species", height=5) \
  .map(plt.scatter, "sepal_length", "sepal_width") \
  .add_legend();
Wynik:

Też można zrobić .boxplot().
Коd

sns.boxplot(x="species", y="petal_length", data=iris);
Wynik:

Teraz .stripplot(), który jest szczególnym przypadkiem scatterplot().
Коd

sns.boxplot(x="species", y="petal_length", data=iris);
Wynik:

Nałóżmy teraz jeszcze mechanizm jitter, czyli losowanie, dzięki czemu kropki będą się mniej pokrywały. W tym przypadku akurat to nie jest aż tak bardzo zauważalne, ale warto wiedzieć, że taki parameter istnieje.
Коd

sns.stripplot(x="species", y="petal_length", jitter=True, data=iris);
Wynik:

W skrajnym przypadku: scatterplot + jitter to .swarmplot(). W tym przypadku mamy gwarancję, że za każdą kropką stoi pojedyncza kropka. Tylko to podejście też ma wady. Jak myślisz jakie?
Коd

sns.swarmplot(x="species", y="petal_length", data=iris);
Wynik:

Można zrobić szereg .boxplot() w jednym miejscu i tak to będzie wyglądało:
Коd

melt_iris = pd.melt(iris, "species", var_name="measurement")
sns.boxplot(x="measurement", y="value", hue="species", data=melt_iris);
Wynik:

Коd

sns.swarmplot(x="measurement", y="value", hue="species", data=melt_iris);
Wynik:

Коd

sns.pairplot(iris, hue="species");
Wynik:

Pandas (plotting)
Pandas też może tworzyć wykresy z pudełka (używając matplotlib pod spodem).

Коd

cmaps = dict(zip(iris.species.unique(), ['red', 'blue', 'green']))

scatter_matrix(iris, c=iris.species.map(lambda x: cmaps[x]), s=90, figsize=(12, 10));
Wynik:

Tu jest przykładowa lekcja z pierwszego modułu kursu "Data Science kurs podstaw". W sumie jest 40 lekcji + ćwiczenia bonusowe, 40+ filmów krok po kroku dla każdej lekcji ze szczegółowymi wyjaśnieniami.
Dostaniesz gotowe środowisko, w którym uczysz się materiału za pomocą tekstu i wideo. Nie musisz niczego instalować ani ustawiać.

Zdobywasz praktykę, piszesz kod już od pierwszej lekcji! Ucz się we własnym tempie - kurs będzie dostępny 24/7.
Dołącz już teraz i rozpocznij swoją wygodną naukę.