{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Que corra en la máquina de otro\n",
    "\n",
    "Por qué un joblib no viaja solo, qué le falta, y cómo se empaqueta el modelo con su código y sus versiones para que arranque igual en cualquier sitio.\n",
    "\n",
    "Cuaderno de práctica del capítulo 26 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/que-corra-en-otra-maquina/\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"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# Este capitulo abre el archivo por su nombre, sin URL, que es lo que se hace\n",
    "# cuando lo tienes al lado. Aqui se baja primero para que exista.\n",
    "for nombre in ['ventas-miss-yera.csv']:\n",
    "    urllib.request.urlretrieve(f\"https://missyera.com/static/datasets/{nombre}\", nombre)\n",
    "    print(nombre, \"lista\")"
   ]
  },
  {
   "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: \"YWhvcmEgc2kgY2FyZ2EgeSBwcmVkaWNlOiBbMV0KZWwgbW9kZWxvIG51bmNhIGVzdHV2byByb3RvOiAxIGNvbHVtbmE=\",\n",
    "    2: \"Y2FyZ28gc2luIHByb2JsZW1hcyB5IHByZWRpY2U6IFsxXQptZW5jaW9uYSBsaW1waWV6YSA6IEZhbHNl\",\n",
    "    3: \"cmVxdWlyZW1lbnRzLnR4dCAtPiAgc2Npa2l0LWxlYXJuCnJlcXVpcmVtZW50cy50eHQgLT4gIHBhbmRhcwpyZXF1aXJlbWVudHMudHh0IC0+ICBudW1weQpyZXF1aXJlbWVudHMudHh0IC0+ICBqb2JsaWIKY3VhbnRhcyBsaW5lYXM6IDQ=\",\n",
    "    4: \"ZWwgY3N2IHBlc2EgICAgIDI3Mi4zIEtCCmVsIG1vZGVsbyBwZXNhICAxLjMgS0IKbWVuY2lvbmEgZWwgbm9tYnJlIGRlbCBjc3Y6IEZhbHNl\",\n",
    "    5: \"bWF4aW1hIGRpZmVyZW5jaWE6IDAuMAppZGVudGljb3MgICAgICAgIDogVHJ1ZQ==\",\n",
    "    6: \"ICBOTyBzdWJpciAgIC5lbnYKICB2YSAgICAgICAgYXBpLnB5CiAgTk8gc3ViaXIgICBleHBsb3JhY2lvbi5pcHluYgogIHZhICAgICAgICBsaW1waWV6YS5weQogIHZhICAgICAgICBtb2RlbG8uam9ibGliCiAgTk8gc3ViaXIgICB2ZW50YXMuY3N2\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tienes la API del capítulo 25 corriendo en tu\n",
    "computadora. Ahora hay que llevarla a un servidor, y ahí empieza la frase más\n",
    "cara del oficio: **\"en mi máquina funciona\"** 🙃"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Primero, por qué el archivo no viaja solo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Casi todo el mundo cree que un `.joblib` se basta a sí mismo. No.\n",
    "Vamos a verlo, y de paso hago las cosas como hay que hacerlas: la función de\n",
    "limpieza en su propio archivo, no suelta en el cuaderno."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import sys\n",
    "import tempfile\n",
    "import joblib\n",
    "from sklearn.preprocessing import FunctionTransformer\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "\n",
    "carpeta = tempfile.mkdtemp()\n",
    "with open(os.path.join(carpeta, 'limpieza.py'), 'w') as f:\n",
    "    f.write('def limpia_ciudad(X):\\n    return X\\n')\n",
    "\n",
    "sys.path.insert(0, carpeta)\n",
    "import limpieza\n",
    "\n",
    "pipe = Pipeline([('limpia', FunctionTransformer(limpieza.limpia_ciudad)),\n",
    "                 ('modelo', LogisticRegression())]).fit([[0.0], [1.0]], [0, 1])\n",
    "\n",
    "ruta = os.path.join(carpeta, 'modelo.joblib')\n",
    "joblib.dump(pipe, ruta)\n",
    "\n",
    "crudo = open(ruta, 'rb').read()\n",
    "print('el archivo guarda el NOMBRE de tu funcion:', b'limpia_ciudad' in crudo)\n",
    "print('y el modulo donde vivia                 :', b'limpieza' in crudo)\n",
    "print('pero NO guarda su codigo                :', b'return X' not in crudo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lee las tres líneas juntas, porque ahí está todo el capítulo. El archivo sabe\n",
    "**cómo se llama** tu función y **de dónde** sacarla, y\n",
    "no lleva dentro ni una línea de lo que hace.\n",
    "\n",
    "Es una nota que dice *\"acá va la función limpia_ciudad, que está en el\n",
    "módulo limpieza\"*. Y una nota solo sirve si lo que apunta existe.\n",
    "\n",
    "Ahora hago desaparecer ese módulo, que es exactamente lo que pasa cuando el\n",
    "archivo llega a un servidor donde tu proyecto no está:"
   ]
  },
  {
   "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",
    "    del sys.modules['limpieza']\n",
    "    sys.path.remove(carpeta)\n",
    "\n",
    "    joblib.load(ruta)\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",
    "ModuleNotFoundError: No module named 'limpieza'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El modelo está intacto dentro del archivo. Lo que falta es el\n",
    "**código** para rearmarlo.\n",
    "\n",
    "Y esto no es un caso raro de laboratorio: en cuanto metes un\n",
    "`FunctionTransformer` con una limpieza tuya, que es lo que recomendé\n",
    "en el capítulo 10, ya estás aquí. Y si esa\n",
    "función la tenías suelta en el cuaderno en vez de en un archivo, estás peor,\n",
    "porque entonces ni siquiera hay un módulo que copiar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y hay una segunda cosa que tampoco viaja"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las versiones. Un pipeline guardado con una versión de scikit-learn y cargado\n",
    "con otra puede fallar al cargar, o peor, cargar y **predecir distinto**,\n",
    "porque el valor por defecto de algún parámetro cambió entre medio.\n",
    "\n",
    "Así que lo que hace falta llevar junto son tres cosas:\n",
    "\n",
    "- El archivo del modelo.\n",
    "\n",
    "- Tu código, el de las funciones de limpieza y el de la API.\n",
    "\n",
    "- Las versiones exactas de todo lo que importas.\n",
    "\n",
    "Un contenedor es una caja que lleva las tres, y encima el Python con el que\n",
    "las corres 📦"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Fija las versiones, y fíjalas de verdad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto es lo mínimo, y se hace antes de pensar en Docker. En\n",
    "`requirements.txt`:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "scikit-learn==1.7.2\n",
    "pandas==2.3.1\n",
    "numpy==2.1.3\n",
    "joblib==1.4.2\n",
    "fastapi==0.115.6\n",
    "uvicorn==0.34.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con el `==`, no con `>=` ni sin nada. Los números de\n",
    "arriba son un ejemplo: los tuyos salen de correr `pip freeze` en el\n",
    "entorno donde el modelo funcionó, y son los que tienes que apuntar el día que\n",
    "entrenaste, no el día que despliegas.\n",
    "\n",
    "Esta es la parte que la gente se salta porque parece burocracia, y es la que\n",
    "te salva. Sin versiones fijas, tu servicio funciona hoy y deja de funcionar\n",
    "dentro de cuatro meses sin que nadie haya tocado una línea, porque una librería\n",
    "sacó versión nueva."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El Dockerfile"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un archivo llamado `Dockerfile`, sin extensión, al lado de tu\n",
    "código:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "FROM python:3.11-slim\n",
    "\n",
    "WORKDIR /app\n",
    "\n",
    "COPY requirements.txt .\n",
    "RUN pip install --no-cache-dir -r requirements.txt\n",
    "\n",
    "COPY modelo.joblib .\n",
    "COPY limpieza.py api.py .\n",
    "\n",
    "EXPOSE 8000\n",
    "CMD [\"uvicorn\", \"api:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\"]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo de arriba abajo, que es como se ejecuta:\n",
    "\n",
    "- **FROM**: de qué partes. `slim` es la versión sin\n",
    "cosas de más, y pesa como una quinta parte de la normal.\n",
    "\n",
    "- **WORKDIR**: la carpeta donde va a vivir todo dentro de la\n",
    "caja.\n",
    "\n",
    "- **COPY requirements y RUN pip antes de copiar tu código.** Esto\n",
    "parece un detalle de orden y no lo es: Docker guarda cada paso, y si tu código\n",
    "cambia pero las dependencias no, se salta la instalación entera. Es la diferencia\n",
    "entre esperar dos minutos o dos segundos cada vez que cambias una línea.\n",
    "\n",
    "- **COPY del modelo y del código.** Los tres que faltaban.\n",
    "\n",
    "- **CMD**: qué se ejecuta al arrancar. El\n",
    "`--host 0.0.0.0` no es opcional: sin eso, la API solo escucha dentro\n",
    "de la caja y desde fuera parece muerta. Es el error número uno de la primera vez\n",
    "que alguien mete una API en un contenedor 🧷\n",
    "\n",
    "Y se usa así:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "docker build -t modelo-ventas .\n",
    "docker run -p 8000:8000 modelo-ventas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `-p 8000:8000` conecta el puerto de tu máquina con el de dentro.\n",
    "Si te lo saltas, el contenedor corre y no lo alcanza nadie."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El archivo que evita que subas medio disco"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un `.dockerignore`, hermano del `.gitignore` del\n",
    "capítulo de ignorar archivos de mi libro de Git:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    ".git\n",
    "__pycache__\n",
    "*.ipynb\n",
    "datos/\n",
    ".venv\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin esto, el `COPY` se lleva tus cuadernos, tus datos crudos y la\n",
    "carpeta `.git` entera. He visto imágenes de tres gigas que en realidad\n",
    "eran cuarenta megas y el resto era historia de Git."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dónde ponerlo a correr"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con la imagen construida, ya puedes usarla en cualquier sitio que acepte\n",
    "contenedores. Sin recomendarte ninguno en particular, porque los precios cambian\n",
    "cada semana, las tres formas que hay son estas:\n",
    "\n",
    "- **Un servicio que corre contenedores por ti.** Le das el\n",
    "repositorio, él construye y lo levanta. Es lo más rápido para empezar y lo más\n",
    "caro por unidad si crece.\n",
    "\n",
    "- **Una máquina virtual tuya.** Instalas Docker y corres el\n",
    "contenedor. Más barato, y el mantenimiento es tuyo.\n",
    "\n",
    "- **El servicio de contenedores de una nube grande.** Escala solo\n",
    "y la factura es difícil de predecir hasta que aprendes a leerla.\n",
    "\n",
    "Mi consejo para un primer modelo: el más simple de los tres. Si el proyecto\n",
    "crece hasta que el precio duela, moverte va a ser fácil justamente porque está\n",
    "en un contenedor 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que un contenedor NO arregla"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Te lo digo porque se vende como si arreglara todo:\n",
    "\n",
    "- **No arregla la deriva.** Tu modelo empaquetado se puede quedar\n",
    "obsoleto igual, y eso es el capítulo 27.\n",
    "\n",
    "- **No arregla los datos sucios.** Si entra una ciudad que el\n",
    "modelo no vio, dentro de la caja pasa exactamente lo mismo que fuera.\n",
    "\n",
    "- **No hace tu modelo más rápido.** Es la misma cuenta con un\n",
    "sombrero.\n",
    "\n",
    "- **No es una máquina virtual.** Comparte el núcleo del sistema,\n",
    "así que arranca en segundos y pesa mucho menos, y tampoco te aísla igual de\n",
    "bien."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Un joblib guarda los nombres de tus funciones, no su código.\n",
    "\n",
    "- Si el código no está del otro lado, revienta al cargar.\n",
    "\n",
    "- Hay que llevar tres cosas juntas: modelo, código y versiones.\n",
    "\n",
    "- Fija versiones con `==`, salidas de `pip freeze` el\n",
    "día que entrenaste.\n",
    "\n",
    "- Copia las dependencias antes que el código, para aprovechar la caché.\n",
    "\n",
    "- `--host 0.0.0.0` y `-p`, o nadie lo alcanza.\n",
    "\n",
    "- Un `.dockerignore` o subes tu historia de Git entera.\n",
    "\n",
    "- El contenedor no arregla la deriva ni los datos sucios."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Guardas el pipeline con joblib y lo mandas a un servidor. Allá revienta con \"Can't get attribute\". ¿Qué falta?\n",
    "\n",
    "a) El código de tus funciones: el archivo guarda su nombre, no su cuerpo\n",
    "\n",
    "b) El dataset con el que se entrenó\n",
    "\n",
    "c) Volver a entrenarlo en el servidor\n",
    "\n",
    "d) Una versión más nueva de scikit-learn"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Devuelve el módulo a su sitio y míralo revivir\n",
    "\n",
    "El arreglo es literalmente lo que hace un COPY del\n",
    "Dockerfile."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Un modelo sin funciones tuyas sí viaja\n",
    "\n",
    "Comprueba que el problema es tu código, no joblib."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Escribe tu requirements con las versiones de verdad\n",
    "\n",
    "Las tuyas, no las que copies de un tutorial."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El modelo no lleva los datos dentro\n",
    "\n",
    "Una duda razonable cuando vas a mandarlo a un servidor\n",
    "ajeno."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 4\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. La prueba que va antes de desplegar\n",
    "\n",
    "Que lo que cargas prediga exactamente lo que predecía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Qué NO debería entrar en la imagen\n",
    "\n",
    "El `.dockerignore`, comprobado en vez de\n",
    "supuesto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 26 de **Machine learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/machine-learning-desde-cero/que-corra-en-otra-maquina/\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
}
