Métodos no supervisados

 

A lo largo de esta práctica veremos como aplicar distintas técnicas no supervisadas así como algunas de sus aplicaciones reales:

  1. Clustering clásico: k-means y la regla del codo.
  • Clustering con formas y feature engineering.
  • Optimización con reducción de dimensionalidad: t-SNE.
  • Aplicación: agrupación de documentos.

Para ello vamos a necesitar las siguientes librerías:

In [1]:
import random

import numpy as np
import pandas as pd
from sklearn import cluster      # Algoritmos de clustering.
from sklearn import datasets     # Crear datasets.
from sklearn import manifold     # Algoritmos de reduccion de dimensionalidad.

# Visualizacion.
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns

%matplotlib inline

1. Clustering clásico: k-means y la regla del codo

Vamos a partir de un dataset de clientes en un negocio retail cualquiera (en el fichero pec2_1.p un DataFrame de pandas en formato pickle o pec2_1.csv en formato CSV).

Para cada cliente se cuenta con 3 variables:

  • n_days_per_week: frecuencia de asistencia a la tienda a la semana.
  • n_month_purchases: número de compras al mes.
  • avg_month_turnover: el gasto medio de un cliente al mes.

    Primero se pide visualizar las variables para entender como están distribuidas y preprocesarlas para aplicar un k-means.

Implementación: visualizar y preprocesar las variables (para que todas las variables tengan el mismo peso).
In [2]:
df = pd.read_csv("pec2_1.csv")

print("Visualizo las variables y su frecuéncia.  ")
df.hist()
plt.tight_layout()
Visualizo las variables y su frecuéncia.
In [3]:

print("Primer análisis de los datos")
display(df.head())

print("Descripción de los datos")
display(df.describe())
print("El número de líneas es: " + str(df.shape[0]) + " y el número de columnas: "+ str(df.shape[1]))

print("No existe ningún null")
display(df.isnull().sum())

print("Normalizo las variables")
from sklearn.preprocessing import MinMaxScaler
#importante la copia, sino realizo las modificaciones sobre df. 
datos_normalizado = df.copy()
for col in df.columns:
    datos_normalizado[col] = MinMaxScaler().fit_transform( datos_normalizado[col].values.reshape(-1, 1))

display(datos_normalizado.head())
Primer análisis de los datos
avg_month_turnovern_days_per_weekn_month_purchases
0108.3314343.2631825.523083
1101.2531425.1973335.303098
260.9000023.1768715.082450
390.9268114.1847475.272594
4114.9364973.3353934.754987
Descripción de los datos
avg_month_turnovern_days_per_weekn_month_purchases
count1200.0000001200.0000001200.000000
mean215.4875502.7119465.120623
std180.5347931.9696714.362437
min-41.4448180.1000000.100000
25%79.2624860.8541831.578684
50%133.6552872.7281474.233838
75%331.4324954.3313596.857876
max793.7345937.68539216.856447
El número de líneas es: 1200 y el número de columnas: 3
No existe ningún null
avg_month_turnover    0
n_days_per_week       0
n_month_purchases     0
dtype: int64
Normalizo las variables
avg_month_turnovern_days_per_weekn_month_purchases
00.1793340.4170100.323642
10.1708590.6719930.310513
20.1225420.4056310.297345
30.1584950.5385020.308693
40.1872430.4265290.277803

Se pide estimar el número de clusters a detectar por k-means. Una técnica para estimar k es, como se explica en la teoría:

Los criterios anteriores (minimización de distancias intra grupo o maximización de distancias inter grupo) pueden usarse para establecer un valor adecuado para el parámetro k. Valores k para los que ya no se consiguen mejoras significativas en la homogeneidad interna de los segmentos o la heterogeneidad entre segmentos distintos, deberían descartarse.

Lo que popularmente se conocer como regla del codo.

Primero es necesario calcular la suma de los errores cuadráticos (SSE) que consiste en la suma de todos los errores (distancia de cada punto a su centroide asignado) al cuadrado.

SSE=i=1KxCieuclidean(x,ci)2SSE = \sum_{i=1}^{K} \sum_{x \in C_i} euclidean(x, c_i)^2

Donde K es el número de clusters a buscar por k-means, xCix \in C_i son los puntos que pertenecen a i-ésimo cluster, cic_i es el centroide del cluster CiC_i (al pertenece el punto x), y euclidean es la distancia euclídea.

Este procedimiento realizado para cada posible valor k, resulta en una función monótona decreciente, donde el eje x representa los distintos valores de k, y el eje y el SSE. Intuitivamente se podrá observar un significativo descenso del error, que indicará el valor idóneo de k.

Se pide realizar la representación gráfica de la regla del codo junto a su interpretación, utilizando la librería matplotlib y la implementación en scikit-learn de k-means.

Implementación: cálculo y visualización de la regla del codo.
In [4]:
# Metodo del Codo para encontrar el numero optimo de clusters

features=['avg_month_turnover', 'n_days_per_week','n_month_purchases']
x = datos_normalizado.loc[:, features].values

from sklearn.cluster import KMeans
wcss = []
x_label = []
for i in range(1, 20):
    kmeans = KMeans(n_clusters = i, init = 'k-means++', random_state = 42)
    kmeans.fit(x)
    wcss.append(kmeans.inertia_)
    x_label.append(i)

# Grafica de la suma de las distancias
plt.plot(range(1, 20), wcss)

plt.title('The Elbow Method')
plt.xlabel('Number of clusters')
plt.xticks(x_label)

plt.ylabel('WCSS')
plt.show()
Análisis: ¿Qué se interpreta en la gráfica? ¿Cómo podría mejorarse la elección de k?.

En la gráfica se observa una disminución muy importante para el valor k=4, es decir, la subdivisión en 4 clústers es la opción más acertada para resolver el problema. A partir de ese valor la disminución del error es muy poco relevante.

Utilizando otra métrica, por ejemplo, se podría utilizar una técnica para maximizar la suma de distancias entre segmentos. Utilizando una tecnica que uniera la minimización de distancias intra- grupo y la maximización de distancias intergrupo obtendríamos mejores resultados.

Análisis: Observando los centroides de cada cluster. ¿Qué tipos de usuarios describen cada cluster?
In [5]:
kmeans = KMeans(n_clusters = 4, init = 'k-means++', random_state = 42)
kmeans.fit(x)
centroids = kmeans.cluster_centers_
print(datos_normalizado.columns)
display(centroids)
Index(['avg_month_turnover', 'n_days_per_week', 'n_month_purchases'], dtype='object')
array([[0.40703499, 0.6464966 , 0.70940991],
       [0.11329995, 0.08032122, 0.05008262],
       [0.64888252, 0.15175766, 0.16899211],
       [0.17023367, 0.51330559, 0.29418709]])

Representan la siguiente tipología de clientes:

  • El cliente habitual, es decir, realizas compras frecuentes y con un valor elevado.
  • El cliente ocasionarl, acude muy poco en el mes para comprar y gastar muy poco.
  • El cliente que gasta mucho, pero que acude muy poco a la tienda.
  • El cliente que gasta poco, pero acude mucho a la tienda.
[OPCIONAL] Implementación: visualiza el dataset en 3 dimensiones, donde los puntos del mismo color pertenezcan al mismo cluster.
In [6]:
# for 3D projection to work
from mpl_toolkits.mplot3d import Axes3D

import warnings
random.seed(10)
warnings.simplefilter('ignore')

estimators = [('k_means_4', KMeans(n_clusters=4)),
              ]
print('Visualización del dataset en 3 dimensiones')
titles = ['4 clusters']
for name, est in estimators:
    fig = plt.figure().gca(projection='3d')
    est.fit(x)
    labels = est.labels_
    fig.scatter(datos_normalizado['n_days_per_week'], datos_normalizado['n_month_purchases'], datos_normalizado['avg_month_turnover'],
               c=labels.astype(np.float), edgecolor='k')
    fig.set_xlabel('n_days_per_week')
    fig.set_ylabel('n_month_purchases')
    fig.set_zlabel('avg_month_turnover')
    fig.set_title(titles[0])
    plt.tight_layout()
    plt.show()


Visualización del dataset en 3 dimensiones

De forma optativa se plantea realizar el apartado anterior con una implementación propia del algoritmo k-means.

[OPCIONAL] Implementación: algoritmo k-means desde cero.
In [7]:
import copy
import random
random.seed(10)

data=x
# Number of clusters
k = 4
# Number of training data
n = data.shape[0]
# Number of features in the data
c = data.shape[1]

# Generate random centers, here we use sigma and mean to ensure it represent the whole data
mean = np.mean(data, axis = 0)
std = np.std(data, axis = 0)
centers = np.random.randn(k,c)*std + mean



centers_old = np.zeros(centers.shape) # to store old centers
centers_new = copy.deepcopy(centers) # Store new centers

data.shape
clusters = np.zeros(n)
distances = np.zeros((n,k))

error = np.linalg.norm(centers_new - centers_old)

# When, after an update, the estimate of that center stays the same, exit loop
while error != 0:
    # Measure the distance to every center
    for i in range(k):
        distances[:,i] = np.linalg.norm(data - centers[i], axis=1)
    # Assign all training data to closest center
    clusters = np.argmin(distances, axis = 1)

    centers_old = copy.deepcopy(centers_new)
    # Calculate mean for every cluster and update the center
    for i in range(k):
        centers_new[i] = np.mean(data[clusters == i], axis=0)
    error = np.linalg.norm(centers_new - centers_old)
print(centers_new)
[[0.31925093 0.63182734 0.5404736 ]
 [0.20268337 0.27266971 0.2277484 ]
 [0.11899447 0.00887271 0.02935249]
 [0.68292228 0.15964736 0.1608252 ]]
In [8]:
print("Comparación de centroides")
print("Algoritmo kmeans libreria python")
kmeans = KMeans(n_clusters = 4, init = 'k-means++', random_state = 42)
kmeans.fit(x)
centroids = kmeans.cluster_centers_
print(datos_normalizado.columns)
display(centroids)
print("Implementado manualmente")
print(centers_new)
Comparación de centroides
Algoritmo kmeans libreria python
Index(['avg_month_turnover', 'n_days_per_week', 'n_month_purchases'], dtype='object')
array([[0.40703499, 0.6464966 , 0.70940991],
       [0.11329995, 0.08032122, 0.05008262],
       [0.64888252, 0.15175766, 0.16899211],
       [0.17023367, 0.51330559, 0.29418709]])
Implementado manualmente
[[0.31925093 0.63182734 0.5404736 ]
 [0.20268337 0.27266971 0.2277484 ]
 [0.11899447 0.00887271 0.02935249]
 [0.68292228 0.15964736 0.1608252 ]]
In [9]:
print('Visualización de lo centroides')
from mpl_toolkits.mplot3d import Axes3D

titles = ['4 clusters']
for name, est in estimators:
    fig = plt.figure().gca(projection='3d')
    est.fit(x)

    fig.scatter(datos_normalizado['n_days_per_week'], datos_normalizado['n_month_purchases'], datos_normalizado['avg_month_turnover'],
             alpha=.1  )
    fig.scatter(centers_new[:,1], centers_new[:,2],centers_new[:,0], marker='*',alpha=1, c='g', s=[400,400,400,400])

    fig.set_xlabel('n_days_per_week')
    fig.set_ylabel('n_month_purchases')
    fig.set_zlabel('avg_month_turnover')
    fig.set_title(titles[0])
    #for i in range(k):
     #   points = np.array([datos_normalizado[j] for j in range(len(datos_normalizado[j])) if clusters[j] == i])
      #  ax.scatter(points[:, 0], points[:, 1], s=7, c=colors[i])

    plt.tight_layout()
    plt.show()
Visualización de lo centroides

En este caso, con 2 dimensiones, es muy sencillo inferir el número de clusters visualizando los datos. Pero este método es de gran utilidad cuando se cuenta con datos de alta dimensionalidad.

2. Clustering con formas y feature engineering

Pero no todos los datasets son como los del ejercicio anterior. Para esta segunda parte vamos a emplear el siguiente conjunto de datos:

In [10]:
data_circles = ('circles', *datasets.make_circles(n_samples=1000, factor=.5, noise=.05))

Donde data_circles es una tupla con tres posiciones: el nombre del dataset y los dos valores devueltos por la función que genera el dataset:

In [11]:
#datasets.make_circles?
In [12]:
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(data_circles[1][:,0], data_circles[1][:,1], c=data_circles[2], s=2)
ax.set_title('Dataset {}'.format(data_circles[0]))
plt.tight_layout()

2 a. Encontrando los clusters con k-means

Implementación: aplica la regla del codo para decidir el valor de k.
In [13]:
# Metodo del Codo para encontrar el numero optimo de clusters

x=data_circles[1][:, [0,1]]

from sklearn.cluster import KMeans
wcss = []
for i in range(1, 20):
    kmeans = KMeans(n_clusters = i, init = 'k-means++', random_state = 42)
    kmeans.fit(x)
    wcss.append(kmeans.inertia_)
    x_label.append(i)

# Grafica de la suma de las distancias
plt.plot(range(1, 20), wcss)

plt.title('The Elbow Method')
plt.xlabel('Number of clusters')
plt.xticks(range(1, 20))

plt.ylabel('WCSS')
plt.show()
In [14]:
# Metodo del Codo para encontrar el numero optimo de clusters

x=data_circles[1][:, [0,1]]

from sklearn.cluster import KMeans
wcss = []
for i in range(1, 7):
    kmeans = KMeans(n_clusters = i, init = 'k-means++', random_state = 42)
    kmeans.fit(x)
    wcss.append(kmeans.inertia_)
    x_label.append(i)

# Grafica de la suma de las distancias
plt.plot(range(1, 7), wcss)

plt.title('The Elbow Method')
plt.xlabel('Number of clusters')
plt.xticks(range(1, 7))

plt.ylabel('WCSS')
plt.show()

print('La k escogida óptima es 3')
La k escogida óptima es 3
Implementación: aplica k-means con el valor de k elegido.
Visualiza el resultado en un scatter plot representando cada cluster con un color distinto.
In [15]:
# for 3D projection to work
from mpl_toolkits.mplot3d import Axes3D
import warnings
random.seed(10)
warnings.simplefilter('ignore')

estimators = [('k_means_3', KMeans(n_clusters=3)),
              ]
print('Visualización del dataset en 3 dimensiones')
titles = ['3 clusters']
for name, est in estimators:
    fig = plt.figure().gca(projection='3d')
    est.fit(x)
    labels = est.labels_
    fig.scatter( x[:, 1], x[:, 0],
               c=labels.astype(np.float), edgecolor='k')
    fig.set_xlabel('x')
    fig.set_ylabel('y')
    fig.set_title(titles[0])
    plt.tight_layout()
    plt.show()
Visualización del dataset en 3 dimensiones