Z uczeniem bez nadzoru mamy do czynienia wtedy, gdy oryginalne dane nie posiadają przypisania do konkretnych klas tak, jak jest to np. w przypadku drzew decyzyjnych. Nie następuje tu więc proces uczenia się klasyfikatora na podstawie konkretnych przypadków.
Jednym z szerokich tematów w uczeniu bez nadzoru jest grupowanie, również nazywane analizą skupień czy klasteryzacja (od ang. cluster analysis). Podstawową ideą tego podejścia jest zbieranie (grupowanie) elementów tak, aby utworzyć (dość) jednorodne klasy. W ramach dzisiejszych zajęć zajmiemy się dwiema metodami: k-średnich oraz hierarchiczną.
Metoda k-średnich (ang. k-means), została szerzej omówiona na Wykładzie 8. Jej założenia są następujące:
W praktyce algorytm metody k-średnich inicjalizuje środki \(K\) skupień (gdzie \(K\) jest narzucone z góry) i w kolejnych iteracjach przypisuje najbliższe punkty do tych skupień oraz wyznacza nowe skupienia itd. aż w układzie nie będzie zachodzić żadna zmiana.
Do wizualizacji metody k-średnich w pakiecie użyjemy funkcji KMeans() z modułu sklearn.cluster
Zaczynamy od danych:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
def draw_data(n1, n2, m1, m2, S1, S2):
data1 = np.random.multivariate_normal(m1, S1, n1)
data2 = np.random.multivariate_normal(m2, S2, n2)
data = np.concatenate((data1, data2), axis = 0)
data = pd.DataFrame(data, columns=["x", "y"])
data["klasa"] = np.repeat([0, 1], [n1, n2])
return data
m1, m2 = [-1, -1], [1, 1]
S1 = [[2, 0], [0, 2]]
n1, n2 = 10, 10
np.random.seed(1290)
data = draw_data(n1, n2, m1, m2, S1, S1)
Następnie uruchamiamy funkcję do lczenia skupień - narzucamy z góry
dwa klastry. Pola, które będą nas najbardziej intersować to
inertia_ przechowujące wartość \(W\) oraz labels_, gdziej
lądują etykiety znalezionych klastrów:
from sklearn.cluster import KMeans
km = KMeans(n_clusters = 2).fit(data[["x", "y"]])
print("W =", km.inertia_)
## W = 88.53387888180296
print("etykiety:", km.labels_)
## etykiety: [1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 0]
Mające te wyniki, możemy teraz stworzyć wykresy porównujące oryginalne dane, z nanisionymi na nie rezultatami analizy klastrów:
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 5))
axs[0].scatter(data.x, data.y, c = np.array(["orange", "blue"])[data.klasa], s = 200)
axs[0].set_title("Dane oryginalne")
axs[0].set_xlabel("X")
axs[0].set_ylabel("Y")
axs[0].tick_params(direction='in')
for i in range(len(data.index)):
axs[0].text(data.x[i], data.y[i], i, fontsize=8, fontweight="bold", color = "white", horizontalalignment='center', verticalalignment='center')
axs[1].scatter(data.x, data.y, c = np.array(["orange", "blue"])[data.klasa], s = 200, zorder = 2)
axs[1].scatter(data.x, data.y, c = "#ffffffff", edgecolor = np.array(["orange", "blue"])[km.labels_], s = 400)
for i in range(len(data.index)):
axs[1].text(data.x[i], data.y[i], i, fontsize=8, fontweight="bold", color = "white", zorder = 2,
horizontalalignment='center', verticalalignment='center')
axs[1].set_title("Dane oryginalne + klastry")
axs[1].set_xlabel("X")
axs[1].set_ylabel("Y")
axs[1].tick_params(direction='in')
plt.show()
Oczywiście, liczba \(K=2\) została
przez nas z góry narzucona. Równie dobrze można zażądać wyszukiwania 3
skupień. Przy okazji możemy też zwizualizować fakt, że algorytm nie
zawsze prowadzi do tych samych wyników: dotąd używaliśmy domyślnej
funkcji KMeans() - działa ona w ten sposób, że startuje z
10 różnych warunków początkowych i wybiera najlepsze rozłożenie
klastrów. Jeśli wybierzemy opcję n_init = 1, algortym
uruchomi się raz. Efektem będą różne rozkłady klastrów. Aby wybrać
optymalny, powinniśmy się kierować wartością \(W\) (pokazaną w lewym górnym rogu każdego
panelu) - im większa, tym lepiej:
fig, axs = plt.subplots(nrows=3, ncols=3, figsize=(7, 7))
np.random.seed(129)
for i, ax in enumerate(axs.reshape(9)):
km = KMeans(n_clusters = 3, n_init = 1).fit(data[["x", "y"]])
ax.scatter(data.x, data.y, c = np.array(["orange", "blue", "green"])[km.labels_], alpha = 0.4, s = 20)
ax.text(.05, .85, np.round(km.inertia_,2), fontweight="bold",transform=ax.transAxes)
ax.tick_params(direction='in')
Patrząc na wyniki z poprzedniej części powstaje dość oczywiste pytanie: jaka wartość \(K=K^∗\) jest optymalna dla danego zbioru? Do wyznaczenia \(K^∗\) wykorzystamy dwie techniki: tzw. statystykę odstępu (ang. gap statistics) oraz indeks silhouette (co można przetłumaczyć jako “sylwetkę”). Poniżej dane, z których będziemy korzystać - bardzo podobne do tych, z których korzystaliśmy poprzednio, ale rozłożone na 3 klasy:
def draw_data(n1, n2, n3, m1, m2, m3, S1, S2, S3):
data1 = np.random.multivariate_normal(m1, S1, n1)
data2 = np.random.multivariate_normal(m2, S2, n2)
data3 = np.random.multivariate_normal(m3, S3, n3)
data = np.concatenate((data1, data2, data3), axis = 0)
data = pd.DataFrame(data, columns=["x", "y"])
data["klasa"] = n1 * [0] + n2 * [1] + n3 * [2]
return data
n = 60
S = [[1, 0], [0, 1]]
m1, m2, m3 = [4, 4], [1, 1], [4, -1]
data = draw_data(n, n, n, m1, m2, m3, S, S, S)
plt.scatter(data.x, data.y, c = np.array(["orange", "blue", "green"])[data.klasa], s = 100, alpha = 0.4)
plt.title("Dane oryginalne")
plt.xlabel("X")
plt.ylabel("Y")
plt.tick_params(direction='in')
plt.show()
Punktem odniesienia w metodzie statystyki odstępu są dane losowe: w pudełku o wymiarach wyznaczonych przez skrajne współrzędne losujemy taką samą liczbę punktów co w danych oryginalnych lecz korzystamy z rozkładu jednorodnego.
data_ranges = data[["x", "y"]].agg(["min", "max"])
x = np.random.uniform(data_ranges["x"]["min"], data_ranges["x"]["max"], n)
y = np.random.uniform(data_ranges["y"]["min"], data_ranges["y"]["max"], n)
data_rnd = pd.DataFrame({"x": x, "y": y})
plt.scatter(data_rnd.x, data_rnd.y, s = 100, alpha = 0.4, c = "black")
plt.title("Dane przetasowane")
plt.xlabel("X")
plt.ylabel("Y")
plt.tick_params(direction='in')
plt.show()
Przechodzimy do kluczowego etapu procedury. Jak widać poniżej sama wartość \(W\) jest niewystarczająca, gdyż po prostu spada wraz ze wzrostem \(K\). W efekcie z dla każdej wartości \(K\) liczmy \(W\) z danych oryginalnych, a następnie tworzymy robimy to samo dla danych losowych (przy czym powtarzamy to \(N\) razy - tutaj 10) wyznaczając \(\langle W_{rnd} \rangle = \sum_{i=1}^N W^i_{rnd}\).
def get_random_clusters(data_ranges, n, nk):
x = np.random.uniform(data_ranges["x"]["min"], data_ranges["x"]["max"], n)
y = np.random.uniform(data_ranges["y"]["min"], data_ranges["y"]["max"], n)
data = pd.DataFrame({"x": x, "y": y})
km = KMeans(n_clusters = nk, n_init = 1).fit(data_rnd[["x", "y"]])
return km.inertia_
k_clust = range(1, 20)
res_rnd = np.array([[get_random_clusters(data_ranges, 3*n, k) for i in range(10)] for k in k_clust])
def get_real_clusters(data, k):
km = KMeans(n_clusters = k, n_init = 10).fit(data[["x", "y"]])
return km.inertia_
res_data = [get_real_clusters(data, k) for k in k_clust]
Staystyka odstępu dla danego \(K\) jest zdefiniowana jako \(\ln \langle W_{rnd} \rangle - \ln W\), a optymalną wartość \(K = K^*\) bierze się z maksimum wykresu.
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 5))
axs[0].plot(k_clust, res_data, "o--", label = "dane rzeczywiste")
axs[0].plot(k_clust, res_rnd.mean(axis = 1), "o--", label = "dane przestasowane")
axs[0].set_ylabel(r"$k$");
axs[0].set_ylabel(r"$W$");
axs[0].tick_params(direction='in')
axs[0].legend();
axs[1].plot(k_clust, np.log(res_rnd.mean(axis = 1))-np.log(res_data), "o--", color = "black");
axs[1].set_ylabel(r"$k$");
axs[1].set_ylabel(r"$\ln \langle W_{rnd} \rangle - \ln W$");
axs[1].tick_params(direction='in')
plt.show()
W tym przypadku podejście jest trochę inne. Jeśli założymy, że dla obserwacji \(i\) należącej do pewnego klastra \(a(i)\) to średnia odległość pomiędzy tym punktem, a wszystkimi innymi w tym klastrze, natomiast \(b(i)\) to najmniejsza średnia odegłość do punktów z innego klastra, to wtedy
\(s(i) = \frac{b(i)-a(i)}{\max\{a(i), b(i)\}}\)
nazywamy indeksem silhouette dla danego punktu. Łatwo sprawdzić, że \(-1 \le s(i) \le 1\), przy czym im wyższa wartość \(s(i)\), tym lepiej dopasowany jest punkt do danego kalstra. Wartość zbliżona do zera oznacza, że punkt leży na granicy klastrów. Oczywiście, optymalną sytuacja występuje wtedy, gdy wszystkie punkty mają dość zbliżoną do siebie wartość \(s(i)\). W praktyce, rozkład \(s(i)\) w poszczególnych klastrach jest różny i im bardziej odbiega on od średniej wartości polieczonej po wszystkich punktach, tym gorzej. Jakoś klastrów często ocenia się graficznie, pokazując rozkład \(s(i)\) dla każdego z nich i zaznaczając średnią (poniższy kod jest zaczerpnięty z dokumentacji sklearn)
from sklearn.metrics import silhouette_samples, silhouette_score
import matplotlib.cm as cm
range_n_clusters = [2,3,4,5,6]
fig, ax = plt.subplots(5, 2, figsize = (10, 15))
for j, n_clusters in enumerate(range_n_clusters):
ax[j][0].set_xlim([-0.1, 1])
ax[j][0].set_ylim([0, len(data.x) + (n_clusters + 1) * 10])
km = KMeans(n_clusters=n_clusters)
cluster_labels = km.fit_predict(data[["x","y"]])
silhouette_avg = silhouette_score(data[["x","y"]], cluster_labels)
sample_silhouette_values = silhouette_samples(data[["x","y"]], cluster_labels)
y_lower = 10
for i in range(n_clusters):
ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / n_clusters)
ax[j][0].fill_betweenx(
np.arange(y_lower, y_upper),
0,
ith_cluster_silhouette_values,
facecolor=color,
edgecolor=color,
alpha=0.7,
)
ax[j][0].text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10 # 10 for the 0 samples
ax[j][0].axvline(x=silhouette_avg, color="red", linestyle="--")
colors = cm.nipy_spectral(cluster_labels.astype(float) / n_clusters)
ax[j][1].scatter(
data.x, data.y, marker=".", s=100, lw=0, alpha=0.7, c=colors, edgecolor="k"
)
plt.show()
Aby przekonać się, jak wygląda taki wykres w bardziej skrajnym przypadku ustawmy dziesięciokrotnie mniejsze wariancje
Innym posobem wykonania analizy skupień jest wykorzystanie metod hierarchicznych. Ich ogólny opis można zawrzeć w następujących punktach:
Skorzystamy tu z danych z początku zajęć
Następnie za pomocą funkcji linkage() z modułu
sklearn.hierarchy wyznaczymy kolejne etapy łączenia się
punktów w klastry (użyjemy complete linkage, czyli
bedziemy liczyć największe odległości pomiędzy klastrami)
## [[ 3. 8. 0.64889171 2. ]
## [14. 15. 0.71021708 2. ]
## [13. 16. 0.71622392 2. ]
## [ 1. 2. 0.72564048 2. ]
## [12. 17. 0.85687496 2. ]
## [ 6. 20. 1.08526634 3. ]
## [ 4. 23. 1.2911712 3. ]
## [ 9. 25. 1.54658408 4. ]
## [10. 24. 1.57166301 3. ]
## [ 0. 26. 1.90959258 4. ]
## [ 5. 21. 1.93908331 3. ]
## [ 7. 18. 2.10692076 2. ]
## [22. 28. 2.86988765 5. ]
## [27. 29. 3.05733394 8. ]
## [19. 31. 3.64495928 3. ]
## [30. 32. 4.70726332 8. ]
## [11. 35. 6.04940642 9. ]
## [33. 34. 6.36524025 11. ]
## [36. 37. 9.32597974 20. ]]
Macierz połaczeń podaje w kolejnych kolumnach obserwacje lub klastry (wartości powyżej liczby obserwacji), odległość graniczną, dla której zostało wykonane połączenie, oraz liczbę obserwacji w skupieniu. Innymi słowy 5 pierwszy linijek to połaczenia pomiędzy dwójkami obserwacji, natomiast w szóstym kroku punkt 6 łączy się z klastrem powstałym w kroku 1 etc. Oczywiście wygodniej jest wykonać dendrogram, podając przy okazji wartość progową odległości, która doprowadzi do tego, że klastry stworzone dla wartości poniżej progu otrzymają swoje własne kolory, a pozostałe punkty - jeden.
th = 4.2
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 5))
dn = hierarchy.dendrogram(Z, color_threshold = th, ax = axs[0])
axs[0].axhline(th, ls = "--", c = "black")
axs[1].scatter(data.x, data.y, alpha = 0)
for i in range(len(data.index)):
axs[1].text(data.x[i], data.y[i], i, fontsize=8, fontweight="bold", color = "white", horizontalalignment='center', verticalalignment='center')
axs[1].scatter(data.x[dn["leaves"]], data.y[dn["leaves"]], c = dn["leaves_color_list"], alpha = 0.9, s = 200)
Ostatnim punktem zajęć jest wykorzystanie metody hierarchicznej do danych z amerykańskich wyborów prezydenckich:
df = pd.read_csv("https://jsienkiewicz.pl/pliki/SDE/votes.csv", index_col=0)
print(df)
## X1856 X1860 X1864 X1868 ... X1964 X1968 X1972 X1976
## Alabama NaN NaN NaN 51.44 ... 69.5 14.0 72.4 43.48
## Alaska NaN NaN NaN NaN ... 34.1 45.3 58.1 62.91
## Arizona NaN NaN NaN NaN ... 50.4 54.8 64.7 58.62
## Arkansas NaN NaN NaN 53.73 ... 43.9 30.8 68.9 34.97
## California 18.77 32.96 58.63 50.24 ... 40.9 47.8 55.0 50.89
## Colorado NaN NaN NaN NaN ... 38.7 50.5 62.6 55.89
## Connecticut 53.18 53.86 51.38 51.54 ... 32.2 44.3 58.6 52.64
## Delaware 2.11 23.71 48.20 40.98 ... 39.1 45.1 59.6 47.27
## Florida NaN NaN NaN NaN ... 48.9 40.5 71.9 46.83
## Georgia NaN NaN NaN 35.72 ... 54.1 30.4 75.0 33.02
## Hawaii NaN NaN NaN NaN ... 21.2 38.7 62.5 48.72
## Idaho NaN NaN NaN NaN ... 49.1 56.8 64.2 61.77
## Illinois 40.25 50.68 54.41 55.69 ... 40.5 47.1 59.0 51.11
## Indiana 40.03 51.09 53.60 51.39 ... 44.0 50.3 66.1 53.77
## Iowa 49.13 54.87 64.23 61.92 ... 38.1 53.0 57.6 50.51
## Kansas NaN NaN 78.61 68.89 ... 45.9 54.8 67.7 53.91
## Kentucky 0.26 0.93 30.17 25.45 ... 36.0 43.8 63.4 46.24
## Louisiana NaN NaN NaN 29.31 ... 56.8 23.5 65.3 47.00
## Maine 61.37 64.15 60.22 62.42 ... 31.2 43.1 61.5 50.34
## Maryland 0.32 3.11 55.10 32.80 ... 34.5 41.9 61.3 46.87
## Massachusetts 64.72 62.75 72.22 69.67 ... 23.8 32.9 45.2 41.93
## Michigan 56.98 57.18 55.89 56.98 ... 33.3 41.5 56.2 52.68
## Minnesota NaN 63.42 59.06 60.80 ... 36.2 41.5 51.6 44.30
## Mississippi NaN NaN NaN NaN ... 87.1 13.5 78.2 49.21
## Missouri NaN 10.29 70.17 58.90 ... 36.0 44.9 62.2 48.22
## Montana NaN NaN NaN NaN ... 41.1 50.6 57.9 53.65
## Nebraska NaN NaN NaN 64.14 ... 47.4 59.8 70.5 60.31
## Nevada NaN NaN 59.84 55.39 ... 41.4 47.5 63.7 52.27
## New Hampshire 53.59 56.89 52.56 55.02 ... 36.1 52.1 64.0 55.68
## New Jersey 28.52 48.13 47.16 49.12 ... 34.4 46.1 61.6 50.99
## New Mexico NaN NaN NaN NaN ... 41.0 51.8 61.0 51.04
## New York 46.14 53.71 50.46 49.41 ... 31.4 44.3 58.5 47.84
## North Carolina NaN NaN NaN 53.37 ... 43.8 39.5 69.5 44.43
## North Dakota NaN NaN NaN NaN ... 42.0 55.9 62.1 52.93
## Ohio 48.49 52.33 56.33 53.97 ... 37.1 45.2 59.6 49.90
## Oklahoma NaN NaN NaN NaN ... 44.3 47.7 73.7 50.52
## Oregon NaN 34.48 53.90 49.63 ... 36.3 49.8 52.4 50.01
## Pennsylvania 33.95 56.25 51.75 52.20 ... 34.5 44.0 59.1 48.57
## Rhode Island 57.85 61.22 62.20 66.49 ... 19.1 31.8 53.0 44.24
## South Carolina NaN NaN NaN 57.93 ... 58.9 38.1 70.8 43.54
## South Dakota NaN NaN NaN NaN ... 44.4 53.3 54.2 50.92
## Tennessee NaN NaN NaN 68.33 ... 44.5 37.8 67.7 43.21
## Texas NaN NaN NaN NaN ... 36.7 39.9 66.2 48.01
## Utah NaN NaN NaN NaN ... 45.3 56.5 67.6 64.94
## Vermont 78.23 75.79 76.10 78.57 ... 33.7 52.8 62.7 56.01
## Virginia 0.19 1.15 NaN NaN ... 46.5 41.4 67.8 50.73
## Washington NaN NaN NaN NaN ... 38.0 45.1 56.9 51.37
## West Virginia NaN NaN 68.95 58.84 ... 32.1 40.8 63.6 41.95
## Wisconsin 55.29 56.58 55.88 56.24 ... 37.9 47.9 53.4 49.16
## Wyoming NaN NaN NaN NaN ... 43.4 55.8 69.0 59.85
##
## [50 rows x 31 columns]
Dane oddają procentowy udział głosów oddanych na kandydata republikańskiego w poszczególnych stanach, począwszy od roku 1856 do 1976 roku. Niestety, ze względu na to, że poszczególne stany dołączały się sukcesywnie, co powoduje braki (NaN), musimy trochę “okroić” dane: ograniczmy się tylko do XX w. (OK, formalnie rok 1900 to jeszcze XIX w.) oraz pozbędziemy się tych stanów, które nie były aktywne przez cały czas (np. Hawaje)
dff = df.iloc[:,11:]
dff = dff.drop(dff[dff.apply(lambda x: x.isnull().values.any(), axis = 1)].index)
print(dff)
## X1900 X1904 X1908 X1912 ... X1964 X1968 X1972 X1976
## Alabama 34.67 20.65 24.38 8.26 ... 69.5 14.0 72.4 43.48
## Arkansas 35.04 40.25 37.31 19.73 ... 43.9 30.8 68.9 34.97
## California 54.48 61.90 55.46 0.58 ... 40.9 47.8 55.0 50.89
## Colorado 42.04 55.27 46.88 21.88 ... 38.7 50.5 62.6 55.89
## Connecticut 56.94 58.13 59.43 35.88 ... 32.2 44.3 58.6 52.64
## Delaware 53.65 54.04 52.09 32.85 ... 39.1 45.1 59.6 47.27
## Florida 19.03 21.15 21.58 8.25 ... 48.9 40.5 71.9 46.83
## Georgia 28.56 18.32 31.40 4.27 ... 54.1 30.4 75.0 33.02
## Idaho 47.14 65.84 54.09 31.02 ... 49.1 56.8 64.2 61.77
## Illinois 52.83 58.77 54.52 22.13 ... 40.5 47.1 59.0 51.11
## Indiana 50.60 53.99 48.40 23.11 ... 44.0 50.3 66.1 53.77
## Iowa 57.99 63.37 55.62 24.30 ... 38.1 53.0 57.6 50.51
## Kansas 52.56 64.81 52.46 20.48 ... 45.9 54.8 67.7 53.91
## Kentucky 48.50 47.11 48.04 25.46 ... 36.0 43.8 63.4 46.24
## Louisiana 20.96 9.65 11.92 4.83 ... 56.8 23.5 65.3 47.00
## Maine 61.89 67.10 63.00 20.48 ... 31.2 43.1 61.5 50.34
## Maryland 51.50 48.83 48.85 23.69 ... 34.5 41.9 61.3 46.87
## Massachusetts 57.67 57.92 58.21 31.89 ... 23.8 32.9 45.2 41.93
## Michigan 58.10 69.50 61.93 27.63 ... 33.3 41.5 56.2 52.68
## Minnesota 60.21 73.95 59.11 19.25 ... 36.2 41.5 51.6 44.30
## Mississippi 9.73 5.46 6.56 2.47 ... 87.1 13.5 78.2 49.21
## Missouri 45.94 49.92 48.50 29.75 ... 36.0 44.9 62.2 48.22
## Montana 39.84 54.21 46.98 23.19 ... 41.1 50.6 57.9 53.65
## Nebraska 50.46 61.38 47.60 21.73 ... 47.4 59.8 70.5 60.31
## Nevada 37.75 56.66 43.93 15.89 ... 41.4 47.5 63.7 52.27
## New Hampshire 59.34 60.14 59.32 37.43 ... 36.1 52.1 64.0 55.68
## New Jersey 55.27 56.68 56.79 20.54 ... 34.4 46.1 61.6 50.99
## New York 53.10 53.13 53.11 28.68 ... 31.4 44.3 58.5 47.84
## North Carolina 45.47 39.70 45.55 11.94 ... 43.8 39.5 69.5 44.43
## North Dakota 62.11 74.83 60.87 26.67 ... 42.0 55.9 62.1 52.93
## Ohio 52.30 59.73 51.03 26.82 ... 37.1 45.2 59.6 49.90
## Oregon 55.25 67.06 56.39 25.30 ... 36.3 49.8 52.4 50.01
## Pennsylvania 60.74 67.99 58.84 22.40 ... 34.5 44.0 59.1 48.57
## Rhode Island 59.74 60.60 60.76 35.57 ... 19.1 31.8 53.0 44.24
## South Carolina 7.04 4.63 5.97 1.06 ... 58.9 38.1 70.8 43.54
## Tennessee 44.93 43.40 45.95 23.84 ... 44.5 37.8 67.7 43.21
## Texas 30.83 21.90 22.35 8.77 ... 36.7 39.9 66.2 48.01
## Utah 50.59 61.45 56.19 37.46 ... 45.3 56.5 67.6 64.94
## Vermont 75.79 77.98 75.12 37.13 ... 33.7 52.8 62.7 56.01
## Virginia 43.81 36.67 38.36 17.00 ... 46.5 41.4 67.8 50.73
## Washington 53.44 69.95 57.47 21.82 ... 38.0 45.1 56.9 51.37
## West Virginia 54.27 55.26 53.41 21.10 ... 32.1 40.8 63.6 41.95
## Wisconsin 60.04 63.24 54.52 32.68 ... 37.9 47.9 53.4 49.16
##
## [43 rows x 20 columns]
Na takim zbiorze uruchomimy funkcję clustermap() z
moduły seaborn. Wykonuje ona nie tylko mapę ciepła, ale
również kalstrowanie hierarchiczne - domyślnie zarówno po rzędach jak i
kolumnach, ale my ograniczmy się jedynie do rzędów, gdyż interesują nas
podobieństwa pomiędzy stanami.
sns.clustermap(dff, cmap="vlag", col_cluster=False, yticklabels=True);