Algoritmos Clúster#

Introducción#

Los clústeres son grupos de objetos que son similares entre sí y son diferentes de los objetos en otros grupos. Los algoritmos de agrupamiento son algoritmos no supervisados que se utilizan para encontrar grupos de objetos similares en un conjunto de datos. Los algoritmos de agrupamiento se utilizan en una amplia variedad de aplicaciones, como la segmentación de clientes, la agrupación de documentos, la agrupación de genes y la agrupación de imágenes.

A continuación, se presentan K means, un algoritmo de agrupamiento muy popular y ampliamente utilizado.

Casos de Uso de K-Means#

Tomado de: https://www.aprendemachinelearning.com/k-means-en-python-paso-a-paso/

El algoritmo de Clustering K-means es uno de los más usados para encontrar grupos ocultos, o sospechados en teoría sobre un conjunto de datos no etiquetado. Esto puede servir para confirmar -o desterrar- alguna teoría que teníamos asumida de nuestros datos. Y también puede ayudarnos a descubrir relaciones asombrosas entre conjuntos de datos, que de manera manual, no hubiéramos reconocido. Una vez que el algoritmo ha ejecutado y obtenido las etiquetas, será fácil clasificar nuevos valores o muestras entre los grupos obtenidos.

Algunos casos de uso son:

  • Segmentación por Comportamiento: relacionar el carrito de compras de un usuario, sus tiempos de acción e información del perfil.

  • Categorización de Inventario: agrupar productos por actividad en sus ventas

  • Detectar anomalías o actividades sospechosas: según el comportamiento en una web reconocer un troll -o un bot- de un usuario normal

Ejercicio Python de K-means#

Realizaremos un ejercicio de prueba para comprender como funciona este algoritmo

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sb
from sklearn.cluster import KMeans
from sklearn.metrics import pairwise_distances_argmin_min

%matplotlib inline
from mpl_toolkits.mplot3d import Axes3D
plt.rcParams['figure.figsize'] = (16, 9)
plt.style.use('ggplot')

Como ejemplo utilizaremos de entradas un conjunto de datos que obtuvo el autor, en el que se analizaban rasgos de la personalidad de usuarios de Twitter. El autor ha filtrado a 140 “famosos” del mundo en diferentes areas: deporte, cantantes, actores, etc. Basado en una metodología de psicología conocida como “Ocean: The Big Five” tendemos como características de entrada:

  • usuario (el nombre en Twitter)

  • “op” = Openness to experience – grado de apertura mental a nuevas experiencias, curiosidad, arte

  • “co” =Conscientiousness – grado de orden, prolijidad, organización

  • “ex” = Extraversion – grado de timidez, solitario o participación ante el grupo social

  • “ag” = Agreeableness – grado de empatía con los demás, temperamento

  • “ne” = Neuroticism, – grado de neuroticismo, nervioso, irritabilidad, seguridad en sí mismo.

  • Wordcount – Cantidad promedio de palabras usadas en sus tweets

  • Categoria – Actividad laboral del usuario (actor, cantante, etc.)

Utilizaremos el algoritmo K-means para que agrupe estos usuarios -no por su actividad laboral- si no, por sus similitudes en la personalidad.

En la siguiente base de datos las categoría que representan la actividad laborla de los famosos están codificados según el siguiente diccionario;

{1:"actores", 2:"cantantes", 3:"modelo", 4:"TV", 5:"radio", 6:"tecnología", 7:"deportes", 8:"politica", 9:"escritor"}

Cargamos los datos de entrada del archivo csv#

dataframe = pd.read_csv("../data/analisis.csv")
dataframe
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[2], line 1
----> 1 dataframe = pd.read_csv("../data/analisis.csv")
      2 dataframe

File ~\miniconda3\Lib\site-packages\pandas\io\parsers\readers.py:948, in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options, dtype_backend)
    935 kwds_defaults = _refine_defaults_read(
    936     dialect,
    937     delimiter,
   (...)
    944     dtype_backend=dtype_backend,
    945 )
    946 kwds.update(kwds_defaults)
--> 948 return _read(filepath_or_buffer, kwds)

File ~\miniconda3\Lib\site-packages\pandas\io\parsers\readers.py:611, in _read(filepath_or_buffer, kwds)
    608 _validate_names(kwds.get("names", None))
    610 # Create the parser.
--> 611 parser = TextFileReader(filepath_or_buffer, **kwds)
    613 if chunksize or iterator:
    614     return parser

File ~\miniconda3\Lib\site-packages\pandas\io\parsers\readers.py:1448, in TextFileReader.__init__(self, f, engine, **kwds)
   1445     self.options["has_index_names"] = kwds["has_index_names"]
   1447 self.handles: IOHandles | None = None
-> 1448 self._engine = self._make_engine(f, self.engine)

File ~\miniconda3\Lib\site-packages\pandas\io\parsers\readers.py:1705, in TextFileReader._make_engine(self, f, engine)
   1703     if "b" not in mode:
   1704         mode += "b"
-> 1705 self.handles = get_handle(
   1706     f,
   1707     mode,
   1708     encoding=self.options.get("encoding", None),
   1709     compression=self.options.get("compression", None),
   1710     memory_map=self.options.get("memory_map", False),
   1711     is_text=is_text,
   1712     errors=self.options.get("encoding_errors", "strict"),
   1713     storage_options=self.options.get("storage_options", None),
   1714 )
   1715 assert self.handles is not None
   1716 f = self.handles.handle

File ~\miniconda3\Lib\site-packages\pandas\io\common.py:863, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    858 elif isinstance(handle, str):
    859     # Check whether the filename is to be opened in binary mode.
    860     # Binary mode does not support 'encoding' and 'newline'.
    861     if ioargs.encoding and "b" not in ioargs.mode:
    862         # Encoding
--> 863         handle = open(
    864             handle,
    865             ioargs.mode,
    866             encoding=ioargs.encoding,
    867             errors=errors,
    868             newline="",
    869         )
    870     else:
    871         # Binary mode
    872         handle = open(handle, ioargs.mode)

FileNotFoundError: [Errno 2] No such file or directory: '../data/analisis.csv'
dataframe.columns=["usuario","Apertura Mental","Escrupulosidad","Extraversión","Empatia","Neuroticismo","Cantidad de palabras","Categoría"]
dataframe["Categoría"]=dataframe["Categoría"].replace({1:"actores", 2:"cantantes", 3:"modelo", 4:"TV", 5:"radio", 6:"tecnología", 7:"deportes", 8:"politica", 9:"escritor"})
dataframe
dataframe.describe()
#vemos cuantos usuarios hay de cada categoria
dataframe.groupby('Categoría').size()

Visualizamos los datos#

Visualizaremos la distribución de cada rasgo psicológico en esta población.

#Pintamos un histograma para cada una de las variables cuantitativas
dataframe.drop(['Categoría'],1).hist()
plt.show()
dataframe.drop(['Categoría'],1).boxplot()
plt.show()
sb.pairplot(dataframe.dropna(), hue='Categoría',height=4,vars=dataframe.columns[1:-1],kind='scatter')

Hagamos componentes principales para tratar de disminuir la dimensionalidad de estos datos.

Nota: Evidentemente el gráfico muestra poca correlación entre variables, es posible que el PCA no sea provechoso

fig, ax = plt.subplots()
s=sb.heatmap(pd.DataFrame(data=dataframe[dataframe.columns[1:-1]]).corr(),cmap='coolwarm', center=0,
             linewidths=.5, cbar_kws={"shrink": .5},annot=True) 
s.set_yticklabels(s.get_yticklabels(),rotation=30,fontsize=7)
s.set_xticklabels(s.get_xticklabels(),rotation=30,fontsize=7)
ax.set_xlim(0,6)
ax.set_ylim(0,6)
plt.show()
import prince
pca = prince.PCA(
     n_components=6,
     n_iter=3,
     rescale_with_mean=True,
     rescale_with_std=True,
     copy=True,
     check_input=True,
     engine='auto',
     random_state=42
 )
pca = pca.fit(dataframe[dataframe.columns[1:-1]])
np.cumsum(pca.explained_inertia_)
 ax = pca.plot_row_coordinates(
     dataframe[dataframe.columns[1:-1]],
     ax=None,
     figsize=(6, 6),
     x_component=0,
     y_component=1,
     labels=None,
     color_labels=dataframe['Categoría'],
     ellipse_outline=False,
     ellipse_fill=False,
     show_points=True
 )

Creamos el modelo#

X = np.array(dataframe[dataframe.columns[1:-1]])
y = np.array(dataframe['Categoría'])
yu=np.array(dataframe['Categoría'].unique())
X.shape
dicty={}
for i in enumerate(yu):
    dicty[i[1]]=i[0]
dicty
fig = plt.figure()
ax = Axes3D(fig)
colores=['blue','red','green','blue','cyan','yellow','orange','black','pink','brown','purple']
#NOTA: asignamos la posición cero del array repetida pues las categorias comienzan en id 1. 
asignar=[]
for row in y:
    asignar.append(colores[dicty[row]])
ax.scatter(X[:, 0], X[:, 1], X[:, 2], c=asignar,s=60)

Buscamos el valor K#

Nc = range(1, 20)
kmeans = [KMeans(n_clusters=i) for i in Nc]
kmeans
score = [kmeans[i].fit(X).score(X) for i in range(len(kmeans))]
score
plt.plot(Nc,score)
plt.xlabel('Number of Clusters')
plt.ylabel('Score')
plt.title('Elbow Curve')
plt.show()
# Para el ejercicio, elijo 3 como un buen valor de K. Pero podría ser otro.
kmeans = KMeans(n_clusters=3).fit(X)
centroids = kmeans.cluster_centers_
print(centroids)
# Obtenemos las etiquetas de cada punto de nuestros datos
labels = kmeans.predict(X)
# Obtenemos los centroids
C = kmeans.cluster_centers_
colores=['purple','red','blue']
asignar=[]
for row in labels:
    asignar.append(colores[row])

fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(X[:, 0], X[:, 1], X[:, 5], c=asignar,s=60)
ax.scatter(C[:, 0], C[:, 1], C[:, 5], marker='*', c=colores, s=1000)
C[:,5]
 ax = pca.plot_row_coordinates(
     dataframe[dataframe.columns[1:-1]],
     ax=None,
     figsize=(6, 6),
     x_component=0,
     y_component=1,
     labels=None,
     color_labels=asignar,
     ellipse_outline=False,
     ellipse_fill=False,
     show_points=True
 )
# Hacemos una proyección a 2D con los diversos ejes 
f1 = dataframe['Apertura Mental'].values
f2 = dataframe['Extraversión'].values

plt.scatter(f1, f2, c=asignar, s=70)
plt.scatter(C[:, 0], C[:, 2], marker='*', c=colores, s=1000)
plt.show()
dataframe
# Hacemos una proyección a 2D con los diversos ejes 
f1 = dataframe['Apertura Mental'].values
f2 = dataframe['Extraversión'].values

plt.scatter(f1, f2, c=asignar, s=70)
plt.scatter(C[:, 0], C[:, 2], marker='*', c=colores, s=1000)
plt.show()
f1 = dataframe['Extraversión'].values
f2 = dataframe['Empatia'].values

'''
# este codigo comentado agrega las categorias sobre cada punto
for label, x, y in zip(dataframe['categoria'].values, f1, f2):
    plt.annotate(
        label,
        xy=(x, y), xytext=(-10, 10),
        textcoords='offset points', ha='right', va='bottom',
        bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
        arrowprops=dict(arrowstyle = '->', connectionstyle='arc3,rad=0'))
'''
plt.scatter(f1, f2, c=asignar, s=70)
plt.scatter(C[:, 2], C[:, 3], marker='*', c=colores, s=1000)
plt.show()
# contamos cuantos usuarios hay en cada grupo
copy =  pd.DataFrame()
copy['usuario']=dataframe['usuario'].values
copy['categoria']=dataframe['Categoría'].values
copy['label'] = labels;
cantidadGrupo =  pd.DataFrame()
cantidadGrupo['color']=colores
cantidadGrupo['cantidad']=copy.groupby('label').size()
cantidadGrupo
!pip install tabulate
from IPython.core.display import display,Markdown
# Veamos cuantos usuarios en cada categoria
for i in range(5):
    group_referrer_index = copy['label'] ==i
    group_referrals = copy[group_referrer_index]
    diversidadGrupo =  pd.DataFrame()
    diversidadGrupo['cantidad']=group_referrals.groupby('categoria').size()
    display(Markdown(diversidadGrupo.to_markdown()))
    print()
#vemos el representante del grupo, el usuario cercano a su centroid
closest, _ = pairwise_distances_argmin_min(kmeans.cluster_centers_, X)
closest
#Los usuarios más cercanos al centroide
users=dataframe['usuario'].values
for row in closest:
    print(users[row])
from IPython.core.display import display, HTML
#miramos los usuarios de cada grupo
text="<table><tr> <td> Grupo 0</td><td> Grupo 1</td><td> Grupo 2</td><td> Grupo 3</td><td> Grupo 4</td></tr><tr>"
for i in range(5):
    text+="<td>"
    for index, row in copy.iterrows():
        if row["label"] == i:
            text+="<p>"+ row["usuario"]+" " +row["categoria"]+"</p>"
    text+="</td>"
text+="</tr></table>"
display(HTML(text))

        

Clasificación de nuevos registros#

X_new = np.array([[45.92,57.74,15.66,12.11,97,89.9]]) #davidguetta personality traits
new_labels = kmeans.predict(X_new)
print(new_labels)

#NOTA: en el array podemos poner más de un array para evaluar a varios usuarios nuevos a la vez

NOTA FINAL: Los resultados obtenidos pueden varias de ejecución en ejecución pues al inicializar aleatoriamente los centroids, podemos obtener grupos distintos o los mismos pero en distinto orden y color