Lección 0.7.3 · 50 min
Álgebra lineal intuitiva: vectores y matrices como preparación para carteras
Una cartera son unos pesos; un día de mercado, unas rentabilidades; cinco años, una tabla. Con tres operaciones de álgebra lineal (producto escalar, producto de matrices y forma cuadrática) se calcula la rentabilidad y el riesgo de cualquier cartera, y se entiende por qué diversificar reduce el riesgo y por qué repartir el capital a partes iguales no reparte el riesgo.
- Módulo
- 0.7 · Matemáticas y estadística mínimas
- Duración
- 50 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
Una cartera es un vector
La lección 0.5.1 usó @ y np.cov para calcular la volatilidad de una cartera. Esta lección explica por qué esas
cuentas funcionan, con la menor cantidad posible de álgebra lineal, porque es el lenguaje en que están escritos todos
los métodos de construcción de carteras del curso.
Un vector es una lista ordenada de números. Una cartera de cuatro activos es un vector de pesos: la fracción del capital en cada uno. Las rentabilidades de los cuatro activos en un día son otro vector, en el mismo orden:
El producto escalar multiplica elemento a elemento y suma. Es exactamente la rentabilidad simple de la cartera ese día (lección 0.7.1: entre activos, la simple de la cartera es la media ponderada de las simples). Con los cuatro activos ficticios del curso y una cartera 40/30/10/20, en el peor día de RVAR:
import numpy as np
import pandas as pd
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
index_col="fecha")
rc = cartera.pct_change().dropna()
activos = list(rc.columns)
w = np.array([0.40, 0.30, 0.10, 0.20])
dia = rc["RVAR"].idxmin()
r_dia = rc.loc[dia].to_numpy()
print(f"{dia:%Y-%m-%d} {activos}")
print(f"Rentabilidades: {r_dia.round(4)}")
bucle = sum(wi * ri for wi, ri in zip(w, r_dia))
print(f"Bucle: {bucle:.4%} · w @ r: {w @ r_dia:.4%}")2025-01-23 ['RVAR', 'RFIJ', 'ORO', 'TECH']
Rentabilidades: [-0.0374 0.0004 -0.0071 -0.0459]
Bucle: -2.4725% · w @ r: -2.4725%El bucle y w @ r_dia dan lo mismo: @ es el producto escalar cuando sus dos operandos son vectores. El día en que
RVAR cae un 3,74 %, la cartera pierde un 2,47 %: el 30 % en renta fija, que apenas se mueve, y el 10 % en oro, que cae
mucho menos, amortiguan la caída; TECH, con un −4,59 %, la agrava. Esa compensación entre activos es, en un solo día,
la idea de la diversificación.
De un día a toda la serie: matrices y formas
Una matriz es una tabla de números con filas y columnas. Las rentabilidades de cinco años son una matriz con una fila por día y una columna por activo. Multiplicar la matriz por el vector de pesos hace el producto escalar de cada fila con los pesos: la rentabilidad de la cartera cada día, de una vez y sin bucles.
Fuente Mermaid
flowchart LR R[R · 1302×4] --> M[R @ w] W[w · 4] --> M M --> S[cartera · 1302]
La regla que decide si un producto de matrices es válido es la forma (shape): en , el número de columnas de
tiene que ser igual al de filas de , y esa dimensión desaparece del resultado. por da
:
R = rc.to_numpy()
serie = R @ w
print(f"R {R.shape} @ w {w.shape} = {serie.shape}")
print(f"Primer día: {serie[0]:.6f} = {w @ R[0]:.6f}")
print(f"Media anual: {serie.mean() * 252:.2%} · "
f"volatilidad: {serie.std(ddof=1) * np.sqrt(252):.2%}")R (1302, 4) @ w (4,) = (1302,)
Primer día: -0.005898 = -0.005898
Media anual: 7.00% · volatilidad: 12.00%Una serie con pesos constantes cada día supone que la cartera se rebalancea a diario para volver a los pesos iniciales. Sin rebalanceo, los pesos derivan con los precios: el activo que sube gana peso. Es una aproximación habitual que se precisará cuando el curso llegue a los backtests de carteras.
Con el orden cambiado, las dimensiones interiores ya no coinciden y NumPy lo dice:
w @ RValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 1302 is different from 4)Ese error es una suerte. Lo peligroso es lo contrario: un producto que sí funciona con las formas equivocadas.
Con cuatro activos y cuatro días, R sería y w @ R daría un resultado sin sentido sin protestar. Antes
de multiplicar, imprime las formas y di en voz alta qué es cada eje (filas: días; columnas: activos).
| Objeto | Forma | Qué es |
|---|---|---|
w | (4,) | Pesos de la cartera |
R | (1302, 4) | Rentabilidades: días × activos |
R @ w | (1302,) | Rentabilidad diaria de la cartera |
Sigma | (4, 4) | Covarianzas entre activos |
Sigma @ w | (4,) | Covarianza de cada activo con la cartera |
w @ Sigma @ w | () | Varianza de la cartera, un número |
La matriz de covarianzas, pieza a pieza
La lección 0.5.1 calculó la matriz de covarianzas con np.cov. Su contenido se entiende mejor construyéndola
con lo que ya se conoce: la volatilidad de cada activo y las correlaciones de la lección 0.7.2. Cada elemento es
y en forma de matrices, con la matriz diagonal de volatilidades y la de correlaciones, :
vols = rc.std().to_numpy() * np.sqrt(252)
C = rc.corr().to_numpy()
D = np.diag(vols)
Sigma = D @ C @ D
print(np.allclose(Sigma, rc.cov().to_numpy() * 252))
print(Sigma.round(4))True
[[ 0.0283 -0.0016 0.0031 0.0404]
[-0.0016 0.0025 0.0014 -0.002 ]
[ 0.0031 0.0014 0.0228 0.001 ]
[ 0.0404 -0.002 0.001 0.0799]]La diagonal son las varianzas anuales (: 0,0283 para RVAR, un 16,8 % de volatilidad al cuadrado). Fuera de la diagonal, las covarianzas: positivas y grandes entre TECH y RVAR, negativas entre RFIJ y los dos, casi cero entre ORO y TECH. Es simétrica, porque la covarianza de A con B es la de B con A.
La volatilidad de una cartera
La varianza de la cartera es la forma cuadrática : cada par de activos aporta su peso por su peso por su covarianza. Con dos activos se ve entera:
El último término es el que diversifica: con , la varianza de la cartera es menor que la de la suma ponderada. Si todas las correlaciones fueran 1, la volatilidad de la cartera sería exactamente la suma ponderada de volatilidades. La celda lo comprueba cambiando por una matriz de unos y por la identidad (correlación cero):
def vol_cartera(w, Sigma):
return np.sqrt(w @ Sigma @ w)
w_eq = np.full(4, 0.25)
unos = D @ np.ones((4, 4)) @ D
cero = D @ np.eye(4) @ D
print(f"Suma ponderada w @ vols: {w_eq @ vols:.2%}")
print(f"Correlaciones todas 1: {vol_cartera(w_eq, unos):.2%}")
print(f"Correlaciones reales: {vol_cartera(w_eq, Sigma):.2%}")
print(f"Correlaciones todas 0: {vol_cartera(w_eq, cero):.2%}")Suma ponderada w @ vols: 16.29%
Correlaciones todas 1: 16.29%
Correlaciones reales: 11.68%
Correlaciones todas 0: 9.14%La suma ponderada (16,29 %) coincide con el caso de correlación 1: es el riesgo de una cartera que no diversifica nada. Con las correlaciones reales la volatilidad baja al 11,68 %, y con correlaciones nulas bajaría al 9,14 %. La diferencia entre la suma ponderada y la volatilidad real es lo que aporta la diversificación, y depende solo de las correlaciones.
Pesos iguales en capital, no en riesgo
La cartera 1/N pone el mismo capital en cada activo. Es simple y difícil de batir, pero tiene un problema que la volatilidad total no muestra: los activos volátiles dominan el riesgo. La contribución al riesgo de cada activo es su peso por su covarianza con la cartera, dividida entre la varianza de la cartera, y las cuatro suman 1:
Una alternativa sencilla son los pesos inversos a la volatilidad (1/σ): cada activo pesa en proporción inversa a su volatilidad, así que el más tranquilo recibe más capital. Las dos carteras, lado a lado:
def contribuciones(w, Sigma):
return w * (Sigma @ w) / (w @ Sigma @ w)
w_iv = (1 / vols) / (1 / vols).sum()
tabla = pd.DataFrame({
"peso_1N": w_eq, "riesgo_1N": contribuciones(w_eq, Sigma),
"peso_1σ": w_iv, "riesgo_1σ": contribuciones(w_iv, Sigma),
}, index=activos)
print(tabla.round(3))
for nombre, pesos in (("1/N", w_eq), ("1/σ", w_iv)):
print(f"{nombre:<4} volatilidad {vol_cartera(pesos, Sigma):.2%}"
f" · suma ponderada {pesos @ vols:.2%}") peso_1N riesgo_1N peso_1σ riesgo_1σ
RVAR 0.25 0.322 0.164 0.313
RFIJ 0.25 0.001 0.556 0.150
ORO 0.25 0.130 0.182 0.233
TECH 0.25 0.547 0.098 0.303
1/N volatilidad 11.68% · suma ponderada 16.29%
1/σ volatilidad 6.58% · suma ponderada 11.03%Con pesos iguales, TECH aporta el 54,7 % del riesgo con el 25 % del capital, y RFIJ prácticamente nada: la cartera 1/N es, en riesgo, sobre todo una apuesta por TECH y RVAR. Con pesos 1/σ, RFIJ recibe más de la mitad del capital y las contribuciones quedan mucho más repartidas, entre el 15 % y el 31 %. No son iguales porque los pesos 1/σ ignoran las correlaciones: TECH y RVAR, muy correlacionados, siguen sumando más riesgo del que su volatilidad sugiere.
La figura de la cabecera resume las dos carteras: cada barra es la volatilidad real y el tramo claro, lo que la suma ponderada de volatilidades exagera. La cartera 1/σ es mucho menos volátil (6,58 % frente a 11,68 %), pero eso no la hace mejor: también rinde menos, porque pesa más la renta fija. Comparar carteras exige mirar rentabilidad y riesgo a la vez, y eso se hace en los niveles de construcción de carteras.
Por qué la mínima varianza es tan inestable
La lección 0.5.1 calculó la cartera de mínima varianza, con pesos proporcionales a , y dejó una pregunta abierta: ¿por qué estas carteras son tan sensibles a la estimación de ? Porque usan la inversa. Invertir una matriz amplifica sus errores, y una estimada con pocos datos tiene muchos. La prueba: estimar con ventanas consecutivas de 60 sesiones (unos tres meses), calcular en cada una los pesos de mínima varianza y los 1/σ, y ver cuánto se mueven:
mv, iv = [], []
for inicio in range(0, len(R) - 59, 60):
S = np.cov(R[inicio:inicio + 60], rowvar=False)
x = np.linalg.solve(S, np.ones(4))
mv.append(x / x.sum())
v = np.sqrt(np.diag(S))
iv.append((1 / v) / (1 / v).sum())
mv, iv = np.array(mv), np.array(iv)
print(f"Ventanas: {len(mv)}")
for nombre, P in (("mín. varianza", mv), ("1/σ", iv)):
rango = P.max(axis=0) - P.min(axis=0)
rotacion = np.abs(np.diff(P, axis=0)).sum(axis=1).mean()
print(f"{nombre:<14} rango de pesos {rango.round(2)} · "
f"rotación media {rotacion:.0%}")Ventanas: 21
mín. varianza rango de pesos [0.26 0.15 0.19 0.17] · rotación media 21%
1/σ rango de pesos [0.06 0.14 0.05 0.03] · rotación media 8%Los datos de cartera_sint.csv se generaron con correlaciones y volatilidades constantes: toda la variación de
los pesos es error de estimación. Aun así, los pesos de mínima varianza cambian mucho más de una ventana a otra que
los 1/σ, pasan de largos a cortos en ORO y TECH, y obligan a rotar más del doble de la cartera en cada rebalanceo,
con sus costes (lección 0.1.2). Los pesos 1/σ solo usan la diagonal de (las volatilidades), que se estima
mucho mejor que las correlaciones.
Las soluciones habituales —prohibir posiciones cortas, limitar el peso de cada activo, «encoger» la matriz de covarianzas hacia una más simple, usar ventanas más largas— son todas formas de no fiarse del todo de , y se verán con las técnicas de construcción de carteras. De esta lección basta quedarse con la idea: cuanto más usa un método las correlaciones estimadas, más sensible es a sus errores.
Ejercicios
- Calcula la volatilidad de una cartera 60/40 de RVAR y RFIJ con la fórmula de dos activos (escrita a mano, con
volsyC) y compruébala convol_cartera. ¿Cuánto aporta la correlación negativa frente a una correlación cero?
Solución
w1, w2 = 0.6, 0.4
s1, s2, rho = vols[0], vols[1], C[0, 1]
var_formula = (w1**2 * s1**2 + w2**2 * s2**2
+ 2 * w1 * w2 * s1 * s2 * rho)
w_6040 = np.array([w1, w2, 0, 0])
print(f"Fórmula: {np.sqrt(var_formula):.3%}")
print(f"w @ Sigma @ w: {vol_cartera(w_6040, Sigma):.3%}")
sin_corr = np.sqrt(w1**2 * s1**2 + w2**2 * s2**2)
print(f"Con correlación 0: {sin_corr:.3%}")Fórmula: 9.921%
w @ Sigma @ w: 9.921%
Con correlación 0: 10.291%Las dos vías coinciden. La correlación negativa de RFIJ con RVAR (−0,19) quita casi cuatro décimas de volatilidad frente a una correlación cero (9,92 % frente a 10,29 %). Los pesos de ORO y TECH a cero hacen que la forma cuadrática de cuatro activos se reduzca a la de dos.
- Comprueba que las contribuciones al riesgo de la cartera 1/N suman 1 y que
w * (Sigma @ w)suma la varianza de la cartera. ¿Qué activo reduce el riesgo de la cartera si se le da un poco más de peso?
Solución
cr = contribuciones(w_eq, Sigma)
print(f"Suma de contribuciones: {cr.sum():.6f}")
print(np.isclose((w_eq * (Sigma @ w_eq)).sum(),
w_eq @ Sigma @ w_eq))
for nombre, c in zip(activos, Sigma @ w_eq):
print(f"{nombre:<5}{c:8.4f}")Suma de contribuciones: 1.000000
True
RVAR 0.0176
RFIJ 0.0001
ORO 0.0071
TECH 0.0298Las contribuciones reparten la varianza total sin que sobre ni falte nada. Sigma @ w es la covarianza de cada activo
con la cartera: el que la tiene más baja es el que menos riesgo añade por cada euro más. RFIJ tiene una covarianza
prácticamente nula con la cartera 1/N, así que darle más peso (a costa de los demás) reduce la volatilidad: es lo que
hacen, llevado al extremo, los pesos 1/σ y la mínima varianza.
- Calcula la volatilidad de dos carteras 50/50: TECH con RVAR y RVAR con RFIJ. Para cada una, la suma ponderada de volatilidades y la diferencia. ¿Qué pareja diversifica más y por qué?
Solución
parejas = {"TECH+RVAR": [0.5, 0, 0, 0.5],
"RVAR+RFIJ": [0.5, 0.5, 0, 0]}
for nombre, pesos in parejas.items():
p = np.array(pesos)
real, suma = vol_cartera(p, Sigma), p @ vols
print(f"{nombre}: real {real:.2%} · suma {suma:.2%} · "
f"diferencia {suma - real:.2%}")TECH+RVAR: real 21.74% · suma 22.55% · diferencia 0.81%
RVAR+RFIJ: real 8.32% · suma 10.89% · diferencia 2.58%TECH y RVAR, con una correlación de 0,85, apenas diversifican: la volatilidad real (21,74 %) queda a menos de un punto de la suma ponderada. RVAR y RFIJ, con correlación negativa, quedan 2,58 puntos por debajo: casi una cuarta parte de su suma ponderada. Lo que diversifica no es el número de activos, sino lo poco que se parecen sus movimientos.
Respuesta plegada en cada pregunta
Autoevaluación
Rtiene forma(1302, 4)(días × activos) ywforma(4,). ¿Qué devuelveR @ w?- AUn número: la rentabilidad total de la cartera
- BUn array de forma
(1302,): la rentabilidad de la cartera cada día - CUn array de forma
(4,): la rentabilidad media de cada activo - DUn error, porque las formas no coinciden
Ver respuesta
Respuesta B. Un array de forma
(1302,): la rentabilidad de la cartera cada díaEn un producto de matrices, la dimensión interior (4 activos) desaparece y quedan las exteriores: 1302 días. Cada elemento es el producto escalar de los pesos con las rentabilidades de un día.
Una cartera tiene un 50 % en dos activos con un 20 % de volatilidad cada uno. ¿En qué caso su volatilidad es exactamente del 20 %?
- ASiempre: la media de 20 % y 20 % es 20 %
- BSolo si la correlación entre los dos es 1
- CSi la correlación es 0
- DNunca
Ver respuesta
Respuesta B. Solo si la correlación entre los dos es 1
La suma ponderada de volatilidades es la volatilidad de la cartera solo con correlación 1. Con correlación 0 sería 20 % / √2 ≈ 14,1 %; con −1, cero.
En la cartera con un 25 % en cada activo, TECH aporta en torno al 55 % del riesgo. ¿Por qué?
- APorque tiene más peso que los demás
- BPorque es el más volátil y está muy correlacionado con RVAR: su peso en capital es el mismo, pero su peso en riesgo es mucho mayor
- CPorque su rentabilidad es la más alta
- DEs un error de cálculo: con pesos iguales, cada activo aporta un 25 %
Ver respuesta
Respuesta B. Porque es el más volátil y está muy correlacionado con RVAR: su peso en capital es el mismo, pero su peso en riesgo es mucho mayor
La contribución al riesgo de cada activo es su peso por su covarianza con la cartera. Un activo volátil y correlacionado con el resto pesa en riesgo mucho más que en capital.
¿Por qué los pesos de mínima varianza estimados con ventanas de 60 sesiones cambian tanto de una ventana a otra?
- APorque la cartera de mínima varianza está mal definida
- BPorque resuelven un sistema con la inversa de una matriz de covarianzas estimada con pocos datos, y amplifican su error de estimación
- CPorque los precios cambian
- DPorque NumPy redondea
Ver respuesta
Respuesta B. Porque resuelven un sistema con la inversa de una matriz de covarianzas estimada con pocos datos, y amplifican su error de estimación
Con 60 datos, covarianzas y correlaciones tienen mucho error, y resolver el sistema con la inversa amplifica las diferencias pequeñas. Los pesos 1/σ solo usan las volatilidades, que se estiman mejor, y cambian mucho menos.
Ideas clave
- Una cartera es un vector de pesos; su rentabilidad, el producto escalar
w @ rcon las rentabilidades simples del periodo. - En un producto de matrices, la dimensión interior tiene que coincidir y desaparece:
(1302, 4) @ (4,)da(1302,). Comprobarshapeevita errores silenciosos. - La matriz de covarianzas es Σ = D C D: volatilidades en la diagonal de D y correlaciones en C. La varianza de la cartera es w′Σw.
- La suma ponderada de volatilidades solo es la volatilidad de la cartera si todas las correlaciones son 1; la diferencia es la diversificación.
- Pesos iguales en capital no son pesos iguales en riesgo. Los pesos 1/σ reparten mejor el riesgo y, frente a la mínima varianza, son mucho más estables.