Lección 0.5.1 · 50 min

NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios

Todo el stack cuantitativo de Python —pandas, Polars, scikit-learn, statsmodels— descansa sobre los arrays de NumPy. Esta lección enseña a pensar en columnas de números en lugar de en bucles, con los precios de SINT y una cartera de cuatro activos.

Fig. — Volatilidad anual de los cuatro activos ficticios, su media ponderada y la de la cartera con un 25 % en cada uno, calculada con la matriz de covarianzas
01/05
Módulo
0.5 · Computación numérica y datos
Duración
50 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

Del bucle al array

Hasta ahora, una serie de precios era una lista de Python y una rentabilidad, un bucle for (lección 0.3.4). Funciona, pero no escala: cinco años de cierres diarios son 1.300 números; cinco años de barras de un minuto de 500 acciones son casi 250 millones. NumPy resuelve el problema con una estructura, el ndarray, y una forma de pensar: operar con columnas enteras en lugar de elemento a elemento.

Un ndarray se diferencia de una lista en tres cosas:

PropiedadLista de Pythonndarray de NumPy
Tipo de los elementosCualquiera, mezcladoUno solo (dtype): float64, int64, bool…
MemoriaPunteros a objetos sueltosUn bloque contiguo de números
OperacionesElemento a elemento, en PythonSobre todo el array, en código compilado
DimensionesListas de listasshape explícita: (n,), (n, k)…

Los cierres de SINT, el ETF ficticio del curso, se leen con pandas (lección 0.5.2) y se pasan a NumPy con to_numpy():

IN [01]python
import numpy as np
import pandas as pd

precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")
cierres = precios["cierre"].to_numpy()
print(type(cierres).__name__, cierres.dtype, cierres.shape)
print(cierres[:5])
print(cierres[-1])
cierres[-1]
OUT [01]Cierres de SINT como array
ndarray float64 (1303,)
[99.14 99.25 97.38 98.56 99.05]
142.22
np.float64(142.22)

La última línea muestra un detalle de NumPy 2: el repr de un escalar incluye su tipo (np.float64(142.22)), mientras que print muestra solo el número. Es un cambio de NumPy 2.0 (NEP 51) que sorprende al leer notebooks antiguos: el valor es el mismo; solo cambia cómo se representa.

Vectorización: rentabilidades sin bucles

La rentabilidad simple de un día es el cierre de hoy entre el de ayer, menos uno. Con un array, «hoy» son todos los cierres salvo el primero (cierres[1:]) y «ayer», todos salvo el último (cierres[:-1]). Dividir los dos arrays divide cada par de elementos alineados:

rt=PtPt−1−1r_t = \frac{P_t}{P_{t-1}} - 1
IN [02]python
r_bucle = []
for i in range(1, len(cierres)):
    r_bucle.append(cierres[i] / cierres[i - 1] - 1)
r_bucle = np.array(r_bucle)

r = cierres[1:] / cierres[:-1] - 1
print(r.shape)
print(np.array_equal(r, r_bucle))
OUT [02]Bucle frente a vectorización
(1302,)
True

El resultado es idéntico bit a bit: las mismas divisiones en el mismo orden. La diferencia está en quién ejecuta el bucle. En la versión vectorizada, el bucle existe, pero lo recorre código compilado de NumPy sobre un bloque de memoria contiguo, sin crear un objeto de Python por cada número. Medido con timeit sobre un millón de precios, en el equipo en que se preparó esta lección la versión vectorizada fue unas 50 veces más rápida que el bucle; la cifra exacta depende de la máquina, el orden de magnitud no. El código para medirlo en tu equipo:

IN [03]python
import timeit

largo = np.tile(cierres, 800)  # algo más de un millón de precios
timeit.timeit(lambda: largo[1:] / largo[:-1] - 1, number=10)

Las rentabilidades logarítmicas, ln⁡(Pt/Pt−1)\ln(P_t / P_{t-1}), se suman en el tiempo en lugar de multiplicarse. NumPy tiene funciones universales (ufuncs) como np.log, np.diff o np.cumprod que actúan sobre todo el array:

IN [04]python
r_log = np.diff(np.log(cierres))
total_simple = np.prod(1 + r) - 1
total_log = np.expm1(r_log.sum())
print(f"Rentabilidad total: {total_simple:.4%}")
print(f"Con logaritmos:     {total_log:.4%}")
print(f"Último / primero:   {cierres[-1] / cierres[0] - 1:.4%}")
OUT [04]Rentabilidades logarítmicas y acumuladas
Rentabilidad total: 43.4537%
Con logaritmos:     43.4537%
Último / primero:   43.4537%

Las tres cuentas llegan al mismo número por caminos distintos. Comprobar un resultado por dos vías es un hábito que ahorra muchos errores en un backtest.

Las comparaciones también se vectorizan: r < -0.02 devuelve un array de bool del mismo tamaño, que sirve de máscara para seleccionar, contar o construir una señal con np.where:

IN [05]python
caidas = r < -0.02
print(f"Sesiones con caída > 2 %: {caidas.sum()}")
print(f"Peor sesión: {r.min():.2%} (posición {r.argmin()})")
print(f"Media tras las caídas: {r[1:][caidas[:-1]].mean():.3%}")

# 1 si el cierre está por encima de su media de 200 sesiones
media_200 = np.convolve(cierres, np.ones(200) / 200, mode="valid")
senal = np.where(cierres[199:] > media_200, 1, 0)
print(senal.shape, senal.mean().round(3))
OUT [05]Máscaras y señales
Sesiones con caída > 2 %: 45
Peor sesión: -4.81% (posición 338)
Media tras las caídas: -0.077%
(1104,) 0.745

True cuenta como 1 al sumar, así que caidas.sum() es el número de días que cumplen la condición. La tercera línea es un pequeño estudio de evento: la rentabilidad media del día siguiente a una caída fuerte; el desfase r[1:] frente a caidas[:-1] hace que solo se use la información disponible al cierre del día de la caída. Con 45 casos, un −0,077 % no permite concluir nada; el método sí sirve. La media móvil con np.convolve funciona, pero es la última vez que se calcula así en el curso: pandas lo hace en una línea y con las fechas alineadas (lección 0.5.3).

Broadcasting: operar con formas distintas

cartera_sint.csv tiene los cierres de cuatro activos ficticios: RVAR (renta variable), RFIJ (renta fija), ORO y TECH. En NumPy son una matriz con una fila por sesión y una columna por activo, y las rentabilidades se calculan igual que con un solo activo, porque el corte [1:] actúa sobre las filas:

IN [06]python
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
                      index_col="fecha")
activos = cartera.columns.to_list()
P = cartera.to_numpy()
R = P[1:] / P[:-1] - 1
medias = R.mean(axis=0)
print(activos, P.shape, R.shape, medias.shape)
print((medias * 252).round(4))
OUT [06]Matriz de rentabilidades de la cartera
['RVAR', 'RFIJ', 'ORO', 'TECH'] (1303, 4) (1302, 4) (4,)
[0.1089 0.0053 0.0019 0.1232]

axis=0 recorre las filas y deja una media por columna. Para restar a cada activo su media, la matriz (1302, 4) y el vector (4,) tienen que operar juntos. NumPy lo permite con broadcasting: compara las formas empezando por la derecha y, en cada dimensión, acepta que sean iguales o que una de las dos valga 1 (o no exista, que cuenta como 1). La dimensión de tamaño 1 se «estira» sin copiar datos.

Forma AForma BResultadoPor qué
(1302, 4)(4,)(1302, 4)4 con 4; la B no tiene filas: cuenta como 1
(1302, 4)(1302, 1)(1302, 4)4 con 1; 1302 con 1302
(1302, 4)(1302,)Error4 con 1302: ni iguales ni 1
IN [07]python
X = R - medias
print(X.shape)
print(np.abs(X.mean(axis=0)).max() < 1e-15)

Z = X / R.std(axis=0, ddof=1)   # rentabilidades estandarizadas
print(Z.std(axis=0, ddof=1).round(6))
OUT [07]Restar la media de cada activo
(1302, 4)
True
[1. 1. 1. 1.]

Tras restar la media, cada columna tiene media cero salvo errores de redondeo minúsculos (por eso se compara con una tolerancia y no con == 0; la coma flotante se vio en la lección 0.3.1). Dividir por la desviación típica de cada columna estandariza las cuatro series: ya se pueden comparar en la misma escala.

La tercera fila de la tabla es el error típico. La rentabilidad de RVAR es un vector (1302,), y restarla a la matriz para obtener el rendimiento relativo de cada activo frente a la renta variable falla:

IN [08]python
R - R[:, 0]
OUT [08]Formas incompatibles
ValueError: operands could not be broadcast together with shapes (1302,4) (1302,)

La solución es dar a ese vector la forma de una columna, (1302, 1), con None (o np.newaxis) en el índice:

IN [09]python
relativa = R - R[:, 0][:, None]
print(relativa.shape)
print((relativa.mean(axis=0) * 252).round(4))
OUT [09]Rentabilidad relativa a RVAR
(1302, 4)
[ 0.     -0.1036 -0.107   0.0143]

La primera columna es cero (RVAR menos sí misma). Cuando un broadcasting falla, imprime las formas (.shape) de los dos operandos y compáralas por la derecha: casi siempre falta o sobra una dimensión.

Álgebra lineal: covarianzas y volatilidad de una cartera

La matriz de covarianzas resume cómo se mueven los activos juntos. Con las rentabilidades ya centradas (X), es un producto de matrices: Σ=X⊤X/(n−1)\Sigma = X^\top X / (n - 1). El operador @ es el producto matricial de NumPy, y np.cov hace la misma cuenta:

IN [10]python
n = R.shape[0]
S_manual = X.T @ X / (n - 1)
S = np.cov(R, rowvar=False)
print(np.allclose(S_manual, S))

S_anual = S * 252
volatilidades = np.sqrt(np.diag(S_anual))
correlaciones = np.corrcoef(R, rowvar=False)
for nombre, vol in zip(activos, volatilidades):
    print(f"{nombre:<5}{vol:7.2%}")
print(correlaciones.round(2))
OUT [10]Matriz de covarianzas
True
RVAR  16.83%
RFIJ   4.96%
ORO   15.11%
TECH  28.27%
[[ 1.   -0.19  0.12  0.85]
 [-0.19  1.    0.19 -0.14]
 [ 0.12  0.19  1.    0.02]
 [ 0.85 -0.14  0.02  1.  ]]

rowvar=False le dice a np.cov que las variables (los activos) están en las columnas. La diagonal de la matriz son las varianzas de cada activo; anualizadas (×252 sesiones) y con raíz cuadrada, las volatilidades. La correlación de 0,85 entre TECH y RVAR y la negativa de RFIJ con los dos son las que hacen interesante combinar los activos.

La varianza de una cartera con pesos ww es la forma cuadrática w⊤Σww^\top \Sigma w. Con pesos iguales:

IN [11]python
w = np.full(4, 0.25)
vol_cartera = np.sqrt(w @ S_anual @ w)
media_vols = w @ volatilidades
print(f"Media ponderada de volatilidades: {media_vols:.2%}")
print(f"Volatilidad de la cartera:        {vol_cartera:.2%}")
OUT [11]Volatilidad de la cartera equiponderada
Media ponderada de volatilidades: 16.29%
Volatilidad de la cartera:        11.68%

La cartera es bastante menos volátil que la media de sus componentes (la figura de la cabecera compara las seis cifras): las correlaciones bajas o negativas compensan parte de los movimientos. Esa diferencia es la diversificación, y solo aparece si se usa la matriz completa; la media ponderada de volatilidades la ignora.

np.linalg resuelve sistemas de ecuaciones lineales, invierte matrices y calcula autovalores. Un ejemplo con consecuencia práctica: la cartera de mínima varianza (totalmente invertida) tiene pesos proporcionales a Σ−11\Sigma^{-1}\mathbf{1}. En lugar de invertir la matriz, se resuelve el sistema Σx=1\Sigma x = \mathbf{1} con np.linalg.solve, que es más estable numéricamente:

IN [12]python
x = np.linalg.solve(S_anual, np.ones(4))
w_mv = x / x.sum()
for nombre, peso in zip(activos, w_mv):
    print(f"{nombre:<5}{peso:7.1%}")
print(f"Volatilidad: {np.sqrt(w_mv @ S_anual @ w_mv):.2%}")
OUT [12]Cartera de mínima varianza
RVAR   17.7%
RFIJ   85.7%
ORO     1.0%
TECH   -4.4%
Volatilidad: 4.40%

El resultado concentra la cartera en renta fija y pone un peso negativo (una posición corta) en TECH: es lo que pide una optimización que solo mira la varianza. Por qué estas carteras son tan sensibles a la estimación de Σ\Sigma y cómo se acotan se ve en la lección 0.7.3; aquí basta con saber que @ y np.linalg hacen el trabajo.

Números aleatorios reproducibles

Simulaciones de Monte Carlo, bootstrap de rentabilidades, particiones de validación: el trading cuantitativo usa aleatoriedad en todas partes y necesita que sea reproducible. NumPy ofrece dos interfaces:

  • La heredada: np.random.seed(42) seguido de np.random.normal(...). Fija la semilla de un generador global que comparte todo el programa, incluidas las librerías que importes.
  • La actual: rng = np.random.default_rng(42) crea un Generator propio (algoritmo PCG64) que se pasa como argumento a quien lo necesite. La política de NumPy (NEP 19) recomienda esta para todo código nuevo; la heredada se mantiene solo por compatibilidad.

El problema del estado global se ve con dos funciones que comparten ese generador:

IN [13]python
def simular_cartera():
    return np.random.normal(0.0004, 0.01, size=3).round(4)

def otra_libreria():
    np.random.normal(size=5)  # consume números del mismo generador

np.random.seed(42)
print(simular_cartera())
np.random.seed(42)
otra_libreria()
print(simular_cartera())
OUT [13]El problema del estado global
[ 0.0054 -0.001   0.0069]
[-0.0019  0.0162  0.0081]

Misma semilla, resultados distintos: entre la semilla y la simulación, otra parte del programa gastó números. En un proyecto real, ese «otra_libreria» puede ser una función de validación cruzada o un paquete que ni sabes que usa np.random. Con un Generator explícito, cada componente consume solo el suyo:

IN [14]python
def simular_rentabilidades(rng, dias, mu=0.0004, sigma=0.01):
    return rng.normal(mu, sigma, size=dias)

a = simular_rentabilidades(np.random.default_rng(2026), 252)
b = simular_rentabilidades(np.random.default_rng(2026), 252)
print(np.array_equal(a, b))

# corrientes independientes para procesos en paralelo
hijos = np.random.default_rng(2026).spawn(3)
print([round(float(g.normal()), 4) for g in hijos])
OUT [14]Generadores explícitos
True
[0.1107, 0.544, 1.403]

spawn deriva generadores hijos estadísticamente independientes a partir de uno padre: es la forma correcta de repartir aleatoriedad entre procesos (lección 0.4.6) sin que dos de ellos repitan la misma secuencia. float(...) pasa el escalar de NumPy a un float de Python para que la lista se imprima sin np.float64.

Un uso típico en trading es el bootstrap: remuestrear con reposición las rentabilidades observadas para ver cuánto podría variar un estadístico. Por ejemplo, la rentabilidad anualizada de SINT:

IN [15]python
rng = np.random.default_rng(7)
idx = rng.integers(0, len(r), size=(5_000, len(r)))
medias_boot = r[idx].mean(axis=1) * 252
p5, p95 = np.percentile(medias_boot, [5, 95])
print(f"Observada: {r.mean() * 252:.1%}")
print(f"Intervalo del 90 %: de {p5:.1%} a {p95:.1%}")
OUT [15]Bootstrap de la rentabilidad media
Observada: 8.6%
Intervalo del 90 %: de -4.1% a 21.7%

idx es una matriz de 5.000 × 1.302 índices aleatorios; r[idx] usa esa matriz como índice (fancy indexing) y devuelve 5.000 muestras de golpe, sin un solo bucle. El intervalo es muy ancho: cinco años de datos dicen poco de la rentabilidad esperada, como anticipaba la lección 0.1.2.

Valores ausentes: NaN

Los datos reales tienen huecos: una sesión sin cotización, un fallo del proveedor, un activo que todavía no existía. En NumPy, un hueco en un array de float es np.nan (not a number), y tiene dos propiedades peligrosas:

IN [16]python
con_hueco = cierres[:6].copy()
con_hueco[3] = np.nan          # sesión sin cotización
r_hueco = con_hueco[1:] / con_hueco[:-1] - 1
print(r_hueco.round(4))
print(r_hueco.mean(), np.nanmean(r_hueco).round(5))
print(np.isnan(r_hueco).sum(), "rentabilidades perdidas")
print(np.nan == np.nan, r_hueco > 0)
OUT [16]NaN se propaga
[ 0.0011 -0.0188     nan     nan -0.0039]
nan -0.00722
2 rentabilidades perdidas
False [ True False False False False]

Primero, se propaga: un solo cierre ausente deja dos rentabilidades en nan y la media de todo el array en nan. np.nanmean, np.nansum o np.nanstd ignoran los ausentes, pero hay que pedirlo. Segundo, toda comparación con nan es falsa, incluso nan == nan; por eso se detecta con np.isnan, no con ==. Para una señal, eso significa que r > 0 devuelve False en un día sin dato, y la regla lo trata como «no comprar» sin avisar.

Ejercicios

  1. Calcula la volatilidad anualizada de cada uno de los cuatro activos sin usar np.cov: solo con R, broadcasting y np.sqrt. Comprueba que coincide con volatilidades.
Solución
IN [17]python
varianzas = ((R - R.mean(axis=0)) ** 2).sum(axis=0) / (len(R) - 1)
vol_manual = np.sqrt(varianzas * 252)
print(vol_manual.round(4))
print(np.allclose(vol_manual, volatilidades))
OUT [17]Volatilidad sin np.cov
[0.1683 0.0496 0.1511 0.2827]
True

La varianza es la media de los cuadrados de las desviaciones (con n−1n - 1 en el denominador, como hace np.cov por defecto); es la diagonal de la matriz de covarianzas.

  1. Con pesos del 60 % en RVAR y del 40 % en RFIJ (y cero en los demás), calcula la volatilidad de la cartera y compárala con la media ponderada de las dos volatilidades. ¿Por qué la diferencia es mayor que en la cartera equiponderada?
Solución
IN [18]python
w_6040 = np.array([0.6, 0.4, 0.0, 0.0])
print(f"Cartera:        {np.sqrt(w_6040 @ S_anual @ w_6040):.2%}")
print(f"Media ponderada: {w_6040 @ volatilidades:.2%}")
OUT [18]Cartera 60/40
Cartera:        9.92%
Media ponderada: 12.08%

La correlación entre RVAR y RFIJ es negativa (−0,19): parte de las caídas de uno coinciden con subidas del otro. Cuanto menor es la correlación entre los componentes, más se separa la volatilidad de la cartera de la media de las volatilidades.

  1. Simula con default_rng(11) 1.000 trayectorias de un año (252 sesiones) de rentabilidades normales con la media y la desviación típica diarias de SINT. ¿En qué porcentaje de trayectorias el año cierra en pérdidas?
Solución
IN [19]python
rng = np.random.default_rng(11)
anos = rng.normal(r.mean(), r.std(ddof=1), size=(1_000, 252))
final = np.prod(1 + anos, axis=1) - 1
print(f"Años en pérdidas: {(final < 0).mean():.1%}")
print(f"Mediana: {np.median(final):.1%}")
OUT [19]Mil años simulados de SINT
Años en pérdidas: 36.7%
Mediana: 6.7%

Cada fila es un año simulado; np.prod(..., axis=1) compone las 252 rentabilidades de cada fila. La simulación asume rentabilidades normales e independientes, una simplificación que el nivel 1 revisa con datos reales (colas gruesas, agrupamiento de la volatilidad).

Respuesta plegada en cada pregunta

Autoevaluación

  1. R es una matriz de rentabilidades de forma (1302, 4) y m = R.mean(axis=0). ¿Qué forma tiene R - m?

    • A(4,)
    • B(1302,)
    • C(1302, 4): m se estira a lo largo de las filas
    • DError: las formas no coinciden
    Ver respuesta

    Respuesta C. (1302, 4): m se estira a lo largo de las filas

    Broadcasting compara las dimensiones por la derecha: 4 con 4 coincide y la dimensión que falta en m se trata como 1, así que la media de cada columna se resta a todas las filas.

  2. ¿Por qué la documentación de NumPy desaconseja np.random.seed(42) en código nuevo?

    • APorque es más lento que default_rng
    • BPorque fija un estado global que comparte todo el programa: cualquier otra llamada a np.random cambia la secuencia y los resultados dejan de ser reproducibles por partes
    • CPorque ya no existe en NumPy 2
    • DPorque solo admite semillas pequeñas
    Ver respuesta

    Respuesta B. Porque fija un estado global que comparte todo el programa: cualquier otra llamada a np.random cambia la secuencia y los resultados dejan de ser reproducibles por partes

    np.random.seed sigue existiendo, pero gobierna un generador global heredado. Un Generator propio, creado con default_rng(semilla), se pasa como argumento y nadie más lo consume.

  3. Un array de cierres tiene un NaN por una sesión sin cotización. ¿Qué devuelve np.mean de sus rentabilidades?

    • ALa media sin ese día
    • BCero
    • Cnan: el valor ausente se propaga a cualquier operación que lo toque
    • DUn error
    Ver respuesta

    Respuesta C. nan: el valor ausente se propaga a cualquier operación que lo toque

    Un NaN contamina toda la operación. Para ignorarlo hay que pedirlo (np.nanmean) o filtrarlo con una máscara; nunca se rellena con cero sin pensar, porque un cero es un dato.

  4. Una cartera tiene pesos w y matriz de covarianzas anualizada S. ¿Qué expresión da su volatilidad anual?

    • Anp.sqrt(w @ S @ w)
    • B(w * np.sqrt(np.diag(S))).sum()
    • CS.sum() * w.sum()
    • Dnp.linalg.solve(S, w)
    Ver respuesta

    Respuesta A. np.sqrt(w @ S @ w)

    La varianza de la cartera es la forma cuadrática w′Σw. La segunda opción es la media ponderada de las volatilidades, que ignora las correlaciones y sobrestima el riesgo de una cartera diversificada.

Ideas clave

  • Un ndarray guarda números de un solo tipo en memoria contigua; las operaciones se aplican a todo el array en código compilado, sin bucles de Python.
  • Broadcasting alinea las formas por la derecha: una media por columna (4,) se resta a una matriz (n, 4) sin copiarla.
  • La volatilidad de una cartera es np.sqrt(w @ S @ w): depende de las correlaciones y suele ser menor que la media de las volatilidades.
  • Aleatoriedad reproducible: un Generator propio con default_rng(semilla), pasado como argumento; nunca el estado global de np.random.seed.
  • NaN se propaga y las comparaciones con él son siempre falsas: cuenta los ausentes antes de calcular y decide qué hacer con ellos.

Recursos