Home / IA & ML / Distribución normal y regla 68-95-99.7 explicadas con Python
Distribución normal y regla 68-95-99.7 explicadas con Python

Distribución normal y regla 68-95-99.7 explicadas con Python

26 October 2026 · 16 min de lectura ··

Por qué la campana de Gauss importa a cualquier desarrollador

Imagina dos preguntas que parecen idénticas. Si la estatura media de un adulto es de 1,70 metros, ¿con qué frecuencia te cruzarás con alguien que mida diez veces más? Y si el ingreso medio de esa misma población es de 50 000 euros al año, ¿con qué frecuencia conocerás a alguien que gane diez veces más? La primera respuesta es “nunca”: nadie mide 17 metros. La segunda es “de vez en cuando”: los ingresos de medio millón existen y no son tan raros.

La diferencia no está en los números, sino en la forma en que se distribuyen. La estatura sigue aproximadamente una distribución normal, la famosa campana de Gauss, donde los valores extremos son rarísimos. Los ingresos siguen otra forma muy distinta, con una cola larga hacia la derecha. Entender esta diferencia es clave para analizar datos, detectar anomalías, diseñar experimentos A/B o entrenar modelos de aprendizaje automático.

En esta guía verás qué es una distribución normal, cómo la describen la media y la desviación estándar, por qué funciona la regla 68-95-99.7 y cómo aplicarlo todo en Python 3.15 con el módulo statistics de la biblioteca estándar, NumPy 2.5 y SciPy 1.18. Todos los resultados que aparecen proceden de ejecutar el código, no de estimaciones.

Qué es una distribución normal

Una distribución describe con qué frecuencia aparece cada valor posible de una variable. La distribución normal tiene una forma muy reconocible: un pico en el centro, donde se concentran la mayoría de las observaciones, y dos laderas que descienden de forma simétrica y suave hacia los extremos sin llegar nunca a tocar cero.

Tres propiedades la definen:

  • Es simétrica respecto a su centro: hay la misma probabilidad de estar un poco por encima que un poco por debajo.
  • La media, la mediana y la moda coinciden en el mismo punto, justo en el pico.
  • La probabilidad de encontrar un valor cae muy rápido a medida que te alejas del centro.

Muchos fenómenos se aproximan a esta forma: la estatura y el peso dentro de una población homogénea, los errores de medición de un instrumento, el peso de los paquetes que salen de una línea de envasado o las puntuaciones de ciertos tests estandarizados. La razón de fondo, como verás más adelante, es que estos fenómenos son la suma de muchos factores pequeños e independientes.

La curva tiene una fórmula exacta, conocida como función de densidad de probabilidad:

f(x) = 1 / (σ · √(2π)) · e^( -(x - μ)² / (2σ²) )

No necesitas memorizarla. Lo importante es observar que solo depende de dos parámetros: μ (la media) y σ (la desviación estándar). Conocidos esos dos números, conoces la distribución completa.

Los dos parámetros que lo explican todo

La media indica dónde está el centro de la campana. Se calcula sumando todas las observaciones y dividiendo entre el número de observaciones.

La desviación estándar indica lo ancha que es la campana, es decir, cuánto se alejan normalmente los datos de la media. Una desviación pequeña produce una campana estrecha y alta; una grande, una campana baja y extendida. Formalmente es la raíz cuadrada de la varianza, que a su vez es el promedio de las distancias al cuadrado respecto a la media.

Python incluye ambas medidas en la biblioteca estándar. Supón que controlas el peso de bolsas de café de 500 gramos en una fábrica:

import statistics as st

pesos = [498.2, 501.7, 499.9, 503.1, 497.4, 500.6, 502.2, 498.8]

print(f"Media: {st.mean(pesos):.2f} g")
print(f"Desviación estándar muestral: {st.stdev(pesos):.2f} g")
print(f"Desviación estándar poblacional: {st.pstdev(pesos):.2f} g")
Media: 500.24 g
Desviación estándar muestral: 2.02 g
Desviación estándar poblacional: 1.89 g

La diferencia entre stdev y pstdev es importante. Cuando tus datos son una muestra de una población mayor, como ocurre casi siempre, se usa la versión muestral, que divide entre n − 1 en lugar de n para corregir el sesgo de estimar a partir de pocos datos. En NumPy ocurre lo mismo, pero con un detalle que causa muchos errores: np.std usa por defecto la versión poblacional, así que para la muestral debes indicar ddof=1.

import numpy as np

pesos_np = np.array(pesos)
print(pesos_np.std())         # poblacional, ddof=0 por defecto
print(pesos_np.std(ddof=1))   # muestral

La regla 68-95-99.7

La propiedad más útil de la distribución normal es que la proporción de datos que cae a una cierta distancia de la media es siempre la misma, sin importar qué se esté midiendo. Esta es la regla 68-95-99.7 de la normal, también llamada regla empírica:

Intervalo alrededor de la mediaProporción aproximada de los datos
μ ± 1σ68 %
μ ± 2σ95 %
μ ± 3σ99,7 %

Dicho de otra forma: casi siete de cada diez observaciones están a menos de una desviación estándar de la media, diecinueve de cada veinte a menos de dos y prácticamente todas a menos de tres. Lo que queda fuera de ±3σ es solo un 0,3 %, repartido a partes iguales entre ambas colas.

No tienes por qué fiarte de la tabla. La clase NormalDist, disponible en el módulo statistics, permite calcular los porcentajes exactos a partir de la función de distribución acumulada (cdf), que devuelve la probabilidad de obtener un valor menor o igual que uno dado:

from statistics import NormalDist

estatura = NormalDist(mu=170, sigma=7)

for k in (1, 2, 3):
    dentro = estatura.cdf(170 + k * 7) - estatura.cdf(170 - k * 7)
    print(f"±{k}σ: {dentro:.4%}")
±1σ: 68.2689%
±2σ: 95.4500%
±3σ: 99.7300%

Los valores exactos son 68,27 %, 95,45 % y 99,73 %. La regla redondea esas cifras para que sean fáciles de recordar.

Resolver problemas con la regla empírica

La gracia de la regla es que permite responder preguntas de probabilidad con cálculos mentales. Tomemos como ejemplo una población cuya estatura sigue una normal con media de 170 cm y desviación estándar de 7 cm.

¿Qué porcentaje mide entre 170 y 177 cm? Ese tramo va de la media a una desviación por encima. Como el intervalo ±1σ contiene el 68 % y la curva es simétrica, la mitad superior contiene un 34 %.

¿Qué porcentaje mide entre 156 y 163 cm? Ese tramo va de −2σ a −1σ. El intervalo ±2σ contiene el 95 % y el ±1σ el 68 %, así que las dos franjas entre ambos suman un 27 %. Por simetría, cada una contiene la mitad: un 13,5 %.

¿Qué porcentaje mide más de 191 cm? Eso está a tres desviaciones por encima. Fuera de ±3σ queda un 0,3 %, y la cola superior es la mitad: un 0,15 %.

Comprobémoslo con código, que además nos permite responder preguntas que la regla no cubre, como el percentil 90:

print(f"Entre 170 y 177 cm: {estatura.cdf(177) - estatura.cdf(170):.2%}")
print(f"Entre 156 y 163 cm: {estatura.cdf(163) - estatura.cdf(156):.2%}")
print(f"Más de 191 cm: {1 - estatura.cdf(191):.3%}")
print(f"Percentil 90: {estatura.inv_cdf(0.90):.1f} cm")
Entre 170 y 177 cm: 34.13%
Entre 156 y 163 cm: 13.59%
Más de 191 cm: 0.135%
Percentil 90: 179.0 cm

Los resultados coinciden con los cálculos mentales dentro del margen de redondeo. El método inv_cdf hace la operación inversa a cdf: dada una probabilidad, devuelve el valor que la deja por debajo. Así sabemos que el 90 % de la población mide menos de 179 cm.

Puntuaciones z: comparar valores de escalas distintas

La regla empírica funciona en cualquier normal porque todas son, en el fondo, la misma curva desplazada y estirada. La puntuación z aprovecha esa idea: indica a cuántas desviaciones estándar de la media está un valor.

z = (x - μ) / σ

Una persona de 185 cm en nuestra población tiene una puntuación z de (185 − 170) / 7 ≈ 2,14. Está algo más de dos desviaciones por encima de la media, dentro del 1,6 % más alto de la población. NormalDist lo calcula directamente:

print(f"z de 185 cm: {estatura.zscore(185):.2f}")
z de 185 cm: 2.14

La potencia de las puntuaciones z está en comparar magnitudes que no tienen las mismas unidades. Si un candidato saca 82 puntos en una prueba de programación con media 70 y desviación 6 (z = 2) y 640 en una de lógica con media 600 y desviación 40 (z = 1), sabemos que su rendimiento relativo fue mucho mejor en la primera, aunque los números brutos no lo sugieran.

Al convertir cualquier variable normal en puntuaciones z se obtiene la llamada normal estándar, con media 0 y desviación 1. Muchos algoritmos de aprendizaje automático trabajan mejor con variables estandarizadas así, y por eso herramientas como StandardScaler de scikit-learn aplican exactamente esta transformación.

Calcular probabilidades con SciPy

Para análisis más completos, el módulo scipy.stats ofrece el objeto norm, con una API común a decenas de distribuciones. Sus métodos más usados son pdf (densidad), cdf (probabilidad acumulada), sf (función de supervivencia, es decir, la probabilidad de superar un valor), ppf (inversa de la acumulada) e interval (intervalo central que contiene una probabilidad dada).

from scipy import stats

dist = stats.norm(loc=170, scale=7)

print(f"P(X > 185): {dist.sf(185):.4f}")
inferior, superior = dist.interval(0.95)
print(f"Intervalo central del 95 %: {inferior:.1f} – {superior:.1f} cm")
print(f"Densidad en la media: {dist.pdf(170):.4f}")
P(X > 185): 0.0161
Intervalo central del 95 %: 156.3 – 183.7 cm
Densidad en la media: 0.0570

Usar sf(x) en lugar de 1 - cdf(x) no es un capricho: para probabilidades muy pequeñas en las colas, la resta pierde precisión numérica, mientras que sf calcula el resultado directamente. En detección de fraude o fiabilidad de sistemas, donde interesan sucesos de una entre un millón, esa diferencia importa.

Un detalle de nomenclatura: en SciPy los parámetros se llaman loc y scale, no mu y sigma, porque la misma API se reutiliza en distribuciones donde esos nombres no tendrían sentido.

Simular datos normales con NumPy

Comprobar la regla empírica con datos simulados es un buen ejercicio para afianzar la intuición. La forma moderna de generar números aleatorios en NumPy es a través de un generador creado con default_rng; las funciones del antiguo módulo global, como np.random.normal o np.random.seed, siguen existiendo por compatibilidad, pero ya no se recomiendan para código nuevo.

import numpy as np

rng = np.random.default_rng(seed=2026)
muestra = rng.normal(loc=170, scale=7, size=100_000)

media = muestra.mean()
sigma = muestra.std(ddof=1)

for k in (1, 2, 3):
    dentro = np.mean(np.abs(muestra - media) <= k * sigma)
    print(f"±{k}σ: {dentro:.2%}")
±1σ: 68.22%
±2σ: 95.46%
±3σ: 99.73%

Con cien mil observaciones simuladas, los porcentajes coinciden con la teoría casi al decimal. Fijar la semilla (seed=2026) hace que el experimento sea reproducible: cualquiera que ejecute el código obtendrá exactamente los mismos números.

Si quieres visualizar la campana, Matplotlib permite superponer el histograma de la muestra y la curva teórica:

import matplotlib.pyplot as plt
from scipy import stats

x = np.linspace(140, 200, 400)
plt.hist(muestra, bins=80, density=True, alpha=0.5, label="Simulación")
plt.plot(x, stats.norm.pdf(x, 170, 7), label="Normal teórica")
for k in (1, 2, 3):
    plt.axvline(170 + k * 7, linestyle="--", linewidth=0.8)
    plt.axvline(170 - k * 7, linestyle="--", linewidth=0.8)
plt.legend()
plt.savefig("campana.png", dpi=150)

Cuando los datos no son normales

Volvamos a la pregunta inicial sobre ingresos. Los salarios, el tamaño de los archivos en un servidor, el número de seguidores en una red social o el tiempo de respuesta de una API no siguen una normal. Suelen tener una cola larga hacia la derecha: la mayoría de valores se concentran en la zona baja, pero hay unos pocos enormes. Una forma habitual de modelar este comportamiento es la distribución log-normal.

Comparemos lo raro que es multiplicar por diez la media en cada caso:

rng = np.random.default_rng(seed=2026)
ingresos = rng.lognormal(mean=10.6, sigma=0.8, size=200_000)
estaturas = rng.normal(170, 7, size=200_000)

m, s = ingresos.mean(), ingresos.std()
print(f"Media {m:,.0f}  Mediana {np.median(ingresos):,.0f}")
print(f"Ingresos > 10 veces la media: {np.mean(ingresos > 10 * m):.4%}")
print(f"Estaturas > 1,5 veces la media: {np.mean(estaturas > 1.5 * 170):.4%}")
print(f"z de 10 veces la media si fuera normal: {(10 * m - m) / s:.1f}")
Media 55,258  Mediana 40,194
Ingresos > 10 veces la media: 0.0505%
Estaturas > 1,5 veces la media: 0.0000%
z de 10 veces la media si fuera normal: 9.5

En una población de un millón de personas habría unas 500 con ingresos diez veces superiores a la media. En cambio, nadie supera ni siquiera 1,5 veces la estatura media. Si hubiéramos tratado los ingresos como normales, un valor así estaría a 9,5 desviaciones de la media, algo que según la normal no ocurriría ni una vez en la historia del universo. Observa también que la media es bastante mayor que la mediana: es la señal clásica de asimetría hacia la derecha.

La lección es clara: la regla 68-95-99.7 solo es válida si los datos son aproximadamente normales. Aplicarla a datos asimétricos subestima de forma dramática la frecuencia de los extremos.

Cómo comprobar si tus datos son normales

Antes de aplicar cualquier razonamiento basado en la normal, conviene comprobar si los datos son normales. Hay tres enfoques complementarios.

El primero es visual: un histograma ya revela asimetrías evidentes, y un gráfico Q-Q, que compara los cuantiles de los datos con los de una normal teórica, es aún más preciso. Si los puntos forman una línea recta, los datos son aproximadamente normales.

stats.probplot(ingresos[:500], dist="norm", plot=plt)
plt.savefig("qq_ingresos.png", dpi=150)

El segundo son los contrastes estadísticos. La prueba de Shapiro-Wilk evalúa la hipótesis de que los datos proceden de una normal; un p-valor pequeño, habitualmente por debajo de 0,05, indica que probablemente no lo son:

rng = np.random.default_rng(seed=2026)
pesos_bolsas = rng.normal(500, 2, size=400)
ingresos_muestra = rng.lognormal(mean=10.6, sigma=0.8, size=400)

for nombre, datos in [("pesos", pesos_bolsas), ("ingresos", ingresos_muestra)]:
    estadistico, p = stats.shapiro(datos)
    print(f"{nombre}: W={estadistico:.3f}, p={p:.4f}")
pesos: W=0.997, p=0.7692
ingresos: W=0.678, p=0.0000

El tercero es el sentido común sobre el fenómeno. Una variable que no puede ser negativa y cuya media está cerca de cero, como el número de errores diarios, difícilmente será normal.

Ten en cuenta una limitación práctica de los contrastes: con muestras muy grandes detectan desviaciones minúsculas que no tienen ninguna importancia real, y con muestras pequeñas apenas tienen potencia. Por eso conviene combinarlos siempre con la inspección visual.

El teorema del límite central

Si tantos fenómenos reales no son normales, ¿por qué la distribución normal aparece por todas partes? La respuesta está en el teorema del límite central, uno de los resultados más importantes de la estadística. Dice que la media de muchas observaciones independientes tiende a seguir una distribución normal, aunque las observaciones individuales no lo hagan.

Un dado no tiene nada de normal: cada cara tiene la misma probabilidad. Pero si lanzas treinta dados y calculas la media, y repites el experimento muchas veces, las medias forman una campana:

rng = np.random.default_rng(seed=2026)
medias = rng.integers(1, 7, size=(50_000, 30)).mean(axis=1)

teorica = np.sqrt(35 / 12) / np.sqrt(30)
print(f"Media de las medias: {medias.mean():.3f}")
print(f"Desviación observada: {medias.std(ddof=1):.3f}  teórica: {teorica:.3f}")
Media de las medias: 3.498
Desviación observada: 0.311  teórica: 0.312

La desviación de las medias es la desviación de un solo dado dividida por la raíz del número de dados. Este resultado, el error estándar, es la base de los intervalos de confianza y de las pruebas A/B: aunque la conversión de cada usuario sea un sí o un no, la tasa media de conversión de miles de usuarios se comporta como una normal.

Aplicaciones prácticas en desarrollo de software

La distribución normal aparece en muchas tareas cotidianas de programación y ciencia de datos:

  • Control de calidad: detectar lotes defectuosos cuando el peso, el tamaño o el tiempo de proceso se salen de ±3σ, el principio en el que se basan los gráficos de control y la metodología Six Sigma.
  • Pruebas A/B: decidir si la diferencia de conversión entre dos variantes es real o fruto del azar.
  • Aprendizaje automático: estandarizar variables antes de entrenar, inicializar pesos de redes neuronales con distribuciones normales escaladas o modelar el ruido en regresión lineal.
  • Generación de datos sintéticos: crear conjuntos de prueba realistas para tests de carga o demostraciones.
  • Monitorización: establecer umbrales de alerta sobre métricas estables.

Este último caso merece un ejemplo, porque la detección de anomalías con puntuaciones z es una técnica sencilla y muy extendida. Supón que registras la duración de una tarea programada:

rng = np.random.default_rng(seed=2026)
duraciones = rng.normal(120, 15, size=1000)      # milisegundos
duraciones[[100, 500, 900]] = [210, 35, 260]     # anomalías inyectadas

z = (duraciones - duraciones.mean()) / duraciones.std(ddof=1)
for i in np.flatnonzero(np.abs(z) > 3):
    print(f"Ejecución {i}: {duraciones[i]:.1f} ms (z={z[i]:+.2f})")
Ejecución 100: 210.0 ms (z=+5.43)
Ejecución 188: 63.8 ms (z=-3.44)
Ejecución 500: 35.0 ms (z=-5.18)
Ejecución 900: 260.0 ms (z=+8.47)

El detector encontró las tres anomalías inyectadas, pero también una ejecución perfectamente normal, la 188. No es un fallo del código: con mil observaciones, la regla empírica predice que unas tres caerán fuera de ±3σ por puro azar. Es un recordatorio importante de que cualquier umbral estadístico genera falsos positivos, y de que su número crece con el volumen de datos.

Errores comunes al trabajar con la distribución normal

Algunos tropiezos se repiten con frecuencia en análisis de datos y sistemas de monitorización:

  • Asumir normalidad sin comprobarla. Latencias, tamaños de archivo o ingresos casi nunca son normales, y aplicarles la regla empírica subestima los extremos.
  • Confundir desviación muestral y poblacional, especialmente por el ddof=0 por defecto de NumPy.
  • Calcular la media y la desviación con datos que ya contienen las anomalías que quieres detectar. Un valor extremo infla la desviación y puede ocultarse a sí mismo; en esos casos son más robustas la mediana y la desviación absoluta mediana (MAD).
  • Usar 1 - cdf(x) para probabilidades diminutas en lugar de sf(x).
  • Ignorar el efecto del tamaño de muestra en los contrastes de normalidad.
  • Olvidar fijar la semilla en simulaciones que deben ser reproducibles.

Una versión robusta del detector anterior sustituye media y desviación por mediana y MAD:

mediana = np.median(duraciones)
mad = stats.median_abs_deviation(duraciones, scale="normal")
z_robusto = (duraciones - mediana) / mad

El argumento scale="normal" ajusta la MAD para que sea comparable con la desviación estándar cuando los datos sí son normales.

Qué hacer cuando la normal no encaja

Si tus datos no son normales, tienes varias alternativas antes de abandonar el análisis.

La primera es transformarlos. Aplicar un logaritmo a datos log-normales, como ingresos o tamaños de archivo, suele producir una distribución bastante normal, sobre la que sí puedes aplicar la regla empírica. SciPy ofrece además transformaciones más generales como Box-Cox o Yeo-Johnson:

log_ingresos = np.log(ingresos)
transformados, lambda_optima = stats.boxcox(ingresos)

La segunda es usar percentiles empíricos en lugar de desviaciones estándar. En monitorización de latencias, por ejemplo, es habitual definir objetivos sobre el percentil 99 calculado directamente con np.percentile, sin suponer ninguna forma concreta.

La tercera es recurrir a la desigualdad de Chebyshev, que funciona con cualquier distribución con varianza finita. Garantiza que al menos un 1 − 1/k² de los datos está a menos de k desviaciones de la media: al menos el 75 % dentro de ±2σ y al menos el 88,9 % dentro de ±3σ. Son cotas mucho menos precisas que las de la normal, pero nunca fallan.

SituaciónHerramienta recomendada
Datos aproximadamente normalesRegla empírica y puntuaciones z
Datos asimétricos positivosTransformación logarítmica o Box-Cox
Métricas operativas con colas largasPercentiles empíricos (p95, p99)
Distribución desconocidaDesigualdad de Chebyshev o métodos robustos

Conclusiones

La distribución normal es una herramienta extraordinariamente potente precisamente porque es muy simple: dos parámetros, la media y la desviación estándar, describen la curva completa. De esa sencillez nace la regla 68-95-99.7, que permite estimar probabilidades con cálculos mentales y que se aplica por igual a estaturas, pesos de productos o errores de medición.

Python pone todo esto al alcance de unas pocas líneas: statistics.NormalDist para cálculos rápidos sin dependencias, scipy.stats.norm para análisis más completos y precisos en las colas, y el generador de NumPy para simulaciones reproducibles. Con las puntuaciones z puedes comparar magnitudes heterogéneas y construir detectores de anomalías sencillos, y el teorema del límite central explica por qué la campana aparece incluso cuando los datos de partida no son normales.

La advertencia más importante es también la más olvidada: la regla solo funciona si los datos son normales. Antes de aplicarla, comprueba la forma de tus datos con histogramas, gráficos Q-Q y contrastes como Shapiro-Wilk. Cuando la normal no encaje, transforma los datos, usa percentiles o recurre a métodos robustos. Saber distinguir una variable como la estatura de una como los ingresos es lo que separa un análisis fiable de uno que subestima los extremos justo cuando más importan.