---
title: "0.7.1 Rentabilidades, interés compuesto y estadística"
description: "Rentabilidad simple y logarítmica, cómo se suman, interés compuesto y CAGR, anualización con √252 y media, volatilidad, asimetría y curtosis de SINT."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/rentabilidades-y-estadistica-descriptiva/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/rentabilidades-y-estadistica-descriptiva.png"
---

# 0.7.1 · Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva

> Casi todas las cifras de un backtest salen de una serie de rentabilidades: la media, la volatilidad, la rentabilidad anual, el peor día. Esta lección fija cómo se calculan, cuál se suma y cuál se multiplica, cómo se anualizan y qué dicen la asimetría y la curtosis de una distribución que no es normal.

- 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.5.2 · pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-3-series-y-dataframe/)

## Al terminar esta lección

- Calcular rentabilidades simples y logarítmicas con pandas y elegir la adecuada para agregar en el tiempo o entre activos.
- Calcular el interés compuesto y la tasa anual compuesta (CAGR) de una serie de precios con fechas reales.
- Anualizar media y volatilidad (×252 y ×√252) y explicar por qué la media aritmética supera a la rentabilidad compuesta.
- Describir una serie de rentabilidades con media, desviación típica, asimetría y curtosis en exceso, con pandas y SciPy.

## Del precio a la rentabilidad

Un precio aislado no dice nada: 142,22 puede ser mucho o poco. Lo que se compara entre activos, periodos y estrategias
es la **rentabilidad**, el cambio relativo del precio. Hay dos formas de medirla, y casi todos los errores de cálculo
de un backtest principiante vienen de mezclarlas.

La **rentabilidad simple** es la variación porcentual de toda la vida:

$$
R_t = \frac{P_t}{P_{t-1}} - 1
$$

La **rentabilidad logarítmica** (o continua) es el logaritmo natural del cociente de precios:

$$
r_t = \ln\frac{P_t}{P_{t-1}} = \ln(1 + R_t)
$$

En pandas, la primera es `pct_change()` y la segunda, la diferencia de los logaritmos de los precios. Con los cierres
de SINT, el ETF ficticio del curso:

```python
import numpy as np
import pandas as pd

precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")
cierre = precios["cierre"]
rent = pd.DataFrame({
    "simple": cierre.pct_change(),
    "log": np.log(cierre).diff(),
}).dropna()
rent.head(3).round(5)
```
```output
             simple      log
fecha
2021-01-05  0.00111  0.00111
2021-01-06 -0.01884 -0.01902
2021-01-07  0.01212  0.01204
```

El primer día desaparece con `dropna()`: no tiene día anterior. En movimientos diarios de una décima o de un punto
porcentual, las dos medidas difieren en centésimas de punto: −1,884 % frente a −1,902 % el segundo día. Se separan cuando el movimiento es grande:

```python
for R in (-0.5, -0.2, -0.05, -0.01, 0.01, 0.05, 0.2, 1.0):
    print(f"simple {R:+7.0%}   log {np.log1p(R):+8.2%}")
```
```output
simple    -50%   log  -69.31%
simple    -20%   log  -22.31%
simple     -5%   log   -5.13%
simple     -1%   log   -1.01%
simple     +1%   log   +1.00%
simple     +5%   log   +4.88%
simple    +20%   log  +18.23%
simple   +100%   log  +69.31%
```

`np.log1p(R)` calcula $\ln(1 + R)$ con más precisión que `np.log(1 + R)` cuando $R$ es muy pequeño. La tabla muestra
la asimetría de la rentabilidad simple: una caída del 50 % es −69,31 % en log, y una subida del 100 % (la que hace
falta para recuperarla, lección 0.1.2) es +69,31 %. En log, perder y recuperar lo perdido pesan lo mismo; en simple,
no.

## Cuál se suma y cuál se multiplica

La diferencia práctica entre las dos está en cómo se agregan.

**En el tiempo**, las simples se componen (se multiplican) y las logarítmicas se suman:

$$
1 + R_{0 \to T} = \prod_{t=1}^{T} (1 + R_t) \qquad r_{0 \to T} = \sum_{t=1}^{T} r_t
$$

```python
total_precios = cierre.iloc[-1] / cierre.iloc[0] - 1
total_simple = (1 + rent["simple"]).prod() - 1
total_log = np.exp(rent["log"].sum()) - 1
suma_simple = rent["simple"].sum()
print(f"Del primer al último cierre: {total_precios:.4%}")
print(f"Producto de (1 + R):         {total_simple:.4%}")
print(f"exp(suma de r) - 1:          {total_log:.4%}")
print(f"Suma de R (mal):             {suma_simple:.4%}")
```
```output
Del primer al último cierre: 43.4537%
Producto de (1 + R):         43.4537%
exp(suma de r) - 1:          43.4537%
Suma de R (mal):             44.5425%
```

Las tres primeras cifras son la misma, calculada de tres formas. La cuarta, sumar rentabilidades simples, es un error
frecuente que aquí infla el resultado en algo más de un punto (44,54 % frente a 43,45 %), y el error crece con la
volatilidad y con el número de periodos: ignora que cada día se gana o se pierde sobre un capital distinto.

**Entre activos**, en un mismo periodo, es al revés: la rentabilidad simple de una cartera es exactamente la media de
las simples de sus activos, ponderada por su peso al empezar el periodo. La logarítmica no cumple esa propiedad. Un día
con dos posiciones del mismo tamaño, una que sube un 20 % y otra que cae un 20 %:

```python
pesos = np.array([0.5, 0.5])
R_dia = np.array([0.20, -0.20])
print(f"Cartera (simples ponderadas): {pesos @ R_dia:+.4%}")
print(f"Media ponderada de los log:   "
      f"{pesos @ np.log1p(R_dia):+.4%}")
```
```output
Cartera (simples ponderadas): +0.0000%
Media ponderada de los log:   -2.0411%
```

La cartera no gana ni pierde: 0 %, y su rentabilidad logarítmica es $\ln(1) = 0$. La media ponderada de los
logaritmos da −2,04 %, una pérdida que no existe. El operador `@` es el producto escalar de la lección 0.5.1; la
lección 0.7.3 lo usa para carteras de verdad.

| Para… | Usa | Porque |
|---|---|---|
| Encadenar periodos (días a años) | Logarítmicas (suma) o simples (producto) | Las dos sirven; la suma de log es más cómoda |
| Combinar activos de una cartera | Simples | La simple de la cartera es la media ponderada de las simples |
| Informar a una persona | Simples | «Ha ganado un 7 %» se entiende; «0,068 en log», no |
| Modelos estadísticos (lección 0.7.2) | Logarítmicas, a menudo | Suman, y son algo más simétricas |

## Interés compuesto y CAGR

El **interés compuesto** es la regla de las rentabilidades simples aplicada a un capital: cada periodo se gana sobre
lo ganado antes. Un capital $C_0$ que rinde una tasa $g$ al año durante $n$ años termina en $C_0 (1 + g)^n$:

```python
capital = 10_000
for anios in (10, 20, 30):
    print(f"{anios} años: {capital * 1.07 ** anios:>9,.0f} €")
```
```output
10 años:    19,672 €
20 años:    38,697 €
30 años:    76,123 €
```

A los treinta años el capital se ha multiplicado por más de siete, y los diez últimos años aportan más que los veinte
primeros. Es el motivo por el que los costes y las comisiones anuales, pequeños cada año, son tan caros a largo plazo
(lección 0.1.2).

La frecuencia con que se compone también cuenta. Un 7 % nominal compuesto cada mes rinde algo más que compuesto una
vez al año, y el límite es la composición continua, $e^{g} - 1$, que es justo la relación entre rentabilidad
logarítmica y simple:

```python
tasa = 0.07
for nombre, m in (("anual", 1), ("mensual", 12), ("diaria", 252)):
    print(f"{nombre:<9}{(1 + tasa / m) ** m - 1:.4%}")
print(f"{'continua':<9}{np.exp(tasa) - 1:.4%}")
```
```output
anual    7.0000%
mensual  7.2290%
diaria   7.2498%
continua 7.2508%
```

La operación inversa responde a la pregunta habitual sobre un histórico: ¿a qué tasa anual constante habría que
crecer para ir del primer precio al último? Es la **tasa anual compuesta**, CAGR (*compound annual growth rate*):

$$
\text{CAGR} = \left(\frac{P_T}{P_0}\right)^{1/\text{años}} - 1
$$

```python
anios = (cierre.index[-1] - cierre.index[0]).days / 365.25
cagr = (cierre.iloc[-1] / cierre.iloc[0]) ** (1 / anios) - 1
print(f"Años: {anios:.2f} · CAGR: {cagr:.2%}")
print(f"Sesiones por año: {len(cierre) / anios:.0f}")
```
```output
Años: 4.99 · CAGR: 7.50%
Sesiones por año: 261
```

Los años se cuentan con las fechas: del 4 de enero de 2021 al 31 de diciembre de 2025 hay 4,99 años. La segunda línea
muestra por qué no conviene contarlos dividiendo sesiones entre 252: SINT no tiene festivos y cotiza 261 días al año,
así que dividir sus 1.303 sesiones entre 252 daría 5,17 años y un CAGR menor del real.

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

El 252 es una convención: las sesiones de un año típico en la bolsa estadounidense. Un futuro que cotiza casi todos
los días, una criptomoneda (365 días) o una serie sin festivos como SINT tienen otro número. Para anualizar medias y
volatilidades, el curso usa 252 por coherencia con la literatura; para el CAGR, siempre las fechas.

</aside>

## Anualizar: media, volatilidad y el coste de la volatilidad

Las rentabilidades diarias son números pequeños; para comparar estrategias se anualizan. Si las rentabilidades de días
distintos son independientes, la media y la varianza crecen con el número de días, y la desviación típica, con su raíz:

$$
\mu_{\text{anual}} = 252\,\mu_{\text{diaria}} \qquad
\sigma_{\text{anual}} = \sqrt{252}\,\sigma_{\text{diaria}}
$$

La desviación típica anualizada es la **volatilidad**, la medida de riesgo más usada. Se puede comprobar con datos
mensuales, que se anualizan con $\sqrt{12}$:

```python
vol_d = rent["simple"].std()
mensual = cierre.resample("ME").last().pct_change().dropna()
vol_m = mensual.std()
print(f"Diaria:  {vol_d:.2%} · anualizada {vol_d * np.sqrt(252):.2%}")
print(f"Mensual: {vol_m:.2%} · anualizada {vol_m * np.sqrt(12):.2%}")
```
```output
Diaria:  1.14% · anualizada 18.10%
Mensual: 4.73% · anualizada 16.39%
```

Las dos anualizadas se parecen pero no coinciden. Con 59 meses, la estimación mensual tiene mucho más error que la
diaria, con 1.302 días; y si los días no fueran del todo independientes (rachas, volatilidad que se agrupa en
episodios), la regla de la raíz dejaría de ser exacta. En la práctica, la volatilidad se estima con datos diarios y se
anualiza con $\sqrt{252}$, sabiendo que es una aproximación.

La media tiene una trampa más. La media aritmética de las rentabilidades simples, anualizada, no es lo que crece el
capital: lo que crece es la media de las logarítmicas. La diferencia es aproximadamente la mitad de la varianza:

$$
\overline{r} \approx \overline{R} - \frac{\sigma^2}{2}
$$

```python
media_R = rent["simple"].mean()
media_r = rent["log"].mean()
varianza = rent["simple"].var()
print(f"Media simple ×252:  {media_R * 252:.2%}")
print(f"Media log ×252:     {media_r * 252:.2%}")
print(f"Diferencia ×252:    {(media_R - media_r) * 252:.2%}")
print(f"σ²/2 ×252:          {varianza / 2 * 252:.2%}")
```
```output
Media simple ×252:  8.62%
Media log ×252:     6.98%
Diferencia ×252:    1.64%
σ²/2 ×252:          1.64%
```

La aproximación acierta casi al céntimo. Es el **coste de la volatilidad** (*volatility drag*): dos estrategias con la
misma media aritmética crecen distinto si una es más volátil. Un folleto que anuncia «un 8,6 % de media anual» con la
media aritmética de SINT promete más de lo que el capital habría crecido de verdad.

## Describir la distribución

Media y volatilidad resumen el centro y la dispersión. Dos estadísticos más dicen cómo es la forma:

- **Asimetría** (*skewness*): negativa si la cola de las pérdidas es más larga que la de las ganancias (caídas bruscas
  y subidas lentas, típico de los índices de acciones); positiva en el caso contrario (típico de las estrategias de
  tendencia, con muchas pérdidas pequeñas y pocas ganancias grandes). La normal tiene 0.
- **Curtosis**: mide el peso de las colas. La normal tiene curtosis 3; la **curtosis en exceso** resta ese 3, y es la
  que devuelven pandas y, por defecto, SciPy. Un valor positivo indica colas más gruesas que la normal: más días
  extremos de los que la normal espera.

```python
from scipy import stats

r = rent["simple"]
print(r.describe().round(4).to_string())
print(f"Asimetría  pandas {r.skew():.3f} · scipy "
      f"{stats.skew(r):.3f}")
print(f"Curtosis exceso  pandas {r.kurt():.3f} · scipy "
      f"{stats.kurtosis(r):.3f}")
```
```output
count    1302.0000
mean        0.0003
std         0.0114
min        -0.0481
25%        -0.0068
50%         0.0006
75%         0.0074
max         0.0508
Asimetría  pandas -0.015 · scipy -0.015
Curtosis exceso  pandas 1.084 · scipy 1.075
```

`describe()` da el recuento, la media, la desviación típica, el mínimo, los cuartiles y el máximo. La mediana (50 %)
queda algo por encima de la media, y el peor día (−4,81 %) y el mejor (+5,08 %) están a más de cuatro desviaciones
típicas del centro. La asimetría es prácticamente cero: SINT no tiene sesgo hacia las caídas. La curtosis en exceso
de 1,08 dice que las colas son más gruesas que las de la normal.

pandas y SciPy no dan exactamente lo mismo. pandas corrige el sesgo de la muestra (como la desviación típica con
$n - 1$); SciPy, por defecto, no (`bias=True`). Con 1.302 datos la diferencia está en el segundo decimal; con 60, no.
Al comparar cifras de dos fuentes, mira con qué función se calcularon.

La figura de la cabecera dibuja la distribución completa: las barras son las rentabilidades diarias de SINT; la curva,
una normal con la misma media y la misma desviación típica. La barra central sobresale por encima de la curva (más
días tranquilos) y en los extremos aparecen barras donde la curva casi no llega. Contados:

```python
fuera = int((r.abs() > 0.035).sum())
normal = stats.norm(loc=r.mean(), scale=r.std())
esperados = len(r) * (normal.cdf(-0.035) + normal.sf(0.035))
print(f"Días con |R| > 3,5 %: {fuera} · normal: {esperados:.1f}")
```
```output
Días con |R| > 3,5 %: 11 · normal: 2.8
```

Casi cuatro veces más días extremos de los que una normal predice. Es la forma de la curtosis que importa en riesgo:
un modelo que supone rentabilidades normales subestima la frecuencia de los días malos. La lección 0.7.2 mide esa
diferencia con probabilidades y prueba una distribución con colas más gruesas, la t de Student.

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

Media, volatilidad y curtosis se estiman con el pasado. Cinco años de datos sin una crisis dan una volatilidad y unas
colas que la siguiente crisis puede desmentir el primer día. Una cifra de riesgo sin el periodo con que se calculó no
se puede valorar.

</aside>

## Ejercicios

1. Comprueba con código el ejemplo de la autoevaluación: un +10 % seguido de un −10 %. Calcula la rentabilidad total
   con simples (producto) y con logarítmicas (suma). ¿Qué subida hace falta tras el −10 % para volver al punto de
   partida?

**Solución.**

```python
dias = np.array([0.10, -0.10])
print(f"Producto de simples: {np.prod(1 + dias) - 1:+.2%}")
print(f"Suma de log:         {np.log1p(dias).sum():+.4f}")
print(f"exp(suma) - 1:       {np.expm1(np.log1p(dias).sum()):+.2%}")
print(f"Subida necesaria:    {1 / 0.9 - 1:+.2%}")
```
```output
Producto de simples: -1.00%
Suma de log:         -0.0101
exp(suma) - 1:       -1.00%
Subida necesaria:    +11.11%
```

Las dos vías dan el mismo −1 %: la suma de log es −0,0101, y $e^{-0{,}0101} - 1 = -1\ \%$. Para recuperar un −10 %
hace falta un +11,11 %. `np.expm1(x)` es $e^x - 1$, el inverso de `np.log1p`.

2. Calcula la volatilidad anualizada, la asimetría y la curtosis en exceso de las rentabilidades diarias de los cuatro
   activos de `cartera_sint.csv`. ¿Cuál tiene las colas más gruesas?

**Solución.**

```python
cartera = pd.read_csv("cartera_sint.csv", parse_dates=["fecha"],
                      index_col="fecha")
rc = cartera.pct_change().dropna()
resumen = pd.DataFrame({
    "vol_anual": rc.std() * np.sqrt(252),
    "asimetria": rc.skew(),
    "curtosis_exceso": rc.kurt(),
})
resumen.round(3)
```
```output
      vol_anual  asimetria  curtosis_exceso
RVAR      0.168      0.013            0.087
RFIJ      0.050     -0.087           -0.197
ORO       0.151      0.048           -0.103
TECH      0.283      0.039            0.147
```

Los métodos de `DataFrame` calculan cada estadístico por columna, así que cada columna de la tabla sale en una
línea. TECH, el más volátil, tiene la curtosis más alta (0,147), pero las cuatro están cerca de cero: estos activos se
generaron con una volatilidad constante y una distribución normal. SINT, con dos episodios de volatilidad alta, llega
a 1,08. Mezclar periodos tranquilos y periodos agitados es una de las causas de las colas gruesas, y en series de
mercado reales las colas suelen ser más gruesas todavía que en SINT.

3. Calcula el CAGR de SINT de cada año natural (del último cierre del año anterior al último del año, y para 2021
   desde el primer cierre) y compáralo con la media aritmética anualizada de las rentabilidades diarias de ese año.
   ¿En qué año es mayor la diferencia y por qué?

**Solución.**

```python
fin = cierre.groupby(cierre.index.year).last()
base = fin.shift(1).fillna(cierre.iloc[0])
anual = pd.DataFrame({
    "rent_anual": fin / base - 1,
    "media_x252": rent["simple"].groupby(
        rent.index.year).mean() * 252,
    "vol": rent["simple"].groupby(rent.index.year).std()
    * np.sqrt(252),
})
anual["diferencia"] = anual["media_x252"] - anual["rent_anual"]
anual.round(3)
```
```output
       rent_anual  media_x252    vol  diferencia
fecha
2021        0.152       0.150  0.160      -0.002
2022       -0.085      -0.055  0.248       0.030
2023        0.142       0.140  0.149      -0.002
2024        0.143       0.140  0.150      -0.003
2025        0.042       0.056  0.179       0.014
```

Cada año es un año completo, así que la rentabilidad del año es su CAGR. En 2022, el año más volátil (24,8 %), la
media aritmética anualizada supera a la rentabilidad real en 3 puntos; en 2025 (17,9 %), en 1,4. Es el coste de la
volatilidad, que crece con su cuadrado. En los años tranquilos la diferencia sale casi nula o ligeramente negativa: con
261 sesiones, multiplicar la media diaria por 252 se queda corto, y ese error compensa un coste de la volatilidad que,
con un 15 %, es de apenas un punto ($0{,}15^2/2 \approx 1{,}1\ \%$).

## Autoevaluación

1. Una acción sube un 10 % un día y baja un 10 % al siguiente. ¿Dónde queda?

   - A. Igual que al principio
   - B. Un 1 % por debajo: 1,10 × 0,90 = 0,99
   - C. Un 1 % por encima
   - D. Depende del precio inicial

   *Respuesta B: Un 1 % por debajo: 1,10 × 0,90 = 0,99.* Las rentabilidades simples se multiplican, no se suman: (1 + 0,10) × (1 − 0,10) − 1 = −1 %. Con rentabilidades logarítmicas sí se suman, y +10 % y −10 % simples son +9,53 % y −10,54 % en log.

2. Tienes las rentabilidades diarias de dos activos y una cartera 50/50 rebalanceada a diario. ¿Cómo calculas la rentabilidad de la cartera de un día?

   - A. Media de las rentabilidades logarítmicas
   - B. Media ponderada de las rentabilidades simples
   - C. Suma de las rentabilidades logarítmicas
   - D. Producto de las rentabilidades simples

   *Respuesta B: Media ponderada de las rentabilidades simples.* Entre activos, en un mismo periodo, la rentabilidad simple de la cartera es exactamente la media ponderada de las simples. La media ponderada de las logarítmicas no es la logarítmica de la cartera.

3. La volatilidad diaria de un activo es del 1 %. ¿Cuál es su volatilidad anualizada con la convención habitual?

   - A. 252 %
   - B. Un 15,9 %, el 1 % multiplicado por la raíz de 252
   - C. Un 12 %, el 1 % por 12 meses
   - D. Un 1 %, la volatilidad no se anualiza

   *Respuesta B: Un 15,9 %, el 1 % multiplicado por la raíz de 252.* Si las rentabilidades diarias son independientes, la varianza crece con el número de días y la desviación típica con su raíz: 1 % × √252 ≈ 15,9 %.

4. Una serie de rentabilidades tiene curtosis en exceso de 1,1 según `Series.kurt()`. ¿Qué significa?

   - A. Que la distribución es normal
   - B. Que tiene más días extremos (y más días tranquilos) que una normal con la misma volatilidad
   - C. Que la media es mayor que la mediana
   - D. Que hay un error: la curtosis no puede ser positiva

   *Respuesta B: Que tiene más días extremos (y más días tranquilos) que una normal con la misma volatilidad.* pandas devuelve la curtosis en exceso: 0 para una normal. Un valor positivo indica colas más gruesas y un centro más alto; en riesgo, más días extremos de los que la normal predice.

## Ideas clave

- Rentabilidad simple: el cierre de hoy entre el de ayer, menos 1; logarítmica: el logaritmo natural de ese cociente, ln(1 + R). Para movimientos diarios pequeños casi coinciden; para movimientos grandes, no.
- En el tiempo, las simples se multiplican y las logarítmicas se suman. Entre activos, la simple de la cartera es la media ponderada de las simples.
- El CAGR se calcula con los años transcurridos según el calendario. La media aritmética anualizada lo supera en torno a σ²/2: la volatilidad resta crecimiento.
- La media se anualiza por 252 y la volatilidad por √252, suponiendo días independientes y el calendario de un mercado real.
- Las rentabilidades reales tienen colas más gruesas que la normal: curtosis en exceso positiva y más días extremos de los que la normal espera.

## Recursos

### Research

- [Estimadores de Volatilidad](https://pythonparatrading.com/estimadores-de-volatilidad-para-trading/) · Research-0002 · 4 min

### Documentación

- [pandas: Series.pct_change](https://pandas.pydata.org/docs/reference/api/pandas.Series.pct_change.html) · pandas.pydata.org
- [pandas: Series.kurt (curtosis en exceso, de Fisher)](https://pandas.pydata.org/docs/reference/api/pandas.Series.kurt.html) · pandas.pydata.org
- [SciPy: scipy.stats.kurtosis](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.kurtosis.html) · docs.scipy.org

### Datos

- [Datos del nivel 0: precios diarios del ETF ficticio SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Descarga · sint_diario.csv

Lección anterior: [0.6.2 · Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script](https://pythonparatrading.com/curso-python-trading/nivel-0/dashboards-con-streamlit/)

Lección siguiente: [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/)
