Wizualizacja danych w Python

Wstęp

Cykl życia projektów Data Science składa się z różnych elementów. Jednym z nich jest etap Expolratory Data Analysis , a więc Eksploracyjna Analiza Danych. Jego celem jest przeprowadzenie zaawansowanej analizy statystycznej opierając sie nie tylko na statystykach opisowych, ale również na dobrze dobranych wizualizacjach. Stąd przygotowanie dobrej analizy nie jest możliwe bez umiejętności wizualizacji danych oraz prawidłowej ich interpretacji.

Istnieje wiele rodzajów wizualizacji danych. Wszystko zależy od tego jakiego rodzaju dane posiadamy - kategoryczne, numeryczne, a może dane zależne od czasu. Każda z metod wizualizacji odpowie nam na inne pytanie, dlatego dobranie odpowiedniej do naszego problemu jest kluczowe w kontekście zrozumienia danych przez nas, jak i naszych kolegów z zespołu czy kierownictwo.

Poniższy Tutorial opiera się na trzech najczęściej wykorzystywanych bibliotekach do wizualizacji danych w Python: matplotlib, seaborn oraz plotly. Są to najczęściej wykorzystywane przez nas biblioteki w trakcie trwania projektu. Ich opanowanie jest często kluczowe w kontekście przygotowania dobrej jakości analizy. W naszym odczuciu opanowanie przynajmniej podstaw każdej z tych bibiliotek może zapewnić zbudowanie ciekawej analizy, która odpowie nam na postawione przez nas pytanie. Oczywiście musicie mieć na uwadze, że nie wyczerpują one pełnej gamy dostępnych bibliotek czy metod wizualizacjii danych.

Wnioski:
  1. Wizualizacja danych jako niezbędny element Eksploracyjnej Analizy Danych.

  2. Dobra wizualizacja to klucz do sukcesu i prawidłowego zrozumienia danych.

Importujemy biblioteki przydatne do wizualizacji.

# Pobranie przykladowych danych
from sklearn.datasets import fetch_california_housing

# Data wrangling 
import pandas as pd
import numpy as np

# Data visualization
import seaborn as sns
from matplotlib import pyplot as plt
from plotly import express as px

# Inne
import datetime as dt

Przygotowanie danych

Do przygotowania wizualizacji posłużą nam dane dostępne w sklearn.datasets oraz seaborn.datasets.

Housing

Opis: Zestaw danych California Housing zawiera informacje ze spisu powszechnego w Kalifornii z 1990 roku.

Dane nie są w żaden sposób wyczyszczone, stąd posłużą jako dobry materiał do eksploracyjnej analizy danych.

# Funkcja do przygotowania ramki danych
def data_preparation():
    """Przygotowanie danych na potrzeby notatnika."""
    
    # Load data
    housing = fetch_california_housing()
    
    # Data Array
    df_array = housing.data.copy()
    target = housing.target.copy()
    target = target.reshape(len(target), 1)
    
    df_array = np.concatenate([df_array, target], axis=1)
    
    # Colnames
    colnames = housing.feature_names.copy()
    colnames.append(housing.target_names[0])
    
    # Data Frame
    df = pd.DataFrame(df_array, columns=colnames)
    
    return df
# Pobranie danych

## Housing
housing = data_preparation()

print(f'Wymiar ramki danych: {housing.shape}')
print(f'Nazwy kolumn: {housing.columns.values}')
Wymiar ramki danych: (20640, 9)
Nazwy kolumn: ['MedInc' 'HouseAge' 'AveRooms' 'AveBedrms' 'Population' 'AveOccup'
 'Latitude' 'Longitude' 'MedHouseVal']
# Wysietlenie pierwsyzch 5 obserwacji
housing.head()
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude MedHouseVal
0 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88 -122.23 4.526
1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86 -122.22 3.585
2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85 -122.24 3.521
3 5.6431 52.0 5.817352 1.073059 558.0 2.547945 37.85 -122.25 3.413
4 3.8462 52.0 6.281853 1.081081 565.0 2.181467 37.85 -122.25 3.422

Niektóre z tych zmiennych poddam transformacji do celów edukacyjnych.

# Data transformation
housing['AveRooms'] = housing['AveRooms'].astype(int)
housing['AveBedrms'] = housing['AveBedrms'].astype(int)

# Feature Engineering
housing['AveRooms_greater_5'] = np.where(housing['AveRooms'] > 5, '>5', '<=5')
housing['MedInc_greater_5'] = np.where(housing['MedInc'] > 5, '>5', '<=5')
housing.head()
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude Longitude MedHouseVal AveRooms_greater_5 MedInc_greater_5
0 8.3252 41.0 6 1 322.0 2.555556 37.88 -122.23 4.526 >5 >5
1 8.3014 21.0 6 0 2401.0 2.109842 37.86 -122.22 3.585 >5 >5
2 7.2574 52.0 8 1 496.0 2.802260 37.85 -122.24 3.521 >5 >5
3 5.6431 52.0 5 1 558.0 2.547945 37.85 -122.25 3.413 <=5 >5
4 3.8462 52.0 6 1 565.0 2.181467 37.85 -122.25 3.422 >5 <=5

Przed przystąpieniem do wizualizacji należy zaznaczyć, które z powyższych zmiennych to zmienna celu, a które to zmienne objaśniające.

Zmienna celu: MedHouseVal

Zmienne objaśniające: MedInc, HouseAge, AveRooms, AveBedrms, Population, AveOccup, Latitude, Longitude

Fmri

Opis: Funkcjonalne obrazowanie rezonansem magnetycznym lub funkcjonalny MRI (fMRI) mierzy aktywność mózgu za pomocą silnego, statycznego pola magnetycznego w celu wykrycia zmian związanych z przepływem krwi. Kiedy używany jest obszar mózgu, przepływ krwi do tego obszaru również wzrasta. Zwiększony przepływ krwi jest reprezentowany przez sygnał o wyższej amplitudzie, postrzegany jako silna aktywność nerwowa.

## Fmri
fmri = sns.load_dataset("fmri")

print(f'Wymiar ramki danych: {fmri.shape}')
print(f'Nazwy kolumn: {fmri.columns.values}')
Wymiar ramki danych: (1064, 5)
Nazwy kolumn: ['subject' 'timepoint' 'event' 'region' 'signal']
fmri.head()
subject timepoint event region signal
0 s13 18 stim parietal -0.017552
1 s5 14 stim parietal -0.080883
2 s12 18 stim parietal -0.081033
3 s11 18 stim parietal -0.046134
4 s10 18 stim parietal -0.037970

W tym przypadku mamy do czynienia z pewnymi szeregami czasowymi, gdzie zmienna czasu jest timepoint, a zmienna celu signal. Szeregi czasowe możemy podzielić względem zmiennej region oraz event, gdzie region == parietal dotyczy płatu ciemieniowego, natomimast region == frontal dotyczy jego płatu czołowego mózgu.

Tips

Opis: Ramka danych opierająca się na informacjach reprezentujących niektóre dane dotyczące napiwków, w których jeden kelner zapisał informacje o każdym napiwku, który otrzymał w ciągu kilku miesięcy pracy w jednej restauracji. Kelner zebrał kilka zmiennych: napiwek w dolarach, rachunek w dolarach, płeć płatnika rachunku, czy na imprezie byli palacze, dzień tygodnia, pora dnia i wielkość imprezy.

## Tips
tips = sns.load_dataset("tips")

print(f'Wymiar ramki danych: {tips.shape}')
print(f'Nazwy kolumn: {tips.columns.values}')
Wymiar ramki danych: (244, 7)
Nazwy kolumn: ['total_bill' 'tip' 'sex' 'smoker' 'day' 'time' 'size']
tips.head()
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4

W tym przypadku możemy ustalić następujące zminne objaśniające oraz zmienna celu:

Zmienna celu: tip

Zmienne objaśniające: total_bill, sex, smoker, day, time, size

Anscombe

Opis: Kwartet Anscombe’a to zestaw czterech zestawów danych o identycznych cechach statystycznych, takich jak średnia arytmetyczna, wariancja, współczynnik korelacji czy równanie regresji liniowej, jednocześnie wyglądających zgoła różnie przy przedstawieniu graficznym. Układ tych danych został stworzony w 1973 roku przez brytyjskiego statystyka Francisa Anscombe’a aby ukazać znaczenie graficznej reprezentacji danych przy okazji ich analizy statystycznej.

## Anscombe
anscombe = sns.load_dataset("anscombe")

print(f'Wymiar ramki danych: {anscombe.shape}')
print(f'Nazwy kolumn: {anscombe.columns.values}')
Wymiar ramki danych: (44, 3)
Nazwy kolumn: ['dataset' 'x' 'y']
anscombe.head()
dataset x y
0 I 10.0 8.04
1 I 8.0 6.95
2 I 13.0 7.58
3 I 9.0 8.81
4 I 11.0 8.33

Poniżej, dowód dotyczących tych samych wartości statystyki średniej arytmetycznej oraz odchylenia standardowego dla zmiennych x i y.

anscombe.groupby(['dataset']).agg({
    'x': ['mean', 'std', 'min', 'max'],
    'y': ['mean', 'std', 'min', 'max']
})
x y
mean std min max mean std min max
dataset
I 9.0 3.316625 4.0 14.0 7.500909 2.031568 4.26 10.84
II 9.0 3.316625 4.0 14.0 7.500909 2.031657 3.10 9.26
III 9.0 3.316625 4.0 14.0 7.500000 2.030424 5.39 12.74
IV 9.0 3.316625 8.0 19.0 7.500909 2.030579 5.25 12.50

Wizualizacja zależności statystycznych

Analiza statystyczna to proces zrozumienia, w jaki sposób zmienne w zbiorze danych są ze sobą powiązane i jak te relacje zależą od innych zmiennych. Wizualizacja może być kluczowym elementem tego procesu, ponieważ gdy dane są odpowiednio wizualizowane, możemy dostrzec trendy i wzorce wskazujące na związek między nimi. Przeprowadzenie dokładnej analizy statystycznej jest procesem często niezbędnym w pierwszej fazie projektu, gdyd dobre zrozumienie danych jest kluczowe przed przystąpieniem do dalszych etapów projektu.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

sns.set_theme(style="darkgrid")

Badanie relacji między zmiennymi przy użyciu wykresu punktowego

Wykres punktowy jest podstawą wizualizacji statystycznej. Pozwala nam odpowiedzieć na kilka podstwowych pytań:

- Czy istnieje jakakolwiek zależność między dwiema zmiennymi ?
- Czy istnieją obserwacje nietypowe (anomalie) ?
- Czy dane układają nam się może w jakieś podgrupy ?

Oczywiście to tylko przykłady. Generalnie, wykres punktowy przedstawia łączny rozkład dwóch zmiennych za pomocą punktów, gdzie każdy punkt reprezentuje obserwacje w zbiorze danych. Wykres tego rodzaju umożliwia wywnioskowanie czy istnieje między zmiennymi jakakolwiek relacja - liniowa bądź nieliniowa. Na jej podstawie możemy też dojść do pierwszych wniosków związanych z przyszłym etapem Feature Engineering - może się okazać, że transformacja zmiennej objaśnianej np. log, power, square itp. w lepszy sposób wyjaśnia naszą zmienną celu.

sns.relplot(x="MedInc", y="MedHouseVal", data=housing, alpha=0.3)
plt.title('Wykres punktowy', fontsize=18)
Text(0.5, 1.0, 'Wykres punktowy')
../_images/3_Wizualizacja danych_40_1.png
Info

W wielu rodzajach wizualizacji pojawia sie parametr alpha. Sluzy on do ustawienia przezroczystosci wykresu. Zdefiniowanie tego parametru jest szczegolnie przydatne, gdy wiele obserwacji nachodzi na siebie. Dzieki ustawieniu malej wartosci alpha jestesmy w stanie okreslic w jakim obszarze wystepuja najczesciej nasze dane.

W przypadku biblioteki matplotlib wykres wyglądał by następująco:

plt.scatter(x=housing['MedInc'], y=housing['MedHouseVal'], alpha=0.3)
plt.xlabel('MedInc')
plt.ylabel('MedHouseVal')
plt.title('Wykres punktowy', fontsize=18)
Text(0.5, 1.0, 'Wykres punktowy')
../_images/3_Wizualizacja danych_43_1.png
Info

Z uwagi na to, że pod spodem biblioteki seaborn działa matplotlib to wiele funkcji, działająych dla matplotlib tj. xlabel, ylabel, title działa również dla seaborn.

W przypadku biblioteki seaborn w prosty sposób można dodać dodatkową relację do tego rodzaju wykresu. Dodając parameter hue, mamy możliwość sprawdzić powyższą relację względem zmiennej kategorycznej.

sns.relplot(x="MedInc", y="MedHouseVal", hue="AveRooms_greater_5", data=housing);
../_images/3_Wizualizacja danych_46_0.png

Aby sprawdzić kolejną relację, mamy możliwość dodania zmiennej kategorycznej która będzie charakteryzowała się innym znacznikiem - do tego służy parametr style.

sns.relplot(x="MedInc", y="MedHouseVal", hue="AveRooms_greater_5", style="MedInc_greater_5", data=housing);
../_images/3_Wizualizacja danych_48_0.png

Na powyższych wykresach w parametrach hue oraz style podane są zmienne kategoryczne. W przypadku podania tam zmiennych numerycznych wizualizacja wyglądała by następująco.

sns.relplot(x="MedInc", y="MedHouseVal", hue="HouseAge", data=housing)
<seaborn.axisgrid.FacetGrid at 0x24c0ee0eee0>
../_images/3_Wizualizacja danych_50_1.png
ha_value = np.sort(housing['HouseAge'].unique())

print(f'Unikalne wartości HouseAge: {ha_value}')
Unikalne wartości HouseAge: [ 1.  2.  3.  4.  5.  6.  7.  8.  9. 10. 11. 12. 13. 14. 15. 16. 17. 18.
 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36.
 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52.]

Jak widać funkcja relplot sama poradziła sobie z podziałem naszej zmiennej tak, aby kolory punktów nie były przypisane do każdej z wartości tylko do ich przedziałów - w tym przypadku do przedziału długości 10 jednostek.

Bardzo często podczas pracy z danymi chcemy mieć możliwość pracy interaktywnej z naszymi danymi - w tym przypadku bardzo pomocny jest pakiet plotly.

# Wizualizacja - plotly
fig = px.scatter(data_frame=housing, x='MedInc', y='MedHouseVal', color='MedInc_greater_5', title='Wykres punktowy')
fig.show()

W przypadku plotly zachęcam do indywidualnej nauki. Jego opanowanie na wysokim poziomie na pewno przyda się podczas pracy projektowej.

Podkreślenie relacji liniowych między zmiennymi

Choć wykresy punktowe są bardzo efektywne i same w sobie są w stanie bardzo dużo powiedzieć nam o relacjach między zmiennymi to nie są jednak uniwersalnym typem wykresu. Wizualizacja powinna być dostosowana do specyfiki analizowanych danych i pytania, na które próbujemy sobie odpowiedzieć.

W przypadku szeregów czasowych chcemy zrozumieć zmiany ciągłej zmiennej w stosunku do zmiennej czasu. W takiej sytuacji niezbędnym jesy narysowanie wykresu liniowego.

Wykresy liniowe sprawdzają się gdy naszym celem jest wizualizacja szeregu czasowego lub sprawdzenie ostatecznej zależności liniowej między zmiennymi - jak w przypadku regplot powyżej.

W poniższym przykładzie przed przystąpieniem do takiej wizualizacji należy przygotować dane.

import datetime as dt

n = 500
start = dt.date(2020, 1, 1)
end = start + dt.timedelta(days=n-1)

df_dict = dict(
    time=pd.date_range(start, end),
    value=np.random.randn(n).cumsum()
)

df = pd.DataFrame(df_dict)
g = sns.relplot(x="time", y="value", kind="line", data=df, height=8)
../_images/3_Wizualizacja danych_59_0.png
Uwaga!

Ponieważ relplot zakłada, że najczęściej próbujemy narysować y jako funkcję od x, dlatego domyślnym zachowaniem jest sortowanie danych według wartości x narysowaniem wykresu. Sortowanie danych w tym przypadku jest niezwykle istotne - nalezy o tym pamiętać korzystając z innych bibliotek np. matplotlib lub plotly.

# Przypadek gdy wyłączymy sortowanie względem zmiennej "x"
df_dict = dict(
    x=np.random.rand(100),
    y=np.random.rand(100).cumsum()
)

df = pd.DataFrame(df_dict)
g = sns.relplot(x="x", y="y", kind="line", sort = False, data=df, height=8)
../_images/3_Wizualizacja danych_61_0.png

Wizualizacja w przypadku agregacji danych

Bardziej złożone zbiory danych będą miały wiele pomiarów dla tej samej wartości zmiennej x. Domyślnym zachowaniem w seaborn jest agregacja wielokrotnych pomiarów dla każdej wartości x poprzez wykreślenie średniej i 95% przedziału ufności wokół średniej.

sns.relplot(x="timepoint", y="signal", kind="line", data=fmri);
../_images/3_Wizualizacja danych_64_0.png

Czasami wykreślenie przedziału ufności może być czasochłonne, dlatego jest możliwość wyłączenia tego wykorzystując parametr ci.

sns.relplot(x="timepoint", y="signal", ci=None, kind="line", data=fmri);
../_images/3_Wizualizacja danych_66_0.png

Inną interesującą opcją jest wykreślenie przedziału ufności jako wartość odchylenia standardowego dla danej zmiennej.

sns.relplot(x="timepoint", y="signal", kind="line", ci="sd", data=fmri);
../_images/3_Wizualizacja danych_68_0.png

Oczywiście jest możliwość wyłączenia estymacji średniej dla danej zmiennej, jednak w wyniku tej operacji mamy kilka wartości zmiennej y dla zmiennej x. Domyślnym estymatorem jest średnia.

sns.relplot(x="timepoint", y="signal", estimator=None, kind="line", data=fmri);
../_images/3_Wizualizacja danych_70_0.png

Funkcja relplot(kind="line") ma taką samą elastyczność co wykres punktowy. Może pokazywać do trzech dodatkowych zmiennych, modyfikując odcień, rozmiar i styl elementów wykresu. Robi to przy użyciu tego samego API.

sns.relplot(x="timepoint", y="signal", hue="event", kind="line", data=fmri)
<seaborn.axisgrid.FacetGrid at 0x24c0f13f130>
../_images/3_Wizualizacja danych_72_1.png
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
            kind="line", data=fmri);
../_images/3_Wizualizacja danych_73_0.png
sns.relplot(x="timepoint", y="signal", hue="region", style="event",
            dashes=False, markers=True, kind="line", data=fmri);
../_images/3_Wizualizacja danych_74_0.png

Taki sam typ wykresu można przedstawić przy pomocy biblioteki matplotlib jednak wtedy wymagana jest większa liczba operacji na danych.

# Data preparation
fmri_agg = fmri.sort_values(by='timepoint').groupby(['timepoint', 'event', 'region']).agg({'signal': 'mean'}).reset_index()

print(f'Wymiar ramki danych: {fmri_agg.shape}')
print(f'Nazwy kolumn: {fmri_agg.columns.values}')
Wymiar ramki danych: (76, 4)
Nazwy kolumn: ['timepoint' 'event' 'region' 'signal']
# Data filters
mask1 = (fmri_agg['event'] == 'cue') & (fmri_agg['region'] == 'frontal')
mask2 = (fmri_agg['event'] == 'cue') & (fmri_agg['region'] == 'parietal')
mask3 = (fmri_agg['event'] == 'stim') & (fmri_agg['region'] == 'frontal')
mask4 = (fmri_agg['event'] == 'stim') & (fmri_agg['region'] == 'parietal')

plt.figure(figsize=(12, 8))

plt.plot('timepoint', 'signal', data=fmri_agg[mask1], color='blue')
plt.plot('timepoint', 'signal', data=fmri_agg[mask2], color='green')
plt.plot('timepoint', 'signal', data=fmri_agg[mask3], color='red')
plt.plot('timepoint', 'signal', data=fmri_agg[mask4], color='black')
plt.title('Wykres liniowy', fontsize=20)
plt.xlabel('Time')
plt.ylabel('Signal')
Text(0, 0.5, 'Signal')
../_images/3_Wizualizacja danych_77_1.png

Prezentacja wielu relacji na róznych polach wykresu

Czasami chcemy zrozumieć jakie zachodzą relację między zmiennymi wizualizując je na oddzienlych polach wykresu. W tym przypadku przydatnym staję się parametr col lub row.

sns.relplot(x="timepoint", y="signal", hue="event",
            col="region", kind='line', data=fmri);
../_images/3_Wizualizacja danych_80_0.png

W tym przypadku przydatny okazuje się także wykres oparty o plotly. Jego podstawowa forma wygląda następująco:

fig = px.line(data_frame=fmri_agg, x='timepoint', y='signal', color='event', facet_col='region', title='Wykres liniowy')
fig.show()

Czasami do wykresy opartego o seaborn warto dodać parametr col_wrap, który umożliwia nam zdefiniowanie liczby kolumn dla danej wizualizacji.

sns.relplot(x="timepoint", y="signal", hue="event", col="subject", 
            col_wrap=5, kind='line', data=fmri.query("region == 'frontal'"), height=3);
../_images/3_Wizualizacja danych_84_0.png

Wizualizacja rozkładu zmiennych

Jednym z pierwszych kroków podczas budowy modeli uczenia maszynowego powinno być zrozumienie, w jaki sposób rozkładają się zmienne. Techniki wizualizacji rozkładu zmiennych mogą dostarczyć szybkich odpowiedzi na wiele ważnych pytań. Jaki zakres obejmują obserwacje? Jaka jest ich główna tendencja? Czy są mocno przekrzywione w jednym kierunku? Czy istnieją znaczące wartości odstające? Czy odpowiedzi na te pytania różnią się w podzbiorach zdefiniowanych przez inne zmienne?

Biblioteka seaborn zawiera kilka bardzo przydatnych funkcji pod kątem badania rozklładu tj. histplot(), kdeplot(), ecdfplot() i rugplot(). Są one zgrupowane w ramach funkcji displot(), jointplot() i pairplot().

Istnieje kilka różnych podejść do wizualizacji rozładu zmiennych, a każde z nich ma swoje względne zalety i wady. Ważne jest, aby zrozumieć te czynniki, aby wybrać najlepsze podejście do konkretnego celu.

Rozkład zmiennej jednowymiarowej

Najczęstszym podejściem do wizualizacji rozkładu jest histogram, który jest graficzną reprezentacją danych, która agreguje grupę punktów w zakresy określone przez analityka. Podobnie jak w przypadku wykresu słupkowego, histogram kondensuje obserwacje w łatwo interpretowalną wizualizację. Histogram jest jedną z podstawowych form wizualizacji wykorzystywaną w celu określenie rozkładu danej cechy. Podczas pracy z danymi często kluczym jest aby określić w jaki sposób rozkładają się nasze dane - takie informacje można wtedy wykorzystać w dalszej pracy m.in. określając rodzaj algorytmu czy dokonując różnego rodzaju transformacji danej zmiennej.

sns.displot(housing, x="MedHouseVal")
<seaborn.axisgrid.FacetGrid at 0x24c11022ac0>
../_images/3_Wizualizacja danych_89_1.png

Powyższy histogram dla zmiennej MedHouseVal w bardzo szybki sposób jest w stanie przedstawić nam pewne kluczowe informacje na temat zmiennej celu. Przykładem może być kwestia najpopularniszych wartości tej zmiennej która jest na poziomie około 1.5 oraz 5. Jednak bardzo ciekawą sytuacją jest kwestia wartości 5, która jest bardzo popularna jednak z drugiej strony może być pewnego rodzaju błędem - zwłaczsza jak spojrzymy na rozkład zmiennej bez wartości 5, gdzie wartości dla tego ogona są malejące.

W przypadku histogramu bardzo ważną rolę odgrywają parametry bins lub binwdith, które mówią nam o wielkości lub długości przedziałów dla histogramu. Te same dane przedstawione z inną wartością parametru binwidth mogą nam odpowiedzieć na zadane pytanie w zupełnie inny sposób. Przykładam jesy poniższy histogram na którym nie widać żadnych anomalii dla wartości zmiennej równej 5.

sns.displot(housing, x="MedHouseVal", binwidth=1)
<seaborn.axisgrid.FacetGrid at 0x24c0cd3c610>
../_images/3_Wizualizacja danych_91_1.png

Dodatkowo pokażmy jak ten samy wykres wyglądałby z wykorzystaniem pakietów matplotlib oraz plotly - oczywiście poniższe wizualizację uwzględniają domyślne opcję.

plt.figure(figsize=(8, 6))
plt.hist(x='MedHouseVal', data=housing, bins=5)
plt.title('Histogram', fontsize=20)
plt.ylabel('Frequency')
plt.xlabel('MedHouseVal')        
Text(0.5, 0, 'MedHouseVal')
../_images/3_Wizualizacja danych_93_1.png
fig = px.histogram(data_frame=housing, x='MedHouseVal', nbins=5, title='Histogram')
fig.show()

Wizualizacja zmiennych kategorycznych

Możliwa jest również wizualizacja rozkładu zmiennej kategorycznej za pomocą logiki histogramu.W tym pzypadku pomocny może być parametr shirnk, który nieco zwężą słupki aby podkreślić kategorialny charakter osi.

sns.displot(housing, x="MedInc_greater_5", shrink=0.7)
<seaborn.axisgrid.FacetGrid at 0x24c12f101c0>
../_images/3_Wizualizacja danych_97_1.png

Rozkład warunkowy względem innych zmiennych

Po zrozumieniu rozkładu zmiennej, następnym krokiem jest często pytanie, czy cechy tego rozkładu różnią się od innych zmiennych w zbiorze danych. displot zapewnia obsługę podzbiorów warunkowych poprzez barwy.

sns.displot(fmri, x='signal', hue="region")
<seaborn.axisgrid.FacetGrid at 0x24c0cd41520>
../_images/3_Wizualizacja danych_100_1.png

Czasam warto nieco zmienić wizualizacje naszych danych poprzez zmianę wartości parametru element na step. Pomaga to czasem nieco lepiej zauwazyć różnicę w danych.

sns.displot(fmri, x='signal', hue="region", element='step')
<seaborn.axisgrid.FacetGrid at 0x24c10f40520>
../_images/3_Wizualizacja danych_102_1.png

Innym rozwiązaniem jest pokazanie rozkładów, które się na siebie nakładają.

sns.displot(fmri, x='signal', hue="region", multiple='stack')
<seaborn.axisgrid.FacetGrid at 0x24c10ff8340>
../_images/3_Wizualizacja danych_104_1.png
sns.displot(fmri, x='signal', hue="region", multiple='dodge')
<seaborn.axisgrid.FacetGrid at 0x24c1107dbe0>
../_images/3_Wizualizacja danych_105_1.png

Rozkłady możemy też pokazać na dwóch osobnych wykresach.

sns.displot(fmri, x='signal', col="region")
<seaborn.axisgrid.FacetGrid at 0x24c12dee040>
../_images/3_Wizualizacja danych_107_1.png
Wnioski

Wybór wizualizacji zależy głównie od analityka i od problemu, który pragnie rozwiązać. Róznego rodzaju dane wymagają róznego podejścia.

Wartym zaznaczenia jest jeszcze jeden przydatny parametr, mianowicie stat. Umożliwia on nam zmianę sposobu wyświetlenia zmiennej z domyślnej count na density lub probability.

sns.displot(fmri, x='signal', hue="region", stat='density')
<seaborn.axisgrid.FacetGrid at 0x24c12b68bb0>
../_images/3_Wizualizacja danych_110_1.png
sns.displot(fmri, x='signal', hue="region", stat='probability')
<seaborn.axisgrid.FacetGrid at 0x24c13042d90>
../_images/3_Wizualizacja danych_111_1.png

Gęstość rozkładu

Histogram ma na celu przybliżenie podstawowej funkcji gęstości prawdopodobieństwa, która wygenerowała dane, poprzez grupowanie i liczenie obserwacji. Szacowanie gęstości jądra (KDE) przedstawia inne rozwiązanie tego samego problemu. Zamiast używać oddzielnych przedziałów, wykres KDE wygładza obserwacje za pomocą jądra Gaussa, tworząc ciągłe oszacowanie gęstości rozkładu.

sns.displot(fmri, x='signal', kind='kde')
<seaborn.axisgrid.FacetGrid at 0x24c129fb0a0>
../_images/3_Wizualizacja danych_114_1.png

Podobnie jak w przypadku histogramu, wykres kde posiada parametr bw_adjust (parametr wygładzenia), który spełnia podobną rolę co binwidth lub bins dla histogramu. Ustawnienie tego parametru na zbyt niskim poziomie spowoduje nadmierne dopasowanie dodanych, w odwrotnym przypadku nadrmierne wygładzenie spowoduję wymazanie znaczących cech rozkładu.

sns.displot(fmri, x='signal', kind='kde', bw_adjust=0.1)
<seaborn.axisgrid.FacetGrid at 0x24c112d4370>
../_images/3_Wizualizacja danych_116_1.png
sns.displot(fmri, x='signal', kind='kde', bw_adjust=2)
<seaborn.axisgrid.FacetGrid at 0x24c1304a040>
../_images/3_Wizualizacja danych_117_1.png

Rozkład gęstości ma podobne funkcjonalności jak wyżej przedstawiony histogram. Oto kilka z nich:

sns.displot(fmri, x="signal", hue="region", kind="kde")
<seaborn.axisgrid.FacetGrid at 0x24c13257b50>
../_images/3_Wizualizacja danych_119_1.png
sns.displot(fmri, x="signal", hue="region", multiple='stack', kind="kde")
<seaborn.axisgrid.FacetGrid at 0x24c1311bc70>
../_images/3_Wizualizacja danych_120_1.png
sns.displot(fmri, x="signal", hue="region", fill=True, kind="kde")
<seaborn.axisgrid.FacetGrid at 0x24c130cce20>
../_images/3_Wizualizacja danych_121_1.png

Bardzo częstym podejściem podczas wizualizacji danych jest skorzystanie z obu typów wykresu jednocześnie.

sns.displot(fmri, x="signal", kde=True)
<seaborn.axisgrid.FacetGrid at 0x24c162b67f0>
../_images/3_Wizualizacja danych_123_1.png

Dystrybuanta rozkładu

Trzecia opcja wizualizacji rozkładów oblicza empiryczną dystrybuantę rozkładu (ECDF).

sns.displot(fmri, x="signal", kind='ecdf')
<seaborn.axisgrid.FacetGrid at 0x24c132433d0>
../_images/3_Wizualizacja danych_126_1.png

Wizualizacja dystrybuanty rozkładu ma dwie istotne zalety. Po pierwsze bezpośrednio reprezentuje każdy punkt danych. Oznacza to, że nie trzeba brać pod uwagę rozmiaru przedziału ani parametru wygładzania. Dodatkowo, ponieważ krzywa rośnie monotonicznie, dobrze nadaje się do porównywania wielu rozkładów.

sns.displot(fmri, x="signal", hue="region", kind="ecdf")
<seaborn.axisgrid.FacetGrid at 0x24c1638d5e0>
../_images/3_Wizualizacja danych_128_1.png

Główną wadą wykresu dystrybuanty jest to, że mniej intuicyjnie przedstawia kształt rozkładu niż histogram lub krzywa gęstości.

Rozkład wielowymiarowy

Wszystkie dotychczasowe przykłady uwzględniały rozkłady jednowymiarowe: rozkłady jednej zmiennej, z uwzględnieniem zależności od drugiej zmiennej przypisanej do barwy. Teraz zajemiemy się badaniem rozkładów dwuwymiarowych.

sns.displot(housing, x="MedHouseVal", y="MedInc")
<seaborn.axisgrid.FacetGrid at 0x24c16546ac0>
../_images/3_Wizualizacja danych_132_1.png
sns.displot(housing, x="MedHouseVal", y="MedInc", kind='kde')
<seaborn.axisgrid.FacetGrid at 0x24c112969a0>
../_images/3_Wizualizacja danych_133_1.png
sns.displot(housing, x="MedInc", y="MedHouseVal", hue="AveRooms_greater_5")
<seaborn.axisgrid.FacetGrid at 0x24c1654b2b0>
../_images/3_Wizualizacja danych_134_1.png
sns.displot(housing, x="MedInc", y="MedHouseVal", hue="AveRooms_greater_5", kind='kde')
<seaborn.axisgrid.FacetGrid at 0x24c1656e3a0>
../_images/3_Wizualizacja danych_135_1.png

Jak widać każdy z powyższych wykresów posiada podobne lub wręcz takie same parametry jak w przypadku wykresów jednowymiarowych. Zachęcam do zgłębienia innych dodatkowych opcji pod kątem analizy rozkładu jedno i dwuwymiarowego.

Ostatnim typem wykresu, który warto mieć w zanadrzu podczas analizy rozkładu jest jointplot.

sns.jointplot(data=housing, x="MedInc", y="MedHouseVal")
<seaborn.axisgrid.JointGrid at 0x24c165cc280>
../_images/3_Wizualizacja danych_137_1.png
sns.jointplot(data=housing, x="MedInc", y="MedHouseVal", hue="AveRooms_greater_5", kind='kde')
<seaborn.axisgrid.JointGrid at 0x24c165b1850>
../_images/3_Wizualizacja danych_138_1.png

Zaletą wykresy typu joinplot jest dodatkowe wyświetlanie jednowymiarowego rozkładu zmiennej w postaci histogramu lub funkcji gęstości. Dzięki temu możemu jeszcze lepiej zrozumieć relacje zachodzące w naszym zbiorze danych.

Wizualizacja modeli regresyjnych

Zazwyczaj zbiory danych zawierają wiele zmiennych numerycznych, a celem analizy jest często powiązanie tych zmiennych ze sobą. Wcześniej omówione zostały funkcje, które mogą to pokazują łączny rozkład dwóch zmiennych lub pewne relacje określone wykresem punktowym. Bardzo pomocne może być jednak wykorzystanie modeli statystycznych do oszacowania prostej zależności między dwoma zaszumionymi zestawami obserwacji.

Info

Pakiet seaborn nie jest pakietem do analizy statystycznej. Aby uzyskać miary ilościowe związane z dokładnym dopasowaniem modeli regresji, należy użyć modeli statystycznych zawartych w bibliotekach sklearn czy statsmodels.

Wizualizacja regresji liniowej

W seaborn wykorzystywane są dwie główne funkcje do wizualizacji zależności liniowej określonej za pomocą regresji. Te funkcje to: regplot() i lmplot(). Obie funkcje są ze sobą ściśle powiązane i dzielą większość swoich podstawowych parametrów.

W najprostszym użyciu obie funkcje rysują wykres rozrzutu dwóch zmiennych, x i y, a następnie dopasowują model regresji y ~ x. Następnie wykreślają wynikową linię regresji oraz 95% przedział ufności.

sns.regplot(x="total_bill", y="tip", data=tips);
../_images/3_Wizualizacja danych_145_0.png
sns.lmplot(x="total_bill", y="tip", data=tips);
../_images/3_Wizualizacja danych_146_0.png

W poniższym notatniku w głównej mierze skupimy się na funkcji lmplot.

Możliwe jest dopasowanie regresji liniowej, gdy jedna ze zmiennych przyjmuje wartości dyskretne, jednak prosty wykres rozrzutu utworzony przez ten rodzaj danych często jest nieoptymalny.

sns.lmplot(x="size", y="tip", data=tips);
../_images/3_Wizualizacja danych_149_0.png

Jedną z opcji rozwiązania tego problemu jest dodanie losowego szumu jitter do wartości dyskretnych, aby rozkład tych wartości był bardziej przejrzysty.

sns.lmplot(x="size", y="tip", data=tips, x_jitter=.05);
../_images/3_Wizualizacja danych_151_0.png

Drugą opcją jest “zwinięcie” obserwacji w każdym dyskretnym przedziale, aby wykreślić oszacowanie tendencji centralnej (w tym przypadku średniej) wraz z przedziałem ufności.

sns.lmplot(x="size", y="tip", data=tips, x_estimator=np.mean);
../_images/3_Wizualizacja danych_153_0.png

Wizualizacja z wykorzystaniem innych modeli

Wizualizacja wielu relacji jednocześnie

sns.jointplot(x="total_bill", y="tip", data=tips, kind="reg");
../_images/3_Wizualizacja danych_156_0.png
sns.pairplot(tips, x_vars=["total_bill", "size"], y_vars=["tip"],
             height=5, aspect=.8, kind="reg");
../_images/3_Wizualizacja danych_157_0.png

Wizualizacja danych kategorycznych

Powyższe metody wizulizacji skupiały się najczęściej na danych liczbowych, gdzie zmienna kategoryczna była nam potrzebna jedynie do grupowania czy agregowania naszych danych. W tej części skupimy się na metodach bezpośrednij wizualizacji zmiennej kategorycznej.

Zmienna kategoryczna w postaci wykresu punktowego

Podstawową formą wizualizacji dla zmiennej katgorycznej jest wykres rozrzutu przedstawiający daną zmienną liczbową w zależności od zmiennej kategorycznej. Parametr kind == strip oznacza, że do zmiennej y dodawany jest pewinen element losowy w celu “rozrzucenia” obserwacji na osi y. Tak przedstawiony wykres może nam odpowiedzieć na pytania dotyczące rozkładu naszej zmiennej y wzlędem zmiennej x oraz może pomóc nam dostrzeć pewne anomalie w naszych danych.

sns.catplot(x="day", y="total_bill", kind="strip", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c176242b0>
../_images/3_Wizualizacja danych_162_1.png

Tak wykres wyglądałby w przypadku pominięcia dodawania składnika losowego do każdej obserwacji z osobna.

sns.catplot(x="day", y="total_bill", jitter=False, data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c18a85be0>
../_images/3_Wizualizacja danych_164_1.png

Czasami dobrym pomysłem jest, także dodanie do wykresu kolejnego wymiaru (w tym przypadku płci). Może nam to pomoć zrozumieć kolejne relacje zachodzące w naszych danych.

sns.catplot(x="day", y="total_bill", hue="sex", kind="strip", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c1771b760>
../_images/3_Wizualizacja danych_166_1.png

Rozkład zmiennej w zależności od danych kategorycznych

W poprzednich wizualizacjach pokazaliśmy w jaki sposób można zwizulizować histogram, a wieć wykres rozkładu naszej zmiennej w zależności od danych kategorycznych. Istnieją również innego rodzju wykresy, które będące odpowiedzią na rozkład naszej zmiennej - wykresy pudełkowe.

Podstawowy wykres pudełkowy można uzyskać korzystając z matplotlib.

plt.boxplot(x=housing['MedHouseVal'])
{'whiskers': [<matplotlib.lines.Line2D at 0x24c18d785e0>,
  <matplotlib.lines.Line2D at 0x24c18d78940>],
 'caps': [<matplotlib.lines.Line2D at 0x24c18d78ca0>,
  <matplotlib.lines.Line2D at 0x24c18d82040>],
 'boxes': [<matplotlib.lines.Line2D at 0x24c18d78280>],
 'medians': [<matplotlib.lines.Line2D at 0x24c18d823a0>],
 'fliers': [<matplotlib.lines.Line2D at 0x24c18d82700>],
 'means': []}
../_images/3_Wizualizacja danych_170_1.png

Jednakże, tym samym małym wysiłkiem można uzyskać dużo ładniejszy wykres korzystając z API seaborn.

sns.catplot(y='MedHouseVal', data=housing, kind="box")
<seaborn.axisgrid.FacetGrid at 0x24c18c32dc0>
../_images/3_Wizualizacja danych_172_1.png
sns.catplot(x="day", y="total_bill", kind="box", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c18dd3790>
../_images/3_Wizualizacja danych_173_1.png
sns.catplot(x="day", y="total_bill", hue="smoker", kind="box", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c196a57c0>
../_images/3_Wizualizacja danych_174_1.png

Interpretacja wykresu pudełkowego jest dość prosta. Są w nim zawarte informacje na temat wartości minimalnej, kwartylu I, medianie, kwartylu III, wartości maksymalnej oraz potencjalnym anomalią wyliczonym przy pomocy rozstępu między kwartylowego. Jednakże wykres pudełkowy można zastąpić innymi podobnymi wizualizacjami, którą czasami mogą okazać się bardzo pomocne.

sns.catplot(x="AveRooms_greater_5", y="MedHouseVal", kind="box",
            data=housing.sort_values("AveRooms_greater_5"))
<seaborn.axisgrid.FacetGrid at 0x24c19250eb0>
../_images/3_Wizualizacja danych_176_1.png

Wykresem podobnym do wykresu pudełkowego - boxplot, ale zoptymalizowany pod kątem wyświetlania większej ilości informacji o kształcie rozkładu jest inny wykres pudełowy - boxenplot.

sns.catplot(x="AveRooms_greater_5", y="MedHouseVal", kind="boxen",
            data=housing.sort_values("AveRooms_greater_5"))
<seaborn.axisgrid.FacetGrid at 0x24c18b22520>
../_images/3_Wizualizacja danych_178_1.png

Ciekawą oraz często wykorzystywana interpretacją rozkładu jest violinplot* łączący wykres pudełkowy z estymacją gęstości rozkładu. Jest on w stanie jeszcze dokładniej pomóc nam oszacować miejsca w których występują większe skupiska naszych obserwacji.

sns.catplot(x="AveRooms_greater_5", y="MedHouseVal", kind="violin",
            data=housing.sort_values("AveRooms_greater_5"))
<seaborn.axisgrid.FacetGrid at 0x24c18d829d0>
../_images/3_Wizualizacja danych_180_1.png

Dobrym połączeniem jest violinplot z stripplot, czyli przedstawionym wcześniej wykresem rozrzutu. Jednakże w przypadku dużej ilości danych wizualizacja może się długo ładować.

g = sns.catplot(x="day", y="total_bill", kind="violin", inner=None, data=tips)
sns.stripplot(x="day", y="total_bill", color="k", size=3, data=tips, ax=g.ax)
<AxesSubplot:xlabel='day', ylabel='total_bill'>
../_images/3_Wizualizacja danych_182_1.png

Podczas pracy z danymi są sytuację gdy chcemy aby nasz wykres był bardziej interaktywny. Wtedy w przypadku wykresu pudełkowego można skorzystać z API plotly. Podstawowa forma takiego wykresu wygląda następująco:

fig = px.box(data_frame=tips, x='day', y='total_bill', title='Wykres pudełkowy')
fig.show()

Estymacja statystyczna w ramach zmiennej kategorycznej

W pewnych przypadkach naszym celem może być oszacowanie tendencji centralnej dla naszych danych np. średniej czy mediany. Wtedy bardzo często korzysta się z wykresu słupkowego wraz z pewnym pozniomem ufności. W przypadku biblioteki seaborn narysowanie tego typu wykresu jest bardzo proste. Domyślnie tendecją centralną jest średnia arytmetyczna.

sns.catplot(x="sex", y="tip", hue="time", kind="bar", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c192012e0>
../_images/3_Wizualizacja danych_187_1.png

Szczególnym przypadkiem wykresu catplot jest wykres przedstawiający liczbę obserwacji przypadającą na daną kategorię.

sns.catplot(x="sex", kind="count", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c192193a0>
../_images/3_Wizualizacja danych_189_1.png
sns.catplot(y="sex", hue="time", palette="pastel",  kind="count", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c19208550>
../_images/3_Wizualizacja danych_190_1.png

Inna formą wizualizacji może być, także pointplot, który te same relacje przedstawia w postaci wykresu punktowego, gdzie dany punkt przestawia tendencję centralną danej kategorii natomiast pionowe linię określają przedział ufności. Atutem tego wykresu jest łączenie tych samych kategorii podanych w parametrze hue.

sns.catplot(x="sex", y="tip", hue="time", kind="point", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c1947cd60>
../_images/3_Wizualizacja danych_192_1.png
sns.catplot(x="size", y="tip", hue='sex', kind="point", data=tips)
<seaborn.axisgrid.FacetGrid at 0x24c1955e700>
../_images/3_Wizualizacja danych_193_1.png

Podsumowanie

Wnioski:

Wykresy oparte o pandas lub matplotlib sprawdzają się w przypadku chęci zbudowania prostej wizualizacji podczas pierwszej fazy Exploratory Data Analysis. Wykresy z rodziny seaborn oraz plotly dają większe możliwości, dzięki czemu jesteśmy w stanie w ładniejszy sposób przedstawić nasze dane i wyciągnąć na ich podstawie istotne wnioski.