{
 "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 soluciones 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",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "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": [
    "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\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* El modelo ya aprendió, no necesita los datos otra vez. Lo que le falta es poder reconstruir los objetos que hay dentro del archivo.\n",
    "\n",
    "*c)* Entrenar de nuevo lo arregla y es la peor forma: acabas con dos modelos distintos y ya no sabes cuál contestó qué.\n",
    "\n",
    "*d)* La versión importa para otras cosas y no para esta. Ese error habla de un nombre que el proceso no encuentra, no de una versión.\n",
    "\n",
    "Un joblib no es autosuficiente: es una lista de instrucciones para rearmar objetos que tienen que existir del otro lado 📦"
   ]
  },
  {
   "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": [
    "sys.path.insert(0, carpeta)\n",
    "\n",
    "recuperado = joblib.load(ruta)\n",
    "print('ahora si carga y predice:', recuperado.predict([[1.0]]))\n",
    "print('el modelo nunca estuvo roto:',\n",
    "      recuperado.named_steps['modelo'].n_features_in_, 'columna')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ahora si carga y predice: [1]\n",
    "el modelo nunca estuvo roto: 1 columna\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No hubo que reentrenar nada ni tocar el archivo: bastó con que el módulo\n",
    "estuviera donde el proceso pudiera encontrarlo. Esa línea de\n",
    "`sys.path.insert` es, en pequeño, lo mismo que hace\n",
    "`COPY limpieza.py` dentro del contenedor."
   ]
  },
  {
   "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": [
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "limpio = Pipeline([('esc', StandardScaler()),\n",
    "                   ('modelo', LogisticRegression())]).fit([[0.0], [1.0]], [0, 1])\n",
    "ruta2 = os.path.join(carpeta, 'sin_funcion.joblib')\n",
    "joblib.dump(limpio, ruta2)\n",
    "\n",
    "otro = joblib.load(ruta2)\n",
    "print('cargo sin problemas y predice:', otro.predict([[1.0]]))\n",
    "print('menciona limpieza :', b'limpieza' in open(ruta2, 'rb').read())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "cargo sin problemas y predice: [1]\n",
    "menciona limpieza : False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin código tuyo dentro, el archivo solo pide clases de scikit-learn,\n",
    "que van a existir en cualquier máquina que tenga scikit-learn instalado. Por eso\n",
    "un pipeline hecho solo con piezas de la librería es mucho más fácil de mover, y\n",
    "por eso conviene usar las piezas de fábrica siempre que se pueda."
   ]
  },
  {
   "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": [
    "import sklearn\n",
    "import pandas\n",
    "import numpy\n",
    "\n",
    "paquetes = {'scikit-learn': sklearn, 'pandas': pandas,\n",
    "            'numpy': numpy, 'joblib': joblib}\n",
    "lineas = [n + '==' + m.__version__ for n, m in paquetes.items()]\n",
    "print('\\n'.join('requirements.txt ->  ' + l.split('==')[0] for l in lineas))\n",
    "print('cuantas lineas:', len(lineas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "requirements.txt ->  scikit-learn\n",
    "requirements.txt ->  pandas\n",
    "requirements.txt ->  numpy\n",
    "requirements.txt ->  joblib\n",
    "cuantas lineas: 4\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Imprimo solo los nombres a propósito, porque las versiones de tu máquina no\n",
    "son las mías y este libro no te puede mentir con un número. Quita el\n",
    "`.split` y vas a ver las tuyas con su `==` puesto: esas son\n",
    "las que van al archivo. Y el día que actualices una, lo actualizas ahí y\n",
    "reconstruyes la imagen, que es justamente la gracia de tener esto escrito."
   ]
  },
  {
   "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": [
    "print('el csv pesa    ', round(os.path.getsize('ventas-miss-yera.csv') / 1024, 1), 'KB')\n",
    "print('el modelo pesa ', round(os.path.getsize(ruta2) / 1024, 1), 'KB')\n",
    "print('menciona el nombre del csv:', b'ventas-miss-yera' in open(ruta2, 'rb').read())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "el csv pesa     272.3 KB\n",
    "el modelo pesa  1.3 KB\n",
    "menciona el nombre del csv: False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El modelo guarda lo que aprendió, no de dónde lo aprendió. Eso es bueno para\n",
    "el tamaño y también para la privacidad: puedes mandar el modelo a un sitio donde\n",
    "no podrías mandar los datos.\n",
    "\n",
    "Con la trampa de que un modelo sí puede filtrar información de sus datos si\n",
    "alguien lo interroga con paciencia, y eso es harina de otro costal."
   ]
  },
  {
   "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": [
    "import numpy as np\n",
    "\n",
    "muestra = [[0.0], [1.0], [0.5]]\n",
    "antes = limpio.predict_proba(muestra)[:, 1]\n",
    "despues = joblib.load(ruta2).predict_proba(muestra)[:, 1]\n",
    "print('maxima diferencia:', float(np.abs(antes - despues).max()))\n",
    "print('identicos        :', bool(np.array_equal(antes, despues)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "maxima diferencia: 0.0\n",
    "identicos        : True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cero exacto, no parecido. Guarda unas cuantas filas con su respuesta el día\n",
    "que entrenaste y corre esta comprobación cada vez que construyas la imagen. Es\n",
    "la forma más barata que hay de cazar un cambio de versión que altera resultados\n",
    "sin dar ningún error."
   ]
  },
  {
   "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": [
    "revisar = tempfile.mkdtemp()\n",
    "for nombre in ('api.py', 'limpieza.py', 'modelo.joblib', '.env',\n",
    "               'ventas.csv', 'exploracion.ipynb'):\n",
    "    open(os.path.join(revisar, nombre), 'w').write('x')\n",
    "\n",
    "NUNCA = ('.env', '.ipynb', '.csv', '.git')\n",
    "for archivo in sorted(os.listdir(revisar)):\n",
    "    peligro = any(archivo.endswith(x) or archivo == x for x in NUNCA)\n",
    "    print(('  NO subir  ' if peligro else '  va       '), archivo)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  NO subir   .env\n",
    "  va        api.py\n",
    "  NO subir   exploracion.ipynb\n",
    "  va        limpieza.py\n",
    "  va        modelo.joblib\n",
    "  NO subir   ventas.csv\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres de seis archivos no tienen nada que hacer dentro de la imagen, y el\n",
    "`.env` es el que quita el sueño: ahí viven las claves. Una imagen es\n",
    "un archivo que se copia, se sube a un registro y se queda ahí; lo que metas\n",
    "dentro, metido está.\n",
    "\n",
    "Y ojo, que borrarlo en una línea posterior del Dockerfile no lo quita: cada\n",
    "paso queda guardado y la clave sigue en la capa de antes. Es el primo hermano de\n",
    "lo que cuento en el capítulo de subir una clave sin querer de mi libro de Git."
   ]
  },
  {
   "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
}
