Visualización para Métodos de Agrupamiento

Visualización de Agrupamiento

Nota del editor: Evie Fowler es una ponente en ODSC West. ¡Asegúrese de ver su charla, “Cerrando la brecha de interpretabilidad en la segmentación de clientes”, allí!

En la Conferencia de Ciencia de Datos Abiertos de este otoño, hablaré sobre cómo llevar un enfoque sistemático a la interpretación de modelos de agrupamiento. Para prepararnos para eso, hablemos sobre la visualización de datos para modelos de agrupamiento.

Preparando un Espacio de Trabajo

Todas estas visualizaciones se pueden crear con las herramientas básicas de manipulación de datos (pandas y numpy) y los conceptos básicos de visualización (matplotlib y 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

Para este tutorial, utilizaré el conjunto de datos de predicción de diabetes incorporado en matplotlib. Ofreceré mucha más información sobre cómo entrenar y evaluar un modelo de agrupamiento efectivo en ODSC, pero por ahora, simplemente ajustaré algunos modelos de k-means simples.

# cargar datos de diabetes
diabetesData = load_diabetes(as_frame = True).data

# centrar y escalar características agrupables
diabetesScaler = MinMaxScaler().fit(diabetesData)
diabetesDataScaled = pd.DataFrame(diabetesScaler.transform(diabetesData)
                                , columns = diabetesData.columns
                                , index = diabetesData.index)

# construir tres modelos de agrupamiento pequeños
km3 = KMeans(n_clusters = 3).fit(diabetesDataScaled)
km4 = KMeans(n_clusters = 4).fit(diabetesDataScaled)
km10 = KMeans(n_clusters = 10).fit(diabetesDataScaled)

Elegir un Esquema de Colores

El paquete matplotlib proporciona una serie de esquemas de colores incorporados a través de su registro de colormaps. Es conveniente elegir un colormap para la totalidad de una visualización, y es importante elegirlo cuidadosamente. Esto puede implicar evaluar todo, desde si el mapa es secuencial (cuando los datos se pueden interpretar a lo largo de una escala de baja a alta) o divergente (cuando los datos son más relevantes en cualquiera de los dos extremos) hasta si es temáticamente apropiado para el tema (verdes y marrones para un proyecto de topografía). Cuando no hay una relación particular entre los datos y el orden en el que se presentará, el colormap nipy_spectral es una buena elección.

# elegir el colormap nipy_spectral de matplotlib
nps = colormaps['nipy_spectral']

# ver todo el colormap
nps

Cada colormap de matplotlib consta de una serie de tuplas, donde cada una describe un color en formato RGBA (aunque con componentes escalados a [0, 1] en lugar de [0, 255]). Se puede acceder a colores individuales del mapa tanto por entero (entre 0 y 255) como por decimal (entre 0 y 1). Los números cercanos a 0 corresponden a colores en el extremo inferior del colormap, mientras que los enteros cercanos a 255 y los decimales cercanos a 1.0 corresponden a colores en el extremo superior del colormap. Intuitivamente, el mismo color se puede describir tanto con un entero como con un decimal que representa ese entero como un cociente de 255.

# ver colores seleccionados del colormap
print(nps(51))
#(0.0, 0.0, 0.8667, 1.0)

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

Crear Visualizaciones

Gráficos de Dispersión

La visualización clásica para un modelo de agrupamiento es una serie de gráficos de dispersión que comparan cada par de características que se utilizaron en el modelo de agrupamiento, con la asignación de clúster indicada por el color. Hay métodos incorporados para lograr esto, pero un enfoque propio brinda más control sobre detalles como el esquema de colores.

def plotScatters(df, model):
    """Crear gráficos de dispersión basados en cada par de columnas en un dataframe.
    Utilizar color para denotar la etiqueta del modelo.
    """
  
    # crear una figura y ejes
    plotRows = df.shape[1]
    plotCols = df.shape[1]
    fig, axes = plt.subplots(
        # crear una fila y una columna para cada característica en el dataframe
        plotRows, plotCols
        # ampliar el tamaño de la figura para una fácil visualización
        , figsize = ((plotCols * 3), (plotRows * 3))
    )   
    # iterar a través de las subfiguras para crear gráficos de dispersión
    pltindex = 0
    for i in np.arange(0, plotRows):
        for j in np.arange(0, plotCols):
            pltindex += 1
            # identificar la subfigura actual
            plt.subplot(plotRows, plotCols, pltindex)
            plt.scatter(
                # comparar la i-ésima y la j-ésima características del dataframe
                df.iloc[:, j], df.iloc[:, i]
                # utilizar etiquetas de clúster enteras y un mapa de colores para unificar la selección de colores
                , c = model.labels_, cmap = nps
                # elegir un tamaño de marcador pequeño para reducir la superposición
                , s = 1)
            # etiquetar el eje x en la última fila de subfiguras
            if i == df.shape[1] - 1:
                plt.xlabel(df.columns[j])
            # etiquetar el eje y en la primera columna de subfiguras
            if j == 0:
                plt.ylabel(df.columns[i])         

    plt.show()

Estas gráficas hacen doble función, mostrando la relación entre un par de características y la relación entre esas características y la asignación de clúster.

plotScatters(diabetesDataScaled, km3)

A medida que avanza el análisis, es fácil enfocarse en un subconjunto más pequeño de características.

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

Diagramas de Violín

Para tener una mejor idea de la distribución de cada característica dentro de cada clúster, también podemos ver los diagramas de violín. Si no estás familiarizado con los diagramas de violín, piensa en ellos como el primo adulto del clásico diagrama de caja. Mientras que los diagramas de caja identifican solo algunos descriptores clave de una distribución, los diagramas de violín están contorneados para ilustrar la función de densidad de probabilidad completa.

def plotViolins(df, model, plotCols = 5):
    """Crea diagramas de violín de cada característica en un dataframe
    Usa las etiquetas del modelo para agrupar.
    """

    # calcular el número de filas necesarias para la cuadrícula del gráfico
    plotRows = df.shape[1] // plotCols
    while plotRows * plotCols < df.shape[1]:
        plotRows += 1

    # crear una figura y ejes
    fig, axes = plt.subplots(plotRows, plotCols
                              # aumentar el tamaño de la figura para una fácil visualización
                              , figsize = ((plotCols * 3), (plotRows * 3))
                             )

    # identificar las etiquetas de clúster únicas del modelo
    uniqueLabels = sorted(np.unique(model.labels_))

    # crear una subpaleta personalizada a partir de las etiquetas únicas
    # esto devolverá
    npsTemp = nps([x / max(uniqueLabels) for x in uniqueLabels])

    # agregar etiquetas de clúster enteras al dataframe de entrada
    df2 = df.assign(cluster = model.labels_)

    # iterar a través de los subgráficos para crear los diagramas de violín
    pltindex = 0
    for col in df.columns:
        pltindex += 1
        plt.subplot(plotRows, plotCols, pltindex)
        sns.violinplot(
            data = df2
            # usar las etiquetas de clúster como agrupador x
            , x = 'cluster'
            # usar la característica actual como valores y
            , y = col
            # usar las etiquetas de clúster y la paleta personalizada para unificar la selección de color
            , hue = model.labels_
            , palette = npsTemp
        ).legend_.remove()
        # etiquetar el eje y con el nombre de la característica
        plt.ylabel(col)

    plt.show()

plotViolins(diabetesDataScaled, km3, plotCols = 5)

Histogramas

Los diagramas de violín muestran la distribución de cada característica dentro de cada clúster, pero también es útil ver cómo se representa cada clúster en la distribución más amplia de cada característica. Un histograma modificado puede ilustrar esto bien.

def histogramByCluster(df, labels, plotCols = 5, nbins = 30, legend = False, vlines = False):
    """Crea un histograma de cada característica.
    Usa las etiquetas del modelo para codificar por color.
    """

    # calcular el número de filas necesarias para la cuadrícula del gráfico
    plotRows = df.shape[1] // plotCols
    while plotRows * plotCols < df.shape[1]:
        plotRows += 1

    # identificar las etiquetas de clúster únicas
    uniqueLabels = sorted(np.unique(labels))

    # crear una figura y ejes
    fig, axes = plt.subplots(plotRows, plotCols
                              # aumentar el tamaño de la figura para una fácil visualización
                              , figsize = ((plotCols * 3), (plotRows * 3))
                             )
    pltindex = 0
    # recorrer las características en los datos de entrada
    for col in df.columns:
        # discretizar la característica en el número especificado de bins
        tempBins = np.trunc(nbins * df[col]) / nbins
        # cruzar la característica discretizada con las etiquetas de clúster
        tempComb = pd.crosstab(tempBins, labels)
        # crear un índice del mismo tamaño que la tabla cruzada
        # esto ayudará con el alineamiento
        ind = np.arange(tempComb.shape[0])

        # identificar el subgráfico relevante
        pltindex += 1
        plt.subplot(plotRows, plotCols, pltindex)
        # crear datos de histograma agrupados
        histPrep = {}
        # trabajar un clúster a la vez
        for lbl in uniqueLabels:
            histPrep.update(
                {
                    # asociar la etiqueta del clúster ...
                    lbl:
                    # ... con un gráfico de barras
                    plt.bar(
                        # usar el índice específico de la característica para establecer las ubicaciones x
                        x = ind
                        # usar los recuentos asociados con este clúster como altura de la barra
                        , height = tempComb[lbl]
                        # apilar esta barra encima de las barras de clúster anteriores
                        , bottom = tempComb[[x for x in uniqueLabels if x < lbl]].sum(axis = 1)
                        # eliminar espacios entre barras
                        , width = 1
                        , color = nps(lbl / max(uniqueLabels))
                    )
                }
            )

        # usar el nombre de la característica para etiquetar el eje x de cada gráfico
        plt.xlabel(col)

        # etiquetar el eje y de los gráficos en la primera columna
        if pltindex % plotCols == 1:
            plt.ylabel('Frecuencia')
        plt.xticks(ind[0::5], np.round(tempComb.index[0::5], 2))

        # si se desea, superponer líneas verticales
        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_)

Este proceso se escala fácilmente cuando se necesitan más categorías de clúster.

histogramByCluster(diabetesDataScaled, km10.labels_)

Conclusión

Estas visualizaciones proporcionarán una base sólida para evaluar modelos de agrupación. Para obtener más información sobre cómo hacerlo de manera sistemática, asegúrese de asistir a mi charla en la Conferencia de Ciencia de Datos Abiertos de este otoño en San Francisco.

Sobre el autor:

Evie Fowler es una científica de datos con sede en Pittsburgh, Pennsylvania. Actualmente trabaja en el sector de la salud liderando un equipo de científicos de datos que desarrollan modelos predictivos centrados en la experiencia de atención al paciente. Tiene un especial interés en la aplicación ética de la analítica predictiva y en explorar cómo los métodos cualitativos pueden contribuir al trabajo de la ciencia de datos. Posee una licenciatura de la Universidad Brown y una maestría de Carnegie Mellon.