El Python que necesitas si construyes agentes en TypeScript
Tu producto está en TypeScript. Tu agente también.
Y aun así, en algún momento vas a acabar escribiendo Python. No porque quieras cambiar de lenguaje, sino porque entre tus datos en crudo y el contexto que lee tu agente hace falta una capa que transforme lo uno en lo otro, y esa capa se escribe casi siempre en Python.
No es un curso de Data Science. No hace falta álgebra lineal, ni estadística inferencial, ni un notebook con gráficos bonitos. Hacen falta cuatro operaciones, y con ellas se resuelve prácticamente todo lo que un agente necesita que le den masticado.
Empiezo por el número que justifica el post entero.
339.276 tokens contra 96
Tengo una exportación de ventas: 40.000 filas, cinco columnas. La pregunta que quiero que responda el agente es sencilla: ¿qué curso conviene empujar el mes que viene?
La forma perezosa es meterle el CSV entero en el contexto. Vamos a medir qué significa eso:
import pandas as pd
df = pd.read_csv("ventas.csv") # 40.000 filas
crudo = df.to_csv(index=False)
resumen = (df.groupby("curso")
.agg(ventas=("precio", "size"),
ingresos=("precio", "sum"),
tasa_completado=("completado", "mean"))
.round(2)
.reset_index()
.sort_values("ingresos", ascending=False))
compacto = resumen.to_json(orient="records")
print(f"crudo : {len(crudo):,} caracteres")
print(f"resumen : {len(compacto):,} caracteres")
print(f"factor : {len(crudo)/len(compacto):,.0f}x")
Resultado:
crudo : 1.357.104 caracteres
resumen : 386 caracteres
factor : 3.516x
A razón de unos cuatro caracteres por token, eso es pasar de ~339.000 tokens a ~96. Y esto es lo que ve el agente después de la reducción:
curso ventas ingresos tasa_completado
IA 8792 439512.08 0.27
Angular 12349 370346.51 0.48
TypeScript 9522 190344.78 0.39
Testing 5621 168573.79 0.55
Node 3716 74282.84 0.34
Cinco filas. Y ahí dentro ya está la respuesta, que además no es la obvia: IA es lo que más factura pero tiene la peor tasa de finalización (0,27), y Testing es lo que menos factura con la mejor con diferencia (0,55). Ese contraste no se ve en 40.000 filas ni lo va a encontrar un modelo leyéndolas.
Esto no va de ahorrar dinero, aunque también. Va de que un modelo con 40.000 filas delante tiene 40.000 oportunidades de fijarse en lo que no toca. Reducir no es una optimización: es parte de la respuesta. Es exactamente el argumento de context engineering para estructurar la memoria de tus agentes, aplicado a la capa de datos. Y si quieres ver a dónde se te va la factura, lo desglosé en medir el consumo de tokens de un agente.
Las cuatro operaciones
Todo lo que necesitas hacer con Python en este contexto cae en una de estas cuatro:
1. CARGAR → CSV, SQL, JSON, Parquet
2. LIMPIAR → tipos, nulos, duplicados
3. REDUCIR → agrupar, agregar, ordenar
4. VALIDAR → esquema antes de entregar
─────────────────────────────────────────
la 3 es la que decide si el agente acierta
No hay una quinta. Si te encuentras entrenando un modelo, te has ido del carril.
1 y 2. Cargar y limpiar
Pandas carga desde casi cualquier sitio con una línea, y el 90% del trabajo de limpieza son tres cosas:
import pandas as pd
df = pd.read_csv("ventas.csv", parse_dates=["fecha"])
df = df.drop_duplicates(subset=["id_pedido"]) # duplicados por clave
df = df.dropna(subset=["curso", "precio"]) # filas sin lo esencial
df["precio"] = pd.to_numeric(df["precio"], errors="coerce")
Ese errors="coerce" es el detalle que más disgustos evita: convierte a NaN lo que no se pueda parsear en vez de reventar. Un CSV real siempre trae una celda con "29,99 €" donde esperabas un número.
Dos cosas que muerden el primer día:
El filtrado es una máscara booleana. df["completado"] devuelve una serie de True/False, y df[mascara] se queda con las filas donde es True. Con condiciones compuestas usa & y | con paréntesis, nunca and y or.
Casi todo devuelve un objeto nuevo. Si haces df.drop(columns=["x"]) y no reasignas, no ha pasado nada.
3. Reducir, que es donde está el trabajo de verdad
groupby más agg resuelve la inmensa mayoría de las preguntas que le vas a hacer a un agente sobre tus datos:
resumen = (df.groupby("curso")
.agg(ventas=("precio", "size"),
ingresos=("precio", "sum"),
tasa_completado=("completado", "mean"))
.round(2)
.reset_index())
Tres detalles que importan:
.agg()con nombres te deja bautizar las columnas de salida. Sin eso acabas con nombres compuestos horribles y el agente los lee peor.mean()sobre una columna booleana da la proporción directamente. Ahí sale el0,27de la tabla de arriba sin cálculo extra..reset_index()baja la clave del agrupado del índice a columna. Si se te olvida, el JSON que entregas sale con otra forma.
La regla, si te quedas con una sola de este post: agrega hasta que la tabla quepa en una pantalla. Si no cabe, todavía no has terminado de reducir.
Cuando el volumen crezca o prefieras SQL a encadenar métodos, tienes la alternativa sin salir del portátil en DuckDB para analizar con SQL sin exportar nada.
4. Validar en la frontera: Pydantic es el Zod de Python
Aquí es donde tu instinto de TypeScript te sirve tal cual. Lo que hace Zod en tu producto lo hace Pydantic en la capa de datos: defines el esquema y lo que no encaja no pasa.
from pydantic import BaseModel, Field
class ResumenCurso(BaseModel):
curso: str
ventas: int = Field(ge=0)
ingresos: float = Field(ge=0)
tasa_completado: float = Field(ge=0, le=1)
filas = [ResumenCurso(**r) for r in resumen.to_dict(orient="records")]
payload = [f.model_dump() for f in filas]
Ese le=1 en tasa_completado parece una tontería y es justo el guardarraíl que quieres: si un cambio en el pipeline te deja una proporción en 1,4, prefieres que explote aquí y no que el agente construya un razonamiento entero sobre un dato imposible.
Y esa es la diferencia de fondo con el análisis de datos clásico: un dato sucio ya no es una celda rara en un gráfico, es una alucinación con aspecto de respuesta correcta. El agente no va a dudar de lo que le des.
Los patrones de contrato del lado TypeScript están en el curso de Zod para TypeScript, y se trasladan casi literalmente a Pydantic.
¿Y NumPy? Solo cuando toca
NumPy aparece en todos los tutoriales de Python y datos, así que conviene decir cuándo lo vas a necesitar de verdad: cuando hagas aritmética sobre muchos números.
Una lista de Python guarda punteros a objetos dispersos y obliga al intérprete a resolver el tipo en cada elemento. NumPy guarda los números en un bloque contiguo y opera en C sobre todo el bloque. Sobre un millón de valores:
import numpy as np, timeit
lista = [float(i) for i in range(1_000_000)]
arr = np.array(lista)
t_list = timeit.timeit(lambda: [x * 0.85 for x in lista], number=5) / 5
t_np = timeit.timeit(lambda: arr * 0.85, number=5) / 5
print(f"lista: {t_list*1000:.1f} ms | numpy: {t_np*1000:.1f} ms | {t_list/t_np:.0f}x")
En mi máquina: 102,3 ms contra 6,4 ms. Dieciséis veces. Córrelo tú, que el factor depende del hardware.
Ahora, la parte honesta: si tu pipeline agrupa 40.000 filas una vez al día, esa diferencia no la vas a notar. Pandas ya usa NumPy por debajo. Aprende NumPy cuando el perfilado te diga que ahí está el problema, no antes.
Cuándo NO deberías meter Python
Añadir un lenguaje a un proyecto tiene un coste real: otro entorno, otro despliegue, otra cosa que se rompe.
No lo metas si:
- Son menos de unos miles de registros y ya los tienes en tu app. Un
reduceen TypeScript te lo resuelve sin añadir nada. - Los datos ya están en tu base de datos. Un
GROUP BYen SQL es más rápido y más simple que exportar, cargar en Pandas y volver. - Es una consulta que harás una vez. Escríbela donde te resulte más rápido y olvídala.
Merece la pena cuando cruzas fuentes distintas (un CSV de la pasarela de pago con un export de tu base y una hoja de cálculo), cuando la limpieza tiene reglas de verdad, o cuando el paso se repite cada día. Ahí Pandas gana con claridad. Para automatizar ese paso una vez que funcione, tengo el terreno cubierto en scripts de Python para tu productividad semanal.
El pipeline entero
Junto, esto es todo lo que hace falta entre tu exportación y el contexto de tu agente:
import json
import pandas as pd
from pydantic import BaseModel, Field
class ResumenCurso(BaseModel):
curso: str
ventas: int = Field(ge=0)
ingresos: float = Field(ge=0)
tasa_completado: float = Field(ge=0, le=1)
def contexto_para_agente(ruta: str) -> str:
df = (pd.read_csv(ruta, parse_dates=["fecha"])
.drop_duplicates(subset=["id_pedido"])
.dropna(subset=["curso", "precio"]))
resumen = (df.groupby("curso")
.agg(ventas=("precio", "size"),
ingresos=("precio", "sum"),
tasa_completado=("completado", "mean"))
.round(2)
.reset_index()
.sort_values("ingresos", ascending=False))
filas = [ResumenCurso(**r) for r in resumen.to_dict(orient="records")]
return json.dumps([f.model_dump() for f in filas], ensure_ascii=False)
Treinta líneas. La salida cabe en un mensaje y ya viene validada.
Cómo se conecta esa salida con un agente que decide y actúa sobre ella, de la idea a producción, lo enseño paso a paso en el curso Construye con IA: de la idea al producto con Claude Code.
Qué hacer esta semana
- Monta el entorno con
uv:uv venvyuv pip install pandas pydantic. Un segundo. (Aviso por si te lo cruzas en algún tutorial: Bun no gestiona Python, es un runtime de JavaScript; el equivalente aquí esuv.) - Coge la exportación más grande que le estés pasando a un agente y mide sus caracteres. Divide entre cuatro para hacerte una idea de los tokens.
- Escribe el
groupbyque responde la pregunta que de verdad le haces, y vuelve a medir. El factor de reducción que te salga es lo que te estabas gastando de más. - Ponle un esquema Pydantic a la salida antes de entregársela al modelo.
En Dominicode Labs comparto los pipelines de datos y telemetría que uso de verdad para mirar lanzamientos y retención.
Tu agente no necesita tus datos. Necesita la respuesta que hay dentro de ellos, y esa parte todavía la pones tú.
Preguntas frecuentes
¿Por qué no le paso el CSV entero al agente y que se apañe?
Por dos motivos. El coste, que es el menor: en el ejemplo de este post, 40.000 filas son unos 1,35 millones de caracteres —del orden de 339.000 tokens— frente a los 386 caracteres del resumen. Y el importante: un modelo con 40.000 filas delante tiene 40.000 oportunidades de fijarse en lo que no toca. Reducir no es solo ahorrar, es acotar dónde puede mirar.
¿Necesito saber estadística para esto?
No. Las cuatro operaciones son cargar, limpiar, reducir y validar, y todas son programación. La estadística hace falta cuando entras en modelado predictivo, que es un problema distinto y que en la mayoría de los productos con agentes no aparece nunca.
¿Pandas o Polars?
Empieza por Pandas: más documentación, más respuestas cuando te atasques y es lo que vas a encontrar en el código de otros. Polars es más rápido y su API más consistente, y compensa cuando el volumen te empiece a doler. Los conceptos —DataFrame, filtrado, agrupación— se trasladan casi enteros.
¿Puedo hacer esto en TypeScript y ahorrarme el Python?
Para volúmenes pequeños, sí, y probablemente deberías: un reduce no justifica añadir un lenguaje al proyecto. Python empieza a compensar cuando cruzas fuentes distintas, cuando la limpieza tiene reglas de verdad o cuando el paso se repite a diario. Si los datos ya viven en tu base de datos, la respuesta suele ser ninguno de los dos: un GROUP BY en SQL.
¿Dónde pongo esta capa: en el agente o antes?
Antes, siempre, y como un paso determinista. Si el agente tiene que cargar y agregar por su cuenta, estás usando un modelo probabilístico para hacer aritmética que un groupby resuelve exacto, más barato y sin variar entre ejecuciones. El agente debe recibir la tabla ya reducida y validada, y dedicarse a lo suyo: decidir.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
