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.
- Módulo
- 6.1 · NLP financiero clásico y moderno
- Duración
- 70 min
Lecciones del nivel 6
6.1 · NLP financiero clásico y moderno
6.2 · LLMs para investigación y trading
6.3 · RAG financiero
6.4 · Agentes de IA para trading
- 6.4.1 Agentes con herramientas: tool calling, ReAct y planificación
- 6.4.2 Marcos de agentes y MCP
- 6.4.3 Arquitecturas multiagente tipo TradingAgents
- 6.4.4 Evidencia y límites de los agentes
- 6.4.5 Guardarraíles para agentes: el agente propone, el motor decide
- 6.4.6 Agentes de investigación: hipótesis, alpha mining y revisión
6.5 · IA como copiloto del quant
6.6 · Big data para trading
- 6.6.1 Cuándo los datos son «big»: ticks, libros, opciones y universos
- 6.6.2 Parquet, Arrow, Polars en streaming y DuckDB fuera de memoria
- 6.6.3 Dask, Spark y Ray: de una máquina a un clúster
- 6.6.4 Bases de series temporales: TimescaleDB, QuestDB, ClickHouse, ArcticDB y kdb+
- 6.6.5 Almacenamiento en la nube, lakehouse (Delta, Iceberg) y GPU (RAPIDS, cuDF)
6.7 · Datos alternativos
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.
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.
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"])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.546Las 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:
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))['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í:
| Algoritmo | Cómo construye el vocabulario | Modelos de esta lección |
|---|---|---|
| WordPiece | Une 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 byte | Parte de los 256 bytes y une los pares más frecuentes; ningún texto queda fuera del vocabulario | Qwen2.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:
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()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.9en 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:
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) wordpiece qwen cl100k
idioma
en 1.47 1.52 1.44
es 2.03 1.74 1.75El 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 () y las negativas () de un texto:
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:
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))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 vicepresidenteLa 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:
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%}")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?
_, 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)demanda 426
caída 52
Name: count, dtype: int64Casi 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:
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) 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.784En 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:
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))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:
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)(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.00Las 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:
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) prueba corr_ridge corr_dic
idioma
en 524.0 0.919 0.702
es 491.0 0.826 0.729Con 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.
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 , entre −1 y 1, comparable con el tono del diccionario:
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) 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.001La 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:
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) 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.473En 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:
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) 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.790En 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étodo | Coste | Idiomas | Transparencia | Riesgo principal |
|---|---|---|---|---|
| Diccionario financiero | Casi nulo | El de la lista | Total | Negaciones, contexto, palabras nuevas |
| Diccionario general | Casi nulo | El de la lista | Total | Vocabulario contable leído como negativo |
| Embeddings + modelo lineal | Bajo; necesita etiquetas | Los del modelo de embeddings | Baja | Etiquetas caras o ruidosas; deriva del lenguaje |
| FinBERT | Medio (CPU o GPU) | Inglés | Baja | Licencia no comercial; 512 tokens; dominio de entrenamiento |
| LLM generativo (6.2.3) | Alto | Muchos | Baja | Coste, 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
- 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
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))antes: 0.21
después: 0.768Una 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.
- 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
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}")noticias > 256: 0.0% > 512: 0.0%
10-K > 512: 100.0% mediana 898Las 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.
- 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
-
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. ↩
-
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 -
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. ↩
-
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. ↩
-
Reimers y Gurevych (2019), «Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks» (https://arxiv.org/abs/1908.10084). ↩
-
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
El tokenizador de FinBERT (WordPiece, minúsculas y sin acentos) convierte «caída» en
cai,##day «demanda» endemand,##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.
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.
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).
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.
¿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
ianombra 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
Documentación
- Loughran y McDonald: Master Dictionary y listas de sentimiento (Universidad de Notre Dame)
- Loughran y McDonald (2011): When Is a Liability Not a Liability?, Journal of Finance 66(1)
- Araci (2019): FinBERT, Financial Sentiment Analysis with Pre-trained Language Models
- Ficha de ProsusAI/finbert en Hugging Face (licencia CC-BY-NC-SA-4.0)
- Hugging Face: algoritmos de tokenización (BPE, WordPiece, Unigram)
- Reimers y Gurevych (2019): Sentence-BERT