Tras el éxito de Dune tanto de taquilla como de crítica en 2021, Dune: Part Two fue una de las películas más esperadas de 2024 y no decepcionó. En camino de ganar más y con calificaciones más altas tanto en Rotten Tomatoes como en iMDB que su precuela al momento de escribir este artículo, con su panorama político en constante cambio, Dune es la franquicia perfecta para sumergirse a través de la ciencia de las redes. En este breve artículo, nuestro objetivo era explorar las conexiones entre las diferentes Casas y personas del Impremium basándose en los tres primeros libros de Frank Herbert: Dune (1965), Dune Messiah (1969) y Children of Dune (1976).
En la primera parte de este artículo, presentamos un enfoque basado en Python para recopilar datos de perfil de personaje del Wiki Dunas y convierta esos perfiles en un atractivo gráfico de red. Luego, en la segunda sección, bastante llena de spoilers, nos sumergimos en las profundidades de la red y extraemos todas las historias que tiene que decir sobre la primera trilogía de Dune.
Todas las imágenes fueron creadas por los autores.
Primero, usamos Python para recopilar la lista completa de personajes de Dune. Luego, descargamos sus perfiles biográficos del sitio wiki de fans de cada personaje y contamos el número de veces que la historia de cada personaje menciona la historia de cualquier otro personaje, asumiendo que estas menciones codifican varias interacciones entre dos pares de personajes cualesquiera. Luego, usaremos la ciencia de redes para convertir estas relaciones en un gráfico complejo.
1.1 Recopilación de la lista de personajes.
En primer lugar, recopilamos la lista de todos los personajes relevantes del sitio wiki de fans de Dune. Es decir, utilizamos urllib y bs4 para extraer los nombres y los ID de fan wiki de cada personaje mencionado y tenemos su propia página wiki codificada por su ID. Hicimos esto para los primeros tres libros: Dune, Dune Messiah y Childen of Dune. Estos tres libros cubren el surgimiento de la casa de Atreides.
Fuentes:
Primero, descargue el html del sitio de listado de personajes:
dune_meta = {
'Dune': {'url': 'https://dune.fandom.com/wiki/Dune_(novel)'},
'Dune Messiah': {'url': 'https://dune.fandom.com/wiki/Dune_Messiah'},
'Children of Dune': {'url': 'https://dune.fandom.com/wiki/Children_of_Dune_(novel)'}
}for book, url in dune_meta.items():
sauce = urlopen(url['url']).read()
soup = bs.BeautifulSoup(sauce,'lxml')
dune_meta[book]['chars'] = soup.find_all('li')
Una pequeña ayuda manual para ajustar el nombre y la identificación del personaje:
dune_meta['Dune']['char_start'] = 'Abulurd'
dune_meta['Dune']['char_end'] = 'Arrakis'
dune_meta['Dune Messiah']['char_start'] = 'Abumojandis'
dune_meta['Dune Messiah']['char_end'] = 'Arrakis'
dune_meta['Children of Dune']['char_start'] = '2018 Edition'
dune_meta['Children of Dune']['char_end'] = 'Categories'
Luego, extrajimos todos los nombres potencialmente relevantes y las URL de perfil correspondientes. Aquí, verificamos manualmente desde qué bloques de etiquetas comienzan los nombres (por ejemplo, en contraposición al esquema del sitio de listado de personajes). Además, decidimos eliminar los personajes marcados con ‘XD’ y ‘DE’ correspondientes a la serie extendida, así como los personajes que fueron “Sólo mencionados” en un libro determinado:
for k, v in dune_meta.items():
names_urls = {}
keep_row = False
print(f'----- {k} -----')
for char in v['chars']:
if v['char_start'] in char.text.strip():
keep_row = True
if v['char_end'] in char.text.strip():
keep_row = False
if keep_row and 'Video' not in char.text:
try:
url = 'https://dune.fandom.com' + str(char).split('href="')[1].split('" title')[0]
name = char.text.strip()
if 'wiki' in url and 'XD' not in name and 'DE' not in name and '(Mentioned only)' not in name:
names_urls[name] = url
print(name)
except:
pass
dune_meta[k]['names_urls'] = names_urls
Este bloque de código luego genera la lista de caracteres, como por ejemplo:
Finalmente, verificamos la cantidad de caracteres que recopilamos y guardamos las URL e identificadores de su perfil para el siguiente subcapítulo.
dune_names_urls = {}
for k, v in dune_meta.items():
dune_names_urls.update(dune_meta[k]['names_urls'])names_ids = {n : u.split('/')[-1] for n, u in dune_names_urls.items()}
print(len(dune_names_urls))
Los resultados de esta celda, que muestran 119 caracteres con URL de perfil:
1.2 Descarga de perfiles de personajes
Nuestro objetivo es trazar la red social de los personajes de Dune, lo que significa que debemos descubrir quién interactuó con quién. En el subcapítulo anterior, obtuvimos la lista de todos los ‘quiénes’ y ahora obtendremos información sobre sus historias personales. Obtendremos esas historias usando nuevamente técnicas simples de raspado web y luego guardaremos la fuente del sitio personal de cada personaje en un archivo separado localmente:
# output folder for the profile htmls
folderout = 'fandom_profiles'
if not os.path.exists(folderout):
os.makedirs(folderout)# crawl and save the profile htmls
for ind, (name, url) in enumerate(dune_names_urls.items()):
if not os.path.exists(folderout + '/' + name + '.html'):
try:
fout = open(folderout + '/' + name + '.html', "w")
fout.write(str(urlopen(url).read()))
except:
pass
El resultado de ejecutar este código será una carpeta en nuestro directorio local con todos los perfiles del sitio wiki de fans que pertenecen a cada personaje seleccionado.
1.3 Construyendo la red
Para construir la red entre personajes, contamos el número de veces que la fuente del sitio wiki de cada personaje hace referencia al identificador wiki de cualquier otro personaje usando la siguiente lógica. Aquí, construimos la lista de bordes: la lista de conexiones que contienen tanto el nodo (carácter) de origen como el de destino de las conexiones, así como el peso (frecuencia de correferencia) entre las páginas de los dos personajes.
# extract the name mentions from the html sources
# and build the list of edges in a dictionary
edges = {}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)
Una vez que ejecutemos este bloque de código, obtendremos el resultado de 307 como el número de bordes que conectan los 119 personajes de Dune.
A continuación, utilizamos la biblioteca de análisis de gráficos NetworkX para convertir la lista de bordes en un objeto de gráfico y generar la cantidad de nodos y bordes que tiene 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())
El resultado de este bloque de código:
El número de nodos es sólo 72, lo que significa que 47 caracteres no estaban vinculados a ningún miembro central en sus perfiles wiki, probablemente bastante breves. Además, vemos una disminución de cuatro en el número de bordes porque también se eliminaron algunos bucles automáticos.
Echemos un vistazo breve a la red utilizando el trazador Matplotlib incorporado:
# 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)
La salida de esta celda:
Si bien este objeto visual ya muestra cierta estructura de red, exportamos el gráfico a un archivo Gephi usando la siguiente línea de código y diseñamos la red adjunta en la figura siguiente (el procedimiento para dichos objetos visuales de red será el tema de un próximo tutorial). artículo):
nx.write_gexf(G, 'dune_network.gexf')
La red Dune completa: