Pruebas de Hipótesis¶
Una prueba de hipótesis es un procedimiento formal para tomar decisiones basadas en datos. Nos permite responder preguntas del tipo: ¿es este efecto real o producto del azar? Este notebook cubre el marco conceptual completo y las pruebas más utilizadas en la práctica.
Librerías¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as stats
from statsmodels.stats.weightstats import ztest
from statsmodels.stats.weightstats import ttest_ind as sm_ttest_ind
plt.rcParams['figure.figsize'] = (9, 4)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
np.random.seed(42)
1. Marco Conceptual¶
1.1 Hipótesis nula e hipótesis alternativa¶
Toda prueba de hipótesis parte de dos afirmaciones en competencia:
| Hipótesis | Símbolo | Descripción |
|---|---|---|
| Nula | $H_0$ | El efecto no existe / el parámetro toma un valor específico. Es lo que asumimos por defecto. |
| Alternativa | $H_1$ | El efecto existe / el parámetro difiere del valor bajo $H_0$. Es lo que queremos demostrar. |
Ejemplo: una empresa quiere saber si una nueva campaña aumentó las ventas promedio por cliente de $500.
$$H_0: \mu = 500 \qquad H_1: \mu > 500$$
💡 Regla: $H_0$ siempre contiene el signo de igualdad ($=$). $H_1$ puede ser bilateral ($\neq$) o unilateral ($>$ o $<$).
1.2 Tipos de prueba¶
La forma de $H_1$ determina dónde está la región de rechazo:
| Tipo | $H_1$ | Región de rechazo |
|---|---|---|
| Bilateral | $\mu \neq \mu_0$ | Ambas colas |
| Unilateral derecha | $\mu > \mu_0$ | Cola derecha |
| Unilateral izquierda | $\mu < \mu_0$ | Cola izquierda |
# Visualización de los tres tipos de prueba
x = np.linspace(-4, 4, 400)
alpha = 0.05
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
titles = ['Bilateral ($H_1: \\mu \\neq \\mu_0$)',
'Unilateral derecha ($H_1: \\mu > \\mu_0$)',
'Unilateral izquierda ($H_1: \\mu < \\mu_0$)']
configs = [
(stats.norm.ppf(alpha/2), stats.norm.ppf(1-alpha/2), True, True),
(None, stats.norm.ppf(1-alpha), False, True),
(stats.norm.ppf(alpha), None, True, False),
]
for ax, title, (z_low, z_high, shade_left, shade_right) in zip(axes, titles, configs):
ax.plot(x, stats.norm.pdf(x), 'steelblue', lw=2)
if shade_left and z_low is not None:
x_fill = x[x <= z_low]
ax.fill_between(x_fill, stats.norm.pdf(x_fill), color='salmon', alpha=0.6, label='Rechazo')
if shade_right and z_high is not None:
x_fill = x[x >= z_high]
ax.fill_between(x_fill, stats.norm.pdf(x_fill), color='salmon', alpha=0.6)
ax.set_title(title, fontsize=10)
ax.set_xlabel('z')
ax.set_ylabel('Densidad' if ax == axes[0] else '')
axes[0].legend(fontsize=9)
plt.suptitle(f'Regiones de rechazo para α = {alpha}', y=1.02)
plt.tight_layout()
plt.show()
2. Errores Tipo I y Tipo II¶
Al tomar una decisión basada en datos, podemos equivocarnos de dos maneras:
| $H_0$ es verdadera | $H_0$ es falsa | |
|---|---|---|
| No rechazar $H_0$ | ✅ Decisión correcta | ❌ Error Tipo II ($\beta$) |
| Rechazar $H_0$ | ❌ Error Tipo I ($\alpha$) | ✅ Decisión correcta |
- Error Tipo I ($\alpha$): falsa alarma. Concluimos que hay efecto cuando no lo hay. Se controla eligiendo el nivel de significancia.
- Error Tipo II ($\beta$): efecto perdido. No detectamos un efecto que sí existe.
- Potencia ($1-\beta$): probabilidad de detectar un efecto real.
⚠️ Existe un trade-off: reducir $\alpha$ (ser más exigente) aumenta $\beta$ (perdemos más efectos reales). El balance depende del costo relativo de cada error.
Ejemplo práctico:
- Test médico: el error Tipo I (falso positivo) genera tratamiento innecesario; el error Tipo II (falso negativo) deja enfermedades sin diagnosticar.
- Control de calidad: el error Tipo I rechaza producto bueno; el error Tipo II aprueba producto defectuoso.
3. Valor-p e Interpretación¶
El valor-p (p-value) es la probabilidad de obtener un resultado tan extremo o más extremo que el observado, asumiendo que $H_0$ es verdadera.
$$p\text{-value} = P(\text{dato tan extremo o más} \mid H_0 \text{ verdadera})$$
Regla de decisión:
- Si $p\text{-value} < \alpha$ → rechazamos $H_0$
- Si $p\text{-value} \geq \alpha$ → no rechazamos $H_0$
¿Qué NO significa el valor-p?¶
| Interpretación incorrecta | Interpretación correcta |
|---|---|
| Probabilidad de que $H_0$ sea verdadera | Probabilidad de los datos dado $H_0$ |
| Magnitud del efecto | Solo evidencia contra $H_0$ |
| p < 0.05 implica resultado importante | Significancia estadística ≠ relevancia práctica |
💡 Un valor-p muy pequeño solo dice que el efecto es detectable con los datos disponibles. Con muestras muy grandes, efectos triviales pueden ser estadísticamente significativos.
# Visualización del valor-p en una prueba bilateral
z_obs = 2.1 # estadístico observado
x = np.linspace(-4, 4, 400)
fig, ax = plt.subplots()
ax.plot(x, stats.norm.pdf(x), 'steelblue', lw=2)
# Zona del p-value (bilateral)
for lado in [1, -1]:
x_fill = x[np.abs(x) >= np.abs(z_obs)] if lado == 1 else x[x <= -np.abs(z_obs)]
x_fill = x[x >= np.abs(z_obs)] if lado == 1 else x[x <= -np.abs(z_obs)]
ax.fill_between(x_fill, stats.norm.pdf(x_fill), color='salmon', alpha=0.7)
ax.axvline(z_obs, color='red', linestyle='--', lw=1.5, label=f'z observado = {z_obs}')
ax.axvline(-z_obs, color='red', linestyle='--', lw=1.5)
p_value = 2 * (1 - stats.norm.cdf(np.abs(z_obs)))
ax.set_title(f'Valor-p bilateral = {p_value:.4f} → {"Rechazar H₀" if p_value < 0.05 else "No rechazar H₀"} (α=0.05)')
ax.set_xlabel('z')
ax.set_ylabel('Densidad')
ax.legend()
plt.tight_layout()
plt.show()
4. Prueba z (una muestra)¶
Se usa cuando queremos contrastar la media de una muestra contra un valor de referencia $\mu_0$, y conocemos $\sigma$ (o $n$ es grande).
$$z = \frac{\bar{x} - \mu_0}{\sigma / \sqrt{n}}$$
Bajo $H_0$, este estadístico sigue una distribución $\mathcal{N}(0,1)$.
Ejemplo: ¿El tiempo promedio de atención de una sucursal (muestra de 60 clientes, $\bar{x}=8.2$ min, $\sigma=2$ min conocida) es distinto del estándar de 8 minutos?
# Datos simulados consistentes con el ejemplo
np.random.seed(7)
muestra = np.random.normal(loc=8.2, scale=2, size=60)
mu_0 = 8.0
sigma = 2.0
# Cálculo manual
z_stat = (muestra.mean() - mu_0) / (sigma / np.sqrt(len(muestra)))
p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))
print("=== Prueba z (una muestra) ===")
print(f"Media muestral: {muestra.mean():.4f}")
print(f"μ₀ (bajo H₀): {mu_0}")
print(f"Estadístico z: {z_stat:.4f}")
print(f"Valor-p: {p_value:.4f}")
print(f"Decisión (α=0.05): {'Rechazar H₀' if p_value < 0.05 else 'No rechazar H₀'}")
# Con statsmodels
z_stat_sm, p_value_sm = ztest(muestra, value=mu_0, alternative='two-sided')
print("=== Prueba z con statsmodels ===")
print(f"Estadístico z: {z_stat_sm:.4f}")
print(f"Valor-p: {p_value_sm:.4f}")
5. Prueba t (una y dos muestras)¶
En la práctica, casi nunca conocemos $\sigma$. La prueba t reemplaza $\sigma$ por $s$ (desviación estándar muestral) y usa la distribución t-Student.
5.1 Prueba t de una muestra¶
$$t = \frac{\bar{x} - \mu_0}{s / \sqrt{n}} \sim t_{n-1}$$
Ejemplo: ¿El gasto promedio mensual de una muestra de 25 clientes ($\bar{x}=\$520$, $s=\$80$) es significativamente distinto de $\$500$?
np.random.seed(10)
muestra = np.random.normal(loc=520, scale=80, size=25)
mu_0 = 500
# scipy
t_stat, p_value = stats.ttest_1samp(muestra, popmean=mu_0)
print("=== Prueba t (una muestra) ===")
print(f"Media muestral: {muestra.mean():.2f}")
print(f"Desv. estándar: {muestra.std(ddof=1):.2f}")
print(f"Estadístico t: {t_stat:.4f}")
print(f"Grados de libertad: {len(muestra)-1}")
print(f"Valor-p: {p_value:.4f}")
print(f"Decisión (α=0.05): {'Rechazar H₀' if p_value < 0.05 else 'No rechazar H₀'}")
5.2 Prueba t de dos muestras independientes¶
Compara las medias de dos grupos distintos.
$$H_0: \mu_1 = \mu_2 \qquad H_1: \mu_1 \neq \mu_2$$
$$t = \frac{\bar{x}_1 - \bar{x}_2}{\sqrt{\frac{s_1^2}{n_1} + \frac{s_2^2}{n_2}}}$$
Ejemplo: ¿El tiempo de entrega promedio es igual para dos proveedores? Proveedor A: 30 entregas, Proveedor B: 35 entregas.
np.random.seed(21)
grupo_a = np.random.normal(loc=5.2, scale=1.0, size=30) # días promedio proveedor A
grupo_b = np.random.normal(loc=5.8, scale=1.2, size=35) # días promedio proveedor B
# scipy (Welch: no asume varianzas iguales)
t_stat, p_value = stats.ttest_ind(grupo_a, grupo_b, equal_var=False)
print("=== Prueba t (dos muestras independientes — Welch) ===")
print(f"Media Proveedor A: {grupo_a.mean():.3f} días")
print(f"Media Proveedor B: {grupo_b.mean():.3f} días")
print(f"Diferencia: {grupo_a.mean() - grupo_b.mean():.3f} días")
print(f"Estadístico t: {t_stat:.4f}")
print(f"Valor-p: {p_value:.4f}")
print(f"Decisión (α=0.05): {'Rechazar H₀' if p_value < 0.05 else 'No rechazar H₀'}")
# Con statsmodels (entrega además el intervalo de confianza de la diferencia)
t_stat_sm, p_value_sm, df_sm = sm_ttest_ind(grupo_a, grupo_b, alternative='two-sided', usevar='unequal')
print("=== Prueba t con statsmodels ===")
print(f"Estadístico t: {t_stat_sm:.4f}")
print(f"Valor-p: {p_value_sm:.4f}")
print(f"Grados de libertad: {df_sm:.2f}")
5.3 Prueba t de muestras pareadas¶
Cuando las dos mediciones provienen del mismo sujeto (antes/después), los datos no son independientes. Se calcula la diferencia $d_i = x_{1i} - x_{2i}$ y se aplica una prueba t de una muestra sobre las diferencias.
Ejemplo: ¿Un programa de capacitación mejoró el puntaje de los empleados?
np.random.seed(5)
n = 20
antes = np.random.normal(loc=65, scale=10, size=n)
despues = antes + np.random.normal(loc=4, scale=5, size=n) # mejora promedio de ~4 puntos
t_stat, p_value = stats.ttest_rel(antes, despues)
print("=== Prueba t pareada ===")
print(f"Media antes: {antes.mean():.2f}")
print(f"Media después: {despues.mean():.2f}")
print(f"Diferencia promedio:{(antes - despues).mean():.2f}")
print(f"Estadístico t: {t_stat:.4f}")
print(f"Valor-p: {p_value:.4f}")
print(f"Decisión (α=0.05): {'Rechazar H₀ → capacitación tuvo efecto' if p_value < 0.05 else 'No rechazar H₀'}")
6. Región de Rechazo vs Valor-p¶
Existen dos enfoques equivalentes para tomar la decisión:
| Enfoque | ¿Cómo funciona? | Rechazar $H_0$ si... | |---|---|---| | Valor crítico | Comparar el estadístico con el valor crítico $z_{\alpha/2}$ o $t_{\alpha/2, gl}$ | $|t_{obs}| > t_{crítico}$ | | Valor-p | Comparar la probabilidad acumulada con $\alpha$ | $p\text{-value} < \alpha$ |
Ambos siempre coinciden. El enfoque del valor-p es más informativo porque da una medida continua de evidencia.
# Comparación visual: región de rechazo vs valor-p para la prueba t de dos muestras
df = 50 # grados de libertad aproximados
alpha = 0.05
t_obs = t_stat # del ejemplo anterior
t_crit = stats.t.ppf(1 - alpha/2, df=df)
x = np.linspace(-4.5, 4.5, 500)
fig, axes = plt.subplots(1, 2, figsize=(14, 4))
# --- Panel izquierdo: región de rechazo ---
ax = axes[0]
ax.plot(x, stats.t.pdf(x, df), 'steelblue', lw=2)
for signo in [1, -1]:
x_fill = x[x >= t_crit] if signo == 1 else x[x <= -t_crit]
ax.fill_between(x_fill, stats.t.pdf(x_fill, df), color='salmon', alpha=0.6)
ax.axvline(t_obs, color='darkred', lw=2, linestyle='-', label=f't obs = {t_obs:.2f}')
ax.axvline(-t_obs, color='darkred', lw=2, linestyle='-')
ax.axvline(t_crit, color='black', lw=1.5, linestyle='--', label=f't crít = ±{t_crit:.2f}')
ax.axvline(-t_crit, color='black', lw=1.5, linestyle='--')
ax.set_title('Enfoque: valor crítico')
ax.set_xlabel('t')
ax.set_ylabel('Densidad')
ax.legend(fontsize=9)
# --- Panel derecho: valor-p ---
ax = axes[1]
ax.plot(x, stats.t.pdf(x, df), 'steelblue', lw=2)
for signo in [1, -1]:
x_fill = x[x >= abs(t_obs)] if signo == 1 else x[x <= -abs(t_obs)]
ax.fill_between(x_fill, stats.t.pdf(x_fill, df), color='salmon', alpha=0.7, label='p-value' if signo == 1 else '')
ax.axvline(t_obs, color='darkred', lw=2, label=f'p-value = {p_value:.4f}')
ax.axvline(-t_obs, color='darkred', lw=2)
ax.set_title('Enfoque: valor-p')
ax.set_xlabel('t')
ax.legend(fontsize=9)
plt.suptitle(f'Prueba bilateral — α = {alpha} — Decisión: {"Rechazar H₀" if p_value < alpha else "No rechazar H₀"}', y=1.02)
plt.tight_layout()
plt.show()
7. Ejemplo Integrador¶
Aplicamos todo el flujo a un caso de negocio completo.
Contexto: Una cadena de retail tiene dos sucursales (Norte y Sur). El equipo de operaciones quiere saber si el ticket promedio de compra es igual en ambas sucursales, con un nivel de significancia del 5%.
Datos: 40 tickets de cada sucursal registrados la última semana.
np.random.seed(99)
norte = np.random.normal(loc=32_500, scale=8_000, size=40)
sur = np.random.normal(loc=30_000, scale=9_500, size=40)
df_retail = pd.DataFrame({'Norte': norte, 'Sur': sur})
# Estadísticas descriptivas
print("=== Estadísticas descriptivas ===")
print(df_retail.describe().round(0))
# Visualización de las distribuciones
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
for ax, datos, nombre, color in [
(axes[0], norte, 'Norte', 'steelblue'),
(axes[1], sur, 'Sur', 'seagreen')
]:
ax.hist(datos, bins=12, color=color, edgecolor='white', alpha=0.8)
ax.axvline(datos.mean(), color='black', lw=2, linestyle='--', label=f'Media = {datos.mean():,.0f}')
ax.set_title(f'Sucursal {nombre}')
ax.set_xlabel('Ticket (CLP)')
ax.set_ylabel('Frecuencia')
ax.legend()
plt.tight_layout()
plt.show()
# Prueba t de dos muestras
t_stat, p_value = stats.ttest_ind(norte, sur, equal_var=False)
alpha = 0.05
print("=== Prueba de hipótesis ===")
print(f"H₀: μ_norte = μ_sur")
print(f"H₁: μ_norte ≠ μ_sur")
print(f"")
print(f"Diferencia de medias: {norte.mean() - sur.mean():,.0f} CLP")
print(f"Estadístico t: {t_stat:.4f}")
print(f"Valor-p: {p_value:.4f}")
print(f"Nivel de significancia: α = {alpha}")
print()
if p_value < alpha:
print(f"✅ Decisión: Rechazar H₀")
print(f" Hay evidencia estadística de que los tickets promedio difieren entre sucursales.")
else:
print(f"⚪ Decisión: No rechazar H₀")
print(f" No hay evidencia suficiente para concluir que los tickets promedio difieren.")
Resumen¶
| Concepto | Idea central |
|---|---|
| $H_0$ vs $H_1$ | $H_0$ es el estado por defecto; $H_1$ es lo que queremos demostrar |
| Error Tipo I ($\alpha$) | Rechazar $H_0$ cuando es verdadera (falsa alarma) |
| Error Tipo II ($\beta$) | No rechazar $H_0$ cuando es falsa (efecto perdido) |
| Valor-p | Probabilidad de los datos observados bajo $H_0$; no es P($H_0$ verdadera) |
| Prueba z | Usa distribución Normal; requiere $\sigma$ conocida o $n$ grande |
| Prueba t una muestra | Contrasta $\bar{x}$ contra $\mu_0$; usa t-Student con $n-1$ grados de libertad |
| Prueba t dos muestras | Compara medias de dos grupos independientes (Welch: varianzas distintas) |
| Prueba t pareada | Para mediciones antes/después del mismo sujeto |
Referencias¶
- Wackerly, D., Mendenhall, W., Scheaffer, R. (2008). Mathematical Statistics with Applications. Thomson.
- Seabold, S. & Perktold, J. (2010). Statsmodels: Econometric and statistical modeling with Python.
- Wasserstein, R. L., & Lazar, N. A. (2016). The ASA statement on p-values: context, process, and purpose. The American Statistician, 70(2), 129–133.