Módulo 8: Project Validate Mercados Recommendations

Paso 6: sintetiza las señales, actualiza la confianza, decide

Descripción

Tienes, después de las lecciones 4, 5 y 6, tres fuentes de evidencia sobre recommendations: las notas de las ocho entrevistas de comportamiento, la señal del fake door, y esa misma evidencia ya auditada por sesgo. Esta lección junta esas fuentes en el paso que da nombre al módulo 7 de esta guía —sintetizar y decidir— y por primera vez en todo este módulo, mueve el confidence de la apuesta desde el techo de 0.3 que arrastra desde product-thinking-for-engineers-guide, hasta llegar a una decisión: persevere, pivot o kill.

Conexión con el módulo. El módulo 7 de esta guía —que se completa junto con este— formalizó tres modelos para esta misma tarea: synthesize(), que agrupa hallazgos por problema y cuenta señales reales entre usuarios distintos; updateConfidence(), que mueve el confidence de la apuesta en proporción a la fuerza y la dirección de la evidencia; y decide(), que traduce ese número, contra dos umbrales acordados de antemano, en una de tres acciones. Esta lección reutiliza las tres funciones exactamente como quedaron en el módulo 7, sin ningún cambio —misma firma, misma fórmula, mismo vocabulario— sobre el caso completo de recommendations, el mismo que has estado construyendo desde la lección 2 de este módulo.

Una analogía: el jurado que ya escuchó todos los testimonios

Un jurado no delibera después de escuchar un solo testigo — espera a que pasen todos, con su propia credibilidad y su propio peso, y solo entonces se retira a deliberar. Las ocho entrevistas de comportamiento son un testimonio colectivo —lo que varios compradores, sin ponerse de acuerdo, dijeron sobre sus propios problemas—; el fake door es un testigo distinto, y más directo —no lo que la gente dice, sino lo que la gente hizo con un clic real en el checkout—. El jurado no promedia los dos testimonios ni los mezcla en un solo número: primero escucha al conjunto de compradores para saber si el problema de fondo es real (synthesize()), y después pesa, por separado, lo que el comportamiento observado en el fake door aporta a la confianza en la solución específica (updateConfidence()) — y recién con las dos piezas sobre la mesa, delibera (decide()). Esta lección hace exactamente eso con la evidencia de recommendations: no un veredicto instantáneo al ver el primer resultado positivo, sino una deliberación que procesa cada fuente por lo que realmente es, hasta llegar a una conclusión que se puede defender pieza por pieza.

Ejemplo trabajado: synthesize(), updateConfidence() y decide() sobre el caso completo

Parte 1 — Contar señales reales en las entrevistas

Retomamos las diez notas de las ocho entrevistas de comportamiento que el módulo 7 (lecciones 2 a 4) ya sintetizó sobre esta misma apuesta —no una ronda nueva, la misma evidencia real— y corremos synthesize() exactamente como quedó en la lección 4 de ese módulo: agrupa por problema, cuenta cuántos usuarios distintos, sin que se les sugiera, mencionaron cada uno, y clasifica el grupo como signal si al menos 2 usuarios distintos lo confirmaron por su cuenta, o noise si no llega a ese mínimo:

// L7 (M8), Parte 1: synthesize() -- funcion CANONICA del modulo 7 (L2-L4),
// reutilizada sin ningun cambio, sobre las notas reales de las ocho
// entrevistas de comportamiento que el modulo 7 ya sintetizo para esta misma
// apuesta -- no una ronda nueva de entrevistas propia de este modulo.
function synthesize(findings, options = {}) {
  const signalMinUsers = options.signalMinUsers || 2;
  const byProblem = {};
  findings.forEach((f) => {
    if (!byProblem[f.problem]) byProblem[f.problem] = [];
    byProblem[f.problem].push(f);
  });
  return Object.keys(byProblem)
    .map((problem) => {
      const items = byProblem[problem];
      const distinctUsersUnprompted = new Set(
        items.filter((f) => f.unprompted).map((f) => f.user)
      ).size;
      return {
        problem,
        mentions: items.length,
        distinctUsersUnprompted,
        classification: distinctUsersUnprompted >= signalMinUsers ? 'signal' : 'noise',
      };
    })
    .sort((a, b) => b.distinctUsersUnprompted - a.distinctUsersUnprompted);
}

console.log('=== synthesize() sobre las 10 notas de las 8 entrevistas de Mercado ===\n');
const findings = [
  { user: 'buyer_01', problem: 'no descubro productos que me gustarian sin buscarlos por nombre exacto', unprompted: true },
  { user: 'buyer_01', problem: 'no descubro productos que me gustarian sin buscarlos por nombre exacto', unprompted: true },
  { user: 'buyer_02', problem: 'no descubro productos que me gustarian sin buscarlos por nombre exacto', unprompted: true },
  { user: 'buyer_02', problem: 'se me olvida el carrito y no vuelvo a completarlo', unprompted: true },
  { user: 'buyer_03', problem: 'no confio en vendedores nuevos sin reseñas', unprompted: true },
  { user: 'buyer_04', problem: 'no descubro productos que me gustarian sin buscarlos por nombre exacto', unprompted: false },
  { user: 'buyer_05', problem: 'se me olvida el carrito y no vuelvo a completarlo', unprompted: true },
  { user: 'buyer_06', problem: 'no descubro productos que me gustarian sin buscarlos por nombre exacto', unprompted: true },
  { user: 'buyer_07', problem: 'se me olvida el carrito y no vuelvo a completarlo', unprompted: true },
  { user: 'buyer_08', problem: 'la app se cierra sola cuando el celular tiene poca memoria', unprompted: true },
];
const synthesized = synthesize(findings);
synthesized.forEach((s) => {
  console.log('[' + s.classification.toUpperCase().padEnd(6) + '] "' + s.problem + '"');
  console.log('         menciones=' + s.mentions + '  usuarios-distintos-sin-sugerir=' + s.distinctUsersUnprompted + '\n');
});
const signals = synthesized.filter((s) => s.classification === 'signal');
const noise = synthesized.filter((s) => s.classification === 'noise');
console.log('Resumen: ' + signals.length + ' señal(es), ' + noise.length + ' de ruido, de ' + synthesized.length + ' problemas distintos mencionados.');

Qué esperar.

=== synthesize() sobre las 10 notas de las 8 entrevistas de Mercado ===

[SIGNAL] "no descubro productos que me gustarian sin buscarlos por nombre exacto"
         menciones=5  usuarios-distintos-sin-sugerir=3

[SIGNAL] "se me olvida el carrito y no vuelvo a completarlo"
         menciones=3  usuarios-distintos-sin-sugerir=3

[NOISE ] "no confio en vendedores nuevos sin reseñas"
         menciones=1  usuarios-distintos-sin-sugerir=1

[NOISE ] "la app se cierra sola cuando el celular tiene poca memoria"
         menciones=1  usuarios-distintos-sin-sugerir=1

Resumen: 2 señal(es), 2 de ruido, de 4 problemas distintos mencionados.

Con signalMinUsers: 2 —el umbral que el módulo 7 fijó de antemano—, dos problemas cruzan la barra, no uno solo. El primero, "no descubro productos que me gustarían...", es la misma oportunidad de mayor impacto (impact: 5) del árbol del módulo 3, la que sostiene recommendations desde el principio, confirmada por 3 compradores distintos sin que nadie se lo sugiriera. El segundo, "se me olvida el carrito...", queda empatado con la misma fuerza —también 3 usuarios distintos— aunque tenga menos menciones totales (3 contra 5): la síntesis premia usuarios distintos, no volumen de notas, exactamente la corrección que la lección 3 del módulo 7 introdujo. Los otros dos problemas —confianza en vendedores nuevos, el reporte aislado de que la app se cierra sola— se quedan del lado del ruido, con un solo comprador cada uno.

Parte 2 — Actualizar el confidence con el resultado del fake door

Con la señal del fake door (lección 5) validando la hipótesis, movemos el confidence de la apuesta desde el techo de 0.3 que dejó RICE. updateConfidence(prior, evidence), exactamente como quedó en la lección 7 del módulo 7, recibe una sola pieza de evidencia formal: el resultado agregado del fake door, con strength: 0.7 —la misma fuerza que la lección 7 de ese módulo declaró para este mismo test—.

// L7 (M8), Parte 2: updateConfidence() -- funcion CANONICA del modulo 7 (L7),
// reutilizada sin ningun cambio. Recibe UNA sola pieza de evidencia formal --
// el resultado agregado del fake door -- para no contar dos veces la misma
// señal que ya resume, en un solo numero, cientos de clics reales.
function updateConfidence(prior, evidence) {
  const posteriorRaw = evidence.validates
    ? prior + (1 - prior) * evidence.strength
    : prior - prior * evidence.strength;
  const posterior = Math.round(posteriorRaw * 100) / 100;
  return {
    prior,
    posterior,
    delta: Math.round((posterior - prior) * 100) / 100,
    direction: evidence.validates ? 'sube' : 'baja',
  };
}

console.log('=== updateConfidence() -- el fake door de la leccion 5 VALIDO la hipotesis ===\n');
console.log('recordatorio: fakeDoorSignal() dio 6.5% de clic-through contra un umbral del 5% -- paso.');
console.log('strength=0.7: comportamiento real observado en el checkout, pero una sola semana, una sola ubicacion.\n');
const PRIOR = 0.3;
const confidenceUpdate = updateConfidence(PRIOR, { validates: true, strength: 0.7 });
console.log('prior=' + confidenceUpdate.prior + ' (topado por RICE desde product-thinking-for-engineers)');
console.log('posterior=' + confidenceUpdate.posterior + '  (' + confidenceUpdate.direction + ', delta=' + confidenceUpdate.delta + ')');

Qué esperar.

=== updateConfidence() -- el fake door de la leccion 5 VALIDO la hipotesis ===

recordatorio: fakeDoorSignal() dio 6.5% de clic-through contra un umbral del 5% -- paso.
strength=0.7: comportamiento real observado en el checkout, pero una sola semana, una sola ubicacion.

prior=0.3 (topado por RICE desde product-thinking-for-engineers)
posterior=0.79  (sube, delta=0.49)

Vale la pena explicar por qué updateConfidence() recibe una pieza de evidencia aquí, y no las cinco que la lección 6 dejó ordenadas por strength. El resultado agregado del fake door —260 clics de 4,000 impresiones, 6.5%ya es la suma de exactamente ese tipo de observaciones individuales de comportamiento, multiplicado por miles de visitas reales al checkout, no solo los pocos casos que el equipo pudo anotar a mano. Sumar el agregado y, además, las piezas individuales que lo componen —el clic, el "ignoró la sección", el producto agregado al carrito— sería contar la misma señal dos veces, exactamente el error que la fórmula canónica de updateConfidence() evita por diseño: una pieza de evidencia, un solo salto de prior a posterior. Eso no vuelve inútil la auditoría de la lección 6: las cinco piezas ordenadas por rankByStrength() siguen cumpliendo su función real, que es cualitativa, no aritmética —confirman que la evidencia detrás del agregado no es unánime (alguien ignoró la sección en dos visitas), una razón concreta para no leer el 6.5% como una validación aplastante, aunque ese matiz no cambie el cálculo del confidence.

Parte 3 — Decidir: persevere, pivot, o kill

Con el confidence en 0.79, y los dos umbrales acordados de antemano en la lección 6 del módulo 7 —persevere: 0.6, pivot: 0.3—, llega la decisión:

// L7 (M8), Parte 3: decide() -- funcion CANONICA del modulo 7 (L6). Los
// umbrales se acuerdan ANTES de calcular el confidence, no se ajustan
// despues para que el resultado "quede bien".
function decide(signalStrength, threshold) {
  if (signalStrength >= threshold.persevere) {
    return { signalStrength, action: 'persevere', reason: 'la evidencia acumulada supera el umbral para seguir invirtiendo tal como esta' };
  }
  if (signalStrength >= threshold.pivot) {
    return { signalStrength, action: 'pivot', reason: 'hay señal real pero no alcanza para seguir sin cambios' };
  }
  return { signalStrength, action: 'kill', reason: 'la evidencia acumulada no alcanza ni para pivotar' };
}

console.log('\n=== decide() ===\n');
const THRESHOLD = { persevere: 0.6, pivot: 0.3 };
const decision = decide(confidenceUpdate.posterior, THRESHOLD);
console.log('umbrales acordados de antemano: persevere >= ' + THRESHOLD.persevere + '  |  pivot >= ' + THRESHOLD.pivot);
console.log('signalStrength=' + decision.signalStrength + '  ->  ' + decision.action.toUpperCase());
console.log(decision.reason);

Qué esperar.

=== decide() ===

umbrales acordados de antemano: persevere >= 0.6  |  pivot >= 0.3
signalStrength=0.79  ->  PERSEVERE
la evidencia acumulada supera el umbral para seguir invirtiendo tal como esta

0.79 supera el umbral de persevere (0.6) con un margen real —casi dos décimas, no un empate técnico—. decide() no necesita llegar siquiera al segundo if: la decisión es PERSEVERE, la misma que el módulo 7 ya alcanzó, con la misma evidencia, en su propio proyecto de cierre. Esto no es casualidad ni redundancia — es exactamente lo que se espera de un modelo canónico aplicado dos veces a los mismos datos: si synthesize(), updateConfidence() y decide() produjeran resultados distintos entre el módulo 7 y este módulo sobre la misma apuesta, algo estaría mal con al menos una de las dos corridas, no con la evidencia misma.

Los tres modelos, uno junto al otro

Modelo               Entrada                    Salida                        Pregunta que responde
────────────────────  ─────────────────────────  ────────────────────────────  ──────────────────────────
synthesize()          notas de entrevista,        problemas agrupados, con      ¿cuantos usuarios distintos
                       cada una con user +         mentions y                    mencionan lo mismo, sin
                       problem + unprompted        distinctUsersUnprompted,       que se lo sugieran?
                                                    y classification (signal/
                                                    noise)

updateConfidence()    prior + evidence            prior, posterior, delta,      ¿cuanto mueve esta pieza de
                       {validates, strength}       direction                     evidencia la creencia?

decide()              signalStrength + threshold  signalStrength, action        con TODO lo anterior, ¿que
                                                    (persevere/pivot/kill)        hacemos ahora?
────────────────────  ─────────────────────────  ────────────────────────────  ──────────────────────────

Errores comunes

Tratar un confidence holgado como si fuera un 1.0 perfecto. Qué pasa: al ver que 0.79 supera cómodamente el umbral de persevere (0.6), alguien comunica el resultado como "recommendations ya está validado al 100%", sin mencionar que sigue siendo un modelo pedagógico basado en una sola pieza formal de evidencia —el fake door—, con sus propios límites declarados (una semana, una ubicación, medido en clics, no en compras reales). Por qué pasa: un margen amplio sobre el umbral se siente como certeza absoluta, y es fácil olvidar que el número sigue siendo una estimación con evidencia limitada, no una medición exhaustiva. Cómo detectarlo: si le preguntas al equipo "¿qué pasaría si corriéramos el fake door una segunda semana y diera un resultado distinto?", y la respuesta es "no debería cambiar nada, ya está validado al 100%", el confidence se comunicó sin sus límites. Cómo corregirlo: reporta siempre el número exacto —0.79, no "validado" ni "100%"— y recuerda que, igual que declaró la lección 7 del módulo 7, es la mejor estimación disponible hoy, no una garantía permanente.

Tratar PERSEVERE como "ya está construido", en vez de "sigamos invirtiendo en la apuesta tal como está planteada". Qué pasa: al comunicar la decisión, alguien resume "recommendations ya se validó, vamos a lanzarlo" —saltándose que persevere es una decisión sobre seguir invirtiendo en el discovery y la construcción de la apuesta, no un anuncio de que el motor de recomendaciones ya existe en producción—. Por qué pasa: después de siete lecciones y una decisión favorable, es tentador tratar el cierre del módulo como si fuera el cierre del proyecto completo. Cómo detectarlo: si la comunicación del resultado no distingue entre "decidimos seguir invirtiendo en esta apuesta" y "el motor ya está construido y midiendo GMV real", se perdió la frontera central de esta guía. Cómo corregirlo: PERSEVERE en este pipeline significa que la evidencia recolectada —barata, temprana, cualitativa y de un solo fake door— justifica seguir invirtiendo en recommendations sin cambiar de solución; no significa que el trabajo de construir y medir con rigor ya esté hecho. La lección 8 hace explícito, con el mapa completo de la guía, qué queda pendiente después de esta decisión.

Ejercicios

Ejercicio 1 — Recalcula decide() con un umbral más exigente. Si el equipo hubiera fijado { persevere: 0.85, pivot: 0.3 } en vez de { persevere: 0.6, pivot: 0.3 } —un umbral de persevere más alto, decidido antes de correr cualquier test—, ¿cambiaría la decisión final con el mismo confidence de 0.79?

Ver solución

Sí cambiaría: con persevere: 0.85, 0.79 >= 0.85 es falso, así que decide() pasa al segundo chequeo; como 0.79 >= 0.3 (el umbral de pivot) sí es verdadero, la decisión sería PIVOT en vez de PERSEVERE. Este ejercicio no es un permiso para "elegir el umbral que da el resultado que quieres" —eso es exactamente el error que la lección 6 del módulo 7 advirtió—, sino una demostración de por qué el umbral tiene que fijarse antes, con un criterio de negocio explícito sobre cuánto confidence justifica seguir invirtiendo en recommendations tal como está planteada, y no ajustarse después para que el número calce con lo que el equipo esperaba encontrar.

Ejercicio 2 — Calcula el caso contrario: si el fake door hubiera refutado la hipótesis. Sin ejecutar Node, si el mismo fake door hubiera dado un resultado por debajo del 5% de umbral —validates: false, con la misma strength: 0.7—, ¿cuál sería el posterior resultante, partiendo del mismo prior: 0.3? ¿Qué decidiría decide() sobre ese número?

Ver solución

posterior = 0.3 - 0.3 * 0.7 = 0.3 - 0.21 = 0.09. Con decide(0.09, { persevere: 0.6, pivot: 0.3 }), el resultado sería KILL: 0.09 no llega ni al umbral de pivot (0.3), así que la función devuelve la tercera rama. Este ejercicio confirma la asimetría de updateConfidence() que ya viste en el módulo 7: partiendo del mismo prior bajo (0.3), una validación fuerte sube mucho (+0.49, hasta 0.79) porque hay mucho margen para crecer, mientras que una refutación con la misma strength baja menos en términos absolutos (-0.21, hasta 0.09) porque el margen para caer, desde un prior ya bajo, es más chico — pero en este caso concreto, esa caída de todas formas es suficiente para cruzar los dos umbrales de decide() de un salto y llegar directo a kill.

Ejercicio 3 — Agrega una novena entrevista y recalcula la síntesis. Sin ejecutar Node, si una novena entrevista agregara { user: 'buyer_09', problem: 'no confio en vendedores nuevos sin reseñas', unprompted: true } a findings, ¿cambiaría la clasificación de "no confío en vendedores nuevos sin reseñas"? ¿Y cambiaría la decisión final de decide() sobre recommendations?

Ver solución

La clasificación de ese problema sí cambiaría: distinctUsersUnprompted subiría de 1 (buyer_03) a 2 (buyer_03, buyer_09), cruzando el umbral de signalMinUsers: 2 y pasando de noise a signal. Pero la decisión final sobre recommendations no cambiaría: el signalStrength que entra a decide() viene específicamente de updateConfidence() sobre el resultado del fake door (0.79), no de la síntesis de las entrevistas — un cluster nuevo que cruza a signal es evidencia real sobre un problema distinto, que ameritaría su propio ciclo de discovery, pero no mueve el número que ya decidió la apuesta de recommendations. Este ejercicio confirma algo importante: la síntesis de entrevistas confirma o no la oportunidad subyacente, y updateConfidence() mueve la confianza en la solución específica — son dos preguntas distintas, con sus propios números, que decide() combina solo a través de este último.

Resumen y siguiente paso

En esta lección corriste los tres modelos canónicos del módulo 7 sobre el caso completo de recommendations: synthesize() confirmó que la oportunidad de descubrimiento de productos tiene señal real (3 de 8 compradores la mencionan sin que se les sugiera, empatada con "carrito olvidado"), updateConfidence() movió el confidence de la apuesta desde el techo de 0.3 hasta 0.79 con la única pieza de evidencia formal —el fake door—, y decide() tradujo ese número, contra umbrales fijados de antemano, en la primera decisión completa de todo este módulo: PERSEVERE.

Antes de avanzar deberías poder: explicar por qué los umbrales de decide() se fijan antes de calcular el confidence, no después; y distinguir, con tus propias palabras, qué significa PERSEVERE frente a PIVOT y KILL en este modelo.

La lección 8 —el proyecto final de esta guía— no repite este pipeline en aislamiento: lo corre una vez más, de punta a punta, encadenado con los seis pasos anteriores en un solo archivo, y convierte la decisión PERSEVERE de hoy en un plan concreto de lanzamiento y medición, con el cierre completo de todo lo que aprendiste desde la lección 1 del módulo 1.

Recursos

  • Teresa Torres, Continuous Discovery Habitsproducttalk.org/continuous-discovery-habits. Sobre cómo sintetizar evidencia de discovery de forma continua, no como un evento único al final de un sprint. En inglés.
  • Eric Ries, The Lean Startuptheleanstartup.com/book. La fuente original del vocabulario "pivotar" — vale la pena releerlo ahora que la decisión de este proyecto fue persevere, no pivot, para tener claro, de antemano, qué hubiera significado la alternativa. En inglés.
  • Marty Cagan (Silicon Valley Product Group), "The Four Big Risks" — svpg.com/four-big-risks. Un recordatorio final de por qué una decisión de esta magnitud —persevere, pivot o kill— merece el pipeline completo, no una intuición de reunión. En inglés.