En GAP nos hacemos preguntas importantes, como ¿cuál es la verdadera diferencia entre un churchill y granizado? ¿es lo suficientemente distinto el segundo del primero, o son esencialmente lo mismo? La documentación existente es insuficiente, con conflictos en la lista de ingredientes, la diferencia con el concepto de granizado, y pocas fuentes de información. Es por esto que nos dimos a la tarea de realizar una encuesta interna con la cual pudiéramos realizar un análisis objetivo y cuantitativo de la percepción pública de estos postres.
Este documento representa el análisis de los resultados de dicha encuesta.
# Exploración y preparación de datos
import pandas as pd
import numpy as np
# Visualización
import seaborn as sns
import matplotlib.pyplot as plt
# Aprendizaje de Máquina
import sklearn
El archivo de respuestas puede descargarse desde Google Sheets en forma de CSV, un formato que pandas puede ingerir sin problemas:
# Ingerimos el archivo de respuestas
churchill_df = pd.read_csv("churchill.csv")
# Imprimimos las primeras observaciones:
churchill_df.head()
cols = ["timestamp", "es_tico", "ha_consumido", "diferentes", "ing_granizado", "ing_churchill", "comentarios"]
churchill_df.columns = cols
# Verificamos resultado
churchill_df.head()
# Obtener dummies para ingredientes de granizado
dummies_granizado = churchill_df['ing_granizado'].str.strip().str.get_dummies(sep=',')
# Renombrar columnas
dummies_granizado = dummies_granizado.rename(lambda x: x.strip().lower(), axis='columns')
# Agregar columna de tipo (0=granizado)
dummies_granizado["target"] = 0
# Verificar cambios:
dummies_granizado.head()
# Obtener dummies para ingredientes de granizado
dummies_churchill = churchill_df['ing_churchill'].str.strip().str.get_dummies(sep=', ')
# Renombrar columnas
dummies_churchill = dummies_churchill.rename(lambda x: x.strip().lower(), axis='columns')
# Agregar columna de tipo (0=granizado)
dummies_churchill["target"] = 1
# Verificar cambios:
dummies_churchill.head()
# Agrupamos entradas de ingredientes escritas en "otros"
# Columnas con las que nos queremos quedar
regular_cols = ["frutas", "helado", "leche", "leche condensada", "leche en polvo", "queque", "sirope", "hielo", "target"]
# Sumar "otros"
dummies_churchill_otros = dummies_churchill.drop(regular_cols, axis=1)
dummies_churchill_otros = dummies_churchill_otros.sum(axis=1)
dummies_churchill_otros.name = "otros"
# Agregar al dataframe
dummies_churchill = dummies_churchill[regular_cols].join(dummies_churchill_otros)
# Verificamos resultado
dummies_churchill.head()
# Unimos observaciones de churchill y granizados:
final_df = dummies_churchill.append(dummies_granizado, ignore_index=True, sort=False)
final_df.head()
# Presencia de valores nulos en columnas de granizado:
final_df.info()
# Reemplazamos NaNs con 0:
final_df = final_df.fillna(0)
# Verificamos existencia de valores válidos en todas las columnas
final_df.info()
# Reordenamos columnas para que target quede de última:
cols = ["frutas", "helado", "leche", "leche condensada", "leche en polvo", "queque", "sirope", "hielo", "otros", "target"]
final_df = final_df[cols]
# Porcentajes para respuestas sencillas:
churchill_df.groupby("es_tico")["es_tico"].count().plot(kind="pie", figsize=(5,5))
# Porcentajes para respuestas sencillas:
churchill_df.groupby("ha_consumido")["ha_consumido"].count().plot(kind="barh", figsize=(8,5))
# Tabla resumen de ingredientes
tabla_resumen = final_df.groupby("target").sum()
tabla_resumen
# Comparativa visual:
ax = tabla_resumen.plot(kind="bar",figsize=(12,8))
ax.set_xlabel("Target (0 = Granizado, 1 = Churchill)")
Por medio de este análisis, buscamos establecer las variables (ingredientes) que marcan la diferencia entre un churchill y un granizado.
corr = final_df.corr()
corr
Acá nos interesa sólamente la correlación de los ingredientes con el target. Mientras se aproxime el índice de correlación a 1 o -1, este va a indicar una correlación positiva o negativa con el tipo de observación. Como podemos ver, la variable helado, con una correlación positiva de 0.81, es la más importante.
mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
f, ax = plt.subplots(figsize=(11, 9))
cmap = sns.diverging_palette(220, 10, as_cmap=True)
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .5})
Construimos un árbol de decisión que clasifique observaciones en granizado a churchill basándose en sus ingredientes.
# Extraemos el target del resto del dataset:
y = final_df["target"]
X = final_df.drop("target", axis=1)
X.head()
from sklearn.model_selection import train_test_split
# Generamos grupos de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 5)
from sklearn import tree
# Instanciación del modelo
t = tree.DecisionTreeClassifier(max_depth = 4,
criterion = 'entropy',
class_weight = 'balanced',
random_state = 2)
# Entrenamiento
t.fit(X_train, y_train)
# Prueba
t.score(X_test, y_test)
Conclusión: Hemos generado un modelo capaz de detectar si una observación es un granizado o un churchill con un 96% de precisión.
from sklearn.tree import export_graphviz
import graphviz
import pydotplus
# Generamos la visualización, exportada a un archivo .dot
dot_data = export_graphviz(t, out_file=None,
feature_names=X.columns,
class_names=["granizado", "churchill"],
filled=True, rounded=True,
special_characters=True)
# Generamos la imagen a partir del archivo .dot
pydot_graph = pydotplus.graph_from_dot_data(dot_data)
pydot_graph.set_size('"8,8!"')
pydot_graph.write_png('resized_tree.png')
gvz_graph = graphviz.Source(pydot_graph.to_string())
gvz_graph
Como se observa arriba, la presencia de helado es por mucho el factor que más se relaciona a la variable etiqueta ("Churchill" vs "Granizado"), con un índice de correlación de 0.81.
El segundo factor más importante es "otros", lo cual sugiere que mientras más ingredientes tenga la observación, mas probable es que califique como un churchill y no un granizado.
Es posible generar un modelo de clasificación automática que decida, basado en la presencia de ingredientes, si un producto debe ser llamado churchill o granizado.
La investigación descrita en este documento fue ideada, elaborada y ejecutada por el Acxiom team de GAP.