Ejemplo de clustering con k-means en Python

Sin duda k-means es uno de los algoritmos de aprendizaje automático no supervisado más popular. El objetivo de k-means es simple: agrupa puntos de datos similares con el objetivo de descubrir patrones subyacentes. Para lograr este objetivo, k-means busca un número fijo (k) de agrupamientos (clústers) en el conjunto de datos .

1. Funcionamiento básico de k-means.

Aunque estoy casi seguro que si has llegado hasta aquí ya conoces cómo funciona k-means (y lo que buscas es un ejemplo de uso) permíteme que te haga una muy breve descripción de su funcionamiento.

En k-means se define de inicio un número k, que se refiere al número de centroides en los que se dividirá el conjunto de datos. Cada centroide sería la ubicación que marca el centro de cada agrupación.
A cada punto se asigna uno de los grupos mediante la reducción de la suma de cuadrados en el grupo. Dicho de otra forma, el algoritmo k-means identifica k número de centroides, y luego asigna cada punto de los datos al grupo más cercano, mientras mantiene los centroides lo más pequeños posible.

Una vez tenemos cada punto asociado a un clúster, podemos etiquetarlo en el dataframe original asociándolo a dicho grupo y “catalogando” por tanto nuestros datos.

2. Datos de inicio: valores de las acciones de Samsung.

Vamos a aplicar el algoritmo sobre un conjunto de datos de las acciones de Samsung en bolsa para determinar cómo se agrupan usando k-means. Para ello nos descargamos de la web de Yahoo Finanzas el histórico con los valores desde el 1 de enero de 2008 de las acciones de Samsung Electronics Co., Ltd.

De los datos descargados, buscaremos patrones de agrupamiento entre dos señales elegidas, en este caso seleccionamos la señal CLOSE (precio de cierre diario) y la señal VOLUME (volumen de contrataciones diario). Contaremos con una dataframe con los datos diarios desde el 1 de enero de 2008 hasta el 28 de junio de 2019, lo que nos da un total de 2849 registros (que serán algo menos cuando limpiemos los valores nulos).

NOTA: podemos aplicar k-means sobre todas las señales/variables necesarias, pero para poder visualizar los agrupamientos k-means en una gráfica 2-D aplicaremos el algoritmo exclusivamente sobre la dos señales anteriormente detalladas.

3. Carga de librerías y del dataframe.

#%% Carga de librerías.
import pandas as pd
from sklearn import preprocessing 
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt

#%% Carga del dataframe.
df = pd.read_excel("Samsung.xlsx")

Las librerías usadas serán Pandas para almacenar y manipular el dataframe, Preprocessing para normalizar los datos antes de aplicar al algoritmo, KMeans para realizar el clustering, y Pyplot para hacer las representaciones gráficas.

El dataframe cargado queda de la siguiente forma:

Dataframe inicial.

4. Preprocesado de los datos.

En primer lugar, eliminamos los datos vacíos (NA) y resetamos el índice. El objetivo de dicho reseteo es que el índice que numera las filas no se reajusta cuando eliminamos filas vacías, y pueden presentarse problemas de dimensionalidad al extraer columnas para posteriormente agregarlas:

 #%% Se eliminan filas que tengan valor NaN.
 df = df.dropna()
 df = df.reset_index(drop=True)

En nuestro ejemplo el dataset “limpio” es de 2784 registros, es decir, que se han eliminado 66 registros NA.

El siguiente paso es extraer la columna DATE (fecha) que no usaremos en el algoritmo k-means. Antes de extraerla la guardaremos en la variable dates, ya que volveremos a insertarla en nuestro dataframe tras aplicar el algoritmo:

#%% Eliminamos columna de fecha que no usaremos en el algoritmo.
dates = df['Date'] # Guardamos la columna Date.
df = df.drop('Date', 1) # Borramos la columna del dataframe.

5. Normalización de los datos.

Como ocurre con cualquier algoritmo de Machine Learning que utilice funciones de distancia, los datos deben ser normalizados antes de aplicarles el algoritmo. En este caso utilizaremos la función MinMaxScaler() que normaliza todos los datos entre [0, 1].

#%% Se normalizan los datos con MinMax()
min_max_scaler = preprocessing.MinMaxScaler() 
df_escalado = min_max_scaler.fit_transform(df)
df_escalado = pd.DataFrame(df_escalado) # Hay que convertir a DF el resultado.
df_escalado = df_escalado.rename(columns = {0: 'Close', 1: 'Volume'})

Tras aplicar el escalado, nuestro nuevo dataframe normalizado se llama df_ escalado y tiene todos sus valores normalizados entre [0,1]:

Dataframe preprocesado y normalizado.

6. Representación gráfica de los datos.

Ahora podemos echar un vistazo a la representación gráfica de nuestros datos. En el eje x representaremos el precio de cierre (CLOSE) y en el eje y el volumen (VOLUME):

#%% Representación gráfica de los datos.
x = df_escalado['Close'].values
y = df_escalado['Volume'].values
plt.xlabel('Close price')
plt.ylabel('Volume')
plt.title('Samsung stocks CLOSE vs. VOLUME')
plt.plot(x,y,'o',markersize=1)
Precio CLOSE vs. VOLUMEN de acciones Samsung.

A priori, la nube de puntos parece indicar que, a mayor volumen menor es el precio de cierre estabilizándose conforme el precio del CLOSE aumenta.

Hay que indicar que hemos representado gráficamente los datos normalizados, pero que, si hubiésemos representado los datos sin normalizar la gráfica sería exactamente igual, con la única diferencia que la escala de los ejes sería diferente.

7. Aplicación de k-means.

El primer paso antes de aplicar k-means es decidir qué valor de k (número de clústeres) queremos usar. Una forma de elegir este valor k es por criterio propio: si conocemos bien la distribución de nuestros datos y queremos “forzar” un número determinado de clústeres simplemente lo elegimos.

La otra opción es realizar una gráfica elbow o de codo para determinar el número óptimo de clústeres. Hacemos una iteración de k-means variando el valor de k, de forma que representamos en el eje x dicho valor de k y en el eje y la suma de los errores cuadráticos (SSE). De esta forma podemos elegir el valor de k dónde se produce el “codo” de la curva:

#%% Curva elbow para determinar valor óptimo de k.
nc = range(1, 30) # El número de iteraciones que queremos hacer.
kmeans = [KMeans(n_clusters=i) for i in nc]
score = [kmeans[i].fit(df_escalado).score(df_escalado) for i in range(len(kmeans))]
score
plt.xlabel('Número de clústeres (k)')
plt.ylabel('Suma de los errores cuadráticos')
plt.plot(nc,score)
Curva elbow para k-means.

La curva elbow nos muestra que un valor de k = 5 puede ser apropiado, aunque se podría probar con valores entre 5 y 10 y comparar resultados. No hay una solución, un valor de k, más correcto que otro, ya que el objetivo de una clusterización con k-means es obtener información útil nuestros datos, por lo que nuestra interpretación a posteriori de los clústeres creados marcará la calidad de nuestra solución escogida.

Así que ya podemos aplicar el algoritmo de k-means:

#%% Aplicación de k-means con k = 5.
kmeans = KMeans(n_clusters=5).fit(df_escalado)
centroids = kmeans.cluster_centers_
print(centroids)

El algoritmo muestra por pantalla las coordenadas de los 5 centroides:

Centroides creados por k-means.

8. Etiquetado de datos.

Ya hemos ejecutado k-means y obtenido los centroides. Ahora podemos asignar cada registro de nuestro dataset a uno de los clústers:

#%% Etiquetamos nuestro dataframe.
labels = kmeans.predict(df_escalado)
df['label'] = labels

Hemos añadido la columna “label” a nuestro dataframe original sin normalizar, por lo que ahora, cada registro está asignado a un único clúster. Le añadimos también la columna “Date” que extrajimos al inicio para saber a qué fecha corresponde cada registro:

#%% Añadimos la columna de fecha
df.insert(0, 'Date', dates)

El dataframe etiquetado queda así:

9. Representación gráfica de los clústeres k-means.

Una vez con los datos etiquetados, podemos visualizar gráficamente en dos dimensiones el clustering realizado por k-means, ya que hemos usado sólo dos variables.

#%% Plot k-means clustering.
colores=['red','green','blue','yellow','fuchsia']
asignar=[]
for row in labels:
     asignar.append(colores[row])
plt.scatter(x, y, c=asignar, s=1)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='*', c='black', s=20) # Marco centroides.
plt.xlabel('Close price')
plt.ylabel('Volume')
plt.title('Samsung stocks k-means clustering')
plt.show()

Hemos creado una lista de 5 colores, una para cada clúster y se ha marcado cada centroide con un punto estrellado de color negro:

La interpretación de los grupos creados por k-means es una tarea que debe realizar el especialista de los datos. En este caso, y de forma simplificada, podríamos describir cada grupo de la siguiente forma:

  • Clúster azul: grupo de bajo volumen y precio de cierre alto. Los dos puntos de la parte superior podrían considerarse outliers.
  • Clúster rojo: grupo de volumen bajo y precio de cierre medio.
  • Clúster fucsia: grupo de volumen medio y precio de cierre medio.
  • Clúster verde: grupo de volumen medio y precio de cierre bajo.
  • Clúster amarillo: grupo de volumen alto y precio de cierre bajo.

10. Clasificación de nuevas muestras.

Por último, queda por determinar la forma de clasificar nuevas muestras. Es decir, que dados nuevos datos de entrada, determinar a qué clúster pertenecen.

Supongamos que nuestros nuevos datos a categorizar son los siguientes:

  • CLOSE: 46.850
  • VOLUME: 7.196.370

Introducimos estos nuevos datos como un dataframe de una única fila:

close = 46850
volume = 7196370

nuevo_dato = pd.DataFrame([[close,volume]]) # Nueva muestra
nuevo_dato = nuevo_dato.rename(columns = {0: 'Close', 1: 'Volume'})

No podemos introducir como tal estos valores en el algoritmo k-means ya que no están normalizados. Así que en primer lugar hay que normalizar, y para ello debemos agregarlos al conjunto de datos original.

Añadimos por tanto esta nueva fila de datos a nuestro dataframe de inicio y lo guardamos con el nombre df_n para no sobrescribir el original:

df_n = df.append(nuevo_dato)
La última fila del dataframe es nuestro nuevo dato introducido pero sin la columna de “Date” ni “label“.

Nuestro nuevo dataframe df_n tiene aun las columnas “date” y “label” del datafame original, así que las eliminamos y resetamos el índice:

df_n = df_n.drop('Date', 1)
df_n = df_n.drop('label', 1)
df_n = df_n.reset_index(drop=True)
Dataframe con nuestro nuevo dato introducido (última fila) preparado para normalizar.

Ahora procedemos a normalizar el Dataframe completo como hizo anteriormente:

min_max_scaler = preprocessing.MinMaxScaler() 
df_escalado = min_max_scaler.fit_transform(df_n)
df_escalado = pd.DataFrame(df_escalado) # Hay que convertir a DF el resultado.
df_escalado = df_escalado.rename(columns = {0: 'Close', 1: 'Volume'})

Ya tenemos nuestros nuevos datos (última final del dataframe) normalizados:

Nuevos datos normalizados.

Por tanto, los valores normalizados son:

  • CLOSE: 0,789142
  • VOLUME: 0,110929

Podemos introducir estos nuevos datos ya normalizados a mano o extraerlos en forma de vector numpy:

close_n = df_escalado['Close'][2784]
volume_n = df_escalado['Volume'][2784]
import numpy as np
X_new = np.array([[close_n, volume_n]]) # Nueva muestra
X_new es el array con los nuevos datos normalizados.

Por último, introducimos el array X_new en k-means:

new_labels = kmeans.predict(X_new)
print(new_labels)
Etiquetación del nuevo dato en el grupo 2.

El resultado es el clúster 2, que en nuestro caso es el AZUL, es decir, grupo de bajo volumen y precio de cierre alto.

11. Representación gráfica de la nueva muestra.

Podemos representar gráficamente el nuevo punto y verificar que, efectivamente, corresponde con el clúster AZUL:

#%% Plot del nuevo dato clusterizado.

colores=['red','green','blue','yellow','fuchsia']

asignar=[]
for row in labels:
     asignar.append(colores[row])

fig, ax = plt.subplots()
x_n = close_n
y_n = volume_n
 
plt.plot(x_n,y_n, '*', color = 'lime', markersize = 20)
plt.scatter(x, y, c=asignar, s=1)
plt.xlabel('Close price')
plt.ylabel('Volume')
plt.title('Samsung stocks k-means clustering')
plt.show()
El nuevo dato, marcado con una estrella de color verde claro, corresponde claramente al clúster azul.
Para saber más:
K-Means en Python paso a paso.
Understanding K-means Clustering in Machine Learning.
Using the elbow method to determine the optimal number of clusters for k-means clustering.

6 comentarios en “Ejemplo de clustering con k-means en Python”

  1. hola, tu ejemplo es genial, me sirvió de mucho para ir armando algo que necesito clasificar en el trabajo.
    pero tengo problemas (debido a mi escaso conocimiento) al querer graficar.
    te comento: mi tabla tiene 7 atributos (prestamos, calificación, morosidad, depósitos, etc, etc) y quiero graficarlos, pero no sé cómo realizarlo.
    hasta aquí llegué bien:
    0 1 2 3 4 5 6 7 label
    0 1.000000 0.0 0.0 1.0 0.0 0.012232 0.039595 0.001515 4
    1 1.000000 0.0 0.0 1.0 0.0 0.027677 0.025791 0.004117 4
    2 1.000000 0.0 0.0 0.0 0.0 0.034001 0.103239 0.081781 5
    3 0.833333 0.0 0.0 0.5 0.0 0.051012 0.043279 0.005598 1
    4 1.000000 0.0 0.0 0.0 0.0 0.028064 0.016616 0.000984 5

    en tu ejemplo usas solamente dos variables: ¿cómo hago con 7? desde ya muchas gracias

    1. Hola Damian, gracias por comentar en el blog.

      No se pueden graficar 7 dimensiones para visualizar los clústeres creados. Se pueden graficar 2 (como en mi ejemplo) o 3 haciendo una gráfica 3D. En estos casos lo que puedes hacer es crear varias gráficas cogiendo tus variables 2 a 2 (por ejemplo la 1 con la 2, la 3 con la 4….). También puedes visualizarlas todas con una matriz de dispersión usando pairplot() de la librería Seabron. Aquí tienes un ejemplo:

      https://relopezbriega.github.io/blog/2016/09/18/visualizaciones-de-datos-con-python/

      Otra opción es hacer clustering con un Mapa Auto-Organizado. En mi blog tiene un ejemplo, pero está en R, no en Python. El mapa auto-organizado “convierte” a 2D todas tus variables (lo convierte en un mapa) y ahí puedes visualizar el clustering perfectamente. Aquí tienes un ejemplo de cómo resultaría:

      https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRA3QOI6-oaKNvlnIkTzcwmoH8h9bK6RSia1Qw4W1PQogvWS5-h

      Un saludo!

  2. Realmente valoro mucho tu ayuda.
    Te comento: empecé a practicar de a poco: sumé una sola variable, con lo cual las cosas quedaron así:

    datos=pd.read_csv(“base2.csv”,”;”)
    df=pd.DataFrame(datos)
    x=df[‘prest’]
    y=df[‘depos’]
    z=df[‘irreg’]
    X=np.array(list(zip(x,y,z))).reshape(len(x),3)

    a estas alturas ya tengo en X las 3 variables cargadas.
    realizo la curva del codo, el entrenamiento, obteniendo las coordenadas con esas tres variables.
    coordenada: [ 457634 1148293 3662] etiqueta: 5
    coordenada: [ 457296 1188164 912] etiqueta: 5
    coordenada: [453123 396820 5927] etiqueta: 0
    coordenada: [452762 302196 7905] etiqueta: 0
    coordenada: [451945 292007 890] etiqueta: 0

    todo sigue perfecto.
    con este código que sigue, puedo graficar cada coordenada:

    for i in range(len(X)):
    print(“coordenada: “,X[i],” etiqueta: “,labels[i])
    plt.plot(X[i][0],X[i][1],colors[labels[i]],markersize=10)

    queda de 10, un lujo.
    el problema se me está presentando cuando quiero colocar en ese gráfico los centroides. No manejo muy bien este asunto (por eso te solicito consejo) y lo hago a través de este código (el cual no funciona):
    plt.scatter(centroids[:,0],centroids[:,1],marker=”x”,s=150,linewidths=5,zorder=10)

    estuve practicando bastante y pude hacer que funcione todo, menos colocar los centroides en la gráfica. Se visualizan perfectamente los agrupamientos (6 en mi caso), pero a esos centroides no los puedo incorporar. Como te dije, creo que el problema está con scatter, tengo que usar otro método y no sé cual.
    buscando info encontré que “scatter” solamente soporta dos variables ¿qué otro método existe para realizar dicha tarea?
    Un abrazo y muchas gracias

    1. Hola Damian,

      No tengo mucho tiempo de analizar tu problema, pero a simple vista parece que tus centroides tienen 3 coordenadas con lo cual es imposible representarlos en un plano. Busca hacer una representación 3D de tus datos con matplotlib o modifica las dimensiones de tu problema.

      Te recomiendo que hagas consultas en https://stackoverflow.com/ donde la comunidad suele contestar este tipo de preguntas.

      Gracias y un saludo.

Deja una respuesta