Capítulo 1. Obtención de las primeras percepciones a partir de datos textuales

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Una de las primeras tareas de todo proyecto de análisis de datos y aprendizaje automático es familiarizarse con los datos. De hecho, siempre es esencial tener un conocimiento básico de los datos para obtener resultados sólidos. Las estadísticas descriptivas proporcionan perspectivas fiables y sólidas y ayudan a evaluar la calidad y la distribución de los datos.

Al considerar los textos, el análisis de frecuencias de palabras y frases es uno de los principales métodos de exploración de datos. Aunque las frecuencias absolutas de las palabras no suelen ser muy interesantes, las frecuencias relativas o ponderadas sí lo son. Al analizar un texto sobre política, por ejemplo, las palabras más frecuentes probablemente contendrán muchos términos obvios y poco sorprendentes, como pueblo, país, gobierno, etc. Pero si comparas las frecuencias relativas de palabras en textos de distintos partidos políticos o incluso de políticos del mismo partido, puedes aprender mucho de las diferencias.

Lo que aprenderás y lo que construiremos

Este capítulo presenta los planos para el análisis estadístico de texto. Te permite empezar rápidamente e introduce conceptos básicos que necesitarás conocer en capítulos posteriores. Empezaremos analizando metadatos categóricos y luego nos centraremos en el análisis y la visualización de la frecuencia de palabras.

Después de estudiar este capítulo, tendrás conocimientos básicos sobre procesamiento y análisis de textos. Sabrás cómo tokenizar el texto, filtrar las palabras vacías y analizar el contenido textual con diagramas de frecuencia y nubes de palabras. También introduciremos la ponderación TF-IDF como un concepto importante que se retomará más adelante en el libro para la vectorización de textos.

Los planos de este capítulo se centran en resultados rápidos y siguen el principio KISS: "¡Mantenlo simple, estúpido!". Así, utilizamos principalmente Pandas como biblioteca de elección para el análisis de datos en combinación con expresiones regulares y la funcionalidad básica de Python. El Capítulo 4 tratará sobre métodos lingüísticos avanzados para la preparación de datos.

Análisis Exploratorio de Datos

Exploratorio el análisis de datos es el proceso de examinar sistemáticamente los datos a nivel agregado. Los métodos típicos incluyen estadísticas de resumen para características numéricas, así como recuentos de frecuencia para características categóricas. Los histogramas y los gráficos de caja ilustrarán la distribución de los valores, y los gráficos de series temporales mostrarán su evolución.

Un conjunto de datos formado por documentos de texto, como noticias, tweets, correos electrónicos o llamadas de servicio, se denomina corpus en el procesamiento del lenguaje natural. La exploración estadística de un corpus de este tipo tiene distintas facetas. Algunos análisis se centran en los atributos de los metadatos, mientras que otros se ocupan del contenido textual. La Figura 1-1 muestra los atributos típicos de un corpus de texto, algunos de los cuales están incluidos en la fuente de datos, mientras que otros podrían calcularse o derivarse. Los metadatos del documento comprenden múltiples atributos descriptivos, útiles para la agregación y el filtrado. Los atributos relacionados con el tiempo son esenciales para comprender la evolución del corpus. Si están disponibles, los atributos relacionados con el autor te permiten analizar grupos de autores y comparar estos grupos entre sí.

Figura 1-1. Características estadísticas para la exploración de datos de texto.

El análisis estadístico del contenido se basa en las frecuencias de palabras y frases. Con los métodos de preprocesamiento lingüístico de datos descritos en el Capítulo 4, ampliaremos el espacio de análisis a determinados tipos de palabras y entidades con nombre. Además, las puntuaciones descriptivas de los documentos podrían incluirse en el conjunto de datos o derivarse mediante algún tipo de modelado de características. Por ejemplo, el número de respuestas a la publicación de un usuario podría tomarse como medida de popularidad. Por último, los hechos blandos interesantes, como las puntuaciones de sentimiento o emocionalidad, pueden determinarse mediante uno de los métodos que se describen más adelante en este libro.

Ten en cuenta que las cifras absolutas no suelen ser muy interesantes cuando se trabaja con texto. El mero hecho de que la palabra problema aparezca cien veces no contiene ninguna información relevante. Pero el hecho de que la frecuencia relativa del problema se haya duplicado en una semana puede ser notable.

Presentación del conjunto de datos

El análisis de textos políticos, ya sean noticias o programas de partidos políticos o debates parlamentarios, puede ofrecer interesantes perspectivas sobre temas nacionales e internacionales. A menudo, el texto de muchos años está disponible públicamente, por lo que se puede obtener una visión del zeitgeist. Pongámonos en la piel de un analista político que quiera hacerse una idea del potencial analítico de un conjunto de datos de este tipo.

Para ello, trabajaremos con elconjunto de datos Debate General de la ONU. El corpus consta de 7.507 discursos pronunciados en las sesiones anuales de la Asamblea General de las Naciones Unidas desde 1970 hasta 2016. Fue creado en 2017 por Mikhaylov, Baturo y Dasandi en Harvard "para comprender y medir las preferencias de los Estados en la política mundial". Cada uno de los casi 200 países de las Naciones Unidas tiene la oportunidad de exponer sus puntos de vista sobre temas globales como los conflictos internacionales, el terrorismo o el cambio climático en el Debate General anual.

El conjunto de datos original de en Kaggle se proporciona en forma de dos archivos CSV, uno grande que contiene los discursos y otro más pequeño con información sobre los oradores. Para simplificar las cosas, hemos preparado un único archivo CSV comprimido que contiene toda la información. Puedes encontrar el código para la preparación, así como el archivo resultante, en nuestro repositorio de GitHub.

En Pandas, se puede cargar un archivo CSV con pd.read_csv(). Carguemos el archivo y mostremos dos registros aleatorios del DataFrame:

file = "un-general-debates-blueprint.csv"
df = pd.read_csv(file)
df.sample(2)

Out:

sesión año país nombre_país altavoz posición texto
3871 51 1996 POR Perú Francisco Tudela Van Breughel Douglas Ministro de Asuntos Exteriores Ante todo, permítame, señor, transmitirle a usted y a esta Asamblea el saludo y la felicitación del pueblo peruano, así como de su...
4697 56 2001 GBR Reino Unido Jack Paja Ministro de Asuntos Exteriores Permítame felicitarle cordialmente, señor, por haber asumido la presidencia de la 56ª sesión de la Asamblea General.

La primera columna contiene el índice de los registros. La combinación del número de sesión y el año puede considerarse la clave primaria lógica de la tabla. La columna country contiene un código ISO de país estandarizado de tres letras y va seguida de la descripción textual. Después tenemos dos columnas sobre el ponente y su posición. La última columna contiene el texto real del discurso.

Nuestro conjunto de datos es pequeño; sólo contiene unos pocos miles de registros. Es un gran conjunto de datos para utilizar porque no tendremos problemas de rendimiento. Si tu conjunto de datos es mayor, consulta "Trabajar con conjuntos de datos grandes" para conocer las opciones.

Plano: Obtener una visión general de los datos con Pandas

En nuestro primer proyecto de , sólo utilizamos metadatos y recuentos de registros para explorar la distribución y calidad de los datos; aún no examinaremos el contenido textual. Trabajaremos en los siguientes pasos:

  1. Calcula estadísticas resumidas.
  2. Comprueba si faltan valores.
  3. Traza distribuciones de atributos interesantes.
  4. Compara las distribuciones entre categorías.
  5. Visualiza la evolución en el tiempo.

Antes de empezar a analizar los datos, necesitamos al menos cierta información sobre la estructura de DataFrame . La Tabla 1-1 muestra algunas propiedades o funciones descriptivas importantes.

Tabla 1-1. Comandos Pandas para obtener información sobre dataframes
df.columns Lista de nombres de columnas
df.dtypes Tuplas (nombre de columna, tipo de datos) Las cadenas se representan como objetos en las versiones anteriores a Pandas 1.0.
df.info() Tipos D más consumo de memoria Utilízalo con memory_usage='deep' para obtener buenas estimaciones sobre el texto.
df.describe() Resumen estadístico Utilízalo con include='O' para datos categóricos.

Cálculo de estadísticas de resumen para columnas

La función describe de Pandas calcula resúmenes estadísticos de las columnas de DataFrame. Funciona tanto en una sola serie como en la DataFrame completa. En este último caso, la salida por defecto se limita a las columnas numéricas. Actualmente, nuestro DataFrame sólo contiene el número de sesión y el año como datos numéricos. Añadamos una nueva columna numérica a DataFrame que contenga la longitud del texto para obtener información adicional sobre la distribución de las longitudes de los discursos. Recomendamos transponer el resultado con describe().T para intercambiar filas y columnas en la representación:

df['length'] = df['text'].str.len()

df.describe().T

Out:

cuenta media std min 25% 50% 75% max
sesión 7507.00 49.61 12.89 25.00 39.00 51.00 61.00 70.00
año 7507.00 1994.61 12.89 1970.00 1984.00 1996.00 2006.00 2015.00
longitud 7507.00 17967.28 7860.04 2362.00 12077.00 16424.00 22479.50 72041.00

describe(), sin parámetros adicionales, calcula el recuento total de valores, su media y desviación típica, y un resumen de cinco números sólo de las columnas numéricas. El DataFrame contiene 7.507 entradas para session, year y length. La media y la desviación típica no tienen mucho sentido para year y session, pero el mínimo y el máximo siguen siendo interesantes. Obviamente, nuestro conjunto de datos contiene discursos de las sesiones 25ª a 70ª del Debate General de la ONU, que abarcan los años 1970 a 2015.

Se puede obtener un resumen para columnas no numéricas especificando include='O' (el alias de np.object). En este caso, también obtenemos el recuento, el número de valores únicos, el elemento más alto (o uno de ellos si hay muchos con el mismo número de apariciones) y su frecuencia. Como el número de valores únicos no es útil para los datos textuales, vamos a analizar sólo las columnas country y speaker:

df[['country', 'speaker']].describe(include='O').T

Out:

cuenta único top frec
país 7507 199 ITA 46
altavoz 7480 5428 Seyoum Mesfin 12

El conjunto de datos contiene datos de 199 países únicos y aparentemente 5.428 hablantes. El número de países es válido, ya que esta columna contiene códigos ISO normalizados. Pero contar los valores únicos de columnas de texto como speaker no suele dar resultados válidos, como mostraremos en la siguiente sección .

Comprobar si faltan datos

Observando los recuentos de la tabla anterior, podemos ver que la columna speaker tiene valores nulos. Así que vamos a comprobar todas las columnas para ver si tienen valores nulos utilizando df.isna() (el alias de df.isnull()) y calcular un resumen del resultado:

df.isna().sum()

Out:

session            0
year               0
country            0
country_name       0
speaker           27
position        3005
text               0
length             0
dtype: int64

Debemos tener cuidado al utilizar las columnas speaker y position, ¡ya que el resultado nos indica que esta información no siempre está disponible! Para evitar problemas, podríamos sustituir los valores que faltan por algún valor genérico como unknown speaker o unknown position o simplemente la cadena vacía.

Para ello, Pandas proporciona la función df.fillna():

df['speaker'].fillna('unknown', inplace=True)

Pero incluso los valores existentes pueden ser problemáticos, porque el nombre del mismo orador a veces se escribe de forma diferente o incluso ambigua. La siguiente sentencia calcula el número de registros por locutor para todos los documentos que contengan Bush en la columna locutor:

df[df['speaker'].str.contains('Bush')]['speaker'].value_counts()

Out:

George W. Bush        4
Mr. George W. Bush    2
George Bush           1
Mr. George W Bush     1
Bush                  1
Name: speaker, dtype: int64

Cualquier análisis sobre los nombres de los hablantes produciría resultados erróneos si no resolvemos estas ambigüedades. Así que será mejor que comprobemos los valores distintos de los atributos categóricos. Sabiendo esto, ignoraremos aquí la información sobre el hablante.

Trazar distribuciones de valores

Una forma de visualizar el resumen de cinco números de una distribución numérica es un gráfico de caja. Se puede obtener fácilmente con la función de gráficos integrada en Pandas. Veamos el gráfico de caja de la columna length:

df['length'].plot(kind='box', vert=False)

Out:

Como ilustra este gráfico, el 50% de los discursos (el recuadro del centro) tienen una longitud de entre 12.000 y 22.000 caracteres aproximadamente, con una mediana de unos 16.000 y una larga cola con muchos valores atípicos a la derecha. Obviamente, la distribución está sesgada a la izquierda. Podemos obtener más detalles trazando un histograma:

df['length'].plot(kind='hist', bins=30)

Out:

Para el histograma, el intervalo de valores de la columna length se divide en 30 intervalos de igual anchura, los bins. El eje y muestra el número de documentos que caen en cada uno de estos intervalos.

Comparar distribuciones de valores entre categorías

Las peculiaridades de los datos suelen hacerse visibles cuando se examinan diferentes subconjuntos de los datos. Una buena visualización para comparar distribuciones entre diferentes categorías es Seaborn's catplot.

En mostramos gráficos de caja y de violín para comparar las distribuciones de la longitud de los discursos de los cinco miembros permanentes del consejo de seguridad de la ONU(Figura 1-2). Así, la categoría para el eje x de sns.catplot es country:

where = df['country'].isin(['USA', 'FRA', 'GBR', 'CHN', 'RUS'])
sns.catplot(data=df[where], x="country", y="length", kind='box')
sns.catplot(data=df[where], x="country", y="length", kind='violin')
Figura 1-2. Gráficos de caja (izquierda) y de violín (derecha) que visualizan la distribución de las longitudes de palabra de los países seleccionados.

El gráfico de violín es la versión "suavizada" de un gráfico de caja. Las frecuencias se visualizan por la anchura del cuerpo del violín, mientras que la caja sigue siendo visible dentro del violín. Ambos gráficos revelan que la dispersión de los valores, en este caso las longitudes de los discursos, para Rusia es mucho mayor que para Gran Bretaña. Pero la existencia de múltiples picos, como en Rusia, sólo se hace evidente en el gráfico del violín .

Visualizar la evolución en el tiempo

Si tus datos de contienen atributos de fecha u hora, siempre es interesante visualizar la evolución de los datos a lo largo del tiempo. Podemos crear una primera serie temporal analizando el número de discursos por año. Podemos utilizar la función de agrupación de Pandas size() para devolver el número de filas por grupo. Simplemente añadiendo plot(), podemos visualizar el resultado DataFrame (Figura 1-3, izquierda):

df.groupby('year').size().plot(title="Number of Countries")

El calendario refleja la evolución del número de países en la ONU, ya que cada país sólo puede optar a un discurso al año. En la actualidad, la ONU tiene 193 miembros. Curiosamente, la duración de los discursos tuvo que disminuir a medida que aumentaba el número de países que participaban en los debates, como revela el siguiente análisis(Figura 1-3, derecha):

df.groupby('year').agg({'length': 'mean'}) \
  .plot(title="Avg. Speech Length", ylim=(0,30000))
Figura 1-3. Número de países y duración media del discurso a lo largo del tiempo.
Nota

Los marcos de datos de Pandas no sólo se pueden visualizar fácilmente en los cuadernos Jupyter, sino que también se pueden exportar a Excel (.xlsx), HTML, CSV, LaTeX y muchos otros formatos mediante funciones incorporadas. En hay incluso una función to_clipboard(). Consulta la documentación para más detalles.

Plano: Construir una cadena sencilla de preprocesamiento de textos

El análisis de metadatos como las categorías, el tiempo, los autores y otros atributos ofrece unas primeras ideas sobre el corpus. Pero es mucho más interesante profundizar en el contenido real y explorar las palabras frecuentes en diferentes subconjuntos o periodos de tiempo. En esta sección, desarrollaremos un plan básico para preparar el texto para un primer análisis rápido, que consiste en una sencilla secuencia de pasos(Figura 1-4). Dado que la salida de una operación constituye la entrada de la siguiente, dicha secuencia también se denomina canal de procesamiento que transforma el texto original en una serie de tokens.

Figura 1-4. Proceso de preprocesamiento simple.

El proceso presentado aquí consta de tres pasos: conversión de mayúsculas a minúsculas, tokenización y eliminación de palabras vacías. Estos pasos se tratarán en profundidad y se ampliarán en el Capítulo 4, donde utilizaremos spaCy. Para hacerlo rápido y sencillo, construimos nuestro propio tokenizador basado en expresiones regulares y mostramos cómo utilizar una lista arbitraria de palabras vacías.

Realizar la tokenización con expresiones regulares

Tokenización es el proceso de extraer palabras de una secuencia de caracteres. En las lenguas occidentales , las palabras suelen estar separadas por espacios en blanco y caracteres de puntuación. Por ello, el tokenizador más sencillo y rápido es el método nativo de Python str.split() de Python, que divide en espacios en blanco. Una forma más flexible es utilizar expresiones regulares.

Las expresiones regulares y las bibliotecas de Python re y regex se presentarán con más detalle en el Capítulo 4. Aquí queremos aplicar un patrón simple que coincida con palabras. En nuestra definición, las palabras constan de al menos una letra, además de dígitos y guiones. Los números puros se omiten porque representan casi exclusivamente fechas o identificadores de discurso o sesión en este corpus.

La expresión [A-Za-z], de uso frecuente, no es una buena opción para emparejar letras porque pasa por alto letras acentuadas como ä o â. Mucho mejor es la clase de caracteres POSIX \p{L} , que selecciona todas las letras Unicode. Ten en cuenta que necesitamos la bibliotecaregex en lugar de re para trabajar con clases de caracteres POSIX. La siguiente expresión coincide con tokens formados por al menos una letra (\p{L}), precedida y seguida de una secuencia arbitraria de caracteres alfanuméricos (\w incluye dígitos, letras y guión bajo) y guiones (- ) :

import regex as re

def tokenize(text):
    return re.findall(r'[\w-]*\p{L}[\w-]*', text)

Probémoslo con una frase de ejemplo del corpus:

text = "Let's defeat SARS-CoV-2 together in 2020!"
tokens = tokenize(text)
print("|".join(tokens))

Out:

Let|s|defeat|SARS-CoV-2|together|in

Tratamiento de las palabras vacías

Las palabras más frecuentes del texto son palabras comunes como determinantes, verbos auxiliares, pronombres, adverbios, etc. Estas palabras se denominan palabras vacías. Las stop words no suelen aportar mucha información, pero ocultan contenido interesante debido a sus altas frecuencias. Por eso, las palabras vacías suelen eliminarse antes del análisis de los datos o del entrenamiento del modelo.

En esta sección, mostramos cómo descartar las palabras de parada contenidas en una lista predefinida. Existen listas comunes de palabras de parada para muchos idiomas y están integradas en casi cualquier biblioteca de PNL. Aquí trabajaremos con la lista de palabras vacías de NLTK, pero podrías utilizar cualquier lista de palabras como filtro.2 Para una búsqueda rápida, siempre debes convertir una lista en un conjunto. Los conjuntos son estructuras basadas en hash, como los diccionarios, con un tiempo de búsqueda casi constante:

import nltk

stopwords = set(nltk.corpus.stopwords.words('english'))

Nuestro enfoque para eliminar las palabras vacías de una lista dada, envuelto en la pequeña función que se muestra aquí, consiste en una simple comprensión de la lista. Para la comprobación, los tokens se convierten a minúsculas, ya que la lista de NLTK sólo contiene palabras en minúsculas:

def remove_stop(tokens):
    return [t for t in tokens if t.lower() not in stopwords]

A menudo necesitarás añadir palabras de parada específicas del dominio a la lista predefinida. Por ejemplo, si estás analizando correos electrónicos, es probable que los términos querido y saludos aparezcan en casi cualquier documento. Por otra parte, puede que quieras tratar algunas de las palabras de la lista predefinida no como palabras de parada. Podemos añadir palabras de parada adicionales y excluir otras de la lista utilizando dos de los operadores de conjunto de Python, | (unión/or) y - (diferencia):

include_stopwords = {'dear', 'regards', 'must', 'would', 'also'}
exclude_stopwords = {'against'}

stopwords |= include_stopwords
stopwords -= exclude_stopwords

La lista de palabras interceptivas de NLTK es conservadora y sólo contiene 179 palabras. Sorprendentemente, would no se considera una stop word, mientras que wouldn't sí. Esto ilustra un problema común de las listas predefinidas de palabras vacías: la incoherencia. Ten en cuenta que eliminar palabras vacías puede afectar significativamente al rendimiento de los análisis semánticos, como se explica en "Por qué eliminar palabras vacías puede ser peligroso".

Además o en lugar de una lista fija de palabras reservadas, puede ser útil tratar como palabra reservada toda palabra que aparezca en más de, digamos, el 80% de los documentos. Estas palabras comunes dificultan la distinción del contenido. El parámetro max_df de los vectorizadores scikit-learn, como se explica en el Capítulo 5, hace exactamente esto. Otro método consiste en filtrar las palabras en función del tipo de palabra (parte de la oración). Este concepto se explicará en el Capítulo 4.

Procesar una tubería con una línea de código

Volvamos al DataFrame que contiene los documentos de nuestro corpus. Queremos crear una nueva columna llamada tokens que contenga el texto en minúsculas, tokenizado y sin palabras de parada de cada documento. Para ello, utilizamos un patrón extensible para una cadena de procesamiento. En nuestro caso, cambiaremos todo el texto a minúsculas, lo tokenizaremos y eliminaremos las palabras vacías. Se pueden añadir otras operaciones simplemente ampliando el proceso:

pipeline = [str.lower, tokenize, remove_stop]

def prepare(text, pipeline):
    tokens = text
    for transform in pipeline:
        tokens = transform(tokens)
    return tokens

Si ponemos todo esto en una función, se convierte en un caso de uso perfecto para la operación map o apply de Pandas. Las funciones como map y apply, que toman otras funciones como parámetros, se denominan funciones de orden superior en matemáticas e informática.

Tabla 1-2. Funciones de orden superior de Pandas
Función Descripción
Series.map Trabaja elemento a elemento en un Pandas Series
Series.apply Igual que map pero permite parámetros adicionales
DataFrame.applymap Elemento por elemento en un Pandas DataFrame (igual que map en Series)
DataFrame.apply Funciona en filas o columnas de un DataFrame y admite la agregación

Pandas admite las distintas funciones de orden superior sobre series y marcos de datos (Tabla 1-2). Estas funciones no sólo te permiten especificar una serie de transformaciones funcionales de datos de forma comprensible, sino que también se pueden paralelizar fácilmente. El paquete Python pandarallelpor ejemplo, proporciona versiones paralelas de map y apply.

Los marcos escalables como Apache Spark soportan operaciones similares en marcos de datos de forma aún más elegante. De hecho, las operaciones map y reduce de la programación distribuida se basan en el mismo principio de la programación funcional. Además, muchos lenguajes de programación, como Python y JavaScript, tienen una operación de mapa nativa para listas o matrices.

Utilizando una de las operaciones de orden superior de Pandas, aplicar una transformación funcional se convierte en una simple operación:

df['tokens'] = df['text'].apply(prepare, pipeline=pipeline)

La columna tokens consta ahora de listas Python que contienen los tokens extraídos para cada documento. Por supuesto, esta columna adicional básicamente duplica el consumo de memoria de DataFrame, pero te permite acceder rápidamente a los tokens de directamente para su posterior análisis. No obstante, los siguientes planos están diseñados de forma que la tokenización también pueda realizarse sobre la marcha durante el análisis. De este modo, se puede intercambiar rendimiento por consumo de memoria: o bien tokenizar una vez antes del análisis y consumir memoria, o bien tokenizar sobre la marcha y esperar.

También añadimos otra columna que contiene la longitud de la lista de fichas para hacer resúmenes más adelante:

df['num_tokens'] = df['tokens'].map(len)
Nota

tqdm (pronunciado taqadum por "progreso" en árabe) es una gran biblioteca para barras de progreso en Python. Admite bucles convencionales, por ejemplo, utilizando tqdm_range en lugar de range, y admite Pandas proporcionando progress_map y progress_apply operaciones sobre marcos de datos.3 Nuestros cuadernos adjuntos en GitHub utilizan estas operaciones, pero aquí en el libro nos ceñimos a simplemente Pandas.

Planos para el análisis de frecuencia de palabras

Las palabras y frases utilizadas con frecuencia en pueden proporcionarnos una comprensión básica de los temas tratados. Sin embargo, el análisis de frecuencia de palabras ignora el orden y el contexto de las palabras. Esta es la idea del famoso modelo de bolsa de palabras (véase también el Capítulo 5): todas las palabras se echan en una bolsa donde se revuelven en un revoltijo. Se pierde la disposición original en el texto; sólo se tiene en cuenta la frecuencia de los términos. Este modelo no funciona bien para tareas complejas como el análisis de sentimientos o la respuesta a preguntas, pero funciona sorprendentemente bien para la clasificación y el modelado de temas. Además, es un buen punto de partida para comprender de qué tratan los textos.

En esta sección, desarrollaremos una serie de planos para calcular y visualizar las frecuencias de las palabras. Como las frecuencias brutas sobrevaloran las palabras sin importancia pero frecuentes, también introduciremos el TF-IDF al final del proceso. Implementaremos el cálculo de frecuencias utilizando un Counter porque es sencillo y extremadamente rápido.

Plano: Contar palabras con un contador

La biblioteca estándar de Python tiene una clase integrada Counter , que hace exactamente lo que podrías pensar: cuenta cosas.4 La forma más sencilla de trabajar con un contador es crearlo a partir de una lista de elementos, en nuestro caso cadenas que representan las palabras o tokens. El contador resultante es básicamente un objeto diccionario que contiene esos elementos como claves y sus frecuencias como valores.

Vamos a ilustrar su funcionalidad con un ejemplo sencillo:

from collections import Counter

tokens = tokenize("She likes my cats and my cats like my sofa.")

counter = Counter(tokens)
print(counter)

Out:

Counter({'my': 3, 'cats': 2, 'She': 1, 'likes': 1, 'and': 1, 'like': 1,
         'sofa': 1})

El contador requiere una lista como entrada, por lo que cualquier texto debe ser tokenizado de antemano. Lo bueno del contador es que puede actualizarse incrementalmente con una lista de tokens de un segundo documento:

more_tokens = tokenize("She likes dogs and cats.")
counter.update(more_tokens)
print(counter)

Out:

Counter({'my': 3, 'cats': 3, 'She': 2, 'likes': 2, 'and': 2, 'like': 1,
         'sofa': 1, 'dogs': 1})

Para encontrar las palabras más frecuentes dentro de un corpus, tenemos que crear un contador a partir de la lista de todas las palabras de todos los documentos. Un método ingenuo consistiría en concatenar todos los documentos en una única lista gigante de tokens, pero eso no es escalable para conjuntos de datos más grandes. Es mucho más eficaz llamar a la función update del objeto contador para cada documento.

counter = Counter()

df['tokens'].map(counter.update)

Hacemos un pequeño truco aquí y pone counter.update en la función map. La magia ocurre dentro de la función update bajo el capó. Toda la llamada a map se ejecuta extremadamente rápido; sólo tarda unos tres segundos para los 7.500 discursos de la ONU y escala linealmente con el número total de tokens. La razón es que los diccionarios en general y los contadores en particular se implementan como tablas hash. Un único contador es bastante compacto en comparación con todo el corpus: contiene cada palabra una sola vez, junto con su frecuencia.

Ahora podemos recuperar las palabras más comunes del texto con la función contador correspondiente:

print(counter.most_common(5))

Out:

[('nations', 124508),
 ('united', 120763),
 ('international', 117223),
 ('world', 89421),
 ('countries', 85734)]

Para su posterior procesamiento y análisis, es mucho más conveniente transformar el contador en un Pandas DataFrame, y esto es lo que finalmente hace la siguiente función de plano. Los tokens constituyen el índice del DataFrame, mientras que los valores de frecuencia se almacenan en una columna llamada freq. Las filas se ordenan de forma que las palabras más frecuentes aparezcan en la cabecera:

def count_words(df, column='tokens', preprocess=None, min_freq=2):

    # process tokens and update counter
    def update(doc):
        tokens = doc if preprocess is None else preprocess(doc)
        counter.update(tokens)

    # create counter and run through all data
    counter = Counter()
    df[column].map(update)

    # transform counter into a DataFrame
    freq_df = pd.DataFrame.from_dict(counter, orient='index', columns=['freq'])
    freq_df = freq_df.query('freq >= @min_freq')
    freq_df.index.name = 'token'

    return freq_df.sort_values('freq', ascending=False)

La función toma, como primer parámetro, un Pandas DataFrame y, como segundo parámetro, el nombre de la columna que contiene los tokens o el texto. Como ya hemos almacenado los tokens preparados en la columna tokens del DataFrame que contiene los discursos, podemos utilizar las dos líneas de código siguientes para calcular el DataFrame con las frecuencias de palabras y mostrar los cinco tokens principales:

freq_df = count_words(df)
freq_df.head(5)

Out:

ficha frec
naciones 124508
unidos 120763
internacional 117223
mundo 89421
países 85734

Si no queremos utilizar tokens precalculados para algún análisis especial, podríamos tokenizar el texto sobre la marcha con una función de preprocesamiento personalizada como tercer parámetro. Por ejemplo, podríamos generar y contar todas las palabras con 10 o más caracteres con esta tokenización sobre la marcha del texto:

    count_words(df, column='text',
                preprocess=lambda text: re.findall(r"\w{10,}", text))

El último parámetro de count_words define una frecuencia mínima de tokens que se incluirán en el resultado. Su valor predeterminado es 2 para reducir la larga cola de hapaxes, es decir, los tokens que sólo aparecen una vez.

Plano: Crear un diagrama de frecuencias

Hay docenas de formas de producir tablas y diagramas en Python. Preferimos Pandas con su funcionalidad de diagramas incorporada porque es más fácil de usar que Matplotlib. Asumimos un DataFrame freq_df generado por el plano anterior para su visualización. Crear un diagrama de frecuencias basado en dicho DataFrame se convierte ahora básicamente en una sola línea. Añadimos dos líneas más para el formato:

ax = freq_df.head(15).plot(kind='barh', width=0.95)
ax.invert_yaxis()
ax.set(xlabel='Frequency', ylabel='Token', title='Top Words')

Out:

Utilizar barras horizontales (barh) para las frecuencias de las palabras mejora mucho la legibilidad, porque las palabras aparecen horizontalmente en el eje y de forma legible. El eje y se invierte para colocar las palabras más importantes en la parte superior del gráfico. Las etiquetas de los ejes y el título pueden modificarse opcionalmente.

Plano: Crear nubes de palabras

Los diagramas de distribuciones de frecuencias como los mostrados anteriormente ofrecen información detallada sobre las frecuencias de los tokens. Pero es bastante difícil comparar diagramas de frecuencias para distintos periodos de tiempo, categorías, autores, etc. Las nubes de palabras, en cambio, visualizan las frecuencias mediante distintos tamaños de letra. Son mucho más fáciles de comprender y comparar, pero carecen de la precisión de las tablas y los diagramas de barras. Debes tener en cuenta que las palabras largas o con mayúsculas obtienen una atracción desproporcionadamente alta.

El módulo Python wordcloud genera bonitas nubes de palabras a partir de textos o contadores. La forma más sencilla de utilizarlo es instanciar un objeto nube de palabras con algunas opciones, como el número máximo de palabras y una lista de palabras de parada, y luego dejar que el módulo wordcloud se encargue de la tokenización y la eliminación de las palabras de parada. El código siguiente muestra cómo generar una nube de palabras para el texto del discurso de EE.UU. de 2015 y mostrar la imagen resultante con Matplotlib:

from wordcloud import WordCloud
from matplotlib import pyplot as plt

text = df.query("year==2015 and country=='USA'")['text'].values[0]

wc = WordCloud(max_words=100, stopwords=stopwords)
wc.generate(text)
plt.imshow(wc, interpolation='bilinear')
plt.axis("off")

Sin embargo, esto sólo funciona para un único texto y no para un conjunto (potencialmente grande) de documentos. Para este último caso de uso, es mucho más rápido crear primero un contador de frecuencias y utilizar después la función generate_from_frequencies().

Nuestro plano es una pequeña envoltura alrededor de esta función para que también admita un Pandas Series que contenga los valores de frecuencia creados por count_words. La clase WordCloud ya dispone de una magnitud de opciones para ajustar el resultado. Utilizamos algunas de ellas en la siguiente función para demostrar posibles ajustes, pero deberías consultar la documentación para obtener más detalles:

def wordcloud(word_freq, title=None, max_words=200, stopwords=None):

    wc = WordCloud(width=800, height=400,
                   background_color= "black", colormap="Paired",
                   max_font_size=150, max_words=max_words)

    # convert DataFrame into dict
    if type(word_freq) == pd.Series:
        counter = Counter(word_freq.fillna(0).to_dict())
    else:
        counter = word_freq

    # filter stop words in frequency counter
    if stopwords is not None:
        counter = {token:freq for (token, freq) in counter.items()
                              if token not in stopwords}
    wc.generate_from_frequencies(counter)

    plt.title(title)

    plt.imshow(wc, interpolation='bilinear')
    plt.axis("off")

La función tiene dos parámetros convenientes para filtrar palabras. skip_n omite las n primeras palabras de la lista. Obviamente, en un corpus de la ONU palabras como unida, naciones o internacional encabezan la lista. Puede ser más interesante visualizar lo que viene a continuación. El segundo filtro es una lista (adicional) de palabras de parada. A veces es útil filtrar determinadas palabras frecuentes pero poco interesantes sólo para la visualización.5

Echemos un vistazo a los discursos de 2015(Figura 1-5). La nube de palabras de la izquierda visualiza las palabras más frecuentes sin filtrar. La nube de palabras de la derecha, en cambio, trata las 50 palabras más frecuentes del corpus completo como stop words:

freq_2015_df = count_words(df[df['year']==2015])
plt.figure()
wordcloud(freq_2015_df['freq'], max_words=100)
wordcloud(freq_2015_df['freq'], max_words=100, stopwords=freq_df.head(50).index)
Figura 1-5. Nubes de palabras de los discursos de 2015 con todas las palabras (izquierda) y sin las 50 palabras más frecuentes (derecha).

Está claro que la nube de palabras correcta sin las palabras más frecuentes del corpus da una idea mucho mejor de los temas de 2015, pero sigue habiendo palabras frecuentes e inespecíficas como hoy o retos. Necesitamos una forma de dar menos peso a esas palabras, como se muestra en la siguiente sección de .

Plano: Clasificación con TF-IDF

Como se ilustra en la Figura 1-5, visualizar las palabras más frecuentes no suele revelar mucho. Incluso si se eliminan las palabras de parada, las palabras más frecuentes suelen ser términos obvios específicos del dominio que son bastante similares en cualquier subconjunto (rebanada) de los datos. Pero nos gustaría dar más importancia a las palabras que aparecen con más frecuencia en una determinada rebanada de los datos que las "habituales". Ese trozo puede ser cualquier subconjunto del corpus, por ejemplo, un solo discurso, los discursos de una década determinada o los discursos de un país.

Queremos resaltar las palabras cuya frecuencia real en un trozo es superior a lo que sugeriría su probabilidad total. Existen varios algoritmos para medir el factor "sorpresa" de una palabra. Uno de los enfoques más sencillos y que mejor funciona es complementar la frecuencia de términos con la frecuencia inversa de documentos (ver barra lateral).

Definamos una función para calcular el IDF de todos los términos del corpus. Es casi idéntica a count_words, salvo que cada token se cuenta sólo una vez por documento (counter.update(set(tokens))), y los valores de IDF se calculan después del recuento. El parámetro min_df sirve como filtro para la cola larga de palabras poco frecuentes. El resultado de esta función es de nuevo un DataFrame:

def compute_idf(df, column='tokens', preprocess=None, min_df=2):

    def update(doc):
        tokens = doc if preprocess is None else preprocess(doc)
        counter.update(set(tokens))

    # count tokens
    counter = Counter()
    df[column].map(update)

    # create DataFrame and compute idf
    idf_df = pd.DataFrame.from_dict(counter, orient='index', columns=['df'])
    idf_df = idf_df.query('df >= @min_df')
    idf_df['idf'] = np.log(len(df)/idf_df['df'])+0.1
    idf_df.index.name = 'token'
    return idf_df

Los valores IDF deben calcularse una vez para todo el corpus (¡no utilices aquí un subconjunto!) y luego pueden utilizarse en todo tipo de análisis. Creamos un DataFrame que contiene los valores IDF de cada token (idf_df) con esta función:

idf_df = compute_idf(df)

Como tanto el IDF como la frecuencia DataFrame tienen un índice formado por los tokens, podemos simplemente multiplicar las columnas de ambos DataFrames para calcular la puntuación TF-IDF de los términos:

freq_df['tfidf'] = freq_df['freq'] * idf_df['idf']

Vamos a comparar las nubes de palabras basadas únicamente en el recuento de palabras (frecuencias de términos) y las puntuaciones TF-IDF de los discursos del primer y último año del corpus. Eliminamos algunas palabras de parada más que representan los números de las respectivas sesiones de debate.

freq_1970 = count_words(df[df['year'] == 1970])
freq_2015 = count_words(df[df['year'] == 2015])

freq_1970['tfidf'] = freq_1970['freq'] * idf_df['idf']
freq_2015['tfidf'] = freq_2015['freq'] * idf_df['idf']

#wordcloud(freq_df['freq'], title='All years', subplot=(1,3,1))
wordcloud(freq_1970['freq'], title='1970 - TF',
          stopwords=['twenty-fifth', 'twenty-five'])
wordcloud(freq_2015['freq'], title='2015 - TF',
          stopwords=['seventieth'])
wordcloud(freq_1970['tfidf'], title='1970 - TF-IDF',
          stopwords=['twenty-fifth', 'twenty-five', 'twenty', 'fifth'])
wordcloud(freq_2015['tfidf'], title='2015 - TF-IDF',
          stopwords=['seventieth'])

Las nubes de palabras de la Figura 1-6 demuestran de forma impresionante el poder de la ponderación TF-IDF. Aunque las palabras más comunes son casi idénticas en 1970 y 2015, las visualizaciones ponderadas TF-IDF destacan las diferencias de los temas políticos.

Figura 1-6. Palabras ponderadas por recuentos de llanura (arriba) y TF-IDF (abajo) para discursos de dos años seleccionados.

El lector experimentado de podría preguntarse por qué hemos implementado nosotros mismos las funciones para contar palabras y calcular los valores de la FID en lugar de utilizar las clases CountVectorizer y TfidfVectorizer de scikit-learn. En realidad, hay dos razones. En primer lugar, los vectorizadores producen un vector con frecuencias de términos ponderadas para cada documento individual, en lugar de subconjuntos arbitrarios del conjunto de datos. En segundo lugar, los resultados son matrices (buenas para el aprendizaje automático) y no marcos de datos (buenos para el troceado, la agregación y la visualización). Al final tendríamos que escribir aproximadamente el mismo número de líneas de código para producir los resultados de la Figura 1-6, pero perderíamos la oportunidad de introducir este importante concepto de desde cero. Los vectorizadores de scikit-learn se tratarán en detalle en el Capítulo 5.

Plano: Encontrar una palabra clave en contexto

Las nubes de palabras y los diagramas de frecuencias son herramientas estupendas para resumir visualmente datos textuales. Sin embargo, también suelen plantear dudas sobre por qué un determinado término aparece de forma tan destacada. Por ejemplo, la nube de palabras TF-IDF de 2015 comentada antes muestra los términos pv, sdgs o sids, y probablemente no conozcas su significado. Para averiguarlo, necesitamos una forma de inspeccionar las apariciones reales de esas palabras en el texto original, no preparado. Una forma sencilla pero inteligente de realizar esa inspección es el análisis de palabras clave en contexto (KWIC). Produce una lista de fragmentos de texto de igual longitud que muestran el contexto izquierdo y derecho de una palabra clave. Aquí tienes una muestra de la lista KWIC para sdgs, que nos da una explicación de ese término:

5 random samples out of 73 contexts for 'sdgs':
 of our planet and its people. The   SDGs   are a tangible manifestation of th
nd, we are expected to achieve the   SDGs   and to demonstrate dramatic develo
ead by example in implementing the   SDGs   in Bangladesh. Attaching due impor
the Sustainable Development Goals (  SDGs  ). We applaud all the Chairs of the
new Sustainable Development Goals (  SDGs  ) aspire to that same vision. The A

Obviamente, sdgs es la versión minúscula de SDGs, que significa "objetivos de desarrollo sostenible". Con el mismo análisis podemos saber que sids significa "pequeños estados insulares en desarrollo". Es una información importante para interpretar los temas de 2015! pv, sin embargo, es un artefacto de tokenización. En realidad, es el resto de referencias de citas como (A/70/PV.28), que significa "Asamblea 70, Proceso Verbal 28", es decir, discurso 28 de la 70ª asamblea.

Nota

Consulta siempre los detalles de cuando encuentres fichas que no conozcas o que no tengan sentido para ti. A menudo llevan información importante (como sdgs) que tú, como analista, deberías ser capaz de interpretar. Pero también encontrarás a menudo artefactos como pv. Éstos deben descartarse si son irrelevantes o deben tratarse correctamente.

El análisis KWIC se implementa en NLTK y textacy. Utilizaremos la funciónKWIC de textacy porque es rápida y funciona en el texto no codificado. Así, podemos buscar cadenas que abarquen varios tokens como "cambio climático", mientras que NLTK no puede. Tanto las funciones KWIC de NLTK como las de textacy sólo funcionan en un único documento. Para ampliar el análisis a varios documentos en un DataFrame, proporcionamos la siguiente función:

from textacy.text_utils import KWIC

def kwic(doc_series, keyword, window=35, print_samples=5):

    def add_kwic(text):
        kwic_list.extend(KWIC(text, keyword, ignore_case=True,
                              window_width=window, print_only=False))

    kwic_list = []
    doc_series.map(add_kwic)

    if print_samples is None or print_samples==0:
        return kwic_list
    else:
        k = min(print_samples, len(kwic_list))
        print(f"{k} random samples out of {len(kwic_list)} " + \
              f"contexts for '{keyword}':")
        for sample in random.sample(list(kwic_list), k):
            print(re.sub(r'[\n\t]', ' ', sample[0])+'  '+ \
                  sample[1]+'  '+\
                  re.sub(r'[\n\t]', ' ', sample[2]))

La función recopila de forma iterativa los contextos de palabras clave mediante aplicando la función add_kwic a cada documento con map. Este truco, que ya utilizamos en los planos de recuento de palabras, es muy eficaz y permite el análisis KWIC también para corpus más grandes. Por defecto, la función devuelve una lista de tuplas de la forma (left context, keyword, right context). Si print_samples es mayor que 0, se imprime una muestra aleatoria de los resultados.8 El muestreo es especialmente útil cuando trabajas con muchos documentos, ya que, de lo contrario, las primeras entradas de la lista procederían de un único documento o de un número muy reducido de ellos.

La lista KWIC para sdgs de antes era generada por esta llamada:

kwic(df[df['year'] == 2015]['text'], 'sdgs', print_samples=5)

Plano: Análisis de N-Gramas

El mero hecho de saber que clima es una palabra frecuente no nos dice demasiado sobre el tema de discusión porque, por ejemplo, cambio climático y clima político tienen significados completamente distintos. Incluso cambio climático no es lo mismo que cambio climático. Por eso puede ser útil ampliar los análisis de frecuencia de palabras sueltas a secuencias cortas de dos o tres palabras.

Básicamente, buscamos dos tipos de secuencias de palabras : compuestos y colocaciones. Un compuesto es una combinación de dos o más palabras con un significado específico. En inglés, encontramos compuestos en forma cerrada, como terremoto; guionizada, como seguro de sí mismo; y abierta, como cambio climático. Así, puede que tengamos que considerar dos tokens como una sola unidad semántica . Las colocaciones, en cambio, son palabras que se utilizan juntas con frecuencia. A menudo, constan de un adjetivo o verbo y un sustantivo, como alfombra roja o naciones unidas.

En el tratamiento de textos, solemos trabajar con bigrams (secuencias de longitud 2), a veces incluso trigrams (longitud 3). Los n-grams de tamaño 1 son palabras sueltas, también llamadas unigrams. La razón para ceñirse a n 3 es que el número de n-gramas diferentes aumenta exponencialmente con respecto a n, mientras que sus frecuencias disminuyen del mismo modo. Con mucho, la mayoría de los trigramas sólo aparecen una vez en un corpus.

La siguiente función produce de forma elegante el conjunto de n-gramas de una secuencia de tokens:9

def ngrams(tokens, n=2, sep=' '):
    return [sep.join(ngram) for ngram in zip(*[tokens[i:] for i in range(n)])]

text = "the visible manifestation of the global climate change"
tokens = tokenize(text)
print("|".join(ngrams(tokens, 2)))

Out:

the visible|visible manifestation|manifestation of|of the|the global|
global climate|climate change

Como puedes ver, la mayoría de los bigramas contienen stop words como preposiciones y determinantes. Por tanto, es aconsejable construir bigramas sin palabras de parada. Pero debemos tener cuidado: si eliminamos primero las palabras de parada y luego construimos los bigramas, generaremos bigramas que no existen en el texto original como "manifestación global" en el ejemplo. Por tanto, creamos los bigramas en todos los tokens, pero conservamos sólo los que no contienen ninguna palabra vacía con esta función ngrams modificada:

def ngrams(tokens, n=2, sep=' ', stopwords=set()):
    return [sep.join(ngram) for ngram in zip(*[tokens[i:] for i in range(n)])
            if len([t for t in ngram if t in stopwords])==0]

print("Bigrams:", "|".join(ngrams(tokens, 2, stopwords=stopwords)))
print("Trigrams:", "|".join(ngrams(tokens, 3, stopwords=stopwords)))

Out:

Bigrams: visible manifestation|global climate|climate change
Trigrams: global climate change

Utilizando esta función ngrams, podemos añadir una columna que contenga todos los bigramas a nuestro DataFrame y aplicar el plano de recuento de palabras para determinar los cinco bigramas principales:

df['bigrams'] = df['text'].apply(prepare, pipeline=[str.lower, tokenize]) \
                          .apply(ngrams, n=2, stopwords=stopwords)

count_words(df, 'bigrams').head(5)

Out:

ficha frec
naciones unidas 103236
comunidad internacional 27786
asamblea general 27096
consejo de seguridad 20961
derechos humanos 19856

Te habrás dado cuenta de que hemos ignorado los límites de las frases durante la tokenización de . Así, generaremos bigramas sin sentido con la última palabra de una frase y la primera palabra de la siguiente. Esos bigramas no serán muy frecuentes, por lo que realmente no importan para la exploración de datos. Si quisiéramos evitarlo, tendríamos que identificar los límites de las frases, lo que es mucho más complicado que la tokenización de palabras y no merece la pena aquí.

Ahora vamos a ampliar nuestro análisis de unigramas basado en TF-IDF del apartado anterior e incluir los bigramas. Añadimos los valores IDF de los bigramas, calculamos las frecuencias de bigramas ponderadas por TF-IDF de todos los discursos de 2015 y generamos una nube de palabras a partir del resultado DataFrame:

# concatenate existing IDF DataFrame with bigram IDFs
idf_df = pd.concat([idf_df, compute_idf(df, 'bigrams', min_df=10)])

freq_df = count_words(df[df['year'] == 2015], 'bigrams')
freq_df['tfidf'] = freq_df['freq'] * idf_df['idf']
wordcloud(freq_df['tfidf'], title='all bigrams', max_words=50)

Como podemos ver en la nube de palabras de la izquierda de la Figura 1-7, el cambio climático fue un bigrama frecuente en 2015. Pero para comprender los distintos contextos del clima, puede ser interesante echar un vistazo a los bigramas que sólo contienen clima. Podemos utilizar un filtro de texto sobre clima para conseguirlo y trazar de nuevo el resultado como una nube de palabras(Figura 1-7, derecha):

where = freq_df.index.str.contains('climate')
wordcloud(freq_df[where]['freq'], title='"climate" bigrams', max_words=50)
Figura 1-7. Nubes de palabras para todos los bigramas y bigramas que contienen la palabra clima.

El enfoque presentado aquí crea y pondera todos los n-gramas que no contienen palabras de parada. Para un primer análisis, los resultados parecen bastante buenos. Simplemente no nos importa la larga cola de bigramas infrecuentes. Existen algoritmos más sofisticados, aunque también costosos computacionalmente, para identificar las colocaciones de , por ejemplo, en el buscador de colocaciones de NLTK. Nosotros mostraremos alternativas para identificar frases significativas en los Capítulos 4 y 10.

Plano: Comparación de frecuencias entre intervalos de tiempo y categorías

seguramente conoces Google Trends, donde puedes seguir la evolución de una serie de términos de búsqueda a lo largo del tiempo. Este tipo de análisis de tendencias calcula las frecuencias por día y las visualiza con un gráfico de líneas. Queremos seguir la evolución de determinadas palabras clave a lo largo de los años en nuestro conjunto de datos Debates de la ONU para hacernos una idea de la importancia creciente o decreciente de temas como el cambio climático, el terrorismo o la migración.

Crear plazos de frecuencia

Nuestro enfoque consiste en calcular las frecuencias de las palabras clave dadas por documento y luego agregar esas frecuencias utilizando la función groupby de Pandas. La siguiente función es para la primera tarea. Extrae los recuentos de las palabras clave dadas de una lista de tokens:

def count_keywords(tokens, keywords):
    tokens = [t for t in tokens if t in keywords]
    counter = Counter(tokens)
    return [counter.get(k, 0) for k in keywords]

Vamos a demostrar la funcionalidad con un pequeño ejemplo:

keywords = ['nuclear', 'terrorism', 'climate', 'freedom']
tokens = ['nuclear', 'climate', 'climate', 'freedom', 'climate', 'freedom']

print(count_keywords(tokens, keywords))

Out:

[1, 0, 3, 2]

Como puedes ver, la función devuelve una lista o vector de recuentos de palabras. De hecho, es un vectorizador de recuentos muy sencillo para palabras clave. Si aplicamos esta función a cada documento de nuestro DataFrame, obtendremos una matriz de recuentos. La función del plano count_keywords_by, que se muestra a continuación, hace exactamente esto como primer paso. A continuación, la matriz se convierte de nuevo en un DataFrame que finalmente se agrega y ordena por la columna de agrupación suministrada.

def count_keywords_by(df, by, keywords, column='tokens'):

    freq_matrix = df[column].apply(count_keywords, keywords=keywords)
    freq_df = pd.DataFrame.from_records(freq_matrix, columns=keywords)
    freq_df[by] = df[by] # copy the grouping column(s)

    return freq_df.groupby(by=by).sum().sort_values(by)

Esta función es muy rápida porque sólo tiene que ocuparse de las palabras clave. Contar las cuatro palabras clave anteriores del corpus de la ONU sólo lleva dos segundos en un ordenador portátil. Veamos el resultado:

freq_df = count_keywords_by(df, by='year', keywords=keywords)

Out:

nuclear terrorismo clima libertad año
1970 192 7 18 128
1971 275 9 35 205
... ... ... ... ...
2014 144 404 654 129
2015 246 378 662 148
Nota

Aunque en nuestros ejemplos sólo utilizamos el atributo year como criterio de agrupación, la función blueprint te permite comparar frecuencias de palabras a través de cualquier atributo discreto, por ejemplo, país, categoría, autor... lo que quieras. De hecho, incluso podrías especificar una lista de atributos de agrupación para calcular, por ejemplo, recuentos por país y año.

El DataFrame resultante ya está perfectamente preparado para el trazado, ya que tenemos una serie de datos por palabra clave. Utilizando la función plot de Pandas, obtenemos un bonito gráfico lineal similar al de Google Trends(Figura 1-8):

freq_df.plot(kind='line')
Figura 1-8. Frecuencias de palabras seleccionadas por año.

Fíjate en el pico nuclear de los años 80, que indica la carrera armamentística, y en el pico alto del terrorismo en 2001. Resulta en cierto modo sorprendente que el tema del clima ya recibiera cierta atención en los años 70 y 80. ¿De verdad? Bueno, si lo compruebas con un análisis KWIC ("Blueprint: Finding a Keyword-in-Context"), descubrirías que la palabra clima en esas décadas se utilizaba casi exclusivamente en un sentido figurado .

Crear mapas térmicos de frecuencia

Digamos que queremos analizar la evolución histórica de crisis mundiales como la guerra fría, el terrorismo y el cambio climático. Podríamos elegir una selección de palabras significativas y visualizar sus líneas temporales mediante gráficos de líneas, como en el ejemplo anterior. Pero los gráficos de líneas se vuelven confusos si tienen más de cuatro o cinco líneas. Una visualización alternativa de sin esa limitación es un mapa de calor, como el que proporciona la biblioteca Seaborn. Así pues, añadamos algunas palabras clave más a nuestro filtro y mostremos el resultado como un mapa de calor(Figura 1-9).

keywords = ['terrorism', 'terrorist', 'nuclear', 'war', 'oil',
            'syria', 'syrian', 'refugees', 'migration', 'peacekeeping',
            'humanitarian', 'climate', 'change', 'sustainable', 'sdgs']

freq_df = count_keywords_by(df, by='year', keywords=keywords)

# compute relative frequencies based on total number of tokens per year
freq_df = freq_df.div(df.groupby('year')['num_tokens'].sum(), axis=0)
# apply square root as sublinear filter for better contrast
freq_df = freq_df.apply(np.sqrt)

sns.heatmap(data=freq_df.T,
            xticklabels=True, yticklabels=True, cbar=False, cmap="Reds")
Figura 1-9. Frecuencias de palabras a lo largo del tiempo como mapa de calor.

Hay que tener en cuenta algunas cosas para este tipo de análisis :

Prefiere las frecuencias relativas para cualquier tipo de comparación.
Las frecuencias absolutas de términos de son problemáticas si el número total de tokens por año o categoría no es estable. Por ejemplo, en nuestro ejemplo, las frecuencias absolutas suben naturalmente si cada año hablan más países.
Ten cuidado con la interpretación de los diagramas de frecuencia basados en listas de palabras clave.
Aunque el gráfico parezca una distribución de temas, ¡no lo es! Puede haber otras palabras que representen el mismo tema pero que no estén incluidas en la lista. Las palabras clave también pueden tener significados diferentes (por ejemplo, "clima de la discusión"). Técnicas avanzadas como el modelado de temas(Capítulo 8) y la incrustación de palabras(Capítulo 10) pueden ayudar en este caso.
Utiliza la escala sublineal.
Como los valores de frecuencia de difieren mucho, puede ser difícil ver algún cambio para las fichas menos frecuentes. Por lo tanto, debes escalar las frecuencias de forma sublineal (hemos aplicado la raíz cuadrada np.sqrt). El efecto visual es similar a disminuyendo el contraste.

Observaciones finales

Demostramos en cómo empezar a analizar datos textuales. El proceso de preparación y tokenización del texto se mantuvo sencillo para obtener resultados rápidos. En el Capítulo 4, presentaremos métodos más sofisticados y discutiremos las ventajas y desventajas de los distintos enfoques.

La exploración de datos no sólo debe proporcionar una visión inicial, sino ayudar a desarrollar la confianza en tus datos. Una cosa que debes tener en cuenta es que siempre debes identificar la causa raíz de cualquier token extraño que aparezca. El análisis KWIC es una buena herramienta para buscar dichos tokens.

Para un primer análisis del contenido, introdujimos varios modelos de análisis de frecuencia de palabras. La ponderación de los términos se basa en la frecuencia de términos sola o en la combinación de la frecuencia de términos y la frecuencia inversa de documentos (TF-IDF). Estos conceptos se retomarán más adelante, en el Capítulo 5, porque la ponderación TF-IDF es un método estándar para vectorizar documentos para el aprendizaje automático.

Hay muchos aspectos del análisis textual que no hemos tratado en este capítulo:

  • La información relacionada con el autor puede ayudar a identificar a escritores influyentes, si ése es uno de los objetivos de tu proyecto. Los autores pueden distinguirse por su actividad, puntuación social, estilo de escritura, etc.
  • A veces es interesante comparar autores o corpus diferentes sobre el mismo tema por su legibilidad . La bibliotecatextacy tiene una función llamada textstats que calcula diferentes puntuaciones de legibilidad y otras estadísticas en una sola pasada sobre el texto.
  • Una herramienta interesante para identificar y visualizar en términos distintivos entre categorías (por ejemplo, partidos políticos) es la Scattertext de Jason Kessler.
  • Además del simple Python, también puedes utilizar herramientas visuales interactivas para el análisis de datos. PowerBI de Microsoft tiene un bonito complemento de nube de palabras y muchas otras opciones para producir gráficos interactivos. Lo mencionamos porque es de uso gratuito en la versión de escritorio y admite Python y R para la preparación y visualización de datos.
  • Para proyectos más grandes, recomendamos configurar un motor de búsqueda como Apache SOLR, Elasticsearch o Tantivy. Estas plataformas crean índices especializados (también utilizando la ponderación TF-IDF) para realizar búsquedas rápidas de texto completo. Hay API de Python disponibles para todas ellas.

1 Consulta la documentación de Pandas para obtener una lista completa.

2 Puedes dirigirte a la lista de spaCy de forma similar con spacy.lang.en.STOP_WORDS.

3 Consulta la documentación para más detalles.

4 La clase NLTK FreqDist deriva de Counter y añade algunas funciones prácticas.

5 Ten en cuenta que el módulo wordcloud ignora la lista de palabras de parada si se llama a generate_from_frequencies. Por lo tanto, aplicamos un filtro adicional.

6 Por ejemplo, TfIdfVectorizer de scikit-learn añade +1.

7 Otra opción es añadir +1 en el denominador para evitar una división por cero de los términos no vistos con df(t) = 0. Esta técnica se denomina suavizado.

8 El parámetro print_only de la función KWIC de textacy funciona de forma similar, pero no muestrea.

9 Véase la entrada del blog de Scott Triglia para una explicación.

Get Planos para el análisis de textos con Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.