Визуализация для методов кластеризации

Visualization for clustering methods

Примечание редактора: Эви Фаулер является спикером на ODSC West. Обязательно посмотрите ее доклад “Преодоление разрыва интерпретируемости в сегментации клиентов”!

На осенней конференции Open Data Science я расскажу о том, как привнести системный подход к интерпретации моделей кластеризации. Чтобы подготовиться к этому, давайте поговорим о визуализации данных для моделей кластеризации.

Подготовка рабочего пространства

Все эти визуализации можно создать с помощью основных инструментов манипулирования данными (pandas и numpy) и основ визуализации (matplotlib и seaborn).

from matplotlib import colormaps, pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import load_diabetes
from sklearn.preprocessing import MinMaxScaler
import numpy as np
import pandas as pd
import seaborn as sns

Для этого руководства я буду использовать набор данных для прогнозирования диабета, встроенный в matplotlib. Я предоставлю гораздо больше информации о том, как обучить и оценить эффективную модель кластеризации на ODSC, но пока что я просто подгоню несколько простых моделей k-means.

# загрузка данных о диабете
diabetesData = load_diabetes(as_frame = True).data

# центрирование и масштабирование признаков кластеризации
diabetesScaler = MinMaxScaler().fit(diabetesData)
diabetesDataScaled = pd.DataFrame(diabetesScaler.transform(diabetesData)
                                  , columns = diabetesData.columns
                                  , index = diabetesData.index)

# создание трех небольших моделей кластеризации
km3 = KMeans(n_clusters = 3).fit(diabetesDataScaled)
km4 = KMeans(n_clusters = 4).fit(diabetesDataScaled)
km10 = KMeans(n_clusters = 10).fit(diabetesDataScaled)

Выбор цветовой схемы

Пакет matplotlib предоставляет несколько встроенных цветовых схем через свой реестр colormaps. Удобно выбирать одну цветовую схему для всей визуализации, и важно выбирать ее тщательно. Это может означать оценку всего, начиная от того, является ли карта последовательной (для случаев, когда данные можно интерпретировать по шкале от низкого до высокого) или разногласующейся (для случаев, когда данные наиболее значимы на одном из двух крайних значений), и заканчивая тем, является ли она тематически соответствующей теме (зеленые и коричневые для проекта топографии). Когда нет особой связи между данными и порядком их представления, хорошим выбором является цветовая схема nipy_spectral.

# выбор цветовой схемы nipy_spectral из matplotlib
nps = colormaps['nipy_spectral']

# просмотр всей цветовой схемы
nps

Каждая цветовая схема matplotlib состоит из серии кортежей, каждый из которых описывает цвет в формате RGBA (но с компонентами, масштабированными в диапазоне [0, 1], а не [0, 255]). Отдельные цвета из карты можно получить как с помощью целых чисел (от 0 до 255), так и с помощью десятичных чисел (от 0 до 1). Числа, близкие к 0, соответствуют цветам в нижней части цветовой карты, а целые числа, близкие к 255, и десятичные числа, близкие к 1.0, соответствуют цветам в верхней части цветовой карты. Интуитивно можно описать один и тот же цвет как целое число или десятичное число, представляющее это целое число в виде частного от деления на 255.

# просмотр выбранных цветов из цветовой схемы
print(nps(51))
#(0.0, 0.0, 0.8667, 1.0)

print(nps(0.2))
#(0.0, 0.0, 0.8667, 1.0)

Создание визуализаций

Диаграммы рассеяния

Классическая визуализация для модели кластеризации – это серия диаграмм рассеяния, сравнивающих каждую пару признаков, входящих в модель кластеризации, с помощью цветовой графики для обозначения принадлежности к кластеру. Есть встроенные методы для достижения этого, но собственный подход дает больший контроль над деталями, такими как цветовая схема.

def plotScatters(df, model):
    """ Создание диаграмм рассеяния на основе каждой пары столбцов в dataframe.
    Используйте цвет для обозначения метки модели.
    """

    # создание фигуры и осей
    plotRows = df.shape[1]
    plotCols = df.shape[1]
    fig, axes = plt.subplots(
        # создание одной строки и одного столбца для каждого признака в dataframe
        plotRows, plotCols
        # увеличение размера фигуры для удобного просмотра
        , figsize = ((plotCols * 3), (plotRows * 3))
    )   
    # перебор подграфиков для создания диаграмм рассеяния
    pltindex = 0
    for i in np.arange(0, plotRows):
        for j in np.arange(0, plotCols):
            pltindex += 1
            # определение текущего подграфика
            plt.subplot(plotRows, plotCols, pltindex)
            plt.scatter(
                # сравнение i-го и j-го признаков dataframe
                df.iloc[:, j], df.iloc[:, i]
                # использование целочисленных меток кластера и цветовой схемы для унификации выбора цвета
                , c = model.labels_, cmap = nps
                # выбор маленького размера маркера для уменьшения перекрытия
                , s = 1)
            # подпись оси x на последней строке подграфиков
            if i == df.shape[1] - 1:
                plt.xlabel(df.columns[j])
            # подпись оси y на первом столбце подграфиков
            if j == 0:
                plt.ylabel(df.columns[i])           

    plt.show()

Эти графики выполняют двойную функцию, показывая связь между парой признаков и связь между этими признаками и присвоением кластера.

plotScatters(diabetesDataScaled, km3)

По мере развития анализа легко сосредоточиться на более небольшом подмножестве признаков.

plotScatters(diabetesDataScaled.iloc[:, 2:7], km4)

Скрипичные графики

Чтобы получить более точное представление о распределении каждого признака в каждом кластере, мы также можем посмотреть на скрипичные графики. Если вы не знакомы со скрипичными графиками, подумайте о них как о взрослых родственниках классической коробчатой диаграммы. Если коробчатые диаграммы идентифицируют только несколько ключевых характеристик распределения, то скрипичные графики контурированы, чтобы показать всю функцию плотности вероятности.

def plotViolins(df, model, plotCols = 5):
    """Создать скрипичные графики для каждого признака в наборе данных
    Используйте метки модели для группировки.
    """
  
    # рассчитать количество строк, необходимых для сетки графиков
    plotRows = df.shape[1] // plotCols
    while plotRows * plotCols < df.shape[1]:
        plotRows += 1
  
    # создать фигуру и оси
    fig, axes = plt.subplots(plotRows, plotCols
                             # увеличить размер фигуры для удобного просмотра
                             , figsize = ((plotCols * 3), (plotRows * 3))
                            )
     
    # определить уникальные метки кластеров из модели
    uniqueLabels = sorted(np.unique(model.labels_))
  
    # создать пользовательскую подпалитру из уникальных меток
    npsTemp = nps([x / max(uniqueLabels) for x in uniqueLabels])
  
    # добавить целочисленные метки кластеров к входным данным
    df2 = df.assign(cluster = model.labels_)
  
    # перебирать подграфики для создания скрипичных графиков
    pltindex = 0
    for col in df.columns:
        pltindex += 1
        plt.subplot(plotRows, plotCols, pltindex)
        sns.violinplot(
            data = df2
            # использовать метки кластеров для группировки по оси x
            , x = 'cluster'
            # использовать текущий признак в качестве значений оси y
            , y = col
            # использовать метки кластеров и пользовательскую палитру для единого выбора цвета
            , hue = model.labels_
            , palette = npsTemp
        ).legend_.remove()
        # подписать ось y названием признака
        plt.ylabel(col)
  
    plt.show()

plotViolins(diabetesDataScaled, km3, plotCols = 5)

Гистограммы

Скрипичные графики показывают распределение каждого признака в каждом кластере, но также полезно посмотреть, как каждый кластер представлен в более широком распределении каждого признака. Модифицированная гистограмма может хорошо иллюстрировать это.

def histogramByCluster(df, labels, plotCols = 5, nbins = 30, legend = False, vlines = False):
    """Создать гистограмму для каждого признака.
    Используйте метки модели для кодирования цвета.
    """
 
    # рассчитать количество строк, необходимых для сетки графиков
    plotRows = df.shape[1] // plotCols
    while plotRows * plotCols < df.shape[1]:
        plotRows += 1
 
    # определить уникальные метки кластеров
    uniqueLabels = sorted(np.unique(labels))
  
    # создать фигуру и оси
    fig, axes = plt.subplots(plotRows, plotCols
                             # увеличить размер фигуры для удобного просмотра
                             , figsize = ((plotCols * 3), (plotRows * 3))
                            )
    pltindex = 0
    # перебирать признаки во входных данных
    for col in df.columns:
        # дискретизировать признак в заданное количество интервалов
        tempBins = np.trunc(nbins * df[col]) / nbins
        # пересечь дискретизированный признак с метками кластеров
        tempComb = pd.crosstab(tempBins, labels)
        # создать индекс той же размерности, что и таблица пересечений
        # это поможет с выравниванием
        ind = np.arange(tempComb.shape[0])
 
        # определить соответствующий подграфик
        pltindex += 1
        plt.subplot(plotRows, plotCols, pltindex)
        # создать группированные данные гистограммы
        histPrep = {}
        # работать с одним кластером за раз
        for lbl in uniqueLabels:
            histPrep.update(
                {
                    # связать метку кластера...
                    lbl:
                    # ... с столбчатой диаграммой
                    plt.bar(
                        # использовать индекс, специфичный для признака, чтобы задать положение по оси x
                        x = ind
                        # использовать количество, связанное с этим кластером, в качестве высоты столбца
                        , height = tempComb[lbl]
                        # наложить этот столбец поверх предыдущих столбцов кластеров
                        , bottom = tempComb[[x for x in uniqueLabels if x < lbl]].sum(axis = 1)
                        # устранить промежутки между столбцами
                        , width = 1
                        , color = nps(lbl / max(uniqueLabels))
                    )
                }
            )
       
        # использовать название признака для подписи оси x каждого графика
        plt.xlabel(col)
    
        # подписать ось y графиков в первом столбце
        if pltindex % plotCols == 1:
            plt.ylabel('Частота')
        plt.xticks(ind[0::5], np.round(tempComb.index[0::5], 2))
     
        # если нужно, наложить вертикальные линии
        if vlines:
            for vline in vlines:
                plt.axvline(x = vline * ind[-1], lw = 0.5, color = 'red')
    
    if legend:
        leg1 = []; leg2 = []
        for key in histPrep:
            leg1 += [histPrep[key]]
            leg2 += [str(key)]
        plt.legend(leg1, leg2)
 
    plt.show()
histogramByCluster(diabetesDataScaled, km4.labels_)

Этот процесс легко масштабируется при необходимости добавления большего количества категорий кластеров.

histogramByCluster(diabetesDataScaled, km10.labels_)

Заключение

Эти визуализации предоставляют прочную основу для оценки моделей кластеризации. Если вы хотите узнать больше о том, как это сделать систематически, обязательно посетите мою презентацию на осенней конференции Open Data Science в Сан-Франциско!

Об авторе:

Эви Фаулер – дата-сайентист, работающая в Питтсбурге, штат Пенсильвания. В настоящее время она руководит командой дата-сайентистов, разрабатывающих прогностические модели, связанные с опытом ухода за пациентами, в сфере здравоохранения. Она особенно интересуется этичным применением прогностической аналитики и исследованием того, как качественные методы могут помочь в работе с данными. Она имеет степень бакалавра Брауновского университета и степень магистра Карнеги-Меллона.