---
title: "0.7.3 Álgebra lineal intuitiva para carteras"
description: "Vectores de pesos, matrices de rentabilidades y matriz de covarianzas: volatilidad de una cartera, pesos iguales frente a 1/σ y contribuciones al riesgo."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/algebra-lineal-para-carteras/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/algebra-lineal-para-carteras.png"
---

# 0.7.3 · Á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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.7 · Matemáticas y estadística mínimas
- Duración: 50 min
- Requisitos: [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/), [0.5.1 · NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios](https://pythonparatrading.com/curso-python-trading/nivel-0/numpy-arrays-y-vectorizacion/)

## Al terminar esta lección

- Representar una cartera como un vector de pesos y calcular su rentabilidad con el producto escalar `w @ r`.
- Leer la forma (`shape`) de rentabilidades, pesos y covarianzas, y prever qué productos de matrices son válidos.
- Construir la matriz de covarianzas a partir de volatilidades y correlaciones y calcular la volatilidad de una cartera con w′Σw.
- Comparar una cartera de pesos iguales con una de pesos inversos a la volatilidad por su volatilidad y por la contribución de cada activo al riesgo.

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

$$
w = \begin{pmatrix} w_1 \\ w_2 \\ w_3 \\ w_4 \end{pmatrix} \qquad
r = \begin{pmatrix} r_1 \\ r_2 \\ r_3 \\ r_4 \end{pmatrix} \qquad
w \cdot r = \sum_{i=1}^{4} w_i\, r_i
$$

El **producto escalar** $w \cdot r$ 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:

```python
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%}")
```
```output
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 $R$ 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.

```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 $A\,B$, el número de columnas de
$A$ tiene que ser igual al de filas de $B$, y esa dimensión desaparece del resultado. $(1302 \times 4)$ por $(4)$ da
$(1302)$:

```python
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%}")
```
```output
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:

```python raises
w @ R
```
```output
ValueError: 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 $4 \times 4$ 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 $\Sigma$ 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

$$
\Sigma_{ij} = \sigma_i\,\sigma_j\,\rho_{ij}
$$

y en forma de matrices, con $D$ la matriz diagonal de volatilidades y $C$ la de correlaciones, $\Sigma = D\,C\,D$:

```python
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))
```
```output
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 ($\sigma_i^2$: 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** $w^\top \Sigma\, w$: cada par de activos aporta su peso por su
peso por su covarianza. Con dos activos se ve entera:

$$
\sigma_p^2 = w_1^2\sigma_1^2 + w_2^2\sigma_2^2 + 2\,w_1 w_2\,\sigma_1\sigma_2\,\rho_{12}
$$

El último término es el que diversifica: con $\rho_{12} < 1$, 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 $C$ por una matriz de unos y por la identidad (correlación cero):

```python
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%}")
```
```output
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:

$$
\text{CR}_i = \frac{w_i\,(\Sigma w)_i}{w^\top \Sigma\, w}
$$

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:

```python
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%}")
```
```output
      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.

<aside class="callout" data-kind="aviso">

Comparar dos carteras por su volatilidad sin igualar el riesgo favorece siempre a la más conservadora. Para
compararlas de forma justa, se escalan a la misma volatilidad objetivo (*volatility targeting*, enlace en «Recursos»)
o se mira la rentabilidad por unidad de riesgo.

</aside>

## 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 $\Sigma^{-1}\mathbf{1}$, y dejó
una pregunta abierta: ¿por qué estas carteras son tan sensibles a la estimación de $\Sigma$? Porque usan la inversa.
Invertir una matriz amplifica sus errores, y una $\Sigma$ estimada con pocos datos tiene muchos. La prueba: estimar
$\Sigma$ 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:

```python
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%}")
```
```output
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 $\Sigma$ (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 $\Sigma^{-1}$, 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

1. Calcula la volatilidad de una cartera 60/40 de RVAR y RFIJ con la fórmula de dos activos (escrita a mano, con
   `vols` y `C`) y compruébala con `vol_cartera`. ¿Cuánto aporta la correlación negativa frente a una correlación cero?

**Solución.**

```python
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%}")
```
```output
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.

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

```python
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}")
```
```output
Suma de contribuciones: 1.000000
True
RVAR   0.0176
RFIJ   0.0001
ORO    0.0071
TECH   0.0298
```

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

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

```python
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%}")
```
```output
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.

## Autoevaluación

1. `R` tiene forma `(1302, 4)` (días × activos) y `w` forma `(4,)`. ¿Qué devuelve `R @ w`?

   - A. Un número: la rentabilidad total de la cartera
   - B. Un array de forma `(1302,)`: la rentabilidad de la cartera cada día
   - C. Un array de forma `(4,)`: la rentabilidad media de cada activo
   - D. Un error, porque las formas no coinciden

   *Respuesta B: Un array de forma `(1302,)`: la rentabilidad de la cartera cada día.* En 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.

2. Una cartera tiene un 50 % en dos activos con un 20 % de volatilidad cada uno. ¿En qué caso su volatilidad es exactamente del 20 %?

   - A. Siempre: la media de 20 % y 20 % es 20 %
   - B. Solo si la correlación entre los dos es 1
   - C. Si la correlación es 0
   - D. Nunca

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

3. En la cartera con un 25 % en cada activo, TECH aporta en torno al 55 % del riesgo. ¿Por qué?

   - A. Porque tiene más peso que los demás
   - 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
   - C. Porque su rentabilidad es la más alta
   - D. Es un error de cálculo: con pesos iguales, cada activo aporta un 25 %

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

4. ¿Por qué los pesos de mínima varianza estimados con ventanas de 60 sesiones cambian tanto de una ventana a otra?

   - A. Porque la cartera de mínima varianza está mal definida
   - B. Porque resuelven un sistema con la inversa de una matriz de covarianzas estimada con pocos datos, y amplifican su error de estimación
   - C. Porque los precios cambian
   - D. Porque NumPy redondea

   *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 @ r` con las rentabilidades simples del periodo.
- En un producto de matrices, la dimensión interior tiene que coincidir y desaparece: `(1302, 4) @ (4,)` da `(1302,)`. Comprobar `shape` evita 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.

## Recursos

### Research

- [Volatility Targeting en Python](https://pythonparatrading.com/volatility-targeting/) · Research-0004 · 8 min

### Documentación

- [NumPy: numpy.matmul (operador @)](https://numpy.org/doc/stable/reference/generated/numpy.matmul.html) · numpy.org
- [NumPy: numpy.cov](https://numpy.org/doc/stable/reference/generated/numpy.cov.html) · numpy.org

### Datos

- [Datos del nivel 0: cierres de la cartera ficticia (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/cartera_sint.csv) · Descarga · cartera_sint.csv

Lección anterior: [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/)
