Churchill vs. Granizado: Análisis de Encuesta GAP

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.

Imports

In [1]:
# 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

Ingestión de datos

El archivo de respuestas puede descargarse desde Google Sheets en forma de CSV, un formato que pandas puede ingerir sin problemas:

In [2]:
# Ingerimos el archivo de respuestas
churchill_df = pd.read_csv("churchill.csv")

# Imprimimos las primeras observaciones:
churchill_df.head()
Out[2]:
Timestamp ¿Es usted residente de Costa Rica o ha residido al menos 1 año en el país? ¿Ha consumido usted productos descritos como "granizado" y "churchill" en momentos diferentes de su vida? ¿Considera usted que un churchill y un granizado son dos productos diferentes? A continuación, seleccione los ingredientes que considera son parte de un granizado: A continuación, seleccione los ingredientes que considera son parte de un churchill: ¿Considera usted que aparte de los ingredientes, existe alguna otra diferencia entre un churchill y un granizado? De ser así, descríbala en este campo.
0 7/9/2019 9:28:08 Si No Si Hielo, Sirope, Leche Condensada, Leche en Polvo Nunca he comido un churchill Ni idea
1 7/9/2019 9:30:04 Si Si Si Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche Condensada, Leche en Polv... No :)
2 7/9/2019 9:30:27 Si Si Si Hielo, Sirope, Leche, Leche Condensada, Leche ... Hielo, Sirope, Leche, Leche Condensada, Leche ... mas leche condensada
3 7/9/2019 9:30:38 Si Si No Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche Condensada, Leche en Polvo No hay diferencias entre uno y otro.
4 7/9/2019 9:33:56 Si Si Si Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche, Leche Condensada, Leche ... No :D

Transformaciones/Limpieza

  1. Renombrar las columnas para que sean fáciles de referenciar.
  2. Extraer los ingredientes para granizados y churchill en columnas individuales.
  3. Agrupar ingredientes extra en "otros".
  4. Crear un dataset de observaciones de tipo churchill y tipo granizado.
  5. Rankear correlaciones entre ingredientes y tipos.
In [3]:
cols = ["timestamp", "es_tico", "ha_consumido", "diferentes", "ing_granizado", "ing_churchill", "comentarios"]

churchill_df.columns = cols

# Verificamos resultado
churchill_df.head()
Out[3]:
timestamp es_tico ha_consumido diferentes ing_granizado ing_churchill comentarios
0 7/9/2019 9:28:08 Si No Si Hielo, Sirope, Leche Condensada, Leche en Polvo Nunca he comido un churchill Ni idea
1 7/9/2019 9:30:04 Si Si Si Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche Condensada, Leche en Polv... No :)
2 7/9/2019 9:30:27 Si Si Si Hielo, Sirope, Leche, Leche Condensada, Leche ... Hielo, Sirope, Leche, Leche Condensada, Leche ... mas leche condensada
3 7/9/2019 9:30:38 Si Si No Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche Condensada, Leche en Polvo No hay diferencias entre uno y otro.
4 7/9/2019 9:33:56 Si Si Si Hielo, Sirope, Leche Condensada, Leche en Polvo Hielo, Sirope, Leche, Leche Condensada, Leche ... No :D
In [4]:
# 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()
Out[4]:
frutas helado leche leche condensada leche en polvo sirope hielo target
0 0 0 0 1 1 1 1 0
1 0 0 0 1 1 1 1 0
2 1 1 1 1 1 1 1 0
3 0 0 0 1 1 1 1 0
4 0 0 0 1 1 1 1 0
In [5]:
# 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()
Out[5]:
barquillos! frutas helado hielo la leche condensada en latita leche leche condensada leche en polvo nunca he comido un churchill queque sirope creo que le ponen extras a veces queque solo si es churchill coloso target
0 0 0 0 0 0 0 0 0 1 0 0 0 0 1
1 0 0 1 1 0 0 1 1 0 0 1 0 0 1
2 0 0 1 1 0 1 1 1 0 0 1 0 0 1
3 0 0 0 1 0 0 1 1 0 0 1 0 0 1
4 0 1 1 1 0 1 1 1 0 0 1 0 0 1
In [6]:
# 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)
In [7]:
# Verificamos resultado
dummies_churchill.head()
Out[7]:
frutas helado leche leche condensada leche en polvo queque sirope hielo target otros
0 0 0 0 0 0 0 0 0 1 1
1 0 1 0 1 1 0 1 1 1 0
2 0 1 1 1 1 0 1 1 1 0
3 0 0 0 1 1 0 1 1 1 0
4 1 1 1 1 1 0 1 1 1 0
In [8]:
# Unimos observaciones de churchill y granizados:

final_df = dummies_churchill.append(dummies_granizado, ignore_index=True, sort=False)
final_df.head()
Out[8]:
frutas helado leche leche condensada leche en polvo queque sirope hielo target otros
0 0 0 0 0 0 0.0 0 0 1 1.0
1 0 1 0 1 1 0.0 1 1 1 0.0
2 0 1 1 1 1 0.0 1 1 1 0.0
3 0 0 0 1 1 0.0 1 1 1 0.0
4 1 1 1 1 1 0.0 1 1 1 0.0
In [9]:
# Presencia de valores nulos en columnas de granizado:
final_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 84 entries, 0 to 83
Data columns (total 10 columns):
frutas              84 non-null int64
helado              84 non-null int64
leche               84 non-null int64
leche condensada    84 non-null int64
leche en polvo      84 non-null int64
queque              42 non-null float64
sirope              84 non-null int64
hielo               84 non-null int64
target              84 non-null int64
otros               42 non-null float64
dtypes: float64(2), int64(8)
memory usage: 6.6 KB
In [10]:
# Reemplazamos NaNs con 0:

final_df = final_df.fillna(0)
In [11]:
# Verificamos existencia de valores válidos en todas las columnas
final_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 84 entries, 0 to 83
Data columns (total 10 columns):
frutas              84 non-null int64
helado              84 non-null int64
leche               84 non-null int64
leche condensada    84 non-null int64
leche en polvo      84 non-null int64
queque              84 non-null float64
sirope              84 non-null int64
hielo               84 non-null int64
target              84 non-null int64
otros               84 non-null float64
dtypes: float64(2), int64(8)
memory usage: 6.6 KB
In [12]:
# 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]

Visualizaciones Básicas

In [13]:
# Porcentajes para respuestas sencillas:
churchill_df.groupby("es_tico")["es_tico"].count().plot(kind="pie", figsize=(5,5))
Out[13]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7d232b8320>
In [14]:
# Porcentajes para respuestas sencillas:
churchill_df.groupby("ha_consumido")["ha_consumido"].count().plot(kind="barh", figsize=(8,5))
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7d232497b8>
In [15]:
# Tabla resumen de ingredientes
tabla_resumen = final_df.groupby("target").sum()
tabla_resumen
Out[15]:
frutas helado leche leche condensada leche en polvo queque sirope hielo otros
target
0 2 3 9 39 40 0.0 42 42 0.0
1 19 37 11 41 40 3.0 40 39 5.0
In [16]:
# Comparativa visual:
ax = tabla_resumen.plot(kind="bar",figsize=(12,8))

ax.set_xlabel("Target (0 = Granizado, 1 = Churchill)")
Out[16]:
Text(0.5, 0, 'Target (0 = Granizado, 1 = Churchill)')

Análisis de Correlación:

Por medio de este análisis, buscamos establecer las variables (ingredientes) que marcan la diferencia entre un churchill y un granizado.

In [17]:
corr = final_df.corr()
corr
Out[17]:
frutas helado leche leche condensada leche en polvo queque sirope hielo otros target
frutas 1.000000e+00 0.550482 0.064550 3.296571e-16 4.729863e-16 0.185185 -0.090167 -0.037037 -0.029050 0.467379
helado 5.504819e-01 1.000000 0.138580 1.012703e-01 -1.066004e-02 0.201843 -0.007445 -0.073398 0.163111 0.810443
leche 6.454972e-02 0.138580 1.000000 1.250000e-01 1.250000e-01 -0.107583 0.087304 -0.043033 -0.022502 0.055902
leche condensada 3.296571e-16 0.101270 0.125000 1.000000e+00 7.375000e-01 0.043033 0.331754 0.258199 -0.180014 0.111803
leche en polvo 4.729863e-16 -0.010660 0.125000 7.375000e-01 1.000000e+00 0.043033 0.331754 0.258199 -0.180014 0.000000
queque 1.851852e-01 0.201843 -0.107583 4.303315e-02 4.303315e-02 1.000000 -0.390724 -0.308642 0.222714 0.192450
sirope -9.016696e-02 -0.007445 0.087304 3.317544e-01 3.317544e-01 -0.390724 1.000000 0.811503 -0.290744 -0.156174
hielo -3.703704e-02 -0.073398 -0.043033 2.581989e-01 2.581989e-01 -0.308642 0.811503 1.000000 -0.222714 -0.192450
otros -2.904964e-02 0.163111 -0.022502 -1.800141e-01 -1.800141e-01 0.222714 -0.290744 -0.222714 1.000000 0.251577
target 4.673788e-01 0.810443 0.055902 1.118034e-01 0.000000e+00 0.192450 -0.156174 -0.192450 0.251577 1.000000

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.

In [18]:
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})
Out[18]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f7d22aa10f0>

Clasificador de granizado vs churchill

Construimos un árbol de decisión que clasifique observaciones en granizado a churchill basándose en sus ingredientes.

In [19]:
# Extraemos el target del resto del dataset:

y = final_df["target"]
X = final_df.drop("target", axis=1)

X.head()
Out[19]:
frutas helado leche leche condensada leche en polvo queque sirope hielo otros
0 0 0 0 0 0 0.0 0 0 1.0
1 0 1 0 1 1 0.0 1 1 0.0
2 0 1 1 1 1 0.0 1 1 0.0
3 0 0 0 1 1 0.0 1 1 0.0
4 1 1 1 1 1 0.0 1 1 0.0

Separación en grupos de entrenamiento y prueba

In [20]:
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)

Entrenamiento del modelo (Árbol de Decisión)

In [21]:
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)
Out[21]:
DecisionTreeClassifier(class_weight='balanced', criterion='entropy',
                       max_depth=4, max_features=None, max_leaf_nodes=None,
                       min_impurity_decrease=0.0, min_impurity_split=None,
                       min_samples_leaf=1, min_samples_split=2,
                       min_weight_fraction_leaf=0.0, presort=False,
                       random_state=2, splitter='best')

Evaluación

In [22]:
# Prueba
t.score(X_test, y_test)
Out[22]:
0.9615384615384616

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.

Visualización del modelo

In [23]:
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
Out[23]:
Tree 0 helado ≤ 0.5 entropy = 1.0 samples = 58 value = [29.0, 29.0] class = churchill 1 frutas ≤ 0.5 entropy = 0.576 samples = 31 value = [26.1, 4.143] class = granizado 0->1 True 8 leche condensada ≤ 0.5 entropy = 0.483 samples = 27 value = [2.9, 24.857] class = churchill 0->8 False 2 otros ≤ 0.5 entropy = 0.489 samples = 30 value = [26.1, 3.107] class = granizado 1->2 7 entropy = -0.0 samples = 1 value = [0.0, 1.036] class = churchill 1->7 3 leche ≤ 0.5 entropy = 0.379 samples = 29 value = [26.1, 2.071] class = granizado 2->3 6 entropy = -0.0 samples = 1 value = [0.0, 1.036] class = churchill 2->6 4 entropy = 0.432 samples = 24 value = [21.267, 2.071] class = granizado 3->4 5 entropy = 0.0 samples = 5 value = [4.833, 0.0] class = granizado 3->5 9 entropy = 0.0 samples = 1 value = [0.967, 0.0] class = granizado 8->9 10 queque ≤ 0.5 entropy = 0.374 samples = 26 value = [1.933, 24.857] class = churchill 8->10 11 frutas ≤ 0.5 entropy = 0.408 samples = 23 value = [1.933, 21.75] class = churchill 10->11 14 entropy = 0.0 samples = 3 value = [0.0, 3.107] class = churchill 10->14 12 entropy = 0.337 samples = 15 value = [0.967, 14.5] class = churchill 11->12 13 entropy = 0.523 samples = 8 value = [0.967, 7.25] class = churchill 11->13

Conclusiones:

  • 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.

Trabajo futuro:

  • Incrementar el tamaño de la muestra.
  • Probar distintos tipos de modelos de clasificación.
  • Incorporar diferentes diferenciadores (ubicación geográfica, contenedor del producto).

Créditos:

La investigación descrita en este documento fue ideada, elaborada y ejecutada por el Acxiom team de GAP.