Capítulo 4: Python para IA — desde cero absoluto¶
AI Master Academy · Módulo 01-FUNDAMENTOS · Capítulo 4 Nivel: Principiante absoluto (nunca has programado) → Preparado para Machine Learning Tiempo estimado de estudio: 12-16 horas (hazlo en varias sesiones, con el editor abierto) Última actualización: julio de 2026 · Python 3.12/3.13 ·
uv+pip
Bienvenido al capítulo más importante de tu formación práctica. Hasta ahora hemos hablado de qué es la IA (Capítulo 1), de cómo piensa (Capítulo 2) y de las matemáticas que la sostienen (Capítulo 3). Hoy vas a aprender el idioma en el que se escribe la IA: Python.
No necesitas experiencia previa. Ninguna. Vamos a empezar desde "¿qué es un programa?" y vamos a terminar con un script profesional que analiza un fichero de clientes real, con manejo de errores, funciones tipadas y un informe generado automáticamente. Todo lo que hay entre medias está explicado línea por línea.
Nota
Este capítulo es largo a propósito. Es tu manual de referencia: la primera vez léelo en orden y teclea cada ejemplo (teclear, no copiar-pegar: la memoria muscular importa). Después, vuelve a él cada vez que lo necesites.
Índice¶
- Por qué Python domina la IA
- 1.1 Un poco de historia
- 1.2 El ecosistema: la verdadera razón
- 1.3 Python vs. otros lenguajes para IA
- Tu entorno profesional
- 2.1 Instalar Python correctamente
- 2.2 El REPL: tu laboratorio
- 2.3 Scripts: tu primer programa
- 2.4 VS Code: tu taller
- 2.5 Entornos virtuales: el concepto que separa amateurs de profesionales
- 2.6 uv: el gestor moderno (2026)
- 2.7 pip, requirements.txt y pyproject.toml
- Fundamentos del lenguaje
- 3.1 Variables y tipos
- 3.2 Operadores y condicionales
- 3.3 Bucles: for, while, range, enumerate, zip
- 3.4 Estructuras de datos
- 3.5 Comprehensions
- 3.6 Funciones
- 3.7 Manejo de errores
- 3.8 Archivos: texto, JSON y CSV
- 3.9 Clases y OOP básica
- 3.10 Módulos, imports y estructura de proyecto
- 3.11 Async básico: la intuición
- NumPy: la base numérica de toda la IA
- Pandas: datos tabulares como un profesional
- Ejemplo integrador: del CSV al informe
- Buenas prácticas
- Malas prácticas que debes evitar
- Errores comunes: tabla de diagnóstico
- Preguntas frecuentes (FAQ)
- Resumen del capítulo
- Bibliografía y recursos
1. Por qué Python domina la IA¶
1.1 Un poco de historia¶
Python nació en 1991, creado por Guido van Rossum en los Países Bajos. Su objetivo no era la IA (que entonces vivía uno de sus "inviernos"), sino algo más humilde y más radical: crear un lenguaje legible, donde el código se pareciera lo máximo posible al pseudocódigo que un humano escribiría en una pizarra.
Esa decisión de diseño —legibilidad primero— resultó ser profética. Cuando en los años 2000 los científicos empezaron a necesitar programar (físicos, biólogos, economistas... gente brillante pero que no era programadora profesional), Python era el lenguaje que podían aprender en semanas en lugar de años. Sobre esa comunidad científica se construyeron tres pilares:
- NumPy (2006): computación numérica rápida (los cálculos pesados se hacen en C por debajo).
- Pandas (2008): análisis de datos tabulares, creado en un fondo de inversión para analizar series financieras.
- Scikit-learn (2010): machine learning clásico accesible para todos.
Cuando llegó la explosión del deep learning (2012, AlexNet) y los frameworks Google y Meta tuvieron que elegir lenguaje para TensorFlow (2015) y PyTorch (2016), la respuesta era obvia: donde ya estaban los científicos de datos. Y cuando llegó la era de los LLM (2020-2026), los SDK oficiales de OpenAI, Anthropic, Google y compañía se publicaron primero en Python. El círculo virtuoso se cerró.
Nota
Python no es el lenguaje más rápido (C++ y Rust lo superan de largo en ejecución pura). Domina la IA porque es el más rápido para el humano: prototipas en minutos, y el trabajo pesado lo delegan sus bibliotecas a código C/C++/CUDA compilado. Tú escribes en Python; la máquina calcula en C.
1.2 El ecosistema: la verdadera razón¶
Un lenguaje vale lo que valen sus bibliotecas. Este es el mapa del ecosistema Python para IA en 2026:
graph TD
PY["Python 3.12/3.13"] --> NUM["NumPy<br/>arrays y álgebra"]
PY --> PD["Pandas / Polars<br/>datos tabulares"]
PY --> VIS["Matplotlib / Seaborn / Plotly<br/>visualización"]
NUM --> SK["Scikit-learn<br/>ML clásico"]
NUM --> TORCH["PyTorch<br/>deep learning"]
NUM --> JAXX["JAX<br/>investigación"]
TORCH --> HF["Hugging Face<br/>Transformers / Datasets"]
HF --> LLM["APIs de LLM<br/>Anthropic · OpenAI · Google"]
LLM --> AGENTS["Frameworks de agentes<br/>LangChain · LlamaIndex · SDKs"]
PD --> SK
PY --> WEB["FastAPI<br/>backends de IA"]
WEB --> AGENTS
style PY fill:#3776ab,color:#fff
style TORCH fill:#ee4c2c,color:#fff
style LLM fill:#6b46c1,color:#fff
Fíjate en la estructura: todo se apoya en NumPy (por eso le dedicamos la sección 4 completa) y los datos casi siempre pasan por Pandas (sección 5). Los LLM y agentes están arriba del todo: son la meta de esta academia, y este capítulo construye sus cimientos.
1.3 Python vs. otros lenguajes para IA¶
Comparemos honestamente los candidatos que escucharás mencionar:
| Criterio | Python | JavaScript/TypeScript | R | Julia |
|---|---|---|---|---|
| Curva de aprendizaje | Muy suave, sintaxis legible | Media (asincronía y tooling complican) | Suave para estadística, rara para todo lo demás | Media |
| Ecosistema de IA/ML | El estándar absoluto: PyTorch, HF, scikit-learn | Limitado (TensorFlow.js, transformers.js), enfocado a inferencia en navegador | Excelente en estadística clásica, débil en deep learning | Bueno en computación científica, comunidad pequeña |
| SDKs de LLM (Anthropic, OpenAI...) | Primera clase, siempre al día | Primera clase también (TS es el 2º ciudadano) | Comunidad, no oficial | Comunidad, no oficial |
| Análisis de datos | Pandas/Polars, estándar de industria | Débil | Su punto más fuerte (tidyverse) | Bueno (DataFrames.jl) |
| Rendimiento bruto | Lento interpretado; rápido vía NumPy/C | Rápido (V8 JIT) | Lento | Muy rápido (compilado JIT) |
| Backend / producción | FastAPI, muy usado en servicios de IA | Dominante en web (Node, Next.js) | Casi inexistente | Raro |
| Frontend / interfaces | Débil (Streamlit para prototipos) | Dominante absoluto (React, etc.) | Shiny (nicho) | No |
| Ofertas de empleo IA (2026) | Mayoría abrumadora | Creciente en "AI engineering" web | Nicho (bioestadística, academia) | Nicho (HPC, finanzas cuantitativas) |
| Veredicto para esta academia | Lenguaje principal: IA, datos, backend | Lo aprenderás después: frontend y algunos agentes | Solo si trabajas en estadística pura | Interesante, no prioritario |
Nota
En AI Master Academy la estrategia es clara y la verás repetida: Python para IA, datos y backend; TypeScript para frontend y algunos agentes que viven en el ecosistema web. TypeScript se enseña en módulos posteriores. No son rivales: son las dos herramientas del ingeniero de IA moderno, cada una en su terreno.
Consejo profesional
No caigas en las "guerras de lenguajes" de internet. Un ingeniero senior elige la herramienta según el problema. Para entrenar un modelo o analizar datos, Python no tiene rival práctico en 2026. Punto.
2. Tu entorno profesional¶
Antes de escribir código de IA necesitas un taller bien montado. Esta sección te ahorrará semanas de frustración: la mayoría de los problemas de los principiantes no son de programación, son de entorno.
2.1 Instalar Python correctamente¶
En julio de 2026, las versiones recomendadas son Python 3.12 o 3.13 (estables, soportadas por todo el ecosistema de IA). Evita versiones más antiguas que 3.10 y desconfía de la más nueva del todo hasta que las bibliotecas la soporten.
Windows:
- Ve a https://www.python.org/downloads/ y descarga el instalador de Python 3.12.x.
- CRÍTICO: en la primera pantalla del instalador, marca la casilla "Add python.exe to PATH". Es una casilla pequeña abajo. Si no la marcas, tendrás el error más famoso de todos los principiantes.
- Pulsa "Install Now".
macOS / Linux: suele venir preinstalado, pero instala tu propia versión con brew install python@3.12 (macOS) o el gestor de tu distribución. Mejor aún: como veremos en 2.6, uv python install 3.12 te lo gestiona todo.
Verificación (abre una terminal: PowerShell en Windows, Terminal en macOS/Linux):
Advertencia — el error de PATH
Si al escribir python la terminal responde "python" no se reconoce como un comando... (Windows) o command not found (macOS/Linux), significa que el sistema no sabe dónde está instalado Python. El PATH es la lista de carpetas donde el sistema busca programas. Soluciones:
- Windows: desinstala Python y reinstala marcando "Add to PATH". Es lo más rápido para un principiante.
- Prueba
py --versionen Windows (el lanzadorpycasi siempre funciona). - En macOS/Linux prueba
python3 --version(a menudo el ejecutable se llamapython3).
Advertencia — Microsoft Store
En Windows, si escribes python y se abre la Microsoft Store, es un "alias fantasma" del sistema. Desactívalo en Configuración → Aplicaciones → Alias de ejecución de aplicaciones (apaga los de python.exe y python3.exe).
2.2 El REPL: tu laboratorio¶
REPL significa Read-Eval-Print Loop: lee lo que escribes, lo evalúa, imprime el resultado y repite. Es una conversación con Python. Escribe python en la terminal:
Cada línea que empieza por >>> es tuya; la de debajo es la respuesta de Python. El REPL es perfecto para probar ideas de una línea: ¿cómo se comporta esta función?, ¿qué devuelve esta expresión? Los profesionales lo usan constantemente.
Nota
Desde Python 3.13 el REPL es a color, con historial mejorado y edición multilínea. Si vienes de tutoriales antiguos que recomendaban instalar ipython para tener un REPL decente, ya no es imprescindible (aunque sigue siendo bueno).
2.3 Scripts: tu primer programa¶
El REPL es efímero: al cerrarlo, todo se pierde. Un script es un fichero de texto con extensión .py que contiene instrucciones que Python ejecuta de arriba abajo. Crea un fichero hola.py con este contenido:
# hola.py — tu primer script de Python
# Las líneas que empiezan con # son comentarios: Python las ignora,
# pero los humanos (incluido tu yo del futuro) las agradecen.
nombre = "futuro ingeniero de IA" # guardamos un texto en una variable
print(f"Hola, {nombre}") # print() muestra texto en pantalla
print("Esto es un programa de verdad.")
Ejecútalo desde la terminal (estando en la carpeta donde guardaste el fichero):
Este es el flujo básico de ejecución de cualquier programa Python:
flowchart LR
A["Escribes<br/>hola.py"] --> B["⌨ Ejecutas<br/>python hola.py"]
B --> C["Intérprete lee<br/>línea por línea"]
C --> D{"¿Error de<br/>sintaxis?"}
D -- "Sí"--> E["Traceback:<br/>mensaje de error"]
D -- "No"--> F["▶ Ejecuta cada<br/>instrucción en orden"]
F --> G{"¿Excepción en<br/>tiempo de ejecución?"}
G -- "Sí"--> E
G -- "No"--> H["Salida en pantalla<br/>y fin del programa"]
E --> A
Consejo profesional
Los mensajes de error de Python (tracebacks) se leen de abajo hacia arriba: la última línea dice QUÉ falló (NameError, TypeError...), y las líneas superiores dicen DÓNDE. Aprender a leer tracebacks es la habilidad número 1 del programador. No los ignores: son tu mejor profesor.
2.4 VS Code: tu taller¶
Puedes escribir Python en el Bloc de notas, pero nadie lo hace. El editor estándar de la industria (y de esta academia) es Visual Studio Code:
- Descárgalo de https://code.visualstudio.com/ e instálalo.
- Instala la extensión Python (de Microsoft) y Pylance (autocompletado inteligente y comprobación de tipos).
- Opcional pero recomendado: la extensión Ruff (linter/formateador ultrarrápido, estándar en 2026).
Con VS Code obtienes: autocompletado, detección de errores mientras escribes, ejecución con un clic (botón ▶), depurador visual y terminal integrada (menú Terminal → New Terminal).
Consejo profesional
Abre siempre la carpeta del proyecto (File → Open Folder), no ficheros sueltos. VS Code entiende tu proyecto como un todo: encuentra tu entorno virtual, tus imports y tu configuración.
2.5 Entornos virtuales: el concepto que separa amateurs de profesionales¶
Este es, sin exagerar, el concepto de entorno más importante del capítulo. Léelo dos veces.
El problema. Imagina que trabajas en dos proyectos:
- Proyecto A (antiguo): necesita la biblioteca
pandasversión 1.5. - Proyecto B (nuevo): necesita
pandasversión 2.3.
Si instalas las bibliotecas "en el Python del sistema" (instalación global), solo puede existir una versión de pandas a la vez. Al actualizar para el proyecto B, rompes el proyecto A. Multiplica esto por las decenas de dependencias de un proyecto de IA real y tienes el famoso "infierno de dependencias".
La solución. Un entorno virtual es una carpeta (normalmente llamada .venv) que contiene una copia aislada de Python con sus propias bibliotecas. Cada proyecto tiene la suya:
proyectos/
├── proyecto_a/
│ ├── .venv/ ← aquí vive pandas 1.5 (solo para A)
│ └── analisis.py
└── proyecto_b/
├── .venv/ ← aquí vive pandas 2.3 (solo para B)
└── modelo.py
Creación y uso con el módulo estándar venv:
# 1. Sitúate en la carpeta de tu proyecto
cd mi_proyecto
# 2. Crea el entorno virtual (crea la carpeta .venv)
python -m venv .venv
# 3. ACTÍVALO (esto le dice a la terminal: "usa ESTE Python")
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# Windows (cmd):
.venv\Scripts\activate.bat
# macOS / Linux:
source .venv/bin/activate
# 4. Verás el nombre del entorno en el prompt: (.venv) C:\mi_proyecto>
# A partir de ahora, pip instala DENTRO del entorno:
pip install pandas
# 5. Para salir del entorno:
deactivate
Advertencia — instalar todo global
El error clásico del principiante es hacer pip install lo-que-sea sin entorno virtual activo durante meses. Consecuencias: conflictos de versiones imposibles de depurar, proyectos que "funcionaban ayer" y hoy no, y la incapacidad de decirle a un compañero qué necesita tu proyecto para funcionar. Regla de oro: un proyecto = un entorno virtual. Sin excepciones. Si el prompt de tu terminal no muestra (.venv), no instales nada.
Advertencia — PowerShell y scripts
Si Windows te da el error "la ejecución de scripts está deshabilitada" al activar, ejecuta una vez: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser y responde S.
Nota
La carpeta .venv nunca se sube a Git ni se copia entre máquinas: es regenerable. Lo que se comparte es la lista de dependencias (requirements.txt o pyproject.toml), y cada máquina reconstruye su entorno. Añade .venv/ a tu .gitignore siempre.
2.6 uv: el gestor moderno (2026)¶
venv + pip funcionan, pero en 2026 el estándar profesional emergente es uv (de Astral, los creadores de Ruff): un gestor de paquetes y proyectos escrito en Rust, 10-100× más rápido que pip, que unifica todo el flujo: instala versiones de Python, crea entornos, resuelve dependencias y las fija con un lockfile reproducible.
Instalación de uv:
# Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
Flujo de trabajo completo con uv:
# 1. Crear un proyecto nuevo (genera pyproject.toml, .python-version, etc.)
uv init analisis-ventas
cd analisis-ventas
# 2. Añadir dependencias (crea .venv automáticamente y actualiza pyproject.toml)
uv add pandas numpy
# 3. Ejecutar un script DENTRO del entorno, sin activar nada a mano
uv run python analisis.py
# 4. Instalar una versión concreta de Python (¡uv gestiona también los Python!)
uv python install 3.13
# 5. Reproducir el entorno en otra máquina (lee pyproject.toml + uv.lock)
uv sync
Comparativa de herramientas de gestión:
| Aspecto | venv + pip (clásico) |
uv (moderno 2026) |
conda (mundo científico) |
|---|---|---|---|
| Velocidad de instalación | Lenta | Ultrarrápida (Rust, caché global) | Lenta |
| Crea el entorno virtual | Manual (python -m venv) |
Automático (uv add, uv run) |
conda create |
| Gestiona versiones de Python | No (usas la instalada) | Sí (uv python install) |
Sí |
| Lockfile reproducible | Manual (pip freeze) |
Automático (uv.lock) |
environment.yml (parcial) |
| Fichero de proyecto | requirements.txt |
pyproject.toml (estándar PEP 621) |
environment.yml |
| Binarios no-Python (CUDA, MKL) | No | No | Sí (su gran ventaja histórica) |
| Recomendación de la academia | Apréndelo (es la base) | Úsalo a diario | Solo si un proyecto lo exige |
Consejo profesional
Aprende primero venv + pip porque es lo que encontrarás en el 90 % de tutoriales, documentación y proyectos antiguos. Pero para tus proyectos nuevos, usa uv: es el futuro (y el presente) y te evita errores de entorno por diseño, porque uv run siempre ejecuta dentro del entorno correcto.
2.7 pip, requirements.txt y pyproject.toml¶
pip es el instalador clásico de paquetes desde PyPI (Python Package Index, el "repositorio de apps" de Python, con más de 550 000 paquetes):
pip install pandas # instala la última versión
pip install pandas==2.2.3 # instala una versión exacta
pip install --upgrade pandas # actualiza
pip uninstall pandas # desinstala
pip list # lista lo instalado en el entorno activo
requirements.txt es el formato clásico para declarar dependencias: un fichero de texto con un paquete por línea.
pip freeze > requirements.txt # exportar lo instalado (versiones exactas)
pip install -r requirements.txt # reconstruir el entorno en otra máquina
pyproject.toml es el estándar moderno (PEP 621): un único fichero que describe tu proyecto entero: nombre, versión, dependencias, herramientas. Es el que genera y mantiene uv:
# pyproject.toml — la ficha técnica de tu proyecto
[project]
name = "analisis-ventas"
version = "0.1.0"
description = "Análisis de ventas para AI Master Academy"
requires-python = ">=3.12"
dependencies = [
"pandas>=2.2",
"numpy>=1.26",
]
Nota
¿Cuál usar? En proyectos nuevos, pyproject.toml (con uv). Pero entiende requirements.txt porque lo verás en todas partes durante años. No son enemigos: muchos proyectos generan un requirements.txt a partir del pyproject.toml para despliegues.
Ejercicio rápido 1: Crea una carpeta practica-python, dentro un entorno virtual, actívalo, instala numpy y verifica con pip list que está. Después desactívalo.
Ver solución
Con `uv` sería aún más corto: `uv init practica-python && cd practica-python && uv add numpy`.3. Fundamentos del lenguaje¶
Aquí empieza la programación de verdad. Cada subsección sigue el mismo patrón: explicación desde cero → código comentado línea por línea → ejemplo aplicado a IA/datos → error común.
3.1 Variables y tipos¶
Desde cero. Un programa manipula datos: números, textos, verdadero/falso. Para trabajar con un dato necesitas darle un nombre: eso es una variable. Piensa en una variable como una etiqueta pegada a un valor: temperatura = 0.7 significa "la etiqueta temperatura apunta al valor 0.7".
Python tiene cinco tipos básicos que usarás constantemente:
| Tipo | Nombre | Ejemplos | Uso típico en IA |
|---|---|---|---|
int |
Entero | 42, -3, 0 |
Número de tokens, épocas de entrenamiento, tamaño de lote |
float |
Decimal (coma flotante) | 0.7, 3.14, -0.001 |
Temperatura del modelo, learning rate, probabilidades |
str |
Cadena de texto (string) | "hola", 'GPT' |
Prompts, respuestas del modelo, nombres de columnas |
bool |
Booleano | True, False |
¿Streaming activado? ¿Modelo cargado? Condiciones |
None |
Ausencia de valor | None |
"Todavía no hay respuesta", valor opcional no dado |
Código comentado línea por línea:
# --- Variables y tipos básicos ---
modelo = "claude-sonnet-4-5" # str: texto entre comillas (dobles o simples)
temperatura = 0.7 # float: número con parte decimal
max_tokens = 1024 # int: número entero, sin decimales
streaming = True # bool: solo puede ser True o False (¡mayúscula inicial!)
respuesta = None # None: "aún no tenemos respuesta" (ausencia de valor)
# type() te dice el tipo de cualquier valor — úsalo en el REPL para explorar
print(type(modelo)) # <class 'str'>
print(type(temperatura)) # <class 'float'>
print(type(max_tokens)) # <class 'int'>
# Python es de TIPADO DINÁMICO: la variable puede cambiar de tipo...
x = 5 # ahora x es int
x = "cinco" # ...y ahora es str (legal, pero mala práctica)
# Conversiones explícitas entre tipos (casting)
numero_texto = "42" # esto es un str, NO un número
numero_real = int(numero_texto) # int() lo convierte a entero: 42
pi_aproximado = float("3.14") # float() convierte texto a decimal
edad_texto = str(30) # str() convierte número a texto: "30"
f-strings: la forma moderna de construir texto. Una f-string (formatted string) es una cadena con f delante en la que puedes incrustar variables y expresiones entre llaves {}. Es LA forma de formatear texto en Python moderno (y la usarás muchísimo para construir prompts):
modelo = "claude-sonnet-4-5"
tokens_entrada = 850
tokens_salida = 420
precio_por_millon = 3.0 # dólares por millón de tokens de entrada
# f-string básica: las llaves se sustituyen por el valor de la variable
print(f"Modelo en uso: {modelo}")
# → Modelo en uso: claude-sonnet-4-5
# Puedes poner EXPRESIONES dentro de las llaves (cálculos al vuelo)
print(f"Tokens totales: {tokens_entrada + tokens_salida}")
# → Tokens totales: 1270
# Formato de números: :.4f = 4 decimales, :, = separador de miles, :.1% = porcentaje
coste = tokens_entrada / 1_000_000 * precio_por_millon # el _ separa miles: 1_000_000 == 1000000
print(f"Coste de la llamada: ${coste:.6f}")
# → Coste de la llamada: $0.002550
print(f"Tokens procesados: {1_250_000:,}")
# → Tokens procesados: 1,250,000
print(f"Precisión del modelo: {0.923:.1%}")
# → Precisión del modelo: 92.3%
Ejemplo aplicado a IA: configuración de una llamada a un LLM, todo con variables tipadas:
# Parámetros típicos de una llamada a la API de un LLM
system_prompt: str = "Eres un asistente experto en análisis de datos." # instrucciones del sistema
user_prompt: str = "Resume las ventas del último trimestre." # petición del usuario
temperature: float = 0.2 # baja = respuestas más deterministas (bueno para datos)
max_tokens: int = 500 # límite de longitud de la respuesta
stream: bool = False # ¿recibir la respuesta token a token?
# Construimos un resumen de la configuración con una f-string multilínea
resumen = (
f"Configuración de llamada:\n"
f" temperatura={temperature} | max_tokens={max_tokens} | stream={stream}\n"
f" prompt: '{user_prompt[:30]}...'" # [:30] corta a los 30 primeros caracteres
)
print(resumen)
Error común
confundir el texto "42" con el número 42. Si lees datos de un fichero o de un formulario, siempre llegan como texto, y "2" + "2" es "22" (concatenación), no 4. Convierte con int() o float() antes de operar. Y ojo: int("3.5") falla (ValueError); usa float("3.5") y luego int() si necesitas truncar.
3.2 Operadores y condicionales¶
Desde cero. Los operadores son los símbolos con los que combinas valores. Las condicionales (if) son la primera herramienta que hace que un programa tome decisiones: "si pasa esto, haz aquello".
# --- Operadores aritméticos ---
print(7 + 3) # 10 suma
print(7 - 3) # 4 resta
print(7 * 3) # 21 multiplicación
print(7 / 3) # 2.3333... división REAL (siempre devuelve float)
print(7 // 3) # 2 división ENTERA (trunca el decimal)
print(7 % 3) # 1 módulo: el RESTO de la división (7 = 3*2 + 1)
print(7 ** 2) # 49 potencia (7 al cuadrado)
# --- Operadores de comparación (devuelven True o False) ---
print(5 > 3) # True
print(5 == 5) # True igualdad: DOS signos igual (uno solo es asignación)
print(5 != 4) # True desigualdad
print(5 >= 5) # True
# --- Operadores lógicos: combinan booleanos ---
print(True and False) # False (ambos deben ser True)
print(True or False) # True (basta con uno)
print(not True) # False (invierte)
Condicionales — código comentado línea por línea:
# Clasificador simple de la "confianza" de un modelo de IA.
# En ML, los modelos suelen devolver una probabilidad entre 0 y 1.
confianza = 0.87 # probabilidad que devolvió el modelo
if confianza >= 0.9: # if: si la condición es True, entra aquí
veredicto = "alta" # OJO: el bloque va INDENTADO con 4 espacios
elif confianza >= 0.7: # elif: "si no, prueba esta otra condición"
veredicto = "media" # se evalúan EN ORDEN; entra en la primera que cumpla
elif confianza >= 0.5: # puedes encadenar tantos elif como quieras
veredicto = "baja"
else: # else: si NINGUNA condición anterior se cumplió
veredicto = "descartar"
print(f"Confianza {confianza:.0%} → veredicto: {veredicto}")
# → Confianza 87% → veredicto: media
Nota
En Python la indentación no es estética: es sintaxis. Los bloques se delimitan con sangrado (4 espacios, el estándar), no con llaves {} como en otros lenguajes. Esto obliga a que el código sea visualmente ordenado. VS Code indenta por ti al pulsar Enter tras los dos puntos :.
Ejemplo aplicado a IA: enrutador de modelos según la complejidad de la petición (patrón real en sistemas de producción para ahorrar costes):
longitud_prompt = 3200 # caracteres del prompt del usuario
requiere_razonamiento = True # ¿la tarea exige razonamiento complejo?
# Patrón "router": elegir el modelo más barato que resuelva la tarea
if requiere_razonamiento and longitud_prompt > 2000:
modelo = "claude-opus-4-1" # el más potente para lo más difícil
elif requiere_razonamiento or longitud_prompt > 2000:
modelo = "claude-sonnet-4-5" # equilibrio potencia/coste
else:
modelo = "claude-haiku-4-5" # rápido y barato para tareas simples
print(f"Petición enrutada a: {modelo}")
Error común
usar = (asignación) donde toca == (comparación): if x = 5: es un SyntaxError. Y el clásico IndentationError: mezclar tabuladores y espacios, u olvidar indentar el bloque tras los dos puntos. Configura VS Code para que el tabulador inserte 4 espacios (es el valor por defecto de la extensión de Python).
3.3 Bucles: for, while, range, enumerate, zip¶
Desde cero. Un bucle repite un bloque de código. Es el superpoder básico de la programación: lo que a un humano le llevaría horas (procesar 10 000 filas), un bucle lo hace en milisegundos. Python tiene dos: for (recorrer una colección de elementos) y while (repetir mientras una condición sea cierta).
# --- for: recorrer una colección elemento a elemento ---
modelos = ["haiku", "sonnet", "opus"] # una lista (colección ordenada)
for m in modelos: # en cada vuelta, m toma el siguiente valor
print(f"Modelo disponible: {m}") # este bloque se ejecuta 3 veces
# → Modelo disponible: haiku
# → Modelo disponible: sonnet
# → Modelo disponible: opus
# --- range(): generar secuencias de números ---
for epoca in range(3): # range(3) genera 0, 1, 2 (¡el 3 NO entra!)
print(f"Entrenando época {epoca}...")
for i in range(2, 10, 2): # range(inicio, fin, paso): 2, 4, 6, 8
print(i)
# --- enumerate(): cuando necesitas el elemento Y su posición ---
for indice, m in enumerate(modelos): # devuelve pares (0, "haiku"), (1, "sonnet")...
print(f"{indice}: {m}")
# → 0: haiku
# → 1: sonnet
# → 2: opus
# --- zip(): recorrer DOS (o más) colecciones en paralelo ---
precios = [1.0, 3.0, 15.0] # $/millón de tokens de entrada
for m, p in zip(modelos, precios): # empareja: ("haiku",1.0), ("sonnet",3.0)...
print(f"{m}: ${p}/millón de tokens")
# --- while: repetir MIENTRAS se cumpla una condición ---
intentos = 0
exito = False
while not exito and intentos < 3: # patrón real: reintentos a una API
intentos += 1 # += suma y asigna: intentos = intentos + 1
print(f"Intento {intentos} de llamada a la API...")
exito = (intentos == 2) # simulamos que el 2º intento funciona
print(f"Terminado tras {intentos} intentos.")
# --- break y continue: control fino del bucle ---
for m in modelos:
if m == "sonnet":
continue # continue: salta a la SIGUIENTE vuelta (omite sonnet)
if m == "opus":
break # break: SALE del bucle por completo
print(m) # solo imprime "haiku"
Ejemplo aplicado a IA: calcular el coste total de un lote de llamadas a un LLM:
# Tokens consumidos por cada llamada de un lote de procesamiento
tokens_por_llamada = [850, 1200, 430, 2100, 990]
PRECIO_POR_MILLON = 3.0 # constante: MAYÚSCULAS por convención
coste_total = 0.0 # acumulador: empieza en cero
for i, tokens in enumerate(tokens_por_llamada, start=1): # start=1: numerar desde 1
coste = tokens / 1_000_000 * PRECIO_POR_MILLON # coste de ESTA llamada
coste_total += coste # lo añadimos al acumulador
print(f"Llamada {i}: {tokens:>5} tokens → ${coste:.6f}") # :>5 alinea a la derecha
print(f"Coste total del lote: ${coste_total:.4f}")
Error común
el bucle infinito con while: si olvidas actualizar la variable de la condición (intentos += 1), el programa nunca termina (pulsa Ctrl+C en la terminal para matarlo). Y con range: recordar que range(n) va de 0 a n-1 — el límite superior nunca se incluye. Es la convención de todo Python (la reencontrarás en slicing).
Ejercicio rápido 2: Con datasets = ["train", "val", "test"] y tamaños = [8000, 1000, 1000], imprime una línea por dataset con formato 1. train: 8000 ejemplos (80.0%) usando zip, enumerate y f-strings.
Ver solución
datasets = ["train", "val", "test"]
tamaños = [8000, 1000, 1000]
total = sum(tamaños) # sum() suma una colección de números
for i, (nombre, n) in enumerate(zip(datasets, tamaños), start=1):
# zip empareja; enumerate añade el contador; start=1 numera desde 1
print(f"{i}. {nombre}: {n} ejemplos ({n / total:.1%})")
# 1. train: 8000 ejemplos (80.0%)
# 2. val: 1000 ejemplos (10.0%)
# 3. test: 1000 ejemplos (10.0%)
3.4 Estructuras de datos¶
Desde cero. Hasta ahora, cada variable guardaba UN valor. Las estructuras de datos guardan colecciones de valores. Python trae cuatro de serie, y elegir la correcta es media programación:
| Estructura | Sintaxis | ¿Ordenada? | ¿Modificable? | ¿Duplicados? | Uso típico en IA |
|---|---|---|---|---|---|
Lista (list) |
[1, 2, 3] |
Sí | Sí (mutable) | Sí | Secuencias de ejemplos, historial de mensajes, lote de prompts |
Tupla (tuple) |
(1, 2, 3) |
Sí | No (inmutable) | Sí | Coordenadas, shape de un array, retornos múltiples |
Diccionario (dict) |
{"clave": valor} |
Sí (orden de inserción) | Sí | Claves únicas | JSON, peticiones/respuestas de APIs de IA, configuración |
Set (set) |
{1, 2, 3} |
No | Sí | No | Vocabularios, eliminar duplicados, pertenencia rápida |
Listas — la colección de trabajo diario:
# Crear y manipular listas
mensajes = ["hola", "¿cómo estás?"] # lista de strings
mensajes.append("muy bien") # append: añade AL FINAL → 3 elementos
mensajes.insert(0, "sistema: sé amable") # insert: añade en la posición 0
print(len(mensajes)) # len() = longitud: 4
# Indexación: acceder por posición (¡empieza en 0!)
print(mensajes[0]) # primer elemento: "sistema: sé amable"
print(mensajes[-1]) # ÍNDICE NEGATIVO: el último ("muy bien") — súper útil
# Slicing: extraer sub-listas con [inicio:fin] (fin NO incluido, como range)
print(mensajes[1:3]) # elementos en posiciones 1 y 2
print(mensajes[:2]) # del principio hasta la posición 2 (sin incluirla)
print(mensajes[2:]) # de la posición 2 hasta el final
print(mensajes[::-1]) # truco: la lista invertida (paso -1)
# Otras operaciones frecuentes
numeros = [3, 1, 4, 1, 5, 9, 2, 6]
numeros.sort() # ordena EN EL SITIO (modifica la lista)
print(numeros) # [1, 1, 2, 3, 4, 5, 6, 9]
print(max(numeros), min(numeros), sum(numeros)) # 9 1 31
print(9 in numeros) # in: ¿está el 9 en la lista? → True
Tuplas — listas inmutables:
# Una tupla se crea con paréntesis y NO se puede modificar después
shape = (32, 224, 224, 3) # típico shape de un lote de imágenes en deep learning:
# 32 imágenes, 224x224 píxeles, 3 canales (RGB)
print(shape[0]) # se indexa igual que una lista: 32
# shape[0] = 64 # ¡ERROR! TypeError: las tuplas son inmutables
# Desempaquetado: asignar cada elemento a una variable de una vez
lote, alto, ancho, canales = shape
print(f"Lote de {lote} imágenes de {alto}x{ancho} con {canales} canales")
# Las funciones que "devuelven varias cosas" en realidad devuelven una tupla
def min_max(valores):
return min(valores), max(valores) # esto es una tupla (min, max)
menor, mayor = min_max([0.2, 0.9, 0.5]) # desempaquetamos al recibirla
Diccionarios — LA estructura de la era de las APIs de IA. Un diccionario asocia claves con valores, como un diccionario real asocia palabras con definiciones. Es crucial porque el formato JSON, en el que hablan todas las APIs de IA, es esencialmente un diccionario de Python:
# Un mensaje para una API de LLM tiene EXACTAMENTE esta forma
mensaje = {
"role": "user", # clave "role" → valor "user"
"content": "Explícame qué es un embedding", # clave "content" → valor (el texto)
}
# Acceso por clave
print(mensaje["role"]) # "user"
mensaje["content"] = "Otra pregunta" # modificar un valor existente
mensaje["timestamp"] = "2026-07-03" # añadir una clave nueva (sobre la marcha)
# ⚠ Acceder a una clave inexistente lanza KeyError. Solución: .get()
print(mensaje.get("model")) # None (no explota)
print(mensaje.get("model", "claude-haiku")) # valor por defecto si no existe
# Recorrer un diccionario
for clave, valor in mensaje.items(): # .items() da pares (clave, valor)
print(f" {clave}: {valor}")
print(list(mensaje.keys())) # solo las claves
print(list(mensaje.values())) # solo los valores
# Diccionarios anidados: así se ve una petición REAL a una API de LLM
peticion = {
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [ # una LISTA de DICCIONARIOS
{"role": "user", "content": "Hola, ¿qué es Python?"},
{"role": "assistant", "content": "Python es un lenguaje de programación..."},
{"role": "user", "content": "¿Y por qué se usa en IA?"},
],
}
# Navegar la estructura anidada paso a paso:
ultimo_mensaje = peticion["messages"][-1] # último dict de la lista
print(ultimo_mensaje["content"]) # "¿Y por qué se usa en IA?"
Sets — colecciones sin duplicados:
# Un set elimina duplicados automáticamente y busca ultrarrápido
palabras = ["el", "gato", "y", "el", "perro", "y", "el", "loro"]
vocabulario = set(palabras) # {'el', 'gato', 'y', 'perro', 'loro'}
print(len(palabras), "→", len(vocabulario)) # 8 → 5 (duplicados fuera)
# Operaciones de conjuntos (como en matemáticas)
stopwords = {"el", "la", "y", "de"} # palabras vacías a filtrar
utiles = vocabulario - stopwords # diferencia: quita las stopwords
print(utiles) # {'gato', 'perro', 'loro'}
print("gato" in vocabulario) # pertenencia: True (búsqueda O(1), instantánea)
Error común
IndexError: list index out of range — pedir lista[5] cuando la lista tiene 3 elementos. Recuerda: una lista de n elementos se indexa de 0 a n-1. Y KeyError en diccionarios: usa .get(clave, defecto) cuando no estés seguro de que la clave exista (típico al procesar respuestas JSON de APIs, donde algunos campos son opcionales).
3.5 Comprehensions¶
Desde cero. Una comprehension es la forma pythónica de crear una lista (o dict/set) transformando otra colección, en una sola línea legible. Sustituye al patrón "crear lista vacía + bucle + append":
# ANTES (estilo verboso): elevar al cuadrado una lista de números
cuadrados = [] # 1) lista vacía
for n in range(5): # 2) bucle
cuadrados.append(n ** 2) # 3) añadir cada resultado
# DESPUÉS (list comprehension): las mismas 3 piezas en una línea
cuadrados = [n ** 2 for n in range(5)] # [0, 1, 4, 9, 16]
# ^^^^^^ transformación
# ^^^^^^^^^^^^^^^ el bucle
# Con FILTRO: añade un if al final (solo pares)
pares_cuadrados = [n ** 2 for n in range(10) if n % 2 == 0] # [0, 4, 16, 36, 64]
# Dict comprehension: crear un diccionario al vuelo
modelos = ["haiku", "sonnet", "opus"]
precios = [1.0, 3.0, 15.0]
tarifa = {m: p for m, p in zip(modelos, precios)}
# {'haiku': 1.0, 'sonnet': 3.0, 'opus': 15.0}
# Set comprehension: vocabulario en minúsculas de una frase
frase = "El Gato y el gato saltan"
vocab = {palabra.lower() for palabra in frase.split()} # .split() corta por espacios
# {'el', 'gato', 'y', 'saltan'}
Ejemplo aplicado a IA: limpiar y preparar textos para un modelo:
comentarios = [
" Excelente producto!! ",
"",
"muy malo",
" Repetiría sin duda ",
" ",
]
# Pipeline de limpieza en UNA comprehension legible:
# 1) .strip() quita espacios de los extremos 2) filtramos vacíos 3) minúsculas
limpios = [c.strip().lower() for c in comentarios if c.strip()]
print(limpios)
# ['excelente producto!!', 'muy malo', 'repetiría sin duda']
# Longitud de cada comentario (feature típica para un clasificador)
longitudes = [len(c) for c in limpios] # [20, 8, 18]
Error común
anidar tres niveles de comprehensions "porque cabe en una línea". Si no se lee de un vistazo, usa un bucle normal. La regla: una comprehension debe ser MÁS legible que el bucle equivalente, o no la uses.
3.6 Funciones¶
Desde cero. Una función es un bloque de código con nombre y reutilizable: le das entradas (parámetros), hace su trabajo y te devuelve una salida (return). Son la unidad básica de organización de todo programa serio: en lugar de repetir 20 líneas cada vez, las encapsulas una vez y las llamas por su nombre.
# --- Anatomía de una función, línea por línea ---
def calcular_coste(tokens: int, precio_millon: float = 3.0) -> float:
# def → palabra clave que INICIA la definición
# calcular_coste → nombre (minúsculas_con_guiones_bajos, estilo snake_case)
# tokens: int → parámetro con TYPE HINT: "espero un entero"
# precio_millon: float = 3.0 → parámetro con VALOR POR DEFECTO (opcional al llamar)
# -> float → type hint del RETORNO: "devuelvo un decimal"
"""Calcula el coste en dólares de una llamada a un LLM.
Esto es un DOCSTRING: documentación de la función. VS Code lo muestra
cuando pasas el ratón por encima del nombre. Escríbelo siempre.
"""
coste = tokens / 1_000_000 * precio_millon # cuerpo: el trabajo real
return coste # return: devuelve el resultado y TERMINA
# --- Llamadas ---
print(calcular_coste(850)) # usa el precio por defecto (3.0)
print(calcular_coste(850, 15.0)) # argumentos posicionales (en orden)
print(calcular_coste(tokens=850, precio_millon=15.0)) # argumentos con nombre (más claro)
Nota sobre type hints
las anotaciones tokens: int y -> float no cambian la ejecución (Python no las comprueba en tiempo de ejecución), pero en 2026 son obligatorias en código profesional: VS Code/Pylance las usa para autocompletar y avisarte de errores antes de ejecutar, los equipos las exigen en revisión de código, y herramientas como mypy o pyright las verifican automáticamente. Acostúmbrate desde el día uno: todo el código de esta academia las lleva.
*args y **kwargs — funciones con argumentos variables:
def media(*valores: float) -> float:
# *valores empaqueta TODOS los argumentos posicionales en una TUPLA.
# Así la función acepta 1, 5 o 100 números sin cambiar su firma.
return sum(valores) / len(valores)
print(media(4.0, 8.0)) # 6.0 (valores = (4.0, 8.0))
print(media(1.0, 2.0, 3.0, 4.0)) # 2.5 (valores = (1.0, 2.0, 3.0, 4.0))
def llamar_llm(prompt: str, **opciones) -> None:
# **opciones empaqueta los argumentos CON NOMBRE en un DICCIONARIO.
# Patrón omnipresente en los SDKs de IA: pasar configuración flexible.
print(f"Prompt: {prompt}")
for clave, valor in opciones.items():
print(f" opción {clave} = {valor}")
llamar_llm("Resume este texto", temperature=0.2, max_tokens=500, stream=True)
# opciones = {"temperature": 0.2, "max_tokens": 500, "stream": True}
# El operador ** también DESEMPAQUETA un dict al llamar (lo verás en SDKs):
config = {"temperature": 0.2, "max_tokens": 500}
llamar_llm("Otro prompt", **config) # equivale a temperature=0.2, max_tokens=500
Lambdas — funciones anónimas de una línea:
# Una lambda es una mini-función sin nombre: lambda parámetros: expresión
duplicar = lambda x: x * 2 # (didáctico; en la práctica usarías def)
print(duplicar(5)) # 10
# Su uso REAL: como argumento de otras funciones, sobre todo para ordenar
resultados = [
{"modelo": "haiku", "precision": 0.81},
{"modelo": "opus", "precision": 0.94},
{"modelo": "sonnet", "precision": 0.90},
]
# sorted() con key: "ordena por lo que devuelva esta función para cada elemento"
mejores = sorted(resultados, key=lambda r: r["precision"], reverse=True)
print(mejores[0]["modelo"]) # "opus" (el de mayor precisión)
Ejemplo aplicado a IA: función completa y tipada para estimar costes de un proyecto:
def estimar_coste_proyecto(
num_documentos: int,
tokens_por_doc: int = 1500,
precio_entrada: float = 3.0, # $/millón tokens entrada
precio_salida: float = 15.0, # $/millón tokens salida
ratio_salida: float = 0.3, # la salida suele ser ~30% de la entrada
) -> dict[str, float]:
"""Estima el coste de procesar un lote de documentos con un LLM."""
tokens_entrada = num_documentos * tokens_por_doc # total tokens que enviamos
tokens_salida = int(tokens_entrada * ratio_salida) # total tokens que recibimos
coste_in = tokens_entrada / 1_000_000 * precio_entrada # coste de la entrada
coste_out = tokens_salida / 1_000_000 * precio_salida # coste de la salida
return { # devolvemos un dict ordenado
"tokens_entrada": tokens_entrada,
"tokens_salida": tokens_salida,
"coste_total_usd": round(coste_in + coste_out, 2), # round(x, 2): 2 decimales
}
presupuesto = estimar_coste_proyecto(num_documentos=10_000)
print(f"Procesar 10.000 docs costará ~${presupuesto['coste_total_usd']}")
Error común — el argumento por defecto mutable (trampa clásica de Python)
# ❌ MAL: la lista por defecto se crea UNA SOLA VEZ y se COMPARTE entre llamadas
def añadir_mensaje(texto: str, historial: list = []) -> list:
historial.append(texto)
return historial
print(añadir_mensaje("hola")) # ['hola']
print(añadir_mensaje("adiós")) # ['hola', 'adiós'] ¡¿¿POR QUÉ está "hola"??!
# ✅ BIEN: usa None como centinela y crea la lista DENTRO
def añadir_mensaje_ok(texto: str, historial: list | None = None) -> list:
if historial is None: # si no nos pasaron historial...
historial = [] # ...creamos uno NUEVO en cada llamada
historial.append(texto)
return historial
Esto ocurre porque los valores por defecto se evalúan una vez, al definir la función, no en cada llamada. Con valores inmutables (números, strings, None) no hay problema; con mutables (listas, dicts) sí. Memorízalo: aparece en entrevistas de trabajo.
3.7 Manejo de errores¶
Desde cero. Los programas fallan: el fichero no existe, la red se cae, la API devuelve basura, el usuario escribe "hola" donde iba un número. Cuando algo falla, Python lanza una excepción, y si nadie la captura, el programa muere con un traceback. El manejo de errores (try/except) te permite anticipar los fallos y reaccionar con elegancia en lugar de morir.
Esto es crítico en IA: llamar a la API de un LLM implica red, límites de velocidad (rate limits), timeouts y respuestas malformadas. El código de producción que llama a LLMs es, en buena parte, manejo de errores.
# --- Anatomía completa de try/except/else/finally ---
def dividir(a: float, b: float) -> float | None:
try:
# 1) TRY: el código "peligroso" que puede fallar
resultado = a / b
except ZeroDivisionError:
# 2) EXCEPT: se ejecuta SOLO si saltó ESA excepción concreta
print("Error: no se puede dividir entre cero")
return None
except TypeError as e:
# Puedes capturar varios tipos; 'as e' guarda el objeto excepción
print(f"Error de tipos: {e}")
return None
else:
# 3) ELSE (opcional): se ejecuta solo si NO hubo excepción
print("División correcta")
return resultado
finally:
# 4) FINALLY (opcional): se ejecuta SIEMPRE, haya error o no.
# Ideal para limpieza: cerrar ficheros, conexiones, liberar recursos.
print("(fin del intento de división)")
dividir(10, 2) # División correcta → (fin...) → 5.0
dividir(10, 0) # Error: no se puede dividir entre cero → (fin...)
raise — lanzar tus propias excepciones:
def configurar_temperatura(t: float) -> float:
"""Valida la temperatura de un LLM (debe estar entre 0 y 1)."""
if not 0.0 <= t <= 1.0: # comparación encadenada, muy pythónica
# raise LANZA una excepción a propósito: "esto es inaceptable, no sigo"
raise ValueError(f"Temperatura {t} inválida: debe estar entre 0.0 y 1.0")
return t
configurar_temperatura(0.7) # OK
# configurar_temperatura(2.5) # ValueError: Temperatura 2.5 inválida...
Excepciones propias + ejemplo aplicado a IA: un cliente de LLM con reintentos, el patrón que escribirás mil veces:
import random # lo usamos para SIMULAR fallos de red aleatorios
import time # para esperar entre reintentos
# --- Excepciones propias: heredan de Exception y dan SEMÁNTICA a tus errores ---
class ErrorDeAPI(Exception):
"""Error genérico al llamar a la API del LLM."""
class LimiteDeVelocidad(ErrorDeAPI):
"""La API devolvió 429: demasiadas peticiones (rate limit)."""
def llamada_simulada(prompt: str) -> str:
"""Simula una API real que falla el 50% de las veces."""
if random.random() < 0.5: # random(): float entre 0 y 1
raise LimiteDeVelocidad("HTTP 429: too many requests")
return f"Respuesta del modelo a: '{prompt}'"
def llamar_con_reintentos(prompt: str, max_reintentos: int = 3) -> str:
"""Llama a la API con reintentos y espera exponencial (patrón estándar)."""
for intento in range(1, max_reintentos + 1): # intentos 1, 2, 3
try:
return llamada_simulada(prompt) # si funciona, devolvemos y SALIMOS
except LimiteDeVelocidad as e:
espera = 2 ** intento # backoff exponencial: 2s, 4s, 8s
print(f"Intento {intento} falló ({e}). Esperando {espera}s...")
time.sleep(espera) # pausa antes de reintentar
# Si agotamos los reintentos, escalamos el error al llamador:
raise ErrorDeAPI(f"Fallo tras {max_reintentos} reintentos")
print(llamar_con_reintentos("Resume este contrato"))
Consejo profesional
captura excepciones específicas (ValueError, KeyError, LimiteDeVelocidad), no except Exception: a secas, y JAMÁS un except: desnudo con pass. Silenciar todos los errores es la receta para bugs invisibles que te costarán noches de sueño: el programa "funciona" pero hace cosas mal en silencio.
Error común
usar try/except como control de flujo normal para todo, o al revés: no usarlo nunca y dejar que cualquier fichero ausente tire el programa. Criterio: try/except para lo externo e impredecible (red, disco, entrada del usuario, APIs); validación con if + raise para los contratos de tus funciones.
3.8 Archivos: texto, JSON y CSV¶
Desde cero. Los datos viven en ficheros. Tres formatos cubren el 95 % del trabajo en IA: texto plano (logs, prompts), JSON (el idioma de las APIs) y CSV (tablas exportadas de Excel/bases de datos).
Texto plano con with open(...):
# --- ESCRIBIR un fichero de texto ---
# 'with' es un CONTEXT MANAGER: garantiza que el fichero se CIERRA solo,
# incluso si algo falla a mitad. Es LA forma correcta de abrir ficheros.
with open("prompt_sistema.txt", "w", encoding="utf-8") as f:
# "w" = write (crea o SOBRESCRIBE) | encoding="utf-8" = soporta tildes y ñ
f.write("Eres un asistente experto en análisis de datos.\n") # \n = salto de línea
f.write("Responde siempre en español.\n")
# --- LEER un fichero de texto ---
with open("prompt_sistema.txt", "r", encoding="utf-8") as f: # "r" = read
contenido = f.read() # todo el fichero en un solo string
print(contenido)
# --- LEER línea a línea (mejor para ficheros grandes: no carga todo en memoria) ---
with open("prompt_sistema.txt", encoding="utf-8") as f: # "r" es el modo por defecto
for numero, linea in enumerate(f, start=1):
print(f"{numero}: {linea.strip()}") # strip() quita el \n final de cada línea
# --- AÑADIR al final sin borrar (modo "a" de append): ideal para logs ---
with open("log_llamadas.txt", "a", encoding="utf-8") as f:
f.write("2026-07-03 10:15 | llamada OK | 850 tokens\n")
JSON — el formato de las APIs de IA. JSON (JavaScript Object Notation) es texto estructurado que se corresponde casi 1:1 con los dicts y listas de Python. Toda petición y toda respuesta de una API de LLM es JSON, así que dominar el módulo json no es opcional:
import json # módulo de la biblioteca estándar
# Un dict de Python con la pinta de una conversación con un LLM
conversacion = {
"model": "claude-sonnet-4-5",
"created": "2026-07-03",
"messages": [
{"role": "user", "content": "¿Qué es un embedding?"},
{"role": "assistant", "content": "Un embedding es un vector numérico..."},
],
"usage": {"input_tokens": 12, "output_tokens": 45},
}
# --- GUARDAR un dict como fichero JSON: json.dump (sin 's' = a fichero) ---
with open("conversacion.json", "w", encoding="utf-8") as f:
json.dump(conversacion, f, ensure_ascii=False, indent=2)
# ensure_ascii=False → guarda tildes/ñ tal cual (no ñ)
# indent=2 → formato legible con sangrado
# --- CARGAR un fichero JSON a un dict: json.load ---
with open("conversacion.json", encoding="utf-8") as f:
recuperada = json.load(f) # ¡y ya es un dict normal de Python!
print(recuperada["usage"]["output_tokens"]) # 45
# --- Las variantes con 's' trabajan con STRINGS (lo que viaja por la red) ---
texto_json = json.dumps(conversacion) # dict → string JSON (para ENVIAR a una API)
otra_vez = json.loads(texto_json) # string JSON → dict (al RECIBIR de una API)
Nota
Tabla mental de equivalencias JSON ↔ Python: object↔dict, array↔list, string↔str, number↔int/float, true/false↔True/False, null↔None. Con eso lees cualquier respuesta de API.
CSV — tablas en texto plano:
import csv
# --- ESCRIBIR un CSV con DictWriter (cada fila es un dict: legible y robusto) ---
clientes = [
{"nombre": "Acme SL", "sector": "retail", "gasto_mensual": 1200.50},
{"nombre": "TechNova", "sector": "software", "gasto_mensual": 4300.00},
{"nombre": "BioSalud", "sector": "salud", "gasto_mensual": 2150.75},
]
with open("clientes.csv", "w", encoding="utf-8", newline="") as f:
# newline="" evita líneas en blanco extra en Windows (peculiaridad del módulo csv)
escritor = csv.DictWriter(f, fieldnames=["nombre", "sector", "gasto_mensual"])
escritor.writeheader() # escribe la fila de cabeceras
escritor.writerows(clientes) # escribe todas las filas de golpe
# --- LEER un CSV con DictReader (cada fila llega como un dict) ---
with open("clientes.csv", encoding="utf-8", newline="") as f:
lector = csv.DictReader(f) # usa la primera fila como claves
for fila in lector:
# ⚠ TODO llega como STRING: hay que convertir los números a mano
gasto = float(fila["gasto_mensual"])
print(f"{fila['nombre']}: {gasto:.2f} €/mes")
Error común
olvidar encoding="utf-8" y encontrarte con UnicodeDecodeError o tildes convertidas en é (mojibake). En Windows el encoding por defecto no es UTF-8 en muchas configuraciones: escribe encoding="utf-8" siempre, en cada open(). Y recuerda: el modo "w" borra el fichero existente sin preguntar; para añadir, usa "a".
Nota
en la sección 5 verás que Pandas lee un CSV entero, con tipos inferidos, en una línea (pd.read_csv). Entonces, ¿para qué aprender el módulo csv? Porque entender el nivel bajo te permite depurar cuando Pandas se atraganta con un fichero raro, y porque no siempre quieres arrastrar una dependencia pesada para leer 10 filas.
3.9 Clases y OOP básica¶
Desde cero. Hasta ahora tus datos (dicts, listas) y tus comportamientos (funciones) viven separados. La Programación Orientada a Objetos (OOP) los une: una clase es un molde que define qué datos tiene una cosa (atributos) y qué sabe hacer (métodos). Cada cosa concreta creada a partir del molde es un objeto o instancia.
¿Por qué te importa? Porque todo el ecosistema de IA es OOP: un DataFrame de Pandas es un objeto, un modelo de PyTorch es un objeto, el cliente de la API de Anthropic es un objeto. Aunque escribas poco OOP al principio, lo usarás desde el minuto uno, y entender qué hay detrás de objeto.metodo() lo cambia todo.
# --- Una clase Documento para un futuro sistema RAG ---
# (RAG = Retrieval-Augmented Generation: darle a un LLM documentos relevantes
# para que responda con conocimiento actualizado. Lo construiremos en módulos futuros.)
class Documento:
"""Representa un documento de texto en un sistema RAG."""
def __init__(self, titulo: str, contenido: str, fuente: str = "desconocida"):
# __init__ es el CONSTRUCTOR: se ejecuta automáticamente al crear el objeto.
# 'self' es EL PROPIO OBJETO: a través de él guardas sus datos.
self.titulo = titulo # atributo: cada documento tiene SU título
self.contenido = contenido # atributo: SU contenido
self.fuente = fuente # atributo con valor por defecto
self.n_caracteres = len(contenido) # atributo CALCULADO en la creación
def resumen_corto(self, n: int = 80) -> str:
# Un MÉTODO es una función que vive dentro de la clase.
# Siempre recibe 'self' primero: así accede a los datos DEL objeto.
return self.contenido[:n] + ("..." if len(self.contenido) > n else "")
def contiene(self, termino: str) -> bool:
"""¿Aparece el término en el documento? (búsqueda simple, base de un retriever)."""
return termino.lower() in self.contenido.lower()
def __repr__(self) -> str:
# __repr__ define cómo se MUESTRA el objeto al imprimirlo (método "mágico").
return f"Documento(titulo='{self.titulo}', chars={self.n_caracteres})"
# --- Crear objetos (instanciar) y usarlos ---
doc1 = Documento( # esto LLAMA a __init__
titulo="Manual de embeddings",
contenido="Un embedding es un vector numérico que captura el significado de un texto...",
fuente="wiki-interna",
)
doc2 = Documento("Política de vacaciones", "Los empleados disponen de 23 días laborables...")
print(doc1) # usa __repr__: Documento(titulo='Manual de embeddings', chars=89)
print(doc1.titulo) # acceder a un atributo con punto
print(doc1.resumen_corto(40)) # llamar a un método (self se pasa solo)
print(doc2.contiene("vacaciones")) # True
# El patrón clásico: una LISTA de objetos + filtrado
biblioteca = [doc1, doc2]
relevantes = [d for d in biblioteca if d.contiene("embedding")]
print(f"Documentos sobre embeddings: {relevantes}")
Herencia simple — una clase puede extender otra, heredando sus atributos y métodos:
class DocumentoPDF(Documento): # entre paréntesis: la clase MADRE
"""Un documento que además sabe de qué página viene."""
def __init__(self, titulo: str, contenido: str, pagina: int):
super().__init__(titulo, contenido, fuente="pdf") # super() llama al __init__ de la madre
self.pagina = pagina # y añadimos lo específico
def cita(self) -> str: # método NUEVO, solo de los PDF
return f"«{self.titulo}», pág. {self.pagina}"
pdf = DocumentoPDF("Informe anual 2025", "Los ingresos crecieron un 14%...", pagina=7)
print(pdf.resumen_corto(30)) # ¡heredado de Documento sin escribirlo de nuevo!
print(pdf.cita()) # «Informe anual 2025», pág. 7
Dataclasses — clases de datos sin ceremonia. Cuando la clase es sobre todo "un contenedor de datos", el decorador @dataclass te escribe __init__ y __repr__ automáticamente:
from dataclasses import dataclass, field
@dataclass
class FragmentoRAG:
"""Un trozo de documento listo para indexar en un sistema RAG."""
texto: str # los atributos se declaran con type hints
doc_origen: str
posicion: int = 0 # con valor por defecto
embedding: list[float] = field(default_factory=list) # ⚠ mutables: SIEMPRE con field()
def tamaño(self) -> int: # también puede tener métodos normales
return len(self.texto)
frag = FragmentoRAG(texto="Un embedding es un vector...", doc_origen="wiki", posicion=3)
print(frag) # __repr__ gratis: FragmentoRAG(texto='Un embedding...', doc_origen='wiki', ...)
print(frag.tamaño()) # 28
Consejo profesional
en 2026, para modelos de datos que validan JSON de APIs se usa muchísimo Pydantic (dataclasses con validación automática). Lo verás en el módulo de backend; conceptualmente es lo que acabas de aprender con esteroides.
Error común
olvidar self como primer parámetro de un método (TypeError: metodo() takes 0 positional arguments but 1 was given — Python pasa el objeto automáticamente y no tiene dónde ponerlo). También: confundir la clase (Documento, el molde) con la instancia (doc1, la galleta). Los atributos definidos en __init__ con self. pertenecen a cada instancia por separado.
3.10 Módulos, imports y estructura de proyecto¶
Desde cero. Un módulo es simplemente un fichero .py cuyo código puedes reutilizar desde otro fichero con import. Un paquete es una carpeta de módulos. Toda la potencia de Python viene de importar: la biblioteca estándar (incluida), y las bibliotecas externas (instaladas con pip/uv).
# --- Formas de importar, de más a menos explícita ---
import json # 1) importar el módulo entero
json.dumps({"a": 1}) # se usa con prefijo: json.loquesea
import numpy as np # 2) importar con ALIAS (convención universal)
np.array([1, 2, 3]) # np y pd son alias sagrados del ecosistema
from pathlib import Path # 3) importar UN nombre concreto del módulo
Path("datos/ventas.csv") # se usa sin prefijo
# from numpy import * # 4) ❌ NUNCA: importa todo sin control,
# contamina el espacio de nombres y crea colisiones
Tus propios módulos. Si tienes utilidades.py con una función limpiar_texto, desde otro fichero de la misma carpeta haces from utilidades import limpiar_texto. Así se trocea un programa grande en piezas mantenibles.
El idioma if __name__ == "__main__": — lo verás en todos los scripts profesionales:
# analisis.py
def ejecutar_analisis() -> None:
print("Analizando datos...")
if __name__ == "__main__":
# Este bloque SOLO se ejecuta si lanzas el fichero directamente
# (python analisis.py). Si otro fichero hace "import analisis",
# NO se ejecuta: solo se cargan las definiciones.
# Así un mismo fichero sirve como script Y como módulo reutilizable.
ejecutar_analisis()
Estructura de un proyecto Python profesional (2026):
mi-proyecto-ia/
├── pyproject.toml ← ficha del proyecto: nombre, dependencias, config
├── uv.lock ← versiones EXACTAS congeladas (lo genera uv)
├── index.md ← qué es el proyecto y cómo usarlo
├── .gitignore ← qué NO subir a git (.venv/, __pycache__/, .env)
├── .env ← claves de API (¡NUNCA se sube a git!)
├── .venv/ ← entorno virtual (regenerable, tampoco se sube)
├── src/
│ └── mi_proyecto/ ← el código fuente, como paquete instalable
│ ├── __init__.py ← (puede estar vacío) marca la carpeta como paquete
│ ├── main.py ← punto de entrada
│ ├── datos.py ← módulo: carga y limpieza de datos
│ └── informes.py ← módulo: generación de informes
├── tests/
│ └── test_datos.py ← tests automáticos (pytest) — módulos futuros
└── data/
└── ventas.csv ← datos de ejemplo (los grandes NO van a git)
Advertencia — el error de import más traicionero
nombrar tu fichero como un módulo famoso. Si creas un fichero llamado random.py o json.py en tu carpeta y luego haces import random, ¡Python importa TU fichero, no el de la biblioteca estándar! El síntoma son errores absurdos tipo AttributeError: module 'random' has no attribute 'random'. Nunca llames a tus ficheros random.py, json.py, csv.py, numpy.py, pandas.py, test.py...
Consejo profesional
las claves de API (como ANTHROPIC_API_KEY) van en variables de entorno o en un fichero .env (leído con la biblioteca python-dotenv), jamás escritas en el código. Un .env subido a GitHub por accidente = clave robada en minutos por bots que escanean repos. Añade .env a tu .gitignore antes que nada.
3.11 Async básico: la intuición¶
Desde cero. Cuando tu programa llama a la API de un LLM, pasa la mayor parte del tiempo... esperando la respuesta por la red (a menudo varios segundos). Con código normal (síncrono), el programa se queda bloqueado mirando al infinito. Con asyncio, mientras una llamada espera, el programa aprovecha para lanzar y atender otras.
La analogía del camarero: un camarero síncrono toma nota en la mesa 1, va a cocina, espera de pie a que el plato salga, lo sirve, y solo entonces va a la mesa 2. Un camarero asíncrono toma nota en la mesa 1, deja la comanda en cocina y, mientras se cocina, atiende las mesas 2 y 3. Mismo camarero (un solo hilo), triple productividad. La clave: async no acelera el trabajo, aprovecha las esperas.
import asyncio # biblioteca estándar para programación asíncrona
import time
async def llamar_llm(nombre: str, segundos: float) -> str:
# 'async def' define una CORRUTINA: una función pausable
print(f"→ Lanzando llamada '{nombre}'...")
await asyncio.sleep(segundos) # 'await' = "pausa AQUÍ y cede el turno";
# simula la espera de red de una API real
print(f"← '{nombre}' respondió")
return f"respuesta de {nombre}"
async def main() -> None:
inicio = time.perf_counter() # cronómetro de precisión
# asyncio.gather lanza las tres llamadas CONCURRENTEMENTE:
# las tres esperas de red transcurren A LA VEZ, no una tras otra
resultados = await asyncio.gather(
llamar_llm("resumen", 2.0),
llamar_llm("traducción", 2.0),
llamar_llm("clasificación", 2.0),
)
duracion = time.perf_counter() - inicio
print(f"3 llamadas de 2s completadas en {duracion:.1f}s (¡no en 6s!)")
print(resultados)
asyncio.run(main()) # arranca el "bucle de eventos" y ejecuta main()
# Salida: las tres se lanzan seguidas y el total es ~2.0s en vez de ~6.0s
Nota
Quédate con tres ideas y sigue adelante: (1) async def define funciones pausables, (2) await marca los puntos de espera donde se cede el turno, (3) asyncio.gather ejecuta varias esperas a la vez. Los SDK de LLM ofrecen clientes async precisamente para esto: procesar 100 documentos en paralelo en el tiempo de ~1. Profundizaremos de verdad en el módulo 10-BACKEND; por ahora basta la intuición.
Error común
llamar a una corrutina sin await (llamar_llm("x", 1) a secas no ejecuta nada: devuelve un objeto corrutina y Python te avisa con RuntimeWarning: coroutine was never awaited). Y usar await fuera de una función async def (SyntaxError).
4. NumPy: la base numérica de toda la IA¶
4.1 Por qué NumPy es la base de todo el stack¶
En el Capítulo 3 viste que la IA es, en el fondo, álgebra lineal: vectores, matrices y operaciones entre ellas. NumPy (Numerical Python) es la biblioteca que hace eso posible de forma eficiente, y es literalmente el cimiento sobre el que se construyen Pandas, scikit-learn, y la inspiración directa de la API de PyTorch (los "tensores" de PyTorch son primos hermanos de los arrays de NumPy).
¿Por qué no usar listas de Python? Dos razones:
- Memoria: una lista de Python guarda objetos dispersos con mucha sobrecarga; un array de NumPy guarda números crudos, contiguos en memoria, como en C.
- Velocidad: las operaciones de NumPy están escritas en C optimizado y operan sobre todo el array de golpe (vectorización), sin el coste del bucle interpretado de Python. Lo mediremos con un benchmark real en 4.5.
4.2 Arrays: creación, shape y dtype¶
import numpy as np # el alias 'np' es universal: úsalo siempre
# --- Crear arrays ---
v = np.array([1.0, 2.0, 3.0]) # vector (1 dimensión) desde una lista
M = np.array([[1, 2, 3], # matriz (2 dimensiones): lista de listas
[4, 5, 6]])
ceros = np.zeros((2, 3)) # matriz 2x3 llena de ceros
unos = np.ones(5) # vector de cinco 1.0
rango = np.arange(0, 10, 2) # como range(): [0 2 4 6 8]
malla = np.linspace(0, 1, 5) # 5 puntos EQUIESPACIADOS entre 0 y 1:
# [0. 0.25 0.5 0.75 1. ]
azar = np.random.rand(3, 3) # matriz 3x3 de aleatorios en [0,1)
# (así se INICIALIZAN los pesos de una red neuronal)
# --- Los tres atributos que SIEMPRE mirarás ---
print(M.shape) # (2, 3) → la FORMA: 2 filas, 3 columnas. El atributo nº1 del deep learning.
print(M.ndim) # 2 → número de dimensiones (ejes)
print(M.dtype) # int64 → tipo de dato de CADA elemento (todos iguales, a diferencia de las listas)
# El dtype importa: float32 usa la mitad de memoria que float64
# (los modelos de deep learning se entrenan típicamente en float32 o menos)
pesos = np.array([0.1, 0.2, 0.3], dtype=np.float32)
print(pesos.dtype) # float32
# --- reshape: reorganizar sin cambiar los datos ---
a = np.arange(12) # [0 1 2 ... 11], shape (12,)
B = a.reshape(3, 4) # ahora es una matriz 3x4 (3*4 debe ser == 12)
C = a.reshape(2, -1) # -1 = "calcula tú esta dimensión": queda 2x6
Nota
en deep learning te pasarás la vida leyendo shapes: un lote de 32 frases de 128 tokens con embeddings de 768 dimensiones es un array de shape (32, 128, 768). Errores de shape (ValueError: shapes not aligned) son EL error diario del practicante de ML. Acostúmbrate a imprimir .shape sin vergüenza.
4.3 Indexación y slicing¶
M = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
# --- Acceso por [fila, columna] ---
print(M[0, 0]) # 10 (esquina superior izquierda)
print(M[1, 2]) # 60 (fila 1, columna 2 — recuerda: desde 0)
print(M[-1, -1]) # 90 (negativos: desde el final)
# --- Slicing por ejes: [filas, columnas], con la regla inicio:fin (fin excluido) ---
print(M[0, :]) # fila 0 completa: [10 20 30] (: = "todo este eje")
print(M[:, 1]) # columna 1 completa: [20 50 80]
print(M[0:2, 1:3]) # submatriz 2x2: [[20 30], [50 60]]
# --- Indexación BOOLEANA: la joya de la corona ---
notas = np.array([4.5, 9.1, 6.7, 3.2, 8.8, 5.0])
mascara = notas >= 5.0 # compara TODO el array de golpe:
print(mascara) # [False True True False True True]
aprobados = notas[mascara] # filtra: se queda donde la máscara es True
print(aprobados) # [9.1 6.7 8.8 5. ]
# En una línea, como se escribe en la práctica:
print(notas[notas >= 5.0]) # mismo resultado
notas[notas < 5.0] = 5.0 # y también sirve para MODIFICAR: "aprobado general"
Error común
los slices de NumPy son vistas, no copias: si haces sub = M[0:2, :] y modificas sub, ¡modificas M también! Si necesitas independencia, pide una copia explícita: sub = M[0:2, :].copy().
4.4 Broadcasting: operar entre formas distintas¶
Desde cero. Broadcasting es la regla por la que NumPy permite operar entre arrays de formas diferentes, "estirando" virtualmente el pequeño para que encaje con el grande, sin copiar datos. Es lo que hace que el código de ML sea tan corto.
El caso más simple — array y escalar:
[1, 2, 3] * 10 El escalar 10 se "estira" a [10, 10, 10]:
[ 1 2 3 ] [ 10 10 10 ] [ 10 20 30 ]
* → (virtual) =
10
Caso matriz + vector (el que usarás a diario para normalizar datos):
Matriz (3,3) + vector (3,) = resultado (3,3)
[ 1 2 3 ] [ 10 20 30 ] ← el vector se replica
[ 4 5 6 ] + [ 10 20 30 ] ← virtualmente en cada fila
[ 7 8 9 ] [ 10 20 30 ] ← (sin gastar memoria)
[ 11 22 33 ]
= [ 14 25 36 ]
[ 17 28 39 ]
La regla formal: dos dimensiones son compatibles si son iguales o si una de ellas es 1 (o no existe). Se comparan de derecha a izquierda:
(3, 3) y (3,) → compatible: el vector se difunde por filas ✅
(4, 1) y (1, 5) → compatible: resultado (4, 5) ✅
(3, 4) y (3,) → ❌ ValueError: 4 ≠ 3 en el último eje
import numpy as np
M = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
v = np.array([10, 20, 30])
print(M + v) # el vector se suma a CADA fila (broadcasting)
print(M * 2) # escalar: multiplica cada elemento
print(M - M.mean(axis=0)) # centrar cada columna en su media:
# M.mean(axis=0) tiene shape (3,) y se difunde.
# Esto ES la estandarización de features de ML en una línea.
Nota sobre axis
axis=0 opera "a lo largo de las filas" (colapsa filas → resultado por columna); axis=1 colapsa columnas (→ resultado por fila). Truco mnemotécnico: el eje que pones es el que desaparece. M.sum(axis=0) en una matriz (3,4) da shape (4,).
4.5 Vectorización vs. bucles: el benchmark¶
La promesa de NumPy es velocidad. Midámosla con rigor, con código que puedes ejecutar tal cual:
import numpy as np
import time
N = 10_000_000 # diez millones de números
# --- Preparamos los mismos datos en ambos formatos ---
lista_python = list(range(N)) # lista clásica de Python
array_numpy = np.arange(N) # array de NumPy
# --- Método 1: bucle de Python puro ---
inicio = time.perf_counter() # cronómetro de alta precisión
suma_cuadrados_lista = 0
for x in lista_python: # 10 millones de vueltas interpretadas
suma_cuadrados_lista += x * x # cada operación pasa por el intérprete
t_bucle = time.perf_counter() - inicio
# --- Método 2: operación vectorizada de NumPy ---
inicio = time.perf_counter()
suma_cuadrados_np = np.sum(array_numpy.astype(np.int64) ** 2)
# ** 2 eleva TODOS los elementos de golpe (en C); np.sum también corre en C
t_numpy = time.perf_counter() - inicio
print(f"Bucle Python : {t_bucle:.3f} s")
print(f"NumPy : {t_numpy:.3f} s")
print(f"NumPy es ~{t_bucle / t_numpy:.0f}x más rápido")
# Resultados típicos en un portátil de 2026:
# Bucle Python : 1.100 s
# NumPy : 0.015 s
# NumPy es ~70x más rápido
# (el factor exacto varía por máquina: entre 30x y 200x según la operación)
Consejo profesional
interioriza la regla: si estás escribiendo un for sobre números, probablemente lo estás haciendo mal. Busca la operación vectorizada equivalente (suma, máscara booleana, broadcasting). Esta mentalidad "piensa en arrays, no en elementos" es exactamente la que necesitarás en PyTorch.
4.6 Funciones clave para IA¶
import numpy as np
# Simulemos la SALIDA de un clasificador de sentimiento para 4 frases:
# cada fila = una frase; columnas = prob(negativo), prob(neutro), prob(positivo)
probs = np.array([
[0.10, 0.20, 0.70],
[0.80, 0.15, 0.05],
[0.30, 0.40, 0.30],
[0.05, 0.10, 0.85],
])
# --- Estadísticos básicos ---
print(probs.mean()) # 0.333... media GLOBAL de toda la matriz
print(probs.mean(axis=0)) # media POR COLUMNA: prob. media de cada clase
print(probs.max(axis=1)) # máximo POR FILA: la confianza de cada predicción
print(probs.std()) # desviación típica (dispersión)
# --- argmax: LA función de la clasificación ---
# "¿QUÉ ÍNDICE tiene el valor máximo?" → convierte probabilidades en decisiones
predicciones = probs.argmax(axis=1) # índice de la clase ganadora por fila
print(predicciones) # [2 0 1 2]
clases = np.array(["negativo", "neutro", "positivo"])
print(clases[predicciones]) # ['positivo' 'negativo' 'neutro' 'positivo']
# esto es EXACTAMENTE lo que hace la última capa de un clasificador neuronal
# --- dot: producto escalar / multiplicación de matrices ---
# El producto escalar mide "cuánto se parecen" dos vectores.
# Es LA operación de los embeddings y la base del mecanismo de atención de los LLM.
emb_perro = np.array([0.9, 0.1, 0.8]) # embedding (inventado) de "perro"
emb_gato = np.array([0.85, 0.15, 0.75]) # "gato": vector parecido
emb_factura = np.array([0.05, 0.9, 0.1]) # "factura": vector muy distinto
print(np.dot(emb_perro, emb_gato)) # 1.38 → alto: significados cercanos
print(np.dot(emb_perro, emb_factura)) # 0.215 → bajo: significados lejanos
print(emb_perro @ emb_gato) # el operador @ es equivalente a np.dot
# Similitud coseno: producto escalar normalizado (el estándar en búsqueda semántica)
def similitud_coseno(a: np.ndarray, b: np.ndarray) -> float:
return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))
# np.linalg.norm = longitud (módulo) del vector
print(similitud_coseno(emb_perro, emb_gato)) # ~0.998 (casi idénticos)
print(similitud_coseno(emb_perro, emb_factura)) # ~0.20 (poco relacionados)
Tabla de referencia rápida de NumPy:
| Función / atributo | Qué hace | Ejemplo de uso en IA |
|---|---|---|
arr.shape |
Forma del array | Verificar dimensiones de un lote de datos |
arr.dtype |
Tipo de los elementos | Comprobar precisión (float32 vs float64) |
arr.reshape(f, c) |
Reorganizar la forma | Aplanar imágenes para una red densa |
arr.mean()/std()/sum() |
Estadísticos (acepta axis=) |
Normalizar features |
arr.argmax(axis=1) |
Índice del máximo | Convertir probabilidades en clase predicha |
a @ b / np.dot |
Producto matricial/escalar | Embeddings, capas de redes neuronales |
arr[mascara_booleana] |
Filtrado vectorizado | Seleccionar predicciones con confianza > 0.9 |
np.linalg.norm(v) |
Módulo del vector | Similitud coseno en búsqueda semántica |
np.random.rand(...) |
Aleatorios uniformes | Inicializar pesos, simular datos |
np.linspace(a, b, n) |
n puntos equiespaciados | Ejes para gráficas, barridos de hiperparámetros |
Ejercicio rápido 3: Dado puntuaciones = np.array([[0.2, 0.8], [0.6, 0.4], [0.1, 0.9], [0.55, 0.45]]) (probabilidades de spam/no-spam por email... al revés: columna 0 = no-spam, columna 1 = spam): (a) ¿qué emails se clasifican como spam (argmax)?; (b) ¿cuál es la confianza media de las predicciones ganadoras?; (c) filtra las filas cuya confianza ganadora supere 0.7.
Ver solución
import numpy as np
puntuaciones = np.array([[0.2, 0.8], [0.6, 0.4], [0.1, 0.9], [0.55, 0.45]])
# (a) clase ganadora por fila: 1 = spam, 0 = no-spam
pred = puntuaciones.argmax(axis=1) # [1 0 1 0] → emails 0 y 2 son spam
# (b) confianza de cada predicción = máximo por fila; luego su media
confianzas = puntuaciones.max(axis=1) # [0.8 0.6 0.9 0.55]
print(confianzas.mean()) # 0.7125
# (c) filtrado booleano por confianza
seguras = puntuaciones[confianzas > 0.7] # filas 0 y 2
print(seguras) # [[0.2 0.8], [0.1 0.9]]
5. Pandas: datos tabulares como un profesional¶
5.1 Series y DataFrame¶
Si NumPy es la calculadora matricial, Pandas es tu Excel programable: la herramienta estándar para datos tabulares (filas y columnas con nombre). Sus dos estructuras:
Series: una columna (un array de NumPy con etiquetas de índice).DataFrame: la tabla completa (varias Series que comparten índice). Es EL objeto del análisis de datos.
import pandas as pd # 'pd' es el alias universal
# --- Una Series: columna con índice ---
gastos = pd.Series([1200.5, 4300.0, 2150.75],
index=["Acme SL", "TechNova", "BioSalud"],
name="gasto_mensual")
print(gastos["TechNova"]) # 4300.0 — acceso por etiqueta
print(gastos.mean()) # 2550.42 — métodos estadísticos incorporados
# --- Un DataFrame: la tabla completa, desde un dict de columnas ---
df = pd.DataFrame({
"cliente": ["Acme SL", "TechNova", "BioSalud", "LogisPro"],
"sector": ["retail", "software", "salud", "logística"],
"gasto_mensual": [1200.5, 4300.0, 2150.75, 890.0],
"meses_activo": [24, 6, 18, 3],
})
print(df)
# cliente sector gasto_mensual meses_activo
# 0 Acme SL retail 1200.50 24
# 1 TechNova software 4300.00 6
# 2 BioSalud salud 2150.75 18
# 3 LogisPro logística 890.00 3
5.2 Leer CSV e inspeccionar¶
En la vida real los datos llegan en ficheros. El ritual profesional al recibir CUALQUIER dataset son estas cuatro llamadas, siempre, antes de tocar nada:
import pandas as pd
df = pd.read_csv("ventas.csv") # lee el CSV: infiere tipos y cabeceras solo
# (read_excel, read_json, read_parquet: igual de fáciles)
# --- El ritual de inspección: 4 comandos, SIEMPRE ---
print(df.head()) # 1) primeras 5 filas: ¿qué pinta tienen los datos?
print(df.info()) # 2) columnas, tipos y NULOS por columna: la radiografía
print(df.describe()) # 3) estadísticos de las columnas numéricas:
# count, mean, std, min, cuartiles, max
print(df.shape) # 4) (n_filas, n_columnas)
Nota
df.info() es tu detector de problemas: si una columna que debería ser numérica aparece como object (texto), hay valores sucios ("N/A", "1.200,50 €"...). Si Non-Null Count es menor que el total de filas, hay nulos. Los dos problemas se arreglan ANTES de analizar.
5.3 Selección y filtrado¶
# --- Seleccionar columnas ---
df["gasto_mensual"] # UNA columna → devuelve una Series
df[["cliente", "gasto_mensual"]] # VARIAS columnas (lista dentro de []) → DataFrame
# --- Filtrar filas con máscaras booleanas (igual que en NumPy) ---
grandes = df[df["gasto_mensual"] > 2000] # clientes que gastan > 2000
software = df[df["sector"] == "software"] # por igualdad
# Condiciones combinadas: & (y), | (o) — ⚠ SIEMPRE con paréntesis en cada condición
fieles_y_grandes = df[(df["gasto_mensual"] > 1000) & (df["meses_activo"] >= 12)]
# .isin(): pertenencia a una lista de valores
tech_salud = df[df["sector"].isin(["software", "salud"])]
# --- loc e iloc: selección explícita de filas y columnas ---
df.loc[0, "cliente"] # loc: por ETIQUETA → "Acme SL"
df.iloc[0, 0] # iloc: por POSICIÓN → "Acme SL"
df.loc[df["sector"] == "salud", ["cliente", "gasto_mensual"]] # filtro + columnas
# --- Crear columnas nuevas (vectorizado, sin bucles) ---
df["gasto_anual"] = df["gasto_mensual"] * 12 # aritmética entre columnas
df["es_vip"] = df["gasto_anual"] > 20_000 # columna booleana
Error común
usar and/or en filtros de Pandas (ValueError: The truth value of a Series is ambiguous). En Pandas/NumPy las condiciones se combinan con & y |, y cada condición entre paréntesis. También verás el famoso SettingWithCopyWarning: suele significar que estás modificando un filtro (una posible vista) en vez del DataFrame original; asigna con df.loc[condición, "columna"] = valor para hacerlo bien.
5.4 Valores nulos¶
Los datos reales vienen sucios: celdas vacías, "N/A", nulos. Pandas los representa como NaN (Not a Number):
import numpy as np
df_sucio = pd.DataFrame({
"cliente": ["Acme", "TechNova", "BioSalud", "LogisPro"],
"gasto": [1200.5, np.nan, 2150.75, np.nan], # dos nulos
"email": ["a@acme.com", "t@technova.io", None, "l@logispro.es"],
})
# --- Detectar ---
print(df_sucio.isna()) # tabla de True/False donde hay nulos
print(df_sucio.isna().sum()) # nulos POR COLUMNA: el resumen que quieres
# gasto 2
# email 1
# --- Estrategia 1: eliminar filas con nulos (si son pocas y prescindibles) ---
sin_nulos = df_sucio.dropna() # elimina TODA fila con algún nulo
sin_nulos_gasto = df_sucio.dropna(subset=["gasto"]) # solo si el nulo está en 'gasto'
# --- Estrategia 2: imputar (rellenar) con un valor razonable ---
mediana = df_sucio["gasto"].median() # robusta ante extremos
df_sucio["gasto"] = df_sucio["gasto"].fillna(mediana) # rellena nulos con la mediana
df_sucio["email"] = df_sucio["email"].fillna("sin-email") # o con un marcador
Consejo profesional
eliminar vs. imputar no es una decisión técnica sino de negocio: ¿puedes permitirte perder esas filas? ¿la mediana es representativa? Documenta siempre qué hiciste con los nulos: en ML, una imputación mal hecha sesga silenciosamente el modelo.
5.5 groupby: el corazón del análisis¶
groupby responde a las preguntas de negocio de verdad: "ventas por región", "gasto medio por sector", "errores por modelo". El patrón mental es dividir → aplicar → combinar: divide la tabla en grupos, aplica una función a cada grupo, combina los resultados.
ventas = pd.DataFrame({
"region": ["Norte", "Sur", "Norte", "Este", "Sur", "Norte", "Este"],
"producto": ["A", "A", "B", "A", "B", "A", "B"],
"importe": [1200, 850, 2100, 640, 1750, 980, 1430],
})
# --- Una métrica por grupo ---
print(ventas.groupby("region")["importe"].sum())
# region
# Este 2070
# Norte 4280
# Sur 2600
# --- Varias métricas de golpe con .agg() ---
resumen = ventas.groupby("region")["importe"].agg(["sum", "mean", "count"])
print(resumen)
# sum mean count
# Este 2070 1035.000000 2
# Norte 4280 1426.666667 3
# Sur 2600 1300.000000 2
# --- Agrupar por VARIAS columnas ---
por_region_producto = ventas.groupby(["region", "producto"])["importe"].sum()
# --- Ordenar el resultado: el "top N" de toda la vida ---
top = ventas.groupby("region")["importe"].sum().sort_values(ascending=False)
print(top.head(2)) # las 2 regiones con más ventas
5.6 merge: cruzar tablas¶
En la práctica los datos viven repartidos en varias tablas (clientes por un lado, pedidos por otro). merge las cruza por una columna común, como el JOIN de SQL o el BUSCARV de Excel:
clientes = pd.DataFrame({
"cliente_id": [1, 2, 3],
"nombre": ["Acme SL", "TechNova", "BioSalud"],
"sector": ["retail", "software", "salud"],
})
pedidos = pd.DataFrame({
"pedido_id": [101, 102, 103, 104],
"cliente_id": [1, 2, 1, 4], # ojo: el cliente 4 no existe en 'clientes'
"importe": [250.0, 1200.0, 90.5, 430.0],
})
# inner (por defecto): solo filas cuyo cliente_id está EN AMBAS tablas
inner = pd.merge(pedidos, clientes, on="cliente_id", how="inner")
# → 3 filas (el pedido 104 desaparece: su cliente no existe)
# left: TODAS las filas de la izquierda; lo que no cruza queda con NaN
left = pd.merge(pedidos, clientes, on="cliente_id", how="left")
# → 4 filas (el pedido 104 tiene NaN en nombre y sector)
print(left)
how= |
Se queda con... | Cuándo usarlo |
|---|---|---|
inner |
Solo claves presentes en ambas tablas | Análisis donde el cruce debe ser completo |
left |
Todas las de la izquierda (+ NaN si no cruza) | Enriquecer una tabla principal sin perder filas |
right |
Todas las de la derecha | Igual que left, invertido (raro; reordena y usa left) |
outer |
Todas las claves de ambas | Auditar diferencias entre tablas |
Caso empresarial
una cadena de tiendas quiere saber qué sectores de clientes generan más ingresos. Los datos están en dos sistemas: el CRM (clientes) y el ERP (pedidos). El análisis completo es exactamente lo que ya sabes: merge de ambas tablas por cliente_id + groupby("sector")["importe"].sum() + sort_values. Cuatro líneas de Pandas que en Excel serían una mañana de BUSCARV y tablas dinámicas — y que aquí quedan guardadas como código reproducible que mañana se re-ejecuta con los datos nuevos en un segundo.
Ejercicio rápido 4: Con el DataFrame ventas de la sección 5.5, calcula el importe medio por producto y quédate solo con los productos cuya media supere 1200.
Ver solución
6. Ejemplo integrador: del CSV al informe¶
Vamos a unirlo TODO —funciones tipadas, excepciones, ficheros, dicts, Pandas, NumPy— en un proyecto realista de dificultad creciente: nivel sencillo → intermedio → avanzado → caso empresarial completo.
Escenario: trabajas en el equipo de datos de una empresa SaaS. Te pasan un CSV exportado del CRM con los clientes y te piden "un informe con lo importante". El CSV viene sucio, claro (siempre viene sucio).
Primero, genera el fichero de datos de prueba (guárdalo como generar_datos.py y ejecútalo una vez):
# generar_datos.py — crea el CSV de ejemplo 'clientes.csv' (con suciedad realista)
import csv
import random
random.seed(42) # semilla fija: resultados reproducibles
SECTORES = ["retail", "software", "salud", "logística", "finanzas"]
PLANES = ["basic", "pro", "enterprise"]
filas = []
for i in range(1, 201): # 200 clientes
fila = {
"cliente_id": i,
"nombre": f"Cliente_{i:03d}", # Cliente_001, Cliente_002...
"sector": random.choice(SECTORES), # sector al azar
"plan": random.choice(PLANES),
"gasto_mensual": round(random.uniform(50, 5000), 2),
"meses_activo": random.randint(1, 48),
"tickets_soporte": random.randint(0, 30),
}
if random.random() < 0.08: # 8% de gastos perdidos
fila["gasto_mensual"] = "" # celda vacía → NaN al leer
if random.random() < 0.05: # 5% de sectores en mayúsculas
fila["sector"] = fila["sector"].upper() # suciedad típica de CRM
filas.append(fila)
with open("clientes.csv", "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=list(filas[0].keys()))
w.writeheader()
w.writerows(filas)
print(f"Generado clientes.csv con {len(filas)} filas (con nulos y suciedad a propósito)")
Nivel 1 — Sencillo: cargar y mirar¶
# nivel1_explorar.py — el primer contacto con CUALQUIER dataset
import pandas as pd
df = pd.read_csv("clientes.csv") # 1) cargar
print(df.head()) # 2) ver la pinta de las primeras filas
print(df.info()) # 3) tipos y nulos: gasto_mensual tendrá nulos
print(df.describe()) # 4) estadísticos de las columnas numéricas
print(df["sector"].value_counts()) # 5) conteo por categoría: ¡verás 'RETAIL' y
# 'retail' como categorías distintas! (suciedad)
Nivel 2 — Intermedio: limpiar y calcular métricas¶
# nivel2_limpiar.py — limpieza y primeras métricas, comentado línea a línea
import pandas as pd
df = pd.read_csv("clientes.csv")
# --- LIMPIEZA ---
df["sector"] = df["sector"].str.lower().str.strip()
# .str accede a operaciones de texto de TODA la columna a la vez:
# minúsculas + sin espacios → 'RETAIL' y 'retail' se unifican
nulos_antes = df["gasto_mensual"].isna().sum() # ¿cuántos gastos faltan?
mediana_gasto = df["gasto_mensual"].median() # mediana: robusta a extremos
df["gasto_mensual"] = df["gasto_mensual"].fillna(mediana_gasto) # imputamos
print(f"Imputados {nulos_antes} gastos nulos con la mediana ({mediana_gasto:.2f} €)")
# --- MÉTRICAS DERIVADAS ---
df["gasto_anual"] = df["gasto_mensual"] * 12 # columna calculada
df["tickets_por_mes"] = df["tickets_soporte"] / df["meses_activo"] # tasa de soporte
# --- PRIMERAS RESPUESTAS DE NEGOCIO ---
print(f"\nGasto mensual medio: {df['gasto_mensual'].mean():.2f} €")
print(f"Cliente que más gasta: {df.loc[df['gasto_mensual'].idxmax(), 'nombre']}")
# idxmax() = índice de la fila con el valor máximo; .loc lo localiza
print("\nGasto medio por sector:")
print(df.groupby("sector")["gasto_mensual"].mean().sort_values(ascending=False).round(2))
Nivel 3 — Avanzado: funciones tipadas, validación y segmentación¶
# nivel3_segmentar.py — estructura profesional: funciones puras, tipadas y validadas
import pandas as pd
import numpy as np
COLUMNAS_OBLIGATORIAS = {"cliente_id", "sector", "gasto_mensual", "meses_activo"}
def cargar_y_validar(ruta: str) -> pd.DataFrame:
"""Carga el CSV y falla PRONTO y CLARO si el formato no es el esperado."""
try:
df = pd.read_csv(ruta)
except FileNotFoundError:
# Reempaquetamos el error con contexto útil para quien llame
raise FileNotFoundError(f"No existe el fichero de datos: {ruta}") from None
faltan = COLUMNAS_OBLIGATORIAS - set(df.columns) # operación de sets (¡sección 3.4!)
if faltan:
raise ValueError(f"El CSV no tiene las columnas obligatorias: {faltan}")
return df
def limpiar(df: pd.DataFrame) -> pd.DataFrame:
"""Normaliza texto e imputa nulos. Devuelve una COPIA (no muta la entrada)."""
df = df.copy() # buena práctica: funciones puras
df["sector"] = df["sector"].str.lower().str.strip()
df["gasto_mensual"] = df["gasto_mensual"].fillna(df["gasto_mensual"].median())
return df
def segmentar(df: pd.DataFrame) -> pd.DataFrame:
"""Añade un segmento de valor por cliente usando cuartiles de gasto."""
df = df.copy()
# np.select: vectoriza un if/elif/else sobre TODA la columna de golpe
q75 = df["gasto_mensual"].quantile(0.75) # cuartil superior
q25 = df["gasto_mensual"].quantile(0.25) # cuartil inferior
condiciones = [
df["gasto_mensual"] >= q75, # top 25% de gasto
df["gasto_mensual"] <= q25, # bottom 25%
]
etiquetas = ["premium", "básico"]
df["segmento"] = np.select(condiciones, etiquetas, default="estándar")
return df
if __name__ == "__main__": # idioma de la sección 3.10
df = segmentar(limpiar(cargar_y_validar("clientes.csv"))) # pipeline encadenado
print(df.groupby("segmento")["gasto_mensual"].agg(["count", "mean"]).round(2))
print("\nClientes premium con muchos tickets (¡riesgo de fuga!):")
riesgo = df[(df["segmento"] == "premium") & (df["tickets_soporte"] > 20)]
print(riesgo[["nombre", "gasto_mensual", "tickets_soporte"]].to_string(index=False))
Nivel 4 — Caso empresarial: el script de informe completo¶
Caso empresarial
dirección quiere, cada lunes, un informe de la cartera de clientes en texto plano (para pegar en el email semanal) y un JSON con las métricas (para el dashboard interno). Este es el script de producción, completo y comentado, que une TODO el capítulo:
# informe_clientes.py — Informe semanal de cartera de clientes
# AI Master Academy · Capítulo 4 · Ejemplo integrador final
# Ejecutar: python informe_clientes.py clientes.csv
# ─────────────────────────────────────────────────────────────
import json # para exportar métricas al dashboard (sección 3.8)
import sys # para leer argumentos de línea de comandos
from dataclasses import dataclass # para el objeto de resultados (sección 3.9)
from datetime import date # fecha del informe
import numpy as np # cálculo numérico (sección 4)
import pandas as pd # datos tabulares (sección 5)
# --- Constantes de negocio (MAYÚSCULAS por convención, sección 3.3) ---
UMBRAL_TICKETS_RIESGO = 20 # más tickets que esto = cliente en riesgo
COLUMNAS_OBLIGATORIAS = {"cliente_id", "nombre", "sector", "plan",
"gasto_mensual", "meses_activo", "tickets_soporte"}
# --- Excepción propia: errores de datos con nombre y apellidos (sección 3.7) ---
class ErrorDeDatos(Exception):
"""El fichero de entrada no cumple el contrato esperado."""
# --- Dataclass para empaquetar el resultado del análisis (sección 3.9) ---
@dataclass
class InformeCartera:
fecha: str # fecha de generación
n_clientes: int # tamaño de la cartera
ingreso_mensual: float # suma de gasto mensual (MRR)
gasto_medio: float # ticket medio
por_sector: dict[str, float] # ingresos por sector (para el dashboard)
por_plan: dict[str, int] # nº de clientes por plan
en_riesgo: list[str] # nombres de clientes premium en riesgo de fuga
def cargar(ruta: str) -> pd.DataFrame:
"""Carga y valida el CSV de clientes. Falla claro si algo no cuadra."""
try:
df = pd.read_csv(ruta) # lectura (sección 5.2)
except FileNotFoundError:
raise ErrorDeDatos(f"No se encuentra el fichero: {ruta}") from None
except pd.errors.ParserError as e:
raise ErrorDeDatos(f"El CSV está malformado: {e}") from None
faltan = COLUMNAS_OBLIGATORIAS - set(df.columns) # sets (sección 3.4)
if faltan:
raise ErrorDeDatos(f"Faltan columnas: {faltan}")
if df.empty:
raise ErrorDeDatos("El fichero no contiene filas")
return df
def limpiar(df: pd.DataFrame) -> pd.DataFrame:
"""Normaliza texto, imputa nulos y añade columnas derivadas."""
df = df.copy() # función pura: no muta la entrada
df["sector"] = df["sector"].str.lower().str.strip() # unificar categorías
n_nulos = int(df["gasto_mensual"].isna().sum()) # contar nulos (sección 5.4)
if n_nulos: # imputación documentada
mediana = df["gasto_mensual"].median()
df["gasto_mensual"] = df["gasto_mensual"].fillna(mediana)
print(f"[limpieza] {n_nulos} gastos nulos imputados con la mediana ({mediana:.2f} €)")
df["gasto_anual"] = df["gasto_mensual"] * 12 # columna vectorizada (sección 5.3)
return df
def analizar(df: pd.DataFrame) -> InformeCartera:
"""Calcula todas las métricas del informe a partir del DataFrame limpio."""
# Segmentación por cuartiles con NumPy (secciones 4.3 y 4.6)
q75 = df["gasto_mensual"].quantile(0.75)
premium = df[df["gasto_mensual"] >= q75] # máscara booleana
# Clientes premium con demasiados tickets: riesgo de fuga (filtro combinado)
riesgo = premium[premium["tickets_soporte"] > UMBRAL_TICKETS_RIESGO]
return InformeCartera(
fecha=date.today().isoformat(), # '2026-07-03'
n_clientes=len(df),
ingreso_mensual=round(float(df["gasto_mensual"].sum()), 2),
gasto_medio=round(float(df["gasto_mensual"].mean()), 2),
por_sector=( # groupby (sección 5.5)
df.groupby("sector")["gasto_mensual"].sum()
.sort_values(ascending=False).round(2).to_dict() # → dict para JSON
),
por_plan=df["plan"].value_counts().to_dict(),
en_riesgo=riesgo["nombre"].tolist(), # Series → lista
)
def redactar_texto(inf: InformeCartera) -> str:
"""Convierte las métricas en un informe legible (f-strings, sección 3.1)."""
lineas = [
f"INFORME DE CARTERA — {inf.fecha}",
"=" * 40,
f"Clientes activos ....... {inf.n_clientes}",
f"Ingreso mensual (MRR) .. {inf.ingreso_mensual:,.2f} €",
f"Gasto medio ............ {inf.gasto_medio:,.2f} €",
"",
"Ingresos por sector:",
]
for sector, total in inf.por_sector.items(): # recorrer dict (sección 3.4)
pct = total / inf.ingreso_mensual # peso de cada sector
lineas.append(f" - {sector:<12} {total:>12,.2f} € ({pct:.1%})")
lineas.append("")
lineas.append(f"⚠ Clientes premium en riesgo de fuga: {len(inf.en_riesgo)}")
for nombre in inf.en_riesgo:
lineas.append(f" - {nombre}")
return "\n".join(lineas) # unir lista de líneas en un texto
def main() -> None:
"""Orquesta el pipeline completo: cargar → limpiar → analizar → publicar."""
# sys.argv es la lista de argumentos: [script, arg1, ...]
ruta = sys.argv[1] if len(sys.argv) > 1 else "clientes.csv"
try:
df = cargar(ruta) # 1) cargar y validar
df = limpiar(df) # 2) limpiar
informe = analizar(df) # 3) calcular métricas
except ErrorDeDatos as e:
# Error controlado: mensaje claro y salida con código de error (para automatizar)
print(f"ERROR: {e}")
sys.exit(1)
# 4a) Informe de texto para el email semanal (sección 3.8)
texto = redactar_texto(informe)
with open("informe_semanal.txt", "w", encoding="utf-8") as f:
f.write(texto)
# 4b) Métricas en JSON para el dashboard (dataclass → dict → JSON)
with open("metricas.json", "w", encoding="utf-8") as f:
json.dump(informe.__dict__, f, ensure_ascii=False, indent=2)
# __dict__ convierte la dataclass en un dict serializable
print(texto) # y también por pantalla
print("\n✅ Generados: informe_semanal.txt y metricas.json")
if __name__ == "__main__": # sección 3.10
main()
Salida típica al ejecutar python informe_clientes.py clientes.csv:
[limpieza] 17 gastos nulos imputados con la mediana (2494.09 €)
INFORME DE CARTERA — 2026-07-03
========================================
Clientes activos ....... 200
Ingreso mensual (MRR) .. 505,347.13 €
Gasto medio ............ 2,526.74 €
Ingresos por sector:
- software 112,406.19 € (22.2%)
- retail 108,933.90 € (21.6%)
- finanzas 104,806.29 € (20.7%)
- salud 92,896.71 € (18.4%)
- logística 86,304.04 € (17.1%)
⚠ Clientes premium en riesgo de fuga: 6
- Cliente_034
- Cliente_112
...
✅ Generados: informe_semanal.txt y metricas.json
Consejo profesional
relee el script y fíjate en la arquitectura, no solo en las líneas: funciones pequeñas con una sola responsabilidad, tipadas, que no mutan sus entradas; validación temprana con excepciones propias; constantes de negocio arriba y con nombre; y un main() que solo orquesta. Esa forma de estructurar es lo que te pedirán en cualquier equipo — y es exactamente la que usaremos al construir sistemas RAG y agentes en módulos posteriores.
Ejercicio rápido 5 (integrador): amplía informe_clientes.py con una función top_clientes(df, n=5) que devuelva los n clientes de mayor gasto anual con su sector, y añade su salida al informe de texto.
Ver solución
def top_clientes(df: pd.DataFrame, n: int = 5) -> pd.DataFrame:
"""Los n clientes con más gasto anual."""
return (
df.sort_values("gasto_anual", ascending=False) # ordenar de mayor a menor
.head(n) # quedarnos con los n primeros
[["nombre", "sector", "gasto_anual"]] # solo las columnas del informe
)
# Y en redactar_texto() (pasándole el df o el resultado), añadir:
# lineas.append("Top clientes por gasto anual:")
# for _, fila in top.iterrows(): # iterar filas (solo para informes)
# lineas.append(f" - {fila['nombre']} ({fila['sector']}): {fila['gasto_anual']:,.2f} €")
7. Buenas prácticas¶
- Sigue PEP 8, la guía de estilo oficial:
snake_casepara variables y funciones (gasto_mensual,calcular_coste),PascalCasepara clases (InformeCartera),MAYÚSCULASpara constantes (UMBRAL_TICKETS_RIESGO), 4 espacios de indentación, líneas de ≤ 88-100 caracteres. No lo memorices: instala Ruff en VS Code y deja que formatee y avise solo. - Nombres que cuentan la verdad.
df_clientes_limpiovale más quedf2.calcular_coste_por_tokenvale más quecalc. El código se lee 10 veces más de lo que se escribe: optimiza para el lector. - Type hints siempre en firmas de funciones y dataclasses. En 2026 no son opcionales en código profesional: alimentan el autocompletado, la detección temprana de errores (Pylance/mypy) y sirven de documentación viva.
- Docstrings en cada función pública: una línea que diga qué hace, y si hace falta, qué recibe y devuelve.
- Funciones pequeñas y puras: una responsabilidad, entradas → salida, sin efectos colaterales sorpresa (¿viste el
df.copy()del ejemplo integrador?). - Un proyecto = un entorno virtual con sus dependencias declaradas (
pyproject.tomlorequirements.txt). Sin excepciones, desde el primer día. - No reinventes la rueda. ¿Necesitas parsear fechas, reintentos HTTP, barras de progreso, validación de JSON? Ya existe (
datetime,tenacity,tqdm,pydantic). Buscar "python library for X" antes de escribirlo tú es un acto de madurez, no de pereza. La biblioteca estándar primero, PyPI después, tu código el último. - Falla pronto y claro: valida entradas al principio, lanza excepciones con mensajes que digan qué esperabas y qué llegó.
- Secretos fuera del código: claves de API en
.env/variables de entorno, y.enven el.gitignore. - Vectoriza: en NumPy/Pandas, si escribes un
forsobre filas o números, párate y busca la operación de columna/array equivalente.
8. Malas prácticas que debes evitar¶
- Instalar todo global sin entornos virtuales (el origen del 50 % de los "en mi máquina funcionaba").
-
except:desnudo oexcept Exception: pass: silenciar errores es fabricar bugs invisibles. - Variables de una letra fuera de contadores triviales (
x,df2,data_final_v3_DEFINITIVO). -
from modulo import *: contamina el espacio de nombres y hace imposible saber de dónde viene cada nombre. - Argumentos por defecto mutables (
def f(x, lista=[])): la trampa de la sección 3.6. - Copiar-pegar bloques de código en vez de extraer una función: si lo escribes dos veces, funcion-ízalo.
- Iterar un DataFrame con
for+iterrows()para calcular: casi siempre existe la versión vectorizada 100× más rápida (iterrows solo para formatear informes pequeños). - Hardcodear rutas absolutas (
C:\Users\yo\Desktop\datos.csv) y claves de API en el código. - Scripts de 500 líneas sin funciones: todo en el nivel superior del módulo, imposible de testear o reutilizar.
- Ignorar los warnings:
SettingWithCopyWarning,DeprecationWarning... son avisos de bugs futuros, no ruido.
9. Errores comunes: tabla de diagnóstico¶
| Error (lo que ves) | Causa típica | Solución |
|---|---|---|
"python" no se reconoce... / command not found |
Python no está en el PATH | Reinstalar marcando "Add to PATH"; probar py (Windows) o python3 (macOS/Linux) |
IndentationError: expected an indented block |
Falta indentar tras :, o mezclas tabuladores y espacios |
Indenta con 4 espacios; configura VS Code (tab = 4 espacios) |
SyntaxError: invalid syntax señalando un = en un if |
Usaste = (asignar) en vez de == (comparar) |
if x == 5: |
NameError: name 'x' is not defined |
Variable usada antes de crearla, o error de tecleo (mayúsculas cuentan) | Define antes de usar; revisa el nombre exacto |
TypeError: can only concatenate str (not "int") to str |
Mezclar texto y número con + |
Usa f-string: f"total: {n}" o convierte con str(n) |
ValueError: invalid literal for int() with base 10: '3.5' |
int() sobre un texto con decimales |
int(float("3.5")) |
IndexError: list index out of range |
Pedir una posición que no existe | Recuerda: índices de 0 a len(lista)-1; comprueba len() |
KeyError: 'campo' |
La clave no existe en el dict (típico con JSON de APIs) | Usa .get("campo", valor_por_defecto) |
ModuleNotFoundError: No module named 'pandas' |
Paquete no instalado en el entorno activo (o entorno equivocado en VS Code) | Activa el .venv e instala; en VS Code selecciona el intérprete del proyecto (Ctrl+Shift+P → "Python: Select Interpreter") |
AttributeError: module 'random' has no attribute... |
Tu fichero se llama igual que un módulo (random.py, json.py) |
Renombra tu fichero y borra el __pycache__ |
| Resultados "fantasma" entre llamadas a una función | Argumento por defecto mutable (def f(lista=[])) |
Usa None como centinela y crea la lista dentro |
x == None funciona raro / linter protesta |
Confundir == (igualdad de valor) con is (identidad de objeto) |
Con None, siempre is: if x is None: — == puede ser redefinido por objetos |
UnicodeDecodeError o tildes rotas (é) |
Fichero abierto sin especificar encoding | Añade encoding="utf-8" a todos los open() |
ValueError: The truth value of a Series is ambiguous |
Usar and/or en filtros de Pandas |
Usa & / \| con cada condición entre paréntesis |
RuntimeWarning: coroutine ... was never awaited |
Llamar a una función async sin await |
await funcion() dentro de async def, o asyncio.run() |
SettingWithCopyWarning (Pandas) |
Modificar el resultado de un filtro (posible vista) | Asigna con df.loc[condición, "col"] = valor |
| El programa no termina nunca | Bucle while cuya condición nunca cambia |
Ctrl+C para matarlo; asegúrate de actualizar la variable de control |
10. Preguntas frecuentes (FAQ)¶
1. ¿Necesito saber matemáticas avanzadas para programar en Python? No para programar. Para IA necesitas lo que viste en el Capítulo 3 (álgebra lineal básica, probabilidad, derivadas conceptuales), y NumPy hace el trabajo pesado. Empieza a programar ya: las matemáticas se consolidan usándolas.
2. ¿Python 3.12 o 3.13? ¿Y si un tutorial usa 3.8?
Para proyectos nuevos en 2026, 3.12 o 3.13 indistintamente (todo el stack de IA los soporta). Los tutoriales de 3.8-3.10 siguen siendo válidos al 95 %: la sintaxis básica no ha cambiado. Lo que sí debes ignorar de tutoriales viejos: os.path en favor de pathlib, % formatting en favor de f-strings, y setup.py en favor de pyproject.toml.
3. ¿uv reemplaza a pip? ¿Cuál aprendo?
Aprende ambos: pip + venv es lo que verás en la documentación y en proyectos existentes; uv es lo que usarás en tus proyectos por velocidad y comodidad. Conceptualmente hacen lo mismo (instalar paquetes en entornos aislados), así que saber uno es saber el 80 % del otro.
4. ¿Cuánto tiempo se tarda en "saber Python"? Con 1-2 horas diarias: en 2-3 semanas escribes scripts útiles (este capítulo), en 2-3 meses te mueves con soltura en NumPy/Pandas, en 6 meses escribes código profesional. La clave no es el tiempo sino teclear: leer código no enseña a programar, como ver cocina no enseña a cocinar.
5. ¿Debo memorizar todas las funciones de Pandas y NumPy? No. Los profesionales memorizan los ~20 patrones frecuentes (los de este capítulo) y buscan el resto en la documentación o se lo preguntan a un LLM. Lo que SÍ debes interiorizar son los conceptos: vectorización, máscaras booleanas, dividir-aplicar-combinar, shapes.
6. ¿Puedo usar ChatGPT/Claude para que me escriba el código mientras aprendo? Úsalos como tutor, no como sustituto: pídeles que te expliquen errores y conceptos, genial. Pedirles el ejercicio resuelto y pegarlo sin entenderlo te fabrica una ilusión de competencia que se derrumba en cuanto algo falla (y en IA, algo siempre falla). Regla práctica: no pegues nunca código que no puedas explicar línea a línea.
7. ¿Cuándo usar una lista, un dict, una tupla o un set? Lista: colección ordenada que crece/cambia (mensajes, ejemplos). Dict: pares clave→valor (JSON, configuración, contadores). Tupla: grupo fijo e inmutable (coordenadas, shape, retornos múltiples). Set: unicidad y pertenencia rápida (vocabularios, deduplicación). Si dudas entre lista y dict, pregúntate: ¿accedo por posición o por nombre?
8. ¿Por qué mi VS Code no encuentra pandas si lo acabo de instalar?
Casi seguro instalaste en un entorno y VS Code usa otro. Mira abajo a la derecha qué intérprete está seleccionado y cámbialo (Ctrl+Shift+P → "Python: Select Interpreter" → elige el .venv del proyecto). Es el problema nº 1 de los principiantes con VS Code.
9. ¿Cuándo necesito clases y cuándo bastan funciones?
Empieza con funciones. Pasa a clases cuando tengas datos y comportamiento que viajan juntos (un Documento con su texto y sus métodos de búsqueda), o estado que persiste entre llamadas (una conexión, un historial). Para puros contenedores de datos: @dataclass. Un buen script de análisis puede ser 100 % funciones y estar perfecto.
10. ¿Debo aprender async ya?
Solo la intuición de la sección 3.11: qué problema resuelve (aprovechar esperas de red) y qué pinta tiene (async/await/gather). Escribirás async en serio en el módulo 10-BACKEND, cuando llames a APIs de LLM concurrentemente. No dejes que te bloquee ahora.
11. ¿Pandas o Polars? Pandas sigue siendo el estándar que encontrarás en el 90 % de empresas, tutoriales y código existente: apréndelo primero. Polars (más rápido, API más consistente) crece rápido y merece una mirada cuando domines Pandas: los conceptos (DataFrame, filtros, groupby) se transfieren casi directos.
12. He visto np.float64(3.14) en mis prints en vez de 3.14. ¿Está roto algo?
No. NumPy/Pandas devuelven sus propios tipos numéricos (np.float64, np.int64), que se comportan como los de Python. Si necesitas el tipo puro de Python (p. ej., para serializar a JSON), convierte con float(x) o int(x) — fíjate en que el ejemplo integrador lo hace antes del json.dump.
11. Resumen del capítulo¶
- Python domina la IA por legibilidad + ecosistema (NumPy → Pandas → scikit-learn → PyTorch → SDKs de LLM), no por velocidad bruta: el cálculo pesado corre en C por debajo. En esta academia: Python para IA/datos/backend, TypeScript (más adelante) para frontend y algunos agentes.
- Entorno profesional: Python 3.12/3.13 en el PATH, VS Code con Pylance y Ruff, y la regla de oro: un proyecto = un entorno virtual (
venvclásico,uvmoderno) con dependencias declaradas enpyproject.toml/requirements.txt. - El lenguaje: variables y tipos con f-strings;
if/elif/elsey bucles (range,enumerate,zip); las cuatro estructuras (lista, tupla, dict —el gemelo del JSON de las APIs—, set) y comprehensions; funciones con type hints,*args/**kwargsy la trampa del default mutable;try/exceptcon excepciones propias (vital para APIs de LLM); ficheros de texto, JSON y CSV conwith open(..., encoding="utf-8"); clases, herencia y@dataclass; módulos,if __name__ == "__main__"y estructurasrc/+tests/+pyproject.toml; y la intuición de asyncio para llamadas concurrentes. - NumPy: arrays con
shape/dtype, slicing e indexación booleana, broadcasting, y vectorización 30-200× más rápida que los bucles.argmax,mean,dot: el vocabulario de la clasificación y los embeddings. - Pandas:
read_csv+ el ritualhead/info/describe/shape, filtros booleanos con&/|, tratamiento de nulos (isna/dropna/fillna),groupby(dividir-aplicar-combinar) ymergepara cruzar tablas. - El ejemplo integrador demostró el patrón profesional completo: cargar → validar → limpiar → analizar → publicar (texto + JSON), con funciones puras y tipadas, excepciones propias y constantes de negocio.
Con esto ya sabes programar. En el próximo capítulo veremos cómo se representan los datos con los que la IA trabaja de verdad: números, texto, imágenes y los famosos embeddings.
12. Bibliografía y recursos¶
- Documentación oficial de Python en español — https://docs.python.org/es/3/ · El tutorial oficial (secciones 3-9) es el complemento perfecto de este capítulo.
- Documentación de NumPy — https://numpy.org/doc/stable/ · Empieza por "NumPy: the absolute basics for beginners".
- Documentación de Pandas — https://pandas.pydata.org/docs/ · La guía "10 minutes to pandas" y las "User Guide" de groupby y merge.
- Automate the Boring Stuff with Python (Al Sweigart) — https://automatetheboringstuff.com/ · Gratuito online; el mejor libro de Python práctico para principiantes absolutos.
- PEP 8 — Guía de estilo — https://peps.python.org/pep-0008/ · La convención que todo el ecosistema sigue.
- uv (Astral) — https://docs.astral.sh/uv/ · Documentación del gestor de paquetes moderno.
- Real Python — https://realpython.com/ · Tutoriales de calidad sobre cada tema del capítulo (en inglés).
- Python Tutor — https://pythontutor.com/ · Visualiza la ejecución de tu código paso a paso: oro para entender variables, referencias y bucles.
| Anterior | Módulo | Siguiente |
|---|---|---|
| Capítulo 3: Matemáticas para IA | 01-FUNDAMENTOS | Capítulo 5: Datos y representación |