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.

Fig. — Rentabilidades diarias de TECH frente a RVAR, 2021–2025 (uno de cada ocho días, 163 puntos) y la recta de regresión estimada con todos los días
02/03
Módulo
0.7 · Matemáticas y estadística mínimas
Duración
55 min
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

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 μ\mu y su desviación típica σ\sigma. Cerca del 68 % de los valores caen a menos de una σ\sigma de la media y cerca del 95 % a menos de dos. Su función de distribución acumulada, F(x)=P(X≤x)F(x) = P(X \le x), 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, 1−F1 - F).

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 μ−3σ\mu - 3\sigma:

IN [01]python
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%}")
OUT [01]Un día de −3σ según la normal
μ = 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:

IN [02]python
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}")
OUT [02]Frecuencia esperada y observada
 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          1

A 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 (ν\nu): 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:

IN [03]python
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%}")
OUT [03]Ajuste de una t de Student
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:

ρXY=cov⁡(X,Y)σX σY\rho_{XY} = \frac{\operatorname{cov}(X, Y)}{\sigma_X\,\sigma_Y}

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:

IN [04]python
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
                      index_col="fecha")
rc = cartera.pct_change().dropna()
rc.corr().round(2)
OUT [04]Matriz de correlaciones
      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.00

La 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:

IN [05]python
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}")
OUT [05]Precios frente a rentabilidades
ORO y RVAR · precios: -0.40 · rentabilidades: +0.12

Con 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:

IN [06]python
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}")
OUT [06]Mil pares de paseos aleatorios independientes
|ρ| > 0,5 con precios:        37.6%
|ρ| > 0,5 con rentabilidades: 0.0%
Mayor |ρ| con rentabilidades: 0.104

Má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:

correlaciónFactor comúnRVARTECH
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 (yy) frente a las de RVAR (xx), el modelo es:

yt=α+β xt+εty_t = \alpha + \beta\,x_t + \varepsilon_t

La pendiente β\beta (la beta) es la sensibilidad: cuánto se mueve TECH de media por cada 1 % de RVAR. α\alpha es lo que queda cuando RVAR no se mueve, y εt\varepsilon_t, el residuo, lo que la recta no explica. Mínimos cuadrados ordinarios (OLS) elige α\alpha y β\beta 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:

IN [07]python
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}")
OUT [07]Beta de TECH frente a RVAR
Beta: 1.43
Alfa diaria: -0.0128%
R²: 0.72
cov / var: 1.43

sm.add_constant añade la columna de unos que estima α\alpha; 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 xx. 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 (0,852≈0,720{,}85^2 \approx 0{,}72). 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:

IN [08]python
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}")
OUT [08]Cubrir la beta
Volatilidad de TECH:          28.3%
Volatilidad de TECH cubierta: 14.9%
Correlación cubierta-RVAR:    -0.00

La 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

  1. 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
IN [09]python
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")
OUT [09]Un día de −5 %
normal  P = 0.00050% · uno cada 790 años
t       P = 0.03827% · uno cada 10 años

La 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.

  1. 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
IN [10]python
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}")
OUT [10]Correlación móvil
Mínima 0.75 · máxima 0.92 · media 0.85

Los 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.

  1. Estima la beta de ORO frente a RVAR y su R². ¿Serviría RVAR para cubrir una posición en oro?
Solución
IN [11]python
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}")
OUT [11]Beta del oro
Beta: 0.11 · R²: 0.015

Una 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

  1. 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.

  2. ¿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.

  3. 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.

  4. 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.stats da 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.

Recursos