Lección 6.1.1 · 70 min

Tokenización, embeddings, diccionarios financieros y FinBERT

Un modelo no lee palabras: lee números. Esta lección recorre las tres formas clásicas de convertir una noticia financiera en una puntuación de sentimiento (contar palabras de un diccionario, proyectarla en un espacio de embeddings y clasificarla con un modelo afinado como FinBERT) y las mide sobre un corpus sintético en el que se conoce el sentimiento verdadero de cada noticia.

Fig. — Correlación de cada puntuación con el sentimiento verdadero en las noticias de prueba (2023-2025): FinBERT es el mejor de los métodos que no se entrenan con el corpus en inglés y no sirve en español; la ridge se entrenó con las etiquetas del propio generador
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

Del texto a un número

Una estrategia que lee noticias necesita, al final, una columna de números: una puntuación por noticia que se pueda agregar, retrasar y correlacionar con rentabilidades. Entre el texto y esa columna hay siempre los mismos pasos: partir el texto en unidades, convertir esas unidades en números y resumirlos en una puntuación.

TextoTokensRecuento por léxicoEmbeddingModelo afinadoPuntuaciónModelo lineal
Fuente Mermaid
flowchart TB
  T[Texto] --> K[Tokens]
  K --> D[Recuento por léxico]
  K --> E[Embedding]
  K --> F[Modelo afinado]
  D --> S[Puntuación]
  E --> R[Modelo lineal]
  R --> S
  F --> S

Las tres ramas son las tres familias de esta lección: diccionarios (contar palabras positivas y negativas), embeddings (un vector por frase, con un modelo entrenado encima) y modelos afinados como FinBERT, que leen la frase y devuelven directamente la probabilidad de cada clase. Los LLM generativos, que también puntúan sentimiento, llegan en la lección 6.2.3.

Los datos son las noticias sintéticas del nivel 6: 39.570 piezas de cinco agencias ficticias sobre 120 empresas del panel del nivel 5, con su instante de publicación. Lo que no existe en un corpus real está en noticias6_verdad.csv: el sentimiento verdadero de cada noticia, entre −1 y 1, con el que el generador escribió el texto y movió el precio. Es la respuesta conocida contra la que se mide cada método.

IN [01]python
import re
import textwrap

import numpy as np
import pandas as pd

noticias = pd.read_parquet("noticias6_corpus.parquet")
verdad = pd.read_csv("noticias6_verdad.csv")
df = noticias.merge(verdad, on="id_noticia")
orig = df[df["tipo_noticia"] == "original"].copy()
orig["texto"] = orig["titular"] + ". " + orig["cuerpo"]
print(len(orig), "noticias originales")
print(orig["idioma"].value_counts().to_dict())
fila = orig.iloc[2]
print(fila["texto"])
print("sentimiento verdadero:", fila["sentimiento_verdadero"])
OUT [01]Noticias originales con su sentimiento verdadero
22593 noticias originales
{'es': 16969, 'en': 5624}
Placeholder & Co. cuts Numvia Communications price target to $19.23. Analysts at Placeholder & Co. cut their price target on Numvia Communications to $19.23. Numvia Communications issued a warning about business weakness.
sentimiento verdadero: -0.546

Las copias sindicadas (la misma historia en otra agencia, a veces traducida) y las crónicas de mercado se quedan fuera: una historia se puntúa una vez. Cómo detectar las copias es parte de la lección 6.1.2.

Tokenizar: palabras y subpalabras

La forma más simple de partir un texto es una expresión regular que extrae palabras. Es lo que usan los diccionarios: pasar a minúsculas y quedarse con secuencias de letras. NLTK añade reglas para la puntuación y las contracciones:

IN [02]python
from nltk.tokenize import word_tokenize

frase = "Numvia didn't beat estimates; revenue fell 3.5%."

def palabras(texto):
    return re.findall(r"[a-záéíóúüñ]+", texto.lower())

print(palabras(frase))
print(word_tokenize(frase))
OUT [02]Palabras con una expresión regular y con NLTK
['numvia', 'didn', 't', 'beat', 'estimates', 'revenue', 'fell']
['Numvia', 'did', "n't", 'beat', 'estimates', ';', 'revenue', 'fell', '3.5', '%', '.']

La expresión regular pierde la negación («didn't» se queda en didn y t) y las cifras; word_tokenize separa did y n't y conserva 3.5 y %. Para un diccionario basta la primera; para un modelo no sirve ninguna, porque un vocabulario de palabras completas nunca está completo: nombres de empresas, cifras y palabras raras quedarían fuera.

Los modelos actuales usan subpalabras: un vocabulario fijo (de 30.000 a 150.000 piezas) aprendido sobre un corpus, con el que cualquier texto se descompone en fragmentos conocidos. Las dos familias que importan aquí:

AlgoritmoCómo construye el vocabularioModelos de esta lección
WordPieceUne los pares de piezas que más aumentan la verosimilitud del corpus; marca las continuaciones con ##FinBERT y MiniLM (vocabulario de BERT, 30.522 piezas, minúsculas y sin acentos)
BPE a nivel de byteParte de los 256 bytes y une los pares más frecuentes; ningún texto queda fuera del vocabularioQwen2.5 (unas 151.000 piezas), cl100k_base de tiktoken

El BPE lo propusieron Sennrich, Haddow y Birch (2016) para traducción automática; la página de Hugging Face sobre algoritmos de tokenización explica las variantes1Nota 1 · Sennrich, Haddow y Birch (2016), «Neural Machine Translation of Rare Words with Subword Units» (https://arxiv.org/abs/1508.07909); Hugging Face, «Tokenization algorithms»: BPE, BPE a nivel de byte (GPT-2), WordPiece (familia BERT) y Unigram/SentencePiece (https://huggingface.co/docs/transformers/tokenizer_summary), consultado el 2026-10-23.. Lo importante para un quant es qué hace cada vocabulario con textos financieros en dos idiomas:

IN [03]python
import tiktoken
from transformers import AutoTokenizer

tok_bert = AutoTokenizer.from_pretrained("ProsusAI/finbert")
tok_qwen = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
tok_tik = tiktoken.get_encoding("cl100k_base")

en = "Revenue was $3,075.9 million, a weak quarter."
es = "Las ventas sumaron 3.075,9 millones: caída de la demanda."

def piezas(texto):
    q = [tok_qwen.convert_tokens_to_string([t])
         for t in tok_qwen.tokenize(texto)]
    t = [tok_tik.decode([i]) for i in tok_tik.encode(texto)]
    return {"wordpiece": tok_bert.tokenize(texto),
            "qwen": q, "cl100k": t}

for texto in (en, es):
    for nombre, p in piezas(texto).items():
        print(f"{nombre:9} {len(p):2}", "|".join(p))
    print()
OUT [03]La misma frase con tres tokenizadores
wordpiece 15 revenue|was|$|3|,|07|##5|.|9|million|,|a|weak|quarter|.
qwen      16 Revenue| was| $|3|,|0|7|5|.|9| million|,| a| weak| quarter|.
cl100k    14 Revenue| was| $|3|,|075|.|9| million|,| a| weak| quarter|.

wordpiece 22 las|vent|##as|sum|##aro|##n|3|.|07|##5|,|9|mill|##ones|:|cai|##da|de|la|demand|##a|.
qwen      21 Las| ventas| sum|aron| |3|.|0|7|5|,|9| millones|:| ca|ída| de| la| demand|a|.
cl100k    19 Las| ventas| sum|aron| |3|.|075|,|9| millones|:| ca|ída| de| la| demand|a|.

Tres detalles con consecuencias:

  • Cifras. WordPiece parte 3,075.9 en cinco piezas y Qwen escribe cada dígito por separado. Ningún modelo de lenguaje «ve» el número 3.075,9 como una magnitud; ve una secuencia de símbolos. Por eso la extracción de cifras con LLM necesita validación (lección 6.2.2).
  • Acentos y minúsculas. El vocabulario de BERT sin mayúsculas quita las tildes: «caída» pasa a cai + ##da.
  • Falsos amigos. «demanda» se convierte en demand + ##a: una pieza inglesa con otro significado. Un modelo entrenado con texto inglés asociará esa pieza a lo que significa demand en inglés.

El coste también cambia con el idioma. Medido sobre 400 noticias de cada idioma, en tokens por palabra:

IN [04]python
muestra = (orig.groupby("idioma", group_keys=False)
           .sample(400, random_state=0))
n_palabras = muestra["texto"].str.split().str.len()

def n_tokens(textos, nombre):
    if nombre == "cl100k":
        return np.array([len(tok_tik.encode(t)) for t in textos])
    tok = tok_bert if nombre == "wordpiece" else tok_qwen
    return np.array([len(tok.tokenize(t)) for t in textos])

coste = {}
for nombre in ("wordpiece", "qwen", "cl100k"):
    ratio = n_tokens(muestra["texto"], nombre) / n_palabras
    coste[nombre] = ratio.groupby(muestra["idioma"]).mean()
pd.DataFrame(coste).round(2)
OUT [04]Tokens por palabra según idioma y tokenizador
        wordpiece  qwen  cl100k
idioma
en           1.47  1.52    1.44
es           2.03  1.74    1.75
Fig. — Tokens por palabra de las noticias del corpus según el tokenizador y el idioma: el vocabulario de BERT, construido con texto inglés, trocea mucho más el español

El español cuesta más tokens que el inglés con los tres vocabularios, y la diferencia es mayor con el de BERT, que se construyó solo con texto inglés. Importa por tres motivos: las APIs comerciales cobran por token (lección 6.2.1), los modelos tipo BERT truncan la entrada a 512 tokens (FinBERT) o a 256 piezas (MiniLM, según su ficha2Nota 2 · Ficha de sentence-transformers/all-MiniLM-L6-v2 (https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2, consultada el 2026-10-23): licencia Apache-2.0, vectores de 384 dimensiones, inglés, y «by default, input text longer than 256 word pieces is truncated».) y un documento largo, como la sección de riesgos de un 10-K, no cabe entero; y un vocabulario que trocea mucho un idioma suele indicar que el modelo vio poco texto en él.

Diccionarios: de Harvard a Loughran-McDonald

Un diccionario de sentimiento es una lista de palabras con su categoría. La puntuación más usada cuenta las positivas (pp) y las negativas (nn) de un texto:

tono=p−np+n+1\text{tono} = \frac{p - n}{p + n + 1}

El 1 del denominador evita dividir por cero y acerca a cero los textos con pocas palabras de tono. El resultado está entre −1 y 1.

Durante años se usaron listas de propósito general, como el diccionario de Harvard (General Inquirer). Loughran y McDonald (2011) mostraron que en los informes anuales (10-K) casi tres cuartas partes de las apariciones de palabras «negativas» de esa lista corresponden a palabras que en un contexto financiero no lo son: tax, cost, liability, capital, vice (de vice president)3Nota 3 · Loughran y McDonald (2011), Journal of Finance 66(1), 35-65 (https://doi.org/10.1111/j.1540-6261.2010.01625.x): «almost three-fourths of the words identified as negative by the widely used Harvard Dictionary are words typically not considered negative in financial contexts». En el cuerpo del artículo la cifra es el 73,8 % de las apariciones de palabras negativas.. Construyeron listas propias del dominio, que mantienen y actualizan en su web con siete categorías: negativa, positiva, incertidumbre, litigio, modal fuerte, modal débil y restrictiva. Las listas son gratuitas para investigación académica y su uso comercial requiere licencia de los autores4Nota 4 · Universidad de Notre Dame, Software Repository for Accounting and Finance, «Loughran-McDonald Master Dictionary w/ Sentiment Word Lists» (https://sraf.nd.edu/loughranmcdonald-master-dictionary/, consultado el 2026-10-23): gratuito para investigación académica, licencia comercial previa consulta a los autores.. No se copian aquí.

El curso trae en su lugar texto6_lexico.csv, un léxico inventado con cinco de esas categorías en español y en inglés (lista financiera) y una lista general que imita el defecto de las de propósito general:

IN [05]python
lex = pd.read_csv("texto6_lexico.csv")
print(lex.pivot_table(index=["lista", "categoria"], columns="idioma",
                      values="palabra", aggfunc="size",
                      fill_value=0))
gen_neg = lex.query("lista == 'general' and categoria == 'negativa'"
                    " and idioma == 'es'")
print(textwrap.fill(" ".join(gen_neg["palabra"]), 72))
OUT [05]El léxico del curso
idioma                    en  es
lista      categoria
financiera incertidumbre  15  17
           litigio        12  14
           negativa       26  31
           positiva       23  31
           restrictiva     9   9
general    negativa       17  17
           positiva        9  10
cargo cargos caída coste costes crudo cáncer demanda deuda débil
impuesto impuestos pasivo pasivos pérdida quiebra vicepresidente

La lista general marca como negativas coste, deuda, impuesto, pasivo, crudo, vicepresidente y demanda: el mismo error que encontraron Loughran y McDonald. Puntuar todas las noticias con las dos listas y compararlas con el sentimiento verdadero:

IN [06]python
listas = {}
for (lista, idioma), g in lex.groupby(["lista", "idioma"]):
    pos = set(g.loc[g["categoria"] == "positiva", "palabra"])
    neg = set(g.loc[g["categoria"] == "negativa", "palabra"])
    listas[(lista, idioma)] = (pos, neg)

def tono(texto, idioma, lista="financiera"):
    pos, neg = listas[(lista, idioma)]
    w = palabras(texto)
    p = sum(x in pos for x in w)
    n = sum(x in neg for x in w)
    return (p - n) / (p + n + 1)

orig["dic_fin"] = [tono(t, i) for t, i in
                   zip(orig["texto"], orig["idioma"])]
orig["dic_gen"] = [tono(t, i, "general") for t, i in
                   zip(orig["texto"], orig["idioma"])]
s = orig["sentimiento_verdadero"]
print("financiera:", round(orig["dic_fin"].corr(s), 3))
print("general:   ", round(orig["dic_gen"].corr(s), 3))
print("noticias neutras con tono general < 0: "
      f"{(orig.loc[s == 0, 'dic_gen'] < 0).mean():.1%}")
OUT [06]Tono de diccionario frente al sentimiento verdadero
financiera: 0.709
general:    0.351
noticias neutras con tono general < 0: 7.0%

La lista financiera se correlaciona 0,71 con el sentimiento verdadero; la general, la mitad (0,35). La última línea apunta a una causa: un 7 % de las noticias neutras (sentimiento exactamente cero, como una junta o una conferencia) recibe un tono negativo de la lista general. ¿Qué palabras disparan esos falsos negativos?

IN [07]python
_, neg_gen = listas[("general", "es")]
neutras = orig.loc[(s == 0) & (orig["idioma"] == "es"), "texto"]
cuenta = pd.Series([w for t in neutras for w in palabras(t)
                    if w in neg_gen]).value_counts()
cuenta.head(6)
OUT [07]Palabras de la lista general en noticias neutras
demanda    426
caída       52
Name: count, dtype: int64

Casi todos salen de «demanda», en frases como «el impacto final dependerá de la demanda»: demanda de clientes, no un pleito. La lista financiera la clasifica como litigio (una categoría que no entra en el tono) y la general, como negativa. Es el mismo tipo de error que encontraron Loughran y McDonald con liability o tax: una palabra que en otro contexto es mala noticia y en el financiero es descriptiva.

Lo que un diccionario no puede ver

Contar palabras ignora el orden. Dos tipos de frase lo rompen: las negaciones («la compañía descarta pérdidas» contiene pérdidas) y las frases de tono sin palabras del léxico («es un jarro de agua fría para los accionistas»). El generador apunta en noticias6_verdad.csv cuántas frases de cada tipo lleva cada noticia, así que se puede medir dónde falla el diccionario:

IN [08]python
grupo = np.select(
    [orig["frases_negacion"] > 0, orig["frases_fuera_lexico"] > 0],
    ["con negación", "fuera del léxico"], "solo léxico")
orig["grupo"] = grupo
por_grupo = orig.groupby("grupo").apply(
    lambda g: pd.Series({
        "noticias": len(g),
        "corr_dic": g["dic_fin"].corr(g["sentimiento_verdadero"]),
    }), include_groups=False)
por_grupo.round(3)
OUT [08]Correlación del diccionario por tipo de noticia
                  noticias  corr_dic
grupo
con negación        2954.0     0.210
fuera del léxico    3718.0     0.833
solo léxico        15921.0     0.784

En las noticias con una negación, la correlación cae a 0,21, una cuarta parte de la del resto: el diccionario lee la palabra negativa y se equivoca de signo. Las frases fuera del léxico, en cambio, no bajan la correlación: en el generador, las noticias con más intensidad llevan más frases de tono (dos de media, frente a menos de una), y las demás frases sí usan el léxico. Un ejemplo de negación:

IN [09]python
ej = orig[(orig["frases_negacion"] > 0)
          & (orig["idioma"] == "es")
          & (orig["sentimiento_verdadero"] > 0.4)
          & (orig["dic_fin"] < 0)].iloc[0]
print(textwrap.fill(ej["cuerpo"], 72))
print("verdadero:", ej["sentimiento_verdadero"],
      "| diccionario:", round(ej["dic_fin"], 3))
OUT [09]Una negación leída al revés
Kogsa Bancorp ha firmado un contrato de 829,7 millones de dólares con la
administración de un estado de EE. UU. para los próximos 3 años. El
grupo deja atrás las dificultades del año pasado. Las acciones cerraron
la última sesión en 201,85 dólares.
verdadero: 0.7784 | diccionario: -0.5

«Deja atrás las dificultades» es una buena noticia escrita con una palabra negativa del léxico (dificultades). El diccionario la cuenta como negativa y la noticia, claramente positiva, puntúa −0,5.

Los diccionarios tienen, aun así, tres virtudes que mantienen su uso en research y en producción: son transparentes (se sabe por qué una noticia puntúa lo que puntúa), baratos (millones de documentos en segundos, sin GPU) y estables (la misma lista da la misma puntuación hoy y dentro de diez años, sin sesgo de anticipación; lección 6.2.5). Las mejoras habituales (reglas de negación en una ventana de pocas palabras, ponderación tf-idf, listas por sector) reducen sus errores sin cambiar su naturaleza.

Embeddings: una frase como vector

Un embedding de frase es un vector de dimensión fija que resume su significado: frases parecidas quedan cerca y la cercanía se mide con la similitud coseno (el producto escalar de los vectores normalizados). all-MiniLM-L6-v2 es un modelo pequeño de sentence-transformers (22 millones de parámetros, 384 dimensiones, entrenado con texto inglés2) que sigue la idea de Sentence-BERT5Nota 5 · Reimers y Gurevych (2019), «Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks» (https://arxiv.org/abs/1908.10084).. Sus pesos están en la caché del entorno ia, fijados por commit:

IN [10]python
import torch
from sentence_transformers import SentenceTransformer

torch.manual_seed(0)
st = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2",
                         device="cpu", local_files_only=True)
frases = [
    "The company reported record profits.",
    "Earnings hit an all-time high.",
    "The company warned of heavy losses.",
    "La empresa obtuvo beneficios récord.",
    "La empresa advirtió de fuertes pérdidas.",
]
E = st.encode(frases, normalize_embeddings=True,
              show_progress_bar=False)
print(E.shape)
pd.DataFrame(E @ E.T, index=range(5), columns=range(5)).round(2)
OUT [10]Similitud coseno entre frases
(5, 384)
      0     1     2     3     4
0  1.00  0.53  0.42  0.41  0.18
1  0.53  1.00  0.27  0.21  0.07
2  0.42  0.27  1.00  0.14  0.15
3  0.41  0.21  0.14  1.00  0.68
4  0.18  0.07  0.15  0.68  1.00

Las frases 0 y 1 dicen lo mismo con palabras distintas y su similitud es la más alta de las inglesas (0,53); la 2 comparte casi todas las palabras con la 0 y dice lo contrario, pero su similitud no es mucho menor (0,42). Un embedding de propósito general mide sobre todo de qué habla una frase, no en qué sentido. En español es peor: las frases 3 y 4, que dicen lo contrario, son las más parecidas de la tabla (0,68), y cada una queda lejos de su traducción inglesa (0,41 y 0,15). MiniLM no es multilingüe: con texto español compara sobre todo la forma.

Para convertir embeddings en sentimiento hace falta un modelo encima entrenado con ejemplos etiquetados. En real, las etiquetas saldrían de un conjunto anotado (como Financial PhraseBank, el que usó FinBERT) o de la reacción del precio; aquí, para medir el techo del método, se usa el sentimiento verdadero de 2019 a 2022 y se evalúa de 2023 en adelante, con la separación temporal del nivel 5 (lección 5.4.3). Una muestra de 1.200 noticias por idioma basta para la comparación y mantiene la celda en segundos en CPU:

IN [11]python
from sklearn.linear_model import Ridge

m = (orig.groupby("idioma", group_keys=False)
     .sample(1200, random_state=1).sort_values("ts"))
X = st.encode(m["texto"].tolist(), batch_size=64,
              normalize_embeddings=True, show_progress_bar=False)
y = m["sentimiento_verdadero"].to_numpy()
entreno = (m["ts"] < "2023-01-01").to_numpy()
m["ridge"] = np.nan
for idioma in ("en", "es"):
    i = (m["idioma"] == idioma).to_numpy()
    modelo = Ridge(alpha=1.0).fit(X[i & entreno], y[i & entreno])
    prueba = i & ~entreno
    m.loc[m.index[prueba], "ridge"] = modelo.predict(X[prueba])

test = m[m["ts"] >= "2023-01-01"]
test.groupby("idioma").apply(lambda g: pd.Series({
    "prueba": len(g),
    "corr_ridge": g["ridge"].corr(g["sentimiento_verdadero"]),
    "corr_dic": g["dic_fin"].corr(g["sentimiento_verdadero"]),
}), include_groups=False).round(3)
OUT [11]Ridge sobre embeddings con separación temporal
        prueba  corr_ridge  corr_dic
idioma
en       524.0       0.919     0.702
es       491.0       0.826     0.729

Con unos cientos de ejemplos por idioma, la regresión sobre embeddings supera al diccionario en los dos idiomas, y más en inglés, el idioma de MiniLM, que en español. Hay que leer la cifra con cuidado: el modelo lineal ha aprendido las plantillas del generador con etiquetas perfectas, y las de 2023 son las mismas que las de 2019. Con noticias reales, unas pocas centenas de etiquetas ruidosas no dan para tanto, y el lenguaje cambia con los años. La ventaja duradera de los embeddings está en otra parte: el mismo vector sirve para buscar documentos parecidos, agrupar noticias por tema (lección 6.1.3) o alimentar un RAG (lección 6.3.1).

FinBERT: un clasificador afinado para finanzas

FinBERT (Araci, 2019)6Nota 6 · Araci (2019), «FinBERT: Financial Sentiment Analysis with Pre-trained Language Models» (https://arxiv.org/abs/1908.10063); ficha de ProsusAI/finbert (https://huggingface.co/ProsusAI/finbert, consultada el 2026-10-23): licencia CC-BY-NC-SA-4.0, tres etiquetas y afinado con Financial PhraseBank. es un BERT de tamaño base (110 millones de parámetros) preentrenado con texto financiero y afinado para clasificar frases en tres clases (positiva, negativa, neutra) con Financial PhraseBank, un conjunto de frases de noticias financieras en inglés anotadas a mano. La versión de ProsusAI/finbert es la más usada.

FraseWordPieceBERT baseCapa de clasesProbabilidades
Fuente Mermaid
flowchart LR
  F[Frase] --> W[WordPiece]
  W --> B[BERT base]
  B --> C[Capa de clases]
  C --> P[Probabilidades]

La salida son tres probabilidades que suman uno. Una puntuación continua habitual es P(positiva)−P(negativa)P(\text{positiva}) - P(\text{negativa}), entre −1 y 1, comparable con el tono del diccionario:

IN [12]python
from transformers import pipeline

finbert = pipeline("text-classification", model="ProsusAI/finbert",
                   device="cpu")

def puntua(textos, lote=32):
    salidas = finbert(list(textos), batch_size=lote, top_k=None,
                      truncation=True)
    filas = [{d["label"]: d["score"] for d in s} for s in salidas]
    probs = pd.DataFrame(filas)[["positive", "negative", "neutral"]]
    return probs.assign(fb=probs["positive"] - probs["negative"])

pruebas = ["The company does not expect any losses this year.",
           "Revenue declined sharply and the outlook was cut.",
           "The annual meeting will be held in Madrid."]
puntua(pruebas).round(3)
OUT [12]FinBERT en local: tres frases
   positive  negative  neutral     fb
0     0.058     0.345    0.596 -0.287
1     0.008     0.974    0.019 -0.966
2     0.027     0.026    0.947  0.001

La segunda frase es negativa sin dudas (0,97) y la tercera, neutra (0,95). La primera es una buena noticia escrita con una negación, y FinBERT tampoco la acierta: la deja entre neutra (0,60) y negativa (0,35). Una frase suelta no prueba nada; hace falta medirlo sobre muchas. Se usan las noticias del periodo de prueba de la celda anterior: las 524 inglesas y 200 españolas al azar (FinBERT es el paso más lento de la lección: unos segundos por cada cien noticias en CPU), y así los cuatro métodos se comparan sobre las mismas noticias:

IN [13]python
sub_en = test[test["idioma"] == "en"].copy()
sub_es = test[test["idioma"] == "es"].sample(200, random_state=2)
sub_en["fb"] = puntua(sub_en["texto"])["fb"].to_numpy()
sub_es["fb"] = puntua(sub_es["texto"])["fb"].to_numpy()

def correlaciones(g):
    s = g["sentimiento_verdadero"]
    return pd.Series({"noticias": len(g),
                      "finbert": g["fb"].corr(s),
                      "ridge": g["ridge"].corr(s),
                      "dic_fin": g["dic_fin"].corr(s),
                      "dic_gen": g["dic_gen"].corr(s)})

pd.DataFrame({"en": correlaciones(sub_en),
              "es": correlaciones(sub_es)}).T.round(3)
OUT [13]FinBERT frente al diccionario y la verdad
    noticias  finbert  ridge  dic_fin  dic_gen
en     524.0    0.876  0.919    0.702    0.459
es     200.0   -0.114  0.857    0.760    0.473

En inglés, FinBERT (0,88) supera con claridad al diccionario financiero (0,70) y a la lista general (0,46) sin haber visto ninguna noticia del corpus. Solo lo supera la ridge sobre embeddings (0,92), entrenada con las etiquetas perfectas de las mismas plantillas: una ventaja que con datos reales no existe. En español, FinBERT da −0,11: el modelo no conoce el idioma y su tokenizador lo trocea en fragmentos sin sentido, mientras que el diccionario español y la ridge mantienen su nivel. La figura de la cabecera resume los cuatro métodos en las 524 noticias inglesas. ¿Dónde gana FinBERT? Donde el diccionario falla:

IN [14]python
sub_en.groupby("grupo").apply(
    lambda g: pd.Series({
        "noticias": len(g),
        "finbert": g["fb"].corr(g["sentimiento_verdadero"]),
        "dic_fin": g["dic_fin"].corr(g["sentimiento_verdadero"]),
    }), include_groups=False).round(3)
OUT [14]Por tipo de noticia: dónde gana FinBERT
                  noticias  finbert  dic_fin
grupo
con negación          72.0    0.870    0.100
fuera del léxico      91.0    0.886    0.841
solo léxico          361.0    0.879    0.790
Fig. — Correlación con el sentimiento verdadero por tipo de noticia (noticias inglesas de prueba): el diccionario se hunde con las negaciones y FinBERT no

En las noticias que solo usan el léxico, el diccionario (0,79) se acerca a FinBERT (0,88): contar palabras basta cuando el texto las contiene. En las que llevan una negación, el diccionario se hunde (0,10) y FinBERT mantiene su nivel (0,87). Que fallara con la frase suelta de la celda anterior y acierte en el grupo no es una contradicción: en las noticias la negación va acompañada del resto del texto, y lo que se mide es la correlación de muchas. Con 524 noticias, los grupos pequeños tienen un error de muestreo apreciable (con unas 70 noticias, una correlación de 0,1 tiene un error típico de algo más de 0,1), así que conviene leer el orden y no los decimales.

Qué elegir y qué cuesta

MétodoCosteIdiomasTransparenciaRiesgo principal
Diccionario financieroCasi nuloEl de la listaTotalNegaciones, contexto, palabras nuevas
Diccionario generalCasi nuloEl de la listaTotalVocabulario contable leído como negativo
Embeddings + modelo linealBajo; necesita etiquetasLos del modelo de embeddingsBajaEtiquetas caras o ruidosas; deriva del lenguaje
FinBERTMedio (CPU o GPU)InglésBajaLicencia no comercial; 512 tokens; dominio de entrenamiento
LLM generativo (6.2.3)AltoMuchosBajaCoste, formato, sesgo de anticipación (6.2.5)

Tres advertencias para llevar a un backtest:

  • La puntuación no es la señal. Un sentimiento bien medido solo vale si mueve el precio después de que se pueda operar. Medir esa reacción exige alinear cada noticia con la sesión correcta: es la lección 6.1.3, donde el mismo diccionario recupera unos 75 pb por unidad de tono el día de reacción frente a 119 pb con el sentimiento verdadero (README del nivel 6).
  • Un modelo afinado también tiene fecha. FinBERT se entrenó con frases anteriores a 2019; un clasificador entrenado con datos posteriores al periodo del backtest introduce información del futuro (lección 6.2.5).
  • El idioma es una decisión de diseño. Con un universo europeo, la mitad de las noticias puede estar en un idioma que el modelo no entiende. En este corpus, aplicar FinBERT a todo habría estropeado la señal del 75 % de las noticias.

Ejercicios

  1. Añade al diccionario una regla de negación: si una de las tres palabras anteriores a una palabra del léxico es «no», «descarta», «sin» o «niega», cambia su signo. Mide la correlación con el sentimiento verdadero en las noticias españolas con negación antes y después.
Solución
IN [15]python
NEG = {"no", "descarta", "sin", "niega"}

def tono_neg(texto, idioma="es"):
    pos, neg = listas[("financiera", idioma)]
    w = palabras(texto)
    p = n = 0
    for k, x in enumerate(w):
        signo = -1 if NEG & set(w[max(0, k - 3):k]) else 1
        if x in pos:
            p, n = (p + 1, n) if signo > 0 else (p, n + 1)
        elif x in neg:
            p, n = (p, n + 1) if signo > 0 else (p + 1, n)
    return (p - n) / (p + n + 1)

g = orig[(orig["idioma"] == "es") & (orig["grupo"] == "con negación")]
nuevo = g["texto"].map(tono_neg)
s_g = g["sentimiento_verdadero"]
print("antes:  ", round(g["dic_fin"].corr(s_g), 3))
print("después:", round(nuevo.corr(s_g), 3))
OUT [15]Diccionario con negación
antes:   0.21
después: 0.768

Una regla de tres palabras recupera buena parte de la señal perdida en las noticias con negación. Es la mejora clásica de los diccionarios; su límite es que hay que conocer de antemano las formas de negar, y en un texto real son muchas más que cuatro.

  1. Calcula cuántas noticias originales en inglés superan los 512 tokens de FinBERT y cuántas los 256 de MiniLM. Haz lo mismo con los 10-K de filings6_documentos.parquet. ¿Qué harías con los documentos que no caben?
Solución
IN [16]python
from transformers.utils import logging as hf_logging

hf_logging.set_verbosity_error()  # sin el aviso de «> 512 tokens»
en_txt = orig.loc[orig["idioma"] == "en", "texto"].tolist()
n = np.array([len(x) for x in tok_bert(en_txt)["input_ids"]])
print(f"noticias > 256: {(n > 256).mean():.1%}",
      f"> 512: {(n > 512).mean():.1%}")
docs = pd.read_parquet("filings6_documentos.parquet")
diez_k = docs.loc[docs["tipo"] == "10-K", "texto"].tolist()
n_k = np.array([len(x) for x in tok_bert(diez_k)["input_ids"]])
print(f"10-K > 512: {(n_k > 512).mean():.1%}",
      f"mediana {np.median(n_k):.0f}")
OUT [16]Cuántos textos no caben
noticias > 256: 0.0% > 512: 0.0%
10-K > 512: 100.0% mediana 898

Las noticias caben todas; los 10-K, no (el recuento incluye las etiquetas HTML, y estos son documentos sintéticos cortos: uno real tiene decenas de miles de palabras). Al tokenizar sin truncar, transformers avisa en su registro de que la secuencia supera la longitud máxima del modelo; la celda baja ese registro a errores para que la salida sea limpia. Con truncation=True el modelo solo leería el principio. Lo correcto es partir el documento en trozos que quepan (por frases o por secciones, lección 6.3.2), puntuar cada trozo y agregar (media, o media ponderada por la confianza), o puntuar solo la sección que interesa, como los factores de riesgo.

  1. Un proveedor ofrece un «sentimiento de noticias» calculado con un modelo afinado sobre noticias de 2015 a 2024 y un histórico desde 2010. ¿Qué preguntas le harías antes de usarlo en un backtest?
Solución

Con qué datos y en qué fecha se entrenó el modelo que puntuó cada noticia (si la puntuación de 2016 se calculó con un modelo entrenado hasta 2024, el histórico lleva información del futuro: lección 6.2.5); si el histórico se recalculó al cambiar de modelo o se conserva lo que se entregó en su día (point-in-time, lección 6.7.4); qué idiomas y fuentes cubre y cómo trata las copias sindicadas; con qué instante se fecha cada puntuación (publicación de la noticia o cálculo del proveedor); y qué licencia tienen el modelo y los textos.

Footnotes

  1. Sennrich, Haddow y Birch (2016), «Neural Machine Translation of Rare Words with Subword Units» (https://arxiv.org/abs/1508.07909); Hugging Face, «Tokenization algorithms»: BPE, BPE a nivel de byte (GPT-2), WordPiece (familia BERT) y Unigram/SentencePiece (https://huggingface.co/docs/transformers/tokenizer_summary), consultado el 2026-10-23. ↩

  2. Ficha de sentence-transformers/all-MiniLM-L6-v2 (https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2, consultada el 2026-10-23): licencia Apache-2.0, vectores de 384 dimensiones, inglés, y «by default, input text longer than 256 word pieces is truncated». ↩ ↩2

  3. Loughran y McDonald (2011), Journal of Finance 66(1), 35-65 (https://doi.org/10.1111/j.1540-6261.2010.01625.x): «almost three-fourths of the words identified as negative by the widely used Harvard Dictionary are words typically not considered negative in financial contexts». En el cuerpo del artículo la cifra es el 73,8 % de las apariciones de palabras negativas. ↩

  4. Universidad de Notre Dame, Software Repository for Accounting and Finance, «Loughran-McDonald Master Dictionary w/ Sentiment Word Lists» (https://sraf.nd.edu/loughranmcdonald-master-dictionary/, consultado el 2026-10-23): gratuito para investigación académica, licencia comercial previa consulta a los autores. ↩

  5. Reimers y Gurevych (2019), «Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks» (https://arxiv.org/abs/1908.10084). ↩

  6. Araci (2019), «FinBERT: Financial Sentiment Analysis with Pre-trained Language Models» (https://arxiv.org/abs/1908.10063); ficha de ProsusAI/finbert (https://huggingface.co/ProsusAI/finbert, consultada el 2026-10-23): licencia CC-BY-NC-SA-4.0, tres etiquetas y afinado con Financial PhraseBank. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. El tokenizador de FinBERT (WordPiece, minúsculas y sin acentos) convierte «caída» en cai, ##da y «demanda» en demand, ##a. ¿Qué consecuencia práctica tiene?

    • ANinguna: el modelo entiende igual el español
    • BEl texto en español se trocea en muchas más piezas, algunas coinciden con palabras inglesas de otro significado y el modelo, entrenado con frases inglesas, apenas lo interpreta
    • CEl texto ocupa menos tokens que en inglés
    • DLos acentos se conservan en un token aparte
    Ver respuesta

    Respuesta B. El texto en español se trocea en muchas más piezas, algunas coinciden con palabras inglesas de otro significado y el modelo, entrenado con frases inglesas, apenas lo interpreta

    El vocabulario de BERT se construyó con texto inglés: las palabras españolas se parten en fragmentos y la tilde desaparece. En la lección, FinBERT sobre noticias en español no se correlaciona con el sentimiento verdadero (−0,11), mientras que en inglés llega a 0,88.

  2. Un diccionario general marca como negativas «coste», «deuda», «impuesto» o «vicepresidente». ¿Qué problema documentaron Loughran y McDonald con ese tipo de listas en los 10-K?

    • AQue tienen pocas palabras positivas
    • BQue no distinguen mayúsculas
    • CQue casi tres cuartas partes de las apariciones de palabras «negativas» del diccionario de Harvard en los 10-K corresponden a palabras que en finanzas no son negativas
    • DQue no funcionan con textos largos
    Ver respuesta

    Respuesta C. Que casi tres cuartas partes de las apariciones de palabras «negativas» del diccionario de Harvard en los 10-K corresponden a palabras que en finanzas no son negativas

    Un informe financiero habla de costes, deudas e impuestos sin que sea una mala noticia. Por eso construyeron listas propias del dominio. En el corpus del curso, la lista general se correlaciona 0,35 con el sentimiento verdadero y la financiera, 0,71.

  3. La frase «la compañía descarta pérdidas» contiene una palabra negativa del léxico. ¿Qué le pasa a un diccionario y qué a FinBERT?

    • AEl diccionario cuenta «pérdidas» como negativa y se equivoca de signo; un modelo que lee la frase entera puede captar la negación
    • BLos dos aciertan
    • CLos dos fallan siempre
    • DEl diccionario acierta porque «descarta» es positiva
    Ver respuesta

    Respuesta A. El diccionario cuenta «pérdidas» como negativa y se equivoca de signo; un modelo que lee la frase entera puede captar la negación

    Contar palabras ignora el orden y el contexto. En las noticias con negación, la correlación del diccionario con el sentimiento verdadero cae a 0,21; FinBERT, que lee la frase como secuencia, conserva casi toda la señal en las noticias inglesas con negación (0,87).

  4. Se entrena una regresión ridge sobre embeddings de MiniLM con el sentimiento de 2019-2022 y se evalúa en 2023-2025. ¿Por qué esa separación y no una aleatoria?

    • APorque es más rápida
    • BPorque así hay más datos de entrenamiento
    • CPorque las plantillas cambian cada año
    • DPorque en un backtest el modelo solo puede haberse entrenado con textos anteriores; una partición aleatoria mezcla el futuro con el pasado y da una estimación optimista
    Ver respuesta

    Respuesta D. Porque en un backtest el modelo solo puede haberse entrenado con textos anteriores; una partición aleatoria mezcla el futuro con el pasado y da una estimación optimista

    Es la regla del nivel 5 (lecciones 5.4.1 y 5.4.3): validación con el tiempo. Además, en real las etiquetas de entrenamiento serían un conjunto anotado (como Financial PhraseBank), no el sentimiento verdadero, que no existe.

  5. ¿Qué condiciona la licencia de los pesos de ProsusAI/finbert (CC-BY-NC-SA-4.0)?

    • ANada: todo lo que está en Hugging Face es de uso libre
    • BSolo impide modificarlos
    • CPermite usarlos citando la autoría, sin fines comerciales y compartiendo las obras derivadas con la misma licencia: para un uso comercial hay que buscar otro modelo o un permiso
    • DObliga a publicar el código de la estrategia
    Ver respuesta

    Respuesta C. Permite usarlos citando la autoría, sin fines comerciales y compartiendo las obras derivadas con la misma licencia: para un uso comercial hay que buscar otro modelo o un permiso

    NC (no comercial) y SA (compartir igual) son las dos cláusulas que importan en un fondo o una empresa. El curso lo usa con fines educativos; el README del entorno ia nombra una alternativa con licencia Apache-2.0.

Ideas clave

  • Los modelos trabajan con tokens de subpalabra. El mismo texto cuesta más tokens en español que en inglés con vocabularios entrenados en inglés, y WordPiece sin acentos convierte «demanda» en demand + ##a.
  • Un diccionario es transparente, rápido y gratis, pero no entiende contexto: en el corpus del curso la lista financiera se correlaciona 0,71 con el sentimiento verdadero, la general 0,35 y, con negaciones, la financiera cae a 0,21.
  • El diccionario de Loughran y McDonald nació porque las listas generales confunden el vocabulario contable con mala noticia; es gratuito para investigación académica y su uso comercial necesita licencia.
  • Los embeddings convierten una frase en un vector comparable por similitud coseno; con un modelo lineal encima y validación temporal, aprenden el tono, pero MiniLM es un modelo inglés y en español rinde menos.
  • FinBERT, sin ver ninguna noticia del corpus, llega a 0,88 en las inglesas (el diccionario, 0,70) y no sirve en español. Su licencia (CC-BY-NC-SA-4.0) impide el uso comercial sin permiso.
  • Los textos del curso salen de plantillas: las cifras de correlación son más altas que con noticias reales. Lo que se traslada al mundo real es el orden de los métodos y el tipo de error de cada uno.

Recursos