💡 Developer Cookbook - FASE 7: Product Thinking
Recetas prácticas para crear productos exitosos centrados en el usuario
📚 Tabla de Contenidos
- Receta 7.1: Jobs-to-be-Done Framework
- Receta 7.2: Lean Canvas - Validar Ideas Rápido
- Receta 7.3: Métricas de Producto Pirata (AARRR)
- Receta 7.4: A/B Testing y Experimentación
- Receta 7.5: User Research - Descubrir Qué Construir
Receta 7.1: Jobs-to-be-Done Framework
¿Qué es? Framework que se enfoca en entender qué trabajo está tratando de hacer tu usuario, no qué características quiere. Los usuarios “contratan” productos para hacer un trabajo.
Concepto clave: Las personas no quieren un taladro de 1/4 pulgada, quieren un agujero de 1/4 pulgada (y realmente quieren colgar un cuadro).
Template de JTBD:
Cuando [situación],
quiero [motivación],
para poder [resultado esperado].
Casos de uso:
- Validar ideas de features antes de construir
- Entender por qué los usuarios abandonan
- Priorizar backlog basado en trabajos reales
Ejemplo práctico:
# ❌ MAL: Feature-driven thinking
features = [
"Dark mode",
"Export to PDF",
"Keyboard shortcuts",
"Mobile app"
]
# ✅ BIEN: Jobs-to-be-Done thinking
jobs_to_be_done = [
{
"situation": "Cuando trabajo de noche",
"motivation": "reducir la fatiga visual",
"outcome": "seguir siendo productivo sin dolor de cabeza",
"frequency": "Daily",
"current_solution": "F.lux app + reduce brillo",
"satisfaction": 3, # 1-10
"importance": 8,
"solution": "Dark mode"
},
{
"situation": "Cuando termino un análisis",
"motivation": "compartir hallazgos con stakeholders no-técnicos",
"outcome": "que tomen decisiones informadas rápidamente",
"frequency": "Weekly",
"current_solution": "Screenshots + manual formatting en PowerPoint",
"satisfaction": 2,
"importance": 10,
"solution": "Export to PDF con visualizaciones"
},
{
"situation": "Cuando analizo datos repetitivos",
"motivation": "acelerar mi flujo de trabajo",
"outcome": "ahorrar 30 minutos al día",
"frequency": "Daily",
"current_solution": "Usar mouse para todo",
"satisfaction": 5,
"importance": 6,
"solution": "Keyboard shortcuts"
}
]
# Calcular Opportunity Score = Importance + (Importance - Satisfaction)
def calculate_opportunity_score(jobs):
for job in jobs:
opportunity = job['importance'] + (job['importance'] - job['satisfaction'])
job['opportunity_score'] = opportunity
# Ordenar por opportunity score
return sorted(jobs, key=lambda x: x['opportunity_score'], reverse=True)
prioritized_jobs = calculate_opportunity_score(jobs_to_be_done)
print("🎯 Jobs priorizados por opportunity:")
for i, job in enumerate(prioritized_jobs, 1):
print(f"\n{i}. {job['solution']}")
print(f" Opportunity Score: {job['opportunity_score']}/20")
print(f" Job: {job['situation']}, {job['motivation']}")
print(f" Frecuencia: {job['frequency']}")
Output esperado:
🎯 Jobs priorizados por opportunity:
1. Export to PDF con visualizaciones
Opportunity Score: 18/20
Job: Cuando termino un análisis, compartir hallazgos...
Frecuencia: Weekly
2. Dark mode
Opportunity Score: 13/20
Job: Cuando trabajo de noche, reducir fatiga visual...
Frecuencia: Daily
3. Keyboard shortcuts
Opportunity Score: 7/20
Job: Cuando analizo datos repetitivos, acelerar flujo...
Frecuencia: Daily
Investigación de Jobs:
# Template de entrevista JTBD
interview_script = """
JTBD Interview Script
====================
Contexto:
---------
1. Cuéntame sobre la última vez que usaste [producto]
2. ¿Qué estabas tratando de lograr ese día?
3. ¿Por qué era importante para ti en ese momento?
Alternativas:
-------------
4. ¿Qué hacías antes de usar [producto]?
5. ¿Qué otras herramientas consideraste?
6. ¿Por qué no funcionaron esas alternativas?
Progreso:
---------
7. ¿Cómo sabes cuando has terminado exitosamente el trabajo?
8. ¿Qué te frena o hace más lento?
9. ¿Qué harías si [producto] dejara de existir mañana?
Fuerzas:
--------
10. ¿Qué te empuja a buscar una solución? (Push)
11. ¿Qué te atrae de esta solución? (Pull)
12. ¿Qué te hace dudar en adoptarla? (Anxiety)
13. ¿Por qué no cambias? (Habit)
"""
# Analizar respuestas
class JTBDAnalyzer:
def __init__(self):
self.jobs = []
def extract_job(self, interview_notes: dict) -> dict:
"""Extraer job de notas de entrevista"""
return {
'trigger': interview_notes.get('push'), # Qué causa el problema
'desired_outcome': interview_notes.get('pull'), # Qué busca lograr
'barriers': interview_notes.get('anxiety'), # Qué lo detiene
'current_habit': interview_notes.get('habit'), # Qué hace ahora
'success_criteria': interview_notes.get('done_when') # Cuándo está "done"
}
def find_patterns(self, jobs_list: list) -> dict:
"""Encontrar patrones comunes en múltiples entrevistas"""
from collections import Counter
all_triggers = [job['trigger'] for job in jobs_list]
all_outcomes = [job['desired_outcome'] for job in jobs_list]
return {
'common_triggers': Counter(all_triggers).most_common(3),
'common_outcomes': Counter(all_outcomes).most_common(3),
'total_interviews': len(jobs_list)
}
# Ejemplo de uso
analyzer = JTBDAnalyzer()
interview1 = {
'push': 'Tengo que presentar a ejecutivos mañana',
'pull': 'Quiero que vean insights en 5 minutos',
'anxiety': 'No sé si el PDF se verá bien',
'habit': 'Hago screenshots y pego en PowerPoint',
'done_when': 'El CEO dice "esto es exactamente lo que necesitaba"'
}
job = analyzer.extract_job(interview1)
print(f"✅ Job extraído: {job}")
Receta 7.2: Lean Canvas - Validar Ideas Rápido
¿Qué es? Template de 1 página para mapear las hipótesis clave de tu producto. Se completa en 20 minutos, te ayuda a identificar riesgos antes de escribir una línea de código.
9 bloques del Lean Canvas:
from dataclasses import dataclass
from typing import List
@dataclass
class LeanCanvas:
"""Lean Canvas para validación de producto"""
# Bloque 1: Problema
problem: List[str] # Top 3 problemas que resuelves
existing_alternatives: List[str] # Qué usa la gente hoy
# Bloque 2: Segmentos de clientes
customer_segments: List[str] # Early adopters específicos
# Bloque 3: Propuesta de valor única
unique_value_proposition: str # Mensaje claro en 1 línea
high_level_concept: str # Analogía (ej: "Uber para X")
# Bloque 4: Solución
solution: List[str] # Top 3 features que resuelven el problema
# Bloque 5: Canales
channels: List[str] # Cómo llegas a clientes
# Bloque 6: Estructura de ingresos
revenue_streams: List[dict] # Cómo monetizas
# Bloque 7: Estructura de costos
cost_structure: List[dict] # Costos fijos y variables
# Bloque 8: Métricas clave
key_metrics: List[str] # Números que importan
# Bloque 9: Ventaja injusta
unfair_advantage: str # Algo que no se puede copiar fácil
# Ejemplo: Herramienta de análisis de logs para DevOps
canvas = LeanCanvas(
problem=[
"Los logs están en 20 sistemas diferentes",
"Encontrar root cause toma horas",
"No hay alertas proactivas"
],
existing_alternatives=[
"Grep manual en cada servidor",
"Splunk (muy caro $50k/año)",
"ELK stack (requiere equipo dedicado)"
],
customer_segments=[
"Startups tech con 5-50 ingenieros",
"Que usan microservicios",
"Sin equipo de DevOps dedicado"
],
unique_value_proposition="Encuentra bugs en producción en <30 segundos",
high_level_concept="Google para logs de aplicación",
solution=[
"AI que correlaciona logs automáticamente",
"Setup en 5 minutos (1 línea de código)",
"Alertas inteligentes basadas en patrones"
],
channels=[
"Content marketing (blog posts técnicos)",
"GitHub sponsors + open source core",
"Community en Discord"
],
revenue_streams=[
{
'type': 'Freemium',
'free_tier': 'Hasta 1GB logs/día',
'paid_tier': '$49/mes por cada 10GB',
'enterprise': '$499/mes con SLA'
}
],
cost_structure=[
{'item': 'Cloud storage', 'monthly': '$500', 'variable': True},
{'item': 'Cloud compute', 'monthly': '$300', 'variable': True},
{'item': 'Founders salary', 'monthly': '$0', 'variable': False},
{'item': 'Domain + hosting', 'monthly': '$50', 'variable': False}
],
key_metrics=[
"Tiempo promedio para encontrar root cause",
"% de errores detectados automáticamente",
"NPS de usuarios paying",
"Churn rate mensual",
"CAC (Customer Acquisition Cost)"
],
unfair_advantage="Ex-ingenieros de Google con 10 años en observability"
)
# Visualizar canvas
def print_lean_canvas(canvas: LeanCanvas):
print("=" * 80)
print("LEAN CANVAS")
print("=" * 80)
print("\n📋 PROBLEMA:")
for i, p in enumerate(canvas.problem, 1):
print(f" {i}. {p}")
print(f"\n Alternativas actuales:")
for alt in canvas.existing_alternatives:
print(f" - {alt}")
print("\n👥 CUSTOMER SEGMENTS:")
for seg in canvas.customer_segments:
print(f" • {seg}")
print(f"\n🎯 UNIQUE VALUE PROPOSITION:")
print(f" {canvas.unique_value_proposition}")
print(f" Concepto: {canvas.high_level_concept}")
print("\n💡 SOLUCIÓN:")
for i, sol in enumerate(canvas.solution, 1):
print(f" {i}. {sol}")
print("\n📢 CANALES:")
for ch in canvas.channels:
print(f" • {ch}")
print("\n💰 REVENUE STREAMS:")
for stream in canvas.revenue_streams:
for k, v in stream.items():
print(f" {k}: {v}")
print("\n💸 COST STRUCTURE:")
total_fixed = sum(c['monthly'].replace('$', '').replace(',', '')
for c in canvas.cost_structure if not c['variable'])
print(f" Costos fijos: ~${total_fixed}/mes")
for cost in canvas.cost_structure:
var_label = "(variable)" if cost['variable'] else "(fijo)"
print(f" • {cost['item']}: {cost['monthly']} {var_label}")
print("\n📊 KEY METRICS:")
for metric in canvas.key_metrics:
print(f" • {metric}")
print(f"\n🔒 UNFAIR ADVANTAGE:")
print(f" {canvas.unfair_advantage}")
print("\n" + "=" * 80)
print_lean_canvas(canvas)
Proceso de validación:
class LeanCanvasValidator:
"""Validar hipótesis del canvas con experimentos rápidos"""
def __init__(self, canvas: LeanCanvas):
self.canvas = canvas
self.experiments = []
def add_experiment(self, hypothesis: str, test: str, criteria: str):
"""Agregar experimento de validación"""
self.experiments.append({
'hypothesis': hypothesis,
'test': test,
'success_criteria': criteria,
'status': 'Not Started',
'result': None
})
def get_validation_roadmap(self) -> List[dict]:
"""Orden sugerido de validación (más riesgoso primero)"""
# Priorizar por riesgo: Problema > Solución > Canales > Precio
roadmap = [
{
'phase': 'Phase 1: Problem Validation',
'goal': '¿El problema existe? ¿Es doloroso?',
'experiments': [
'Entrevistar a 10 potenciales usuarios',
'Observar cómo resuelven el problema hoy',
'Medir cuánto tiempo/$ pierden'
]
},
{
'phase': 'Phase 2: Solution Validation',
'goal': '¿Nuestra solución resuelve el problema?',
'experiments': [
'Landing page con descripción + email signup',
'Demo/mockup interactivo (no código real)',
'Pre-sell antes de construir (stripe checkout)'
]
},
{
'phase': 'Phase 3: Channel Validation',
'goal': '¿Podemos llegar a clientes a bajo costo?',
'experiments': [
'Publicar 5 blog posts técnicos',
'Medir tráfico orgánico',
'Calcular CAC con ads pequeños ($100)'
]
},
{
'phase': 'Phase 4: Pricing Validation',
'goal': '¿Cuánto están dispuestos a pagar?',
'experiments': [
'A/B test de 3 pricing tiers',
'Encuesta: "A qué precio sería caro/barato/razonable?"',
'Análisis de competencia'
]
}
]
return roadmap
# Ejemplo de uso
validator = LeanCanvasValidator(canvas)
# Agregar experimentos específicos
validator.add_experiment(
hypothesis="Los DevOps engineers gastan >2hrs/día buscando en logs",
test="Entrevistar a 10 DevOps de startups, pedirles que compartan su último incidente",
criteria="7+ de 10 confirman que gastan >2hrs en logs al menos 2x/semana"
)
validator.add_experiment(
hypothesis="Pagarían $49/mes por reducir debugging time de 2hrs a 30min",
test="Landing page con pricing + Stripe checkout real",
criteria="15+ signups en 2 semanas, 5+ completan checkout"
)
# Imprimir roadmap
roadmap = validator.get_validation_roadmap()
print("\n🗺️ VALIDATION ROADMAP\n")
for phase in roadmap:
print(f"{phase['phase']}")
print(f"Goal: {phase['goal']}")
print("Experiments:")
for exp in phase['experiments']:
print(f" ☐ {exp}")
print()
Receta 7.3: Métricas de Producto Pirata (AARRR)
¿Qué es? Framework de métricas que cubre todo el customer lifecycle: Acquisition → Activation → Retention → Revenue → Referral.
Métricas clave por etapa:
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional
import pandas as pd
@dataclass
class ProductMetrics:
"""Métricas AARRR para producto"""
# ACQUISITION: ¿Cómo llegan usuarios?
total_visitors: int
signup_rate: float # % de visitantes que se registran
channels: dict # Source de tráfico
# ACTIVATION: ¿Tienen una buena primera experiencia?
new_users: int
activated_users: int # Los que completan "aha moment"
activation_rate: float
time_to_activation: timedelta # Tiempo promedio
# RETENTION: ¿Vuelven?
dau: int # Daily Active Users
wau: int # Weekly Active Users
mau: int # Monthly Active Users
dau_mau_ratio: float # Stickiness
cohort_retention: dict # % que vuelven por cohorte
# REVENUE: ¿Pagan?
paying_customers: int
conversion_rate: float # Free → Paid
arpu: float # Average Revenue Per User
ltv: float # Lifetime Value
# REFERRAL: ¿Traen amigos?
viral_coefficient: float # Cuántos invita cada usuario
nps: int # Net Promoter Score (-100 a 100)
def calculate_aarrr_metrics(users_df: pd.DataFrame,
events_df: pd.DataFrame) -> ProductMetrics:
"""Calcular métricas AARRR desde datos raw"""
# ACQUISITION
total_visitors = users_df['visitor_id'].nunique()
signups = users_df[users_df['signed_up'] == True]
signup_rate = len(signups) / total_visitors
channels = users_df.groupby('source')['user_id'].count().to_dict()
# ACTIVATION (ejemplo: completar onboarding + primera acción)
activated = events_df[
(events_df['event_name'] == 'onboarding_completed') &
(events_df['timestamp'] <= events_df['signup_timestamp'] + timedelta(days=1))
]['user_id'].nunique()
activation_rate = activated / len(signups) if len(signups) > 0 else 0
# RETENTION
today = datetime.now().date()
dau = events_df[
events_df['timestamp'].dt.date == today
]['user_id'].nunique()
last_7_days = today - timedelta(days=7)
wau = events_df[
events_df['timestamp'].dt.date >= last_7_days
]['user_id'].nunique()
last_30_days = today - timedelta(days=30)
mau = events_df[
events_df['timestamp'].dt.date >= last_30_days
]['user_id'].nunique()
dau_mau = dau / mau if mau > 0 else 0
# REVENUE
paying = users_df[users_df['plan'] != 'free']['user_id'].nunique()
conversion_rate = paying / len(users_df) if len(users_df) > 0 else 0
total_revenue = users_df['total_spent'].sum()
arpu = total_revenue / len(users_df) if len(users_df) > 0 else 0
# LTV simplificado = ARPU / Churn Rate
# (asumiendo churn mensual del 5%)
ltv = arpu / 0.05
# REFERRAL
referrals = events_df[events_df['event_name'] == 'referral_sent']
viral_coef = len(referrals) / len(users_df) if len(users_df) > 0 else 0
# NPS simplificado
promoters = users_df[users_df['nps_score'] >= 9]['user_id'].count()
detractors = users_df[users_df['nps_score'] <= 6]['user_id'].count()
total_responses = users_df['nps_score'].notna().sum()
nps = ((promoters - detractors) / total_responses * 100) if total_responses > 0 else 0
return ProductMetrics(
total_visitors=total_visitors,
signup_rate=signup_rate,
channels=channels,
new_users=len(signups),
activated_users=activated,
activation_rate=activation_rate,
time_to_activation=timedelta(hours=2), # Placeholder
dau=dau,
wau=wau,
mau=mau,
dau_mau_ratio=dau_mau,
cohort_retention={}, # Placeholder
paying_customers=paying,
conversion_rate=conversion_rate,
arpu=arpu,
ltv=ltv,
viral_coefficient=viral_coef,
nps=int(nps)
)
# Dashboard de métricas
def print_metrics_dashboard(metrics: ProductMetrics):
"""Pretty print de métricas AARRR"""
print("\n" + "=" * 80)
print("📊 PRODUCT METRICS DASHBOARD (AARRR)")
print("=" * 80)
print("\n🎯 ACQUISITION - ¿Cómo llegan?")
print(f" Total Visitors: {metrics.total_visitors:,}")
print(f" Signup Rate: {metrics.signup_rate:.1%}")
print(f" Top Channels:")
for channel, count in sorted(metrics.channels.items(),
key=lambda x: x[1],
reverse=True)[:3]:
print(f" • {channel}: {count:,} users")
print("\n✨ ACTIVATION - ¿Tienen buena primera experiencia?")
print(f" New Users: {metrics.new_users:,}")
print(f" Activated: {metrics.activated_users:,}")
print(f" Activation Rate: {metrics.activation_rate:.1%}")
print(f" {'🟢 GOOD' if metrics.activation_rate > 0.40 else '🔴 BAD'} (target: >40%)")
print("\n🔄 RETENTION - ¿Vuelven?")
print(f" DAU: {metrics.dau:,}")
print(f" WAU: {metrics.wau:,}")
print(f" MAU: {metrics.mau:,}")
print(f" DAU/MAU: {metrics.dau_mau_ratio:.1%}")
print(f" {'🟢 STICKY' if metrics.dau_mau_ratio > 0.20 else '🔴 NOT STICKY'} (target: >20%)")
print("\n💰 REVENUE - ¿Pagan?")
print(f" Paying Customers: {metrics.paying_customers:,}")
print(f" Conversion Rate: {metrics.conversion_rate:.1%}")
print(f" ARPU: ${metrics.arpu:.2f}")
print(f" LTV: ${metrics.ltv:.2f}")
print("\n📣 REFERRAL - ¿Traen amigos?")
print(f" Viral Coefficient: {metrics.viral_coefficient:.2f}")
print(f" {'🟢 VIRAL!' if metrics.viral_coefficient > 1 else '🟡 GROWING'} (target: >1.0)")
print(f" NPS: {metrics.nps}")
print(f" {'🟢 EXCELLENT' if metrics.nps > 50 else '🟡 GOOD' if metrics.nps > 0 else '🔴 BAD'}")
print("\n" + "=" * 80)
# Ejemplo de uso con datos simulados
import numpy as np
# Simular datos de usuarios
users_data = {
'visitor_id': range(1000),
'user_id': range(500), # 50% signup rate
'signed_up': [True]*500 + [False]*500,
'source': np.random.choice(['organic', 'paid', 'referral'], 1000, p=[0.5, 0.3, 0.2]),
'plan': np.random.choice(['free', 'paid'], 500, p=[0.85, 0.15]),
'total_spent': np.random.gamma(2, 25, 500),
'nps_score': np.random.choice(range(11), 500)
}
users_df = pd.DataFrame(users_data)
# Simular eventos
events_data = {
'user_id': np.random.choice(range(500), 5000),
'event_name': np.random.choice([
'page_view', 'onboarding_completed', 'feature_used', 'referral_sent'
], 5000, p=[0.6, 0.1, 0.25, 0.05]),
'timestamp': pd.date_range(end=datetime.now(), periods=5000, freq='H'),
'signup_timestamp': pd.date_range(end=datetime.now()-timedelta(days=30), periods=5000, freq='H')
}
events_df = pd.DataFrame(events_data)
# Calcular y mostrar métricas
metrics = calculate_aarrr_metrics(users_df, events_df)
print_metrics_dashboard(metrics)
Cohort Analysis:
def cohort_retention_analysis(users_df: pd.DataFrame,
events_df: pd.DataFrame) -> pd.DataFrame:
"""Analizar retención por cohorte (mes de signup)"""
# Agregar mes de signup
users_df['signup_month'] = pd.to_datetime(users_df['signup_date']).dt.to_period('M')
# Merge con eventos
df = events_df.merge(users_df[['user_id', 'signup_month']], on='user_id')
df['event_month'] = pd.to_datetime(df['timestamp']).dt.to_period('M')
# Calcular "months since signup"
df['months_since_signup'] = (df['event_month'] - df['signup_month']).apply(lambda x: x.n)
# Crear cohort table
cohort_data = df.groupby(['signup_month', 'months_since_signup'])['user_id'].nunique().reset_index()
cohort_pivot = cohort_data.pivot(index='signup_month',
columns='months_since_signup',
values='user_id')
# Calcular % de retención relativo al mes 0
cohort_size = cohort_pivot[0]
retention = cohort_pivot.divide(cohort_size, axis=0) * 100
return retention
# Ejemplo de visualización
def print_cohort_table(retention_df: pd.DataFrame):
"""Imprimir tabla de retención por cohorte"""
print("\n📅 COHORT RETENTION ANALYSIS")
print(" (% de usuarios que vuelven cada mes)\n")
print(retention_df.round(1).to_string())
# Calcular retention promedio por mes
avg_retention = retention_df.mean()
print("\n📊 Average Retention by Month:")
for month, rate in avg_retention.items():
print(f" Month {month}: {rate:.1f}%")
# Ejemplo de output:
"""
📅 COHORT RETENTION ANALYSIS
(% de usuarios que vuelven cada mes)
signup_month 0 1 2 3 4
2024-01 100.0 45.2 32.1 28.3 25.7
2024-02 100.0 48.3 35.4 30.1 -
2024-03 100.0 46.7 33.8 - -
2024-04 100.0 44.1 - - -
📊 Average Retention by Month:
Month 0: 100.0%
Month 1: 46.1%
Month 2: 33.8%
Month 3: 29.2%
Month 4: 25.7%
"""
Receta 7.4: A/B Testing y Experimentación
¿Qué es? Método científico para comparar dos versiones (A vs B) y determinar cuál funciona mejor usando datos, no opiniones.
Cuándo usar A/B testing:
- ✅ Cambios en UI/UX (botón color, copy, layout)
- ✅ Pricing changes
- ✅ Onboarding flows
- ✅ Email subject lines
- ❌ Cambios muy pequeños (no suficiente tráfico)
- ❌ Decisiones de negocio (mejor Lean Canvas)
Ejemplo práctico:
from scipy import stats
import numpy as np
from typing import Tuple
class ABTest:
"""Framework para correr A/B tests con significancia estadística"""
def __init__(self, name: str, hypothesis: str):
self.name = name
self.hypothesis = hypothesis
self.variant_a = {'name': 'Control', 'conversions': 0, 'visitors': 0}
self.variant_b = {'name': 'Treatment', 'conversions': 0, 'visitors': 0}
def add_data(self, variant: str, conversions: int, visitors: int):
"""Agregar datos de conversión"""
if variant == 'A':
self.variant_a['conversions'] = conversions
self.variant_a['visitors'] = visitors
else:
self.variant_b['conversions'] = conversions
self.variant_b['visitors'] = visitors
def calculate_conversion_rate(self, variant: dict) -> float:
"""Calcular conversion rate"""
if variant['visitors'] == 0:
return 0.0
return variant['conversions'] / variant['visitors']
def calculate_significance(self) -> Tuple[float, bool]:
"""Calcular significancia estadística con Chi-squared test"""
# Crear contingency table
observed = np.array([
[self.variant_a['conversions'],
self.variant_a['visitors'] - self.variant_a['conversions']],
[self.variant_b['conversions'],
self.variant_b['visitors'] - self.variant_b['conversions']]
])
# Chi-squared test
chi2, p_value, dof, expected = stats.chi2_contingency(observed)
# Significativo si p < 0.05 (95% confidence)
is_significant = p_value < 0.05
return p_value, is_significant
def calculate_sample_size(self, baseline_rate: float,
minimum_detectable_effect: float,
alpha: float = 0.05,
power: float = 0.80) -> int:
"""Calcular sample size necesario ANTES de correr test"""
# Effect size (Cohen's h)
p1 = baseline_rate
p2 = baseline_rate * (1 + minimum_detectable_effect)
effect_size = 2 * (np.arcsin(np.sqrt(p2)) - np.arcsin(np.sqrt(p1)))
# Z-scores
z_alpha = stats.norm.ppf(1 - alpha/2)
z_beta = stats.norm.ppf(power)
# Sample size per variant
n = ((z_alpha + z_beta) / effect_size) ** 2
return int(np.ceil(n))
def print_results(self):
"""Imprimir resultados del test"""
rate_a = self.calculate_conversion_rate(self.variant_a)
rate_b = self.calculate_conversion_rate(self.variant_b)
lift = ((rate_b - rate_a) / rate_a * 100) if rate_a > 0 else 0
p_value, is_significant = self.calculate_significance()
print("\n" + "=" * 70)
print(f"🧪 A/B TEST RESULTS: {self.name}")
print("=" * 70)
print(f"Hypothesis: {self.hypothesis}\n")
print(f"📊 VARIANT A (Control):")
print(f" Visitors: {self.variant_a['visitors']:,}")
print(f" Conversions: {self.variant_a['conversions']:,}")
print(f" Conversion Rate: {rate_a:.2%}\n")
print(f"📊 VARIANT B (Treatment):")
print(f" Visitors: {self.variant_b['visitors']:,}")
print(f" Conversions: {self.variant_b['conversions']:,}")
print(f" Conversion Rate: {rate_b:.2%}\n")
print(f"📈 RESULTS:")
print(f" Lift: {lift:+.2f}%")
print(f" P-value: {p_value:.4f}")
print(f" Statistical Significance: {'YES ✅' if is_significant else 'NO ❌'}")
print(f" Confidence: {(1-p_value)*100:.1f}%")
if is_significant:
winner = 'B' if rate_b > rate_a else 'A'
print(f"\n🏆 WINNER: Variant {winner}")
print(f" Recommendation: {'Deploy variant B' if winner == 'B' else 'Keep variant A'}")
else:
print(f"\n⚠️ NOT SIGNIFICANT")
print(f" Recommendation: Run test longer or redesign experiment")
print("=" * 70)
# Ejemplo 1: Test de color de botón
button_test = ABTest(
name="CTA Button Color",
hypothesis="Green button will increase signups by 10%+ vs blue button"
)
# Simular una semana de datos
button_test.add_data('A', conversions=145, visitors=5000) # Blue button: 2.9%
button_test.add_data('B', conversions=178, visitors=5000) # Green button: 3.56%
button_test.print_results()
# Ejemplo 2: Test de pricing page
pricing_test = ABTest(
name="Pricing Page Redesign",
hypothesis="Simplified pricing (3 tiers vs 5 tiers) increases conversions"
)
pricing_test.add_data('A', conversions=89, visitors=3200) # 5 tiers: 2.78%
pricing_test.add_data('B', conversions=128, visitors=3200) # 3 tiers: 4.00%
pricing_test.print_results()
# Calcular sample size necesario ANTES de test
baseline = 0.03 # 3% conversion actual
mde = 0.10 # Queremos detectar 10% de lift mínimo
sample_size = button_test.calculate_sample_size(baseline, mde)
print(f"\n📐 Sample Size Calculator:")
print(f" Baseline: {baseline:.1%}")
print(f" Minimum Detectable Effect: {mde:.1%}")
print(f" Sample needed per variant: {sample_size:,} visitors")
print(f" Total sample needed: {sample_size*2:,} visitors")
Errores comunes en A/B testing:
class ABTestingMistakes:
"""Errores comunes y cómo evitarlos"""
@staticmethod
def mistake_1_peeking():
"""❌ MISTAKE: Revisar resultados cada hora y detener cuando parece significativo"""
print("❌ MISTAKE #1: Peeking / Early Stopping")
print(" Problema: Aumenta false positives")
print(" ✅ Fix: Define duración mínima (1-2 semanas) y respétala")
print(" ✅ Fix: Usa Sequential Testing si necesitas detener early\n")
@staticmethod
def mistake_2_multiple_tests():
"""❌ MISTAKE: Correr 10 tests simultáneos y celebrar el que gane"""
print("❌ MISTAKE #2: Multiple Testing Without Bonferroni Correction")
print(" Problema: Con 10 tests, ~40% chance de false positive")
print(" ✅ Fix: Usa p-value ajustado = 0.05 / num_tests")
print(" Ejemplo: Con 5 tests, usa p < 0.01 en vez de p < 0.05\n")
@staticmethod
def mistake_3_small_sample():
"""❌ MISTAKE: Correr test con 100 visitantes y declarar ganador"""
print("❌ MISTAKE #3: Insufficient Sample Size")
print(" Problema: No tienes statistical power para detectar efectos reales")
print(" ✅ Fix: Calcula sample size ANTES con calculate_sample_size()")
print(" Regla práctica: Mínimo 350-400 conversiones por variant\n")
@staticmethod
def mistake_4_choosing_metrics():
"""❌ MISTAKE: Cambiar la métrica después de ver resultados"""
print("❌ MISTAKE #4: HARKing (Hypothesizing After Results Known)")
print(" Ejemplo malo: 'No ganó en signups, pero mejoró time on page!'")
print(" ✅ Fix: Define primary metric ANTES de iniciar test")
print(" ✅ Fix: Secondary metrics son informativas, no decisivas\n")
@staticmethod
def print_all_mistakes():
print("\n" + "🚨 " * 20)
print("COMMON A/B TESTING MISTAKES")
print("🚨 " * 20 + "\n")
ABTestingMistakes.mistake_1_peeking()
ABTestingMistakes.mistake_2_multiple_tests()
ABTestingMistakes.mistake_3_small_sample()
ABTestingMistakes.mistake_4_choosing_metrics()
ABTestingMistakes.print_all_mistakes()
Análisis de resultados:
class ABTestAnalyzer:
"""Analizar resultados de múltiples tests"""
def __init__(self):
self.tests = []
def add_test_result(self, name: str, variant_a_rate: float,
variant_b_rate: float, p_value: float):
self.tests.append({
'name': name,
'variant_a': variant_a_rate,
'variant_b': variant_b_rate,
'lift': ((variant_b_rate - variant_a_rate) / variant_a_rate * 100),
'p_value': p_value,
'significant': p_value < 0.05
})
def print_summary(self):
"""Resumen de todos los tests"""
print("\n📊 A/B TESTING SUMMARY")
print("=" * 80)
for test in self.tests:
status = "✅ WINNER" if test['significant'] and test['lift'] > 0 else \
"❌ LOSER" if test['significant'] and test['lift'] < 0 else \
"⚪ NO RESULT"
print(f"\n{test['name']}")
print(f" A: {test['variant_a']:.2%} → B: {test['variant_b']:.2%}")
print(f" Lift: {test['lift']:+.1f}% | p={test['p_value']:.4f} | {status}")
# Stats agregadas
total_tests = len(self.tests)
significant = sum(1 for t in self.tests if t['significant'])
winners = sum(1 for t in self.tests if t['significant'] and t['lift'] > 0)
print(f"\n{'=' * 80}")
print(f"Total Tests: {total_tests}")
print(f"Statistically Significant: {significant} ({significant/total_tests:.0%})")
print(f"Winners (positive lift): {winners}")
print(f"Win Rate: {winners/total_tests:.0%}")
# Ejemplo de uso
analyzer = ABTestAnalyzer()
analyzer.add_test_result("Button Color", 0.029, 0.0356, 0.0234)
analyzer.add_test_result("Pricing Tiers", 0.0278, 0.0400, 0.0012)
analyzer.add_test_result("Headline Copy", 0.0310, 0.0315, 0.7821)
analyzer.add_test_result("Onboarding Steps", 0.450, 0.520, 0.0089)
analyzer.print_summary()
Receta 7.5: User Research - Descubrir Qué Construir
¿Qué es? Hablar con usuarios reales para descubrir problemas, validar ideas y evitar construir cosas que nadie quiere.
Tipos de research:
from dataclasses import dataclass
from typing import List
@dataclass
class UserResearchMethod:
"""Métodos de investigación de usuarios"""
name: str
when_to_use: str
participants: int
duration: str
cost: str
insights: str
# Catálogo de métodos
research_methods = [
UserResearchMethod(
name="User Interviews (Qualitative)",
when_to_use="Descubrir problemas, validar hipótesis, entender el 'por qué'",
participants=5,
duration="30-60 min por entrevista",
cost="$0 (si reclutas tú) o $50-100/participante",
insights="Profundos pero no cuantificables"
),
UserResearchMethod(
name="Surveys (Quantitative)",
when_to_use="Validar hallazgos de interviews a escala, priorizar features",
participants=100,
duration="5-10 min survey",
cost="$0 (email a usuarios) o $1-3/respuesta",
insights="Cuantificables pero superficiales"
),
UserResearchMethod(
name="Usability Testing",
when_to_use="Encontrar problemas de UX en prototipo o producto existente",
participants=5,
duration="45-60 min",
cost="$50-100/participante",
insights="Específicos sobre qué no funciona en UI"
),
UserResearchMethod(
name="Analytics Analysis",
when_to_use="Entender qué hacen usuarios (no por qué)",
participants=0, # Todos los usuarios
duration="Continuo",
cost="$0 (Google Analytics) a $$$",
insights="Qué pasa, no por qué pasa"
),
UserResearchMethod(
name="Diary Studies",
when_to_use="Entender uso en contexto real durante semanas",
participants=10,
duration="1-2 semanas",
cost="$100-200/participante",
insights="Comportamiento real vs comportamiento reportado"
)
]
def print_research_methods():
"""Imprimir guía de métodos"""
print("\n📚 USER RESEARCH METHODS GUIDE\n")
for i, method in enumerate(research_methods, 1):
print(f"{i}. {method.name}")
print(f" When: {method.when_to_use}")
print(f" n={method.participants} | {method.duration} | {method.cost}")
print(f" Insights: {method.insights}\n")
print_research_methods()
Script de entrevista:
class UserInterviewScript:
"""Template para entrevistas efectivas"""
@staticmethod
def introduction():
return """
INTRO (5 min)
=============
Hola [nombre], gracias por tu tiempo.
Te cuento qué vamos a hacer hoy:
- Voy a hacerte preguntas sobre [tema]
- No hay respuestas correctas o incorrectas
- Esto NO es una venta, solo quiero aprender
- ¿Puedo grabar? (solo para mis notas, nadie más escuchará)
¿Alguna pregunta antes de empezar?
"""
@staticmethod
def problem_discovery():
return """
PROBLEM DISCOVERY (15 min)
===========================
1. Cuéntame sobre tu día típico de trabajo
→ Follow-up: ¿Qué herramientas usas?
2. ¿Cuál es la parte más frustrante de [proceso]?
→ Follow-up: ¿Por qué es frustrante?
→ Follow-up: ¿Cuánto tiempo pierdes en esto?
3. Cuéntame sobre la última vez que [problema ocurrió]
→ Follow-up: ¿Qué trataste de hacer?
→ Follow-up: ¿Funcionó? ¿Por qué sí/no?
4. Si tuvieras una varita mágica, ¿qué cambiarías de [proceso]?
🚨 EVITAR: "¿Te gustaría tener [feature]?" ← Esto contamina la respuesta
✅ MEJOR: "¿Cómo resuelves [problema] hoy?"
"""
@staticmethod
def solution_validation():
return """
SOLUTION VALIDATION (15 min)
=============================
5. Te voy a mostrar una idea que tenemos...
[Muestra prototipo/mockup/descripción]
6. ¿Qué es lo primero que viene a tu mente?
7. ¿Cómo usarías esto en tu trabajo?
→ Follow-up: ¿En qué situación específica?
8. ¿Qué te confunde de esto?
9. En escala 1-10, ¿qué tan útil sería esto?
→ Follow-up: ¿Qué tendría que tener para ser un 10?
10. ¿Pagarías por esto? ¿Cuánto?
→ Follow-up: ¿Por qué ese precio?
"""
@staticmethod
def wrap_up():
return """
WRAP-UP (5 min)
===============
11. ¿Hay algo que no te pregunté que crees que debería saber?
12. ¿Conoces a alguien más que tenga [problema]?
→ Ask for referral
Muchas gracias por tu tiempo. ¿Te puedo contactar si tengo
preguntas de follow-up?
"""
@staticmethod
def print_full_script():
print("\n" + "=" * 70)
print("🎤 USER INTERVIEW SCRIPT")
print("=" * 70)
print(UserInterviewScript.introduction())
print(UserInterviewScript.problem_discovery())
print(UserInterviewScript.solution_validation())
print(UserInterviewScript.wrap_up())
UserInterviewScript.print_full_script()
Analizar respuestas:
class InterviewAnalyzer:
"""Analizar múltiples entrevistas para encontrar patrones"""
def __init__(self):
self.interviews = []
def add_interview(self, participant: str, notes: dict):
"""Agregar notas de una entrevista"""
self.interviews.append({
'participant': participant,
'pain_points': notes.get('pain_points', []),
'current_solution': notes.get('current_solution', ''),
'willingness_to_pay': notes.get('price', 0),
'usefulness_score': notes.get('score', 0),
'quote': notes.get('memorable_quote', '')
})
def find_patterns(self):
"""Encontrar patrones en entrevistas"""
from collections import Counter
# Consolidar pain points
all_pains = []
for interview in self.interviews:
all_pains.extend(interview['pain_points'])
pain_frequency = Counter(all_pains)
# Calcular willingness to pay promedio
avg_wtp = sum(i['willingness_to_pay'] for i in self.interviews) / len(self.interviews)
# Calcular usefulness promedio
avg_score = sum(i['usefulness_score'] for i in self.interviews) / len(self.interviews)
return {
'common_pains': pain_frequency.most_common(5),
'avg_willingness_to_pay': avg_wtp,
'avg_usefulness': avg_score,
'total_interviews': len(self.interviews)
}
def print_insights(self):
"""Imprimir insights encontrados"""
patterns = self.find_patterns()
print("\n" + "=" * 70)
print(f"🔍 INTERVIEW INSIGHTS ({patterns['total_interviews']} interviews)")
print("=" * 70)
print("\n🔥 TOP PAIN POINTS:")
for pain, count in patterns['common_pains']:
percentage = (count / patterns['total_interviews']) * 100
print(f" • {pain}")
print(f" Mentioned by {count}/{patterns['total_interviews']} ({percentage:.0f}%)")
print(f"\n💰 PRICING INSIGHTS:")
print(f" Average willingness to pay: ${patterns['avg_willingness_to_pay']:.2f}")
print(f" Range: ${min(i['willingness_to_pay'] for i in self.interviews):.0f} - "
f"${max(i['willingness_to_pay'] for i in self.interviews):.0f}")
print(f"\n⭐ USEFULNESS SCORE:")
print(f" Average: {patterns['avg_usefulness']:.1f}/10")
print("\n💬 MEMORABLE QUOTES:")
for interview in self.interviews[:3]: # Top 3
if interview['quote']:
print(f" \"{interview['quote']}\"")
print(f" — {interview['participant']}\n")
print("=" * 70)
# Ejemplo de uso
analyzer = InterviewAnalyzer()
analyzer.add_interview("Sarah (DevOps Lead)", {
'pain_points': ['Logs scattered across tools', 'Takes hours to debug', 'No alerts'],
'current_solution': 'Grep + Slack notifications',
'price': 75,
'score': 9,
'memorable_quote': 'I would pay $100/mo if it saves me 5 hours/week'
})
analyzer.add_interview("Mike (Backend Engineer)", {
'pain_points': ['Logs scattered across tools', 'Hard to correlate events', 'Too many false alerts'],
'current_solution': 'Datadog (too expensive)',
'price': 50,
'score': 8,
'memorable_quote': 'We pay $2k/month for Datadog but only use 10% of features'
})
analyzer.add_interview("Jessica (CTO)", {
'pain_points': ['Takes hours to debug', 'Team spends too much time on incidents'],
'current_solution': 'Custom scripts + spreadsheets',
'price': 100,
'score': 7,
'memorable_quote': 'If this works, I would buy it for the whole team immediately'
})
analyzer.print_insights()