{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Abrir el archivo y pelearse con la suciedad\n",
    "\n",
    "37 filas repetidas, Lima escrita de cuatro formas, medio millón de soles que se evaporan sin dar error, y unos huecos que predicen.\n",
    "\n",
    "Cuaderno de práctica del capítulo 4 de **Machine learning desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/machine-learning-desde-cero/eda-y-limpieza/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"ICAgICAgIHVuaWRhZGVzICAgICBtb250byAgZGVzY3VlbnRvICBzYXRpc2ZhY2Npb24KY291bnQgIDMwMDAuMDAwICAzMDAwLjAwMCAgIDI0MDUuMDAwICAgICAgMjc2OS4wMDAKbWVhbiAgICAgMTEuNjAxICAgODAzLjc2MSAgICAgIDAuMTI1ICAgICAgICAgMy4wMTAKc3RkICAgICAgIDUuNzc1ICAgNzUxLjk4OSAgICAgIDAuMDcyICAgICAgICAgMS40MjEKbWluICAgICAgIDEuMDAwIC0yNDk3LjcyMCAgICAgIDAuMDAwICAgICAgICAgMS4wMDAKMjUlICAgICAgIDcuMDAwICAgMjUyLjY5MCAgICAgIDAuMDYzICAgICAgICAgMi4wMDAKNTAlICAgICAgMTIuMDAwICAgNTMzLjYzMCAgICAgIDAuMTI3ICAgICAgICAgMy4wMDAKNzUlICAgICAgMTUuMDAwICAxMDY4LjY4NSAgICAgIDAuMTg1ICAgICAgICAgNC4wMDAKbWF4ICAgICAgMzAuMDAwICA0MjM2LjcxMCAgICAgIDAuMjUwICAgICAgICAgNS4wMDA=\",\n",
    "    3: \"ICAgICAgICAgICAgICAgICAgY291bnQgICAgbWVhbgpjYXRlZ29yaWEgICAgICAgICAgICAgICAgICAgICAgCkFiYXJyb3RlcyAgICAgICAgICAgNjQwICAwLjU5MDYKQmViaWRhcyAgICAgICAgICAgICA1NjUgIDAuNTc4OApDdWlkYWRvIHBlcnNvbmFsICAgIDYwNiAgMC41NTk0CkxpbXBpZXphICAgICAgICAgICAgNTg3ICAwLjU4NzcKU25hY2tzICAgICAgICAgICAgICA2MDIgIDAuNTcxNA==\",\n",
    "    4: \"Y2FuYWwgICAgICAgTWFya2V0cGxhY2UgIFRpZW5kYSAgICBXZWIgIFdoYXRzQXBwCnNlZ21lbnRvICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIApCb2RlZ2EgICAgICAgICAgICAwLjI3NiAgIDAuMzg3ICAwLjM4NSAgICAgMC40NTUKSG9yZWNhICAgICAgICAgICAgMC41MDUgICAwLjc1OSAgMC42MzEgICAgIDAuNjU4Ck1heW9yaXN0YSAgICAgICAgIDAuNjM3ICAgMC43MTIgIDAuNzM3ICAgICAwLjgxOQpNaW5pbWFya2V0ICAgICAgICAwLjQxNiAgIDAuNTk4ICAwLjU3MSAgICAgMC42OTI=\",\n",
    "    5: \"dmVudGFzIGRlIGNsaWVudGVzIG51ZXZvczogNTQ0ICgxOC4xJSkKdGFzYSBkZSBjaWVycmU6IDAuNDEzNg==\",\n",
    "    6: \"MTg5NzI=\",\n",
    "    7: \"KDMwMDAsIDE0KQptb250byAgICAgICAgICAgICAgICAgICAgICAgICBmbG9hdDY0CmZlY2hhICAgICAgICAgICAgICAgICAgZGF0ZXRpbWU2NFt1c10KZmVjaGFfdWx0aW1hX2NvbXByYSAgICBkYXRldGltZTY0W3VzXQpkdHlwZTogb2JqZWN0\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Se dice que el 80% del trabajo es limpiar datos y suena a queja. No lo es: es\n",
    "la descripción del oficio, y es donde se gana o se pierde el proyecto 🧹\n",
    "\n",
    "Una pregunta antes de abrir nada: **¿cuánto tiempo llevas peleándote con un archivo que alguien más llenó a mano?** Porque eso no es mala suerte tuya, es el estado normal de los datos de cualquier empresa 🫠\n",
    "\n",
    "En este capítulo abrimos el archivo de verdad. Trae 37 filas repetidas, Lima\n",
    "escrita de cuatro maneras, 612 montos que Python no va a leer como números, 773\n",
    "fechas en otro formato y tres columnas con huecos. Nada de eso da error.\n",
    "\n",
    "Y al final del capítulo vas a ver la parte que casi nadie mira: que\n",
    "**los huecos de este archivo predicen mejor que la mitad de las\n",
    "columnas**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo primero, siempre lo mismo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "ventas = pd.read_csv(URL)\n",
    "\n",
    "print(ventas.shape)\n",
    "print(ventas.dtypes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí ya hay dos avisos gritando, y hay que saber leerlos:\n",
    "\n",
    "- 💸 `monto` dice `str`. Una columna de dinero que llega\n",
    "como texto significa que algo trae un carácter raro.\n",
    "\n",
    "- 📅 `fecha` y `fecha_ultima_compra` también son texto.\n",
    "Normal al leer un CSV, pero hay que convertirlas y ahí es donde muerde.\n",
    "\n",
    "Mi regla: **si una columna que debería ser número sale como\n",
    "texto, hay suciedad, siempre**. Nunca falla."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las filas repetidas, que se cuelan sin ruido"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('id_venta repetidos:', ventas['id_venta'].duplicated().sum())\n",
    "print('filas idénticas:   ', ventas.duplicated().sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "37 ventas cargadas dos veces, enteras. Es lo típico de un proceso que se\n",
    "ejecutó dos veces, y en un reporte de facturación eso son 37 ventas contadas\n",
    "doble."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas = ventas.drop_duplicates()\n",
    "print(len(ventas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "3.000 filas redonditas. Y ojo con el orden: **quitar duplicados va\n",
    "primero**, antes de cualquier cuenta, porque si no todas las tasas que\n",
    "calcules van a estar contaminadas por esas 37.\n",
    "\n",
    "Cuidado también con lo contrario: si el `id_venta` se repitiera\n",
    "pero las filas fueran distintas, eso *no* es un duplicado, es un problema\n",
    "más feo y hay que preguntar. Aquí las 37 son idénticas, así que fuera."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lima, escrita de cuatro formas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas['ciudad'].value_counts())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está: `lima`, `Líma`, `Lima` y\n",
    "`LIMA`. Con tilde, sin tilde, en mayúsculas y en minúsculas 🫠\n",
    "\n",
    "Para pandas son cuatro ciudades distintas, así que Lima aparece cuatro veces\n",
    "en el fondo de la lista en vez de una sola vez arriba. Cualquier\n",
    "`groupby('ciudad')` te da un reporte partido, y cualquier modelo se\n",
    "come cuatro columnas donde debería haber una."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['ciudad'] = (ventas['ciudad'].str.strip().str.lower()\n",
    "                    .str.normalize('NFKD')\n",
    "                    .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "\n",
    "print(ventas['ciudad'].value_counts())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis ciudades y Lima con 471 ventas. Esa línea hace cuatro cosas y las cuatro\n",
    "hacen falta:\n",
    "\n",
    "- ✂️ `strip()` quita espacios de los bordes.\n",
    "\n",
    "- 🔡 `lower()` unifica mayúsculas.\n",
    "\n",
    "- 🔤 `normalize('NFKD')` separa la letra de su tilde.\n",
    "\n",
    "- 🧽 `encode/decode` tira las tildes sueltas.\n",
    "\n",
    "Y una cosa honesta sobre el resultado: Lima pasa de 123 (su versión más\n",
    "grande) a 471, pero **sigue empatada en el último puesto con\n",
    "Trujillo**. Limpiar no siempre destapa un gigante escondido. Lo que sí\n",
    "hace siempre es que el número deje de estar mal 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El dinero que se evapora sin avisar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta es mi favorita del archivo porque no da error y cuesta plata."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas['monto'].head(8).tolist())\n",
    "print('montos con coma:', ventas['monto'].str.contains(',').sum())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "606 de las 3.000 vienen con coma decimal, a la peruana. Y ahora mira la forma\n",
    "que sale sola de convertirlas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "monto_mal = pd.to_numeric(ventas['monto'], errors='coerce')\n",
    "print('se volvieron nulos:', int(monto_mal.isna().sum()))\n",
    "print('suma:', round(monto_mal.sum(), 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "monto_bien = pd.to_numeric(ventas['monto'].str.replace(',', '.'))\n",
    "print('se volvieron nulos:', int(monto_bien.isna().sum()))\n",
    "print('suma:', round(monto_bien.sum(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "S/1.939.750 contra S/2.411.284. **Se evaporaron S/471.534**, o\n",
    "sea un 19,6% de la facturación, sin un solo error en pantalla 💸\n",
    "\n",
    "El culpable es ese `errors='coerce'`, que significa \"lo que no\n",
    "puedas convertir, ponlo en nulo\". Es cómodo y es peligroso: **siempre que\n",
    "lo uses, cuenta cuántos nulos creó**. Si son más de los que esperabas,\n",
    "ahí está el problema.\n",
    "\n",
    "Sin el `coerce`, pandas te lo dice de frente:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    ventas['monto'].astype(float)\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "ValueError: could not convert string to float: '921,04'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*could not convert string to float: '921,04'*. Te da hasta el valor\n",
    "que le molestó. Mucho mejor que un silencio que te quita medio millón 🙌\n",
    "\n",
    "Y con eso ya se puede guardar la columna buena:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ventas['monto'] = monto_bien\n",
    "print(ventas['monto'].dtype, '| suma:', round(ventas['monto'].sum(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dos formatos de fecha en la misma columna"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    pd.to_datetime(ventas['fecha'], format='%Y-%m-%d')\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "ValueError: time data \"29/06/2025\" doesn't match format \"%Y-%m-%d\". You might want to try:\n",
    "    - passing `format` if your strings have a consistent format;\n",
    "    - passing `format='ISO8601'` if your strings are all ISO8601 but not necessarily in exactly the same format;\n",
    "    - passing `format='mixed'`, and the format will be inferred for each element individually. You might want to use `dayfirst` alongside this.\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "*time data \"29/06/2025\" doesn't match format \"%Y-%m-%d\"*. Hay fechas\n",
    "peruanas mezcladas con fechas ISO.\n",
    "\n",
    "La forma que funciona es en dos pasadas: primero el formato mayoritario, y\n",
    "después el otro solo para las que quedaron sin parsear."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fecha = pd.to_datetime(ventas['fecha'], format='%Y-%m-%d', errors='coerce')\n",
    "faltan = fecha.isna()\n",
    "print('en formato peruano:', int(faltan.sum()))\n",
    "\n",
    "fecha[faltan] = pd.to_datetime(ventas.loc[faltan, 'fecha'],\n",
    "                               format='%d/%m/%Y', errors='coerce')\n",
    "ventas['fecha'] = fecha\n",
    "\n",
    "print('sin parsear al final:', int(fecha.isna().sum()))\n",
    "print('rango:', fecha.min().date(), 'a', fecha.max().date())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "759 de las 3.000 venían a la peruana y ninguna se quedó fuera.\n",
    "\n",
    "Lo que **no** hay que hacer es dejar que pandas adivine fila por\n",
    "fila. Con `29/06/2025` no hay duda porque no hay mes 29, pero con\n",
    "`07/03/2026` sí: puede ser el 7 de marzo o el 3 de julio, y adivinando\n",
    "te cambia el trimestre sin decírtelo. **El formato se dice, no se\n",
    "adivina** 📅"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los atípicos, y por qué no hay una respuesta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este es el punto donde más gente aplica una receta sin mirar lo que hace, así\n",
    "que vamos a mirar 🔬\n",
    "\n",
    "Hay dos formas estándar de marcar un valor como atípico, las dos salen en\n",
    "cualquier tutorial, y las dos son razonables. Aplicadas al mismo monto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "m = pd.to_numeric(ventas['monto'].astype(str).str.replace(',', '.'),\n",
    "                  errors='coerce').dropna()\n",
    "\n",
    "q1, q3 = m.quantile([.25, .75])\n",
    "ri = q3 - q1\n",
    "por_rango = ((m < q1 - 1.5 * ri) | (m > q3 + 1.5 * ri))\n",
    "\n",
    "z = (m - m.mean()) / m.std()\n",
    "por_desviacion = z.abs() > 3\n",
    "\n",
    "print(f'por el rango intercuartil : {por_rango.sum():4d}  ({por_rango.mean():.2%})')\n",
    "print(f'por tres desviaciones     : {por_desviacion.sum():4d}  ({por_desviacion.mean():.2%})')\n",
    "print(f'coinciden en              : {(por_rango & por_desviacion).sum():4d}')\n",
    "print()\n",
    "print('mínimo:', round(m.min(), 2), ' máximo:', round(m.max(), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "203 contra 32. **Seis veces más**, con los mismos datos y las dos\n",
    "recetas que todo el mundo usa 😳\n",
    "\n",
    "Y fíjate en la tercera línea: los 32 están todos dentro de los 203. No son dos\n",
    "grupos distintos, es que uno es mucho más estricto que el otro.\n",
    "\n",
    "El motivo es que el segundo método usa la media y la desviación, y esas dos\n",
    "las mueven los propios atípicos. Un monto de 4.236 estira la desviación, y al\n",
    "estirarla hace que el umbral de \"tres desviaciones\" se aleje, así que ese mismo\n",
    "monto tiene menos probabilidad de ser marcado. El método se protege solo 🙃\n",
    "\n",
    "El primero usa cuartiles, que no se mueven por lo que pasa en los extremos.\n",
    "Por eso marca más.\n",
    "\n",
    "### Entonces, ¿cuál es el bueno?\n",
    "\n",
    "Ninguno. Y esa es la respuesta, no una evasiva 🤷‍♀️\n",
    "\n",
    "\"Atípico\" no es una propiedad del dato, es una decisión tuya sobre qué\n",
    "consideras normal en este negocio. Los datos no la contestan porque no la\n",
    "saben.\n",
    "\n",
    "Lo que sí se puede contestar mirando:\n",
    "\n",
    "- 💀 **Un monto de −2.497,72 es imposible** si una venta no puede\n",
    "ser negativa. Ese no es atípico, es un error, y hay que preguntar qué pasó.\n",
    "\n",
    "- 💰 **Un monto de 4.236,71 puede ser perfectamente real**: un\n",
    "mayorista haciendo un pedido grande. Tirarlo sería tirar justo al cliente que\n",
    "más importa.\n",
    "\n",
    "- 📊 **Los 203 del rango intercuartil son el 6,8% de la tabla.**\n",
    "Si tiras el 6,8% de tus ventas por una fórmula, más vale que sepas a quién\n",
    "tiraste.\n",
    "\n",
    "Mi orden, que es el único que me ha funcionado: **primero separo lo\n",
    "imposible de lo raro**, y con lo imposible voy a preguntar. Lo raro se\n",
    "queda y se marca, porque un modelo que no ha visto pedidos grandes no sabe qué\n",
    "hacer cuando llegue uno 🏷️\n",
    "\n",
    "Y si al final decides recortar, se recorta comprobando: entrena con y sin\n",
    "ellos y mira si el número cambia. Si no cambia, la decisión daba igual y te\n",
    "ahorras defenderla 🧪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los huecos, y la pregunta que casi nadie hace"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(ventas.isna().sum().sort_values(ascending=False).head(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres columnas con huecos. Y aquí viene lo importante del capítulo entero.\n",
    "\n",
    "La reacción normal es rellenarlos con la media y seguir. **Casi siempre\n",
    "está mal**, porque antes hay que contestar una pregunta:\n",
    "*¿por qué falta?*\n",
    "\n",
    "Hay tres respuestas posibles y tienen nombre feo pero significan cosas\n",
    "simples:\n",
    "\n",
    "- 🎲 **MCAR**, falta al azar. Se cayó la conexión, se perdió una\n",
    "fila. Rellenar es razonable.\n",
    "\n",
    "- 🔗 **MAR**, falta según otra columna. Por ejemplo, la\n",
    "satisfacción no se pregunta en el canal web. Se puede rellenar mirando esa otra\n",
    "columna.\n",
    "\n",
    "- 🚨 **MNAR**, falta según su propio valor, o directamente el\n",
    "hueco *significa* algo. Aquí rellenar es destruir información.\n",
    "\n",
    "Vamos a averiguar cuál es cada una, y se hace en tres líneas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for col in ['descuento', 'fecha_ultima_compra', 'satisfaccion']:\n",
    "    hueco = ventas[col].isna()\n",
    "    print(f'{col:22} sin dato: {ventas.loc[hueco, \"compro\"].mean():.4f}'\n",
    "          f'   con dato: {ventas.loc[~hueco, \"compro\"].mean():.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo despacio porque es el hallazgo del capítulo:\n",
    "\n",
    "- 💰 Sin descuento se cierra el 48,1%; con descuento, el 60,2%.\n",
    "**Doce puntos.**\n",
    "\n",
    "- 🆕 Sin fecha de última compra se cierra el 41,4%; con fecha, el 61,4%.\n",
    "**Veinte puntos.**\n",
    "\n",
    "- 😐 La satisfacción vacía casi no cambia nada: 61,9% contra 57,4%.\n",
    "\n",
    "Los dos primeros huecos **no son datos que faltan, son datos en sí\n",
    "mismos**. Que no haya descuento significa que no se ofreció ninguno. Que\n",
    "no haya fecha de última compra significa que es un cliente nuevo, y por eso\n",
    "cierra veinte puntos menos.\n",
    "\n",
    "Si rellenas con la media, borras la señal más fuerte del archivo 😱 Lo que\n",
    "hay que hacer es lo contrario: **convertir el hueco en una columna**,\n",
    "y eso es el capítulo 9."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Siete sobre el archivo ya cargado. Intenta antes de abrir 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. La radiografía de las numéricas\n",
    "\n",
    "Resumen de las columnas numéricas, para cazar rarezas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Los montos extremos\n",
    "\n",
    "Las cinco ventas más grandes y las cinco más chicas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Cuánto pesa cada categoría\n",
    "\n",
    "Cuántas filas y qué tasa de cierre tiene cada categoría de\n",
    "producto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El cruce que sí importa\n",
    "\n",
    "Tasa de cierre por segmento y canal a la vez."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuántos clientes son nuevos\n",
    "\n",
    "Qué porcentaje de las ventas son de clientes sin compra\n",
    "anterior."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. El error de contar antes de limpiar\n",
    "\n",
    "Suma la columna de montos tal como viene del CSV, sin\n",
    "convertirla."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. La limpieza completa, en un bloque\n",
    "\n",
    "Junta todo lo del capítulo en algo que puedas copiar al\n",
    "inicio de cualquier cuaderno."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 7\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "La columna monto llega como texto y to_numeric con errors=coerce la convierte sin quejarse. ¿Qué revisas?\n",
    "\n",
    "a) Cuántos valores se volvieron nulos en la conversión\n",
    "\n",
    "b) Nada, si no dio error está bien\n",
    "\n",
    "c) El tipo final de la columna\n",
    "\n",
    "d) La media, para ver si es razonable"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y una que corre sin quejarse"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "El clásico de los viernes. Hay nulos en descuento, se rellenan con cero y a otra cosa. Es una línea, es lo que hace todo el mundo, y cuesta dinero.\n",
    "\n",
    "```\n",
    "ventas['descuento'] = (ventas['descuento']\n",
    "                       .fillna(0))\n",
    "\n",
    "# \"un nulo en descuento es que no hubo\n",
    "#  descuento, obvio\"\n",
    "```\n",
    "\n",
    "**¿Qué está mal?** La respuesta está en el cuaderno de soluciones. Míralo tú primero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🔍 `dtypes` antes que nada. Una columna de dinero que sale como\n",
    "texto siempre esconde suciedad.\n",
    "\n",
    "- 👯 Los duplicados se quitan primero: aquí eran 37 filas enteras.\n",
    "\n",
    "- 🧽 Cuatro formas de escribir Lima son cuatro ciudades para pandas. Se\n",
    "arregla con strip, lower y normalize.\n",
    "\n",
    "- 💸 `errors='coerce'` convierte lo que no entiende en nulos, sin\n",
    "avisar. Aquí se llevaba S/470.000. Cuenta siempre los nulos que crea.\n",
    "\n",
    "- 📅 El formato de fecha se dice, no se adivina, y se pasa en dos pasadas si\n",
    "hay dos formatos.\n",
    "\n",
    "- 🕳️ Antes de rellenar un hueco, pregunta por qué falta. Aquí, \"sin fecha de\n",
    "última compra\" vale veinte puntos de tasa de cierre.\n",
    "\n",
    "- 🔀 Las tablas cruzadas encuentran lo que ninguna columna sola dice: de 0,265\n",
    "a 0,833 según segmento y canal.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los datos sucios no dan error. Por eso hay que ir a buscarlos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi todo lo de este capítulo es pandas puro, así que si alguna línea te costó, el [libro de Python desde cero](https://missyera.com/guias/python-desde-cero/) la explica desde el principio 🐍\n",
    "\n",
    "En el capítulo 9 convertimos todo lo que acabamos de descubrir en columnas\n",
    "que un modelo pueda usar, y ahí es donde de verdad se gana.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 4 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/eda-y-limpieza/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
