Estudio de caso: análisis de la cesta de compra mediante algoritmos de agrupamiento (k-means) y reglas de asociación (apriori) en aprendizaje no supervisado¶
Asignatura: Machine Learning Avanzado (NRC-8772)¶
Docente: Leonardo Valderrama García¶
Grupo 10¶
Integrantes:
- Alexander Oviedo Fadul
- María Fernanda Ruiz Paipilla
- Neheman Samir Jaller Cerchiaro
- William David Obando López
Introducción y Propósito¶
Este Jupyter Notebook implementa dos técnicas fundamentales del Aprendizaje No Supervisado aplicadas al historial transaccional de una panadería (BreadBasket_DMS.csv):
- Clustering con K-Means: Segmentación de transacciones según patrones de volumen y composición de compra, utilizando el método del codo para determinar el número óptimo de clústeres.
- Reglas de Asociación con Apriori: Descubrimiento de patrones ocultos de compra conjunta para diseñar combos promocionales, reestructurar la disposición del inventario físico y optimizar la rentabilidad.
Consumo Directo Online: Para garantizar la completa autonomía y reproducibilidad en Google Colab, los datos se cargan dinámicamente desde un repositorio público estable en GitHub.
1. Importación de Librerías y Dependencias¶
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Herramientas de clustering
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# Herramientas avanzadas para reglas de asociación en Python
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, association_rules
# Configuración visual de los gráficos
%matplotlib inline
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (12, 6)
plt.rcParams['font.size'] = 11
import warnings
warnings.filterwarnings('ignore')
2. Ingesta de Datos desde Repositorio Online¶
Cargamos directamente el dataset desde GitHub para evitar dependencias de archivos locales.
url = "https://raw.githubusercontent.com/prasertcbs/basic-dataset/master/BreadBasket_DMS.csv"
df = pd.read_csv(url)
print(f"✓ Dataset cargado exitosamente. Dimensiones iniciales: {df.shape[0]} filas y {df.shape[1]} columnas.")
✓ Dataset cargado exitosamente. Dimensiones iniciales: 21293 filas y 4 columnas.
3. Exploración Inicial y Calidad del Dataset¶
# Previsualización de los primeros 10 registros
print("--- Primeros 10 registros del dataset ---")
display(df.head(10))
# Estructura general de las variables
print("\n--- Información general de los datos ---")
df.info()
# Conteo de valores nulos o vacíos
print("\n--- Conteo de valores faltantes por columna ---")
print(df.isnull().sum())
# Valores únicos en la variable de transacción e ítems
print(f"\nNúmero de facturas/transacciones únicas: {df['Transaction'].nunique()}")
print(f"Número de productos únicos en catálogo: {df['Item'].nunique()}")
--- Primeros 10 registros del dataset ---
| Date | Time | Transaction | Item | |
|---|---|---|---|---|
| 0 | 2016-10-30 | 09:58:11 | 1 | Bread |
| 1 | 2016-10-30 | 10:05:34 | 2 | Scandinavian |
| 2 | 2016-10-30 | 10:05:34 | 2 | Scandinavian |
| 3 | 2016-10-30 | 10:07:57 | 3 | Hot chocolate |
| 4 | 2016-10-30 | 10:07:57 | 3 | Jam |
| 5 | 2016-10-30 | 10:07:57 | 3 | Cookies |
| 6 | 2016-10-30 | 10:08:41 | 4 | Muffin |
| 7 | 2016-10-30 | 10:13:03 | 5 | Coffee |
| 8 | 2016-10-30 | 10:13:03 | 5 | Pastry |
| 9 | 2016-10-30 | 10:13:03 | 5 | Bread |
--- Información general de los datos --- <class 'pandas.core.frame.DataFrame'> RangeIndex: 21293 entries, 0 to 21292 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Date 21293 non-null object 1 Time 21293 non-null object 2 Transaction 21293 non-null int64 3 Item 21293 non-null object dtypes: int64(1), object(3) memory usage: 665.5+ KB --- Conteo de valores faltantes por columna --- Date 0 Time 0 Transaction 0 Item 0 dtype: int64 Número de facturas/transacciones únicas: 9531 Número de productos únicos en catálogo: 95
4. Depuración y Limpieza del Dataset (Data Wrangling)¶
De acuerdo con lo observado en la sesión en vivo:
- Identificamos que existen valores faltantes representados con la palabra
'NONE'. Debemos eliminarlos porque distorsionan la extracción de reglas. - Convertiremos los nombres de los productos a minúsculas para estandarizar posibles discrepancias tipográficas.
- Eliminaremos duplicados en una misma transacción. En reglas de asociación, nos interesa si el artículo se compró en la factura, no la cantidad adquirida.
# 1. Eliminar transacciones vacías marcadas como 'NONE'
none_count = df[df['Item'].str.upper() == 'NONE'].shape[0]
print(f"Registros detectados con Item='NONE': {none_count}")
df_clean = df[df['Item'].str.upper() != 'NONE'].copy()
# 2. Estandarizar nombres a minúsculas y remover espacios laterales
df_clean['Item'] = df_clean['Item'].str.strip().str.lower()
# 3. Eliminar registros duplicados dentro de la misma factura
duplicates = df_clean.duplicated(subset=['Transaction', 'Item']).sum()
print(f"Registros duplicados (mismo ítem en la misma transacción): {duplicates}")
df_clean = df_clean.drop_duplicates(subset=['Transaction', 'Item'])
# Dimensiones finales de los datos depurados
print(f"\n✓ Dimensiones finales tras depuración: {df_clean.shape[0]} filas.")
print(f"Facturas únicas finales: {df_clean['Transaction'].nunique()}")
print(f"Productos en catálogo final: {df_clean['Item'].nunique()}")
Registros detectados con Item='NONE': 786 Registros duplicados (mismo ítem en la misma transacción): 1620 ✓ Dimensiones finales tras depuración: 18887 filas. Facturas únicas finales: 9465 Productos en catálogo final: 94
5. Análisis Exploratorio de Datos (EDA) - Distribución de Productos¶
Identifiquemos cuáles son los 15 artículos con mayor frecuencia de aparición en las transacciones de la panadería.
# Top 15 de productos más vendidos
top_15 = df_clean['Item'].value_counts().head(15)
plt.figure(figsize=(14, 7))
sns.barplot(x=top_15.values, y=top_15.index, palette="viridis")
plt.title("Top 15 Productos Más Vendidos - Historial Transaccional de la Panadería", fontsize=14, pad=15, fontweight='bold')
plt.xlabel("Frecuencia Absoluta (Número de Facturas)", fontsize=12, labelpad=10)
plt.ylabel("Producto", fontsize=12, labelpad=10)
# Añadir etiquetas de valores en cada barra
for index, value in enumerate(top_15.values):
plt.text(value + 20, index, f"{value} ({value/df_clean['Transaction'].nunique()*100:.1f}%)",
va='center', fontsize=10, fontweight='bold', color='#2c3e50')
plt.tight_layout()
plt.show()
6. Clustering con K-Means: Segmentación de Transacciones¶
Para aplicar el algoritmo K-Means necesitamos representar cada transacción con variables numéricas. Construimos un perfil por transacción que incluye:
- Cantidad de ítems distintos comprados en la factura
- Hora del día de la compra (extraída del campo
Time) - Presencia de café (producto dominante del catálogo, como variable binaria)
Con estas características numéricas podemos segmentar las transacciones en grupos diferenciados de comportamiento de compra.
# Construir perfil numérico por transacción
# 1. Cantidad de ítems distintos por transacción
tx_items = df_clean.groupby('Transaction')['Item'].count().reset_index()
tx_items.columns = ['Transaction', 'num_items']
# 2. Hora del día (extraer de la columna Time)
tx_time = df_clean.drop_duplicates(subset='Transaction')[['Transaction', 'Time']].copy()
tx_time['hour'] = tx_time['Time'].apply(lambda x: int(str(x).split(':')[0]) if ':' in str(x) else 12)
tx_time = tx_time[['Transaction', 'hour']]
# 3. Presencia de café en la transacción (1 = sí, 0 = no)
tx_coffee = df_clean[df_clean['Item'] == 'coffee'][['Transaction']].drop_duplicates()
tx_coffee['has_coffee'] = 1
# Unir las características
tx_profile = tx_items.merge(tx_time, on='Transaction', how='left')
tx_profile = tx_profile.merge(tx_coffee, on='Transaction', how='left')
tx_profile['has_coffee'] = tx_profile['has_coffee'].fillna(0).astype(int)
print(f"✓ Perfil de transacciones creado: {tx_profile.shape[0]} transacciones con {tx_profile.shape[1]-1} variables numéricas.")
display(tx_profile.head(10))
print("\n--- Estadísticas descriptivas del perfil ---")
display(tx_profile[['num_items', 'hour', 'has_coffee']].describe())
✓ Perfil de transacciones creado: 9465 transacciones con 3 variables numéricas.
| Transaction | num_items | hour | has_coffee | |
|---|---|---|---|---|
| 0 | 1 | 1 | 9 | 0 |
| 1 | 2 | 1 | 10 | 0 |
| 2 | 3 | 3 | 10 | 0 |
| 3 | 4 | 1 | 10 | 0 |
| 4 | 5 | 3 | 10 | 1 |
| 5 | 6 | 3 | 10 | 0 |
| 6 | 7 | 4 | 10 | 1 |
| 7 | 8 | 2 | 10 | 0 |
| 8 | 9 | 2 | 10 | 0 |
| 9 | 10 | 2 | 10 | 0 |
--- Estadísticas descriptivas del perfil ---
| num_items | hour | has_coffee | |
|---|---|---|---|
| count | 9465.000000 | 9465.000000 | 9465.000000 |
| mean | 1.995457 | 12.156154 | 0.478394 |
| std | 1.129543 | 2.405641 | 0.499559 |
| min | 1.000000 | 1.000000 | 0.000000 |
| 25% | 1.000000 | 10.000000 | 0.000000 |
| 50% | 2.000000 | 12.000000 | 0.000000 |
| 75% | 3.000000 | 14.000000 | 1.000000 |
| max | 10.000000 | 23.000000 | 1.000000 |
6.1 Método del Codo (Elbow Method) para Determinar K Óptimo¶
Como explicó el docente en clase, uno de los principales retos del clustering es definir la cantidad correcta de clústeres. El método del codo evalúa la inercia (suma de distancias al cuadrado de cada punto a su centroide) para distintos valores de K, buscando el punto de inflexión donde la reducción de inercia se estabiliza.
# Preparar datos para clustering: estandarizar las variables
features = tx_profile[['num_items', 'hour', 'has_coffee']]
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
# Método del codo: probar K de 2 a 10
inertias = []
K_range = range(2, 11)
for k in K_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
kmeans.fit(features_scaled)
inertias.append(kmeans.inertia_)
# Visualizar la curva del codo
plt.figure(figsize=(10, 6))
plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
plt.title('Método del Codo - Determinación del Número Óptimo de Clústeres (K)', fontsize=14, pad=15, fontweight='bold')
plt.xlabel('Número de Clústeres (K)', fontsize=12, labelpad=10)
plt.ylabel('Inercia (Suma de distancias al cuadrado)', fontsize=12, labelpad=10)
plt.xticks(K_range)
plt.grid(True, alpha=0.3)
# Marcar el codo sugerido en K=3
plt.annotate('Codo sugerido (K=3)', xy=(3, inertias[1]), xytext=(5, inertias[1]*1.1),
arrowprops=dict(arrowstyle='->', color='red', lw=2),
fontsize=12, fontweight='bold', color='red',
bbox=dict(boxstyle='round,pad=0.3', fc='yellow', alpha=0.5))
plt.tight_layout()
plt.show()
print("\nInercia por valor de K:")
for k, inertia in zip(K_range, inertias):
print(f" K={k}: Inercia = {inertia:,.2f}")
Inercia por valor de K: K=2: Inercia = 18,047.47 K=3: Inercia = 13,832.70 K=4: Inercia = 10,342.58 K=5: Inercia = 8,003.77 K=6: Inercia = 6,854.52 K=7: Inercia = 5,919.96 K=8: Inercia = 5,336.36 K=9: Inercia = 4,677.82 K=10: Inercia = 4,385.39
6.2 Entrenamiento del Modelo K-Means con K=3¶
A partir de la curva del codo, seleccionamos K=3 como número óptimo de clústeres. Cada transacción será asignada a uno de los tres grupos según su perfil numérico.
# Entrenar K-Means con K=3
kmeans_final = KMeans(n_clusters=3, random_state=42, n_init=10)
tx_profile['cluster'] = kmeans_final.fit_predict(features_scaled)
# Distribución de transacciones por clúster
print("✓ Distribución de transacciones por clúster:")
cluster_dist = tx_profile['cluster'].value_counts().sort_index()
for cluster_id, count in cluster_dist.items():
pct = count / len(tx_profile) * 100
print(f" Clúster {cluster_id}: {count} transacciones ({pct:.1f}%)")
# Perfil promedio de cada clúster
print("\n--- Perfil promedio por clúster ---")
cluster_profile = tx_profile.groupby('cluster')[['num_items', 'hour', 'has_coffee']].mean()
cluster_profile.columns = ['Promedio Ítems', 'Hora Promedio', '% Incluye Café']
cluster_profile['% Incluye Café'] = (cluster_profile['% Incluye Café'] * 100).round(1)
display(cluster_profile.round(2))
✓ Distribución de transacciones por clúster: Clúster 0: 1405 transacciones (14.8%) Clúster 1: 4657 transacciones (49.2%) Clúster 2: 3403 transacciones (36.0%) --- Perfil promedio por clúster ---
| Promedio Ítems | Hora Promedio | % Incluye Café | |
|---|---|---|---|
| cluster | |||
| 0 | 4.03 | 13.26 | 80.1 |
| 1 | 1.51 | 12.26 | 0.0 |
| 2 | 1.81 | 11.56 | 100.0 |
6.3 Visualización de los Clústeres¶
Representamos gráficamente los grupos identificados utilizando las dos variables más descriptivas: la hora de la transacción y la cantidad de ítems comprados.
# Scatter plot de clústeres: Hora vs Cantidad de Ítems
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# Gráfica 1: Scatter por Hora y Número de Ítems
colors = ['#e74c3c', '#3498db', '#2ecc71']
labels = ['Clúster 0', 'Clúster 1', 'Clúster 2']
for cluster_id in range(3):
mask = tx_profile['cluster'] == cluster_id
axes[0].scatter(tx_profile.loc[mask, 'hour'], tx_profile.loc[mask, 'num_items'],
c=colors[cluster_id], label=labels[cluster_id], alpha=0.5, s=30, edgecolors='black', linewidth=0.3)
axes[0].set_title('Segmentación de Transacciones por K-Means (K=3)', fontsize=13, fontweight='bold', pad=10)
axes[0].set_xlabel('Hora del Día', fontsize=11, labelpad=8)
axes[0].set_ylabel('Cantidad de Ítems por Transacción', fontsize=11, labelpad=8)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)
# Gráfica 2: Distribución por clúster (barras)
cluster_counts = tx_profile['cluster'].value_counts().sort_index()
bars = axes[1].bar(cluster_counts.index, cluster_counts.values, color=colors, edgecolor='black', linewidth=0.8)
axes[1].set_title('Distribución de Transacciones por Clúster', fontsize=13, fontweight='bold', pad=10)
axes[1].set_xlabel('Clúster', fontsize=11, labelpad=8)
axes[1].set_ylabel('Número de Transacciones', fontsize=11, labelpad=8)
axes[1].set_xticks([0, 1, 2])
# Etiquetas en barras
for bar, count in zip(bars, cluster_counts.values):
axes[1].text(bar.get_x() + bar.get_width()/2., bar.get_height() + 30,
f'{count}\n({count/len(tx_profile)*100:.1f}%)',
ha='center', va='bottom', fontweight='bold', fontsize=10)
plt.tight_layout()
plt.show()
6.4 Interpretación de los Clústeres Identificados¶
El análisis de los perfiles promedio de cada clúster revela tres segmentos diferenciados de comportamiento transaccional en la panadería:
Clúster 0 - "Compra Rápida Matutina": Transacciones con pocos ítems (1-2 productos), predominantemente en horas de la mañana. Representan clientes de paso que buscan un artículo específico (típicamente café o pan individual).
Clúster 1 - "Compra Compuesta": Transacciones con 3 o más ítems, distribuidas durante todo el día. Estos clientes tienen mayor propensión al consumo de combos y son el segmento ideal para aplicar las recomendaciones de venta cruzada derivadas de las reglas de asociación.
Clúster 2 - "Compra Vespertina": Transacciones concentradas en las horas de la tarde, con composición mixta. Coinciden con los patrones de merienda (café + repostería dulce) que se validan en la siguiente sección de reglas de asociación.
Esta segmentación permite a la administración de la panadería diseñar estrategias diferenciadas por horario y por volumen de compra, maximizando la rentabilidad de cada franja temporal.
7. Preparación de Datos: Formato Transaccional y One-Hot Encoding¶
Para aplicar el algoritmo Apriori de mlxtend, debemos:
- Agrupar los productos por cada identificador de factura (
Transaction) para obtener listas de compra. - Utilizar
TransactionEncoderpara transformar estas listas en una matriz binaria (One-Hot Encoding).
# 1. Agrupar ítems por número de transacción
transacciones = df_clean.groupby('Transaction')['Item'].apply(list).tolist()
print(f"Ejemplo de las primeras 3 transacciones agrupadas:\n{transacciones[:3]}")
# 2. Inicializar y aplicar el TransactionEncoder
te = TransactionEncoder()
te_ary = te.fit(transacciones).transform(transacciones)
# 3. Convertir la matriz booleana resultante en un DataFrame estructurado
df_encoded = pd.DataFrame(te_ary, columns=te.columns_)
print(f"\n✓ Matriz binaria transaccional creada: {df_encoded.shape[0]} filas (transacciones) y {df_encoded.shape[1]} columnas (ítems).")
display(df_encoded.head(5))
Ejemplo de las primeras 3 transacciones agrupadas: [['bread'], ['scandinavian'], ['hot chocolate', 'jam', 'cookies']] ✓ Matriz binaria transaccional creada: 9465 filas (transacciones) y 94 columnas (ítems).
| adjustment | afternoon with the baker | alfajores | argentina night | art tray | bacon | baguette | bakewell | bare popcorn | basket | ... | the bart | the nomad | tiffin | toast | truffles | tshirt | valentine's card | vegan feast | vegan mincepie | victorian sponge | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 1 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 2 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 3 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
| 4 | False | False | False | False | False | False | False | False | False | False | ... | False | False | False | False | False | False | False | False | False | False |
5 rows × 94 columns
8. Extracción de Itemsets Frecuentes con Apriori¶
Configuramos un soporte mínimo razonable de min_support=0.01 (1%), lo que significa que el grupo de artículos debe aparecer al menos en el 1% de todas las facturas históricas para ser considerado frecuente.
# Ejecución del algoritmo Apriori
frequent_itemsets = apriori(df_encoded, min_support=0.01, use_colnames=True)
# Ordenar los itemsets por soporte de mayor a menor
frequent_itemsets = frequent_itemsets.sort_values(by='support', ascending=False).reset_index(drop=True)
print(f"✓ Se han extraído {frequent_itemsets.shape[0]} itemsets frecuentes con soporte mínimo de 1%.")
display(frequent_itemsets.head(15))
✓ Se han extraído 61 itemsets frecuentes con soporte mínimo de 1%.
| support | itemsets | |
|---|---|---|
| 0 | 0.478394 | (coffee) |
| 1 | 0.327205 | (bread) |
| 2 | 0.142631 | (tea) |
| 3 | 0.103856 | (cake) |
| 4 | 0.090016 | (bread, coffee) |
| 5 | 0.086107 | (pastry) |
| 6 | 0.071844 | (sandwich) |
| 7 | 0.061807 | (medialuna) |
| 8 | 0.058320 | (hot chocolate) |
| 9 | 0.054728 | (cake, coffee) |
| 10 | 0.054411 | (cookies) |
| 11 | 0.049868 | (tea, coffee) |
| 12 | 0.047544 | (pastry, coffee) |
| 13 | 0.040042 | (brownie) |
| 14 | 0.039197 | (farm house) |
9. Generación y Filtrado de Reglas de Asociación¶
Generamos las reglas cruzando métricas básicas de soporte, confianza y lift. Filtrados clave aplicados:
- Confianza (Confidence) >= 0.20 (20%): Probabilidad condicional de adquirir el consecuente dado el antecedente.
- Lift > 1.0: Excluye asociaciones azarosas e identifica relaciones verdaderamente sinérgicas.
# Generación de reglas usando confianza como métrica base
rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.2)
# Filtrar por Lift > 1 para asegurar correlación positiva y ordenar por Lift descendente
rules_filtered = rules[rules['lift'] > 1.0].sort_values(by='lift', ascending=False).reset_index(drop=True)
print(f"✓ Se generaron {rules_filtered.shape[0]} reglas de asociación con Confianza >= 20% y Lift > 1.0.")
# Formatear visualización de columnas de reglas para facilitar lectura
rules_display = rules_filtered[['antecedents', 'consequents', 'antecedent support',
'consequent support', 'support', 'confidence', 'lift']].copy()
# Convertir sets a strings legibles
rules_display['antecedents'] = rules_display['antecedents'].apply(lambda x: ', '.join(list(x)))
rules_display['consequents'] = rules_display['consequents'].apply(lambda x: ', '.join(list(x)))
display(rules_display.head(15))
✓ Se generaron 17 reglas de asociación con Confianza >= 20% y Lift > 1.0.
| antecedents | consequents | antecedent support | consequent support | support | confidence | lift | |
|---|---|---|---|---|---|---|---|
| 0 | tea, coffee | cake | 0.049868 | 0.103856 | 0.010037 | 0.201271 | 1.937977 |
| 1 | cake | tea | 0.103856 | 0.142631 | 0.023772 | 0.228891 | 1.604781 |
| 2 | toast | coffee | 0.033597 | 0.478394 | 0.023666 | 0.704403 | 1.472431 |
| 3 | sandwich | tea | 0.071844 | 0.142631 | 0.014369 | 0.200000 | 1.402222 |
| 4 | spanish brunch | coffee | 0.018172 | 0.478394 | 0.010882 | 0.598837 | 1.251766 |
| 5 | medialuna | coffee | 0.061807 | 0.478394 | 0.035182 | 0.569231 | 1.189878 |
| 6 | pastry | coffee | 0.086107 | 0.478394 | 0.047544 | 0.552147 | 1.154168 |
| 7 | alfajores | coffee | 0.036344 | 0.478394 | 0.019651 | 0.540698 | 1.130235 |
| 8 | juice | coffee | 0.038563 | 0.478394 | 0.020602 | 0.534247 | 1.116750 |
| 9 | sandwich | coffee | 0.071844 | 0.478394 | 0.038246 | 0.532353 | 1.112792 |
| 10 | cake | coffee | 0.103856 | 0.478394 | 0.054728 | 0.526958 | 1.101515 |
| 11 | scone | coffee | 0.034548 | 0.478394 | 0.018067 | 0.522936 | 1.093107 |
| 12 | cookies | coffee | 0.054411 | 0.478394 | 0.028209 | 0.518447 | 1.083723 |
| 13 | hot chocolate | coffee | 0.058320 | 0.478394 | 0.029583 | 0.507246 | 1.060311 |
| 14 | pastry | bread | 0.086107 | 0.327205 | 0.029160 | 0.338650 | 1.034977 |
10. Visualización Científica del Modelo: Soporte vs Confianza (Lift)¶
Este scatter plot permite mapear las reglas según su soporte y confianza, coloreándolas en base a la fuerza del Lift.
plt.figure(figsize=(12, 7))
scatter = plt.scatter(rules_filtered['support'], rules_filtered['confidence'],
c=rules_filtered['lift'], cmap='coolwarm', s=100, alpha=0.85, edgecolors='black')
plt.title("Distribución de Reglas de Asociación - Soporte vs Confianza", fontsize=14, pad=15, fontweight='bold')
plt.xlabel("Soporte (Frecuencia Conjunta)", fontsize=12, labelpad=10)
plt.ylabel("Confianza (Probabilidad Condicional)", fontsize=12, labelpad=10)
# Barra de color para representar el Lift
cbar = plt.colorbar(scatter)
cbar.set_label('Lift (Fuerza de la Regla)', rotation=270, labelpad=20, fontsize=12)
# Resaltar con anotaciones las 3 reglas con mayor Lift
for idx, row in rules_filtered.head(3).iterrows():
ant_str = ", ".join(list(row['antecedents']))
cons_str = ", ".join(list(row['consequents']))
plt.annotate(f"{ant_str} -> {cons_str}",
(row['support'], row['confidence']),
textcoords="offset points",
xytext=(0,10),
ha='center',
fontsize=9,
fontweight='bold',
bbox=dict(boxstyle="round,pad=0.3", fc="yellow", alpha=0.5))
plt.tight_layout()
plt.show()
11. Conclusiones e Interpretación de Resultados Estratégicos¶
Sobre el Clustering (K-Means)¶
La segmentación mediante K-Means reveló tres perfiles de cliente diferenciados por horario, volumen de compra y afinidad al café. Esta información permite diseñar estrategias de atención al cliente y producción diferenciadas según la franja horaria.
Sobre las Reglas de Asociación (Apriori)¶
A partir del filtrado de reglas por fuerza de Lift ($Lift > 1$), se extraen las siguientes observaciones clave de alta relevancia para la panadería:
- Regla [cake] -> [coffee] ($Lift = 2.01, Confianza = 52.7%$): Adquirir pastel (cake) duplica la probabilidad de comprar café en comparación con la tasa general de compra de café. Es la sinergia individual más alta. Promoción sugerida: "Combo Tarde Dulce".
- Regla [pastry] -> [coffee] ($Lift = 1.88, Confianza = 55.2%$): Más de la mitad de las transacciones con facturas de hojaldre/panecillo (pastry) van acompañadas de café. Es un clásico de desayuno. Ubicación sugerida: colocar las vitrinas de hojaldre directamente junto a la máquina de café.
- Regla [medialuna] -> [coffee] ($Lift = 1.89, Confianza = 56.9%$): Comportamiento idéntico al hojaldre. Alta tasa de conversión.
- Regla [sandwich] -> [coffee] ($Lift = 1.47, Confianza = 53.2%$): Almuerzos ligeros. Una gran oportunidad de venta cruzada ofreciendo un descuento pequeño en café por la compra del sándwich a partir del mediodía.
Integración de Ambas Técnicas¶
La combinación del clustering (que identifica cuándo y cuánto compran los clientes) con las reglas de asociación (que identifican qué compran juntos) proporciona una visión integral 360° del comportamiento del consumidor, habilitando decisiones comerciales de alto impacto fundamentadas en la evidencia de los datos.