{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fbe163cd",
   "metadata": {},
   "source": [
    "# COMM04 · IA con Python para marketing\n",
    "**Verde Mediterráneo** (tienda online ficticia de cosmética natural). Cuaderno de prácticas del Módulo 3B.\n",
    "\n",
    "Objetivos:\n",
    "1. Plantear y entrenar un modelo de **aprendizaje automático** que prediga qué clientes volverán a comprar en 90 días.\n",
    "2. Evaluarlo, interpretarlo y usarlo para una **acción de marketing**.\n",
    "3. Caracterizar un **autoencoder** (detección de clientes atípicos).\n",
    "4. Caracterizar una **red neuronal convolucional** (clasificación de imágenes de productos de moda).\n",
    "\n",
    "> Ejecuta las celdas en orden con ▶ o `Mayús + Intro`. En Colab: *Entorno de ejecución › Ejecutar todas*."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5cca5542",
   "metadata": {},
   "source": [
    "## 0 · Preparación y carga de datos\n",
    "Si trabajas en Google Colab, sube `clientes.csv` con el icono de carpeta 📁 de la izquierda o pega en `DATA_URL` la dirección del archivo en el servidor del curso."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d33e75da",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os, numpy as np, pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "DATA_URL = \"\"  # opcional: pega aquí la URL de clientes.csv\n",
    "\n",
    "if os.path.exists(\"clientes.csv\"):\n",
    "    df = pd.read_csv(\"clientes.csv\")\n",
    "elif DATA_URL:\n",
    "    df = pd.read_csv(DATA_URL)\n",
    "else:\n",
    "    from google.colab import files          # solo funciona en Colab\n",
    "    files.upload()\n",
    "    df = pd.read_csv(\"clientes.csv\")\n",
    "\n",
    "print(df.shape)\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "db2e002c",
   "metadata": {},
   "source": [
    "## 1 · Entender los datos\n",
    "La variable objetivo es `recompra_90d` (1 = volvió a comprar entre enero y marzo de 2026)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8542321a",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Tasa de recompra:\", round(df[\"recompra_90d\"].mean() * 100, 1), \"%\")\n",
    "df.describe().round(2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e307f5a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# ¿Cómo cambia la recompra según el número de pedidos y la recencia?\n",
    "print(df.groupby(\"num_pedidos\")[\"recompra_90d\"].mean().round(2).head(8))\n",
    "df[\"recencia_tramo\"] = pd.cut(df[\"dias_desde_ultima_compra\"], [-1, 30, 90, 180, 365], labels=[\"0-30\", \"31-90\", \"91-180\", \"181-365\"])\n",
    "df.groupby(\"recencia_tramo\", observed=True)[\"recompra_90d\"].mean().round(2).plot(kind=\"bar\", title=\"Recompra según días desde la última compra\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0241042d",
   "metadata": {},
   "source": [
    "## 2 · Plantear el algoritmo\n",
    "- **Problema**: clasificación binaria supervisada.\n",
    "- **Variables (features)**: comportamiento e interacción del cliente. Excluimos el identificador.\n",
    "- **Validación**: 80 % entrenamiento, 20 % prueba, estratificado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2ee2db48",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "num = [\"edad\", \"suscrito_newsletter\", \"aperturas_email_90d\", \"visitas_web_90d\", \"num_pedidos\",\n",
    "       \"gasto_total\", \"ticket_medio\", \"dias_desde_ultima_compra\"]\n",
    "X = pd.get_dummies(df[num + [\"canal_captacion\"]], columns=[\"canal_captacion\"], drop_first=True)\n",
    "y = df[\"recompra_90d\"]\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)\n",
    "print(\"Entrenamiento:\", X_train.shape, \"· Prueba:\", X_test.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a312f24c",
   "metadata": {},
   "source": [
    "## 3 · Entrenar y evaluar dos modelos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9083311a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.pipeline import make_pipeline\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.ensemble import RandomForestClassifier\n",
    "from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score, confusion_matrix\n",
    "\n",
    "modelos = {\n",
    "    \"Regresión logística\": make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),\n",
    "    \"Random forest\": RandomForestClassifier(n_estimators=300, max_depth=6, random_state=42),\n",
    "}\n",
    "resultados = {}\n",
    "for nombre, m in modelos.items():\n",
    "    m.fit(X_train, y_train)\n",
    "    pred = m.predict(X_test)\n",
    "    prob = m.predict_proba(X_test)[:, 1]\n",
    "    resultados[nombre] = {\"exactitud\": accuracy_score(y_test, pred), \"precisión\": precision_score(y_test, pred),\n",
    "                          \"exhaustividad\": recall_score(y_test, pred), \"AUC\": roc_auc_score(y_test, prob)}\n",
    "pd.DataFrame(resultados).T.round(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80d197bc",
   "metadata": {},
   "outputs": [],
   "source": [
    "mejor = modelos[\"Regresión logística\"]\n",
    "cm = confusion_matrix(y_test, mejor.predict(X_test))\n",
    "print(\"Matriz de confusión (filas = real, columnas = predicho)\\n\", pd.DataFrame(cm, index=[\"no recompra\", \"recompra\"], columns=[\"pred. no\", \"pred. sí\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e8e0e67",
   "metadata": {},
   "source": [
    "## 4 · Interpretar el modelo\n",
    "¿Qué variables pesan más? En la regresión logística, el signo del coeficiente indica si aumenta (+) o reduce (−) la probabilidad de recompra."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce6bd3f0",
   "metadata": {},
   "outputs": [],
   "source": [
    "coef = pd.Series(mejor.named_steps[\"logisticregression\"].coef_[0], index=X.columns).sort_values()\n",
    "coef.plot(kind=\"barh\", title=\"Coeficientes (variables estandarizadas)\")\n",
    "plt.show()\n",
    "coef.round(2)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12861d07",
   "metadata": {},
   "source": [
    "## 5 · De la predicción a la acción de marketing\n",
    "Puntuamos a **todos** los clientes y buscamos los de **alto valor con baja probabilidad de recompra** para una campaña de reactivación."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "88e23adf",
   "metadata": {},
   "outputs": [],
   "source": [
    "df[\"prob_recompra\"] = mejor.predict_proba(X)[:, 1].round(3)\n",
    "umbral_valor = df[\"gasto_total\"].quantile(0.75)\n",
    "riesgo = df[(df[\"gasto_total\"] >= umbral_valor) & (df[\"prob_recompra\"] < 0.4)].sort_values(\"gasto_total\", ascending=False)\n",
    "print(\"Clientes de alto valor en riesgo:\", len(riesgo))\n",
    "riesgo[[\"id_cliente\", \"gasto_total\", \"num_pedidos\", \"dias_desde_ultima_compra\", \"prob_recompra\"]].head(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d58f791e",
   "metadata": {},
   "outputs": [],
   "source": [
    "riesgo[[\"id_cliente\", \"prob_recompra\"]].to_csv(\"clientes_en_riesgo.csv\", index=False)\n",
    "print(\"Guardado clientes_en_riesgo.csv (descárgalo desde el panel de archivos)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a071b97e",
   "metadata": {},
   "source": [
    "## 6 · Autoencoder: clientes atípicos\n",
    "Un **autoencoder** aprende a comprimir los datos (8 variables → 2) y a reconstruirlos. Los clientes que reconstruye peor son **atípicos**: posibles errores de datos, revendedores o clientes VIP excepcionales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "49c714df",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "from tensorflow import keras\n",
    "tf.keras.utils.set_random_seed(42)\n",
    "\n",
    "Xn = StandardScaler().fit_transform(df[num])\n",
    "entrada = keras.Input(shape=(len(num),))\n",
    "h = keras.layers.Dense(4, activation=\"relu\")(entrada)\n",
    "codigo = keras.layers.Dense(2, activation=\"linear\", name=\"codigo\")(h)\n",
    "h2 = keras.layers.Dense(4, activation=\"relu\")(codigo)\n",
    "salida = keras.layers.Dense(len(num), activation=\"linear\")(h2)\n",
    "autoencoder = keras.Model(entrada, salida)\n",
    "autoencoder.compile(optimizer=\"adam\", loss=\"mse\")\n",
    "hist = autoencoder.fit(Xn, Xn, epochs=60, batch_size=32, verbose=0, validation_split=0.1)\n",
    "print(\"Pérdida final (entrenamiento / validación):\", round(hist.history[\"loss\"][-1], 3), \"/\", round(hist.history[\"val_loss\"][-1], 3))\n",
    "autoencoder.summary()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47b81d7a",
   "metadata": {},
   "outputs": [],
   "source": [
    "error = np.mean((autoencoder.predict(Xn, verbose=0) - Xn) ** 2, axis=1)\n",
    "df[\"error_reconstruccion\"] = error\n",
    "atipicos = df.sort_values(\"error_reconstruccion\", ascending=False).head(15)\n",
    "print(\"Umbral del 1 % más atípico:\", round(np.quantile(error, 0.99), 2))\n",
    "atipicos[[\"id_cliente\", \"num_pedidos\", \"gasto_total\", \"visitas_web_90d\", \"aperturas_email_90d\", \"error_reconstruccion\"]].head(8)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed5662c5",
   "metadata": {},
   "source": [
    "## 7 · Red neuronal convolucional (CNN): reconocer productos\n",
    "Usamos **Fashion-MNIST**: 70.000 imágenes de 28×28 píxeles de 10 tipos de prendas y calzado (camiseta, pantalón, zapatilla, bolso…). Una CNN aprende filtros que detectan bordes y formas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e04e699",
   "metadata": {},
   "outputs": [],
   "source": [
    "(x_tr, y_tr), (x_te, y_te) = keras.datasets.fashion_mnist.load_data()\n",
    "clases = [\"Camiseta\", \"Pantalón\", \"Jersey\", \"Vestido\", \"Abrigo\", \"Sandalia\", \"Camisa\", \"Zapatilla\", \"Bolso\", \"Botín\"]\n",
    "x_tr, x_te = x_tr[..., None] / 255.0, x_te[..., None] / 255.0\n",
    "print(x_tr.shape, x_te.shape)\n",
    "plt.figure(figsize=(8, 2))\n",
    "for i in range(8):\n",
    "    plt.subplot(1, 8, i + 1); plt.imshow(x_tr[i, ..., 0], cmap=\"gray\"); plt.title(clases[y_tr[i]], fontsize=7); plt.axis(\"off\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "194b708c",
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.keras.utils.set_random_seed(42)\n",
    "cnn = keras.Sequential([\n",
    "    keras.Input(shape=(28, 28, 1)),\n",
    "    keras.layers.Conv2D(32, 3, activation=\"relu\"),\n",
    "    keras.layers.MaxPooling2D(),\n",
    "    keras.layers.Conv2D(64, 3, activation=\"relu\"),\n",
    "    keras.layers.MaxPooling2D(),\n",
    "    keras.layers.Flatten(),\n",
    "    keras.layers.Dense(64, activation=\"relu\"),\n",
    "    keras.layers.Dense(10, activation=\"softmax\"),\n",
    "])\n",
    "cnn.compile(optimizer=\"adam\", loss=\"sparse_categorical_crossentropy\", metrics=[\"accuracy\"])\n",
    "cnn.summary()\n",
    "cnn.fit(x_tr, y_tr, epochs=3, batch_size=128, validation_split=0.1, verbose=2)\n",
    "perdida, exactitud = cnn.evaluate(x_te, y_te, verbose=0)\n",
    "print(\"Exactitud en prueba:\", round(exactitud, 3))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f6f8ad36",
   "metadata": {},
   "outputs": [],
   "source": [
    "pred = cnn.predict(x_te[:12], verbose=0).argmax(axis=1)\n",
    "plt.figure(figsize=(10, 2.5))\n",
    "for i in range(12):\n",
    "    plt.subplot(2, 6, i + 1); plt.imshow(x_te[i, ..., 0], cmap=\"gray\"); plt.axis(\"off\")\n",
    "    plt.title(clases[pred[i]], fontsize=7, color=\"green\" if pred[i] == y_te[i] else \"red\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8bd41af",
   "metadata": {},
   "source": [
    "## 8 · Diseño de una solución de IA (para tu proyecto)\n",
    "Responde en esta celda (doble clic para editar):\n",
    "1. **Problema de negocio** y KPI que mejorará.\n",
    "2. **Datos** necesarios, base legal y calidad.\n",
    "3. **Modelo** elegido y por qué (rendimiento frente a explicabilidad).\n",
    "4. **Integración**: cómo llegan las predicciones a la acción (lista a Google Ads, segmento de email, panel en Looker Studio).\n",
    "5. **Supervisión**: cada cuánto se reentrena, cómo se detecta la deriva, qué revisa una persona.\n",
    "6. **Riesgos**: sesgos, privacidad, transparencia (Reglamento de IA, RGPD art. 22)."
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
