from collections import Counter
from pathlib import Path
import logging
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import spacy
from banrep import tabulados
from banrep.io import Textos, crear_carpeta, leer_palabras, leer_jsonl, guardar_jsonl
from banrep.linguistica import Frases
from banrep.transforma import Bags, NgramFrases, Topicos
logfmt = '%(asctime)s : %(levelname)s : %(message)s'
logdatefmt = '%Y-%m-%d %H:%M:%S'
logging.basicConfig(format=logfmt, datefmt=logdatefmt, level=logging.INFO)
logging.getLogger("gensim").setLevel(logging.CRITICAL)
nlp = spacy.load('es_core_news_md')
A lo largo de un análisis de textos se suele querer identificar palabras o expresiones presentes en ellos, bien sea para excluirlas del análisis (ej. stopwords), o porque su conteo es útil para generar indicadores (ej. sentimiento), o porque el análisis se quiere limitar a estudiar aquellos textos que tienen cierto tipo de contenido (ej. expresiones relacionadas con política económica).
En cada proyecto se va a querer tener la flexibilidad para personalizar las palabras y expresiones que se quiere identificar. Para esto se recomienda usar hojas de excel, donde cada hoja puede contener categorías de palabras o expresiones. No es necesario usar un mismo archivo excel, pero facilita tener dichas listas de palabras centralizadas para abrir un solo archivo.
leer_palabras
¶Para este ejemplo se quiere identificar (a) palabras stopwords, (b) palabras que identifican categorías de sentimiento, y (c) expresiones relacionadas con incertidumbre en política económica.
Nota: Dado que estamos iterando un mismo archivo, este ejemplo asume que las columnas relevantes tienen el mismo nombre en cada hoja.
leer_palabras?
Signature: leer_palabras(archivo, hoja, c_grupo, c_palabras) Docstring: Extrae grupos de palabras de un archivo Excel. Parameters ---------- archivo : str | Path Ruta del archivo Excel en disco. hoja : str Hoja de cálculo de archivo Excel. c_grupo : str Nombre de columna para determinar grupos. c_palabras : str Nombre de columna que contiene palabras de cada grupo. Returns ------- dict (str:set) Grupos de palabras en cada grupo. File: ~/munozbravo/banrep/banrep/io.py Type: function
dirdata = Path("/Volumes/tombitoT5/Dropbox/Dropbox/datasets/")
listas = dict()
hojas = ['es_stops', 'es_emocion', 'es_epu']
wlpath = dirdata.joinpath("wordlists", "banrep.xlsx")
for hoja in hojas:
listas[hoja] = leer_palabras(wlpath, hoja, c_grupo='type', c_palabras='word')
Generalmente se quiere ignorar palabras comunes a todos los textos, llamadas stopwords, por no aportar al entendimiento de los diferentes textos.
stopwords = listas['es_stops'].get("stopword")
print(f'{len(stopwords)} palabras stopwords.')
821 palabras stopwords.
Muchas veces se quiere contabilizar cuantas palabras de cada documento hacen parte de listas de palabras predefinidas. Por ejemplo, puedo tener listas de palabras "positivas" y "negativas", y querer contar cuantas palabras de los textos que voy a analizar hacen parte de estas listas. Esto sirve, por ejemplo, para crear indicadores de sentimiento basados en el conteo de palabras que pertenecen a emociones "contrarias".
wordlists = listas['es_emocion']
for tipo in wordlists:
print(f'{len(wordlists.get(tipo))} palabras en grupo {tipo}')
1899 palabras en grupo deterioro 359 palabras en grupo mejora
Al igual que con listas de palabras, a veces se quiere identificar frases de los textos en las que hay presencia de ciertas expresiones. Por ejemplo, un índice reconocido es el Economic Uncertainty Index, cuyos resultados se basan en el conteo de noticias en las que se encuentren diferentes expresiones relacionadas con incertidumbre en política económica. Por ejemplo, puedo querer identificar expresiones como Banco de la República, déficit fiscal, política monetaria, inflación de alimentos, incertidumbre tributaria, etc.
Identificar estas expresiones sirve también para proyectos en los que se quiere crear un training set para entrenar un modelo de clasificación basado en aprendizaje de máquina, dado que el primer paso para dichos modelos es anotar una serie de frases que sean relevantes para lo que se quiere entrenar.
express = listas['es_epu']
for tipo in express:
print(f'{len(express.get(tipo))} expresiones en grupo {tipo}')
1 expresiones en grupo colombia 16 expresiones en grupo economic 80 expresiones en grupo policy 2 expresiones en grupo uncertainty
Se asume una carpeta en disco en la que hay archivos de texto.
Si los textos que quiere utilizar están en archivos binarios como .pdf, .docx, etc., debe primero extraer el texto.
Textos
¶Se usa para iterar los archivos planos en disco y extraer su texto.
Textos?
Init signature: Textos(carpeta, recursivo=False, aleatorio=False, exts=None, chars=0) Docstring: Texto y metadata de párrafos en textos de archivos planos. Parameters ---------- carpeta : str | Path Directorio a iterar. recursivo: bool Iterar recursivamente. aleatorio : bool Iterar aleatoriamente. exts: Iterable[str] Solo considerar estas extensiones de archivo. chars : int Mínimo número de caracteres en una línea de texto. Yields ------ tuple (str, dict) Texto y metadata de cada párrafo. Init docstring: Requiere: carpeta. Opcional: recursivo, aleatorio, exts, chars. File: ~/munozbravo/banrep/banrep/io.py Type: type Subclasses:
Textos
: Registros
y Datos
¶Los textos a analizar pueden también estar en archivos Excel o csv, o en un DataFrame ya cargado en memoria. Para esos casos usar clases Registros
y Datos
respectivamente.
Nota: Las clases Textos
, Registros
y Datos
no almacenan el texto extraído sino que lo extraen, un documento a la vez, cada vez que se requiera.
from banrep.io import Registros, Datos
Registros?
Init signature: Registros( carpeta, textcol, metacols, recursivo=False, exts=None, chars=0, hoja=None, ) Docstring: Texto y metadata de registros en archivos csv o Excel. Parameters ---------- carpeta : str | Path Ruta de carpeta que se quiere iterar. textcol : str Nombre de columna que contiene texto en sus filas. metacols : list Nombre de columnas a incluir como metadata. recursivo: bool Iterar recursivamente. exts: Iterable Solo considerar estas extensiones de archivo. chars : int Mínimo número de caracteres en una línea de texto. hoja : str Nombre de hoja en archivo excel. Yields ------ tuple (str, dict) Texto y metadata de cada registro. Init docstring: Requiere: carpeta, textcol, metacols. Opcional: recursivo, exts, chars, hoja. File: ~/munozbravo/banrep/banrep/io.py Type: type Subclasses:
Datos?
Init signature: Datos(df, textcol, metacols, chars=0) Docstring: Texto y metadata de registros en DataFrame. Parameters ---------- df : pd.DataFrame DataFrame que contiene los textos. textcol : str Nombre de columna que contiene texto en sus filas. metacols : list Nombre de columnas a incluir como metadata. chars : int Mínimo número de caracteres en una línea de texto. Yields ------ tuple (str, dict) Texto y metadata de cada registro. Init docstring: Requiere: df, textcol, metacols. Opcional: chars. File: ~/munozbravo/banrep/banrep/io.py Type: type Subclasses:
En este caso tengo archivos planos organizados en carpetas según la fuente o tipo de documento.
dirtextos = dirdata.joinpath("banrep/corpus/es")
datos = Textos(dirtextos, recursivo=True, aleatorio=False, exts=['.txt'], chars=59)
Se obtendrá información detallada de cada palabra gracias a spaCy, lo que permite filtrar adicionalmente por criterios como categoría gramatical (verbos, sustantivos, etc), si es algún tipo de Named Entity (Juan, Colombia, Banco de la República), si contiene caracteres que no hacen parte del alfabeto (números, monedas, etc), o si no supera una longitud mínima.
En este caso elimino de cualquier análisis posterior todas las stopwords del archivo excel, las categorías gramaticales que identifican números, puntuación y símbolos, y aquellas "palabras" o tokens que contengan caracteres que no hacen parte del alfabeto. También elimino palabras que no tengan más de dos caracteres. También podría eliminar Named Entities, en este caso no elimino ninguna.
pos = ['NUM', 'PUNCT', 'SYM']
dep = []
ents = []
filtros = dict(is_alpha=True, lower_=stopwords, pos_=pos, dep_=dep, ent_type_=ents, chars=2)
Frases
¶Se usa para procesar lingüísticamente los textos y extraer detalles de frases.
Nota: La clase Frases
no almacenan los documentos procesados sino que los procesa, un documento a la vez, cada vez que se requiera.
Frases?
Init signature: Frases(lang, datos, tk=0, filtros=None, grupos=None, entes=None) Docstring: Anotaciones lingüísticas de cada frase en corpus. Parameters ---------- lang : spacy.language.Language Modelo de lenguaje spaCy. datos : Iterable (str, dict) Información de cada documento (texto, metadata). tk : int, optional Filtro para número mínimo de tokens válidos en frases. filtros : dict, optional Filtros a evaluar en cada token. (is_alpha, lower_, pos_, dep_, ent_type_, chars) grupos : dict (str: set), optional Grupos de listas de palabras a identificar. entes : dict (str: set), optional Grupos de expresiones a considerar como Entities. Yields ------ dict (text: str, tokens: list[dict], meta: dict) Anotaciones lingüísticas de cada frase. Init docstring: Requiere: lang, datos. Opcional tk, filtros, grupos, entes. File: ~/munozbravo/banrep/banrep/linguistica.py Type: type Subclasses:
frases = Frases(nlp, datos, tk=4, filtros=filtros, grupos=wordlists, entes=express)
crear_carpeta
, guardar_jsonl
, leer_jsonl
¶Una vez se procesa lingüísticamente un conjunto de documentos se suele querer guardar el detalle de cada frase procesada. Esto incluye su texto, su metadata, y los tokens que la componen (con sus anotaciones lingüísticas).
Almacenar frases procesadas sirve para no mantenerlas en memoria, pero también para usarlas en proyectos de machine learning en los que el primer paso es contar con un training set.
crear_carpeta?
Signature: crear_carpeta(nombre) Docstring: Crea nueva carpeta en disco si no existe. Si no es ruta absoluta será creada relativo a carpeta de trabajo. Parameters ---------- nombre : str | Path Nombre de carpeta a crear. Returns ------- Path Ruta absoluta de carpeta. File: ~/munozbravo/banrep/banrep/io.py Type: function
guardar_jsonl?
Signature: guardar_jsonl(archivo, objs) Docstring: Guarda objetos json en archivo. Parameters ---------- archivo : str | Path Ruta del archivo en el cual se quiere guardar objetos json. objs : Iterable[dict] Contenido de cada objeto json a guardar. File: ~/munozbravo/banrep/banrep/io.py Type: function
leer_jsonl?
Signature: leer_jsonl(archivo) Docstring: Lee objetos json de archivo. Parameters ---------- archivo : str | Path Ruta del archivo del cual se quiere leer objetos json. Yields ------ dict Contenido de cada objeto json. File: ~/munozbravo/banrep/banrep/io.py Type: function
carpeta = crear_carpeta("output")
graficas = crear_carpeta(carpeta.joinpath('graficas'))
fp = carpeta.joinpath("frases.jsonl")
guardar_jsonl(fp, frases)
2020-04-10 14:00:48 : INFO : 10000 párrafos leídos de 32 archivos en carpeta 'es'. 2020-04-10 14:02:41 : INFO : 20000 párrafos leídos de 78 archivos en carpeta 'es'. 2020-04-10 14:03:01 : INFO : 50000 frases procesadas lingüísticamente. 2020-04-10 14:04:35 : INFO : 30000 párrafos leídos de 122 archivos en carpeta 'es'. 2020-04-10 14:06:16 : INFO : 40000 párrafos leídos de 285 archivos en carpeta 'es'. 2020-04-10 14:06:36 : INFO : 100000 frases procesadas lingüísticamente. 2020-04-10 14:07:17 : INFO : Guardados 110799 registros en frases.jsonl
Como en este punto ya se ha iterado sobre los textos y frases, podemos ver cuantos textos y frases hemos iterado.
print(datos)
44844 párrafos leídos de 300 archivos en carpeta 'es'.
print(frases)
110799 frases procesadas lingüísticamente.
frases = [frase for frase in leer_jsonl(fp)]
NgramFrases
¶Una vez se tienen las palabras pertenecientes a cada frase, usualmente se identifican n-gramas. Es decir, palabras que co-ocurren frecuentemente y que por eso podrían considerarse como una sola palabra. Para esto primero se crea un modelo de n-gramas, y luego se le aplica este modelo a cualquier frase para encontrar n-gramas.
La clase NgramFrases
permite generar dicho modelo e iterar sobre las frases para identificar n-gramas.
NgramFrases?
Init signature: NgramFrases(frases, attr='lower_', th=10.0) Docstring: Info de frases incluyendo n-gramas. Parameters ---------- frases : Iterable[dict (text: str, tokens: list[dict], meta: dict)] Anotaciones lingüísticas de cada frase. attr : str Atributo a usar como texto de cada token (text | lower_) th : float Score Threshold para formar n-gramas. Ver https://radimrehurek.com/gensim/models/phrases.html Yields ------ dict (text: str, tokens: list[str], meta: dict) Info de cada frase incluyendo n-gramas. Init docstring: Requiere: frases. Opcional attr, th. File: ~/munozbravo/banrep/banrep/transforma.py Type: type Subclasses:
ng_frases = NgramFrases(frases)
2020-04-10 14:12:57 : INFO : Modelo de bi-gramas creado. 2020-04-10 14:13:11 : INFO : Modelo de tri-gramas creado.
Es posible que se prefiera trabajar con texto en una estructura tradicional de análisis de datos. Para esto se pueden generar DataFrames de los detalles de frases y palabras en un corpus.
Muchas veces el acceso a las palabras desagregadas es deseado, por ejemplo para contar las palabras más frecuentes en un documento o en el corpus, o para limitar el conteo a palabras que hacen parte de una categoría gramatical específica (ej. sustantivos, verbos). También es relevante para el cálculo de indicadores de sentimiento que se basan en conteos de palabras con diferentes tipos de "emociones".
tabulados.df_tokens
¶tabulados.df_tokens?
Signature: tabulados.df_tokens(frases, ng=False) Docstring: DataFrame de tokens en frases. Parameters ---------- frases : Iterable[dict (text: str, tokens: list[dict | str], meta: dict)] Anotaciones lingüísticas o Info de cada frase. ng : Bool Si frases contienen n-gramas. Returns ------- pd.DataFrame Anotaciones lingüísticas o Info de cada token. File: ~/munozbravo/banrep/banrep/tabulados.py Type: function
palabras = tabulados.df_tokens(frases)
palabras.tail()
text | i | ent_type_ | ent_iob_ | lower_ | is_alpha | is_oov | pos_ | dep_ | chars | ok_token | deterioro | mejora | id_parag | id_file | archivo | fuente | id_sent | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1348494 | control | 357 | O | control | True | False | NOUN | obj | 7 | True | False | False | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 | |
1348495 | niveles | 360 | O | niveles | True | False | NOUN | obj | 7 | True | False | False | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 | |
1348496 | posición | 362 | O | posición | True | False | NOUN | nmod | 8 | True | False | False | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 | |
1348497 | vigencia | 366 | O | vigencia | True | False | NOUN | obl | 8 | True | False | False | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 | |
1348498 | ATL | 368 | O | atl | True | False | PROPN | nmod | 3 | True | False | False | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
tokens = tabulados.df_tokens(ng_frases, ng=True)
tokens.tail()
token | id_parag | id_file | archivo | fuente | id_sent | |
---|---|---|---|---|---|---|
974687 | control | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
974688 | niveles | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
974689 | posición | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
974690 | vigencia | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
974691 | atl | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 |
tabulados.df_frases(frases).tail()
id_parag | id_file | archivo | fuente | id_sent | text | |
---|---|---|---|---|---|---|
110794 | 0044841 | 0000300 | 2019-05.txt | ref | 0110795 | El decreto establece dos prohibiciones sobre l... |
110795 | 0044841 | 0000300 | 2019-05.txt | ref | 0110796 | Segundo, no podrán adquirir acciones de los EC... |
110796 | 0044842 | 0000300 | 2019-05.txt | ref | 0110797 | Resolución 2 del 22 de febrero de 2019 y Circu... |
110797 | 0044843 | 0000300 | 2019-05.txt | ref | 0110798 | En febrero de 2019 el Banco de la República mo... |
110798 | 0044844 | 0000300 | 2019-05.txt | ref | 0110799 | Los principales ajustes fueron: 1) se modificó... |
En muchos modelos que realizan transformaciones en texto se suele representar cada documento por el conjunto de palabras que lo componen, sin importar el orden en que aparecen dentro del texto (es decir, sin considerar información semántica de cada palabra en su contexto). Los modelos de tópicos basados en LDA, por ejemplo, hacen uso de esta representación.
Bags
¶Se usa para representar documentos como Bag of Words.
Bags?
Init signature: Bags(frases, idm, attr=None, vocab=None) Docstring: Documentos como Bag Of Words. Parameters ---------- frases : Iterable[dict (text: str, tokens: list, meta: dict)] Anotaciones lingüísticas o Info de cada frase. idm : str Llave de Metadata para agrupar. attr : str | None Atributo a usar de token (text | lower_). None si ngramas. vocab : gensim.corpora.Dictionary, optional Vocabulario a considerar. Yields ------ dict (idm: str, tokens: list[str], sparsed: list[tuple(int, int)]) Bags of Words de cada documento. Init docstring: Requiere frases, idm. Opcional: attr, vocab. File: ~/munozbravo/banrep/banrep/transforma.py Type: type Subclasses:
bags = Bags(ng_frases, 'id_parag')
2020-04-10 14:30:43 : INFO : Diccionario con 34677 términos creado...
Bags
permite agrupar por diferentes llaves de metadata porque según la aplicación se puede querer agrupar a distintos niveles de agregación. Por ejemplo un modelo de tópicos se puede querer entrenar usando párrafos pero aplicarlo a documentos enteros. O puede quererse agrupar por fuente de información, para fines descriptivos...
Nota: Las llaves de metadata disponibles dependen de la clase usada para extraer texto. Textos
viene con id_parag, id_file, fuente* y archivo, mientras que Registros
y Datos
dependen de las columnas de DataFrames usadas como metadata. Al usar Frases
se agrega otra llave id_sent.*
bags1 = Bags(ng_frases, 'fuente')
2020-04-10 14:32:30 : INFO : Diccionario con 34677 términos creado...
fuentes, toks, sparsed = [], [], []
for b in bags1:
fuentes.append(b.get("idm"))
toks.append(b.get("tokens"))
sparsed.append(b.get("sparsed"))
print(fuentes)
['congreso', 'inflacion', 'minutas', 'ref']
banrep
¶La siguiente celda escribe código usual pero que sin embargo no hace parte de la librería.
wc_params =dict(font_path=str(dirdata.joinpath("fonts/cabinsketch/CabinSketch-Bold.otf")),
width=800, height=400, prefer_horizontal=0.6, max_words=30,
random_state=2, background_color=None, mode="RGBA", colormap='viridis',)
figwc = plt.figure(figsize=(16, 12))
figwc.subplots_adjust(hspace=0.05, wspace=0.1)
cols = 2
rows = int(np.ceil(bags1.n / cols))
for i in range(bags1.n):
freq = dict(Counter(toks[i]))
wc = WordCloud(**wc_params).generate_from_frequencies(freq)
plt.subplot(rows, cols, i+1).set_title(f"Fuente {fuentes[i]}")
plt.plot()
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.tight_layout()
plt.savefig(graficas.joinpath('wc_fuentes.png'))
Los modelos de tópicos se usan para encontrar "temáticas" subyacentes en los textos.
El parámetro básico a especificar en un modelo es el número de tópicos que se quiere considerar en el resultado.
Esta librería usa Gensim para la implementación del cálculo de los modelos. En su documentación encontrará todo lo necesario para correr este tipo de modelos y muchas técnicas adicionales no usadas en esta librería. banrep simplemente ofrece una forma de correr varios modelos LDA y calificarlos según Coherence Score, todo basado en Gensim.
Topicos
¶Se usa para crear modelos LDA para diferentes números de tópicos.
Los modelos de tópicos suelen ser evaluados usando una medida llamada Coherence Score. Esta medida sugiere qué tan "interpretables" son los modelos. Un mayor score es un modelo más "interpretable", y por lo tanto mejor.
Topicos
devuelve los modelos generados al iterar, donde cada elemento devuelto es un diccionario que contiene el modelo, el score y el número de tópicos de ese modelo.
Topicos?
Init signature: Topicos(bags, kas, params) Docstring: Modelos de tópicos basados en LDA. Parameters ---------- bags : banrep.transforma.Bags Bags of Words de documentos. kas : Iterable[int] Diferentes k tópicos para los cuales crear modelo. params : dict Parámetros requeridos en modelos LDA. Ver https://radimrehurek.com/gensim/models/ldamodel.html Yields ------ dict (k:int, modelo: gensim.models.ldamodel.LdaModel, score: float) Modelo de Tópicos para cada k. Init docstring: Requiere bags, kas, params. File: ~/munozbravo/banrep/banrep/transforma.py Type: type Subclasses:
# usando el bow generado anteriormente...
kas = (5, 10, 20)
params = dict(passes=10, alpha='auto', eta='auto', random_state=100)
modelos = Topicos(bags, kas, params)
ldas = sorted((modelo for modelo in modelos), key=lambda x: x.get("score"), reverse=True)
2020-04-10 14:38:22 : INFO : Modelo de 5 tópicos creado y evaluado. 2020-04-10 14:40:58 : INFO : Modelo de 10 tópicos creado y evaluado. 2020-04-10 14:43:49 : INFO : Modelo de 20 tópicos creado y evaluado.
print(modelos)
Modelos LDA para k en (5, 10, 20): Mejor k=10 con Coherence Score=0.489
modelo = ldas[0].get('modelo')
print(modelo)
LdaModel(num_terms=34677, num_topics=10, decay=0.5, chunksize=2000)
Una vez se tiene un modelo de tópicos generalmente se quiere investigar los resultados, y potencialmente aplicarlo a otros conjuntos de documentos.
Un tópico no es más que una serie de palabras con cierta probabilidad de ocurrir. Las palabras con mayor probabilidad son las que permiten "caracterizar" un tópico.
tabulados.df_probables
¶Genera un DataFrame con las palabras más probables de cada tópico en un modelo.
tabulados.df_probables?
Signature: tabulados.df_probables(modelo, n=15) Docstring: Distribución de probabilidad de tokens en tópicos de modelo. Parameters ---------- modelo : gensim.models.ldamodel.LdaModel n : int optional Cuantos tokens incluir. Returns ------- pd.DataFrame Tokens probables de cada tópico y sus probabilidades. File: ~/munozbravo/banrep/banrep/tabulados.py Type: function
probables = tabulados.df_probables(modelo, n=20)
probables.head()
token | probabilidad | topico | |
---|---|---|---|
0 | crecimiento | 0.066416 | 0 |
1 | dinámica | 0.018276 | 0 |
2 | menor | 0.018137 | 0 |
3 | comportamiento | 0.013482 | 0 |
4 | pib | 0.012801 | 0 |
banrep
¶La siguiente celda escribe código usual pero que sin embargo no hace parte de la librería.
wcfig = plt.figure(figsize=(16, 12))
wcfig.subplots_adjust(hspace=0.05, wspace=0.1)
k = modelo.num_topics
r = int(np.ceil(k / 2))
for t in range(k):
freq = dict(modelo.show_topic(t, topn=20))
wc = WordCloud(**wc_params).generate_from_frequencies(freq)
plt.subplot(r, 2, t+1).set_title(f"Tópico {t}")
plt.plot()
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.tight_layout()
plt.savefig(graficas.joinpath('wc_topicos.png'))
Usualmente se quiere ver la probabilidad de cada tópico asociada a cada documento. Se puede pensar un documento como generado de una distribución de probabilidad de tópicos.
Para cada documento (fila) se quiere la probabilidad de que hable de cada tópico (columna).
# usando el bow generado anteriormente...
idms, toks, sparsed = [], [], []
for b in bags:
idms.append(b.get("idm"))
toks.append(b.get("tokens"))
sparsed.append(b.get("sparsed"))
# Document-Feature Matrix
dfm = pd.DataFrame(data=(dict(d) for d in modelo[sparsed]), index=idms)
dfm.tail()
0 | 2 | 3 | 4 | 5 | 6 | 7 | 1 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
0044840 | 0.100960 | 0.050212 | 0.016349 | 0.267075 | 0.122468 | 0.201124 | 0.065089 | 0.166842 | NaN | NaN |
0044841 | 0.070450 | 0.024724 | 0.017700 | 0.349660 | 0.078140 | 0.197781 | 0.074218 | 0.040640 | 0.110604 | 0.036084 |
0044842 | 0.086394 | 0.062801 | 0.040563 | 0.292313 | 0.204574 | 0.113193 | 0.013632 | 0.014281 | 0.089706 | 0.082542 |
0044843 | 0.056045 | 0.038308 | 0.025360 | 0.219083 | 0.068989 | 0.322479 | NaN | NaN | 0.200583 | 0.051708 |
0044844 | 0.065804 | 0.029356 | 0.030024 | 0.303337 | 0.077620 | 0.090315 | 0.019387 | 0.020352 | 0.340039 | 0.023766 |
Basado en esta distribución, se quiere saber la "prevalencia" de cada tópico: en cuantos documentos del corpus es dominante (el de mayor probabilidad).
dom = dfm.idxmax(axis=1).value_counts(normalize=True)
dom.index.name = "topico"
dom = dom.to_frame(name="domina").reset_index()
dom.head()
topico | domina | |
---|---|---|
0 | 5 | 0.264962 |
1 | 0 | 0.256422 |
2 | 4 | 0.138676 |
3 | 2 | 0.128709 |
4 | 3 | 0.109727 |
fig = px.bar(dom, 'topico', 'domina', color='domina',
title="Proporción de documentos con tópico 'n' como más probable")
fig.update_layout(title_x=0.5)
fig.write_html(file=str(graficas.joinpath('prevalencia.html')), auto_open=False)
fig.show()
A veces se puede necesitar saber qué tan diferentes son entre ellos los tópicos de un modelo. El resultado puede ser usado para graficar la distancia, posiblemente usando un heatmap donde cada celda representaría la distancia entre dos tópicos.
diff, annos = modelo.diff(modelo, distance="hellinger", annotation=True)
labels = list(range(modelo.num_topics))
fig = go.Figure(data=go.Heatmap(z=diff, x=labels, y=labels, xgap=1, ygap=1),)
fig.update_layout(title_text="Distancia entre tópicos", title_x=0.5)
fig.write_html(file=str(graficas.joinpath('distancia.html')), auto_open=False)
fig.show()
En análisis de tópicos se suele usar PyLDAvis para visualizar resultados de un modelo.
La visualización es muy útil para entender la diferencia entre tópicos (distancia entre los círculos de la gráfica en costado izquierdo), prevalencia de cada tópico (tamaño de cada círculo), y el contenido de cada tópico (palabras más probables desplegadas en costado derecho para cada círculo).
El único "pero" es que suele generar advertencias de uso "obsoleto" (DeprecationWarning).
Nota: La numeración de tópicos en PyLDAVIS
comienza en 1, de tal forma que la información de tópico 1 en la gráfica corresponde al tópico 0 en el modelo.
import warnings
import pyLDAvis
import pyLDAvis.gensim
# Gráfica LDAvis de tópicos y sus palabras
with warnings.catch_warnings():
warnings.simplefilter('ignore')
vis = pyLDAvis.gensim.prepare(modelo, sparsed, bags.vocab, sort_topics=False)
pyLDAvis.display(vis)
Una vez ha entrenado un modelo, puede incluso aplicárselo a otro corpus. Sólo hay que aplicarle el mismo tipo de preprocesamiento y filtros a los datos.
Por ejemplo, en este caso el modelo se entrenó por párrafo, pero puedo estar interesado en la distribución de probabilidad de tópicos para cada archivo de texto.
En un escenario ideal, su fuente de datos es tan amplia que no necesita entrenar a nivel de párrafos. Se necesitan bastantes documentos para entrenar un modelo de tópicos.
bags2 = Bags(ng_frases, 'id_file')
2020-04-10 14:51:02 : INFO : Diccionario con 34677 términos creado...
idms, toks, sparsed = [], [], []
for b in bags2:
idms.append(b.get("idm"))
toks.append(b.get("tokens"))
sparsed.append(b.get("sparsed"))
# Document-Feature Matrix
dfm = pd.DataFrame(data=(dict(d) for d in modelo[sparsed]), index=idms)
dfm.sort_index(inplace=True)
dfm.tail()
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
---|---|---|---|---|---|---|---|---|---|---|
0000296 | 0.134415 | 0.027543 | 0.084961 | 0.049565 | 0.287460 | 0.231275 | 0.094304 | 0.025726 | 0.043409 | 0.021340 |
0000297 | 0.171983 | 0.035479 | 0.086191 | 0.045762 | 0.252717 | 0.241238 | 0.082076 | 0.021267 | 0.043393 | 0.019894 |
0000298 | 0.157005 | 0.030130 | 0.094764 | 0.038289 | 0.239732 | 0.259019 | 0.095772 | 0.023655 | 0.040899 | 0.020735 |
0000299 | 0.150450 | 0.030741 | 0.107697 | 0.040900 | 0.263336 | 0.208023 | 0.103911 | 0.029972 | 0.037871 | 0.027099 |
0000300 | 0.171351 | 0.039782 | 0.101646 | 0.038009 | 0.240373 | 0.208915 | 0.090519 | 0.026963 | 0.057739 | 0.024702 |
traces = []
for i in dfm.columns:
traces.append(go.Scatter(x=dfm.index, y=dfm[i],
line=dict(width=2), name=f'Tópico {i}'))
fig = go.Figure(data=traces)
fig.update_layout(title_text="Evolución de tópicos", title_x=0.5,
xaxis_title='Documento', yaxis_title='Participación',
yaxis=dict(automargin=True, hoverformat='.2f'))
fig.write_html(file=str(graficas.joinpath('doctopics.html')), auto_open=False)
fig.show()
Bags
y Topicos
generan estructuras que pueden ser útiles más adelante, y por lo tanto se quiere guardar a disco.
Se quiere usualmente almacenar los modelos de "ngramas", el "diccionario" que contiene todas las palabras únicas existentes en el corpus (producido por Bags
), el mejor modelo de tópicos (producido por Topicos
).
mpath = crear_carpeta(carpeta.joinpath('modelos'))
#Guardar modelos de bigramas y trigramas
ng_frases.models.get('bigrams').save(str(mpath.joinpath('bigrams')))
ng_frases.models.get('trigrams').save(str(mpath.joinpath('trigrams')))
# Guardar diccionario
bags.vocab.save(str(mpath.joinpath('vocab')))
# Guardar mejor modelo
modelo.save(str(mpath.joinpath(f'topicos-{modelo.num_topics:0>2}.lda')))