Módulo 4: Assumption And Hypothesis Testing

El riskiest assumption test

Descripción

Ya sabes escribir una hipótesis que de verdad se pueda refutar. El siguiente problema es distinto: dado que hay varias formas de probarla —una entrevista, un fake door, un prototipo clickable, o construir el motor completo— ¿cuál eliges? Esta lección le pone nombre a la respuesta correcta: el riskiest assumption test (RAT), el test más barato, de entre los que de verdad pueden refutar la hipótesis, que se puede correr para poner a prueba la suposición más riesgosa de la apuesta.

Fíjate en las dos palabras que importan en esa definición, y en qué orden importan. Primero, "de entre los que de verdad pueden refutar" — esto descarta de entrada cualquier test que, sin importar el resultado, siempre se leería como un éxito. Segundo, y solo entre los que sobrevivieron ese primer filtro, "el más barato" — no el más completo, no el que se siente más riguroso, no el que construye la versión final "para estar seguros". El objetivo del RAT no es la certeza total; es la evidencia más barata que de verdad arriesga la creencia.

Conexión con el módulo. Esta lección introduce pickTest(assumption, candidateTests), la segunda herramienta central del módulo, sobre la lista real de tests candidatos para recommendations. Vas a reutilizar esta función, sin cambios, en la lección 5; combinada con isFalsifiable() en la lección 6; y ampliada con una nueva dimensión —la fuerza de la evidencia— en la lección 7. La versión final, con todas sus mejoras, es la que corre el proyecto de la lección 8 sobre el caso completo de Mercado.

Una analogía: prueba la llave antes de mudarte

Antes de firmar el contrato de un departamento nuevo y mudar todos tus muebles, hay una prueba barata que casi nadie se salta: probar si la llave abre la puerta. No contratas una mudanza completa "para estar seguro de que el departamento es el correcto" — eso sería absurdo, carísimo, y además reversible solo a un costo enorme si te equivocaste. Metes la llave, giras, y en cinco segundos tienes la respuesta que necesitabas.

Ahora compara dos formas distintas de "probar la llave". La primera: le preguntas al agente inmobiliario, "¿esta llave abre bien la puerta?" — y él, con toda la buena intención del mundo, te dice que sí. No importa si la llave de verdad funciona o no: la pregunta, tal como está hecha, siempre te va a devolver un "sí" cómodo. La segunda: metes la llave tú mismo, en la cerradura real, y giras. Esta sí puede fallar — la llave se puede atascar, puede no entrar, puede que gire pero la puerta no abra. Las dos formas de "probar" cuestan casi lo mismo en tiempo, pero solo una de las dos puede, en la práctica, decirte que estás equivocado. El riskiest assumption test es, siempre, la segunda opción — nunca la primera, sin importar lo barata que parezca.

Ejemplo trabajado: pickTest(), la primera versión

Con la suposición riesgosa de recommendations en la mesa, el equipo de Mercado tiene cuatro tests candidatos sobre la mesa, cada uno con un costo (en personDays, la misma unidad de product-thinking-for-engineers), si puede refutar la hipótesis o no, y una fuerza de evidencia estimada (strength, del 1 al 10 — la vas a usar recién en la lección 7, por ahora solo acompaña el dato).

// L4: primera version de pickTest(). Filtra los candidatos que de verdad pueden
// refutar la hipotesis (canFalsify), y entre esos, elige el mas barato (cost).
function pickTest(assumption, candidateTests) {
  const falsifiable = candidateTests.filter((t) => t.canFalsify);
  if (falsifiable.length === 0) {
    return { assumption, chosen: null, discarded: candidateTests, reason: 'ningun candidato puede refutar la suposicion -- hay que disenar uno nuevo' };
  }
  const chosen = falsifiable.reduce((best, t) => (t.cost < best.cost ? t : best));
  const discarded = candidateTests.filter((t) => t.type !== chosen.type);
  return { assumption, chosen, discarded };
}

const recommendationsBet = {
  assumption: 'Los usuarios van a comprar mas si ven recomendaciones personalizadas',
  risk: 0.6,
  impact: 5,
};

const candidateTests = [
  { type: 'ask_would_you_like_it', cost: 1, canFalsify: false, strength: 2 },
  { type: 'fake_door_checkout', cost: 3, canFalsify: true, strength: 6 },
  { type: 'clickable_prototype_interview', cost: 5, canFalsify: true, strength: 7 },
  { type: 'build_the_engine', cost: 40, canFalsify: true, strength: 9 },
];

console.log('=== pickTest() sobre la suposicion riesgosa de "recommendations" ===\n');
console.log('Suposicion: "' + recommendationsBet.assumption + '" (risk=' + recommendationsBet.risk + ', impact=' + recommendationsBet.impact + ')\n');
console.log('Candidatos:');
candidateTests.forEach((t) => {
  console.log('  ' + t.type.padEnd(30) + ' cost=' + t.cost + '  canFalsify=' + t.canFalsify + '  strength=' + t.strength);
});

const result = pickTest(recommendationsBet.assumption, candidateTests);
console.log('\nElegido: ' + result.chosen.type + ' (cost=' + result.chosen.cost + ')');
console.log('\nDescartados:');
result.discarded.forEach((t) => {
  const why = !t.canFalsify
    ? 'no puede refutar la suposicion (canFalsify=false) -- cualquier resultado se leeria como exito'
    : 'puede refutar, pero cuesta mas que el elegido (' + t.cost + ' > ' + result.chosen.cost + ')';
  console.log('  ' + t.type + ' -> ' + why);
});

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== pickTest() sobre la suposicion riesgosa de "recommendations" ===

Suposicion: "Los usuarios van a comprar mas si ven recomendaciones personalizadas" (risk=0.6, impact=5)

Candidatos:
  ask_would_you_like_it          cost=1  canFalsify=false  strength=2
  fake_door_checkout             cost=3  canFalsify=true  strength=6
  clickable_prototype_interview  cost=5  canFalsify=true  strength=7
  build_the_engine               cost=40  canFalsify=true  strength=9

Elegido: fake_door_checkout (cost=3)

Descartados:
  ask_would_you_like_it -> no puede refutar la suposicion (canFalsify=false) -- cualquier resultado se leeria como exito
  clickable_prototype_interview -> puede refutar, pero cuesta mas que el elegido (5 > 3)
  build_the_engine -> puede refutar, pero cuesta mas que el elegido (40 > 3)

Fíjate en los dos descartes que más importan, porque son la lección completa resumida en dos líneas. build_the_engine pierde a pesar de tener el strength más alto de los cuatro (9) y de ser, en un sentido intuitivo, "la prueba definitiva" — pierde porque cuesta 40 person-days, más de trece veces lo que cuesta fake_door_checkout, y ambos pueden refutar la misma hipótesis. Pagar 40 días por una respuesta que 3 días ya te pueden dar no es rigor, es desperdicio. Y ask_would_you_like_it, a pesar de ser el más barato de los cuatro (cost: 1), ni siquiera llega a competir por precio: queda descartado en el primer filtro, porque preguntarle a alguien "¿te gustaría?" es estructuralmente incapaz de salir mal — cualquier respuesta, incluso un "no, la verdad no" ocasional, se puede leer como "bueno, no le gustó a esa persona en particular" sin que la creencia general se vea amenazada.

Por qué el filtro va primero, y minimizar el costo va después

pickTest() hace dos pasos, en un orden que no es arbitrario: primero filtra por canFalsify, y solo después de tener esa lista reducida, busca el menor costo. Si el orden fuera al revés —ordenar todo por costo primero, y recién ahí fijarse en canFalsify— correrías un riesgo real: el candidato más barato de toda la lista original podría no poder refutar nada, y un vistazo apurado a "el más barato" te llevaría directo a él antes de llegar a revisar si sirve. Filtrar primero garantiza que nunca vas a comparar precios entre un test que cuenta y uno que no cuenta — los que no pueden refutar quedan fuera del juego antes de que el costo entre siquiera en la conversación. Vas a ver, en la lección 5, justo qué tan real es este riesgo: un candidato todavía más barato que fake_door_checkout, que si el filtro no fuera el primer paso, ganaría por precio sin merecerlo.

Errores comunes

Elegir el test cómodo en vez del que refuta. Qué pasa: entre ask_would_you_like_it y fake_door_checkout, un equipo con prisa elige preguntar directamente — es más rápido de montar, no necesita ningún cambio en el producto real, y "ya hicimos discovery" se siente satisfactorio con solo un día de trabajo. Por qué pasa: un fake door exige tocar el checkout real, aunque sea de forma mínima — se siente como más trabajo, más riesgo técnico, más coordinación con el equipo de ingeniería. Preguntar es, en comparación, casi gratis de organizar. Cómo detectarlo: el test elegido nunca podría, en principio, terminar con "la hipótesis quedó refutada" — solo puede terminar con distintos grados de "sí, parece que sí". Cómo corregirlo: antes de aprobar cualquier test, corre la pregunta de canFalsify primero, como hace pickTest() — si la respuesta es no, el test queda fuera de la conversación de costo por completo, sin importar cuánto más barato o cómodo parezca.

Pensar que "más caro" es sinónimo de "más confiable". Qué pasa: alguien argumenta que construir el motor completo (build_the_engine) es la única forma de estar realmente seguros, y que un fake door "no cuenta" porque no usa el algoritmo real. Por qué pasa: el esfuerzo se siente como una señal de seriedad — cuarenta días de trabajo suenan más rigurosos que tres. Cómo detectarlo: nadie puede explicar, con precisión, qué información adicional le daría build_the_engine sobre la hipótesis específica ("los usuarios compran más si ven recomendaciones") que fake_door_checkout no pueda dar ya, con recomendaciones curadas a mano en vez de generadas por el algoritmo. Cómo corregirlo: recuerda que fake_door_checkout y build_the_engine, en el ejemplo de hoy, comparten canFalsify: true — ambos pueden refutar la misma hipótesis sobre comportamiento de compra. La diferencia de strength (6 contra 9) importa, y la lección 7 le va a dar su lugar exacto — pero no justifica, por sí sola, pagar más de trece veces el costo cuando el candidato más barato ya cruza la barra de "puede refutar".

Ejercicios

Ejercicio 1 — Predice sin ejecutar Node. Con esta nueva lista de candidatos, ¿cuál elegiría pickTest(), y cuáles quedarían descartados y por qué?

const newCandidates = [
  { type: 'survey_opinion', cost: 0.5, canFalsify: false, strength: 1 },
  { type: 'landing_page_signup', cost: 2, canFalsify: true, strength: 5 },
  { type: 'concierge_manual_service', cost: 8, canFalsify: true, strength: 8 },
];
Ver solución

Elegido: landing_page_signup (cost=2). survey_opinion queda descartado de entrada por canFalsify: false (una encuesta de opinión, igual que ask_would_you_like_it, no puede salir mal de forma estructural). concierge_manual_service sí puede refutar (canFalsify: true), pero cuesta 8 contra los 2 de landing_page_signup — pierde por precio dentro del grupo de los que sí cuentan. landing_page_signup, con canFalsify: true y el menor costo de los dos candidatos válidos, gana.

Ejercicio 2 — Agrega un candidato barato pero sin canFalsify. Si al conjunto original de candidateTests de esta lección le agregas { type: 'show_mockup_in_meeting', cost: 0.2, canFalsify: false, strength: 1 } —el más barato de todos, con una diferencia enorme— ¿cambia el chosen que devuelve pickTest()? Explica por qué, sin ejecutar Node.

Ver solución

No, el elegido sigue siendo fake_door_checkout. show_mockup_in_meeting tiene cost: 0.2, muchísimo menor que cualquier otro candidato — pero canFalsify: false lo saca del array falsifiable en el primer paso de la función, antes de que su costo bajísimo tenga oportunidad de competir por el mínimo. El .reduce() que busca el menor costo solo corre sobre falsifiable, así que un candidato descartado en el filtro nunca puede ganar, sin importar cuán barato sea. Este es, en miniatura, exactamente el escenario que la lección 5 va a explorar con un ejemplo real.

Ejercicio 3 — Explica el orden de los dos pasos con tus propias palabras. En dos o tres frases, explica por qué pickTest() filtra por canFalsify antes de comparar costos, y qué error concreto se evitaría si alguien intentara hacerlo al revés (comparar costos primero, revisar canFalsify después, "si hay tiempo").

Ver solución

Una respuesta razonable: "Si comparas costos primero, el candidato más barato de la lista completa gana esa comparación antes de que nadie revise si de verdad sirve — y si resulta que no puede refutar la hipótesis, ya lo elegiste por error, o hace falta un paso extra para corregir la decisión. Filtrando primero, los candidatos que no pueden refutar quedan completamente fuera del grupo donde se compara el costo, así que nunca hay una oportunidad de elegir, por accidente, un test que no sirve solo porque era el más barato." El error concreto que se evita es exactamente el que muestra el Ejercicio 2: un candidato casi gratis, pero incapaz de fallar, ganando por default si el filtro no va primero.

Resumen y siguiente paso

En esta lección definiste el riskiest assumption test —el más barato de entre los tests que de verdad pueden refutar la hipótesis— y construiste pickTest(), que aplica esa definición en dos pasos: filtrar por canFalsify, y solo después minimizar cost. Sobre los cuatro candidatos de recommendations, la función eligió fake_door_checkout, descartando tanto la opción más barata de todas (ask_would_you_like_it, por no poder refutar nada) como la más "completa" (build_the_engine, por costar trece veces más sin necesidad).

Antes de avanzar deberías poder: explicar, de memoria, los dos pasos de pickTest() y el orden en que ocurren; y predecir, para una lista nueva de candidatos, cuál sería el elegido sin necesidad de ejecutar el código.

La lección 5 lleva la idea un paso más allá: no basta con que un test pueda refutar la hipótesis en teoría — tiene que estar diseñado, desde el primer borrador, con la intención de buscar la evidencia que te probaría equivocado, no la que confirma lo que ya crees. Vas a ver, con un candidato real, qué tan tentador es diseñar (sin darte cuenta) un test que parece riguroso pero en el fondo solo busca aplausos.

Recursos

  • Marty Cagan (Silicon Valley Product Group), "The Four Big Risks" — svpg.com/four-big-risks. El marco de los cuatro riesgos de producto (valor, usabilidad, factibilidad, viabilidad) que justifica por qué la suposición de recommendations —un riesgo de valor— es la que hay que probar primero, y barato. En inglés.
  • David J. Bland y Alexander Osterwalder, resumen de Testing Business Ideasstrategyzer.com/library/testing-business-ideas-book-summary. Un catálogo de decenas de tipos de experimento, cada uno con su costo relativo y su capacidad de generar evidencia — la fuente directa detrás de la lógica de pickTest(). En inglés.
  • Teresa Torres, "Assumption Testing: Everything You Need to Know to Get Started" — producttalk.org/assumption-testing. Sobre cómo elegir, entre varias formas de probar una suposición, la que da la señal más útil al menor costo — el mismo criterio que aplica pickTest() en código. En inglés.