Laboratorio 6: La desperación de Mr. Lepin 🐼

MDS7202: Laboratorio de Programación Científica para Ciencia de Datos

Cuerpo Docente:¶

  • Profesor: Matías Rojas y Mauricio Araneda
  • Auxiliar: Ignacio Meza D.
  • Ayudante: Rodrigo Guerra

Equipo: SUPER IMPORTANTE - notebooks sin nombre no serán revisados¶

  • Nombre de alumno 1: Johnny Godoy

Link de repositorio de GitHub: https://github.com/johnny-godoy/laboratorios-mds/blob/main/lab%206/laboratorio_6.ipynb¶

Indice¶

  1. Temas a tratar
  2. Descripcción del laboratorio
  3. Desarrollo

Temas a tratar¶

  • Aplicar Pandas para obtener características de un DataFrame.
  • Aplicar Pipelines.
  • Aplicar Clusters sobre un conjunto de datos.

Reglas:¶

  • Fecha de entrega: 09/06/2021
  • Grupos de 2 personas
  • Ausentes deberán realizar la actividad solos.
  • Cualquier duda fuera del horario de clases al foro. Mensajes al equipo docente serán respondidos por este medio.
  • Prohibidas las copias.
  • Pueden usar cualquer matrial del curso que estimen conveniente.
  • Código que no se pueda ejecutar, no será revisado.

Objetivos principales del laboratorio¶

  • Comprender y aprovechar las ventajas que nos ofrece la librería pandas con respecto a trabajar en Python 'puro'.
  • Crear nuevas características para entrenar un modelo de clustering.
  • Comprender como aplicar pipelines de Scikit-Learn para generar procesos más limpios.

El laboratorio deberá ser desarrollado sin el uso indiscriminado de iteradores nativos de python (aka "for", "while"). La idea es que aprendan a exprimir al máximo las funciones optimizadas que nos entrega numpy, las cuales vale mencionar, son bastante más eficientes que los iteradores nativos sobre arreglos (o tensores).

Descripción del laboratorio.¶

Importamos librerias utiles 😸¶

In [1]:
import datetime

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.cluster import KMeans
from sklearn.compose import ColumnTransformer
from sklearn.exceptions import NotFittedError
from sklearn.impute import SimpleImputer
from sklearn.manifold import TSNE
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer

from IPython.display import HTML

Segmentación de Clientes en Tienda de Retail 🛍️¶

1.1 Cargar Dataset¶

Mr. Lepin, en una nueva reunión, le cuenta a ud y su equipo que los resultados derivados del análisis exploratorio de dato presentaron una gran utilidad para la empresa y que tiene un gran entusiasmo por continuar trabajando con ustedes. Es por esto, que Mr. Lepin les pide que cargue y visualicen algunas de las filas que componen el Dataset. A continuación un extracto de lo parlamentado en la reunión:

- Usted: Es un gran logro para nuestro equipo que usted haya encontrado excelente el EDA. ¿Qué tiene en mente ahora?
- Mr. Lepin: Resulta que hace algún tiempo, mientras tomaba un mojito en una reunión de gerentes en Panamá, oí a un *chato* acerca de **LRMFP**, que es un modelo que permite personificar a los clientes a través de la farbicación de distintos atributos que describen a los clientes. Lo encontré es-tu-pendo ñatito. 
- Usted: Ehh bueno. Investigaremos acerca de este modelo y veremos lo que podemos hacer.

Por ende, su siguiente tarea es calcular LRMFP sobre cada cliente y luego hacer un análisis de las características generadas. Para esto, el área de ventas les entrega un nuevo archivo llamado online_retail_II_cleaned.pickle, quien posee los datos del DataFrame original limpios y listos para obtener las características solicitadas por Mr. Lepin.

In [2]:
df_retail = pd.read_pickle("data/online_retail_II_cleaned.pickle")
df_retail = df_retail.astype(
    {
        "Invoice": "category",
        "StockCode": "category",
        "Description": "category",
        "Description": str,
        "Customer ID": "category",
        "Country": "category"
    }
)
df_retail.head()
Out[2]:
Invoice StockCode Description Quantity InvoiceDate Price Customer ID Country
0 489434 85048 15CM CHRISTMAS GLASS BALL 20 LIGHTS 12 2009-12-01 07:45:00 6.95 13085.0 United Kingdom
1 489434 79323P PINK CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom
2 489434 79323W WHITE CHERRY LIGHTS 12 2009-12-01 07:45:00 6.75 13085.0 United Kingdom
3 489434 22041 RECORD FRAME 7" SINGLE SIZE 48 2009-12-01 07:45:00 2.10 13085.0 United Kingdom
4 489434 21232 STRAWBERRY CERAMIC TRINKET BOX 24 2009-12-01 07:45:00 1.25 13085.0 United Kingdom

1.2 Creación de nuevas Caracteristicas [2 Puntos]¶

Como ya se les comento, Mr. Lepin esta interesado en obtener las características LRMFP, para esto les señala que estas características se construyen en base a las siguientes definiciones:

  • Length (L): Intervalo de tiempo, en días, entre la primera y la última visita del cliente. Mientras mas grande sea el valor, mas fiel es el cliente.
  • Recency (R): Indica la actualidad de la interacción de un cliente con la empresa, y da información sobre la tendencia a repetir la compra. Se define como: $$Recency(n)=\dfrac{1}{n} \sum^n_{i=1} date\_diff(t_{fecha final}, t_{m-i+1})$$

    Donde $date\_diff$ representa la diferencia en días entre la fecha de finalización del periodo de observación ($t_{fecha final}$), y la fecha de una visita del cliente cercana a $t_{fecha final}$, $t_{m-i+1}; t_{m}$ es la última visita del cliente; y n es el número de visitas recientes del cliente consideradas.

  • Monetary (M): El término "monetario" se refiere a la cantidad media de dinero gastada por cada visita del cliente durante el período de observación y refleja la contribución del cliente a los ingresos de la empresa.

  • Frequency (F): Se refiere al número total de visitas del cliente durante el periodo de observación. Cuanto mayor sea la frecuencia, mayor será la fidelidad del cliente.

  • Periodicity (P): Representa si los clientes visitan las tiendas con regularidad.

$$Periodicity(n)=std(IVT_1, ..., IVT_n)$$

         Donde $IVT$ denota el tiempo entre visitas y n representa el número de valores de tiempo entre visitas de un cliente.

$$IVT_i=date\_diff(t_{i+1},t)$$

En base a las definiciones señaladas, diseñe una función que permita obtener las características LRMFP recibiendo un DataFrame como entrada. Para esto, no estará permitido el uso de iteradores, utilice todas las herramientas que les ofrece pandas para realizar esto.

Una referencia que le puede ser útil es el documento original en donde se propone este método.

Nota: Para la $fechafinal$ utilice la fecha máxima del dataset más 1 día.

Ejemplo de Resultado Esperado:

Customer ID Length Recency Frequency Monetary Periodicity
12346.0 294 67 46 -64.68 37.0
12347.0 37 3 71 1323.32 0.0
12349.0 327 43 107 2646.99 78.0
12352.0 16 11 18 343.80 0.0
12356.0 44 16 84 3562.25 12.0

Respuesta:

In [3]:
def custom_features(dataframe_in: pd.DataFrame, m: int = None) -> pd.DataFrame:
    """Retorna un frame con las características del modelo LRMFP.

    Parametros
    ----------
    dataframe_in: pd.DataFrame
        frame con los datos de retail.
    m : int, optional
        Número de visitas consideradas para calcular recencia.
        Si es None, entonces se calcula con el máximo entre 1 y
        la frecuencia mínima.

    Retorna
    -------
    df_out: pd.DataFrame
        frame con las características LRMFP."""
    dataframe_copy = dataframe_in[["Customer ID", "InvoiceDate"]].copy()
    dataframe_copy["prod"] = dataframe_in.Price*dataframe_in.Quantity
    df_out = pd.DataFrame(index=dataframe_in["Customer ID"].unique().sort_values())
    by_customer = dataframe_copy.sort_values(by="InvoiceDate").groupby("Customer ID")

    dates = by_customer.InvoiceDate
    df_out["Length"] = (dates.max() - dates.min()).dt.days

    freq = dates.count()
    if m is None:
        m = max(1, freq.min())
    last_date = df_retail.InvoiceDate.max() + datetime.timedelta(days=1)
    df_out["Recency"] = (last_date - by_customer.tail(m).groupby("Customer ID").InvoiceDate.mean()).dt.days

    df_out["Frecuency"] = freq
    df_out["Monetary"] = by_customer["prod"].mean()

    dataframe_copy["diff_time"] = dates.diff().dt.days
    df_out["Periodicity"] = dataframe_copy.groupby("Customer ID").diff_time.std()

    return df_out
In [4]:
custom_features(df_retail)
Out[4]:
Length Recency Frecuency Monetary Periodicity
12346.0 196 165 33 11.298788 21.724076
12347.0 37 3 71 18.638310 4.422346
12348.0 0 74 20 11.108000 0.000000
12349.0 181 43 102 26.187647 16.200990
12351.0 0 11 21 14.330000 0.000000
... ... ... ... ... ...
18283.0 275 18 217 2.854240 11.783701
18284.0 0 67 28 16.488571 0.000000
18285.0 0 296 12 35.583333 0.000000
18286.0 247 112 67 19.349701 30.403598
18287.0 188 18 85 27.596588 15.299909

4314 rows × 5 columns

1.3 Pipelines 👷¶

Finalmente Don Mora le pregunta si seria posible realizar un pipeline para realizar una segmentación de los clientes con los nuevos datos generados, a lo que usted responde que sí y propone la utilización de k-means para la segmentación.

A continuación siga los pasos requeridos para obtener la segmentación de clientes.

1.3.1 Estandarizar Caracteristicas [0.5 puntos]¶

Construya una clase llamada MinMax() utilizando BaseEstimator y TransformerMixin para realizar una transformación de cada una de las columnas de un DataFrame utilizando ColumnTransformer() más tarde (tome como referencia el siguiente enlace).

Para esto considere que Min-Max escaler queda dada por la ecuación:

$$MinMax = \dfrac{x-min(x)}{max(x) - min(x)}$$

Con esto buscamos que los valores que componen a las columnas se muevan en el rango de valores $[0, 1]$.

Respuesta:

In [5]:
class MinMax(BaseEstimator, TransformerMixin):
    """Escala y transforma cada característica tal que quede en el rango [0, 1]
    en el conjunto de entrenamiento. Si una feature es constante, se vuelve nula."""
    def fit(self, X, y=None):
        """Calcula el mínimo y máximo para ser usado en escalamiento.

        Parametros
        ----------
        X: array-like de forma (n_muestras, n_características)
            Los datos usados para calcular el mínimo y máximo por característica
            que se usan para escalar.
        y: None
            Ignorado, por compatibilidad.

        Retorna
        -------
        self: MinMax
            El escalador entrenado."""
        self.min_ = np.nanmin(X, axis=0)
        self.denominator_ = np.nanmax(X, axis=0) - self.min_
        self.denominator_[self.denominator_ == 0.] = 1.  # Evitando dividir por 0
        return self

    def transform(self, X):
        """Escala las características de X según el mínimo y máximo encontrado.

        Parametros
        ----------
        X: array-like de forma (n_muestras, n_características)
            Datos de entrada a ser transformados.

        Retorna
        -------
        Xt: array-like de forma (n_muestras, n_características)
            Datos transformados.

        Levanta
        -------
        NotFittedError: Si es que el modelo no ha llamado al método fit antes."""
        try:
            Xt = (X - self.min_)/self.denominator_
            return Xt
        except AttributeError:
            raise NotFittedError("Esta instancia de MinMax no está entrenada todavía. "
                                 "Llame a `fit` con los argumentos apropiados antes de usar este transformador")

1.3.2 T-SNE Pipeline [1.0 puntos]¶

Para comenzar introduciéndose en el uso de pipeline, decide probar realizando un pipeline enfocado en la reducción de dimensionalidad y así hacer no decepcionar a Mr. Lepin con la clusterización del modelo.

Configure un pipeline utilizando el algoritmo T-SNE sobre los datos LRMFP, donde, para la realización del pipeline considera los siguientes pasos:

  1. Como primer paso obtenga las características LRMFP desde el DataFrame df_retail_II_cleaned.pickle utilizando la función custom_features creada anteriormente, junto a FunctionTransformer(). Considere esto como el primer paso de su pipeline.
  2. En segundo lugar usando ColumnTransformer() aplique el MinxMax scaler creado por usted sobre todas las columnas generadas en el paso anterior.
  3. Finalmente, aplique un último paso donde obtiene las 2 componentes más relevantes utilizando el algoritmo T-sne de sckit-learn.

Tras aplicar las transformaciones sobre el dataset LRMFP, gráfique las componentes obtenidas en la reducción de dimensionalidad.

Aplicando la transformaciones

In [6]:
preprocessing = Pipeline(steps=[("feature_transformer", FunctionTransformer(custom_features)),
                                ("imputer", SimpleImputer(strategy="mean")),
                                ("scaler", MinMax()),  # No se necesita ColumnTransformer pues todo se escala
                               ],
                        )
dimensionality_reduction = Pipeline(steps=[("preprocessing", preprocessing),
                                           ("reducer", TSNE(n_components=2, random_state=0,
                                                            learning_rate="auto", init="random",
                                                           )
                                           ),
                                          ],
                                   )
X_reduced = pd.DataFrame(dimensionality_reduction.fit_transform(df_retail), columns=["x", "y"])
X_reduced
Out[6]:
x y
0 8.378868 -42.816170
1 -1.798583 21.489256
2 -71.841568 8.571382
3 21.666998 -18.503113
4 -9.306869 39.164833
... ... ...
4309 53.665306 -13.795135
4310 -68.078026 20.554010
4311 -33.021099 52.639996
4312 24.813828 -38.750008
4313 21.537745 -6.155716

4314 rows × 2 columns

Visualizando el espacio de dimensión reducida

In [7]:
fig = px.scatter(X_reduced, x="x", y="y")
fig.update_layout(title="Espacio de dimensión reducida con T-SNE")

1.3.3 Clustering¶

1.3.3.1 Método del Codo [1 puntos]¶

Utilizando la clase creada para escalamiento, aplique el método del codo para visualizar cual es el número de clusters que mejor se ajustan a los datos. Realice esto utilizando el algoritmo K-means dentro de un pipeline para un $k \in [1,20]$, donde k representa el número de clusters del k-means. Para la realización de esta sección y la próxima (1.3.3.2), considere los mismos pasos utilizados para el t-sne, pero permutando el algoritmo de reducción de dimensionalidad por k-means.

A través del grafico obtenido, comente y justifique que valor de k escogería para realizar el k-means.

Creando el pipeline de KMeans

In [8]:
def process_k_means(n_clusters: int) -> Pipeline:
    """Crea un pipeline con el mismo preprocesamiento anterior,
    agregando un paso de clustering KMeans

    Parametros
    ----------
    n_clusters: int
        Cantidad de clusters que busca KMeans

    Retorna
    -------
    clst: Pipeline
        Pipeline de clustering."""
    clst = Pipeline(steps=[("preprocessing", preprocessing),
                           ("cluster", KMeans(n_clusters=n_clusters, random_state=0)),
                          ],
                   ).fit(df_retail)
    return clst

Calculando las inercias

In [9]:
kvals = np.arange(1, 20)
clustering_models = [process_k_means(k) for k in kvals]
inertias = [clst.named_steps["cluster"].inertia_ for clst in clustering_models]

Visualizando el cambio de inercia.

In [10]:
fig = px.line(x=kvals, y=inertias)
fig.update_layout(title="Método del codo para determinar cantidad de clusters",
                  xaxis_title="Cantidad de clusters", yaxis_title="Inercia")
fig.show()

Se elige como valor óptimo $k=3$, pues a partir de este punto los beneficios ganados por aumentar los cluster disminuyen considerablemente (la inercia cae en 60, mientras que la reducción anterior fue de 150).

In [11]:
optimal_k = 3

1.3.3.2 Segmentación de Clientes con K-Means 🎁 [1 punto]¶

En base a la elección de k realizada en la sección anterior, utilice este valor escogido y entrene un modelo de K-means utilizando el mismo pipeline de scikit-learn utilizado anteriormente.

Una vez ajustado los datos, genere una tabla con los promedios (o medianas) para cada uno de los atributos, agrupando estos por el clúster que pertenecen. ¿Es posible observar agrupaciones coherentes?, ¿Qué tipo de clientes posee el retail?, Justifique su respuesta y no decepcione a Mr. Lepin.

Respuesta:

Generando agrupaciones por cluster

In [12]:
optimal_model = clustering_models[optimal_k - 1]
df_clusters = custom_features(df_retail)
labels = optimal_model.named_steps["cluster"].labels_
df_clusters["Cluster"] = labels
by_cluster = df_clusters.groupby("Cluster")

Mostrando los estadísticos agrupados:

In [13]:
agrupaciones = by_cluster.agg(["mean", "median"])
agrupaciones["cluster size"] = by_cluster.size()
agrupaciones
Out[13]:
Length Recency Frecuency Monetary Periodicity cluster size
mean median mean median mean median mean median mean median
Cluster
0 23.645464 0.0 251.627737 245.0 28.454640 19.0 53.258432 16.870000 3.940067 0.000000 959
1 277.280495 278.0 37.318156 25.0 167.035413 97.0 32.357707 18.077614 21.125143 15.854776 1779
2 39.241117 0.0 54.590736 46.0 48.542513 29.0 32.311214 16.788990 5.395593 0.397565 1576
  • El cluster 0 se caracteiza por tener un Recency mucho más alto que el resto. La media de su Monetary igualmente es alta, pero la mediana no, por lo que puede ser por clientes outlier. Estos son los que tienen mayor tendencia a repetir la compra.
  • El cluster 1 se caracteriza por tener alto Length, Frecuency y Periodicity: Son clientes de alta fidelidad que visitan con regularidad.
  • EL cluster 2 tiene un Recency intermedio a los otros dos clusters, y lo mismo con Frecuency. Son clientes que tienen mayor tendencia a repetir la compra que los de alta fidelidad, pero tienen menor frecuencia.

Respuesta Esperada:

Length Recency Frequency Monetary Periodicity
Cluster
0 258.8 45.2 76.1 1107.7 107.6 449
1 76.1 217.6 45.5 791.7 14.1 466
2 368.5 4.8 2715.0 226621.6 4.2 4
3 85.3 45.7 65.8 1047.0 10.5 987
4 347.2 15.9 1658.0 35829.3 8.0 25
5 298.0 29.8 183.8 3639.9 32.0 1188

1.3.3.3 Plot de K-Means 📈 [0.5 puntos]¶

Por último, Mr. Lepin, impaciente de no entender lo que usted intenta explicarle, le solicita que por favor muestre algún resultado "visual" de los grupos encontrados.

Para esto, grafique nuevamente las características encontradas usando T-SNE (no calcule de nuevo, simplemente utilice las proyecciones encontradas) y agregue las labels calculadas con kmeans como el argumento color.

Comente: ¿Se separan bien los distintos clusters en la visualización?

Respuesta:

In [14]:
X_reduced_with_cluster = X_reduced.copy()
X_reduced_with_cluster["Cluster"] = labels
X_reduced_with_cluster["Cluster"] = X_reduced_with_cluster.Cluster.astype("category")
fig = px.scatter(X_reduced_with_cluster, x="x", y="y", color="Cluster")
fig.update_layout(title="Espacio de dimensión reducida con T-SNE")

La separación es razonable en varias partes (secciones verdes aisladas), pero hay punto de muy baja separación. Esto puede ocurrir porque la reducción de dimensionalidad no capturó agrupaciones igual que KMeans.

Conclusión¶

Eso ha sido todo para el lab de hoy, recuerden que el laboratorio tiene un plazo de entrega de una semana y que los días de atraso no se pueden utilizar para entregas de lab solo para tareas. Cualquier duda del laboratorio, no duden en contactarnos por mail o U-cursos.

Gracias Totales!



Created in deepnote.com Created in Deepnote