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
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).