Lección 6.7.2 · 65 min

Tipos de datos alternativos: qué mide cada uno

Cada tipo de dato alternativo ve una parte distinta de la actividad de una empresa, con su cobertura, su retraso y sus trampas. Esta lección recorre los conjuntos alternativos del curso (tarjetas, tráfico web y apps, ofertas de empleo, reseñas, llegadas de buques, clima, patentes y on-chain) y mide en cada uno qué recoge, a cuántas empresas cubre, cuándo se conoce y qué lo ensucia.

Fig. — Cobertura y retraso de las fuentes alternativas del curso: empresas cubiertas de las 120 del panel y cuándo se conoce el dato (las patentes, unos 18 meses después de la solicitud)
Lecciones del nivel 6
  1. 6.1 · NLP financiero clásico y moderno

    1. 6.1.1 Tokenización, embeddings, diccionarios financieros y FinBERT
    2. 6.1.2 Fuentes de texto: EDGAR, CNMV, transcripciones y redes sociales
    3. 6.1.3 Event studies con señales de texto, alineación temporal y topic modeling
  2. 6.2 · LLMs para investigación y trading

    1. 6.2.1 Cómo funciona un LLM: del siguiente token a la API
    2. 6.2.2 Prompting estructurado: JSON que se puede validar
    3. 6.2.3 Guidance, KPIs y tono: qué extrae bien un LLM
    4. 6.2.4 Fine-tuning ligero para tareas financieras: LoRA y QLoRA
    5. 6.2.5 Sesgo de anticipación en LLMs: el modelo que ya conoce el futuro
  3. 6.3 · RAG financiero

    1. 6.3.1 Embeddings y bases vectoriales: FAISS, Chroma, pgvector y Qdrant
    2. 6.3.2 Chunking de documentos financieros: tablas, notas al pie y XBRL
    3. 6.3.3 RAG sobre informes, folletos y KID, y cómo evaluarlo
  4. 6.4 · Agentes de IA para trading

    1. 6.4.1 Agentes con herramientas: tool calling, ReAct y planificación
    2. 6.4.2 Marcos de agentes y MCP
    3. 6.4.3 Arquitecturas multiagente tipo TradingAgents
    4. 6.4.4 Evidencia y límites de los agentes
    5. 6.4.5 Guardarraíles para agentes: el agente propone, el motor decide
    6. 6.4.6 Agentes de investigación: hipótesis, alpha mining y revisión
  5. 6.5 · IA como copiloto del quant

    1. 6.5.1 Asistentes de código con un flujo de tests primero
    2. 6.5.2 Revisión automática de backtests y migraciones de código
    3. 6.5.3 Riesgos del código generado por IA
  6. 6.6 · Big data para trading

    1. 6.6.1 Cuándo los datos son «big»: ticks, libros, opciones y universos
    2. 6.6.2 Parquet, Arrow, Polars en streaming y DuckDB fuera de memoria
    3. 6.6.3 Dask, Spark y Ray: de una máquina a un clúster
    4. 6.6.4 Bases de series temporales: TimescaleDB, QuestDB, ClickHouse, ArcticDB y kdb+
    5. 6.6.5 Almacenamiento en la nube, lakehouse (Delta, Iceberg) y GPU (RAPIDS, cuDF)
  7. 6.7 · Datos alternativos

    1. 6.7.1 Panorama y tamaño del mercado de datos alternativos
    2. 6.7.2 Tipos de datos alternativos: qué mide cada uno
    3. 6.7.3 Web scraping responsable
    4. 6.7.4 Proveedores y due diligence de datos alternativos
    5. 6.7.5 Del dato alternativo a la señal
    6. 6.7.6 Satélite, visión por computador y cadena de suministro

El mapa de las fuentes

La lección 6.7.1 clasificó los datos alternativos por su origen. Esta los recorre uno a uno con datos. El curso trae una versión sintética de casi cada tipo, todos sobre las 120 empresas ficticias del nivel 5 y de 2018 a septiembre de 2025, con una propiedad que los datos reales no tienen: se sabe qué mide cada fuente. Detrás de todos hay un choque de demanda trimestral z por empresa que mueve las ventas frente al consenso; cada fuente ve una parte, con su ruido y su retraso, y la verdad está en alt6_verdad_trimestral.csv.

TipoQué mideEn el cursoReal (ejemplos de recogida)
Transacciones con tarjetaGasto en los comercios de la empresaalt6_tarjetas_pit.parquetPaneles de bancos o agregadores de pagos
RecibosTicket, productos, precio pagado—Recibos de correo electrónico con permiso del usuario
Tráfico web y de appsVisitas, descargas, usoalt6_trafico_web.parquetPaneles de navegación, tiendas de apps
Ofertas de empleoIntención de contratar, por área y paísalt6_empleo.parquetScraping de portales y webs de empleo (lección 6.7.3)
Reseñas y precios publicadosSatisfacción, defectos, preciosalt6_resenas.parquetScraping de tiendas y portales de reseñas
Envíos (AIS)Llegadas de buques, esperas en puertoalt6_ais_llegadas.parquetTranspondedores AIS de los buques
ClimaTemperaturas, grados-día, lluviaalt6_clima.parquetServicios meteorológicos nacionales
PatentesSolicitudes publicadas por clasealt6_patentes.parquetOficinas de patentes (EPO, USPTO, OMPI)
Satélite y geolocalizaciónCoches en aparcamientos, tránsito de personassatelite6_imagenes.parquet (lección 6.7.6)Proveedores de imágenes; SDK en apps móviles
On-chainFlujos, direcciones activas, comisionesonchain3_metricas.csv (lección 3.7.3)Nodos de la cadena y exploradores

Antes de buscar una señal, cada fuente pasa por las mismas preguntas: qué mide, a quién cubre, cuándo se conoce y qué la ensucia. El camino de un dato hasta el modelo tiene varias fechas, y la que manda en un backtest es la última:

EventoRecogidaEntregaMapeo a empresaSeñal
Fuente Mermaid
flowchart TB
  E[Evento] --> R[Recogida]
  R --> T[Entrega]
  T --> M[Mapeo a empresa]
  M --> S[Señal]

Cobertura y fechas

La primera tabla es el inventario: filas, frecuencia, periodo y cuántas empresas cubre cada fuente. El mapeo de comercios, nombres y solicitantes a empresas es un problema en sí mismo (lección 6.7.5); aquí se usan las tablas de verdad del curso para saltarlo, salvo en el empleo, donde se resuelve con una regla:

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

emp = pd.read_csv("acciones6_empresas.csv")
tk = emp.set_index("id_empresa")["ticker"]
verdad = pd.read_csv("alt6_verdad_trimestral.csv",
                     parse_dates=["fin_trimestre"])
verdad["q"] = verdad["fin_trimestre"].dt.to_period("Q")
expo = pd.read_csv("alt6_exposiciones.csv")

tar = pd.read_parquet("alt6_tarjetas_pit.parquet")
com = pd.read_csv("alt6_comercios_verdad.csv")
web = pd.read_parquet("alt6_trafico_web.parquet")
empleo = pd.read_parquet("alt6_empleo.parquet")
res = pd.read_parquet("alt6_resenas.parquet").merge(
    pd.read_csv("alt6_resenas_verdad.csv"), on="id_resena")
ais = pd.read_parquet("alt6_ais_llegadas.parquet")
clima = pd.read_parquet("alt6_clima.parquet")
pat = pd.read_parquet("alt6_patentes.parquet").merge(
    pd.read_csv("alt6_patentes_verdad.csv"), on="id_patente")

primera = emp["nombre_corto"].str.split().str[0].str.lower()
empleo["ticker"] = (empleo["empresa_publicada"].str.split()
                    .str[0].str.lower().map(
                        dict(zip(primera, emp["ticker"]))))
cubre = {
    "tarjetas": com.query("relacion != 'sin_relacion'")
                   ["id_empresa"].nunique(),
    "web": web["dominio"].isin(emp["dominio"]).groupby(
        web["dominio"]).any().sum(),
    "empleo": empleo["ticker"].nunique(),
    "reseñas": res["id_empresa"].nunique(),
    "buques": (expo["tipo"] == "puerto").sum(),
    "clima": (expo["tipo"] == "clima").sum(),
    "patentes": pat["id_empresa"].nunique(),
}
fechas = {"tarjetas": tar["fecha"], "web": web["semana"],
          "empleo": empleo["mes"], "reseñas": res["fecha"],
          "buques": ais["fecha"], "clima": clima["fecha"],
          "patentes": pat["fecha_publicacion"]}
inv = pd.DataFrame({
    "filas": {k: len(v) for k, v in fechas.items()},
    "desde": {k: v.min().date() for k, v in fechas.items()},
    "hasta": {k: v.max().date() for k, v in fechas.items()},
    "empresas": cubre})
inv
OUT [01]Inventario de las fuentes
           filas       desde       hasta  empresas
tarjetas  359410  2018-01-01  2025-09-30        25
web        12120  2018-01-07  2025-09-28        30
empleo     27900  2018-01-31  2025-09-30        60
reseñas    36213  2018-01-01  2025-09-30        10
buques     42450  2018-01-01  2025-09-30        10
clima      16980  2018-01-01  2025-09-30        12
patentes    5624  2016-06-27  2025-12-31        40

Ninguna fuente cubre todo el panel: de 10 empresas (reseñas, buques) a 60 (empleo). Y la cobertura no es aleatoria: las tarjetas solo ven negocios que venden al consumidor, el clima solo importa a las eléctricas y el AIS a las exportadoras. Un modelo construido con una fuente opera, como mucho, sobre su universo (lección 5.2.2).

Las fechas son la segunda pregunta. La tabla de tarjetas trae fecha_entrega (la fecha del gasto más 3 días); la de tráfico web, la entrega del martes siguiente a la semana; las patentes, la fecha de solicitud y la de publicación. El resto no trae fecha de entrega, y el curso supone que el dato se conoce al acabar su periodo (el día, el mes); con un proveedor real, esa fecha es una pregunta de due diligence (lección 6.7.4):

IN [02]python
retraso = pd.Series({
    "tarjetas": (tar["fecha_entrega"] - tar["fecha"]).dt.days,
    "web (desde el domingo)":
        (web["fecha_entrega"] - web["semana"]).dt.days,
    "patentes": (pat["fecha_publicacion"]
                 - pat["fecha_solicitud"]).dt.days,
})
pd.DataFrame({k: v.describe()[["min", "50%", "max"]]
              for k, v in retraso.items()}).T
OUT [02]Retraso entre el evento y el momento en que se conoce
                          min    50%    max
tarjetas                  3.0    3.0    3.0
web (desde el domingo)    2.0    2.0    2.0
patentes                538.0  557.0  577.0

Tres días en las tarjetas, dos en el tráfico web (más los seis de la semana que agrega) y entre 538 y 577 días en las patentes: unos 18 meses, el plazo de publicación habitual de una solicitud de patente1Nota 1 · OMPI, «Cómo proteger sus invenciones en otros países: preguntas frecuentes sobre el Tratado de Cooperación en materia de Patentes (PCT)» (https://www.wipo.int/pct/es/faqs/faqs.html, consultada el 2026-10-23): la OMPI publica la solicitud internacional «apenas vencido el plazo de 18 meses a contar desde la fecha de prioridad».. La figura de la cabecera resume cobertura y retraso de cada fuente.

Transacciones con tarjeta: el panel cambia

Un proveedor de datos de tarjetas ve el gasto de un panel de tarjetas (las de los bancos o apps que le ceden datos), no el de toda la población. El panel crece y cambia de composición, y eso aparece en el gasto bruto como si fuera demanda. En los datos del curso, el 1 de marzo de 2021 entra un banco nuevo:

IN [03]python
panel = pd.read_csv("alt6_tarjetas_panel.csv",
                    parse_dates=["fecha"]).set_index("fecha")
diario = tar.groupby("fecha")["gasto_usd"].sum().to_frame()
diario = diario.join(panel)
diario["por_tarjeta"] = (diario["gasto_usd"]
                         / diario["tarjetas_activas"])
antes = diario.loc["2021-02-01":"2021-02-28"].mean()
despues = diario.loc["2021-03-01":"2021-03-28"].mean()
(despues / antes - 1)[["gasto_usd", "tarjetas_activas",
                       "por_tarjeta", "fraccion_jovenes"]].round(3)
OUT [03]El salto del panel
gasto_usd           0.382
tarjetas_activas    0.362
por_tarjeta         0.015
fraccion_jovenes    0.147
dtype: float64
Fig. — Gasto semanal con tarjeta del panel, bruto y por tarjeta activa (febrero de 2021 = 100): la entrada de un banco el 1 de marzo de 2021 sube el bruto un 38 % sin que cambie la demanda

El gasto bruto sube un 38 % de febrero a marzo; las tarjetas activas, un 36 %; el gasto por tarjeta, un 1,5 %. La regla es normalizar por el tamaño del panel antes de cualquier otra cosa. Queda un sesgo más sutil: la fracción de titulares jóvenes sube con el banco nuevo, y las empresas con clientela joven ganan peso en el panel sin vender más. Ese sesgo de composición, la diferencia entre el dato tal como se entregó y la historia que el proveedor reconstruye al vender (backfill), y cómo se detectan, son el tema de la lección 6.7.4.

Normalizado, el gasto por tarjeta de los comercios de cada empresa sigue sus ventas. Su crecimiento interanual por trimestre, frente al choque de demanda y a la sorpresa de ventas:

IN [04]python
t = tar.merge(com.query("relacion != 'sin_relacion'"),
              on="descriptor")
t["q"] = t["fecha"].dt.to_period("Q")
tarjetas_q = (panel.groupby(panel.index.to_period("Q"))
              ["tarjetas_activas"].mean())
g = t.groupby(["id_empresa", "q"])["gasto_usd"].sum().reset_index()
g["ticker"] = g["id_empresa"].map(tk)
g["x"] = np.log(g["gasto_usd"] / g["q"].map(tarjetas_q))
g["x"] = g["x"] - g.groupby("ticker")["x"].shift(4)

def frente_a_verdad(df, col="x"):
    m = df.merge(verdad, on=["ticker", "q"]).dropna(subset=[col])
    return pd.Series({
        "n": len(m),
        "corr_z": m[col].corr(m["z_demanda"]),
        "corr_sorpresa": m[col].corr(m["sorpresa_ventas"])}).round(3)

medidas = {"tarjetas": frente_a_verdad(g)}
medidas["tarjetas"]
OUT [04]Lo que miden las tarjetas
n                675.000
corr_z             0.289
corr_sorpresa      0.211
dtype: float64

Una correlación de 0,29 con el choque de demanda y de 0,21 con la sorpresa en 675 empresa-trimestres. El crecimiento interanual es una medida tosca (mezcla el choque de este trimestre con el del mismo trimestre del año anterior); la lección 6.7.5 construye la medida correcta, el crecimiento del gasto frente al que implica el consenso, y la backtestea.

Tráfico web y apps: los bots

El tráfico web llega por semanas y por dominio: visitas (en millones) y descargas de la app (en miles). Las visitas tienen un problema conocido de los datos reales: el tráfico automatizado. En los datos, un 1,3 % de las semanas trae un pico de bots que multiplica las visitas por 2 a 4. Una regla sencilla los detecta: dividir cada semana por la mediana de las 8 semanas anteriores (solo pasado, para que la regla se pueda aplicar el día de la entrega):

IN [05]python
bots = pd.read_csv("alt6_trafico_web_bots_verdad.csv",
                   parse_dates=["semana"]).assign(verdad=True)
web = web.sort_values(["dominio", "semana"])
web["mediana"] = web.groupby("dominio")["visitas_millones"].transform(
    lambda s: s.shift(1).rolling(8, min_periods=4).median())
web["ratio"] = web["visitas_millones"] / web["mediana"]
web = web.merge(bots, on=["dominio", "semana"], how="left")
web["verdad"] = web["verdad"].fillna(False).astype(bool)
for umbral in (1.5, 1.7, 2.0):
    marca = web["ratio"] > umbral
    acierto = (marca & web["verdad"]).sum()
    print(f"umbral {umbral}: marcadas {marca.sum()}, "
          f"precisión {acierto / marca.sum():.1%}, "
          f"exhaustividad {acierto / web['verdad'].sum():.1%}")
OUT [05]Detección de picos de bots
umbral 1.5: marcadas 159, precisión 100.0%, exhaustividad 98.8%
umbral 1.7: marcadas 159, precisión 100.0%, exhaustividad 98.8%
umbral 2.0: marcadas 157, precisión 100.0%, exhaustividad 97.5%
Fig. — Visitas semanales de un dominio del curso (bosdor.example) con sus 10 picos de tráfico de bots, que multiplican las visitas por 2 a 4

Con un umbral de 1,7, la regla marca 159 semanas, todas de bots, y encuentra 159 de las 161. En datos reales los bots no siempre llegan en picos tan limpios (hay tráfico automatizado constante, y picos legítimos por una campaña o una noticia), así que la regla se contrasta con otras señales: el tráfico de apps, el país de origen, la duración de las visitas. Sustituir las semanas marcadas por la mediana cambia lo que mide la fuente:

IN [06]python
web["limpias"] = web["visitas_millones"].mask(
    web["ratio"] > 1.7, web["mediana"])   # solo las marcadas
web["ticker"] = web["dominio"].map(
    emp.set_index("dominio")["ticker"])
web["q"] = web["semana"].dt.to_period("Q")
for col in ["visitas_millones", "limpias", "descargas_app_miles"]:
    s = web.groupby(["ticker", "q"])[col].sum().reset_index()
    s["x"] = np.log(s[col]) - np.log(
        s.groupby("ticker")[col].shift(4))
    medidas[f"web: {col}"] = frente_a_verdad(s)
pd.DataFrame({k: v for k, v in medidas.items()
              if k.startswith("web")}).T
OUT [06]Visitas con y sin bots, y descargas
                              n  corr_z  corr_sorpresa
web: visitas_millones     810.0   0.170          0.176
web: limpias              810.0   0.319          0.258
web: descargas_app_miles  810.0   0.365          0.291

Sin limpiar, las visitas correlacionan 0,17 con el choque de demanda; limpias, 0,32. Las descargas de la app, que en estos datos no tienen bots, son la mejor medida de las tres (0,37 con el choque, 0,29 con la sorpresa).

Ofertas de empleo: nombres y horizonte

Las ofertas de empleo se recogen de portales y webs corporativas, casi siempre por scraping (lección 6.7.3). Cada portal escribe el nombre de la empresa a su manera:

IN [07]python
variantes = empleo.groupby("ticker")["empresa_publicada"].unique()
print(variantes.iloc[0].tolist())
print("variantes por empresa:",
      empleo.groupby("ticker")["empresa_publicada"].nunique()
      .describe()[["min", "max"]].to_dict())
print("sin mapear:", empleo["ticker"].isna().sum())
OUT [07]Variantes del nombre en los portales
['BEBVIA PETROLEUM', 'Bebvia', 'Bebvia Petroleum Holdings Inc.', 'Bebvia Petroleum', 'Bebvia Petroleum Inc']
variantes por empresa: {'min': 5.0, 'max': 5.0}
sin mapear: 0

Cinco variantes por empresa, todas con la primera palabra del nombre corto: en los datos del curso, esa regla basta (los nombres inventados empiezan por una palabra única). En datos reales no basta, porque hay nombres comunes, filiales y marcas que no comparten nada con la matriz; la lección 6.7.5 trata la resolución de entidades.

Lo que miden las ofertas es la intención de contratar, y una empresa contrata antes de vender más. En los datos, las ofertas nuevas anticipan la demanda dos trimestres:

IN [08]python
empleo["q"] = empleo["mes"].dt.to_period("Q")
e = (empleo.groupby(["ticker", "q"])["ofertas_nuevas"].sum()
     .reset_index().sort_values(["ticker", "q"]))
e["x0"] = np.log(e["ofertas_nuevas"]) - np.log(
    e.groupby("ticker")["ofertas_nuevas"].shift(4))
e["x"] = e.groupby("ticker")["x0"].shift(2)   # el de q-2
m = e.merge(verdad, on=["ticker", "q"]).dropna(subset=["x", "x0"])
print("mismo trimestre, con la sorpresa:",
      round(m["x0"].corr(m["sorpresa_ventas"]), 3))
print("dos trimestres antes, con la intención de contratar:",
      round(m["x"].corr(m["h_empleo_q_menos_2"]), 3))
medidas["empleo (q-2)"] = frente_a_verdad(e)
medidas["empleo (q-2)"]
OUT [08]El horizonte de las ofertas
mismo trimestre, con la sorpresa: -0.025
dos trimestres antes, con la intención de contratar: 0.691
n                1500.000
corr_z             -0.048
corr_sorpresa       0.131
dtype: float64

El crecimiento de las ofertas de un trimestre no dice nada de la sorpresa de ese trimestre (−0,025), pero mide bien la intención de contratar (0,69) y, dos trimestres después, correlaciona 0,13 con la sorpresa. Es una correlación pequeña pero con mucha cobertura (60 empresas): el tipo de fuente que aporta en combinación, no sola.

Reseñas: falsas y defectos

Las reseñas de producto tienen la forma de J de los datos reales: muchas de 5 estrellas, algunas de 1 y pocas en medio. En los datos hay dos efectos plantados: ráfagas de reseñas falsas de 5 estrellas sin compra verificada, y 49 episodios de defectos de calidad en los que las estrellas caen y los textos hablan del fallo:

IN [09]python
print("estrellas:", res["estrellas"].value_counts(normalize=True)
      .sort_index().round(3).to_dict())
print(f"falsas: {res['falsa'].mean():.1%}; "
      f"con defecto: {res['producto_con_defecto'].mean():.1%}")
print("media con y sin defecto:", res.groupby(
    "producto_con_defecto")["estrellas"].mean().round(2).to_dict())
marca = (res["estrellas"] == 5) & ~res["compra_verificada"]
acierto = (marca & res["falsa"]).sum()
print(f"regla 5 estrellas sin compra: precisión "
      f"{acierto / marca.sum():.1%}, exhaustividad "
      f"{acierto / res['falsa'].sum():.1%}")
OUT [09]Forma de las estrellas, falsas y defectos
estrellas: {1: 0.081, 2: 0.042, 3: 0.072, 4: 0.167, 5: 0.638}
falsas: 22.1%; con defecto: 5.4%
media con y sin defecto: {False: 4.29, True: 3.29}
regla 5 estrellas sin compra: precisión 77.8%, exhaustividad 100.0%
Fig. — Reparto de las 36.213 reseñas del curso por estrellas: la forma de J de las reseñas reales, con un 22,1 % de reseñas falsas de 5 estrellas sin compra verificada

Un 22 % de las reseñas son falsas, y la regla «5 estrellas sin compra verificada» las encuentra todas, pero con una precisión del 78 %: hay clientes reales que puntúan con 5 estrellas sin compra verificada. Las reseñas de productos con defecto puntúan una estrella menos de media (3,29 frente a 4,29). Para la empresa, lo que importa es la media del trimestre (centrada por empresa, porque cada una tiene su nivel):

IN [10]python
res["ticker"] = res["id_empresa"].map(tk)
res["q"] = res["fecha"].dt.to_period("Q")
for nombre, sub in [("todas", res), ("sin falsas", res[~res["falsa"]]),
                    ("sin la regla", res[~marca])]:
    s = sub.groupby(["ticker", "q"])["estrellas"].mean()
    s = (s - s.groupby(level="ticker").transform("mean"))
    medidas[f"reseñas: {nombre}"] = frente_a_verdad(
        s.rename("x").reset_index())
pd.DataFrame({k: v for k, v in medidas.items()
              if k.startswith("reseñas")}).T
OUT [10]Estrellas medias del trimestre
                           n  corr_z  corr_sorpresa
reseñas: todas         310.0   0.244          0.164
reseñas: sin falsas    310.0   0.352          0.207
reseñas: sin la regla  310.0   0.336          0.186

Las falsas diluyen la señal: con todas, 0,24 con el choque de demanda; sin ellas, 0,35. La regla imperfecta recupera casi todo (0,34) aunque tire reseñas auténticas: la precisión importa menos que no dejar pasar el ruido.

Buques y clima: el mundo físico

Los datos de AIS salen de los transpondedores que llevan los buques: la Organización Marítima Internacional exige el sistema a los buques de 300 toneladas brutas o más en viajes internacionales, a los de carga de 500 o más y a todos los de pasaje2Nota 2 · OMI, «AIS transponders» (https://www.imo.org/en/OurWork/Safety/Pages/AIS.aspx, consultada el 2026-10-23): regla V/19 del convenio SOLAS, buques de 300 GT o más en viajes internacionales, de carga de 500 GT o más que no hacen viajes internacionales y todos los de pasaje.. Agregados por puerto y tipo de buque, dan llegadas diarias y buques fondeados esperando atraque. En el curso, 10 exportadoras dependen de la actividad de cinco puertos ficticios:

IN [11]python
ais["q"] = ais["fecha"].dt.to_period("Q")
puertos = expo.query("tipo == 'puerto'").rename(
    columns={"zona": "puerto"})
for tipo in ["contenedores", "granel", "petrolero"]:
    s = (ais[ais["tipo_buque"] == tipo]
         .groupby(["puerto", "q"])["llegadas"].sum().reset_index())
    s["x"] = np.log(s["llegadas"])
    s["x"] = s["x"] - s.groupby("puerto")["x"].transform("mean")
    medidas[f"buques: {tipo}"] = frente_a_verdad(
        puertos.merge(s, on="puerto"))
pd.DataFrame({k: v for k, v in medidas.items()
              if k.startswith("buques")}).T
OUT [11]Llegadas de portacontenedores y exportadoras
                          n  corr_z  corr_sorpresa
buques: contenedores  310.0   0.674          0.465
buques: granel        310.0   0.400          0.259
buques: petrolero     310.0   0.452          0.305

Las llegadas de portacontenedores del trimestre correlacionan 0,67 con el choque de demanda de las exportadoras de su puerto: es la fuente más directa del curso después del clima. Los graneleros y petroleros también se mueven con la actividad del puerto (0,40 y 0,45), aunque las exportadoras del curso no dependen de ellos. La trampa es de mapeo: un puerto no es una empresa. Cuantas más empresas comparten puerto, más se diluye lo que la fuente dice de cada una.

El clima es el dato alternativo más antiguo y más barato: lo publican los servicios meteorológicos. Para la energía se resume en grados-día: los de calefacción (HDD) suman lo que la temperatura media del día queda por debajo de una base, y los de refrigeración (CDD), lo que queda por encima3Nota 3 · NOAA, Climate Prediction Center, «Weekly & Monthly Degree Day Summaries Explanation» (https://www.cpc.ncep.noaa.gov/products/analysis_monitoring/cdus/degree_days/ddayexp.shtml, consultada el 2026-10-23): «Heating degree days are summations of negative differences between the mean daily temperature and the 65°F base» (18,3 °C), y los de refrigeración, de las diferencias positivas sobre la misma base. El curso usa bases distintas (18 y 22 °C) a propósito.. El curso usa una base de 18 °C para los HDD y de 22 °C para los CDD; la celda lo comprueba y construye el índice del trimestre frente a su climatología:

IN [12]python
print("HDD = max(0, 18 - t):", np.allclose(
    clima["hdd"], np.maximum(0, 18 - clima["temp_media"])))
print("CDD = max(0, t - 22):", np.allclose(
    clima["cdd"], np.maximum(0, clima["temp_media"] - 22)))
clima["q"] = clima["fecha"].dt.to_period("Q")
c = clima.groupby(["region", "q"])[["hdd", "cdd"]].sum()
c = c.reset_index()
c["trim"] = c["q"].dt.quarter
for col in ["hdd", "cdd"]:   # climatología con toda la muestra
    c[col] -= c.groupby(["region", "trim"])[col].transform("mean")
c["x"] = c["hdd"] + 1.5 * c["cdd"]
regiones = expo.query("tipo == 'clima'").rename(
    columns={"zona": "region"})
medidas["clima"] = frente_a_verdad(regiones.merge(c, on="region"))
medidas["clima"]
OUT [12]Grados-día y demanda de las eléctricas
HDD = max(0, 18 - t): True
CDD = max(0, t - 22): True
n                372.000
corr_z             0.855
corr_sorpresa      0.730
dtype: float64

Un 0,86 con el choque de demanda y un 0,73 con la sorpresa de ventas de las eléctricas. Es la fuente más fuerte del curso porque el generador hace depender la demanda de las eléctricas del tiempo de su región; en la realidad la relación es también muy directa (más frío, más calefacción), pero el mercado la conoce bien y la previsión meteorológica la anticipa. Hay además un atajo en la celda: la climatología (la media de cada trimestre) se calcula con toda la muestra, también con los años que aún no habían pasado. Para un backtest, la climatología se calcula solo con el pasado (ejercicio 2).

Patentes: un control y una censura

Las patentes se publican unos 18 meses después de la solicitud. Hasta entonces, nadie fuera de la empresa y de la oficina las conoce. Contarlas por fecha de solicitud es anticipación; y además, las solicitudes recientes aún no están en los datos:

IN [13]python
pat["q_sol"] = pat["fecha_solicitud"].dt.to_period("Q")
por_sol = pat.groupby("q_sol").size()
print({str(k): v for k, v in por_sol.loc["2023Q1":].items()})
print("última publicación:", pat["fecha_publicacion"].max().date())
print("solicitantes distintos:", pat["solicitante"].nunique(),
      "para", pat["id_empresa"].nunique(), "empresas")
pat["ticker"] = pat["id_empresa"].map(tk)
pat["q"] = pat["fecha_publicacion"].dt.to_period("Q")
s = pat.groupby(["ticker", "q"]).size().rename("n")
rejilla = pd.MultiIndex.from_product(
    [s.index.levels[0], pd.period_range("2015Q1", "2025Q3", freq="Q")],
    names=["ticker", "q"])
s = s.reindex(rejilla, fill_value=0).reset_index()
s["x"] = np.log1p(s["n"]) - np.log1p(
    s.groupby("ticker")["n"].shift(4))
medidas["patentes (control)"] = frente_a_verdad(s)
medidas["patentes (control)"]
OUT [13]Solicitudes por trimestre y lo que se sabía
{'2023Q1': 138, '2023Q2': 159, '2023Q3': 163, '2023Q4': 157, '2024Q1': 139, '2024Q2': 118, '2024Q3': 2}
última publicación: 2025-12-31
solicitantes distintos: 160 para 40 empresas
n                1280.000
corr_z              0.023
corr_sorpresa       0.003
dtype: float64

Las solicitudes por trimestre caen en el segundo trimestre de 2024, son 2 en el tercero y ninguna después: no es que las empresas dejen de patentar, es que lo solicitado desde mediados de 2024 aún no se ha publicado (la última publicación de los datos es del 31 de diciembre de 2025). Una serie «por fecha de solicitud» siempre muestra una caída al final, y un modelo que la use ve una señal que no existe (y que además no podía conocer).

Las patentes son el control del curso: el generador no les da ningún efecto sobre la demanda. Su crecimiento interanual correlaciona 0,02 con el choque y 0,00 con la sorpresa. Es lo que debe salir con una fuente sin señal; si un método encuentra «alfa» en ellas, el método está mal (lección 2.5.1). Hay 160 nombres de solicitante para 40 empresas: variantes y filiales, el mismo problema de mapeo que en el empleo.

On-chain: otro calendario

Los datos on-chain son públicos por construcción: cualquiera con un nodo puede leer la cadena. El curso los trató en la lección 3.7.3 (onchain3_metricas.csv: flujo neto hacia los exchanges, direcciones activas, transacciones y comisiones de dos criptoactivos ficticios). Su particularidad como dato alternativo es el calendario:

IN [14]python
oc = pd.read_csv("onchain3_metricas.csv", parse_dates=["fecha"])
print(oc["activo"].value_counts().to_dict())
finde = oc["fecha"].dt.dayofweek >= 5
print(f"días en fin de semana: {finde.mean():.1%}")
OUT [14]El calendario de los datos on-chain
{'XBQ': 2192, 'XEQ': 2192}
días en fin de semana: 28.6%

Un 28,6 % de los días son fines de semana: la cadena no cierra. Para cruzar un dato on-chain con una acción o un ETF que cotiza en sesiones, hay que decidir qué se hace con el sábado y el domingo (sumarlos al lunes, o tomar el último valor antes de la apertura) y fijar la hora de corte del día (en el curso, las 23:00 UTC). La señal plantada y sus controles, en la lección 3.7.3.

La ficha de cada fuente

Con todo lo medido, la ficha:

IN [15]python
claves = ["tarjetas", "web: descargas_app_miles",
          "web: limpias", "empleo (q-2)", "reseñas: sin la regla",
          "buques: contenedores", "clima", "patentes (control)"]
ficha = pd.DataFrame({k: medidas[k] for k in claves}).T
ficha["n"] = ficha["n"].astype(int)
ficha.sort_values("corr_sorpresa", ascending=False)
OUT [15]Resumen: lo que ve cada fuente
                             n  corr_z  corr_sorpresa
clima                      372   0.855          0.730
buques: contenedores       310   0.674          0.465
web: descargas_app_miles   810   0.365          0.291
web: limpias               810   0.319          0.258
tarjetas                   675   0.289          0.211
reseñas: sin la regla      310   0.336          0.186
empleo (q-2)              1500  -0.048          0.131
patentes (control)        1280   0.023          0.003
Fig. — Correlación de cada fuente alternativa del curso, medida con un método sencillo, con la sorpresa de ventas del trimestre (en el empleo, dos trimestres antes; las patentes son el control sin efecto)
FuenteCoberturaRetrasoSesgo principalCorr. con la sorpresa
Clima12 eléctricas (6 regiones)Diario, inmediatoClimatología con el futuro0,73
Buques (AIS)10 exportadoras (5 puertos)DiarioUn puerto no es una empresa0,47
Descargas de apps30Semanal, 2 días—0,29
Visitas web (limpias)30Semanal, 2 díasPicos de bots0,26
Tarjetas25Diario, 3 díasCambios de panel, demografía0,21
Reseñas (sin las marcadas)10DiarioReseñas falsas0,19
Empleo (q−2)60MensualVariantes del nombre0,13
Patentes4018 mesesCensura, anticipación0,00

Dos advertencias para leer la ficha. Las correlaciones salen de datos sintéticos con efectos plantados: miden lo bien que cada método sencillo recupera lo que el generador puso, no lo que valen las fuentes reales. Y una correlación con la sorpresa no es una estrategia: falta saber cuánto de esa información ya está en el precio cuando el dato llega (en el curso, cada vez más desde 2021; lección 6.7.5) y cuánto cuesta (lección 6.7.1).

Errores típicos

  • Usar el gasto bruto de un panel. Sin normalizar por tarjetas activas, la entrada de un banco parece un 38 % de crecimiento de la demanda.
  • Fechar por el evento y no por la entrega. Un dato de gasto del día t se conoce en t + 3; una patente solicitada hoy se conoce dentro de 18 meses.
  • No limpiar el tráfico automatizado. Los bots reducen casi a la mitad lo que las visitas dicen de la demanda.
  • Contar reseñas falsas. Inflan la media y la desconectan de la satisfacción real.
  • Construir series por fecha de solicitud o de evento cuando el dato se publica tarde: anticipación, y una caída artificial al final de la muestra.
  • Calcular normales o climatologías con toda la muestra. Es una forma discreta de usar el futuro.
  • Tomar la correlación de una fuente por una señal operable. Falta el precio: lo que el mercado ya sabe.

Ejercicios

  1. Repite la detección de bots con una mediana centrada (las 4 semanas anteriores y las 4 siguientes) en vez de la de las 8 anteriores. ¿Mejora la detección? ¿Se podría usar en producción?
Solución
IN [16]python
centrada = web.groupby("dominio")["visitas_millones"].transform(
    lambda s: s.rolling(9, center=True, min_periods=5).median())
marca_c = web["visitas_millones"] / centrada > 1.7
acierto = (marca_c & web["verdad"]).sum()
print(f"marcadas {marca_c.sum()}, precisión "
      f"{acierto / marca_c.sum():.1%}, exhaustividad "
      f"{acierto / web['verdad'].sum():.1%}")
OUT [16]Solución: mediana centrada
marcadas 161, precisión 100.0%, exhaustividad 100.0%

La mediana centrada encuentra las 161 semanas de bots sin falsos positivos (al principio de cada serie ya tiene datos), pero usa las cuatro semanas siguientes: en producción, la semana marcada no se podría corregir hasta un mes después. Para limpiar la historia de un backtest vale solo si la limpieza se aplica con el mismo retraso; si no, el backtest ve datos más limpios de los que habría tenido el día de la entrega.

  1. Calcula el índice de clima con una climatología solo con el pasado: para cada trimestre, la media del mismo trimestre de los años anteriores (desde 2019, para tener al menos un año). Compara la correlación con la sorpresa.
Solución
IN [17]python
c2 = clima.groupby(["region", "q"])[["hdd", "cdd"]].sum()
c2 = c2.reset_index().sort_values(["region", "q"])
c2["trim"] = c2["q"].dt.quarter
for col in ["hdd", "cdd"]:
    previa = c2.groupby(["region", "trim"])[col].transform(
        lambda s: s.shift(1).expanding().mean())
    c2[col] = c2[col] - previa
c2["x"] = c2["hdd"] + 1.5 * c2["cdd"]
print(frente_a_verdad(regiones.merge(c2, on="region")))
OUT [17]Solución: climatología expansiva
n                324.000
corr_z             0.826
corr_sorpresa      0.697
dtype: float64

La correlación con la sorpresa baja poco, de 0,73 a 0,70, y se pierde el primer año (324 empresa-trimestres en vez de 372): el clima del generador no tiene tendencia, así que la media de toda la muestra y la de los años anteriores se parecen. Con datos reales, con calentamiento, la climatología de toda la muestra mezcla inviernos más cálidos del futuro con los del pasado y desplaza el índice. Lo honesto es la versión expansiva (o las normales climatológicas oficiales de 30 años vigentes en cada fecha).

  1. Elige un tipo de dato alternativo que el curso no trae (recibos de correo electrónico, geolocalización de móviles o imágenes de satélite reales) y escribe su ficha: qué mide, cobertura probable, retraso, sesgos y riesgos legales.
Solución

Para la geolocalización de móviles: mide visitas a tiendas, oficinas o fábricas a partir de la ubicación que envían los SDK integrados en apps. Cobertura: los usuarios de esas apps que dieron permiso, sesgados por edad, sistema operativo y país; el panel cambia cuando una app deja de compartir datos (como el salto del banco en las tarjetas). Retraso: de días a una semana. Sesgos: tiendas pequeñas con pocas visitas medidas, polígonos mal dibujados (la tienda frente al aparcamiento compartido), cambios de política de privacidad de los sistemas operativos. Riesgo legal: son datos personales (la ubicación identifica a una persona), así que en la UE entra el RGPD: base jurídica, minimización y agregación antes de vender (lecciones 6.7.3 y 6.7.4). La pregunta de due diligence: cómo obtuvo el proveedor el consentimiento y a qué nivel de agregación entrega.

Footnotes

  1. OMPI, «Cómo proteger sus invenciones en otros países: preguntas frecuentes sobre el Tratado de Cooperación en materia de Patentes (PCT)» (https://www.wipo.int/pct/es/faqs/faqs.html, consultada el 2026-10-23): la OMPI publica la solicitud internacional «apenas vencido el plazo de 18 meses a contar desde la fecha de prioridad». ↩

  2. OMI, «AIS transponders» (https://www.imo.org/en/OurWork/Safety/Pages/AIS.aspx, consultada el 2026-10-23): regla V/19 del convenio SOLAS, buques de 300 GT o más en viajes internacionales, de carga de 500 GT o más que no hacen viajes internacionales y todos los de pasaje. ↩

  3. NOAA, Climate Prediction Center, «Weekly & Monthly Degree Day Summaries Explanation» (https://www.cpc.ncep.noaa.gov/products/analysis_monitoring/cdus/degree_days/ddayexp.shtml, consultada el 2026-10-23): «Heating degree days are summations of negative differences between the mean daily temperature and the 65°F base» (18,3 °C), y los de refrigeración, de las diferencias positivas sobre la misma base. El curso usa bases distintas (18 y 22 °C) a propósito. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. El 1 de marzo de 2021 el gasto total con tarjeta del panel sube un 38 % de un día para otro. ¿Qué es lo primero que hay que comprobar?

    • ASi hubo una campaña de rebajas
    • BSi el número de tarjetas del panel cambió: la entrada de un banco nuevo infla el gasto bruto sin que cambie la demanda
    • CSi el proveedor cambió de moneda
    • DNada: un salto así es una señal alcista
    Ver respuesta

    Respuesta B. Si el número de tarjetas del panel cambió: la entrada de un banco nuevo infla el gasto bruto sin que cambie la demanda

    En los datos del curso entra un banco con un 36 % más de tarjetas; el gasto por tarjeta activa apenas se mueve (+1,5 %). Normalizar por el tamaño del panel es el primer paso con cualquier dato de panel.

  2. Una fuente de patentes da, para cada solicitud, la fecha de solicitud y la de publicación (unos 18 meses después). ¿Qué fecha se usa en un backtest?

    • ALa de solicitud, porque es cuando ocurre la innovación
    • BCualquiera: la diferencia es pequeña
    • CLa media de las dos
    • DLa de publicación: antes nadie fuera de la empresa podía conocer la solicitud
    Ver respuesta

    Respuesta D. La de publicación: antes nadie fuera de la empresa podía conocer la solicitud

    Contar patentes por fecha de solicitud usa información que se publicó año y medio después: anticipación. Además, las solicitudes de los últimos 18 meses aún no aparecen en los datos (censura), así que las series por solicitud caen artificialmente al final.

  3. En el tráfico web del curso, un 1,3 % de las semanas tiene un pico de bots de 2 a 4 veces el tráfico normal. ¿Qué pasa si no se limpian?

    • ALa correlación del crecimiento de las visitas con el choque de demanda baja de 0,32 a 0,17
    • BNada: son pocas semanas
    • CMejora la señal, porque los bots siguen a las empresas que crecen
    • DSolo afecta a las descargas de la app
    Ver respuesta

    Respuesta A. La correlación del crecimiento de las visitas con el choque de demanda baja de 0,32 a 0,17

    Un pico de bots entra entero en la suma del trimestre y en el crecimiento interanual. Con una mediana móvil de las 8 semanas anteriores se detectan casi todos (98,8 %, sin falsos positivos en estos datos) y la correlación con el choque de demanda casi se duplica.

  4. Las ofertas de empleo del curso anticipan la demanda dos trimestres. ¿Qué correlación esperarías entre el crecimiento de las ofertas de un trimestre y la sorpresa de ventas de ese mismo trimestre?

    • AAlta, porque contratar es caro
    • BNegativa, porque más ofertas son más costes
    • CCercana a cero: la información de las ofertas se refleja en las ventas dos trimestres después
    • DIgual que con dos trimestres de retraso
    Ver respuesta

    Respuesta C. Cercana a cero: la información de las ofertas se refleja en las ventas dos trimestres después

    En los datos, el crecimiento de las ofertas de q−2 correlaciona 0,13 con la sorpresa de q y 0,69 con la intención de contratar que mide; el del mismo trimestre, −0,02. Cada fuente tiene su horizonte.

  5. Las reseñas falsas del curso son de 5 estrellas y sin compra verificada. Una regla que marca todas las reseñas de 5 estrellas sin compra verificada…

    • Aacierta siempre
    • Bencuentra todas las falsas, pero un 22 % de lo que marca son reseñas auténticas
    • Cno encuentra ninguna
    • Dsolo sirve en inglés
    Ver respuesta

    Respuesta B. encuentra todas las falsas, pero un 22 % de lo que marca son reseñas auténticas

    La regla tiene una exhaustividad del 100 % y una precisión del 78 %: hay clientes reales que ponen 5 estrellas sin compra verificada. Quitar las falsas sube la correlación de las estrellas medias con el choque de demanda de 0,24 a 0,35.

Ideas clave

  • Cada fuente mide una cosa distinta: gasto en comercios, visitas y descargas, intención de contratar, satisfacción, actividad de un puerto, demanda de energía por el tiempo. La relación con las ventas depende de la empresa y del sector.
  • La cobertura es parcial y desigual: en el curso, de 10 a 60 de las 120 empresas por fuente. Un modelo que usa una fuente solo opera su universo.
  • Tres fechas por dato: la del evento, la de entrega y la de publicación. Tarjetas, 3 días; tráfico web, 2 días tras la semana; patentes, unos 18 meses. En el backtest manda la fecha en que se pudo conocer.
  • Cada fuente trae su suciedad: saltos de panel, picos de bots, variantes de nombre, reseñas falsas, censura de lo aún no publicado. Se miden y se corrigen antes de buscar señal.
  • Correlación con la sorpresa de ventas en el curso: clima 0,73, buques 0,47, descargas 0,29, visitas limpias 0,26, tarjetas 0,21, reseñas 0,19, empleo (dos trimestres antes) 0,13; las patentes, el control sin efecto, 0,00.

Recursos