<jupyter_start><jupyter_text>MASTER DATA SCIENCE & BUSINESS ANALYTICS**UNIVERSIDAD DE NEBRIJA** CASO PRÁCTICO FINAL INTRODUCCIÓNTrabajamos como científicos de datos para una empresa de retail que, debido al cambio en los hábitos de consumo de los clientes, está potenciando ampliamente el servicio de venta online. La empresa quiere realizar un modelo de aprendizaje automático para clasificar a los clientes en función de la probabilidad de generar ingresos al comprar en la web.El objetivo es realizar una serie de acciones específicas para los clientes que es más probable que hagan compras en la web.Para ello, la empresa ha ido recopilando datos con su herramienta de Google Analytics. Cuenta con datos de sesiones, cada una de ellas de un cliente distinto en un periodo de un año. INFORMACIÓN DE LOS DATOSEl conjunto de datos consta de diez atributos numéricos y ocho categóricos.El atributo `revenue` puede ser usado como la etiqueta de la clase.Las variables `Administrative`, `administrative duration`, `informational`, `informational duration`, `product related` y `product related duration` representan el número de diferentes tipos de páginas visitadas por el visitante en esa sesión y el tiempo total dedicado a cada una de estas categorías de páginas. Los valores de estas características se derivan de la información del URL de las páginas visitadas por el usuario y se actualizan en tiempo real cuando el usuario realiza una acción, por ejemplo, pasar de una página a otra.Las características `bounce rate`, `exit rate` y `page value` representan las métricas medidas por Google Analytics para cada página del sitio de comercio electrónico.- El valor de la característica `bounce rate` de una página web se refiere al porcentaje de visitantes que entran en el sitio desde esa página y luego salen (rebote) sin activar ninguna otra solicitud al servidor de análisis durante esa sesión.- El valor de la característica `exit rate` para una página web específica se calcula como para todas las visitas a la página, el porcentaje que fueron las últimas en la sesión.- La función `Page Value` representa el valor medio de una página web que un usuario ha visitado antes de completar una transacción de comercio electrónico.La característica de `special day` indica la cercanía de la hora de visita del sitio a un día especial específico (por ejemplo, el Día de la Madre, San Valentín) en el que es más probable que las sesiones finalicen con una transacción. El valor de este atributo se determina teniendo en cuenta la dinámica del comercio electrónico, como la duración entre la fecha del pedido y la fecha de entrega. Por ejemplo, para San Valentín, este valor toma un valor distinto de 0 entre el 2 y el 12 de febrero, 0 antes y después de esta fecha a menos que esté cerca de otro día especial, y su valor máximo de 1 el 8 de febrero.El conjunto de datos también incluye el sistema operativo, el navegador, la región, el tipo de tráfico, el tipo de visitante como visitante que regresa o como nuevo visitante, un valor booleano que indica si la fecha de la visita es de fin de semana, y el mes del año. TRABAJO A REALIZARLas siguientes son las tareas a realizar. 1. Realización de un análisis de las variables del dataset de Google Analytics como pueden ser histogramas, boxplots, etc. Cualquier otro análisis es bienvenido, siempre explicándolo y con un sentido de negocio.2. Tratamiento de los valores faltantes, outliers, etc., en caso de que los hubiese. Si hay valores con missings, habrá que eliminarlos con el método de Pandas llamado `Dropna().a`.3. Tratamiento de categóricas, pasándolas a numéricas por medio de _dummies_, mapeándolas o utilizando un `label encoder`. Hay que justificar las operaciones que se realizan.4. Si existe alguna variable que se necesite borrar, habrá que borrara y justificarlo.5. Estandarizar los datos.6. Dividir los datos en train y en test. Con los datos de train se pretende ajustar modelos con `CrossValidation` y `GridSearch`.    - Utilizar un modelo lineal. Entre los modelos lineales están las regresiones logísticas, las regresiones lineales, etc.    - Utilizar un modelo de redes neuronales.    - Utilizar cualquier otro modelo de clasificación.7. Optimizar algún parámetro de cada modelo utilizando `CrossValidation` y `GridSearch`, o de la forma que se estime oportuna, siempre justificándolo.8. Elegir el mejor modelo de los tres según la métrica ROC en `CrossValidation`. Predecir Test y obtener una métrica estimada.9. Umbralizar las probabilidades utilizando el umbral que maximice el área bajo la curva ROC.10. El entregable final será un Jupyter Notebook en el que se realicen todos los análisis y los modelos.**Anotación**Lo que se pide es un modelo _end-to-end_ como los vistos en prácticas en el módulo. En este caso, hay que aplicar tres modelos y escoger el mejor, pero la parte previa es común a los tres.Se tendrán en cuenta la estructura del código, las interpretaciones y las justificaciones.El esquema que se pone en las tareas es un esquema general. Si se necesita hacer alguna tarea, se puede llevar a cabo. También es posible aplicar cualquier otro algoritmo visto en el módulo, siempre justificando y haciendo las cosas con un sentido.  SOLUCIÓN PROPUESTA  Lectura del Juego de DatosComenzamos el trabajo con las tareas más esenciales de cualquier juego de dato, la lectura y limpieza del mismo para poder trabajar el análisis EDA con la certeza que no estamos asumiendo tendencias de los datos resultados de artefactos de las impureza de los mismos (<NAME>. y <NAME>., 2017).<jupyter_code>import pandas as pd
raw_data = pd.read_csv("online_shoppers_intention.csv")
raw_data.head()
raw_data.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
RangeIndex: 12330 entries, 0 to 12329
Data columns (total 18 columns):
 #   Column                   Non-Null Count  Dtype  
---  ------                   --------------  -----  
 0   Administrative           12316 non-null  float64
 1   Administrative_Duration  12316 non-null  float64
 2   Informational            12316 non-null  float64
 3   Informational_Duration   12316 non-null  float64
 4   ProductRelated           12316 non-null  float64
 5   ProductRelated_Duration  12316 non-null  float64
 6   BounceRates              12316 non-null  float64
 7   ExitRates                12316 non-null  float64
 8   PageValues               12330 non-null  float64
 9   SpecialDay               12330 non-null  float64
 10  Month                    12330 non-null  object 
 11  OperatingSystems         12330 non-null  int64  
 12  Browser                  12330 non-null  int64  
 13  Region                   12330 non-null  int64  
 14  TrafficType           [...]<jupyter_text>Antes de revisar los tipos de columna y transformaciones necesarias, nos asesoramos que no existan datos faltantes.<jupyter_code>raw_data.isnull().sum()<jupyter_output><empty_output><jupyter_text>Existen 14 registros faltantes en varias de las columnas. El número es el mismo en todas, por lo que puede que existan 14 filas con datos mal registrados. Para un juego de datos de 12,330 filas es una razón bastante pequeña, y si pudieramos eliminarlos ya, nos ahorraría mucho trabajo de imputación de datos innecesario. Revisemos las 14 filas bajo escrutinio.<jupyter_code># Código extraído de https://stackoverflow.com/questions/30447083/python-pandas-return-only-those-rows-which-have-missing-values
null_data = raw_data[raw_data.isnull().any(axis=1)]
null_data<jupyter_output><empty_output><jupyter_text>Los 14 registros parecen estar relativamente juntos, y pudieramos especular que fue un usuario con algún tipo de software para bloquear cookies o trackers. Dado que solo afecta al 0.11% de los registros, procedemos a depurar el juego de datos.<jupyter_code>raw_data.drop(labels=null_data.index, inplace=True, axis=0)
raw_data.isnull().sum()<jupyter_output><empty_output><jupyter_text>Nos interesa ver los rangos preliminares de la información cuantitativa, para tomar una idea qué más necesita cambio en el juego de datos.<jupyter_code>raw_data.describe()<jupyter_output><empty_output><jupyter_text>Definitivamente el juego de datos no tiene volumen pero tiene variedad de columnas con diferentes tipos de datos que debemos profundizar un poco antes de seguir. Lo que nos interesa más aquí es el tipo de datos, rango, y que transformación sería la mejor antes de pasar al EDA.   Revisión de Tipo de PáginasEl tipo de páginas debe ser una variable importante, ya que tiene su propia columna segregada por _administrativa_, _informativa_ y _producto_. Nos interesa ver si son valores continuos o discretos, si los debemos tratar como valores de estudio o etiquetas de estudio.<jupyter_code>web_pages = ["Administrative", 'Administrative_Duration', 'Informational', 'Informational_Duration', 'ProductRelated', 'ProductRelated_Duration']
for pages in web_pages:
    print(u'Page ',pages,' - ', raw_data[pages].nunique(),'\n')<jupyter_output>Page  Administrative  -  27 

Page  Administrative_Duration  -  3336 

Page  Informational  -  17 

Page  Informational_Duration  -  1259 

Page  ProductRelated  -  311 

Page  ProductRelated_Duration  -  9552<jupyter_text>En conclusión, hay 27 páginas administrativas, 17 de información, pero muchísimas de producto (311 para ser exactos). No hay mucho más que podamos inferir salvo que las páginas debieran pensarse como variables categóricas ordenadas (ordinales), por lo que preferimos moldearlas (cast) como enteros (Sharma, M., 2020).<jupyter_code># Código extraído de https://www.linkedin.com/pulse/change-data-type-columns-pandas-mohit-sharma/
raw_data[['Administrative', 'Informational', 'ProductRelated']] = raw_data[['Administrative', 'Informational', 'ProductRelated']].apply(pd.to_numeric, downcast='integer')
raw_data.info()<jupyter_output><class 'pandas.core.frame.DataFrame'>
Int64Index: 12316 entries, 0 to 12329
Data columns (total 18 columns):
 #   Column                   Non-Null Count  Dtype  
---  ------                   --------------  -----  
 0   Administrative           12316 non-null  int8   
 1   Administrative_Duration  12316 non-null  float64
 2   Informational            12316 non-null  int8   
 3   Informational_Duration   12316 non-null  float64
 4   ProductRelated           12316 non-null  int16  
 5   ProductRelated_Duration  12316 non-null  float64
 6   BounceRates              12316 non-null  float64
 7   ExitRates                12316 non-null  float64
 8   PageValues               12316 non-null  float64
 9   SpecialDay               12316 non-null  float64
 10  Month                    12316 non-null  object 
 11  OperatingSystems         12316 non-null  int64  
 12  Browser                  12316 non-null  int64  
 13  Region                   12316 non-null  int64  
 14  TrafficType           [...]<jupyter_text>Mapeando Variables Categóricas a NuméricasExisten dos variables adicionales que debemos mapear de categóricas a numéricas. Una es `Month`, o el mes del año, que es bastante sencilla de interpretar, pero la siguiente es `Weekend`, un indicador de si es o no fin de semana, y `VisitorType`, que nos gustaría verificar antes de aplicar un mapeo.<jupyter_code>raw_data['VisitorType'].unique()<jupyter_output><empty_output><jupyter_text>No tiene mucho sentido el orden en las etiquetas, por lo que podemos establecer uno cualquiera en un mapa antes de proceder con la transformación (Keen, B., 2017)<jupyter_code># Código extraído de https://benalexkeen.com/mapping-categorical-data-in-pandas/
raw_data['VisitorType'] = raw_data['VisitorType'].astype("category").cat.codes<jupyter_output><empty_output><jupyter_text>Procedemos a verificar los valores de la columna `Weekend`.<jupyter_code>raw_data['Weekend'].unique()<jupyter_output><empty_output><jupyter_text>Con dos valores del tipo _Boolean_, el mapeo será directo como 0 si es falso y 1 si es verdadero.<jupyter_code>raw_data['Weekend'] = raw_data['Weekend'].astype("category").cat.codes
raw_data['Weekend'].unique()<jupyter_output><empty_output><jupyter_text>Antes de proceder a crear un mapa de los meses, revisemos la validez de los datos en la columna mes, ya que sin conocer las etiquetas puede ser difícil mapear las mismas.<jupyter_code>raw_data['Month'].unique()<jupyter_output><empty_output><jupyter_text>Los valores únicos del atributo mes tienen formatos poco consistentes. Tendremos que crear un diccionario a mano para etiquetarlos como valores numéricos de forma segura (Exhenn, B., 2017). Aquí lo más importante en respecto al código es que `astype` en **Pandas** cambió hace dos años y los parámetros antiguos, sobre todo `ordered=True` ya no funcionan. Lo conveniente es utilizar `CategoricalDType` si el científico de datos le gusta crear mapas a mano (StackOverflow, 2018). En nuestro caso en particular utilizaremos un diccionario sencillo de mapeo.<jupyter_code># NOTA: astype cambio en Pandas hace poco, utilizar CategoricalDType 
meses_ordenados = {"Month" :
                   {
                       'Jan' : 1,
                       'Feb' : 2,
                       'Mar' : 3,
                       'Apr' : 4,
                       'May' : 5,
                       'June' : 6,
                       'Jul' : 7,
                       'Aug' : 8,
                       'Sep' : 9,
                       'Oct' : 10,
                       'Nov' : 11,
                       'Dec' : 12}}

raw_data = raw_data.replace(meses_ordenados)
raw_data.describe()<jupyter_output><empty_output><jupyter_text>Estandarización del Juego de DatosEs probable que para facilitar la tarea de clasificadores y diferentes modelos de aprendizaje automatizado sea conveniente estandarizar ciertos datos. Ahora bien, en nuestro juego de datos muchas columnas son numéricas pero sus valores son etiquetas numéricas de datos categóricos (por ejemplo `Browser` es solo el tipo de explorador que usa el usuario, no una valor contínuo de medición). Solo estaremos estandarizando aquellos datos que tengan sentido, para ser precisos:- Administrative_Duration- Informational_Duration- ProductRelated_Duration- BounceRates- ExitRates- PageValues- SpecialDay<jupyter_code>from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
lista = ['Administrative_Duration', 'Informational_Duration', 
         'ProductRelated_Duration', 'BounceRates', 'ExitRates', 'PageValues', 'SpecialDay']

scaled_data = raw_data.copy()
scaled_data[lista] = StandardScaler().fit_transform(raw_data[lista])
scaled_data[lista].describe()<jupyter_output><empty_output><jupyter_text>Análisis Explorativo Data (EDA)En nuestro caso en particular, la variable `Revenue` es el determinante de un caso exitoso o no de uso. Recordemos los dos puntos críticos del análisis.1. La empresa quiere realizar un modelo de aprendizaje automático para clasificar a los clientes en función de la probabilidad de generar ingresos al comprar en la web.2. Para ello, la empresa ha ido recopilando datos con su herramienta de Google Analytics. Cuenta con datos de sesiones, cada una de ellas de un cliente distinto en un periodo de un año. ¿Qué variables de todas las disponibles influyen más en un cliente para finalizar una compra? ¿Mientras más productos se ven, más aumenta las ganas de comprar, porque definitivamente aumenta el _engagement_? ¿Son las épocas de fiesta momentos donde se dispara la venta digital? ¿Tiene el tipo de sistema operativo realmente alguna influencia en la decisión de compra?  De acuerdo a los estudios preliminares del comienzo del comercio digital, los cuatro factores más importantes en la eficacia de las ventas digitales (Udo, G. y <NAME>., 2001) son:1. tiempo de descarga2. navegación fácil3. uso de imágenes4. interactividad del sitioUn estudio mucho más moderno y técnico (<NAME>., <NAME>., y <NAME>., 2018) utiliza múltiples herramientas estadísticas (pero no de aprendizaje automático) para determinar que los factores principales son siete:1. precio2. disponibilidad3. muestra social4. escasez5. información del producto6. condiciones de venta7. redes socialesDe acuerdo a la consultora de E-Commerce SnapRetail, los factores también son 7 (SnapRetail, 2017), pero difieren en algunos casos. 1. envío gratis2. velocidad de carga del sitio3. uso incremental de mobil (y disponibilidad mobil)4. descuentos y ofertas5. e-mail marketing6. mercadeo por redes sociales7. experience de compra personalizadaUn cuarto estudio que utilizó redes neuronales y la base de datos del sitio en China Tabao.com para determinar factores de éxito, cita cuatro factores principales del exito de un sitio web comercial (Zao,Z. et al, 2019):1. recomendaciones en linea2. sistema de curación de recomendaciones3. mercadeo promocional en linea4. garantías del vendedor Cómo científicos de datos podemos vernos extrañados que los estudios académicos que determinan los factores de éxito arrojan variables muy diferentes de las que nos provee el juego de datos. Habiendo dicho esto, existen bloques de hipótesis de trabajo interesantes que perseguir. * La primera tiene que ver con la información del producto y experiencia de compra. Conocemos de casa usuario el uso de las diferentes páginas administrativas, de información y producto, y si hubo o no compra. Este bloque amerita exploración visual para descubrir algún patrón. Este item también se relaciona con la curación del contenido de producto y recomendaciones que mencionan los estudios. * Los factores de promoción pudieran estar algo relacionados con fechas especiales, meses y cercacía con los fines de semana. Este segundo ángulo merece atención. * Aunque no lo mencionan ninguno de los estudios, los valores de _Bounce Rate_, _Exit Rate_ y _Page Value_ merecen atención también. Somos algos escépticos del _Page Value_ ya que mide una variable en relación al éxito de venta, por lo tanto debiera tener una alta correlación con el valor a predecir, lo que sería un resultado tautológico.    Cuantificado la Etiqueta de IngresosNo sabemos cuanto ingreso tuvo la empresa, pero sabemos que hay un número finito de clientes que generaron ingresos porque la variable `Revenue` es `True`. Veamos la relación de forma visual.<jupyter_code>pd.crosstab(index=raw_data['Revenue'], columns='count')<jupyter_output><empty_output><jupyter_text>A primera vista vemos que las etiquetas de clasificación tienen desbalance, con solo un 18% de las mismas representando uno de los dos estados posibles. ¿Se puede considerar esto una clasificación desbalanceada? <NAME>, citando a <NAME>, especifica que un desbalance ocurre cuando las proporciones de etiquetas se encuentran en rangos de 1:4 a 1:100, pero que en la vida real en los sistemas de detección de fraude o quimioinformática los problemas de desbalance tienen un rango actual de 1:1,000 a 1:5,000 (Brownlee, J., 2019). Adoptamos la postura entonces que no debemos preocuparnos por el desbalance de clases en la busqueda del mejor modelo clasificador.   Analizando Información del Producto y Experiencia de CompraPartamos de la idea que si los clientes pasan bastante tiempo en el sitio web, es probable que el aumento de _engagement_ impacte en la decisión de compra. Midamos entonces el tiempo agregado en cada tipo de página y como incide en la compra o no.<jupyter_code>import matplotlib.pyplot as plt
import seaborn as sns

#fig.suptitle('Análisis de Duración por Segmento e Intención de Compra')

sns.catplot(x="Administrative_Duration", y="Revenue", kind="box", 
            orient="h", height=3, aspect=4, data=raw_data)
sns.catplot(x="Informational_Duration", y="Revenue", kind="box", 
            orient="h", height=3, aspect=4, data=raw_data)
sns.catplot(x="ProductRelated_Duration", y="Revenue", kind="box", 
            orient="h", height=3, aspect=4, data=raw_data)
plt.show()<jupyter_output><empty_output><jupyter_text>La información no arroja mucho para concluir más allá de que mientrás más permanezca una persona enganchada en el sitio, mayor es la probabilidad que compre algo. La duración de cada tipo de página esta normalizada, pero ha de notarse que para los casos negativos (no hubo compra) siempre se encuentra a la izquierda del valor 0, que en nuestro caso en la media del tiempo incurrido en cada segmento. Corolario, si los clientes no pasan por lo menos el tiempo medio en un segmento de página en particular, es más probable que no compren.   Analizando Factores de Promoción Una hipótesis muy factible es que ciertas fechas tienen mayor impacto en la compra en linea. La primera fecha a analizar es el impacto de los fines de semana.<jupyter_code>tab = pd.crosstab(index=raw_data['Revenue'], columns=raw_data['Weekend'])
round(tab / len(raw_data.index),2)<jupyter_output><empty_output><jupyter_text>Solo un 4% de la gente compra el fin de semana, y el 11% en los días de semana. La proporción de gente que no compra los fines de semana parece menor que la que no compra los días laborales sin embargo. ¿Cuál será la distribución de frecuencia en meses?<jupyter_code>tab = pd.crosstab(index=raw_data['Revenue'], columns=raw_data['Month'])
round(tab / len(raw_data.index),2)<jupyter_output><empty_output><jupyter_text>Noviembre es el mes donde se dispara la venta, con 6% del total. Mayo viene de segundo lugar con 3%. ¿Existe acaso alguna relación de los meses con fechas especiales?<jupyter_code>tab = pd.crosstab(index=raw_data['SpecialDay'], columns=raw_data['Month'])
round(tab / len(raw_data.index),2)<jupyter_output><empty_output><jupyter_text>El haber normalizado la data en el momento de explorar los datos es una ventaja y desventaja a la vez. En esta matriz es difícil analizar si las fechas especiales inciden mucho o poco en los meses del juego de datos. Quizás si solo nos limitamos a relacionar fechas especiales con resultados de ingresos la relación sea más sencilla de visualizar.<jupyter_code>tab = pd.crosstab(index=raw_data['Revenue'], columns=raw_data['SpecialDay'])
round(tab / len(raw_data.index),2)<jupyter_output><empty_output><jupyter_text>Un 15% de los ingresos parece estar muy relacionado con los días especiales, ya que la variable normalizada en -0,30 es muy cercana a la media (mu en cero) de la distribución de valores no normalizados que debieran tender a la medida central.   Analizando Bounce Rate, Exit Rate y PageValues El autor <NAME> describe el _bounce rate_ como el número promedio de rebotes para todas las páginas de un sitio dividido por el total de visitas en todas esas páginas durante el mismo período (Patel, N., 2020). Un _rebote_ ocurre cuando alguien visita el sitio, y se aleja sin más interacción. Google considera que ha habido un rebote si por lo menos el consumidor visitó el sitio y una página adicional, luego decidió cerrarla o abandonar el sitio. Según el autor, el _bounce rate_ es el cuarto factor más importante en la optimización de opciones de búsqueda. Sería interesante ver como varía la distribución del mismo en nuestro juego de datos.<jupyter_code>sns.set(rc={'figure.figsize':(8,6)})
sns.histplot(data=raw_data, x="BounceRates")
raw_data['BounceRates'].describe()<jupyter_output><empty_output><jupyter_text>Según Patel, el _bounce rate_ para la industria del retail es de 20% a 40% (Patel, N., 2020). Revisemos como se dispersan nuestros propios números que parecen ser menores.<jupyter_code>sns.set(rc={'figure.figsize':(8,6)})
sns.boxplot(data=raw_data, x="BounceRates")<jupyter_output><empty_output><jupyter_text>Es muy bueno verificar que los _bounce rates_ son bajos, la gente tiende a abandonar menos el sitio, sin que esto esté directamente relacionado a el hecho que produzca o no una venta. ¿Podemos comparar la dispersión del _bounce rate_ en los casos que se hizo una venta y en los que no?<jupyter_code>sns.set(rc={'figure.figsize':(8,6)})
sns.boxplot(data=raw_data, x= "Revenue", y = "BounceRates")<jupyter_output><empty_output><jupyter_text>Definitivamente los valores de _bounce rate_ son mucho más bajos cuando se realiza una compra (lo que no deja de tener cierto sentido común, ya que al hacer una compra se descarta la posibilidad de abandonar el sitio).  ¿Cuál es la diferencia entre _exit rate_ y _bounce rate_? El _exit rate_ es la métrica que define el número de visitantes que abandoron el sitio desde una página en particular. Ende, este número se calcula como la tasa de número de salidas como numerador y número de views para una página específica como denominador (Page, R., 2019). La mejor forma de visualizar esto es a través de un _heatmap_ con la denominación de página y su porcentaje de _exit rate_ como valor particular. Este indicador parece mucho más relacionado con el comportamiento de las páginas que de los consumidores, y quizás no amerite nuestro interés profundo en la busqueda de un clasificador óptimo.  Finalmente llegamos al indicador de _Page Value_. El valor de _Page Value_ es el valor promedio que un usuario visitó antes de completar una transacción o conversión en un sitio comercial. La fórmula específica es el cociente de la suma del valor total de la página más el valor de la transacción sobre el número de vistas únicas (no repetidas) de dicha página. Este número si nos interesa, ya que hay una relación real entre el _Page Value_ y una transacción.<jupyter_code>sns.set(rc={'figure.figsize':(8,6)})
sns.boxplot(y="PageValues", x="Revenue", data = raw_data)<jupyter_output><empty_output><jupyter_text>La relación directa es altamente visible en la gráfica de bigotes. Los usuarios con compras efectuadas tienen un valor de página que varía en el IRQ entre 0 a 90, con una zona segura que oscila entre 10 a 40.  Con una mejor idea de la estructura del juego de datos, y las relaciones potenciales entre variables para clasificar una transacción exitosa, pasamos al entrenamiento de datos.   Entrenamiento de DatosEn la siguiente sección procedemos a revisar tres técnicas diferentes de aprendizaje automático para clasificación de si los consumidores tienen más alta probabilidad de hacer compras por web. Estas son:1. Regresión logística2. Red Neuronal (Perceptrón multicapas)3. KNN Para cada uno de estos utilizaremos el mismo juego de datos de entrenamiento y validación.<jupyter_code>from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score
import mglearn

X = scaled_data.loc[:, raw_data.columns != 'Revenue']
y = scaled_data['Revenue']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=2021, stratify=y)
print(u'Dimensiones en train \n-X:{}\n-Y{}'.format(X_train.shape, y_train.shape))
print(u'Dimensiones en test  \n-X:{}\n-Y{}'.format(X_test.shape, y_test.shape))<jupyter_output>Dimensiones en train 
-X:(8621, 17)
-Y(8621,)
Dimensiones en test  
-X:(3695, 17)
-Y(3695,)<jupyter_text>En los ejemplos de diferentes modelos de entrenamiento de datos, utilizaremos siempre validación cruzada para entrenar y validar el modelo y los hiperparámetros, y el juego de test para evaluar los resultados de precisión y area bajo la curva.   Clasificador con Regresión LogísticaLos clasificadores con regresión logística han sido métodos comprobados de aprendizaje automático por decadas, y es conveniente comenzar por este método sencillo y medir los resultados antes de avanzar a otro más potente (<NAME>., 2015). Veamos los resultados del entrenamiento de un clasificador con regresión logística.<jupyter_code># Código original https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegressionCV.html
from sklearn.linear_model import LogisticRegressionCV
clf_rl = LogisticRegressionCV(cv=5, random_state=0, max_iter=500, penalty='l2').fit(X_train, y_train)
puntaje_rl = clf_rl.score(X_train, y_train)
print(u'Puntaje clasificador regresión logística: ',round(puntaje_rl,2))<jupyter_output>Puntaje clasificador regresión logística:  0.88<jupyter_text>Para el modelo de regresión logística se utilizó la librería `LogisticRegresionCV` que automáticamente incluye validación cruzada de los datos. El puntaje del clasificador que obtenemos es 0.88. Sin embargo los primeros intentos no pudieron converger bien hasta que ajustamos el parámetro de `max_iter=500` para aumentar el ciclo de iteraciones. Ajustar el número de validaciones cruzadas de 10 a 5 tuvo una mejora marginal en el puntaje del clasificador (0.882 a 0.883). Este es el puntaje de entrenamiento, pero debemos revisar las métricas del error fuera de muestra con el juego de datos de evaluación.<jupyter_code># Predecir resultados en el conjunto de evaluación
y_test_pred = clf_rl.predict(X_test)
y_test_prob = clf_rl.predict_proba(X_test)

# Crear métricas
auc = roc_auc_score(y_test, y_test_prob[:, 1])
print("- Precision: ", round(precision_score(y_test, y_test_pred),2))
print("- Recall : ", round(recall_score(y_test, y_test_pred),2))
print("- F-Score : ", round(f1_score(y_test, y_test_pred), 2))
print("- AUC: ", round(auc, 2))
# Curva AUC
from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score, roc_curve
import matplotlib.pyplot as plt
fpr, tpr, thrs = roc_curve(y_test, y_test_prob[:, 1])
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1], "r--")
plt.title("ROC")
plt.xlabel("Falsos Positivos")
plt.ylabel("Verdaderos Positivos")
plt.show()<jupyter_output><empty_output><jupyter_text>En lineas generales, el clasificador usando regresión lineal tuvo un puntaje bueno. El uso de la librería `LogisticRegresionCV` agiliza la validación cruzada, pero los hiperparámetros se deben cambiar en las llamadas con cada ejecución de un ciclo de entrenamiento.   Clasificador con Redes NeuronalesEl poder de las redes neuronales viene condicionado al uso correcto y ajuste de múltiples hiperparámetros, y la precisión del clasificador puede verse comprometida por el tiempo invertido en entrenar el modelo versus el uso de uno menos preciso pero ciertamente más rápido y menos costoso en ciclos computacionales. La documentación de la librería **SciKit** es extenso pero no muy clara en la aplicación correcta del código de automatización de búsqueda de parámetros con **GridSearchCV**. Por suerte existe un artículo claro sobre los pasos a seguir para el ajuste de hipérparámetros en una red neuronal perceptrón multicapas usando GridSearchCV por los científicos de datos <NAME> y <NAME> que dividen la estructura del código en cinco secciones (<NAME>. y <NAME>., 2019):1. Elección del clasificador2. Definición de un diccionario de hiperparámetros3. Entrenamiento4. Obtener mejor combinación de parámetros5. Evaluación en el juego de testPara el caso en cuestión, se han seleccionado parámetros específicos en la red neuronal con el objetivo de maximizar el puntaje del modelo. `max_iter`: el número de ciclos de iteracción para que el modelo converga. Luego de la experiencia del modelo de regresión logística, se utilizó un vector de 100, 200 y 300 ciclos (NOTA: la primera vez se utilizó 500 ciclos pero el entrenamiento sobrecalentó el computador y se tuvo que abortar el proceso cuando la temperatura llegó a 97 grados).`hidden_layer_sizes`: para la topología propia de la red, se utilizó tres combinaciones de capaz ocultas. `activation`: las funciones de activación, se seleccionó ReLU (activación rectificada lineal) y _tanh_ (tangente hiperbólica), que son funciones relativamente comunes en problemas de clasificación. `solver`: este hiperparámetro incluye los algoritmos de optimización. Se eligieron Adam que tiene un uso muy eficiente de ciclos computacionales, y _sgd_ que utiliza decenso gradual estocástico (también uno de los algorítmos que más años tiene en la ciencia del aprendizaje automatizado). `alpha`: término que define la penalización L2 (impacta los pesos más grandes para regularizar como converge el modelo). Se utilizó dos tasas, una más consevadora y una más aggresiva. `learning rate`: tasa de aprendizaje. Se optó por opciones para una tasa constante y una adaptable (que mantiene la misma tasa de aprendizaje mientras la función de pérdida se minimice).<jupyter_code>from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report

mlp = MLPClassifier()

parameters_cv = {
    'max_iter' :[100,200,300],
    'hidden_layer_sizes': [(16,16,16), (16,32,24), (16,24,8)],
    'activation': ['tanh', 'relu'],
    'solver': ['sgd', 'adam'],
    'alpha': [0.0001, 0.05],
    'learning_rate': ['constant','adaptive'],
}

clf_mlpc = GridSearchCV(mlp, parameters_cv, n_jobs = 4, cv=3)
clf_mlpc.fit(X_train, y_train)
puntaje_mlpc = clf_mlpc.score(X_test,y_test)
print("Precisión (en juego de test) = ", round(puntaje_mlpc,2))
y_true, y_pred = y_test, clf_mlpc.predict(X_test)
print(classification_report(y_true, y_pred))
# Predecir resultados en el conjunto de evaluación
y_test_pred = clf_mlpc.predict(X_test)
y_test_prob = clf_mlpc.predict_proba(X_test)

# Crear métricas
auc = roc_auc_score(y_test, y_test_prob[:, 1])
print("- Precision: ", round(precision_score(y_test, y_test_pred),2))
print("- Recall : ", round(recall_score(y_test, y_test_pred),2))
print("- F-Score : ", round(f1_score(y_test, y_test_pred), 2))
print("- AUC: ", round(auc, 2))
# Curva AUC
import matplotlib.pyplot as plt
from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score, roc_curve
fpr, tpr, thrs = roc_curve(y_test, y_test_prob[:, 1])
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1], "r--")
plt.title("ROC")
plt.xlabel("Falsos Positivos")
plt.ylabel("Verdaderos Positivos")
plt.show()<jupyter_output><empty_output><jupyter_text>Lo bueno de haber utilizado `GridSearchCV` es poder extraer el mejor modelo de la búsqueda.<jupyter_code># Mejor juego de parámetros
print('Mejor selección de hiperparámetros:\n', clf_mlpc.best_params_)<jupyter_output>Mejor selección de hiperparámetros:
 {'activation': 'relu', 'alpha': 0.0001, 'hidden_layer_sizes': (16, 24, 8), 'learning_rate': 'adaptive', 'max_iter': 200, 'solver': 'adam'}<jupyter_text>Como demuestra el poder de `GridSearchCV` el mejor juego de hiperparámetros es:* `relu` = activación rectificada lineal.* `alpha` = 0.0001* `hidden_layer_sizes` = topología de capas ocultas 16-24-8.* `learning_rate` = tasa de aprendizaje **constante*** `max_iter` = número máximo de iteraciones 300* `solver` = algoritmo de solucion **adam**  Clasificador con K-Nearest NeighborsEl clasificador KNN es muy usado en casos de negocios, y agnóstico a valores extremos, que se hicieron evidente en los análisis visuales de la dispersión de algunas variables (<NAME>., 2015) Como método de aprendizaje automático es algo caro en ciclos computacionales, pero dado que el juego de datos no es tan extenso, debemos obtener buenos resultados (<NAME>., 2018)<jupyter_code># Código extraído de https://discuss.analyticsvidhya.com/t/grid-search-related-to-machine-learning-knn-algorithm/69126/2
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV

knn = KNeighborsClassifier()

parameters = {'n_neighbors':[4,5,6,7,8,9,10],
              'leaf_size':[1,3,5,7],
              'algorithm':['auto', 'kd_tree'],
              'n_jobs':[-1]}

model = GridSearchCV(knn, param_grid=parameters)
clf_knn = model.fit(X_train,y_train)
puntaje_knn = clf_knn.score(X_train, y_train)
print(u'Puntaje clasificador KNN: ',round(puntaje_knn,2))<jupyter_output>Puntaje clasificador KNN:  0.87<jupyter_text>El modelo tuvo un puntake ligeramente por debajo de nuestro clasificador de regresión logística. Pongamos los resultados a escrutinio con el juego de datos de evaluación.<jupyter_code># Predecir resultados en el conjunto de evaluación
y_test_pred = clf_knn.predict(X_test)
y_test_prob = clf_knn.predict_proba(X_test)

# Crear métricas
auc = roc_auc_score(y_test, y_test_prob[:, 1])
print("- Precision: ", round(precision_score(y_test, y_test_pred),2))
print("- Recall : ", round(recall_score(y_test, y_test_pred),2))
print("- F-Score : ", round(f1_score(y_test, y_test_pred), 2))
print("- AUC: ", round(auc, 2))
# Curva AUC
from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score, roc_curve
fpr, tpr, thrs = roc_curve(y_test, y_test_prob[:, 1])
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1], "r--")
plt.title("ROC")
plt.xlabel("Falsos Positivos")
plt.ylabel("Verdaderos Positivos")
plt.show()<jupyter_output><empty_output><jupyter_text>Lamentablemente nuestro modelo KNN tiene un puntaje mucho más bajo cuando se mide contra el juego de test, solamente cubre un 0.71 del area bajo la curva.   Seleccionando el Mejor ModeloHasta este punto hemos utilizado tres clasificadores diferentes, regresión logística, perceptrón multicapa, y KNN, con el afán de dislumbrar cual tiene el mejor resultado. Pudieramos comparar los puntajes de tasa de aciertos en una tabla, y tomar la mejor. Pero lo más interesante es crear un diseño que utilice `GridSearchCV` para iterar por los tres clasificadores diferentes y darnos los resultados y parámetros del mejor. La bibliografía no es muy clara al respecto, pero mucho depende del uso de la librería `Pipeline`.<jupyter_code># Importar librerías
# NOTA: Código propio. El uso de esta técnica está muy mal explicado
# en el internet y donde se explica, con demasiados errores o funciones
# que ya están DEPRECATED. Hay mucho espacio para explicar esto mejor
# en algún lugar de la universidad.

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
np.random.seed(7556014)

# Crear tubería de uso
pipe = Pipeline([("classifier", KNeighborsClassifier())])

# CUIDADO: Los underscore después de classifier pero antes del parámetro
# son dos: como en classifier__penalty

search_space = [
    {"classifier" : [LogisticRegression()],
     "classifier__penalty" : ["l1","l2"],
    "classifier__max_iter" : [500, 1000]},
    
     {"classifier" : [MLPClassifier()],
     "classifier__max_iter" :[300,400,500],
     "classifier__hidden_layer_sizes": [(16,16,16), (16,32,24), (16,24,8)],
     "classifier__activation": ['tanh', 'relu'],
     "classifier__solver": ['sgd', 'adam'],
     "classifier__alpha": [0.0001, 0.05],
     "classifier__learning_rate": ['constant','adaptive']},
    
    {"classifier" : [KNeighborsClassifier()],
     "classifier__n_neighbors" : [4,5,6,7,8,9,10],
     "classifier__leaf_size" : [1,3,5,7],
     "classifier__algorithm" : ['auto', 'kd_tree']}]

# Crear un clasificador con GridSearchCV
clf = GridSearchCV(pipe, search_space, cv=5, verbose=0, n_jobs=4)   
# Buscar el mejor modelo de todos los que hay en el pipeline
mejor_modelo = clf.fit(X_train, y_train)
print(mejor_modelo.best_estimator_.get_params()["classifier"])<jupyter_output>MLPClassifier(alpha=0.05, hidden_layer_sizes=(16, 32, 24), max_iter=500)<jupyter_text>Curiosamente el modelo seleccionado es el perceptrón multi-capas, pero los resultados son diferentes que cuando corrimos el modelo de entrenamiento por separado. | Hiperparametro 	| Modelo Solo 	| GridSearchCV 	||----------------	|-------------	|--------------	|| activacion     	| relu        	| n/a          	|| alpha          	| 0.0001      	| 0.05         	|| topologia      	| 16-24-8     	| 16-32-24     	|| learning rate  	| constante   	| adaptive     	|| max_iter       	| 300         	| 500          	|| solver         	| adam        	| n/a          	|¿Cómo se comparan los resultados, si el puntaje del modelo utilizando solo MLPC fue 0.9?<jupyter_code># Predecir resultados en el conjunto de evaluación
y_test_pred = clf.predict(X_test)
y_test_prob = clf.predict_proba(X_test)

# Crear métricas
auc = roc_auc_score(y_test, y_test_prob[:, 1])
print("- Precision: ", round(precision_score(y_test, y_test_pred),2))
print("- Recall : ", round(recall_score(y_test, y_test_pred),2))
print("- F-Score : ", round(f1_score(y_test, y_test_pred), 2))
print("- AUC: ", round(auc, 2))

# Curva AUC
from sklearn.metrics import f1_score, precision_score, recall_score, roc_auc_score, roc_curve
fpr, tpr, thrs = roc_curve(y_test, y_test_prob[:, 1])
plt.plot(fpr, tpr)
plt.plot([0,1], [0,1], "r--")
plt.title("ROC")
plt.xlabel("Falsos Positivos")
plt.ylabel("Verdaderos Positivos")
plt.show()<jupyter_output><empty_output><jupyter_text>Nuestro modelo tiene una puntaje AUC de 0.91, el más alto de todos los visto hasta ahora. Utilizando el juego de test podemos generar una tabla de confusión.<jupyter_code># Tabla de Confusión
# Código utilizado en los trabajos de Aprendizaje Automatizado

data = {'y_Actual': list(y_test),
        'y_Predicted': list(clf.predict(X_test))
        }

df = pd.DataFrame(data, columns=['y_Actual','y_Predicted'])
confusion_matrix = pd.crosstab(df['y_Actual'], df['y_Predicted'], rownames=['Actual'], colnames=['Predicción'])

sns.heatmap(confusion_matrix, annot=True, fmt="d")
plt.show()<jupyter_output><empty_output><jupyter_text>Umbralizando el Modelo de PredicciónBuscar un valor umbral para nuestro clasificador es sinónimo de decidir que es más importante, que la tasa de falsos positivos sea mayor o menor que la tasa de falsos negativos. * En un sistema médico es muy importante la detección de casos positivos, aunque la tasa de falsos positivos sea alta (es mejor prevenir que dejar pasar un paciente que tiene la enfermedad y salió negativo en el test). * En un sistema de prevención de fraude, quizás es mejor tener una tasa de falsos negativos alta, ya que es muy probable que los clientes se enfaden con los bloqueos constantes de las tarjetas de crédito por cada falso positivo. Ambos son aspectos críticos en la selección de un umbral en sistemas donde las decisiones son críticas (<NAME>., 2015). ¿Pero en que categoría cae un sistema de e-commerce?  Nuestro modelo actual parece funcionar muy bien prediciendo que no es una transacción (identifica mejor 2,923 casos falsos y solo no acierta en 200) que detectando que es una transacción (identifica correctamente 355 pero no detecta 217). Utilicemos la tabla para identificar el umbral donde la tasa de falsos positivos no es alta pero la tasa de verdaderos positivos excede 80%.<jupyter_code># Umbral de clasificación
# probabilidad de label -1 > 75%
# probabilidad de label  1 > 60%

pred_umbral = np.empty_like(y_test_pred)


for i in range(0, len(y_test_pred)):
    if (y_test_prob[i][0] > 0.75 and y_test_prob[i][1] > 0.60): 
        pred_umbral[i] = True
    else:
        pred_umbral[i] = False
        
data = {'y_Actual': list(y_test),
        'y_Predicted': list(pred_umbral)
        }

df = pd.DataFrame(data, columns=['y_Actual','y_Predicted'])
confusion_matrix = pd.crosstab(df['y_Actual'], df['y_Predicted'], rownames=['Actual'], colnames=['Predicción'])

sns.heatmap(confusion_matrix, annot=True, fmt="d")
plt.show()<jupyter_output><empty_output><jupyter_text>¿Encontramos el modelo perfecto que clasifica sin errores? Probablemente no, solo que por lo escaso del juego de test se nos facilitó mucho la umbralización. En un ejercicio **que no se debe nunca hacer pero solo revisamos por motivos académicos** utilicemos la data de entrenamiento para verificar nuestro modelo de clasificación con umbral.<jupyter_code>y_train_pred = clf.predict(X_train)
y_train_prob = clf.predict_proba(X_train)

pred_umbral = np.empty_like(y_train_pred)

for i in range(0, len(y_train_pred)):
    if (y_train_prob[i][0] > 0.75 and y_train_prob[i][1] > 0.60): 
        pred_umbral[i] = True
    else:
        pred_umbral[i] = False
        
data = {'y_Actual': list(y_train),
        'y_Predicted': list(pred_umbral)
        }

df = pd.DataFrame(data, columns=['y_Actual','y_Predicted'])
confusion_matrix = pd.crosstab(df['y_Actual'], df['y_Predicted'], rownames=['Actual'], colnames=['Predicción'])

sns.heatmap(confusion_matrix, annot=True, fmt="d")
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07454408508890055, 'fraction_numerical': 0.019651701549768332, 'mean_word_length': 4.777924304365027, 'pattern_counts': {'":': 5, '<': 156, '<?xml version=': 0, '>': 162, 'https://': 5, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '36', 'dir': 'jupyter-structured-clean-dedup', 'id': '299732', 'license': 'MIT', 'n_tokens_mistral': 15089, 'n_tokens_neox': 14389, 'n_words': 5273, 'path': 'AA/AA-CP-FINAL.ipynb', 'repo_name': 'ameilij/MDS', 'size': '43893'}