Módulo 7: Comparación técnica de proveedores
Matriz de decisión avanzada
Tienes tres dimensiones medidas: latencia, costo, calidad. Cada proveedor gana en alguna y pierde en otra. La pregunta sigue siendo: ¿cuál eliges?
La respuesta no es elegir el "mejor en todo" (no existe). Es decidir cómo pesas las dimensiones y aplicar esa ponderación con disciplina. Esta cápsula te da el método.
Al terminar vas a poder:
- Asignar pesos a las dimensiones según prioridades reales de tu proyecto
- Normalizar las métricas para que sean comparables (latencia en segundos no se suma con costo en dólares)
- Calcular un score compuesto que ranquea proveedores
- Documentar la decisión con razones que sobreviven a un audit técnico
- Distinguir restricciones absolutas (privacidad, compliance) de preferencias (latencia, costo)
Por qué importa
Cuando un equipo elige proveedor "por consenso", el resultado depende de quién habló más fuerte en la reunión. Cuando elige con una matriz ponderada y documentada, el resultado es defendible:
- Tres meses después, alguien pregunta "¿por qué Modal y no OpenAI?" → la matriz responde con números.
- Necesitas re-evaluar porque cambió un precio → recalculas el score, no rehaces el debate.
- Llega un nuevo team member → entiende la decisión leyendo la matriz, no preguntando.
Es la diferencia entre decisión técnica y opinión grupal con apariencia técnica.
Paso 1 — Distinguir restricciones de preferencias
Antes de ponderar nada, separa qué es innegociable vs qué es deseable:
| Tipo | Ejemplos | Tratamiento |
|---|---|---|
| Restricción absoluta | "Los datos no pueden salir de la UE" / "Necesito SOC2" / "Budget máximo $1000/mes" | Elimina proveedores que no cumplen, ANTES de scoring |
| Preferencia ponderada | "Quiero latencia baja" / "Quiero costo bajo" / "Quiero buena calidad" | Entra en la matriz con peso |
Filtrar primero evita perder tiempo puntuando opciones que no son viables. Si tu cliente exige hosting en EU, no tiene caso scorearle OpenAI (USA) — la elimina antes.
Paso 2 — Definir pesos
Suma de pesos = 1.0. Distribución típica para distintos productos:
| Producto | Latencia | Costo | Calidad |
|---|---|---|---|
| Chatbot consumer real-time | 0.5 | 0.2 | 0.3 |
| Análisis batch nocturno | 0.1 | 0.5 | 0.4 |
| Asistente médico | 0.2 | 0.1 | 0.7 |
| Generación creativa B2C | 0.3 | 0.3 | 0.4 |
| Soporte enterprise | 0.3 | 0.2 | 0.5 |
No hay "pesos correctos universales". Hay pesos que reflejan lo que importa a tu producto. Discútelos con tu equipo antes de scorear.
Paso 3 — Normalizar las métricas
Latencia (segundos), costo ($), calidad (% de score) son unidades distintas. No se pueden sumar directamente. Normaliza cada una a [0, 1] donde 1 = mejor:
latencia_normalizada = 1 - (latencia_proveedor - latencia_min) / (latencia_max - latencia_min)
costo_normalizado = 1 - (costo_proveedor - costo_min) / (costo_max - costo_min)
calidad_normalizada = (calidad_proveedor - calidad_min) / (calidad_max - calidad_min)
El truco del 1 - en latencia y costo es porque "menor es mejor" en esas dimensiones, pero queremos que "mejor = 1" en todas.
Paso 4 — Calcular score compuesto
score = (latencia_normalizada × peso_latencia)
+ (costo_normalizado × peso_costo)
+ (calidad_normalizada × peso_calidad)
Score más alto = mejor opción según tus pesos.
Implementación: matriz reusable
Crea matriz_decision.py:
# matriz_decision.py
from dataclasses import dataclass
from typing import Literal
@dataclass
class Proveedor:
nombre: str
latencia_p95_s: float # de tu benchmark
costo_mensual_usd: float # para tu volumen proyectado
calidad_score: float # 0-100 (% de score de tu rubric)
cumple_data_residency_eu: bool = True
cumple_soc2: bool = True
@dataclass
class Pesos:
latencia: float = 0.33
costo: float = 0.33
calidad: float = 0.34
def __post_init__(self):
total = self.latencia + self.costo + self.calidad
assert abs(total - 1.0) < 0.001, f"Los pesos deben sumar 1.0, suman {total}"
@dataclass
class Restricciones:
data_residency_eu: bool = False
soc2: bool = False
presupuesto_max_usd: float = float("inf")
def filtrar_por_restricciones(
proveedores: list[Proveedor], restricciones: Restricciones
) -> tuple[list[Proveedor], list[str]]:
"""Filtra proveedores que no cumplen restricciones absolutas. Devuelve (validos, descartados_con_razon)."""
validos = []
descartados = []
for p in proveedores:
razones = []
if restricciones.data_residency_eu and not p.cumple_data_residency_eu:
razones.append("no cumple EU data residency")
if restricciones.soc2 and not p.cumple_soc2:
razones.append("no cumple SOC2")
if p.costo_mensual_usd > restricciones.presupuesto_max_usd:
razones.append(f"excede presupuesto (${p.costo_mensual_usd:.0f} > ${restricciones.presupuesto_max_usd:.0f})")
if razones:
descartados.append(f"{p.nombre}: {', '.join(razones)}")
else:
validos.append(p)
return validos, descartados
def normalizar(valores: list[float], mejor_es_menor: bool = False) -> list[float]:
"""Normaliza a [0, 1] donde 1 = mejor."""
if not valores or max(valores) == min(valores):
return [1.0] * len(valores)
vmin, vmax = min(valores), max(valores)
if mejor_es_menor:
return [1 - (v - vmin) / (vmax - vmin) for v in valores]
else:
return [(v - vmin) / (vmax - vmin) for v in valores]
def calcular_scores(proveedores: list[Proveedor], pesos: Pesos) -> list[tuple[Proveedor, float, dict]]:
latencias = [p.latencia_p95_s for p in proveedores]
costos = [p.costo_mensual_usd for p in proveedores]
calidades = [p.calidad_score for p in proveedores]
n_lat = normalizar(latencias, mejor_es_menor=True)
n_cost = normalizar(costos, mejor_es_menor=True)
n_qual = normalizar(calidades, mejor_es_menor=False)
resultados = []
for p, nl, nc, nq in zip(proveedores, n_lat, n_cost, n_qual):
score = nl * pesos.latencia + nc * pesos.costo + nq * pesos.calidad
breakdown = {
"latencia_norm": nl,
"costo_norm": nc,
"calidad_norm": nq,
"contribucion_latencia": nl * pesos.latencia,
"contribucion_costo": nc * pesos.costo,
"contribucion_calidad": nq * pesos.calidad,
}
resultados.append((p, score, breakdown))
return sorted(resultados, key=lambda x: x[1], reverse=True)
def reporte(proveedores: list[Proveedor], pesos: Pesos, restricciones: Restricciones):
print(f"\n=== Restricciones aplicadas ===")
print(f" data residency EU: {restricciones.data_residency_eu}")
print(f" SOC2: {restricciones.soc2}")
print(f" Presupuesto max: ${restricciones.presupuesto_max_usd:,.0f}/mes")
validos, descartados = filtrar_por_restricciones(proveedores, restricciones)
if descartados:
print(f"\n=== Descartados por restricciones ===")
for d in descartados:
print(f" ✗ {d}")
if not validos:
print("\n⚠️ NINGÚN proveedor cumple las restricciones. Revisar.")
return
print(f"\n=== Pesos ===")
print(f" Latencia: {pesos.latencia:.2f}")
print(f" Costo: {pesos.costo:.2f}")
print(f" Calidad: {pesos.calidad:.2f}")
scores = calcular_scores(validos, pesos)
print(f"\n=== Ranking ===\n")
print(f"{'Proveedor':<35} {'P95':>7} {'$/mes':>10} {'Cal%':>6} {'Score':>8}")
print("-" * 70)
for p, score, _ in scores:
print(f"{p.nombre:<35} {p.latencia_p95_s:>6.2f}s {p.costo_mensual_usd:>9.0f} {p.calidad_score:>5.1f} {score:>8.3f}")
ganador, score_g, breakdown = scores[0]
print(f"\n→ Recomendación: {ganador.nombre} (score {score_g:.3f})")
print(f" Composición del score:")
print(f" Latencia: {breakdown['contribucion_latencia']:.3f}")
print(f" Costo: {breakdown['contribucion_costo']:.3f}")
print(f" Calidad: {breakdown['contribucion_calidad']:.3f}")
# ============================================================
# Ejemplo trabajado
# ============================================================
if __name__ == "__main__":
# Datos hipotéticos para tu caso (reemplaza con tus mediciones reales)
proveedores = [
Proveedor("OpenAI GPT-4o-mini", latencia_p95_s=2.3, costo_mensual_usd=39, calidad_score=86,
cumple_data_residency_eu=False),
Proveedor("OpenAI GPT-4o", latencia_p95_s=3.1, costo_mensual_usd=900, calidad_score=92,
cumple_data_residency_eu=False),
Proveedor("OpenRouter Mistral 7B", latencia_p95_s=3.0, costo_mensual_usd=10, calidad_score=69),
Proveedor("Modal Mistral 7B + warm pool", latencia_p95_s=2.7, costo_mensual_usd=283, calidad_score=69),
Proveedor("Self-hosted Ollama Mistral EU", latencia_p95_s=5.9, costo_mensual_usd=720, calidad_score=69,
cumple_data_residency_eu=True),
]
# Caso A: chatbot consumer real-time (latencia importa)
pesos_consumer = Pesos(latencia=0.5, costo=0.2, calidad=0.3)
restr_libres = Restricciones()
print("\n" + "=" * 70)
print("CASO A — Chatbot consumer real-time, sin restricciones especiales")
print("=" * 70)
reporte(proveedores, pesos_consumer, restr_libres)
# Caso B: cliente enterprise EU
pesos_enterprise = Pesos(latencia=0.3, costo=0.2, calidad=0.5)
restr_eu = Restricciones(data_residency_eu=True, presupuesto_max_usd=1000)
print("\n" + "=" * 70)
print("CASO B — Cliente enterprise, data residency EU obligatorio, max $1k/mes")
print("=" * 70)
reporte(proveedores, pesos_enterprise, restr_eu)
Lectura típica de resultados
======================================================================
CASO A — Chatbot consumer real-time, sin restricciones especiales
======================================================================
=== Pesos ===
Latencia: 0.50
Costo: 0.20
Calidad: 0.30
=== Ranking ===
Proveedor P95 $/mes Cal% Score
----------------------------------------------------------------------
OpenAI GPT-4o-mini 2.30s 39 86.0 0.834
Modal Mistral 7B + warm pool 2.70s 283 69.0 0.557
OpenRouter Mistral 7B 3.00s 10 69.0 0.534
OpenAI GPT-4o 3.10s 900 92.0 0.469
Self-hosted Ollama Mistral EU 5.90s 720 69.0 0.060
→ Recomendación: OpenAI GPT-4o-mini (score 0.834)
Composición:
Latencia: 0.500 (perfect: es el más rápido)
Costo: 0.193 (casi mejor que todos los costosos)
Calidad: 0.141 (buena, no la mejor)
======================================================================
CASO B — Cliente enterprise, data residency EU obligatorio
======================================================================
=== Descartados por restricciones ===
✗ OpenAI GPT-4o-mini: no cumple EU data residency
✗ OpenAI GPT-4o: no cumple EU data residency
✗ OpenRouter Mistral 7B: no cumple EU data residency (asumido)
✗ Modal Mistral 7B + warm pool: no cumple EU data residency (asumido)
⚠️ Solo queda: Self-hosted Ollama Mistral EU
Lectura:
- Caso A: OpenAI GPT-4o-mini gana claramente. Latencia y costo competitivos, calidad muy buena.
- Caso B: Las restricciones reducen drásticamente el espacio. Solo queda Self-hosted EU. La elección se vuelve obvia, no por benchmark sino por restricción.
Análisis de sensibilidad
Pregunta crítica: ¿qué tan sensible es la decisión a tus pesos?
Cambia pesos ligeramente y ve si el ranking cambia. Si pequeñas variaciones cambian el ganador, la decisión es frágil — necesitas más datos antes de comprometerte.
# Análisis de sensibilidad
for peso_lat in [0.3, 0.4, 0.5, 0.6]:
peso_cost = (1 - peso_lat) / 2
peso_qual = (1 - peso_lat) / 2
pesos = Pesos(latencia=peso_lat, costo=peso_cost, calidad=peso_qual)
scores = calcular_scores(validos, pesos)
print(f"\n Latencia={peso_lat}: ganador = {scores[0][0].nombre}")
Si ves "ganador = OpenAI" en todos los casos, la decisión es robusta. Si ves alternancia, vale la pena profundizar.
Patrón: tabla de razones, no solo de números
Una matriz numérica esconde el por qué. Para documentar una decisión enterprise, complementa con tabla de razones:
| Proveedor | Score | Razón principal a favor | Razón principal en contra |
|---|---|---|---|
| OpenAI GPT-4o-mini | 0.834 | Latencia y calidad balanceadas | Sin data residency EU; lock-in |
| OpenRouter Mistral | 0.534 | Costo bajísimo | Calidad limitada para casos complejos |
| Self-hosted Ollama EU | 0.060 | Cumple compliance estricto | Costo alto; mantenimiento humano |
Esa tabla es lo que pegas en el doc de decisión.
Trampas comunes
Trampa 1 — "Pesos del CTO, no del producto." A veces el CTO favorece costo por sesgo de fondos limitados, pero el producto necesita calidad. Discute los pesos con product/users, no solo engineering.
Trampa 2 — "Score absoluto, no relativo." Score 0.83 vs 0.81 es casi empate. Tratarlos como diferencia clara es ruido. Si los top 2 están <0.05 entre sí, decide con criterios cualitativos (familiaridad del equipo, soporte, ecosistema).
Trampa 3 — "Normalización dramatiza diferencias chicas." Si latencias son 2.0s, 2.1s, 2.2s, la normalización las convierte en 1.0, 0.5, 0.0. Esa diferencia parece enorme en el score pero es imperceptible para el usuario (100ms). Considera usar normalización con clip o threshold para diferencias mínimas.
Trampa 4 — "Olvidaste una dimensión clave." Latencia + costo + calidad es un buen default, pero hay otras: vendor lock-in, ecosistema, calidad de soporte, alineación con compliance evolutivo. Agrégalas si tu caso lo amerita.
Trampa 5 — "Decisión escrita en piedra." Pricing cambia. Modelos nuevos salen cada trimestre. Tu matriz debe revisarse trimestralmente, no decidirse una vez y olvidarse.
Ejercicio
Construye la matriz para tu propio caso:
- Define tus restricciones absolutas (data residency, budget, compliance)
- Pondera latencia/costo/calidad según prioridades reales de tu producto
- Mete los números que mediste en cápsulas 02-04
- Calcula scores
- Haz análisis de sensibilidad cambiando pesos ±0.1
- Documenta tu recomendación con la tabla de razones
Ejemplo de output esperable
Producto: Asistente de soporte técnico B2B
Restricciones absolutas:
- data_residency_eu para clientes enterprise (5 clientes)
- budget_max: $2000/mes
Pesos:
- Latencia: 0.3 (importante pero no crítico)
- Costo: 0.2 (relevante)
- Calidad: 0.5 (clave para producto B2B)
Tras filtros: 2 proveedores válidos
- Modal con warm pool en EU region (si certifica)
- Self-hosted EU
Score:
- Modal EU: 0.74 (latencia OK, calidad OK, costo medio)
- Self-hosted: 0.51 (latencia peor, costo alto)
Recomendación: Modal EU con plan de migración a self-hosted si volumen supera 500K req/mes.
Sensibilidad: la decisión se mantiene con pesos en rango [latencia 0.2-0.4, costo 0.1-0.3, calidad 0.4-0.6]. Decisión robusta.
Resumen
Aprendiste:
- ✅ Separar restricciones absolutas (filtrar) de preferencias (ponderar)
- ✅ Asignar pesos según prioridades reales del producto, no opinión
- ✅ Normalizar métricas en escalas distintas para hacerlas comparables
- ✅ Calcular score compuesto que ranquea defendiblemente
- ✅ Hacer análisis de sensibilidad para validar robustez
- ✅ Documentar con tabla de razones, no solo números
Checkpoint: si puedes producir un ranking con score, justificación de pesos, y "qué cambia si modifico los pesos", estás listo.
Siguiente cápsula
06 — Migration paths. Tu matriz dijo "X gana hoy". Mañana cambia algo (precio, modelo nuevo, restricción legal) y necesitas migrar. ¿Cuánto código cambia para pasar de OpenAI a OpenRouter? ¿Qué de Modal a Ollama? Vemos los paths reales y sus costos.
Recursos
- Multi-Criteria Decision Analysis (MCDA) — overview — teoría general.
- TOPSIS method — método alternativo de scoring (similitud al ideal).
- Architectural decision records (ADR) — patrón para documentar decisiones técnicas.
- Wardley Mapping — para situar proveedores en el ciclo de madurez.