Lección 0.7.2 · 55 min
Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
¿Cada cuánto cabe esperar un día de −3σ? ¿Se mueven juntos dos activos o solo lo parece? ¿Cuánto de TECH se explica por el mercado? Tres preguntas de trading que se responden con lo mínimo de probabilidad, correlación y regresión, y con los errores típicos de cada una.
- Módulo
- 0.7 · Matemáticas y estadística mínimas
- Duración
- 55 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Probabilidades con una distribución
Una distribución de probabilidad describe qué valores puede tomar una variable y con qué frecuencia. En trading la variable suele ser la rentabilidad de un día, y la pregunta, cuánto de probable es un valor extremo: un día de −3 %, una semana de −10 %. La respuesta depende de la distribución que se suponga, y ahí está el riesgo.
La más usada es la normal, la campana de Gauss, definida por su media y su desviación típica . Cerca
del 68 % de los valores caen a menos de una de la media y cerca del 95 % a menos de dos. Su función de
distribución acumulada, , da la probabilidad de quedar por debajo de un valor. En scipy.stats,
cada distribución tiene esa función (cdf) y la de la cola contraria (sf, ).
Con la media y la desviación típica de SINT (lección 0.7.1), la probabilidad de un día por debajo de :
import numpy as np
import pandas as pd
from scipy import stats
precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
index_col="fecha")
r = precios["cierre"].pct_change().dropna()
mu, sigma = r.mean(), r.std()
normal = stats.norm(loc=mu, scale=sigma)
umbral = mu - 3 * sigma
print(f"μ = {mu:.4%} · σ = {sigma:.4%} · μ − 3σ = {umbral:.2%}")
print(f"P(R < μ − 3σ) según la normal: {normal.cdf(umbral):.3%}")μ = 0.0342% · σ = 1.1400% · μ − 3σ = -3.39%
P(R < μ − 3σ) según la normal: 0.135%Un 0,135 %: un día de cada 741, unos tres años de sesiones. Si la normal fuera una buena descripción, una caída de más del 3,4 % en SINT sería un acontecimiento raro. Los datos dicen otra cosa.
Lo que la normal no ve: las colas
Contar cuántos días quedaron por debajo de cada umbral y compararlo con lo que la normal espera es la prueba más directa:
n = len(r)
print(" k umbral normal observada días norm. días obs.")
for k in (1, 2, 3, 4):
u = mu - k * sigma
p_norm, p_obs = normal.cdf(u), (r < u).mean()
print(f"{k:>2} {u:8.2%} {p_norm:8.3%} {p_obs:10.3%} "
f"{p_norm * n:11.1f} {int((r < u).sum()):10d}") k umbral normal observada días norm. días obs.
1 -1.11% 15.866% 14.439% 206.6 188
2 -2.25% 2.275% 1.997% 29.6 26
3 -3.39% 0.135% 0.538% 1.8 7
4 -4.53% 0.003% 0.077% 0.0 1A una y dos desviaciones típicas, normal y datos casi coinciden (incluso hay algo menos de días de −2σ de lo esperado). A partir de tres, la normal se queda corta: 7 días de −3σ frente a 1,8 esperados, y un día de −4σ (el peor de la serie) cuando la normal espera 0,04, es decir, uno cada 125 años de sesiones. Es la curtosis en exceso de la lección 0.7.1 vista como probabilidad, y la razón de que un modelo de riesgo basado en la normal subestime las pérdidas grandes.
La t de Student es una alternativa con colas más gruesas. Además de posición y escala tiene un parámetro, los
grados de libertad (): con pocos (3, 4, 5) las colas son muy gruesas; con muchos, la t se acerca a la normal.
stats.t.fit estima los tres parámetros por máxima verosimilitud:
nu, loc, escala = stats.t.fit(r)
t_ajustada = stats.t(nu, loc=loc, scale=escala)
print(f"Grados de libertad: {nu:.1f}")
for k in (3, 4):
u = mu - k * sigma
print(f"−{k}σ normal {normal.cdf(u):.3%} "
f"t {t_ajustada.cdf(u):.3%} observada {(r < u).mean():.3%}")Grados de libertad: 8.8
−3σ normal 0.135% t 0.397% observada 0.538%
−4σ normal 0.003% t 0.073% observada 0.077%Con unos nueve grados de libertad, la t da a un día de −3σ una probabilidad del 0,40 %, tres veces la de la normal y mucho más cerca de la observada (0,54 %); y a uno de −4σ, el 0,073 %, casi exactamente la frecuencia observada. No es perfecta (con siete días en la cola, ninguna estimación lo es), pero describe el riesgo mucho mejor que la normal.
Correlación: cómo se mueven juntos
La correlación de Pearson mide si dos variables se mueven juntas en línea recta. Va de −1 (siempre en sentido contrario) a +1 (siempre en el mismo), y 0 indica que no hay relación lineal:
En una cartera es la cifra que decide cuánto diversifica cada activo (lección 0.7.3). Con las rentabilidades diarias
de los cuatro activos ficticios de cartera_sint.csv:
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
index_col="fecha")
rc = cartera.pct_change().dropna()
rc.corr().round(2) RVAR RFIJ ORO TECH
RVAR 1.00 -0.19 0.12 0.85
RFIJ -0.19 1.00 0.19 -0.14
ORO 0.12 0.19 1.00 0.02
TECH 0.85 -0.14 0.02 1.00La diagonal es 1 (cada activo consigo mismo) y la matriz es simétrica. TECH y RVAR, con 0,85, se mueven casi juntos: TECH es un activo de beta alta frente a la renta variable. RFIJ tiene correlación negativa con los dos, que es lo que se espera de la renta fija en una cartera. ORO apenas tiene relación con nada.
La misma cuenta hecha con los precios en lugar de las rentabilidades da otra matriz, y es un error habitual:
precios_corr = cartera.corr().loc["ORO", "RVAR"]
rent_corr = rc.corr().loc["ORO", "RVAR"]
print(f"ORO y RVAR · precios: {precios_corr:+.2f} · "
f"rentabilidades: {rent_corr:+.2f}")ORO y RVAR · precios: -0.40 · rentabilidades: +0.12Con los precios, ORO y RVAR parecen moverse en sentido contrario (−0,40); con las rentabilidades, en el mismo, aunque poco (+0,12). La segunda cifra es la que importa: dice si el día en que sube uno suele subir el otro. La primera solo dice que en estos cinco años uno acabó subiendo y el otro bajando, algo que puede pasar sin relación alguna.
Correlaciones espurias
¿Cuánto puede engañar la correlación de los precios? La prueba más limpia es fabricar series que, por construcción, no tienen nada que ver: pares de paseos aleatorios independientes, el modelo más simple de un precio (cada día, un paso aleatorio desde el cierre anterior). Mil pares de cinco años de sesiones, con semilla:
rng = np.random.default_rng(2026)
pasos = rng.normal(0, 0.01, size=(1000, 1250, 2))
niveles = 100 * np.exp(pasos.cumsum(axis=1))
def corr(x, y):
return np.corrcoef(x, y)[0, 1]
c_niv = np.array([corr(p[:, 0], p[:, 1]) for p in niveles])
c_ren = np.array([corr(d[:, 0], d[:, 1]) for d in pasos])
print(f"|ρ| > 0,5 con precios: {np.mean(abs(c_niv) > 0.5):.1%}")
print(f"|ρ| > 0,5 con rentabilidades: {np.mean(abs(c_ren) > 0.5):.1%}")
print(f"Mayor |ρ| con rentabilidades: {abs(c_ren).max():.3f}")|ρ| > 0,5 con precios: 37.6%
|ρ| > 0,5 con rentabilidades: 0.0%
Mayor |ρ| con rentabilidades: 0.104Más de un tercio de los pares de precios independientes tiene una correlación de más de 0,5 en valor absoluto. Con las rentabilidades, ninguno: la mayor no llega a 0,11. Dos series con tendencia siempre parecen relacionadas, porque las dos se alejan de su punto de partida y la correlación lo confunde con moverse juntas. Es la versión financiera de las correlaciones absurdas que recopila Tyler Vigen (enlace en «Recursos»): con suficientes series, siempre hay alguna pareja que encaja.
Incluso con rentabilidades, una correlación alta no dice por qué dos activos se mueven juntos:
Fuente Mermaid
flowchart LR F[Factor común] --> A[RVAR] F --> B[TECH] A -.->|correlación| B
TECH y RVAR se mueven juntos porque a los dos los mueven las mismas causas (tipos de interés, crecimiento, apetito por el riesgo), no porque uno cause al otro. Para el trading, la diferencia importa: una relación sin mecanismo puede desaparecer en cuanto cambia el entorno, y una estrategia de pares construida sobre ella deja de funcionar sin avisar.
Regresión simple: la beta de un activo
La regresión lineal simple ajusta una recta a una nube de puntos. Con las rentabilidades de TECH () frente a las de RVAR (), el modelo es:
La pendiente (la beta) es la sensibilidad: cuánto se mueve TECH de media por cada 1 % de RVAR. es
lo que queda cuando RVAR no se mueve, y , el residuo, lo que la recta no explica. Mínimos cuadrados
ordinarios (OLS) elige y para que la suma de los residuos al cuadrado sea la menor posible.
statsmodels lo hace y da además las estadísticas del ajuste:
import statsmodels.api as sm
X = sm.add_constant(rc["RVAR"])
modelo = sm.OLS(rc["TECH"], X).fit()
beta = modelo.params["RVAR"]
print(f"Beta: {beta:.2f}")
print(f"Alfa diaria: {modelo.params['const']:.4%}")
print(f"R²: {modelo.rsquared:.2f}")
beta_formula = rc["TECH"].cov(rc["RVAR"]) / rc["RVAR"].var()
print(f"cov / var: {beta_formula:.2f}")Beta: 1.43
Alfa diaria: -0.0128%
R²: 0.72
cov / var: 1.43sm.add_constant añade la columna de unos que estima ; sin ella, la recta pasaría por el origen. La beta es
1,43: un día en que RVAR sube un 1 %, TECH sube de media un 1,43 %, y lo mismo a la baja. La última línea muestra que,
en una regresión simple, la beta es la covarianza dividida entre la varianza de . La alfa diaria es prácticamente
cero, y con cinco años de datos no se distingue de cero.
El R² (0,72) es la fracción de la varianza de TECH que la recta explica; en la regresión simple es la correlación al cuadrado (). La figura de la cabecera muestra la nube de puntos con la recta: alargada en la diagonal, pero con bastante dispersión alrededor.
modelo.summary() imprime una tabla completa (errores estándar, estadísticos t, intervalos de confianza). Para
leerla bien hace falta la inferencia del nivel 2; aquí bastan la pendiente y el R².
La beta tiene un uso directo: cubrir el riesgo de mercado. Una posición larga de 10.000 € en TECH se mueve con RVAR como si fueran 14.300 € de RVAR. Vender esa cantidad de RVAR deja solo la parte de TECH que no depende del mercado:
cubierta = rc["TECH"] - beta * rc["RVAR"]
def vol(s):
return s.std() * np.sqrt(252)
print(f"Volatilidad de TECH: {vol(rc['TECH']):.1%}")
print(f"Volatilidad de TECH cubierta: {vol(cubierta):.1%}")
print(f"Correlación cubierta-RVAR: "
f"{cubierta.corr(rc['RVAR']):+.2f}")Volatilidad de TECH: 28.3%
Volatilidad de TECH cubierta: 14.9%
Correlación cubierta-RVAR: -0.00La cobertura reduce la volatilidad de la posición a casi la mitad y deja una correlación nula con RVAR: lo que queda es el riesgo propio de TECH, el 28 % de la varianza que el mercado no explica. Es la base de las estrategias neutrales al mercado, y tiene la misma letra pequeña que la correlación: la beta se estima con el pasado y cambia con el tiempo.
Ejercicios
- Calcula con la normal ajustada a SINT la probabilidad de un día de −5 % o peor y cuántos años de 252 sesiones habría que esperar, de media, para ver uno. Repite con la t ajustada. ¿Cuál es más prudente para dimensionar una posición?
Solución
for nombre, dist in (("normal", normal), ("t", t_ajustada)):
p = dist.cdf(-0.05)
print(f"{nombre:<7} P = {p:.5%} · uno cada "
f"{1 / p / 252:,.0f} años")normal P = 0.00050% · uno cada 790 años
t P = 0.03827% · uno cada 10 añosLa normal hace casi imposible un día de −5 % en SINT: uno cada 790 años. La t lo pone en uno cada diez años, una escala que un trader sí puede vivir. En los cinco años de datos no hubo ninguno (el peor fue −4,81 %), pero estuvo cerca. Para dimensionar una posición, la distribución prudente es la que da más probabilidad a las pérdidas grandes: la t.
- Calcula la correlación móvil de 60 sesiones entre TECH y RVAR con
rolling(60).corr(). ¿Entre qué valores se mueve? ¿Qué dice eso de una correlación estimada con tres meses de datos?
Solución
movil = rc["TECH"].rolling(60).corr(rc["RVAR"]).dropna()
print(f"Mínima {movil.min():.2f} · máxima {movil.max():.2f} · "
f"media {movil.mean():.2f}")Mínima 0.75 · máxima 0.92 · media 0.85Los datos se generaron con una correlación constante, y aun así la estimación de tres meses va de 0,75 a 0,92 alrededor de su valor medio, 0,85. Esa oscilación es solo error de estimación: con 60 datos, una correlación se conoce con poca precisión. Con datos reales, a ese ruido se suma que la correlación de verdad también cambia.
- Estima la beta de ORO frente a RVAR y su R². ¿Serviría RVAR para cubrir una posición en oro?
Solución
m_oro = sm.OLS(rc["ORO"], sm.add_constant(rc["RVAR"])).fit()
print(f"Beta: {m_oro.params['RVAR']:.2f} · R²: {m_oro.rsquared:.3f}")Beta: 0.11 · R²: 0.015Una beta pequeña y un R² cercano a cero: RVAR explica el 1,5 % de la varianza del oro. Vender RVAR contra una posición en oro no reduciría su riesgo, solo añadiría otro. Una beta solo sirve para cubrir cuando el R² es alto.
Respuesta plegada en cada pregunta
Autoevaluación
Con rentabilidades normales, un día por debajo de la media menos tres desviaciones típicas tiene una probabilidad del 0,135 %. En SINT se observa un 0,54 %. ¿Qué indica?
- AQue los datos tienen un error
- BQue las colas son más gruesas que las de la normal: los días extremos son unas cuatro veces más frecuentes de lo que predice
- CQue la volatilidad está mal calculada
- DNada: con 1.302 días, cualquier diferencia es ruido
Ver respuesta
Respuesta B. Que las colas son más gruesas que las de la normal: los días extremos son unas cuatro veces más frecuentes de lo que predice
Siete días observados frente a menos de dos esperados es una diferencia grande para ese tamaño de muestra. Es la curtosis en exceso de la lección 0.7.1 vista como probabilidad: la normal subestima los días malos.
¿Qué parámetro de la t de Student controla el grosor de las colas?
- ALa media
- BLos grados de libertad: cuantos menos, más gruesas; con muchos, la t se parece a la normal
- CLa escala
- DEl tamaño de la muestra
Ver respuesta
Respuesta B. Los grados de libertad: cuantos menos, más gruesas; con muchos, la t se parece a la normal
Con pocos grados de libertad (3, 4, 5) la t da mucha más probabilidad a los extremos; a partir de unos 30 es casi una normal. El ajuste a SINT da unos 9.
Los precios de dos activos tienen una correlación de −0,40 en cinco años. Sus rentabilidades diarias, de 0,12. ¿Qué cifra describe cómo se mueven juntos?
- ALa de los precios, porque usa los niveles reales
- BLa de las rentabilidades: la de los precios mezcla las tendencias de cada serie y puede ser grande sin relación alguna
- CLa media de las dos
- DNinguna: la correlación no sirve para precios
Ver respuesta
Respuesta B. La de las rentabilidades: la de los precios mezcla las tendencias de cada serie y puede ser grande sin relación alguna
Dos series con tendencia (como dos paseos aleatorios independientes) tienen a menudo correlaciones grandes en niveles. La correlación de las rentabilidades mide si los movimientos de cada día van en la misma dirección.
La regresión de las rentabilidades de TECH sobre las de RVAR da una pendiente de 1,43 y un R² de 0,72. ¿Qué afirmación es correcta?
- ARVAR causa los movimientos de TECH
- BUn día en que RVAR sube un 1 %, TECH sube de media un 1,43 %, y el 72 % de la varianza de TECH va con RVAR
- CTECH gana un 43 % más que RVAR cada año
- DEl 72 % de los días TECH sube
Ver respuesta
Respuesta B. Un día en que RVAR sube un 1 %, TECH sube de media un 1,43 %, y el 72 % de la varianza de TECH va con RVAR
La pendiente (beta) es la sensibilidad media; el R², la parte de la varianza que la recta explica. Ninguna de las dos dice nada de causas ni de rentabilidad a largo plazo.
Ideas clave
scipy.statsda para cada distribución la función de distribución (cdf) y la cola (sf): con ellas se calcula la probabilidad de un día extremo.- Las rentabilidades tienen colas más gruesas que la normal. Un día de −3σ es bastante más frecuente de lo que la normal predice; la t de Student con pocos grados de libertad lo describe mejor.
- Las correlaciones se calculan con rentabilidades. Con precios, dos series con tendencia parecen relacionadas aunque sean independientes.
- Correlación no es causalidad: dos activos pueden moverse juntos porque los mueve un tercero, y la relación puede cambiar o desaparecer.
- La beta de una regresión simple mide la sensibilidad de un activo al mercado; el R², cuánto de su varianza explica. Sirve para cubrir el riesgo de mercado de una posición.