La segunda temporada de EsotéricoUna serie de éxito reciente en Netflix basada en el universo de uno de los videojuegos en línea más populares de todos los tiempos, League of Legends, se desarrolla en un mundo de fantasía con un pesado diseño steampunk, cerrado con impresionantes imágenes y un presupuesto récord. Como buen científico de redes y datos con un interés particular en transformar elementos de la cultura pop en visualización de datos, esto era todo lo que necesitaba después de terminar la temporada final para mapear conexiones ocultas y transformar el escenario Arcano en una visualización de red, usando Python. Entonces, al final de este tutorial, tendrás habilidades prácticas sobre cómo crear y visualizar la red detrás de Arcane.
Sin embargo, estas habilidades y métodos no son en absoluto específicos de esta historia. De hecho, destacan el enfoque general propuesto por la ciencia de redes para mapear, diseñar, visualizar e interpretar las redes de cualquier sistema complejo. Estos sistemas pueden variar desde modelos de redes de transporte y propagación de COVID-19 hasta redes cerebrales y varias redes sociales, como la de la serie Arcane.
Todas las imágenes creadas por el autor.
Dado que aquí vamos a rastrear las conexiones detrás de todos los personajes, primero necesitamos obtener una lista de cada personaje. Para ello, el Arcano wiki de fans El sitio es una excelente fuente de información gratuita (CC BY-SA 3.0), a la que podemos acceder fácilmente mediante técnicas sencillas de web scraping. Es decir, usaremos urllib para descargar, y con BeautifulSoup extraeremos los nombres y las URL del perfil wiki de fans de cada personaje que figura en la página del personaje principal.
Primero descargue el código HTML del sitio de listado de personajes:
import urllib
import bs4 as bs
from urllib.request import urlopenurl_char = 'https://arcane.fandom.com/wiki/Category:Characters'
sauce = urlopen(url_char).read()
soup = bs.BeautifulSoup(sauce,'lxml')
Luego extraje todos los nombres potencialmente relevantes. Se puede determinar fácilmente qué etiquetas introducir en el código HTML analizado almacenado en la variable ‘sopa’ simplemente haciendo clic derecho en el elemento deseado (en este caso, un perfil de personaje) y seleccionando la opción de inspección en cualquier navegador.
Desde aquí aprendí que el nombre y la URL de un personaje se almacenan en una línea que contiene ‘título=’, pero no contiene ‘:’ (que corresponde a categorías). Además, creé una bandera still_character, que me ayudó a decidir qué subpáginas de la página de lista de personajes todavía pertenecen a los personajes legítimos de la historia.
import rechars = soup.find_all('li')
still_character = True
names_urls = {}
for char in chars:
if '" title="' in str(char) and ':' not in char.text and still_character:
char_name = char.text.strip().rstrip()
if char_name == 'Arcane':
still_character = False
char_url = ' + re.search(r'href="([^"]+)"', str(char)).group(1)
if still_character:
names_urls[char_name] = char_url
El bloque de código anterior creará un diccionario (“url_names”) que almacena el nombre y la URL de cada carácter como pares clave-valor. Ahora echemos un vistazo rápido a lo que tenemos e imprimamos el diccionario nombre-url y su longitud total:
for name, url in names_urls.items():
print(name, url)
Una muestra del resultado de este bloque de código, donde podemos enviar texto a cada enlace, apuntando al perfil biográfico de cada personaje:
print(len(names_urls))
Qué celda de código devuelve el resultado de 67, lo que implica el número total de caracteres con nombre que debemos procesar. Esto significa que ya hemos completado la primera tarea: tenemos una lista completa de personajes, así como fácil acceso a su perfil de texto completo en sus sitios wiki de fans.
Para mapear las conexiones entre dos personajes, encontramos una manera de cuantificar la relación entre cada uno de los dos personajes. Para captar esto, me baso en la frecuencia con la que las biografías de los dos personajes se refieren entre sí. Desde un punto de vista técnico, para lograr esto necesitaremos reunir estas biografías completas a las que acabamos de recibir los enlaces. Lo lograremos nuevamente utilizando técnicas simples de raspado web y luego guardaremos la fuente de cada sitio localmente en un archivo separado de la siguiente manera.
# output folder for the profile htmls
import os
folderout = 'fandom_profiles'
if not os.path.exists(folderout):
os.makedirs(folderout)# crawl and save the profile htmls
for ind, (name, url) in enumerate(names_urls.items()):
if not os.path.exists(folderout + '/' + name + '.html'):
fout = open(folderout + '/' + name + '.html', "w")
fout.write(str(urlopen(url).read()))
fout.close()
Al final de esta sección, nuestra carpeta «fandom_profiles» debe contener los perfiles fanwiki de cada personaje de Arcane, listos para ser procesados a medida que avanzamos hacia la construcción de la red Arcane.
Para construir la red entre personajes, asumimos que la intensidad de las interacciones entre dos personajes está indicada por el número de veces que el perfil de cada personaje menciona al otro. Por lo tanto, los nodos de esta red son los personajes, que están vinculados por conexiones de diferente intensidad dependiendo del número de veces que la fuente del sitio wiki de cada personaje hace referencia a la wiki de cualquier otro personaje.
Construye la red
En el siguiente bloque de código, construimos la lista de bordes: la lista de conexiones que contiene tanto el nodo de origen como el nodo de destino (carácter) de cada conexión, así como el peso (frecuencia de correferencia) entre los dos caracteres. Además, para buscar el perfil de manera eficiente, creo un name_ids que contiene solo la identificación específica de cada personaje, sin el resto de la dirección web.
# extract the name mentions from the html sources
# and build the list of edges in a dictionary
edges = {}
names_ids = {n : u.split('/')[-1] for n, u in names_urls.items()}for fn in [fn for fn in os.listdir(folderout) if '.html' in fn]:
name = fn.split('.html')[0]
with open(folderout + '/' + fn) as myfile:
text = myfile.read()
soup = bs.BeautifulSoup(text,'lxml')
text = ' '.join([str(a) for a in soup.find_all('p')[2:]])
soup = bs.BeautifulSoup(text,'lxml')
for n, i in names_ids.items():
w = text.split('Image Gallery')[0].count('/' + i)
if w>0:
edge = '\t'.join(sorted([name, n]))
if edge not in edges:
edges[edge] = w
else:
edges[edge] += w
len(edges)
Al ejecutar este bloque de código, debería devolver alrededor de 180 aristas.
A continuación, utilizamos la biblioteca de análisis de gráficos NetworkX para transformar la lista de aristas en un objeto de gráfico y mostrar la cantidad de nodos y aristas en el gráfico:
# create the networkx graph from the dict of edges
import networkx as nx
G = nx.Graph()
for e, w in edges.items():
if w>0:
e1, e2 = e.split('\t')
G.add_edge(e1, e2, weight=w)G.remove_edges_from(nx.selfloop_edges(G))
print('Number of nodes: ', G.number_of_nodes())
print('Number of edges: ', G.number_of_edges())
La salida de este bloque de código:
Este resultado nos dice que aunque comenzamos con 67 caracteres, 16 de ellos terminaron sin estar conectados a nadie en la red, de ahí la menor cantidad de nodos en el gráfico construido.
Ver la red
Una vez que tengamos la red, ¡podremos visualizarla! Primero, creemos un borrador simple de visualización de red usando Matplotlib y las herramientas integradas de NetworkX.
# take a very brief look at the network
import matplotlib.pyplot as plt
f, ax = plt.subplots(1,1,figsize=(15,15))
nx.draw(G, ax=ax, with_labels=True)
plt.savefig('test.png')
La imagen de salida de esta celda:
Aunque esta red ya brinda una idea de la estructura principal y las características más frecuentes del programa, podemos diseñar una visualización mucho más detallada utilizando el software de visualización de red de código abierto Gephi. Para hacer esto, primero necesitamos exportar la red a un archivo de datos gráficos .gexf, de la siguiente manera.
nx.write_gexf(G, 'arcane_network.gexf')
Ahora el tutorial sobre cómo visualizar esta red usando Gephi:
Suplementos
Aquí hay una parte de extensión, a la que hago referencia en el video. Después de exportar la tabla de nodos, incluidos los índices de comunidades de red, leí esta tabla usando Pandas y asigné colores individuales a cada comunidad. Obtuve los colores (y sus códigos hexadecimales) de ChatGPT, indicándole que se alineara con los temas de colores principales del programa. Luego, este bloque de código exporta el color, que usé nuevamente en Gephi para colorear el gráfico final.
import pandas as pd
nodes = pd.read_csv('nodes.csv')pink = '#FF4081'
blue = '#00FFFF'
gold = '#FFD700'
silver = '#C0C0C0'
green = '#39FF14'
cmap = {0 : green,
1 : pink,
2 : gold,
3 : blue,
}
nodes['color'] = nodes.modularity_class.map(cmap)
nodes.set_index('Id')[['color']].to_csv('arcane_colors.csv')
Al colorear la red según las comunidades que encontramos (comunidades significa subgrafos altamente interconectados de la red original), descubrimos cuatro grupos principales, cada uno correspondiente a conjuntos específicos de personajes dentro de la historia. Como era de esperar, el algoritmo agrupó a la familia de los protagonistas principales con Jinx, Vi y Vander (rosa). Luego vemos también el grupo de figuras clandestinas de Zaun (azul), como Silco, mientras que la élite de Piltover (azul) y las fuerzas militaristas (verde) también están bien agrupadas.
La belleza y utilidad de tales estructuras comunitarias es que, si bien tales explicaciones las ponen muy fácilmente en contexto, generalmente sería muy difícil elaborar un mapa similar basado únicamente en la intuición. Si bien la metodología presentada aquí muestra claramente cómo podemos utilizar la ciencia de redes para extraer las conexiones ocultas de sistemas sociales virtuales (o reales), ya sean socios de una firma de abogados, colegas de una firma de contabilidad y del departamento de recursos humanos. de una importante compañía petrolera.