Laboratorium Statystycznej Eksploracji Danych

LAB 7

Analiza skupień (klastrowanie)

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

Metoda k-średnich (ang. k-means), została szerzej omówiona na Wykładzie 8. Jej założenia są następujące:

  1. mamy \(n\)-elementowy zbiór obserwacji \(\mathbf{x}_i\) przestrzeni \(\mathbb{R}^p\),
  2. chcemy podzielić tę próbę na \(K\) skupień,
  3. rozpatrujemy sumę kwadratów odegłości \(d_{ij} = d(\mathbf{x}_i, \mathbf{x}_j)\) pomiędzy parami punktów \(T = \frac{1}{2}\sum\limits_{i=1}^n\sum\limits_{j=1}^nd_{ij}\),
  4. sumę \(T\) mozna rodzielić na \(T = W + B\), gdzie \(W = \frac{1}{2}\sum\limits_{k=1}^K\sum\limits_{C(i)=k}\sum\limits_{C(j)=k}d_{ij}\), a \(B = \frac{1}{2}\sum\limits_{k=1}^K\sum\limits_{C(i)=k}\sum\limits_{C(j) \neq k}d_{ij}\), \(W\) jest odległością wewnątrz skupień, a \(B\) - pomiędzy nimi,
  5. zadaniem jest minimalizacja \(W\) (lub maksymalizacja \(B\)).

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()

Wpływ warunków początkowych

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')

Optymalna liczba skupień

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()

Statystyka odstępu

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()

Indeks silhouette

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

Metody hierarchiczne

Innym posobem wykonania analizy skupień jest wykorzystanie metod hierarchicznych. Ich ogólny opis można zawrzeć w następujących punktach:

  • opierają sie na pomiarze uogólnionej odmienności między dwoma dowolnymi zbiorami obserwacji,
  • nie wymagają z góry określenia liczby skupień,
  • w pierwszym kroku metody algomeracyjnej tworzymy tyle skupień, ile jest obserwacji,
  • w nastepnym kroku w jedno skupienie łączona jest para najmniej odległych obserwacji,
  • z kroku na krok skupień jest coraz mniej, aż w ostatnim powstaje cała próba w jednym skupieniu,
  • w efekcie otrzymuje się nieskierowane drzewo (dendrogram).

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);