Intervalos de confianza
En resumen: Un intervalo de confianza es un rango alrededor del valor de una muestra que, al 95 %, atrapa el valor verdadero en unas 95 de cada 100 muestras; el bootstrap lo construye con remuestras con reemplazo.
Problema
«¿Cuánto gasta un cliente por pedido?»
El jefe pregunta cuál es el pedido medio. En la vida real rara vez lo tienes todo: digamos que tienes 200 pedidos sacados al azar del año. Aquí tenemos también el año entero, 10 459 pedidos sin los cancelados, y podemos ver qué pasa:
Tres muestras de 200 dan tres medias distintas: 9,77 €, 10,74 € y 11,16 €. Sabes por la lección sobre muestras que la media tiene ruido. Pero solo tienes una muestra. ¿Qué le dices al jefe: un número o algo más?
Es más honesto decir «la media está en algún punto entre … y …». Falta averiguar cuán ancho debe ser ese «algún punto», a partir de la muestra que tienes y nada más.
La idea
La muestra en lugar del año: el bootstrap
Si pudieras sacar muestras nuevas del año, verías cuánto oscila la media. No puedes. Así que usas la muestra como un pequeño modelo del año: sacas de ella 200 pedidos con reemplazo y calculas la media. Eso es una remuestra. Un pedido sale dos veces, otro ninguna, y la media es un poco distinta cada vez.
Lo haces 10 000 veces y miras dónde están las medias. Cortas el 2,5 % de las más pequeñas y el 2,5 % de las más grandes. El 95 % central es el intervalo de confianza. El método se llama bootstrap:
Los primeros 8 pedidos de la muestra van de 0,90 € a 45,20 €. Las tres primeras remuestras dan medias de 10,07 €, 11,07 € y 11,13 €. La media de la muestra es 11,41 €, y el intervalo del 95 % va de 10,04 € a 12,83 €. La media verdadera del año es 10,63 €: está dentro.
rng.choice(sample, size=(10_000, 200)) es una tabla de 10 000 × 200: cada fila es una remuestra. mean(axis=1) da una media por fila. No hace falta ningún bucle.
Míralo
Cómo nace un intervalo de una muestra
El mismo código, despacio: se copian fichas de la muestra a una remuestra, su media cae sobre el eje y 10 000 medias así forman un montón.
import numpy as npimport pandas as pd
orders = pd.read_csv('year_orders.csv')items = pd.read_csv('year_items.csv')cancelled = orders.loc[orders['status'] == 'cancelled', 'id']items = items[~items['order_id'].isin(cancelled)]items['revenue'] = items['quantity'] * items['unit_price']totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(42)sample = rng.choice(totals.to_numpy(), size=200, replace=False)print(sample[:8])boots = rng.choice(sample, size=(10_000, 200)).mean(axis=1)print(boots[:3].round(2))low, high = np.quantile(boots, [0.025, 0.975])print(round(sample.mean(), 2), round(low, 2), round(high, 2))print(round(totals.mean(), 2))Paso a paso, en palabras
- totals es el valor de cada pedido del año. En la vida real no ves todo: solo tienes una muestra.
- sample es una muestra de 200 pedidos. Arriba están los primeros 8, en euros: 3,6, 0,9, 2,1, 17, 45,2, 13,2, 3,4 y 2.
- Una remuestra: 200 extracciones de sample con reemplazo. Un pedido sale dos veces, otro ninguna.
- La media de la remuestra cae como un punto sobre el eje. Las tres primeras son 10,07 €, 11,07 € y 11,13 €.
- Así 10 000 veces. Los puntos se amontonan alrededor de la media de la muestra, 11,41 €.
- Se corta el 2,5 % de la izquierda y el 2,5 % de la derecha. El 95 % central es el intervalo: de 10,04 € a 12,83 €.
- La media verdadera del año es 10,63 €, y está dentro. En la vida real no la conoces: el intervalo dice cuánto puede equivocarse la estimación.
El montón de la escena es un boceto: los números de los rótulos son reales, desde la muestra hasta los límites del intervalo. Aquí están las 10 000 medias reales con matplotlib, con los dos límites (líneas azules) y la media verdadera (la discontinua naranja):
Pruébalo
¿Sin reemplazo?
La mediana de los pedidos es más robusta frente a los pedidos grandes. Haz un intervalo del 95 % para la mediana con la misma muestra de 200: guarda en meds las medianas de 10 000 remuestras, y en low y high los límites del intervalo.
Solución
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(42)
sample = rng.choice(totals.to_numpy(), size=200, replace=False)
meds = np.median(rng.choice(sample, size=(10_000, 200)), axis=1)
low, high = np.quantile(meds, [0.025, 0.975])
print(np.median(sample), low, high)meds = np.median(rng.choice(sample, size=(10_000, 200)), axis=1) da una mediana por remuestra, y np.quantile(meds, [0.025, 0.975]) da el intervalo: de 6,25 € a 9,45 €. La mediana de la muestra es 7,95 € y la del año 7,70 €, de la lección sobre describir los datos: está dentro. Los cuantiles de la propia muestra (1,20 € y 36,72 €) responden otra pregunta: entre qué valores está el 95 % de los pedidos. No necesitas una fórmula para el intervalo de una mediana: el bootstrap calcula cualquier número en cuanto cambias mean por median.
La idea
Qué significa el 95 %
La media verdadera del año es un número: 10,63 €. El intervalo de una muestra o la atrapa o no. El 95 % es una promesa del método: si sacas muchas muestras y construyes un intervalo para cada una, unos 95 de cada 100 intervalos atraparán la verdad. Esto se llama nivel de confianza.
Aquí tenemos el año entero y podemos comprobar la promesa. 100 muestras de 200, para cada una un bootstrap con 1000 remuestras:
93 de 100 intervalos atrapan 10,63 €. Eso está cerca de 95: las 100 muestras son aleatorias ellas mismas, y las siguientes 100 darán algo algo distinto. Unos 5 de cada 100 intervalos honestos no atrapan la verdad, y no hay forma de saber cuáles.
Más precisamente, para curiosos
Es tentador decir «con una probabilidad del 95 % la media verdadera está entre 10,04 € y 12,83 €». Estrictamente, no es así: la media verdadera no es aleatoria, y el intervalo de tu muestra la contiene o no. Lo aleatorio es la muestra. Así que el 95 % habla del método, no de un número concreto. En un informe escribe «intervalo de confianza del 95 %: 10,04 €–12,83 €».
El bootstrap funciona cuando la muestra es aleatoria. Si los 200 pedidos son de un solo diciembre, el intervalo será para diciembre, por muchas remuestras que hagas: las remuestras no arreglan una muestra sesgada.
La idea
La fórmula como atajo
Para una media hay un atajo sin 10 000 remuestras. El ruido de la media es el error estándar: std / √n. Con una distribución normal el 95 % central queda a menos de 1,96 errores estándar del centro. Así que el intervalo es la media ± 1,96 × std / √n:
El error estándar es 0,72 €, y el intervalo va de 10,00 € a 12,81 €. El bootstrap dio 10,04 €–12,83 €: casi lo mismo. La segunda línea comprueba la promesa de la fórmula: de 1000 muestras de 200, 948 intervalos atrapan la verdad. Cerca de 950.
std(ddof=1) divide entre n − 1 en lugar de entre n, como std() en pandas. Con 200 pedidos la diferencia está en la tercera cifra.
Más precisamente, para curiosos
El número 1,96 viene de la distribución normal: el 95 % del área queda entre −1,96 y 1,96 desviaciones estándar. La media de 200 pedidos es casi normal incluso cuando los propios pedidos tienen una cola larga (el teorema central del límite de la lección sobre muestras). Con muestras pequeñas, de menos de 30, o con una cola muy larga la fórmula se aleja más, y el bootstrap es más honesto.
La idea
Un intervalo para una proporción
¿Qué parte de los pedidos está cancelada? Una proporción es una media de ceros y unos: True es 1, False es 0, y mean() da la proporción. Así que el bootstrap y la fórmula funcionan también aquí. La fórmula para una proporción es p ± 1,96 × √(p(1 − p) / n):
En una muestra de 500 pedidos el 5,8 % está cancelado. El bootstrap da del 3,8 al 8,0 %, y la fórmula del 3,8 al 7,8 %. La proporción verdadera del año es del 5,9 %. Con 500 pedidos conoces la proporción con unos ± 2 puntos: «5,8 %» sin intervalo suena más preciso de lo que es.
La idea
Una muestra pequeña, un intervalo ancho
Cuantos menos pedidos, más ruido tiene la media y más ancho es el intervalo:
50 pedidos dan un intervalo de 4,73 € de ancho, 200 pedidos 2,69 € y 800 pedidos 1,35 €. 4 veces más datos dan un intervalo de más o menos la mitad de ancho, porque el error estándar baja con la raíz cuadrada de n. La precisión es cara: reducir el ancho a la mitad otra vez necesita otra vez 4 veces más pedidos.
Ejercicio
La respuesta para el jefe
Una muestra nueva de 400 pedidos (semilla 2026). Haz un intervalo del 95 % para el pedido medio de dos maneras: con un bootstrap de 10 000 remuestras (boot_low, boot_high) y con la fórmula (se es el error estándar, f_low y f_high son los límites).
Solución
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(2026)
sample = rng.choice(totals.to_numpy(), size=400, replace=False)
boots = rng.choice(sample, size=(10_000, 400)).mean(axis=1)
boot_low, boot_high = np.quantile(boots, [0.025, 0.975])
se = sample.std(ddof=1) / np.sqrt(len(sample))
f_low = sample.mean() - 1.96 * se
f_high = sample.mean() + 1.96 * se
print(round(sample.mean(), 2))
print(round(boot_low, 2), round(boot_high, 2))
print(round(f_low, 2), round(f_high, 2))La media de la muestra es 10,80 €. El bootstrap da de 9,79 € a 11,90 €, y la fórmula de 9,74 € a 11,85 €. La respuesta para el jefe es «el pedido medio es de unos 10,80 €, con un intervalo de confianza del 95 % de 9,79 € a 11,90 €». Con se = sample.std(ddof=1) sin la raíz cuadrada de n el intervalo sería ± 21,10 €: eso es cuánto se diferencian los pedidos, no cuánto ruido tiene su media.
El jefe está pensando en envío gratis a partir de 20 €. ¿Qué parte de los pedidos supera los 20 €? Con una muestra de 300 pedidos (semilla 7) guarda la proporción en p y su intervalo del 95 % en low y high.
Solución
import numpy as np
import pandas as pd
orders = pd.read_csv('year_orders.csv')
items = pd.read_csv('year_items.csv')
cancelled = orders.loc[orders['status'] == 'cancelled', 'id']
items = items[~items['order_id'].isin(cancelled)]
items['revenue'] = items['quantity'] * items['unit_price']
totals = items.groupby('order_id')['revenue'].sum()
rng = np.random.default_rng(7)
sample = rng.choice(totals.to_numpy(), size=300, replace=False)
big = sample > 20
p = big.mean()
se = np.sqrt(p * (1 - p) / len(sample))
low, high = p - 1.96 * se, p + 1.96 * se
print(round(p, 3), round(low, 3), round(high, 3))
print(round((totals > 20).mean(), 3))big = sample > 20 es una máscara, y big.mean() es la proporción: 12,7 %. El error estándar de una proporción es np.sqrt(p * (1 - p) / 300), y el intervalo va del 8,9 al 16,4 %. La proporción verdadera del año es del 13,3 %: está dentro. big.sum() daría el número de pedidos, 38, no la proporción.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: El primer gráfico · Distribuciones · Describir los datos · Muestra y población
- Continúa en: Contrastar una hipótesis · Prueba A/B · ¿Por qué bajaron los ingresos?
- Ejercicios sobre el tema: Estadística para decidir