IA-Ingenieria-MLOps · bucle-de-optimizacion-por-benchmark

Benchmark Optimization Loop

Optimización de enrich_batch_job.py — CultivaLeads SaaS · 50 000 leads/noche

Métrica objetivo wall time (s)
Baseline 843 s
Target < 180 s (4-5x)
Presupuesto variantes 8 máx
Correctness gate score delta ≤ 0.5%
Fecha 2026-06-18
Tiempo Ganador
127 s
vs 843 s baseline
Speedup alcanzado
6.6x
target era 4-5x ✓
Variantes probadas
7 / 8
1 dentro del ruido
Coste incremental
3.14 USD
presupuesto: 10 USD ✓

1 Baseline medido

# Comando de medición time python enrich_batch_job.py --dry-run=false --leads=50000 2>&1 | tee baseline.log # Resultados real 14m 03.4s → 843.4 s user 13m 42.1s sys 0m 21.3s # Score checksum (SHA-256 de scores.csv) sha256: a3f9c12e04bb78d9e5ff2a801c6dd3e5... max_delta vs referencia: 0.00% [PASS]

2 Cuellos de botella por evidencia (profiling)

Componente Tiempo (s) % del total Distribución Hipótesis
clearbit_api_call (×50k) 741.2 87.9 %
Llamadas síncronas 1×1 — paralelizar con asyncio/ThreadPool
postgres INSERT (×50k) 61.8 7.3 %
INSERT fila×fila — usar executemany / COPY
sklearn predict (×50k) 22.4 2.7 %
Modelo recargado en cada call — cargar 1 vez + predict_proba batch
postgres SELECT leads 18.0 2.1 %
Sin índice en updated_at — añadir índice parcial

3–6 Variantes — hipótesis, resultados y promoción

# Variante Hipótesis Comando clave Tiempo (s) Speedup Vis. Correcto Estado Notas
0 baseline Path actual python enrich_batch_job.py 843 1.0x
✓ PASS BASELINE Referencia estable
1 thread-10 ThreadPoolExecutor 10 workers para Clearbit --workers 10 298 2.8x
✓ PASS PROMOVIDA Buen punto de partida
2 thread-50 50 workers — saturar Clearbit API --workers 50
✗ FAIL RECHAZADA Rate limit 429 Clearbit — job aborta a 12k leads
3 thread-25 25 workers — buscar límite superior seguro --workers 25 189 4.5x
✓ PASS PROMOVIDA Supera target. No rate limits
4 batch-insert executemany 1000 filas vs INSERT×1 --insert-batch 1000 168 5.0x
✓ PASS PROMOVIDA +21 s vs variante-3 por INSERT optimizado
5 sklearn-batch predict_proba sobre array completo, modelo cargado 1× --sklearn-batch 147 5.7x
✓ PASS PROMOVIDA Delta 0.18% vs ref — dentro del gate ≤0.5%
6 thread25-batch-idx Combinar v3+v4+v5 + índice partial en updated_at --workers 25 --insert-batch 1000 --sklearn-batch 127 6.6x ★
✓ PASS ★ GANADOR Max speedup, delta 0.21%. Promovida a main.
7 asyncio-aiohttp Reescribir en asyncio nativo — ¿más rápido que threads? --async-mode 124 6.8x
✓ PASS ~RUIDO Solo 3 s mejor (2.4%) — mejora dentro del ruido. No justifica refactor.

7 Variante ganadora — código codificado

# enrich_batch_job.py — configuración ganadora (thread25-batch-idx) # Commit: feat(etl): 6.6x speedup via thread-pool + batch-insert + sklearn-batch from concurrent.futures import ThreadPoolExecutor import joblib, psycopg2.extras # 1. Cargar modelo UNA sola vez al inicio MODEL = joblib.load("scoring_model.pkl") def enrich_all(leads: list, workers: int = 25) -> list: # 2. Paralelizar llamadas Clearbit — 25 workers (límite seguro de rate) with ThreadPoolExecutor(max_workers=workers) as pool: enriched = list(pool.map(clearbit_enrich, leads)) # 3. Score batch sobre array completo (sin re-cargar modelo) features = build_feature_matrix(enriched) # vectorizado scores = MODEL.predict_proba(features)[:, 1] # batch predict return [{**lead, "score": float(s)} for lead, s in zip(enriched, scores)] def write_results(conn, results: list, batch_size: int = 1000): # 4. INSERT en batches de 1000 con executemany with conn.cursor() as cur: for i in range(0, len(results), batch_size): psycopg2.extras.execute_values( cur, "INSERT INTO enriched_leads (id,score,...) VALUES %s ON CONFLICT (id) DO UPDATE SET score=EXCLUDED.score", [(r["id"], r["score"], ...) for r in results[i:i+batch_size]] ) conn.commit() # 5. Índice parcial en Postgres (migración aplicada) # CREATE INDEX CONCURRENTLY idx_leads_updated ON leads(updated_at) WHERE processed = false;

8 Ledger de runs + confirmación del delta

Run ledger — enrich_batch_job optimization 50 000 leads · misma forma de entrada en todos los runs
2026-06-18 09:14:02 baseline (×2 confirmación) 840 s
2026-06-18 09:31:12 thread-10 298 s -64.6%
2026-06-18 09:45:08 thread-50 abort rate-limit
2026-06-18 10:01:45 thread-25 189 s -36.6% vs thread-10
2026-06-18 10:18:22 batch-insert 168 s -11.1% vs thread-25
2026-06-18 10:33:01 sklearn-batch 147 s -12.5% vs batch-insert
2026-06-18 10:50:18 thread25-batch-idx ★ 127 s -13.6% vs sklearn-batch · -84.9% vs baseline ★ WINNER
2026-06-18 11:06:55 asyncio-aiohttp 124 s -2.4% vs winner → dentro del ruido (σ=±4 s) ~RUIDO
2026-06-18 11:24:09 winner — reconfirmación final 129 s delta confirmado: 6.5x (±0.15x ruido) · score delta 0.21% ≤ 0.5% ✓ ✓ CONF.

Puerta de promoción

Correctness tests
pytest suite completa verde. Score delta 0.21% ≤ 0.5%.
Delta repetido
2 runs independientes: 127 s y 129 s. Ruido ±4 s. Delta estable.
Rollback obvio
Feature flags: --workers 1 --insert-batch 1 restaura comportamiento original.
En source control
PR #247 mergeado en main. Commit: a3f9c1.
Comandos exactos documentados
Runbook en docs/etl-optimization.md.
Coste dentro del presupuesto
3.14 USD gastados de 10 USD presupuestados.

Resumen ejecutivo — Mejor variante segura medida

Nota: "6.6x" es el mejor speedup medido seguro, no un óptimo global. El espacio de búsqueda (asyncio, COPY binario, Clearbit SDK async) no fue exhaustivo.

843 s → 127 s
6.6x más rápido
3.14 USD coste

Comandos exactos para reproducir:

$ python enrich_batch_job.py --workers 25 --insert-batch 1000 --sklearn-batch --leads 50000
$ pytest tests/test_scoring.py -v # correctness gate — debe ser verde
$ psql cultivaleads -c "CREATE INDEX CONCURRENTLY idx_leads_updated ON leads(updated_at) WHERE processed = false;"

El job puede correr cada hora (127 s << 3600 s). Target de <180 s superado (127 s). Siguiente iteración recomendada si se necesita sub-60 s: migrar a asyncio+aiohttp y evaluar Clearbit bulk-enrich API (endpoint beta).