Modelado de temas de investigación de código abierto con la API OpenAlex | por Alex Davis | Jul, 2024

Mientras ingerimos los datos de la API, aplicaremos algunos criterios. En primer lugar, solo ingeriremos documentos cuyo año esté entre 2016 y 2022. Queremos un lenguaje bastante reciente, ya que los términos y la taxonomía de ciertos temas pueden cambiar en largos períodos de tiempo.

También agregaremos términos clave y realizaremos múltiples búsquedas. Si bien normalmente ingeriríamos áreas temáticas aleatorias, usaremos términos clave para limitar nuestra búsqueda. De esta manera, tendremos una idea de cuántos temas de alto nivel tenemos y podremos compararlos con el resultado del modelo. A continuación, creamos una función donde podemos agregar términos clave y realizar búsquedas a través de la API.

import pandas as pd
import requests
def import_data(pages, start_year, end_year, search_terms):

"""
This function is used to use the OpenAlex API, conduct a search on works, a return a dataframe with associated works.

Inputs:
- pages: int, number of pages to loop through
- search_terms: str, keywords to search for (must be formatted according to OpenAlex standards)
- start_year and end_year: int, years to set as a range for filtering works
"""

#create an empty dataframe
search_results = pd.DataFrame()

for page in range(1, pages):

#use paramters to conduct request and format to a dataframe
response = requests.get(f'https://api.openalex.org/works?page={page}&per-page=200&filter=publication_year:{start_year}-{end_year},type:article&search={search_terms}')
data = pd.DataFrame(response.json()['results'])

#append to empty dataframe
search_results = pd.concat([search_results, data])

#subset to relevant features
search_results = search_results[["id", "title", "display_name", "publication_year", "publication_date",
"type", "countries_distinct_count","institutions_distinct_count",
"has_fulltext", "cited_by_count", "keywords", "referenced_works_count", "abstract_inverted_index"]]

return(search_results)

Realizamos 5 búsquedas diferentes, cada una de ellas en un área tecnológica diferente. Estas áreas tecnológicas están inspiradas en las “Áreas tecnológicas críticas” del Departamento de Defensa. Vea más aquí:

A continuación se muestra un ejemplo de una búsqueda que utiliza la sintaxis OpenAlex requerida:

#search for Trusted AI and Autonomy
ai_search = import_data(35, 2016, 2024, "'artificial intelligence' OR 'deep learn' OR 'neural net' OR 'autonomous' OR drone")

Después de compilar nuestras búsquedas y eliminar los documentos duplicados, debemos limpiar los datos para prepararlos para nuestro modelo de temas. Hay dos problemas principales con nuestro resultado actual.

  1. Los resúmenes se devuelven como un índice invertido (por razones legales). Sin embargo, podemos utilizarlos para devolver el texto original.
  2. Una vez que obtengamos el texto original, estará crudo y sin procesar, lo que generará ruido y dañará nuestro modelo. Realizaremos un preprocesamiento de PNL tradicional para prepararlo para el modelo.

A continuación se muestra una función para devolver el texto original de un índice invertido.

def undo_inverted_index(inverted_index):

"""
The purpose of the function is to 'undo' and inverted index. It inputs an inverted index and
returns the original string.
"""

#create empty lists to store uninverted index
word_index = []
words_unindexed = []

#loop through index and return key-value pairs
for k,v in inverted_index.items():
for index in v: word_index.append([k,index])

#sort by the index
word_index = sorted(word_index, key = lambda x : x[1])

#join only the values and flatten
for pair in word_index:
words_unindexed.append(pair[0])
words_unindexed = ' '.join(words_unindexed)

return(words_unindexed)

Ahora que tenemos el texto sin procesar, podemos realizar nuestros pasos de preprocesamiento tradicionales, como estandarización, eliminación de palabras vacías, lematización, etc. A continuación, se presentan funciones que se pueden asignar a una lista o serie de documentos.

def preprocess(text):

"""
This function takes in a string, coverts it to lowercase, cleans
it (remove special character and numbers), and tokenizes it.
"""

#convert to lowercase
text = text.lower()

#remove special character and digits
text = re.sub(r'\d+', '', text)
text = re.sub(r'[^\w\s]', '', text)

#tokenize
tokens = nltk.word_tokenize(text)

return(tokens)

def remove_stopwords(tokens):

"""
This function takes in a list of tokens (from the 'preprocess' function) and
removes a list of stopwords. Custom stopwords can be added to the 'custom_stopwords' list.
"""

#set default and custom stopwords
stop_words = nltk.corpus.stopwords.words('english')
custom_stopwords = []
stop_words.extend(custom_stopwords)

#filter out stopwords
filtered_tokens = [word for word in tokens if word not in stop_words]

return(filtered_tokens)

def lemmatize(tokens):

"""
This function conducts lemmatization on a list of tokens (from the 'remove_stopwords' function).
This shortens each word down to its root form to improve modeling results.
"""

#initalize lemmatizer and lemmatize
lemmatizer = nltk.WordNetLemmatizer()
lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]

return(lemmatized_tokens)

def clean_text(text):

"""
This function uses the previously defined functions to take a string and\
run it through the entire data preprocessing process.
"""

#clean, tokenize, and lemmatize a string
tokens = preprocess(text)
filtered_tokens = remove_stopwords(tokens)
lemmatized_tokens = lemmatize(filtered_tokens)
clean_text = ' '.join(lemmatized_tokens)

return(clean_text)

Ahora que tenemos una serie de documentos preprocesados, ¡podemos crear nuestro primer modelo de tema!