Intervalos de Confianza¶
Una prueba de hipótesis nos dice si hay efecto. Un intervalo de confianza nos dice cuánto es ese efecto y con qué precisión lo estimamos. Este notebook cubre la construcción e interpretación de intervalos de confianza para los casos más frecuentes 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 DescrStatsW, CompareMeans
from statsmodels.stats.proportion import proportion_confint
plt.rcParams['figure.figsize'] = (9, 4)
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
np.random.seed(42)
1. Intuición: ¿Qué es un Intervalo de Confianza?¶
Cuando estimamos un parámetro con una muestra, obtenemos un número puntual (la media muestral, por ejemplo). Pero ese número tiene incertidumbre: si tomáramos otra muestra, obtendríamos un valor distinto.
Un intervalo de confianza (IC) expresa esa incertidumbre como un rango:
$$IC_{(1-\alpha)} = \left[\bar{x} - z_{\alpha/2} \cdot SE,\ \bar{x} + z_{\alpha/2} \cdot SE\right]$$
donde $SE = \sigma/\sqrt{n}$ es el error estándar.
Interpretación correcta¶
Si repitiéramos el proceso de muestreo muchas veces, el $(1-\alpha)\%$ de los intervalos construidos contendría el parámetro verdadero $\mu$.
Lo que NO significa:
| Incorrecto | Correcto |
|---|---|
| "Hay 95% de probabilidad de que $\mu$ esté en este intervalo" | El intervalo específico o contiene $\mu$ o no lo contiene |
| "El 95% de los datos cae dentro del IC" | El IC es sobre el parámetro, no sobre los datos individuales |
| "Un IC más ancho es peor" | Un IC más ancho solo refleja mayor incertidumbre (muestra pequeña o mayor variabilidad) |
np.random.seed(7)
muestra = np.random.normal(loc=800_000, scale=150_000, size=50)
sigma = 150_000 # conocida
n = len(muestra)
x_bar = muestra.mean()
print("=== IC para la media (σ conocida) ===")
for confianza in [0.90, 0.95, 0.99]:
alpha = 1 - confianza
z = stats.norm.ppf(1 - alpha/2)
margen = z * sigma / np.sqrt(n)
li, ls = x_bar - margen, x_bar + margen
print(f"IC {int(confianza*100)}%: [{li:,.0f} , {ls:,.0f}] (margen ±{margen:,.0f})")
2.2 Con $\sigma$ desconocida (IC t-Student)¶
En la práctica, casi siempre $\sigma$ es desconocida y se estima con $s$. Se usa la distribución t con $n-1$ grados de libertad:
$$IC = \bar{x} \pm t_{\alpha/2,\ n-1} \cdot \frac{s}{\sqrt{n}}$$
A medida que $n$ crece, $t_{\alpha/2, n-1} \to z_{\alpha/2}$ y ambos IC convergen.
s = muestra.std(ddof=1)
# Cálculo manual
alpha = 0.05
t_crit = stats.t.ppf(1 - alpha/2, df=n-1)
margen = t_crit * s / np.sqrt(n)
li, ls = x_bar - margen, x_bar + margen
print("=== IC para la media (σ desconocida — t-Student) ===")
print(f"Media muestral (x̄): {x_bar:,.0f}")
print(f"Desv. estándar (s): {s:,.0f}")
print(f"n: {n}")
print(f"t crítico (α=0.05): {t_crit:.4f}")
print(f"IC 95%: [{li:,.0f} , {ls:,.0f}]")
# Con statsmodels
d = DescrStatsW(muestra)
li_sm, ls_sm = d.tconfint_mean(alpha=0.05)
print("=== IC con statsmodels (DescrStatsW) ===")
print(f"IC 95%: [{li_sm:,.0f} , {ls_sm:,.0f}]")
# Efecto del tamaño de muestra sobre el ancho del IC
tamaños = [10, 30, 50, 100, 300]
anchos = []
for n_i in tamaños:
muestra_i = np.random.normal(loc=800_000, scale=150_000, size=n_i)
d_i = DescrStatsW(muestra_i)
li_i, ls_i = d_i.tconfint_mean(alpha=0.05)
anchos.append(ls_i - li_i)
fig, ax = plt.subplots()
ax.plot(tamaños, [a/1000 for a in anchos], 'o-', color='steelblue', lw=2, ms=7)
ax.set_title('Ancho del IC 95% según tamaño de muestra')
ax.set_xlabel('Tamaño de muestra (n)')
ax.set_ylabel('Ancho del IC (miles CLP)')
plt.tight_layout()
plt.show()
3. IC para Diferencia de Medias¶
Cuando comparamos dos grupos, el IC para la diferencia $\mu_1 - \mu_2$ nos dice no solo si difieren, sino cuánto y con qué precisión.
$$IC = (\bar{x}_1 - \bar{x}_2) \pm t_{\alpha/2,\ gl} \cdot \sqrt{\frac{s_1^2}{n_1} + \frac{s_2^2}{n_2}}$$
💡 Regla de decisión con el IC: si el IC para la diferencia no contiene el cero, hay diferencia significativa al nivel $\alpha$. Esto es equivalente a rechazar $H_0$ en la prueba t.
np.random.seed(21)
grupo_a = np.random.normal(loc=32_500, scale=8_000, size=40)
grupo_b = np.random.normal(loc=30_000, scale=9_500, size=40)
# Con statsmodels
cm = CompareMeans(DescrStatsW(grupo_a), DescrStatsW(grupo_b))
li_diff, ls_diff = cm.tconfint_diff(alpha=0.05, usevar='unequal')
print("=== IC para diferencia de medias ===")
print(f"Media Grupo A: {grupo_a.mean():,.0f}")
print(f"Media Grupo B: {grupo_b.mean():,.0f}")
print(f"Diferencia estimada: {grupo_a.mean()-grupo_b.mean():,.0f}")
print(f"IC 95% diferencia: [{li_diff:,.0f} , {ls_diff:,.0f}]")
print()
if li_diff > 0 or ls_diff < 0:
print("✅ El IC no contiene el cero → diferencia significativa al 5%")
else:
print("⚪ El IC contiene el cero → diferencia no significativa al 5%")
# Visualización: IC por grupo y diferencia
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
# Panel izquierdo: IC por grupo
ax = axes[0]
for i, (datos, nombre, color) in enumerate([
(grupo_a, 'Grupo A', 'steelblue'),
(grupo_b, 'Grupo B', 'seagreen')
]):
d_i = DescrStatsW(datos)
li_i, ls_i = d_i.tconfint_mean(alpha=0.05)
media_i = datos.mean()
ax.errorbar(i, media_i, yerr=[[media_i - li_i], [ls_i - media_i]],
fmt='o', color=color, capsize=8, capthick=2, ms=8, lw=2, label=nombre)
ax.set_xticks([0, 1])
ax.set_xticklabels(['Grupo A', 'Grupo B'])
ax.set_title('IC 95% por grupo')
ax.set_ylabel('Media (CLP)')
# Panel derecho: IC de la diferencia
ax = axes[1]
dif = grupo_a.mean() - grupo_b.mean()
ax.errorbar(0, dif, yerr=[[dif - li_diff], [ls_diff - dif]],
fmt='s', color='darkred', capsize=10, capthick=2, ms=9, lw=2)
ax.axhline(0, color='gray', linestyle='--', lw=1.5, label='Diferencia = 0')
ax.set_xticks([0])
ax.set_xticklabels(['A − B'])
ax.set_title('IC 95% para la diferencia')
ax.set_ylabel('Diferencia de medias (CLP)')
ax.legend()
plt.tight_layout()
plt.show()
4. IC para Proporciones¶
Cuando la variable de interés es binaria (éxito/fracaso, sí/no, churn/no churn), estimamos una proporción $p$ y construimos su IC.
Método Normal (Wilson simplificado)¶
$$IC = \hat{p} \pm z_{\alpha/2} \cdot \sqrt{\frac{\hat{p}(1-\hat{p})}{n}}$$
Válido cuando $n\hat{p} \geq 5$ y $n(1-\hat{p}) \geq 5$.
Ejemplo: de 200 clientes encuestados, 62 cancelaron su suscripción. ¿Cuál es la tasa de churn con su IC al 95%?
n = 200
exitos = 62
p_hat = exitos / n
# Cálculo manual
z = stats.norm.ppf(0.975)
se_p = np.sqrt(p_hat * (1 - p_hat) / n)
li_manual = p_hat - z * se_p
ls_manual = p_hat + z * se_p
print("=== IC para proporción (método Normal) ===")
print(f"Proporción estimada (p̂): {p_hat:.4f} ({p_hat*100:.1f}%)")
print(f"IC 95%: [{li_manual:.4f} , {ls_manual:.4f}]")
print(f" ({li_manual*100:.1f}% , {ls_manual*100:.1f}%)")
# Con statsmodels — múltiples métodos disponibles
print("=== IC para proporción con statsmodels ===")
for metodo in ['normal', 'wilson', 'agresti_coull', 'beta']:
li, ls = proportion_confint(exitos, n, alpha=0.05, method=metodo)
print(f"{metodo:<15}: [{li:.4f} , {ls:.4f}]")
💡 ¿Qué método usar? El método
wilsones generalmente preferible alnormalporque funciona mejor con proporciones cercanas a 0 o 1 y con muestras pequeñas.agresti_coulles una buena alternativa práctica.
# Efecto del tamaño de muestra sobre el IC de una proporción
p_real = 0.31
tamaños = [30, 50, 100, 200, 500, 1000]
fig, ax = plt.subplots(figsize=(10, 4))
for i, n_i in enumerate(tamaños):
ex_i = int(p_real * n_i)
li_i, ls_i = proportion_confint(ex_i, n_i, alpha=0.05, method='wilson')
media_i = ex_i / n_i
ax.errorbar(i, media_i, yerr=[[media_i - li_i], [ls_i - media_i]],
fmt='o', color='steelblue', capsize=6, capthick=2, ms=7, lw=2)
ax.axhline(p_real, color='red', linestyle='--', lw=1.5, label=f'p real = {p_real}')
ax.set_xticks(range(len(tamaños)))
ax.set_xticklabels([f'n={n}' for n in tamaños])
ax.set_title('IC 95% para proporción según tamaño de muestra (Wilson)')
ax.set_ylabel('Proporción estimada')
ax.legend()
plt.tight_layout()
plt.show()
5. Simulación de Cobertura¶
La cobertura es la propiedad central de un IC: si construimos intervalos de confianza al 95%, el 95% de esos intervalos deben contener el parámetro verdadero.
Vamos a verificarlo empíricamente.
# Parámetro verdadero
mu_real = 800_000
sigma_real = 150_000
n = 40
n_simulaciones = 100
alpha = 0.05
intervalos = []
for _ in range(n_simulaciones):
muestra_i = np.random.normal(mu_real, sigma_real, n)
d_i = DescrStatsW(muestra_i)
li_i, ls_i = d_i.tconfint_mean(alpha=alpha)
contiene = li_i <= mu_real <= ls_i
intervalos.append((li_i, ls_i, contiene))
cobertura = sum(1 for _, _, c in intervalos if c) / n_simulaciones
print(f"Cobertura observada: {cobertura*100:.1f}% (teórica: {(1-alpha)*100:.0f}%)")
# Visualización
fig, ax = plt.subplots(figsize=(10, 8))
for i, (li_i, ls_i, contiene) in enumerate(intervalos):
color = 'steelblue' if contiene else 'crimson'
ax.plot([li_i, ls_i], [i, i], color=color, lw=1.5, alpha=0.8)
ax.plot((li_i + ls_i) / 2, i, 'o', color=color, ms=3)
ax.axvline(mu_real, color='black', lw=2, linestyle='--', label=f'μ real = {mu_real:,}')
from matplotlib.lines import Line2D
leyenda = [
Line2D([0], [0], color='steelblue', lw=2, label=f'Contiene μ ({sum(c for _,_,c in intervalos)}/{n_simulaciones})'),
Line2D([0], [0], color='crimson', lw=2, label=f'No contiene μ ({sum(not c for _,_,c in intervalos)}/{n_simulaciones})')
]
ax.legend(handles=leyenda, fontsize=10)
ax.set_title(f'Simulación de cobertura: {n_simulaciones} intervalos de confianza al 95%\nCobertura observada: {cobertura*100:.1f}%')
ax.set_xlabel('CLP')
ax.set_ylabel('Simulación #')
ax.set_yticks([])
plt.tight_layout()
plt.show()
Este gráfico ilustra el significado frecuentista del IC al 95%: no es que este intervalo tenga 95% de probabilidad de contener μ, sino que el procedimiento de construcción produce intervalos que lo contienen el 95% de las veces.
6. Relación entre IC y Prueba de Hipótesis¶
Existe una equivalencia exacta entre un IC al $(1-\alpha)\%$ y una prueba bilateral al nivel $\alpha$:
| Si el IC $(1-\alpha)$... | Equivale a... |
|---|---|
| No contiene $\mu_0$ | Rechazar $H_0: \mu = \mu_0$ al nivel $\alpha$ |
| Contiene $\mu_0$ | No rechazar $H_0: \mu = \mu_0$ al nivel $\alpha$ |
El IC es más informativo: además de la decisión, indica la magnitud y dirección del efecto.
# Demostración de la equivalencia
np.random.seed(15)
muestra = np.random.normal(loc=820_000, scale=150_000, size=50)
mu_0 = 800_000
# IC
d = DescrStatsW(muestra)
li, ls = d.tconfint_mean(alpha=0.05)
# Prueba t
t_stat, p_value, _ = d.ttest_mean(value=mu_0, alternative='two-sided')
print("=== Equivalencia IC ↔ Prueba t ===")
print(f"μ₀ bajo H₀: {mu_0:,}")
print(f"Media muestral: {muestra.mean():,.0f}")
print()
print(f"IC 95%: [{li:,.0f} , {ls:,.0f}]")
print(f"¿Contiene μ₀? {'Sí → No rechazar H₀' if li <= mu_0 <= ls else 'No → Rechazar H₀'}")
print()
print(f"Prueba t — valor-p: {p_value:.4f}")
print(f"Decisión (α=0.05): {'No rechazar H₀' if p_value >= 0.05 else 'Rechazar H₀'}")
print()
print("→ Ambos métodos coinciden ✅")
7. Ejemplo Integrador¶
Contexto: Un banco quiere evaluar el impacto de una nueva política de crédito. Tiene datos de:
- El monto promedio de crédito aprobado (muestra de 60 clientes)
- La tasa de aprobación antes y después de la nueva política (dos muestras de 150 clientes)
- La proporción de clientes que calificaron bajo la nueva política (200 solicitudes)
Se construyen ICs al 95% para cada pregunta.
np.random.seed(77)
# 1. IC para el monto promedio de crédito
montos = np.random.normal(loc=4_500_000, scale=1_200_000, size=60)
d_montos = DescrStatsW(montos)
li_m, ls_m = d_montos.tconfint_mean(alpha=0.05)
print("=== 1. IC para monto promedio de crédito ===")
print(f"Media muestral: {montos.mean():,.0f} CLP")
print(f"IC 95%: [{li_m:,.0f} , {ls_m:,.0f}] CLP")
# 2. IC para diferencia en montos entre dos políticas
politica_antigua = np.random.normal(loc=4_200_000, scale=1_100_000, size=150)
politica_nueva = np.random.normal(loc=4_700_000, scale=1_300_000, size=150)
cm = CompareMeans(DescrStatsW(politica_nueva), DescrStatsW(politica_antigua))
li_d, ls_d = cm.tconfint_diff(alpha=0.05, usevar='unequal')
diff = politica_nueva.mean() - politica_antigua.mean()
print(f"\n=== 2. IC para diferencia de montos (nueva − antigua) ===")
print(f"Diferencia estimada: {diff:,.0f} CLP")
print(f"IC 95%: [{li_d:,.0f} , {ls_d:,.0f}] CLP")
print(f"Interpretación: {'La nueva política aprueba montos significativamente mayores' if li_d > 0 else 'Sin diferencia significativa'}")
# 3. IC para proporción de aprobación
n_sol = 200
aprobados = 134
li_p, ls_p = proportion_confint(aprobados, n_sol, alpha=0.05, method='wilson')
print(f"\n=== 3. IC para tasa de aprobación ===")
print(f"Tasa estimada: {aprobados/n_sol:.1%}")
print(f"IC 95% Wilson: [{li_p:.1%} , {ls_p:.1%}]")
# Visualización resumen del ejemplo integrador
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
# Panel 1: IC monto promedio
ax = axes[0]
media = montos.mean()
ax.barh(0, ls_m - li_m, left=li_m, height=0.4, color='steelblue', alpha=0.5)
ax.axvline(media, color='steelblue', lw=2, label=f'x̄ = {media/1e6:.2f}M')
ax.set_yticks([])
ax.set_title('IC monto promedio')
ax.set_xlabel('CLP')
ax.legend(fontsize=9)
# Panel 2: IC diferencia
ax = axes[1]
ax.barh(0, ls_d - li_d, left=li_d, height=0.4, color='seagreen', alpha=0.5)
ax.axvline(diff, color='seagreen', lw=2, label=f'dif = {diff/1e6:.2f}M')
ax.axvline(0, color='red', lw=1.5, linestyle='--', label='0')
ax.set_yticks([])
ax.set_title('IC diferencia nueva − antigua')
ax.set_xlabel('CLP')
ax.legend(fontsize=9)
# Panel 3: IC proporción
ax = axes[2]
p_est = aprobados / n_sol
ax.barh(0, ls_p - li_p, left=li_p, height=0.4, color='salmon', alpha=0.6)
ax.axvline(p_est, color='darkred', lw=2, label=f'p̂ = {p_est:.1%}')
ax.set_yticks([])
ax.set_title('IC tasa de aprobación')
ax.set_xlabel('Proporción')
ax.legend(fontsize=9)
plt.suptitle('Resumen — Intervalos de Confianza al 95%', fontsize=12, y=1.02)
plt.tight_layout()
plt.show()
Resumen¶
| Caso | Fórmula | statsmodels |
|---|---|---|
| Media, $\sigma$ conocida | $\bar{x} \pm z_{\alpha/2} \cdot \sigma/\sqrt{n}$ | DescrStatsW.tconfint_mean |
| Media, $\sigma$ desconocida | $\bar{x} \pm t_{\alpha/2,n-1} \cdot s/\sqrt{n}$ | DescrStatsW.tconfint_mean |
| Diferencia de medias | $(\bar{x}_1-\bar{x}_2) \pm t \cdot SE_{diff}$ | CompareMeans.tconfint_diff |
| Proporción | $\hat{p} \pm z_{\alpha/2} \cdot \sqrt{\hat{p}(1-\hat{p})/n}$ | proportion_confint |
Ideas clave:
- El IC mide la incertidumbre de la estimación, no solo si hay efecto
- Mayor $n$ → IC más estrecho → mayor precisión
- Mayor confianza ($1-\alpha$) → IC más ancho (hay un trade-off)
- El IC al 95% y la prueba bilateral al 5% son equivalentes en sus decisiones
- Preferir Wilson sobre Normal para proporciones cercanas a 0 o 1
Referencias¶
- Wackerly, D., Mendenhall, W., Scheaffer, R. (2008). Mathematical Statistics with Applications. Thomson.
- Statsmodels — DescrStatsW: https://www.statsmodels.org/stable/generated/statsmodels.stats.weightstats.DescrStatsW.html
- Statsmodels — proportion_confint: https://www.statsmodels.org/stable/generated/statsmodels.stats.proportion.proportion_confint.html