---
title: "2.1.8 Pruebas múltiples: Bonferroni, FDR y «factor zoo»"
description: "Mil estrategias sintéticas y cinco con ventaja real: cuántas pasan p < 0,05, qué dejan Bonferroni, Holm y Benjamini-Hochberg, y el «factor zoo»."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/pruebas-multiples-bonferroni-fdr-y-factor-zoo/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/pruebas-multiples-bonferroni-fdr-y-factor-zoo.png"
---

# 2.1.8 · El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»

> Quien prueba mil estrategias encuentra decenas «significativas» aunque ninguna funcione. Esta lección lo mide con mil estrategias sintéticas de las que se sabe cuáles tienen ventaja, aplica las correcciones clásicas y enseña su precio: con cuatro años de datos, ni las buenas sobreviven. Después, lo mismo con las características de un universo de acciones.

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.1 · Estadística y econometría para trading
- Duración: 70 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/), [1.4.4 · Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo](https://pythonparatrading.com/curso-python-trading/nivel-1/sesgo-de-supervivencia-y-point-in-time/)

## Al terminar esta lección

- Explicar por qué un p-valor pierde su significado cuando se elige el mejor de muchos contrastes y calcular cuántos falsos positivos cabe esperar.
- Aplicar Bonferroni, Holm, Benjamini-Hochberg y Benjamini-Yekutieli con `statsmodels` y distinguir error por familia (FWER) de tasa de falsos descubrimientos (FDR).
- Relacionar el umbral de significación con el Sharpe y los años de datos necesarios para que una ventaja real lo supere.
- Reconocer en un «factor zoo» los tres errores: falsos positivos por azar, confusión entre características correladas y efectos reales que no se ven.

## Mil estrategias, cinco buenas

Un p-valor responde a una pregunta concreta: si esta estrategia no tuviera ventaja, ¿qué probabilidad habría de ver
un resultado tan bueno como el observado? Con un umbral de 0,05, una estrategia sin ventaja pasa el contraste una de
cada veinte veces. Si se prueba una, es un riesgo razonable. Si se prueban mil y se presenta la mejor, el p-valor ya
no responde a la pregunta que importa, que es otra: **¿qué probabilidad hay de que la mejor de mil parezca así de
buena por azar?** Casi toda.

Este es el corazón de la investigación cuantitativa honesta, y la razón de las lecciones 2.2.4 (registrar cada
experimento), 2.5.4 (Deflated Sharpe y PBO) y 2.5.2 (validación fuera de la muestra). Aquí se mide con
`ruido2_estrategias.parquet`: los rendimientos diarios de 1.000 «estrategias» sintéticas de 2022 a 2025, en 50
familias de 20 variantes correladas entre sí, como las que salen de barrer los parámetros de una misma idea. Todas
tienen ventaja cero salvo cinco, cada una en una familia distinta, y `ruido2_verdad.csv` dice cuáles: la respuesta
conocida. Son datos inventados; ninguna cifra de esta lección dice nada de una estrategia real.

```mermaid
flowchart TB
  I[Una idea] --> V[Veinte variantes]
  V --> F[Cincuenta familias]
  F --> T[Mil contrastes]
  T --> M[Se elige la mejor]
  M --> P{"¿p < 0,05?"}
  P -- casi siempre --> E[Parece un hallazgo]
```

```python
import numpy as np
import pandas as pd
from scipy import stats

ruido = pd.read_parquet("ruido2_estrategias.parquet")
verdad = pd.read_csv("ruido2_verdad.csv", index_col="estrategia")
R = ruido.pivot(index="fecha", columns="estrategia",
                values="rendimiento")
n = len(R)
sharpe = R.mean() / R.std() * np.sqrt(252)
t, p = stats.ttest_1samp(R, 0.0)
t = pd.Series(t, index=R.columns)
p = pd.Series(p, index=R.columns)
print(n, "sesiones,", R.shape[1], "estrategias")
print("con p < 0,05:", (p < 0.05).sum(),
      "| esperadas si ninguna tuviera ventaja:", 0.05 * R.shape[1])
print("de ellas, con rendimiento medio positivo:",
      ((p < 0.05) & (t > 0)).sum())
```
```output
1003 sesiones, 1000 estrategias
con p < 0,05: 46 | esperadas si ninguna tuviera ventaja: 50.0
de ellas, con rendimiento medio positivo: 25
```

46 estrategias pasan el umbral del 5 % en un contraste bilateral, cerca de las 50 que daría el puro azar, y casi la
mitad (21) lo pasan por ser significativamente **malas**. El contraste es el de la media de los rendimientos diarios (una t de
Student), que equivale a contrastar el Sharpe: con $n$ sesiones,
$t = \overline{r}/s \cdot \sqrt{n} = \text{SR}_{\text{anual}} \cdot \sqrt{n / 252}$, es decir, $t \approx$ Sharpe anual
por la raíz de los años. ¿Dónde están las cinco buenas?

```python
buenas = verdad.index[verdad["sharpe_real"] > 0]
puesto = sharpe.rank(ascending=False).astype(int)
pd.DataFrame({"sharpe_real": verdad.loc[buenas, "sharpe_real"],
              "sharpe_medido": sharpe[buenas], "t": t[buenas],
              "p": p[buenas], "puesto": puesto[buenas]}).round(4)
```
```output
            sharpe_real  sharpe_medido       t       p  puesto
estrategia
e0080               0.5         0.9508  1.8969  0.0581      31
e0133               0.8         1.4586  2.9099  0.0037       3
e0258               0.7         0.6216  1.2402  0.2152     121
e0518               1.5         1.5791  3.1504  0.0017       1
e0751               0.6         0.8320  1.6599  0.0973      56
```

Dos de las cinco tienen p < 0,05; las otras tres, con Sharpes verdaderos de 0,5 a 0,7, no se distinguen del ruido en
cuatro años. Y en la parte alta de la clasificación la verdad se mezcla con el azar:

```python
top = sharpe.nlargest(8)
pd.DataFrame({"sharpe": top, "p": p[top.index],
              "familia": verdad.loc[top.index, "familia"],
              "ventaja_real": verdad.loc[top.index, "sharpe_real"] > 0}
             ).round(4)
```
```output
            sharpe       p familia  ventaja_real
estrategia
e0518       1.5791  0.0017     f03          True
e0598       1.5030  0.0028     f22         False
e0133       1.4586  0.0037     f37          True
e0316       1.4227  0.0046     f22         False
e0124       1.4105  0.0050     f35         False
e0462       1.4089  0.0050     f35         False
e0542       1.3857  0.0058     f22         False
e0401       1.3549  0.0070     f24         False
```

La figura de la cabecera ordena las sesenta mejores. La segunda de las 1.000, `e0598`, tiene un Sharpe medido de 1,50
en cuatro años y ninguna ventaja: es lo mejor del ruido. Seis de las ocho primeras son ruido. Un analista que eligiera
«la mejor» tendría casi las mismas posibilidades de quedarse con una estrategia vacía que con la buena, y no tendría
forma de saberlo mirando los números.

## Corregir: error por familia y falsos descubrimientos

Las correcciones clásicas responden a dos preguntas distintas.

**Error por familia** (FWER, *family-wise error rate*): la probabilidad de cometer **al menos un** falso positivo entre
los $m$ contrastes. **Bonferroni** lo controla exigiendo $p_i < \alpha / m$ a cada uno: con $m = 1.000$ y
$\alpha = 0{,}05$, $p < 0{,}00005$. **Holm** hace lo mismo de forma escalonada y algo más potente: ordena los
p-valores de menor a mayor y compara el $k$-ésimo con $\alpha / (m - k + 1)$, parando en el primero que no pasa.

**Tasa de falsos descubrimientos** (FDR, *false discovery rate*): la proporción esperada de falsos positivos **entre
los descubrimientos**. Es una exigencia más suave, pensada para cribas en las que se acepta que alguno de los elegidos
sea falso. **Benjamini-Hochberg** (1995) ordena los p-valores, $p_{(1)} \le \dots \le p_{(m)}$, y declara
descubrimientos los $k$ primeros, con

$$
k = \max \left\{ i : p_{(i)} \le \frac{i}{m}\, q \right\}.
$$

Garantiza FDR ≤ q con contrastes independientes o con dependencia positiva; para dependencia arbitraria,
**Benjamini-Yekutieli** divide además q por $\sum_{i=1}^{m} 1/i$ (unos 7,5 con $m = 1.000$).

```mermaid
flowchart TB
  Q{"¿Qué error duele?"} -- uno solo --> W[FWER]
  Q -- una proporción --> D[FDR]
  W --> B[Bonferroni, Holm]
  D --> H{"¿Dependencia?"}
  H -- positiva --> BH[Benjamini-Hochberg]
  H -- cualquiera --> BY[Benjamini-Yekutieli]
```

`statsmodels` tiene las cuatro en `multipletests`:

```python
from statsmodels.stats.multitest import multipletests

es_buena = R.columns.isin(buenas)
pb = p[es_buena]
filas = {"sin corregir": {"descubrimientos": int((p < 0.05).sum()),
                          "con_ventaja": int((pb < 0.05).sum()),
                          "p_min_ajustado": p.min()}}
for metodo, nombre in [("bonferroni", "Bonferroni"), ("holm", "Holm"),
                       ("fdr_bh", "Benjamini-Hochberg"),
                       ("fdr_by", "Benjamini-Yekutieli")]:
    rechaza, p_aj, _, _ = multipletests(p, alpha=0.05, method=metodo)
    filas[nombre] = {"descubrimientos": int(rechaza.sum()),
                     "con_ventaja": int(rechaza[es_buena].sum()),
                     "p_min_ajustado": p_aj.min()}
pd.DataFrame(filas).T.astype({"descubrimientos": int,
                               "con_ventaja": int})
```
```output
                     descubrimientos  con_ventaja  p_min_ajustado
sin corregir                      46            2        0.001679
Bonferroni                         0            0        1.000000
Holm                               0            0        1.000000
Benjamini-Hochberg                 0            0        0.829541
Benjamini-Yekutieli                0            0        1.000000
```

Ninguna sobrevive a ninguna corrección, tampoco las cinco buenas. El p-valor más pequeño de las 1.000 (0,0017, el de
`e0518`, que sí tiene ventaja) se convierte en 1 tras Bonferroni y en un valor muy por encima de 0,05 tras
Benjamini-Hochberg. Ver el procedimiento de Benjamini-Hochberg en los primeros puestos ayuda a entender por qué:

```python
orden = p.sort_values()
k = np.arange(1, 9)
paso = pd.DataFrame(
    {"p": orden.values[:8],
     "umbral_bh": 0.05 * k / len(p),
     "ventaja_real": verdad.loc[orden.index[:8], "sharpe_real"]
     .to_numpy() > 0},
    index=pd.Index(orden.index[:8], name="estrategia"))
t_bonf = stats.t.isf(0.05 / len(p) / 2, n - 1)
print(f"Bonferroni exige |t| > {t_bonf:.2f}, un Sharpe anual de "
      f"{t_bonf * np.sqrt(252 / n):.2f} en {n} sesiones")
print(f"sin corregir basta |t| > {stats.t.isf(0.025, n - 1):.2f}: "
      f"Sharpe {stats.t.isf(0.025, n - 1) * np.sqrt(252 / n):.2f}")
paso
```
```output
Bonferroni exige |t| > 4.07, un Sharpe anual de 2.04 en 1003 sesiones
sin corregir basta |t| > 1.96: Sharpe 0.98
                   p  umbral_bh  ventaja_real
estrategia
e0518       0.001679    0.00005          True
e0598       0.002780    0.00010         False
e0133       0.003695    0.00015          True
e0316       0.004628    0.00020         False
e0124       0.004988    0.00025         False
e0462       0.005038    0.00030         False
e0542       0.005807    0.00035         False
e0401       0.006986    0.00040         False
```

*Fig. — Los 40 p-valores más pequeños de las 1.000 estrategias frente al umbral escalonado de Benjamini-Hochberg con q = 0,05.* Escala logarítmica. Cuarenta puntos crecientes, de 0,0017 a 0,0436, todos por debajo de 0,05. La línea destacada del umbral de Benjamini-Hochberg sube de 0,00005 a 0,002 y queda siempre por debajo de los puntos, entre unas 17 y 34 veces en los primeros puestos: no hay ningún descubrimiento.

El umbral de Benjamini-Hochberg para el primer p-valor es $0{,}05/1.000 = 0{,}00005$, el mismo que el de Bonferroni,
y crece despacio: para el octavo, 0,0004. Los p-valores de los primeros puestos están entre 17 y 34 veces por
encima de su umbral. Traducido a Sharpe: sin corregir, en cuatro años basta un Sharpe de 0,98; con Bonferroni sobre 1.000 pruebas
hace falta 2,04, y ninguna de las 1.000 llega, ni siquiera la que tiene un Sharpe verdadero de 1,5.

## El precio: potencia y años de datos

Las correcciones protegen de los falsos positivos y lo pagan en **potencia**: la probabilidad de detectar una ventaja
que sí existe. Con $t \approx$ Sharpe · $\sqrt{\text{años}}$, los años necesarios para que una ventaja real supere un
umbral son $(t_{\text{umbral}} / \text{SR})^2$:

```python
umbrales = {"t 1,96 (una prueba)": 1.96, "t 3,0 (HLZ)": 3.0,
            f"t {t_bonf:.2f} (Bonferroni, 1.000)": t_bonf}
anos = pd.DataFrame({nombre: {sr: (u / sr) ** 2
                              for sr in [0.5, 0.8, 1.0, 1.5]}
                     for nombre, u in umbrales.items()})
anos.rename_axis("sharpe_verdadero").round(1)
```
```output
                  t 1,96 (una prueba)  t 3,0 (HLZ)  t 4.07 (Bonferroni, 1.000)
sharpe_verdadero
0.5                              15.4         36.0                        66.4
0.8                               6.0         14.1                        25.9
1.0                               3.8          9.0                        16.6
1.5                               1.7          4.0                         7.4
```

Un Sharpe verdadero de 0,5, que sería una buena estrategia en la práctica, necesita 15 años de datos para pasar una
sola prueba al 5 % y 66 para pasar Bonferroni con 1.000 pruebas. Incluso un Sharpe de 1,5 necesita más de siete años.
Son años de **datos diarios** suponiendo que la ventaja no cambia en todo ese tiempo, algo que casi nunca se cumple. La
conclusión no es que haya que renunciar a las correcciones, sino a probar mil cosas: el número de pruebas es una
decisión de diseño de la investigación (lección 2.2.4) y cada prueba de más sube el listón de todas.

## Lo que dice la distribución de los p-valores

Si ninguna estrategia tuviera ventaja, los p-valores se repartirían de forma uniforme entre 0 y 1: unos 50 en cada
tramo de 0,05. Las estrategias con ventaja deberían acumularse cerca de 0. El histograma da una idea de cuántas
hipótesis nulas son verdaderas, sin saber cuáles:

```python
conteo, _ = np.histogram(p, bins=20, range=(0, 1))
print("p-valores por tramos de 0,05:", conteo.tolist())
pi0 = (p > 0.5).mean() / 0.5
print(f"proporción estimada de nulas verdaderas (Storey, "
      f"lambda = 0,5): {pi0:.2f}")
```
```output
p-valores por tramos de 0,05: [46, 45, 49, 48, 54, 53, 42, 57, 58, 49, 48, 48, 56, 47, 39, 57, 47, 55, 50, 52]
proporción estimada de nulas verdaderas (Storey, lambda = 0,5): 1.00
```

*Fig. — Histograma de los p-valores de las 1.000 estrategias en tramos de 0,05: plano, como si ninguna tuviera ventaja.* Veinte barras entre 39 y 58 estrategias alrededor de una línea discontinua en 50, lo que daría el azar puro. La primera, destacada, de p < 0,05, tiene 46: no sobresale de las demás.

El histograma es plano: el primer tramo tiene 46 p-valores, menos de los 50 que daría el azar solo. La estimación de
Storey (el doble de la proporción de p-valores mayores que 0,5) sale cerca de 1: los datos son compatibles con que
prácticamente todas las hipótesis nulas sean ciertas, y lo son, salvo cinco de mil. Cuando hay muchas ventajas
reales, el primer tramo destaca claramente sobre los demás; aquí no hay nada que destaque.

Una última precaución: las 20 variantes de cada familia están correladas, así que 1.000 contrastes no son 1.000
pruebas independientes.

```python
familia = verdad.loc[R.columns, "familia"].to_numpy()
C = R.corr().to_numpy()
diag = np.eye(len(familia), dtype=bool)
misma = (familia[:, None] == familia[None, :]) & ~diag
print(f"correlación media dentro de la familia: {C[misma].mean():.2f};"
      f" entre familias: {C[~misma & ~diag].mean():.2f}")
mejor_de_familia = sharpe.groupby(familia).max()
print("familias cuya mejor variante supera un Sharpe de 0,98:",
      (mejor_de_familia > 0.98).sum(), "de", len(mejor_de_familia))
```
```output
correlación media dentro de la familia: 0.50; entre familias: 0.00
familias cuya mejor variante supera un Sharpe de 0,98: 13 de 50
```

Con una correlación de 0,5 dentro de cada familia, el número «efectivo» de pruebas está entre 50 y 1.000: Bonferroni
es aún más conservador de lo necesario. Pero el problema de fondo no cambia: en 13 de las 50 familias la mejor
variante supera el umbral sin corregir (un Sharpe de 0,98), y solo tres de esas trece contienen una estrategia con
ventaja; en una de las tres, la variante que lo supera ni siquiera es la buena. Quien solo mira la mejor variante de su
familia preferida no sabe que las otras diecinueve existían. Las técnicas que tienen en cuenta la dependencia (el Deflated Sharpe con un número efectivo de
pruebas, el PBO, el *reality check* por bootstrap) son materia de las lecciones 2.5.4 y 2.5.5.

## El «factor zoo»

En la investigación académica de la sección cruzada de rentabilidades, el problema tiene nombre. Harvey, Liu y Zhu
(2016) recopilan cientos de factores publicados para explicar las rentabilidades esperadas de las acciones, argumentan
que con esa minería de datos el criterio habitual de una t mayor que 2 no tiene sentido y estiman que un factor nuevo
debería superar una t de 3[^hlz]. Es el «zoo de factores».

El universo de acciones sintético del nivel 2 permite hacer el ejercicio con la respuesta conocida
(`universo2_caracteristicas.parquet`, una fila por acción y fin de mes, point-in-time). El generador planta tres
primas: **momentum 12-1**, **value** (patrimonio entre capitalización) y **baja volatilidad**. Las demás
características no tienen prima propia: **calidad** (ROE), **tamaño**, **reversión a un mes** y la **puntuación ESG**,
un placebo puro, con dos variantes (su cambio a 1 y a 12 meses) como las que probaría cualquiera que buscara «algo»
en esa columna. Para cada característica, el IC de rangos mensual (la correlación de Spearman entre la característica a
fin de mes y la rentabilidad del mes siguiente) y su t:

```python
car = pd.read_parquet("universo2_caracteristicas.parquet")
car = car.sort_values(["ticker", "fecha"]).reset_index(drop=True)
g = car.groupby("ticker")
# la rentabilidad del mes siguiente incluye la fila final de cada
# deslistado (sin precio, con la rentabilidad de deslistado)
car["rent_sig"] = g["rentabilidad_mes"].shift(-1)
con_precio = car["cierre"].notna()
log_r = np.log1p(car["rentabilidad_mes"].where(con_precio))
mom = log_r.groupby(car["ticker"]).transform(
    lambda s: s.shift(1).rolling(11).sum())
car["momentum_12_1"] = np.expm1(mom)
pn = car["patrimonio_neto"].where(car["patrimonio_neto"] > 0)
car["value"] = np.log(pn / car["capitalizacion"])
car["calidad_roe"] = car["roe"]
car["tamano"] = np.log(car["capitalizacion"])
car["reversion_1m"] = car["rentabilidad_mes"]
car["esg"] = car["puntuacion_esg"]
car["esg_cambio_1m"] = g["puntuacion_esg"].diff()
car["esg_cambio_12m"] = g["puntuacion_esg"].diff(12)

# baja volatilidad: desviación de 252 sesiones con precios diarios
aj = pd.read_parquet("universo2_ajustados.parquet")
aj = aj.sort_values(["ticker", "fecha"])
rd = aj.groupby("ticker")["cierre_ajustado"].pct_change()
aj["vol"] = rd.groupby(aj["ticker"]).transform(
    lambda s: s.rolling(252, min_periods=126).std())
car = car.merge(aj[["fecha", "ticker", "vol"]], how="left",
                on=["fecha", "ticker"])
car["baja_vol"] = -car["vol"]
car = car[car["cierre"].notna()]    # señales solo con precio

def ic_mensual(col):
    d = car.dropna(subset=[col, "rent_sig"])
    return d.groupby("fecha")[[col, "rent_sig"]].apply(
        lambda x: x[col].corr(x["rent_sig"], method="spearman"))

senales = ["momentum_12_1", "value", "baja_vol", "calidad_roe",
           "tamano", "reversion_1m", "esg", "esg_cambio_1m",
           "esg_cambio_12m"]
zoo = {}
for s in senales:
    ic = ic_mensual(s)
    t_ic = ic.mean() / ic.std() * np.sqrt(len(ic))
    zoo[s] = {"meses": len(ic), "ic_medio": ic.mean(), "t": t_ic,
              "p": 2 * stats.norm.sf(abs(t_ic))}
zoo = pd.DataFrame(zoo).T
zoo["prima_real"] = ["sí", "sí", "sí", "no", "no", "no", "placebo",
                     "placebo", "placebo"]
zoo.round(4)
```
```output
                meses  ic_medio       t       p prima_real
momentum_12_1   227.0    0.0282  2.9587  0.0031         sí
value           239.0    0.0254  3.0688  0.0021         sí
baja_vol        233.0    0.0130  1.0602  0.2890         sí
calidad_roe     239.0   -0.0324 -4.5933  0.0000         no
tamano          239.0   -0.0064 -0.8239  0.4100         no
reversion_1m    238.0    0.0246  2.8755  0.0040         no
esg             239.0    0.0103  1.5370  0.1243    placebo
esg_cambio_1m   238.0    0.0058  0.9728  0.3306    placebo
esg_cambio_12m  227.0    0.0065  0.9939  0.3203    placebo
```

*Fig. — Un «factor zoo» con respuesta conocida: t del IC de rangos mensual de nueve características del universo sintético, 2006–2025.* Nueve barras horizontales con líneas en ±1,96 y ±3. Momentum 12-1, 2,96; value, 3,07; baja volatilidad, 1,06 (las tres con prima real); calidad, destacada, −4,59 sin prima propia, por confusión con el value; tamaño, −0,82; reversión a un mes, 2,88 sin prima propia; ESG, 1,54, y sus cambios a 1 y 12 meses, 0,97 y 0,99, placebos.

La tabla tiene los tres errores del zoo a la vez:

- **El azar.** El placebo ESG da una t de 1,54: no llega a 1,96, pero con unas pocas variantes más (otra ventana, otro
  universo, otro periodo) alguna lo pasaría, y su p de 0,12 ya «sugiere» algo en un informe poco exigente.
- **La confusión.** La característica con la t más alta de las nueve es la **calidad**, con −4,59, y su prima verdadera
  es cero. El ROE es beneficio entre patrimonio, y en este universo su correlación de rangos con el value es de −0,79:
  hereda la prima del value con el signo cambiado. Supera el umbral de t > 3 y cualquier corrección por pruebas
  múltiples, porque **no es azar**.
- **Los efectos que no se ven.** La baja volatilidad tiene una prima plantada y da una t de 1,06: las acciones volátiles
  tienen más beta, y en rentabilidad bruta las dos cosas se compensan. Solo aparece controlando por beta (lección
  2.3.5). La reversión a un mes (t = 2,88) no tiene prima propia: hereda la continuación del momentum, de la deriva
  tras resultados y de las quiebras.

Las correcciones sobre estas nueve pruebas no arreglan nada de eso:

```python
for metodo in ["bonferroni", "fdr_bh"]:
    rechaza = multipletests(zoo["p"].astype(float), alpha=0.05,
                            method=metodo)[0]
    print(f"{metodo:10}", list(zoo.index[rechaza]))
print("pasan t > 3:", list(zoo.index[zoo["t"].abs() > 3]))

def z_rangos(s):
    r = s.rank()
    return stats.norm.ppf((r - 0.5) / r.count())

def fama_macbeth(cols):
    """t de la media de las pendientes transversales de cada mes."""
    d = car.dropna(subset=cols + ["rent_sig"]).copy()
    for c in cols:
        d[c] = d.groupby("fecha")[c].transform(z_rangos)
    pend = []
    for _, x in d.groupby("fecha"):
        X = np.column_stack([np.ones(len(x)), x[cols]])
        b = np.linalg.lstsq(X, x["rent_sig"], rcond=None)[0]
        pend.append(b[1:])
    pend = pd.DataFrame(pend, columns=cols)
    return pend.mean() / pend.std() * np.sqrt(len(pend))

fm = ["momentum_12_1", "value", "baja_vol", "calidad_roe", "tamano",
      "esg"]
pd.DataFrame({
    "una_a_una": pd.concat([fama_macbeth([c]) for c in fm]),
    "value_y_calidad": fama_macbeth(["value", "calidad_roe"]),
    "todas": fama_macbeth(fm)}).reindex(fm).round(2)
```
```output
bonferroni ['momentum_12_1', 'value', 'calidad_roe', 'reversion_1m']
fdr_bh     ['momentum_12_1', 'value', 'calidad_roe', 'reversion_1m']
pasan t > 3: ['value', 'calidad_roe']
               una_a_una  value_y_calidad  todas
momentum_12_1       4.43              NaN   5.79
value               2.30            -0.15   2.10
baja_vol            1.02              NaN   0.95
calidad_roe        -3.26            -1.80   0.17
tamano              0.05              NaN   0.29
esg                 1.67              NaN   1.63
```

Bonferroni y Benjamini-Hochberg sobre las nueve dejan las mismas cuatro: momentum, value, calidad y reversión a un
mes. Dos de las cuatro no tienen prima propia, y falta la baja volatilidad, que sí la tiene. El umbral de t > 3 deja
solo value y calidad: una real y una falsa, y fuera el momentum, que es real. Las regresiones de Fama-MacBeth (cada mes, una regresión transversal de la rentabilidad del mes siguiente sobre las
puntuaciones por rangos de las señales, y la t de la media de las pendientes) cuentan la historia completa. Una a una,
la calidad da −3,26. Controlar solo por value no la arregla: con value y calidad juntos, la calidad queda en −1,80 y
el value en −0,15. Con dos señales correladas −0,79, la regresión atribuye el efecto a la que no es. Solo con todas
las señales a la vez la calidad cae a 0,17, el value queda en 2,10 y el momentum sube a 5,79, porque el value,
correlado negativamente con él, ya deja de atenuarlo (parte de ese momentum, según el README de los datos, viene de
las cuatro quiebras, cuyos precios se guían a la baja durante más de un año). La baja volatilidad sigue sin verse
(0,95: le falta el control por beta) y el placebo ESG da 1,63, cerca del umbral. Lo que separa aquí lo real de lo
falso es la respuesta conocida; en datos reales, una hipótesis económica previa y otros mercados y periodos (lección
2.2.1).

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

Las correcciones por pruebas múltiples tratan un problema, el azar de elegir lo mejor de muchas pruebas. No detectan
la confusión entre variables correladas, no recuperan efectos tapados por otro, y solo funcionan si se cuentan
**todas** las pruebas, también las que no se escribieron. Por eso valen tanto un registro de experimentos (lección
2.2.4) y una hipótesis anterior a los datos.

</aside>

## Errores típicos

- **Contar solo las pruebas que funcionaron.** Bonferroni con $m = 3$ porque «solo se presentan tres» no corrige nada si
  se probaron cuarenta.
- **Leer «no significativo» como «no funciona».** En esta muestra, la estrategia con Sharpe verdadero de 0,8 sale con
  p = 0,004 y la de 0,6, con p = 0,097: ventajas parecidas a un lado y a otro del umbral. La ausencia de evidencia no
  es evidencia de ausencia.
- **Elegir la corrección después de ver los resultados.** FWER o FDR, y qué α, se deciden antes.
- **Corregir y olvidar la confusión.** La calidad pasa cualquier corrección.
- **Confiar en el mejor de una familia.** Veinte variantes de la misma idea son veinte pruebas.
- **Repetir el estudio sobre los mismos datos.** Cada vuelta a la misma muestra es otra prueba, aunque no esté en el
  código. La única muestra limpia es la que no se ha mirado (lección 2.5.2).

## Ejercicios

1. Repite Benjamini-Hochberg sobre las 1.000 estrategias con q = 0,10, 0,20 y 0,30. ¿Cuántos descubrimientos hay en cada
   caso y cuántos son estrategias con ventaja?

**Solución.**

```python
for q in [0.10, 0.20, 0.30]:
    rechaza = multipletests(p, alpha=q, method="fdr_bh")[0]
    print(f"q = {q:.2f}: {rechaza.sum()} descubrimientos, "
          f"{rechaza[es_buena].sum()} con ventaja")
```
```output
q = 0.10: 0 descubrimientos, 0 con ventaja
q = 0.20: 0 descubrimientos, 0 con ventaja
q = 0.30: 0 descubrimientos, 0 con ventaja
```

Ni con q = 0,30 hay descubrimientos: el primer umbral de Benjamini-Hochberg sería 0,30/1.000 = 0,0003, y el
p-valor más pequeño es 0,0017; el umbral crece con el puesto, pero los p-valores siguientes crecen más deprisa. Subir
q es aceptar una proporción mayor de falsos entre los elegidos a cambio de potencia, y aun así no alcanza. El problema
de estos datos no es el método: cuatro años no contienen información suficiente para separar del ruido Sharpes de
0,5 a 1,5 entre mil candidatos.

2. Elige las 20 estrategias con mejor Sharpe en 2022-2023 y mide su Sharpe en 2024-2025. ¿Cuántas siguen por encima de
   cero? ¿Qué les pasa a las que tienen ventaja?

**Solución.**

```python
primera = R.loc[:"2023-12-31"]
segunda = R.loc["2024-01-01":]
sr = lambda x: x.mean() / x.std() * np.sqrt(252)
elegidas = sr(primera).nlargest(20).index
fuera = sr(segunda[elegidas])
print(f"Sharpe medio de las 20 elegidas: dentro "
      f"{sr(primera[elegidas]).mean():.2f}, fuera {fuera.mean():.2f}")
print("fuera de muestra con Sharpe > 0:", (fuera > 0).sum(), "de 20")
print("con ventaja entre las elegidas:",
      list(elegidas[elegidas.isin(buenas)]))
print("Sharpe fuera de las cinco buenas:",
      sr(segunda[buenas]).round(2).to_dict())
```
```output
Sharpe medio de las 20 elegidas: dentro 2.01, fuera 0.10
fuera de muestra con Sharpe > 0: 12 de 20
con ventaja entre las elegidas: ['e0133', 'e0518']
Sharpe fuera de las cinco buenas: {'e0080': 1.33, 'e0133': -0.04, 'e0258': 0.33, 'e0518': 0.85, 'e0751': 1.01}
```

Dentro de la muestra las 20 elegidas tienen un Sharpe medio de 2,01, porque se eligieron por eso; fuera, de 0,10, y
12 de las 20 quedan por encima de cero, poco más de lo que daría el azar. Solo dos de las cinco con ventaja entran en
la selección (`e0133` y `e0518`), y fuera dan −0,04 y 0,85: con dos años, incluso una ventaja real puede no verse. Las
otras tres, que no se eligieron, dan entre 0,33 y 1,33 en la segunda mitad. Es la forma más simple de validación (lección 2.5.2) y el motivo de que
el Sharpe dentro de la muestra de una estrategia elegida no sea una estimación de su Sharpe futuro.

3. Un compañero ha probado 40 variantes de un indicador y presenta la mejor con t = 2,6 (p = 0,009). ¿Qué le dirías?

**Solución.**

Que con 40 pruebas el p-valor de la mejor no es 0,009. Con Bonferroni, el umbral al 5 % sería 0,05/40 = 0,00125, una
t de unos 3,2; con Holm, el mismo para la primera. Por azar, con 40 variantes sin ventaja se esperan unas 2 con p < 0,05,
y la mejor de 40 nulas independientes pasa p = 0,009 con una probabilidad de $1 - 0{,}991^{40} \approx 30\ \%$. Si las variantes están
correladas, el número efectivo de pruebas es menor y el problema algo más suave, pero sigue ahí. Le pediría el
registro de las 40 pruebas, una hipótesis de por qué debería funcionar, la t corregida o un Deflated Sharpe (lección
2.5.4) y una validación en datos que no haya usado para elegir.

[^hlz]: Resumen del documento de trabajo NBER w20592 (octubre de 2014), publicado en la *Review of Financial Studies*
29(1), 2016, pp. 5-68: «a newly discovered factor needs to clear a much higher hurdle, with a t-ratio greater than
3.0».

## Autoevaluación

1. Pruebas 1.000 estrategias sin ninguna ventaja con un contraste al 5 %. ¿Cuántas esperas que salgan «significativas»?

   - A. Ninguna
   - B. Unas 50: el 5 % de las nulas se rechaza por azar
   - C. Unas 5
   - D. Depende del Sharpe de cada una

   *Respuesta B: Unas 50: el 5 % de las nulas se rechaza por azar.* Esa es la definición del nivel de significación. En los datos del curso salen 46 con p < 0,05, de las que solo dos tienen ventaja real; las otras 44 son azar.

2. ¿Qué controla la corrección de Bonferroni y qué controla la de Benjamini-Hochberg?

   - A. Las dos controlan lo mismo con fórmulas distintas
   - B. Bonferroni, la probabilidad de cometer al menos un falso positivo (FWER); Benjamini-Hochberg, la proporción esperada de falsos positivos entre los descubrimientos (FDR)
   - C. Bonferroni, la potencia; Benjamini-Hochberg, el tamaño
   - D. Bonferroni solo sirve con contrastes independientes y Benjamini-Hochberg con cualquiera

   *Respuesta B: Bonferroni, la probabilidad de cometer al menos un falso positivo (FWER); Benjamini-Hochberg, la proporción esperada de falsos positivos entre los descubrimientos (FDR).* Bonferroni exige p < α/m a cada contraste. Benjamini-Hochberg ordena los p-valores y acepta el mayor k con p_(k) ≤ k·q/m: admite algunos falsos positivos a cambio de más potencia. Con dependencia arbitraria, la versión segura es Benjamini-Yekutieli.

3. La mejor estrategia de las 1.000 tiene un Sharpe medido de 1,58 en cuatro años y p = 0,0017. Tras Bonferroni no es significativa. ¿Significa que no tiene ventaja?

   - A. Sí: Bonferroni demuestra que es ruido
   - B. No: significa que con 1.000 pruebas y cuatro años no hay evidencia suficiente. De hecho tiene ventaja (Sharpe verdadero 1,5), pero haría falta t > 4,07, unos siete años de datos con ese Sharpe
   - C. Sí, porque el Sharpe es menor que 2
   - D. No, porque el p-valor es menor que 0,05

   *Respuesta B: No: significa que con 1.000 pruebas y cuatro años no hay evidencia suficiente. De hecho tiene ventaja (Sharpe verdadero 1,5), pero haría falta t > 4,07, unos siete años de datos con ese Sharpe.* No rechazar no es demostrar la nula. Con t ≈ Sharpe·√años, superar 4,07 con un Sharpe de 1,5 exige (4,07/1,5)² ≈ 7,4 años; con un Sharpe de 0,5, 66 años. Las correcciones protegen de los falsos positivos al precio de no ver ventajas pequeñas.

4. En el universo de acciones del curso, la calidad (ROE) tiene la t más alta de todas las características (−4,59) y su prima verdadera es cero. ¿Qué la explica?

   - A. El azar, que una corrección de Bonferroni eliminaría
   - B. La confusión: el ROE está correlado −0,79 con el value, que sí tiene prima, y hereda su efecto con signo contrario. Ninguna corrección por pruebas múltiples lo arregla
   - C. Un error en los datos
   - D. Que la calidad tiene una prima negativa

   *Respuesta B: La confusión: el ROE está correlado −0,79 con el value, que sí tiene prima, y hereda su efecto con signo contrario. Ninguna corrección por pruebas múltiples lo arregla.* Su t de −4,59 supera cualquier umbral de pruebas múltiples, incluido el t > 3 de Harvey, Liu y Zhu. Las correcciones tratan el azar, no la confusión. Controlar solo por value no basta (la calidad queda en −1,80); con todas las señales plantadas en la regresión cae a 0,17. Y en datos reales hace falta, sobre todo, una hipótesis económica.

5. ¿Qué propone Harvey, Liu y Zhu (2016) para un factor nuevo en la sección cruzada de rentabilidades?

   - A. Usar t > 2, como siempre
   - B. Exigir un umbral más alto, t > 3, porque cientos de factores se han probado sobre los mismos datos
   - C. No publicar factores nuevos
   - D. Usar solo datos fuera de EE. UU.

   *Respuesta B: Exigir un umbral más alto, t > 3, porque cientos de factores se han probado sobre los mismos datos.* Su resumen (NBER w20592; Review of Financial Studies, 2016) argumenta que con tanta minería de datos el criterio habitual de t > 2 no tiene sentido y estima que un factor nuevo debería superar t > 3.

## Ideas clave

- Con 1.000 estrategias sin ventaja, unas 50 pasan p < 0,05. En los datos del curso pasan 46, y solo dos de ellas tienen ventaja real.
- Lo mejor del ruido parece talento: la segunda mejor estrategia de las 1.000 (Sharpe 1,50) no tiene ninguna ventaja.
- Bonferroni y Holm controlan la probabilidad de algún falso positivo; Benjamini-Hochberg, la proporción de falsos entre los descubrimientos. Aquí no sobrevive ninguna, ni siquiera las cinco buenas.
- El umbral es exigente porque la evidencia crece con la raíz del tiempo: t ≈ Sharpe·√años. Un Sharpe de 0,5 necesita 66 años para superar Bonferroni con 1.000 pruebas.
- En un factor zoo el azar no es el único problema: la confusión (calidad frente a value) y los efectos que no se ven sin controles (baja volatilidad) no los resuelve ninguna corrección.

## Recursos

### Research

- [Introduccion al Quant Research](https://pythonparatrading.com/quant-research/) · Research-0023 · 8 min
- [Alpha Factors y Alpha Trading en Python](https://pythonparatrading.com/alpha-factors-y-alpha-trading-en-python/) · Research-0007 · 5 min

### Documentación

- [statsmodels: multipletests](https://www.statsmodels.org/stable/generated/statsmodels.stats.multitest.multipletests.html) · statsmodels.org

### Libros

- [Harvey, C. R., Liu, Y. y Zhu, H. (2016): … and the Cross-Section of Expected Returns. Review of Financial Studies 29(1), 5-68](https://doi.org/10.1093/rfs/hhv059) · doi.org
- [Harvey, Liu y Zhu: … and the Cross-Section of Expected Returns (documento de trabajo NBER w20592, resumen)](https://www.nber.org/papers/w20592) · nber.org
- [Benjamini, Y. y Hochberg, Y. (1995): Controlling the False Discovery Rate: A Practical and Powerful Approach to Multiple Testing. JRSS B 57(1), 289-300](https://doi.org/10.1111/j.2517-6161.1995.tb02031.x) · doi.org

### Datos

- [Datos del nivel 2: 1.000 estrategias ficticias, 2022-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/ruido2_estrategias.parquet) · Descarga · ruido2_estrategias.parquet

Lección anterior: [2.1.7 · Detección de regímenes: modelos de Markov ocultos y cambios estructurales](https://pythonparatrading.com/curso-python-trading/nivel-2/regimenes-hmm-y-cambios-estructurales/)

Lección siguiente: [2.2.1 · Hipótesis de ventaja: riesgo, comportamiento, estructura e información](https://pythonparatrading.com/curso-python-trading/nivel-2/hipotesis-de-ventaja-edge/)
