Lección 5.4.1 · 60 min
Por qué falla el k-fold aleatorio en datos financieros
Barajar las filas antes de validar es lo primero que enseña cualquier curso de machine learning, y en un panel de acciones con etiquetas a 21 sesiones es la forma más rápida de fabricar un modelo que no existe. Esta lección reproduce el caso del README del nivel, mide sus dos ingredientes y enseña que la validación equivocada no solo infla el resultado: también elige el modelo equivocado.
- Módulo
- 5.4 · Validación para series financieras
- Duración
- 60 min
Lecciones del nivel 5
5.1 · Fundamentos de ML aplicados a finanzas
5.2 · Ingeniería de características
5.3 · Etiquetado y muestreo
- 5.3.1 Etiquetado de horizonte fijo y sus problemas
- 5.3.2 Método de la triple barrera: etiquetas que imitan una operación
- 5.3.3 Meta-labeling: un modelo para el lado y otro para el tamaño
- 5.3.4 Unicidad de las etiquetas, pesos por solapamiento y bootstrap secuencial
- 5.3.5 Muestreo por eventos: el filtro CUSUM simétrico
5.4 · Validación para series financieras
5.5 · Modelos clásicos
- 5.5.1 Regresiones regularizadas: Ridge, Lasso, Elastic Net y logística
- 5.5.2 Árboles de decisión, Random Forest y bagging
- 5.5.3 Gradient boosting: XGBoost, LightGBM y CatBoost
- 5.5.4 Optuna y el riesgo de sobreajustar la validación
- 5.5.5 Aprendizaje no supervisado: PCA, clustering y autoencoders
- 5.5.6 Learning to rank para la selección de acciones
5.6 · Deep learning con Keras 3
5.7 · Deep learning con PyTorch
- 5.7.1 PyTorch: tensores, autograd y el bucle de entrenamiento
- 5.7.2 Arquitecturas de series: TCN, N-BEATS, PatchTST e iTransformer
- 5.7.3 Modelos probabilísticos: pérdida cuantílica, DeepAR e intervalos
- 5.7.4 Redes de grafos para relaciones entre activos
- 5.7.5 Optimización de carteras end-to-end: capas diferenciables y pérdida de Sharpe
5.8 · Modelos fundacionales de series temporales
- 5.8.1 Modelos fundacionales de series: preentrenar una vez, prever sin entrenar
- 5.8.2 Las familias de modelos fundacionales: arquitectura, datos y licencia
- 5.8.3 Uso práctico: contexto, cuantiles, covariables y ajuste fino
- 5.8.4 Modelos fundacionales en finanzas: qué dice la evidencia
- 5.8.5 Laboratorio: modelos fundacionales frente a GARCH, HAR y LightGBM
5.9 · Aprendizaje por refuerzo
5.10 · Interpretabilidad, robustez y riesgo de modelo
Un acierto que no puede existir
La lección 2.5.3 midió la fuga del k-fold barajado en una serie de puro ruido: un AUC de 0,93 donde no había nada que predecir. Aquí el problema es el de un modelo de machine learning de verdad: un panel de 400 acciones con 22 features semanales, una señal pequeña plantada y una pregunta de clasificación como las de 5.3.1, «¿quedará esta acción por encima de la mediana de su semana en las próximas 21 sesiones?». El README de los datos del nivel publica el resultado de un random forest con tres validaciones; esta lección lo reproduce y lo desmonta.
El conjunto es el del README: 80 acciones fijas (elegidas con la semilla del generador) en las 725 semanas que tienen
features y etiqueta de 21 sesiones completas, 21 features sin revision_analistas (la fuga plantada, que se deja para
el final) y una etiqueta binaria relativa a la mediana de las 400 acciones de esa semana. Al ser relativa, el
movimiento común del mercado desaparece de la respuesta: lo que se pide es ordenar acciones, no adivinar el índice.
import zlib
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GroupKFold, KFold
feat = pd.read_parquet("panel5_features.parquet")
etiq = pd.read_parquet("panel5_etiquetas.parquet")
d = feat.merge(etiq, on=["fecha", "ticker"])
# semanas completas: features y etiqueta de 21 sesiones en las 400
completa = (d.groupby("fecha")[["ret_1s", "ret_sig_21d"]]
.transform(lambda s: s.notna().all()).all(axis=1))
d = d[completa].copy()
for col, med in (("ret_sig_21d", "med_21d"),
("ret_sig_semana", "med_sem")):
d[med] = d.groupby("fecha")[col].transform("median")
# las 80 acciones del README (la semilla del generador)
tickers = sorted(d["ticker"].unique())
rng = np.random.default_rng([2029, zlib.crc32(b"n5_panel_cv")])
elegidas = [tickers[i] for i in
np.sort(rng.choice(400, 80, replace=False))]
d = (d[d["ticker"].isin(elegidas)]
.sort_values(["fecha", "ticker"]).reset_index(drop=True))
FEATS = [c for c in feat.columns if c not in
("fecha", "ticker", "sector", "revision_analistas")]
X = np.nan_to_num(d[FEATS].to_numpy(dtype=float))
y = (d["ret_sig_21d"] > d["med_21d"]).astype(int).to_numpy()
semana = d["fecha"].rank(method="dense").astype(int).to_numpy() - 1
print(f"{len(d)} filas = {d['ticker'].nunique()} acciones x "
f"{semana.max() + 1} semanas; {len(FEATS)} features; "
f"proporción de unos {y.mean():.3f}")58000 filas = 80 acciones x 725 semanas; 21 features; proporción de unos 0.498El modelo es el del README: un bosque de 60 árboles con hojas de al menos 5 observaciones, cada árbol entrenado con
un 30 % de las filas. Fija random_state y el número de hilos, así que el resultado es el mismo en cada ejecución.
Las tres validaciones, con 5 pliegues cada una:
- k-fold barajado:
KFold(5, shuffle=True), el de cualquier tutorial. - k-fold por bloques:
KFold(5)sin barajar; como las filas van ordenadas por fecha, cada pliegue son 145 semanas seguidas. - purged k-fold: los mismos bloques, quitando del entrenamiento las 4 semanas de cada lado del bloque de prueba (la regla del README; la lección 5.4.2 la programa bien, con las fechas de fin de cada etiqueta).
def bosque(hoja=5):
return RandomForestClassifier(
n_estimators=60, min_samples_leaf=hoja, max_features="sqrt",
max_samples=0.3, n_jobs=4, random_state=0)
def acierto(divisiones, Xv=X, yv=y, hoja=5):
"""Acierto medio fuera de muestra de los pliegues."""
res = []
for ent, pru in divisiones:
m = bosque(hoja).fit(Xv[ent], yv[ent])
res.append((m.predict(Xv[pru]) == yv[pru]).mean())
return float(np.mean(res))
barajado = list(KFold(5, shuffle=True, random_state=0).split(X))
bloques = list(KFold(5).split(X))
purgado = []
for b in np.array_split(np.arange(semana.max() + 1), 5):
lejos = (semana < b[0] - 4) | (semana > b[-1] + 4)
purgado.append((np.flatnonzero(lejos),
np.flatnonzero(np.isin(semana, b))))
res = {"k-fold barajado": acierto(barajado),
"k-fold por bloques": acierto(bloques),
"purged k-fold": acierto(purgado)}
for nombre, a in res.items():
print(f"{nombre:<20}{a:.1%}")k-fold barajado 59.7%
k-fold por bloques 50.0%
purged k-fold 50.1%Barajado, el bosque acierta casi seis de cada diez; por bloques, la mitad, como una moneda. La pregunta es cuál de las dos cifras es la verdad, y en este panel no hace falta fiarse de ninguna: se puede calcular el techo.
El techo: lo que acertaría el oráculo
panel5_verdad.parquet trae alfa_verdadero, el rendimiento esperado extra de cada acción y semana por los efectos
plantados. Es la mejor previsión posible de la parte predecible del rendimiento: un modelo perfecto la recuperaría
entera y no podría hacer nada más, porque el resto es ruido. Si se clasifica cada acción por encima o por debajo de
la mediana del alfa de su semana, el acierto frente a la etiqueta es el máximo alcanzable por cualquier modelo fuera de
muestra (con la misma precisión de etiqueta: la de 21 sesiones recoge además alfas de semanas posteriores, que el
oráculo de esta semana no ve, así que es un techo aproximado).
verdad = pd.read_parquet("panel5_verdad.parquet",
columns=["fecha", "ticker", "alfa_verdadero"])
verdad["med_alfa"] = (verdad.groupby("fecha")["alfa_verdadero"]
.transform("median"))
o = d[["fecha", "ticker"]].merge(verdad, on=["fecha", "ticker"])
acierta = ((o["alfa_verdadero"] > o["med_alfa"]).to_numpy() == y)
reciente = (d["fecha"] >= "2019-01-01").to_numpy()
print(f"oráculo: {acierta.mean():.1%} "
f"(2019-2025: {acierta[reciente].mean():.1%})")
error_iid = np.sqrt(0.25 / len(y))
print(f"error típico si las filas fueran independientes: "
f"{error_iid:.2%}")oráculo: 51.1% (2019-2025: 50.5%)
error típico si las filas fueran independientes: 0.21%El oráculo acierta el 51,1 %. Es la cifra que pone todo en su sitio: el 59,7 % del k-fold barajado está ocho puntos por encima de lo que permite la información que existe, con un error típico (si las filas fueran independientes, que no lo son) de dos décimas. No es un modelo excelente: es una validación rota. El 50,0 % y el 50,1 % de los bloques, por debajo del techo, son lo que el bosque consigue de verdad, que es casi nada.
La figura de la cabecera resume las cuatro cifras, con el techo como referencia.
Anatomía del solape: cinco semanas por etiqueta
La etiqueta de una fila es el rendimiento desde el cierre de fecha hasta el de 21 sesiones después, fin_sig_21d.
Las features son semanales. Por tanto, la etiqueta de un viernes abarca esa semana y las cuatro siguientes, y la de la
semana siguiente comparte con ella cuatro de sus cinco semanas:
una = d[d["ticker"] == elegidas[0]].set_index("fecha")
tabla = una.loc["2019-03-01":"2019-03-29",
["fin_sig_21d", "ret_sig_21d", "med_21d"]].copy()
tabla["etiqueta"] = (tabla["ret_sig_21d"]
> tabla.pop("med_21d")).astype(int)
print(elegidas[0])
tabla.round({"ret_sig_21d": 4})BAKU
fin_sig_21d ret_sig_21d etiqueta
fecha
2019-03-01 2019-04-01 -0.0860 0
2019-03-08 2019-04-08 -0.0042 1
2019-03-15 2019-04-15 -0.0771 0
2019-03-22 2019-04-23 -0.0905 0
2019-03-29 2019-04-30 -0.0156 1El horizonte se cuenta en sesiones, no en días: la etiqueta del 22 de marzo termina el 23 de abril porque el 19 de abril de 2019 (Viernes Santo) la NYSE no abrió. La figura dibuja esas cinco etiquetas como intervalos. Si la del 15 de marzo cae en el conjunto de prueba y las de las dos semanas anteriores y posteriores en el de entrenamiento, el bosque tiene para entrenar cuatro filas de la misma acción cuya respuesta se calcula en su mayor parte con los mismos rendimientos. En este ejemplo dos de las cuatro tienen la misma etiqueta que la del 15; en todo el panel, la proporción es mucho mayor para las semanas contiguas. Cuánto se parecen esas respuestas se mide directamente: el acuerdo de la etiqueta de cada fila con la de la misma acción k semanas antes.
y_sem = (d["ret_sig_semana"] > d["med_sem"]).astype(int).to_numpy()
etiquetas = pd.DataFrame({"ticker": d["ticker"],
"21 sesiones": y, "1 semana": y_sem})
acuerdo = {}
for k in range(1, 7):
antes = etiquetas.groupby("ticker")[["21 sesiones",
"1 semana"]].shift(k)
hay = antes.notna().all(axis=1)
acuerdo[k] = (antes[hay] == etiquetas.loc[hay, ["21 sesiones",
"1 semana"]]
).mean()
pd.DataFrame(acuerdo).T.rename_axis("k").round(3) 21 sesiones 1 semana
k
1 0.777 0.496
2 0.680 0.503
3 0.601 0.501
4 0.528 0.502
5 0.502 0.501
6 0.504 0.498Con la etiqueta de 21 sesiones, la fila de la semana anterior tiene la misma respuesta el 77,7 % de las veces; dos semanas antes, el 68 %; el acuerdo cae a la mitad en la quinta semana, cuando las dos etiquetas ya no comparten ningún rendimiento. Con la etiqueta de una semana, que no se solapa, el acuerdo es del 50 % desde la primera. Es la misma acción y las mismas features: lo único que cambia es si la respuesta se comparte.
El segundo ingrediente: features que cambian poco
Para que el modelo aproveche ese acuerdo tiene que reconocer que la fila de prueba es la vecina de una de entrenamiento. Lo consigue si las features de una semana se parecen mucho a las de la anterior. La autocorrelación semanal de cada feature, dentro de cada acción, lo mide:
persist = {c: d.groupby("ticker")[c]
.apply(lambda s: s.autocorr(1)).mean() for c in FEATS}
persist = pd.Series(persist, name="autocorr. semanal")
print(persist.sort_values(ascending=False).round(2).to_string())valor_bp 0.99
log_capitalizacion 0.99
roe 0.99
ey 0.99
ruido_3 0.97
mom_12_1 0.97
vol_63d 0.97
dist_max_52s 0.96
sue 0.94
ret_3m 0.92
ruido_2 0.89
vol_21d 0.88
ruido_5 0.77
ret_1m 0.77
rsi_14 0.63
dias_desde_informe 0.63
sentimiento 0.59
volumen_rel 0.46
ruido_4 0.33
ret_1s 0.03
ruido_1 0.00Casi todas las features son lentas: el value, el tamaño, los ratios contables y el momentum de 12 meses tienen
autocorrelación semanal de 0,94 a 0,99, y ruido_3, un ruido AR(1) sin ningún efecto, 0,97. Juntas forman una huella
que identifica a cada acción en cada tramo de tiempo. Solo ret_1s (0,03) y ruido_1 (0,00) se renuevan por
completo cada semana; ruido_4 y volumen_rel, a medias. Para un bosque con hojas pequeñas, encontrar a la vecina es tan fácil como para un humano reconocer una
cara.
Fuente Mermaid
flowchart TB A[Etiqueta solapada] --> C[Misma respuesta] B[Features lentas] --> D[Vecina reconocible] C --> E[k-fold barajado] D --> E E --> F[Acierto inflado]
Los dos ingredientes, por separado
Si la explicación es correcta, quitando cualquiera de los dos ingredientes la fuga desaparece, y con solo el segundo basta para fabricar acierto con columnas que no tienen ningún efecto. Tres experimentos, siempre con el k-fold barajado:
- la etiqueta de una semana (sin solape) con las mismas 21 features;
- la etiqueta de 21 sesiones con cuatro features persistentes y sin efecto:
ruido_2,ruido_3,ruido_5ylog_capitalizacion; - la etiqueta de 21 sesiones con tres features rápidas:
ret_1s,ruido_1yvolumen_rel.
def columnas(cols):
return np.nan_to_num(d[cols].to_numpy(dtype=float))
lentas = ["ruido_2", "ruido_3", "ruido_5", "log_capitalizacion"]
rapidas = ["ret_1s", "ruido_1", "volumen_rel"]
exp = {
"21 sesiones, 21 features": res["k-fold barajado"],
"1 semana, 21 features": acierto(barajado, yv=y_sem),
"21 sesiones, 4 lentas sin efecto":
acierto(barajado, Xv=columnas(lentas)),
"21 sesiones, 3 rápidas": acierto(barajado, Xv=columnas(rapidas)),
}
for nombre, a in exp.items():
print(f"{nombre:<34}{a:.1%}")21 sesiones, 21 features 59.7%
1 semana, 21 features 50.0%
21 sesiones, 4 lentas sin efecto 53.5%
21 sesiones, 3 rápidas 49.8%| Etiqueta | Features | k-fold barajado | Qué falta |
|---|---|---|---|
| 21 sesiones | las 21 | 59,7 % | nada: fuga completa |
| 1 semana | las 21 | 50,0 % | el solape de etiquetas |
| 21 sesiones | 4 lentas sin efecto | 53,5 % | la señal (no hace falta) |
| 21 sesiones | 3 rápidas | 49,8 % | la persistencia |
Con la etiqueta de una semana el k-fold barajado se vuelve honesto: 50,0 %, el mismo resultado que por bloques. Con features rápidas, también. Y con cuatro columnas que no tienen ningún efecto en los rendimientos (tres ruidos y el tamaño, que en este panel no paga prima), el bosque llega al 53,5 %: por encima del oráculo, sin ninguna información sobre el futuro. La fuga no necesita señal; necesita una huella y una respuesta compartida.
Agrupar por fecha no basta
Un panel tiene dos dimensiones, y la intuición habitual es proteger la transversal: que las 80 acciones de una misma
semana no se repartan entre entrenamiento y prueba, porque comparten los movimientos del mercado y del sector de esa
semana. Es lo que hace GroupKFold con la semana como grupo: cada semana entera cae en un solo pliegue. Con
shuffle=True, las semanas se reparten al azar entre los pliegues.
gkf = GroupKFold(5, shuffle=True, random_state=0)
por_semana = list(gkf.split(X, y, groups=semana))
comunes = [np.intersect1d(semana[a], semana[b]).size
for a, b in por_semana]
print(f"semanas a los dos lados, por pliegue: {comunes}")
print(f"GroupKFold por semana, barajado: {acierto(por_semana):.1%}")semanas a los dos lados, por pliegue: [0, 0, 0, 0, 0]
GroupKFold por semana, barajado: 59.2%Ninguna semana está a los dos lados, y el acierto apenas se mueve: 59,2 %. La fuga no era transversal. La etiqueta es relativa a la mediana de la semana, así que el componente común ya estaba fuera de la respuesta; lo que el bosque explota es la misma acción en las semanas de alrededor, que siguen cayendo en el entrenamiento cuando las semanas se barajan. En un panel financiero el grupo que importa es el tiempo, y no como etiqueta de grupo sino como orden: los pliegues tienen que ser bloques contiguos, con un margen alrededor.
| Esquema | Qué protege | Acierto |
|---|---|---|
KFold(shuffle=True) | nada | 59,7 % |
GroupKFold(shuffle=True) por semana | acciones de la misma semana | 59,2 % |
KFold por bloques de tiempo | casi todo; queda la frontera | 50,0 % |
| bloques con purga de 4 semanas | también la frontera | 50,1 % |
La validación equivocada elige el modelo equivocado
La fuga no solo infla una cifra. Si la validación se usa para elegir hiperparámetros (lección 5.5.4), elige lo que mejor aprovecha la fuga. El hiperparámetro que controla cuánto puede memorizar un bosque es el tamaño mínimo de hoja: con hojas de una observación, cada árbol puede aislar a una acción en un tramo de semanas concreto. Cuatro tamaños, con las dos validaciones:
hojas = {}
for hoja in (1, 5, 50, 500):
if hoja == 5: # ya calculado arriba
hojas[hoja] = (res["k-fold barajado"], res["purged k-fold"])
else:
hojas[hoja] = (acierto(barajado, hoja=hoja),
acierto(purgado, hoja=hoja))
pd.DataFrame(hojas, index=["barajado", "purgado"]).T.rename_axis(
"hoja mínima").map(lambda a: f"{a:.1%}") barajado purgado
hoja mínima
1 60.4% 50.3%
5 59.7% 50.1%
50 55.7% 50.2%
500 52.3% 50.2%El k-fold barajado tiene una preferencia clara y monótona: cuanto más pequeña la hoja, más acierto, del 52,3 % con hojas de 500 al 60,4 % con hojas de 1. Un ajuste de hiperparámetros con esa validación elegiría el bosque que más memoriza y lo presentaría con un 60 %. El purged k-fold da entre el 50,1 % y el 50,3 % con los cuatro tamaños: ninguno destaca, porque el bosque no encuentra en estas features una señal que pueda aprovechar fuera de muestra. Es la respuesta correcta, aunque decepcione, y la que hay que tomar en serio: la lección 5.5.1 enseña que una regresión lineal con las features en rangos sí recoge parte de la señal (IC de 0,0185 en el README), y la 5.4.2 lo comprueba con la validación purgada.
Qué hacer en la práctica
- Nada de barajar filas en un problema con etiquetas que abarcan varios periodos o con features persistentes, que en finanzas son casi todos. Pliegues contiguos en el tiempo.
- Purgar y embargar alrededor de cada bloque de prueba con las fechas reales de inicio y fin de cada etiqueta, no con una regla aproximada (lección 5.4.2).
- Walk-forward para la historia que se habría vivido en tiempo real, con reentrenamiento (lección 5.4.3).
- Comparar con un techo y con una baseline: en datos sintéticos, el oráculo; en datos reales, el orden de magnitud de la literatura y un modelo lineal simple. Un resultado muy por encima de los dos es una fuga mientras no se demuestre lo contrario.
- Auditar cuándo se conoce cada dato antes de validar nada. La purga protege del solape de las etiquetas, no de una feature mal fechada.
Ejercicios
- Con la etiqueta de 21 sesiones, el k-fold barajado da 53,5 % con cuatro features lentas sin efecto. ¿Basta con una
sola? Repítelo solo con
log_capitalizaciony solo conruido_3, y explica la diferencia con su persistencia.
Solución
for col in ("log_capitalizacion", "ruido_3"):
a = acierto(barajado, Xv=columnas([col]))
print(f"{col:<20}autocorr. {persist[col]:.2f} "
f"barajado {a:.1%}")log_capitalizacion autocorr. 0.99 barajado 51.1%
ruido_3 autocorr. 0.97 barajado 50.4%Con una sola columna la huella es mucho más pobre: 51,1 % con el tamaño y 50,4 % con ruido_3. Muchas acciones tienen valores parecidos en una dimensión y el
bosque no puede distinguir a la vecina de otras filas con la respuesta contraria. La fuga crece con el número de
features persistentes porque cada una añade una coordenada a la huella; con cuatro ya supera al oráculo. Es la razón
de que los paneles con muchas features contables y de riesgo, que cambian poco, sean los más expuestos.
- Las 58.000 filas no son 58.000 observaciones independientes. Estima el número efectivo de etiquetas independientes (cada acción tiene una etiqueta nueva cada cinco semanas, aproximadamente) y el error típico del acierto que correspondería. ¿Cambia la conclusión sobre el 59,7 %?
Solución
n_ef = len(y) / 5 # una etiqueta independiente cada ~5 semanas
for nombre, n in (("filas", len(y)), ("efectivas", n_ef)):
print(f"{nombre:<10} n = {n:>8,.0f} error típico "
f"{np.sqrt(0.25 / n):.2%}")
print(f"59,7 % frente al techo 51,1 %: "
f"{(0.597 - 0.511) / np.sqrt(0.25 / n_ef):.0f} errores típicos")filas n = 58,000 error típico 0.21%
efectivas n = 11,600 error típico 0.46%
59,7 % frente al techo 51,1 %: 19 errores típicosCon unas 11.600 etiquetas efectivas, el error típico del acierto sube de 0,21 % a 0,46 %, y aún falta la correlación transversal (las acciones de una semana no son independientes entre sí), que lo agranda más. Aun así, 59,7 % frente a un techo de 51,1 % son casi veinte errores típicos: no es mala suerte. La lección práctica es la inversa: en una validación honesta, diferencias de unas décimas entre modelos (50,1 % frente a 50,3 %) son ruido.
- Un compañero presenta un modelo de clasificación semanal de acciones con un 57 % de acierto validado con
TimeSeriesSplitsingapy etiquetas a un mes. ¿Qué tres preguntas le harías antes de creerle?
Solución
Primero, si las features están bien fechadas: cuándo se conoce cada dato (fecha de publicación frente a fecha del
periodo, revisiones, proveedores que fechan al inicio de la semana). Es la fuga que ninguna validación detecta.
Segundo, cómo se trata el solape: TimeSeriesSplit sin gap deja en el entrenamiento las últimas filas antes de cada
prueba, cuyas etiquetas de un mes se solapan con las primeras de la prueba; con pliegues pequeños, esa frontera pesa.
Hace falta un hueco de al menos el horizonte de la etiqueta (lecciones 5.4.2 y 5.4.3). Tercero, con qué se compara:
el acierto de una baseline lineal y el de un modelo de solo ruido (columnas permutadas) con la misma validación. Un
57 % semanal en acciones está muy por encima de lo que se publica con datos reales; sin esas tres respuestas, lo
razonable es suponer una fuga.
Respuesta plegada en cada pregunta
Autoevaluación
En el panel del nivel, el oráculo (el signo verdadero del alfa) acierta el 51,1 % y un random forest con k-fold barajado, el 59,7 %. ¿Qué se concluye?
- AQue el random forest ha encontrado no linealidades que el oráculo no ve
- BQue el oráculo está mal calculado
- CQue con 58.000 filas cualquier acierto es significativo
- DQue la validación tiene una fuga: ningún modelo puede superar fuera de muestra a la esperanza verdadera, así que 8 puntos por encima del techo son información de la respuesta
Ver respuesta
Respuesta D. Que la validación tiene una fuga: ningún modelo puede superar fuera de muestra a la esperanza verdadera, así que 8 puntos por encima del techo son información de la respuesta
El oráculo usa el alfa verdadero, la mejor previsión posible de la parte predecible. Superarlo fuera de muestra es imposible; con purga el mismo bosque queda en el 50,1 %, por debajo del techo, como debe ser.
Con la etiqueta de una semana (
ret_sig_semana, sin solape) y las mismas features, el k-fold barajado da un 50,0 %. ¿Qué demuestra?- AQue la etiqueta de una semana no tiene información
- BQue la fuga necesita etiquetas solapadas: con etiquetas independientes, reconocer a la misma acción una semana antes no dice nada de la respuesta
- CQue el random forest no funciona con datos semanales
- DQue barajar no tiene ningún efecto en finanzas
Ver respuesta
Respuesta B. Que la fuga necesita etiquetas solapadas: con etiquetas independientes, reconocer a la misma acción una semana antes no dice nada de la respuesta
La persistencia de las features es la misma en los dos casos. Lo que cambia es que la etiqueta de 21 sesiones coincide con la de la semana anterior el 78 % de las veces y la de una semana, el 50 %.
Se cambia el k-fold barajado por filas por uno barajado por semanas (todas las acciones de una semana van juntas al mismo pliegue). El acierto apenas baja, del 59,7 % al 59,2 %. ¿Por qué?
- APorque la fuga viene de la misma acción en las semanas vecinas, que siguen cayendo en el entrenamiento: agrupar por fecha quita la fuga transversal, no la temporal
- BPorque la fuga venía de las acciones vecinas en la misma semana, y agruparlas no la toca
- CPorque GroupKFold no baraja
- DPorque hay pocas semanas
Ver respuesta
Respuesta A. Porque la fuga viene de la misma acción en las semanas vecinas, que siguen cayendo en el entrenamiento: agrupar por fecha quita la fuga transversal, no la temporal
La etiqueta es relativa a la mediana de la semana, así que el componente común de mercado ya no está. Lo que el bosque reconoce es la huella de cada acción en las semanas de alrededor, y esas siguen al otro lado de la frontera.
Con k-fold barajado, el mejor tamaño mínimo de hoja es 1 (60,4 %); con purged k-fold, todos los tamaños dan entre 50,1 % y 50,3 %. ¿Qué lectura es correcta?
- AHay que usar hojas de 1, porque es el que mejor resultado da en alguna validación
- BLas dos validaciones coinciden
- CLa validación barajada premia memorizar; la honesta dice que, sin señal aprovechable por el bosque, el hiperparámetro da igual. Elegir con la primera lleva al modelo más sobreajustado
- DEl purged k-fold no sirve para elegir hiperparámetros
Ver respuesta
Respuesta C. La validación barajada premia memorizar; la honesta dice que, sin señal aprovechable por el bosque, el hiperparámetro da igual. Elegir con la primera lleva al modelo más sobreajustado
Hojas de una observación permiten aislar a cada acción en cada tramo de tiempo, que es justo lo que explota la fuga. Fuera del k-fold barajado esa capacidad no vale nada.
¿Cuál de estas fugas NO detecta ni el k-fold por bloques ni el purged k-fold?
- AEl solape de las etiquetas entre entrenamiento y prueba
- BLa memoria de las features persistentes en el k-fold barajado
- CUna feature fechada antes de conocerse (como
revision_analistas, que usa los dos primeros días de la semana siguiente) - DLa concentración de la fuga en las fronteras de los pliegues
Ver respuesta
Respuesta C. Una feature fechada antes de conocerse (como
revision_analistas, que usa los dos primeros días de la semana siguiente)Si una feature ya lleva dentro información posterior a su fecha, cada fila la arrastra a cualquier pliegue. Solo se descubre auditando cuándo se conoce cada dato (lección 5.2.2); la validación no puede verlo.
Ideas clave
- En el panel del nivel (80 acciones, 725 semanas, etiqueta a 21 sesiones), un random forest da 59,7 % con k-fold barajado, 50,0 % por bloques y 50,1 % con purga. El oráculo acierta el 51,1 %.
- Un acierto por encima del oráculo es la firma de una fuga. En ML financiero el techo es bajo, y conocerlo, aunque sea de forma aproximada, protege más que cualquier métrica.
- La fuga necesita dos cosas a la vez: etiquetas solapadas (la de 21 sesiones coincide con la de la semana anterior el 78 % de las veces) y features persistentes (autocorrelación semanal de 0,97 a 0,99 en momentum, tamaño o value).
- Con cuatro columnas sin ningún efecto pero persistentes, el k-fold barajado ya da 53,5 %. Con la etiqueta de una semana, 50,0 %.
- Agrupar por fecha no arregla nada (59,2 %): en un panel la fuga es temporal, de la misma acción en las semanas vecinas.
- La validación barajada elige las hojas más pequeñas, el modelo que más memoriza. Con purga, ningún tamaño de hoja destaca: es la respuesta correcta cuando no hay señal que aprender.