---
title: "6.7.2 Tipos de datos alternativos: qué mide cada uno"
description: "Tarjetas, tráfico web, empleo, reseñas, buques, clima, patentes y on-chain: qué mide cada fuente, cobertura, retraso y sesgos, medidos con datos."
url: "https://pythonparatrading.com/curso-python-trading/nivel-6/tipos-de-datos-alternativos/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-6/tipos-de-datos-alternativos.png"
---

# 6.7.2 · 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.

- Curso: [Nivel 6: IA generativa, LLMs, agentes, big data y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/)
- Módulo: 6.7 · Datos alternativos
- Duración: 65 min
- Requisitos: [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/), [5.2.2 · Fundamentales point-in-time, macro y datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-5/fundamentales-point-in-time-macro-y-datos-alternativos/), [6.7.1 · Panorama y tamaño del mercado de datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/panorama-del-mercado-de-datos-alternativos/)

## Al terminar esta lección

- Describir los tipos principales de dato alternativo (transacciones, tráfico web y de apps, empleo, reseñas, AIS, clima, patentes, satélite, geolocalización, on-chain) por lo que miden y cómo se recogen.
- Medir la cobertura, la frecuencia y el retraso de entrega de cada fuente, y distinguir fecha del evento, fecha de entrega y fecha de publicación.
- Detectar en los datos los sesgos típicos: cambios de panel, picos de bots, variantes de nombre, reseñas falsas y censura por publicación tardía.
- Comparar cada fuente con su verdad conocida y con la sorpresa de ventas, sin confundir lo que mide con lo que predice.

## 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`.

| Tipo | Qué mide | En el curso | Real (ejemplos de recogida) |
|---|---|---|---|
| Transacciones con tarjeta | Gasto en los comercios de la empresa | `alt6_tarjetas_pit.parquet` | Paneles de bancos o agregadores de pagos |
| Recibos | Ticket, productos, precio pagado | — | Recibos de correo electrónico con permiso del usuario |
| Tráfico web y de apps | Visitas, descargas, uso | `alt6_trafico_web.parquet` | Paneles de navegación, tiendas de apps |
| Ofertas de empleo | Intención de contratar, por área y país | `alt6_empleo.parquet` | Scraping de portales y webs de empleo (lección 6.7.3) |
| Reseñas y precios publicados | Satisfacción, defectos, precios | `alt6_resenas.parquet` | Scraping de tiendas y portales de reseñas |
| Envíos (AIS) | Llegadas de buques, esperas en puerto | `alt6_ais_llegadas.parquet` | Transpondedores AIS de los buques |
| Clima | Temperaturas, grados-día, lluvia | `alt6_clima.parquet` | Servicios meteorológicos nacionales |
| Patentes | Solicitudes publicadas por clase | `alt6_patentes.parquet` | Oficinas de patentes (EPO, USPTO, OMPI) |
| Satélite y geolocalización | Coches en aparcamientos, tránsito de personas | `satelite6_imagenes.parquet` (lección 6.7.6) | Proveedores de imágenes; SDK en apps móviles |
| On-chain | Flujos, direcciones activas, comisiones | `onchain3_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:

```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:

```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
```
```output
           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):

```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
```
```output
                          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 patente[^1]. 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:

```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)
```
```output
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.* Dos líneas semanales de noviembre de 2020 a junio de 2021. Hasta febrero de 2021 van juntas, cerca de 100, con un pico en diciembre. Desde la semana del 7 de marzo, el gasto bruto salta a unos 139 y sube hasta unos 146; el gasto por tarjeta sigue entre 100 y 107. Una línea vertical destacada marca el 1 de marzo de 2021.

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:

```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"]
```
```output
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):

```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%}")
```
```output
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.* Línea semanal de 2018 a 2025 entre unos 30 y 50 millones de visitas, con estacionalidad, y diez picos aislados destacados de hasta 139 millones.

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:

```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
```
```output
                              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:

```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())
```
```output
['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:

```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)"]
```
```output
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:

```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%}")
```
```output
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.* Barras: 1 estrella 8,1 %, 2 estrellas 4,2 %, 3 estrellas 7,2 %, 4 estrellas 16,7 % y 5 estrellas 63,8 %, de las que 41,7 puntos son auténticas y 22,1 falsas (destacadas).

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):

```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
```
```output
                           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 pasaje[^2]. 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:

```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
```
```output
                          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 encima[^3]. 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:

```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"]
```
```output
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:

```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)"]
```
```output
{'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:

```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%}")
```
```output
{'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:

```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)
```
```output
                             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).* Barras horizontales: clima 0,73 (destacado), buques 0,47, descargas 0,29, visitas limpias 0,26, tarjetas 0,21, reseñas sin las marcadas 0,19, visitas con bots 0,18, empleo 0,13 y patentes 0,00.

| Fuente | Cobertura | Retraso | Sesgo principal | Corr. con la sorpresa |
|---|---|---|---|---|
| Clima | 12 eléctricas (6 regiones) | Diario, inmediato | Climatología con el futuro | 0,73 |
| Buques (AIS) | 10 exportadoras (5 puertos) | Diario | Un puerto no es una empresa | 0,47 |
| Descargas de apps | 30 | Semanal, 2 días | — | 0,29 |
| Visitas web (limpias) | 30 | Semanal, 2 días | Picos de bots | 0,26 |
| Tarjetas | 25 | Diario, 3 días | Cambios de panel, demografía | 0,21 |
| Reseñas (sin las marcadas) | 10 | Diario | Reseñas falsas | 0,19 |
| Empleo (q−2) | 60 | Mensual | Variantes del nombre | 0,13 |
| Patentes | 40 | 18 meses | Censura, anticipación | 0,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.**

```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%}")
```
```output
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.

2. 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.**

```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")))
```
```output
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).

3. 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.

[^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.

## 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?

   - A. Si hubo una campaña de rebajas
   - 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
   - C. Si el proveedor cambió de moneda
   - D. Nada: un salto así es una señal alcista

   *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?

   - A. La de solicitud, porque es cuando ocurre la innovación
   - B. Cualquiera: la diferencia es pequeña
   - C. La media de las dos
   - D. La de publicación: antes nadie fuera de la empresa podía conocer la solicitud

   *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?

   - A. La correlación del crecimiento de las visitas con el choque de demanda baja de 0,32 a 0,17
   - B. Nada: son pocas semanas
   - C. Mejora la señal, porque los bots siguen a las empresas que crecen
   - D. Solo afecta a las descargas de la app

   *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?

   - A. Alta, porque contratar es caro
   - B. Negativa, porque más ofertas son más costes
   - C. Cercana a cero: la información de las ofertas se refleja en las ventas dos trimestres después
   - D. Igual que con dos trimestres de retraso

   *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…

   - A. acierta siempre
   - B. encuentra todas las falsas, pero un 22 % de lo que marca son reseñas auténticas
   - C. no encuentra ninguna
   - D. solo sirve en inglés

   *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

### Documentación

- [OMI: sistemas de identificación automática (AIS)](https://www.imo.org/en/OurWork/Safety/Pages/AIS.aspx) · imo.org
- [NOAA Climate Prediction Center: grados-día (explicación)](https://www.cpc.ncep.noaa.gov/products/analysis_monitoring/cdus/degree_days/ddayexp.shtml) · cpc.ncep.noaa.gov
- [OMPI: Tratado de Cooperación en materia de Patentes (PCT), publicación internacional a los 18 meses](https://www.wipo.int/pct/es/faqs/faqs.html) · wipo.int

### Datos

- [Datos del nivel 6: transacciones con tarjeta point-in-time (Parquet)](https://pythonparatrading.com/curso/datos/nivel-6/alt6_tarjetas_pit.parquet) · Descarga · alt6_tarjetas_pit.parquet
- [Datos del nivel 6: tráfico web y descargas de apps (Parquet)](https://pythonparatrading.com/curso/datos/nivel-6/alt6_trafico_web.parquet) · Descarga · alt6_trafico_web.parquet
- [Datos del nivel 6: llegadas de buques por puerto (Parquet)](https://pythonparatrading.com/curso/datos/nivel-6/alt6_ais_llegadas.parquet) · Descarga · alt6_ais_llegadas.parquet
- [Datos del nivel 6: verdad trimestral de los datos alternativos (CSV)](https://pythonparatrading.com/curso/datos/nivel-6/alt6_verdad_trimestral.csv) · Descarga · alt6_verdad_trimestral.csv

Lección anterior: [6.7.1 · Panorama y tamaño del mercado de datos alternativos](https://pythonparatrading.com/curso-python-trading/nivel-6/panorama-del-mercado-de-datos-alternativos/)

Lección siguiente: [6.7.3 · Web scraping responsable](https://pythonparatrading.com/curso-python-trading/nivel-6/web-scraping-responsable/)
