Más sobre ciencia de datos: cienciadedatos.net
- Árboles de decisión con Python: regresión y clasificación
- Gradient Boosting con Python
- Machine learning con Python y Scikit-learn
- Grid search de modelos Random Forest con out-of-bag error y early stopping
Introducción
Una de las últimas tendencias en el campo de la inteligencia artificial es el Graph Machine Learning (GML). Esta disciplina se centra en aplicar algoritmos de machine learning y estadística al estudio de redes o grafos complejos.
Un grafo es una estructura de datos formada por nodos y enlaces que permite representar relaciones entre objetos. A modo de ejemplo, en una red social, los nodos podrían representar a los usuarios y los enlaces la conexión entre ellos.
El GML utiliza grafos para entrenar modelos de aprendizaje automático y poder hacer inferencia sobre los nodos o enlaces. Los tres tipos principales de problemas que se pueden resolver con GML son:
Inferencia sobre nodos: predecir características de los nodos utilizando las características de sus nodos vecinos.
Inferencia sobre enlaces: predecir carteristicas o exitencia de enlaces, por ejemplo, inferir qué enlaces tiene mayor probabilidad de ocurrir en un futuro. Este es un caso típico de los sistemas de recomendación.
Inferencia sobre conjuntos de nodos: identificar comunidades de nodos que presentan comportamientos similares. Este es un caso típico en segmentación de clientes con características similares.
En esta serie de artículos se pretende mostrar las bases del Graph Machine Learning y resolver casos prácticos utilizando las librerías de grafos más importantes de Python como NetworkX PyTorch Geometric, Stellargraph o Graph Neural Networks.

Tipos de grafos
Los grafos se clasifican en base a sus nodos y los enlaces. Se pueden diferenciar varios tipos de grafos dependiendo del tipo de relación que existe entre sus nodos.
Grafos no dirigidos
Los enlaces no tienen dirección, es decir, si un nodo está conectado a un nodo , entonces también está conectado a . Un ejemplo de grafo no dirigido es una red social donde los usuarios están conectados entre sí a través de vínculos de amistad.

Grafos dirigidos
Los enlaces tienen una dirección, es decir, si un nodo está conectado a un nodo , no está necesariamente conectado a . Un ejemplo de un grafo dirigido es una red de llamadas telefónicas donde los nodos son personas y los ejes muestran cada llamada, teniendo en cuenta quién llama a quién.

**Grafos ponderados** Los enlaces tienen un peso asociado, que representa la importancia o la intensidad de la relación entre los nodos. Un ejemplo de grafo ponderado es una red de ciudades donde los enlaces representan las rutas de transporte y el peso representa la distancia entre ciudades.

Grafos bipartitos
Los nodos se dividen en dos conjuntos disjuntos (dos tipos de nodos), y solo se permiten enlaces entre nodos de diferentes conjuntos. Un ejemplo de grafo bipartito es una red de películas y actores, donde los nodos de un conjunto son las películas y los nodos del otro conjunto son los actores; y solo se permiten enlaces entre películas y actores.

Matriz de adyacencia
Para poder analizar un grafo es neceario representarlo de forma matemática. Existen dos formas principales de hacerlo: la lista de ejes (edgelist) y la matriz de adyacencia.
La lista de ejes es simplemente una lista en la que se indican todas las conexiones. Por ejemplo, en un grafo dirigido con tres nodos {, y }, donde se conecta con y , la lista de ejes es {(,), (,)}. Si el grafo es no dirigido, la lista debe incluir las conexiones en ambas direcciones {(,), (,) (,), (,)}.
Otra forma de representar un grafo es mediante lo que se conoce como matriz de adyacencia. La matriz de adyacencia es una matriz de dimensión , siendo el número de nodos y donde aparece un 1 si la conexión entre un par de nodos existe y un 0 de lo contrario. Para grafos ponderados, en vez de un 1, la matriz presenta el valor del peso de la conexión. Debido a sus propiedades matemáticas, la matriz de adyacencia es el método de representación de grafos más utilizado.
La desventaja de la matriz de adyacencia, es que para grafos muy grandes, puede ocupar mucho espacio. Por este motivo, se utiliza con frecuencia una matriz de adyacencia sparse, que internamente únicamente almacena la información de las conexiones existentes, es decir, no almacena los ceros.
Para grafos no dirigidos la matriz de adyacencia es simétrica ya que, si existe la conexión entre los nodos y , también existirá la conexión recíproca.

Con una formulación matetemática, la matriz de adyacencia de un grafo dirigido de nodos tiene filas y columnas, siendo sus elementos:
si existe un enlace que apunta desde el nodo al nodo
si los nodos y no están conectados entre sí
La matriz de adyacencia de una red no dirigida tiene dos entradas para cada enlace. El enlace se representa como y . Por lo tanto, la matriz de adyacencia de una red no dirigida es simétrica,
En la siguiente figura se representan los distintos tipos de grafos junto con sus matrices de adyacencia. Como se puede observar, los grafos dirigidos () son los únicos que no tienen una matriz de adyacencia simétrica. En el caso de los grafos bipartitos (), se pueden proyectar para obtener las conexiones indirectas entre cada tipo de nodo (), esto último se verá en profundidad más adelante.

Ejemplo de grafos de distintos tipos con sus matrices de adyacencia correspondientes. A) Grafo no dirigido. B) Grafo dirigido. C) Grafo ponderado. D) Grafo bipartito, con dos tipos de nodos (verde y naranja). E) Proyección del grafo bipartito en los nodos de cada tipo. Fuente: wikipedia.
Meta-información de nodos y ejes
Cuando se trabaja con grafos, especialmente si se quieren hacer predicciones u otro tipo de analítica, es muy común que se disponga de información adicional de los nodos y enlaces. Esta información se denomina meta-información porque no pertenece estrictamente al grafo y se suele almacenar a modo de tabla, cuyas filas son los nodos o enlaces y sus columnas son las variables disponibles de cada uno. Si se utiliza el ejemplo de la red social, la meta-información de los nodos podría ser el nombre, edad, aficiones, etc...
Aunque menos común que para los nodos, los enlaces también pueden tener meta-información. Continuando con el ejmplo de la red social, la meta-información de los ejes podría ser el año en el que se inició la relación de amistad entre dos personas.
Grafos bipartitos
Un grafo bipartito es una red cuyos nodos se pueden dividir en dos conjuntos disjuntos y de tal manera que los enlaces conectan nodos de con nodos de . En otras palabras, si se colorean los nodos de verde y los nodos de morado, unicamente existen enlaces que conecten nodos de diferentes colores.

Es posible generar dos proyecciones para cada red bipartita. La primera proyección conecta nodos si estos están conectados con el mismo nodo en la representación bipartita. La segunda proyección conecta nodos si estos están conectados con el mismo nodo en la representación bipartita.
Volviendo al ejemplo anterior de una red bipartita, en la que un conjunto de nodos corresponde a las películas () y el otro a los actores (), y cada película está conectada a los actores que actúan en ella; la primera proyección de esta red conecta los nodos de actores sí estos han actuado en la misma película. La otra proyección, conecta las películas que comparten al menos un actor en su reparto.
Caminos y distancias
La distancia física desempeña un papel clave en las interacciones entre los componentes de los sistemas. Por ejemplo, la distancia física entre dos ciudades influye al número de visitantes que viajan de una ciudad a otra.
En las redes, la distancia es un concepto diferente a la distancia física. Si se plantea la pregunta: ¿Cuál es la distancia entre dos usuarios de una red social? La distancia física deja de ser relevante porque dos individuos que viven en el mismo edificio pueden no conocerse y tener muy buenos amigos en otras partes del mundo.
En las redes, la distancia física se reemplaza por la longitud del camino. Un camino entre dos nodos es una ruta que se inicia en el primer nodo y recorre los enlaces de la red pasando por distintos nodos hasta llegar al segundo nodo. La longitud del camino representa el número de enlaces que contiene el camino entre dos nodos.
En la ciencia de las redes, los caminos desempeñan un papel central. A continuación, se describen algunas de sus propiedades más importantes:
Reciprocidad
En una red no dirigida, la distancia entre el nodo y el nodo es la misma que la distancia entre el nodo y el nodo ( = ). Por lo contrario, en una red dirigida, a menudo . Además, en una red dirigida, la existencia de un camino desde el nodo al nodo no garantiza la existencia de un camino desde a .
Camino más corto entre dos nodos
En las redes determinar la distancia entre dos nodos equivale a identificar el camino más corto entre dichos nodos. La distancia (o camino más corto) entre dos nodos y se puede calcular directamente a partir de la matriz de adyacencia . Basta con multiplicar la matriz de adyacencia por sí misma tantas veces como distancia entre dos nodos.
: Si hay un enlace directo entre y , entonces ( de lo contrario
: Si hay un camino de longitud 2 entre y , entonces de lo contrario). La cantidad de caminos entre y es
La cantidad de caminos de longitud entre y es
Estas ecuaciones son válidas tanto para redes dirigidas como no dirigidas.
En otras palabras, para obtener la distancia de orden 2 entre cualquier par de nodos, basta con multiplicar la matriz de adyacencia por sí misma. Si la multiplicamos por sí misma n veces, obtenemos la matriz de distancias de orden n.
Así, la matriz de distancia de orden 2 nos mostrará todos los pares de nodos que están conectados con un nodo intermedio. En el siguiente ejemplo, los nodos 2 y 5 tienen entre medias los nodos 4 y 1, es decir, existen dos caminos posibles de orden 2 que los conectan: el 2-4-5 y el 2-1-5. Por este motivo, si se multiplica la matriz de adyacencia por sí misma, se obtiene que el elemento [2,5] de la matriz de distancias es 2. Lo que quiere decir que existen dos caminos de distancia 2 entre los nodos 2 y 5.

Propiedades de redes y nodos
Las siguientes son algunas propiedades importantes de las redes y nodos en teoría de grafos. En artículos sucesivos se explicarán con más detalle, incluyendo su formulación e implicaciones matemáticas.
- Grado de un nodo: el número de enlaces que tiene un nodo. Los grafos dirigidos presentan dos tipos de grado "in degree" para conexiones entrantes y "out degree" para conexiones salientes.
- Vecindario de un nodo: los nodos a los que está conectado directamente un nodo.
- Grado medio: el promedio del grado de todos los nodos en una red.
- Densidad: el número de enlaces en una red dividido por el número máximo de enlaces posibles. La densidad proporciona una idea de qúan poblada de enlaces está la red.
- Componentes conectadas: grupos de nodos conectados entre sí. No tienen porqué existir todas la conexiones, pero sí que desde cualquier nodo puedas viajar al resto de nodos.
- Caminos y ciclos: un camino es una secuencia de enlaces que conectan dos nodos, mientras que un ciclo es un camino que comienza y termina en el mismo nodo.
- Centralidad: medida de la importancia de un nodo en una red, según su grado, vecindario, caminos y ciclos, entre otros factores.
- Clustering: medida de la cantidad de enlaces existentes entre los vecinos de un nodo.
- Cliqué: conjunto de nodos todos conectados con todos.
- Sub-grafo: la red resultante al seleccionar únicamente una serie de nodos.
Centralidad de los nodos
La centralidad es un concepto utilizado para medir la importancia de los nodos en un grafo. Las tres medidas de centralidad más importantes son:
- Centralidad de grado: es la medida más simple, se basa en el número de enlaces que tiene un nodo, la suma de enlaces que entran y salen.
- Centralidad de intermediación: mide la cantidad de caminos que pasan por un nodo.
- Centralidad de cercanía: mide la distancia promedio desde un nodo a todos los demás nodos del grafo.
Para conocer más detalle de las propiedades de los grafos, puedes visitar el siguiente capítulo de la serie XX.
Grafos con NetworkX
NetworkX es una de las librerías de Python más utilizadas para trabajar con grafos y redes. NetworkX permite crear, manipular y analizar grafos de manera eficiente.
Una de las principales ventajas de NetworkX es su capacidad para trabajar con grafos de gran tamaño y complejidad, permitiendo manejar grafos con millones de nodos y enlaces. La librería cuenta con una gran variedad de funcionalidades que permiten crear, importar y exportar grafos en múltiples formatos, así como analizar las propiedades de estas redes (grado medio, la densidad, el coeficiente de clustering, el camino más corto entre dos nodos, y muchas otras más). Además, cuenta con una serie de algoritmos para buscar patrones, como la detección de comunidades, detección de centralidad y detección de componentes conectados. Todas estas propiedades se irán viendo en artículos sucesivos.
NetworkX también es compatible con otras librerías de Python, como NumPy, SciPy, Matplotlib o Pytorch, lo que permite integrar fácilmente el análisis de redes en un flujo de trabajo de análisis de datos más amplio.
Funciones importantes de NetworkX
A continuación, se muestra un listado con algunas de las funciones más utilizadas de NetworkX.
- Creación de grafos: NetworkX permite crear grafos vacíos o con nodos y enlaces específicos utilizando funciones como
Graph()(grafos no dirigidos),DiGraph()(grafos dirigidos), o incluso para conjuntos de grafos conMultiGraph()yMultiDiGraph().
- Agregar nodos y enlaces: Se pueden agregar nodos y enlaces a un grafo existente de forma manual, bien uno a uno con
add_node()yadd_edge(), o desde una lista conadd_nodes_from()yadd_edges_from(). También pueden añadirse desde un archivo o DataFrame confrom_pandas_edgelist(). Para grafos ponderados se utiliza la funciónadd_weighted_edges_from.
- Información del grafo: Se pueden obtener información básica del grafo, como el número de nodos y enlaces, utilizando las funciones
number_of_nodes(),number_of_edges(). Utilizandonodes()yedges()se accede a la meta-información de nodos y ejes.
- Vecinos y grado: Se pueden identificar los vecinos y el grado de un nodo utilizando las funciones:
neighbors()ydegree(); para redes dirigidas se utilizain_degree()yout_degree().
- Centralidad: Se pueden calcular medidas de centralidad, como la centralidad de grado, la centralidad de intermediación, y la centralidad entre los nodos utilizando las funciones
degree_centrality(),betweenness_centrality()ycloseness_centrality().
- Componentes conectadas: Se pueden encontrar las componentes conectadas de un grafo utilizando la función
connected_components().
- Lectura y escritura de archivos: NetworkX permite leer y escribir grafos en varios formatos de archivo utilizando
read_edgelist(),write_edgelist(),read_adjlist()ywrite_adjlist().
- Algoritmos de optimización: NetworkX tiene integrados algoritmos de optimización como el camino más corto y el flujo máximo. Se accede a ellos con las funciones
shortest_path()ydijkstra_path().
- Subgrafos: Se pueden generar subgrafos de un grafo utilizando la función
subgraph(), dando una lista de nodos como input.
- Operaciones de conjunto: NetworkX permite realizar operaciones de conjunto en grafos tales como la unión, la intersección y la diferencia utilizando
union(),intersection()ydifference().
- Matriz de adyacencia: Con la función
nx.adjacency_matrix(G)se obtiene la matriz de adyacencia de un grafo. Por defecto, está en formato sparse, para verla por pantalla hay que utilizar la funciónto_dense()de numpy.
- Gráfico de grafos:La función
draw()permite dibujar grafos utilizando la ibrería Matplotlib.
Creación de grafos no dirigidos
NetworkX permite crear redes de manera manual, añadiendo los nodos y ejes uno por uno o desde un archivo o un DataFrame que contenga las conexiones. Esto último es especialmente útil cuando se trabaja con datos de redes que ya han sido recopilados y se encuentran en un formato estructurado.
Creación manual
# Librerías
# ======================================================================================
import networkx as nx
import pandas as pd
import warnings
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
Para crear un grafo en NetworkX, se debe crear un objeto de tipo "Grafo" utilizando la función nx.Graph(). Esta función crea un grafo vacío, sin nodos ni ejes, al que se pueden agregar elementos más adelante.
# Creación de una instancia de tipo "Grafo"
# ======================================================================================
G = nx.Graph()
print(G)
Graph with 0 nodes and 0 edges
Se verifica que es un grafo no dirigido.
G.is_directed()
False
Una vez que el objeto Grafo ha sido creado, se puede poblar con nodos y conexiones. Para ello se utilizan dos métodos:
add_node: añade un único nodo al grafo.
add_nodes_from: añade multiples nodos al grafo.
add_edge: añade un eje entre los nodos u y v. Si los nodos no existen, se crean y añaden automáticamente al grafo.
add_edges_from: mismo comportamiento queadd_edgepero utilizando una colección de ejes. Cada eje se define con una tupla (u, v).
El nombre de los nodos puede ser tanto de numérico como caracteres.
# Añadir un único nodo
# ======================================================================================
fig, ax = plt.subplots(figsize=(1,1))
G.add_node("A")
nx.draw(G, with_labels=True, ax=ax)
print(G)
Graph with 1 nodes and 0 edges
# Añadir multiples nodos
# ======================================================================================
G.add_nodes_from(["B", "C"])
fig, ax = plt.subplots(figsize=(3, 2))
nx.draw(G, with_labels=True, ax=ax)
ax.set_xlim([1.2*x for x in ax.get_xlim()])
ax.set_ylim([1.2*y for y in ax.get_ylim()])
print(G)
Graph with 3 nodes and 0 edges
# Añadir un único eje
# ======================================================================================
G.add_edge("A", "B")
fig, ax = plt.subplots(figsize=(3, 2))
nx.draw(G, with_labels=True, ax=ax)
ax.set_xlim([1.2*x for x in ax.get_xlim()])
ax.set_ylim([1.2*y for y in ax.get_ylim()])
print(G)
Graph with 3 nodes and 1 edges
# Añadir múltiples ejes
# ======================================================================================
G.add_edges_from([("A", "C"), ("B", "C")])
fig, ax = plt.subplots(figsize=(3, 2))
nx.draw(G, with_labels=True, ax=ax)
ax.set_xlim([1.2*x for x in ax.get_xlim()])
ax.set_ylim([1.2*y for y in ax.get_ylim()])
print(G)
Graph with 3 nodes and 3 edges
Si se añade una conexión cuyos nodos no existen, se crean automáticamente.
G.add_edges_from([("D", "E"), ("E", "F")])
fig, ax = plt.subplots(figsize=(4, 4))
nx.draw(G, with_labels=True, ax=ax)
ax.set_xlim([1.2*x for x in ax.get_xlim()])
ax.set_ylim([1.2*y for y in ax.get_ylim()])
print(G)
Graph with 6 nodes and 5 edges
La matriz de adyacencia correspondiente es:
adjM = nx.adjacency_matrix(G)
# Se convierte la matriz de formato sparse a dense para poder imprimirla
adjM = adjM.todense()
adjM
array([[0, 1, 1, 0, 0, 0],
[1, 0, 1, 0, 0, 0],
[1, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0],
[0, 0, 0, 1, 0, 1],
[0, 0, 0, 0, 1, 0]])
Y el número de nodos y ejes:
print(G.number_of_edges())
print(G.number_of_nodes())
5 6
La información de los nodos y ejes del grafo está almacenada en los attributos nodes y edges.
print(f"Nodos del grafo: {G.nodes}")
print(f"Ejes del grafo: {G.edges}")
Nodos del grafo: ['A', 'B', 'C', 'D', 'E', 'F']
Ejes del grafo: [('A', 'B'), ('A', 'C'), ('B', 'C'), ('D', 'E'), ('E', 'F')]
Creación desde un DataFrame
Para crear un grafo a partir de un dataframe de pandas, la información tiene que estar estructurada de tal forma que una columna represente el incio de cada eje y otra el destino. Por ejemplo, para representar que existen dos nodos ("A" y "B") conectados entre si, se necesita una fila que contenga el valor "A" en una columna y "B" en otra. Esta información es suficiente para que se creen los dos nodos y la conexión entre ambos.
# Dataframe con las conexiones del grafo
# ======================================================================================
conexiones = pd.DataFrame(
{
"inicio": ["A", "B", "C"],
"fin": ["C", "C", "D"],
}
)
conexiones
| inicio | fin | |
|---|---|---|
| 0 | A | C |
| 1 | B | C |
| 2 | C | D |
En la función from_pandas_edgelist se indica la columna de origen y destino (para grafos no dirigidos, se elige un origen indistintamente).
# Crear un grafo a partir de un Dataframe
# ======================================================================================
fig, ax = plt.subplots(figsize=(3,2))
G = nx.from_pandas_edgelist(conexiones, source="inicio", target="fin")
nx.draw(G, with_labels=True, ax=ax)
Creación de grafos dirigidos
Creación manual
Como se ha explicado anteriormente, los enlaces de los grafos dirigidos tienen una dirección definida (los enlaces de estos grafos se representan con una flecha). El proceso de creación de un grafo dirigido es esquivalente al de un grafos no dirigido pero utilizando DiGraph en luigar de Graph.
# Creación de un grafo dirigido".
# ======================================================================================
G = nx.DiGraph()
# Conexiones
G.add_edges_from([(1, 2), (2, 3), (1, 4), (1, 5), (4, 2), (5, 4)])
# Representación gráfica
fig, ax = plt.subplots(figsize=(3,2))
nx.draw(G, with_labels=True, ax=ax)
Se verifica que sí es un grafo dirigido.
G.is_directed()
True
Creación desde un DataFrame
El proceso de creación de un grafo dirigido desde un DataFrame es equivalente al de los grafos no dirigidos mostrado en el apartado anterior, con la única diferencia de que se ha de indicar el argumento create_using = nx.DiGraph.
# Crear un grafo dirigido a partir de un Dataframe
# ======================================================================================
G = nx.from_pandas_edgelist(
conexiones,
source = "inicio",
target = "fin",
create_using = nx.DiGraph
)
fig, ax = plt.subplots(figsize=(3,2))
nx.draw(G, with_labels=True, ax=ax)
Al tratarse de un grafo dirigido, su matriz de adjacencia no es simétrica.
adjM = nx.adjacency_matrix(G)
adjM = adjM.todense()
adjM
array([[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 1, 0, 0],
[0, 0, 0, 0]])
Distancia entre dos nodos
Para obtener la distancia de orden 2 entre cualquier par de nodos, basta con multiplicar la matriz de adyacencia por sí misma. Si la multiplicamos por sí misma n veces, obtenemos la matriz de distancias de orden n.
# Creación del grafo
G = nx.Graph()
# Se añadennodos a grafo
G.add_edges_from([(1, 2), (2, 3), (1, 4), (1, 5), (4, 2), (5, 4)])
# Representación gráfica
fig, ax = plt.subplots(figsize=(3,2))
nx.draw(G, with_labels=True, ax=ax)
# Matrix de adyacencia
adjM = nx.adjacency_matrix(G).todense()
adjM
array([[0, 1, 0, 1, 1],
[1, 0, 1, 1, 0],
[0, 1, 0, 0, 0],
[1, 1, 0, 0, 1],
[1, 0, 0, 1, 0]])
La matriz de distancia de orden 2 muestra todos los pares de nodos que están conectados con un nodo intermedio. En el ejemplo, los nodos 2 y 5 tienen entre medias los nodos 4 y 1, es decir, existen dos caminos posibles de orden 2 que los conectan: el 2-4-5 y el 2-1-5. Por ese motivo, si se multiplica la matriz de adyacencia por sí misma, el elemento [2, 5] de la matriz de distancias tiene el valor 2. Lo que indica que existen dos caminos de distancia 2 entre los nodos 2 y 5.
# Multiplicación de la matriz por sí misma de forma matricial
distancias_orden_dos = adjM @ adjM
distancias_orden_dos
array([[3, 1, 1, 2, 1],
[1, 3, 0, 1, 2],
[1, 0, 1, 1, 0],
[2, 1, 1, 3, 1],
[1, 2, 0, 1, 2]])
# Los indices en python empiezan en 0
print(f"Caminos de orden dos entre los nodos 2 y 5 = {distancias_orden_dos[1, 4]}")
Caminos de orden dos entre los nodos 2 y 5 = 2
También se puede calcular el camino más corto (shortest path) utilizando directamente la función nx.shortest_path.
nx.shortest_path(G, source=2, target=5)
[2, 1, 5]
Grafo ponderado
En un grafo ponderado, los ejes del grafo tienen un peso asociado. En la representación gráfica de este tipo de redes, los ejes se suelen mostrar con una anchura distinta en función de su peso.
# Creacion del grafo
G = nx.Graph()
# Nodos y vonexiones
G.add_weighted_edges_from(
[(1, 2, 0.5),
(2, 3, 0.9),
(1, 4, 0.1),
(1, 5, 0.75),
(4, 2, 0.01),
(5, 4, 0.3)]
)
G.edges(data=True)
EdgeDataView([(1, 2, {'weight': 0.5}), (1, 4, {'weight': 0.1}), (1, 5, {'weight': 0.75}), (2, 3, {'weight': 0.9}), (2, 4, {'weight': 0.01}), (4, 5, {'weight': 0.3})])
# Se verifica que es un grafo no dirigido y ponderado
G.is_directed()
False
nx.is_weighted(G)
True
# Se muestra el peso de cada eje, así como los nodos que conecta
[a for a in G.edges(data=True)]
[(1, 2, {'weight': 0.5}),
(1, 4, {'weight': 0.1}),
(1, 5, {'weight': 0.75}),
(2, 3, {'weight': 0.9}),
(2, 4, {'weight': 0.01}),
(4, 5, {'weight': 0.3})]
Para extraer los pesos, se itera sobre cada eje y se accede al tercer elemento de la tupla.
weights = [a[2]["weight"] for a in G.edges(data=True)]
weights
[0.5, 0.1, 0.75, 0.9, 0.01, 0.3]
Para dibujar un grafo ponderado con NetworkX, es necesario dibujar por separado nodos y ejes. En primer lugar se utiliza un utilizando un graph layout que define la posición de los nodos. Una vez definida su posición se representan los nodos y los ejes. Para este ejemplo se utiliza el spring_layout, pero hay muchos más.
# Definir la posición de los nodos utilizando un layout
pos = nx.spring_layout(G)
# Representar nodos y ejes
fig, ax = plt.subplots(figsize=(3,2))
nx.draw_networkx_nodes(
G,
pos = pos,
ax = ax
)
nx.draw_networkx_edges(
G,
pos = pos,
edgelist = G.edges,
width = weights,
ax = ax
);
Otros tipos de layout

Representar de forma separada los nodos y ejes permite tener más control sobre las caracteristicas visuales, por ejemplo, el color de los nodos y ejes.
pos = nx.circular_layout(G)
fig, ax = plt.subplots(figsize=(3,3))
nx.draw_networkx_nodes(G, pos=pos, node_color="red", ax=ax)
nx.draw_networkx_edges(G, pos=pos, edgelist=G.edges, width=weights, edge_color="blue", ax=ax);
Grafo bipartito
Cuando los nodos de un grafo representan entidades de distinta naturaleza se utiliza el termino grafo bipartito. Un ejemplo común de este típo de grafos son las redes de publicaciones donde existen nodos de tipo "artículo" y otros de tipo "escritor".
En este tipo de grafos, las conexiones únicamente puden ocurrir entre nodos de distinta naturaleza, cada escritor está conectado a los artículos que ha escrito. No pueden existir conexiones directas entre artículos o entre escritores.
Para crear un grafo bipartito NetworkX se utiliza el método add_nodes_from() con el que se agregan los nodos indicando el tipo con el argumento bipartite y luego se utiliza el método add_edges_from() para agregar las relaciones entre ellos. A continuación, se muesra un ejemplo de cómo crear un grafo bipartito con dos conjuntos de nodos llamados "grupo A" y "grupo B".
# Crear un grafo bipartito vacío
G_peliculas_actores = nx.Graph()
# Agregar los nodos de cada grupo
G_peliculas_actores.add_nodes_from(["Pelicula_1", "Pelicula_2", "Pelicula_3"], bipartite="Peliculas")
G_peliculas_actores.add_nodes_from(["Actor_1", "Actor_2", "Actor_3"], bipartite="Actores")
# Agregar las relaciones entre los nodos
G_peliculas_actores.add_edges_from(
[
("Pelicula_1", "Actor_1"),
("Pelicula_2", "Actor_2"),
("Pelicula_3", "Actor_3"),
("Pelicula_2", "Actor_3"),
("Pelicula_2", "Actor_1"),
]
)
G_peliculas_actores.nodes(data=True)
NodeDataView({'Pelicula_1': {'bipartite': 'Peliculas'}, 'Pelicula_2': {'bipartite': 'Peliculas'}, 'Pelicula_3': {'bipartite': 'Peliculas'}, 'Actor_1': {'bipartite': 'Actores'}, 'Actor_2': {'bipartite': 'Actores'}, 'Actor_3': {'bipartite': 'Actores'}})
Para acceder al tipo de cada nodo se recorre cada nodo y se accede al attributo "bipartite".
tipo_nodo = [
G_peliculas_actores.nodes[i]["bipartite"]
for i in G_peliculas_actores.nodes()
]
tipo_nodo
['Peliculas', 'Peliculas', 'Peliculas', 'Actores', 'Actores', 'Actores']
# colores para cada tipo de nodo en el grafo bipartito
colores = {"Peliculas": "red", "Actores": "blue"}
colores_nodos = [colores[n] for n in tipo_nodo]
fig, ax = plt.subplots(figsize=(5, 4))
nx.draw(
G_peliculas_actores,
pos=nx.spring_layout(G_peliculas_actores),
with_labels=True,
node_color=colores_nodos,
ax=ax,
)
En el código anterior se utiliza la función spring_layout para posicionar los nodos en el gráfico. Con el arguemento with_labels=True se muestran las etiquetas de los nodos y con node_colorse asigna el color de cada nodo.
Una vez que creado el grafo bipartito, se puede hacer una proyección de éste para obtener un grafo no bipartito que contenga sólo los nodos de uno de los conjuntos. Para hacer esto, se emplea el método project() especificando el conjunto de nodos que se desea incluir en la proyección. A continuación se genera una proyección del grafo bipartito para obtener sólo los nodos del grupo 'Actores'.
# Se identifican los nodos del conjunto de interés
nodes_bipartite = [
n[0]
for n in G_peliculas_actores.nodes(data=True)
if n[1]["bipartite"] == "Actores"
]
# Proyección del grafo bipartito para obtener sólo los nodos del grupo A
G_actores = nx.bipartite.projected_graph(G_peliculas_actores, nodes_bipartite)
fig, ax = plt.subplots(figsize=(4, 4))
nx.draw(G_actores, with_labels=True, ax=ax)
ax.set_xlim([1.2 * x for x in ax.get_xlim()])
ax.set_ylim([1.2 * y for y in ax.get_ylim()])
(-1.399587467232622, 0.9004204884956905)
Meta-información: atributos de nodos y ejes
En la matoria de los problemas de grafos reales, se dispone de información adicional de nodos y ejes. Los atributos de los nodos se añaden con el método networkx.set_node_atributes(Grafo, diccionario, nombre) y los atributos de los ejes se añaden con el método networkx.set_edge_atributes().
# Creación del grafo
G = nx.Graph()
G.add_edges_from([(1, 2), (2, 3), (1, 4), (1, 5), (4, 2), (5, 4)])
G.edges(data=True)
EdgeDataView([(1, 2, {}), (1, 4, {}), (1, 5, {}), (2, 3, {}), (2, 4, {}), (4, 5, {})])
# Añadir atributos de los nodos
node_name = {1: "Jaime", 2: "María", 3: "Julio", 4: "Rosa", 5: "Alberto"}
node_aficiones = {
1: ["Futbol"],
2: ["Baile", "Pádel"],
3: ["Golf", "Baile"],
4: ["Cocina"],
5: ["Cocina", "Jamón"],
}
nx.set_node_attributes(G, node_name, name="Nombre")
nx.set_node_attributes(G, node_aficiones, name="Aficiones")
# añadir atributos de los ejes
edges_weight = {
(1, 2): 0.5,
(2, 3): 0.9,
(1, 4): 0.1,
(1, 5): 0.75,
(4, 2): 0.01,
(5, 4): 0.3,
}
nx.set_edge_attributes(G, edges_weight, name="weight")
Para acceder a los atributos de nodos y ejes (meta-información) se utiliza G.nodes(data=True) o G.edges(data=True).
Estos comandos devuelven una estructura en forma de diccionario donde la clave es el nombre del nodo y el valor contiene todos los atributos.
G.nodes(data=True)
NodeDataView({1: {'Nombre': 'Jaime', 'Aficiones': ['Futbol']}, 2: {'Nombre': 'María', 'Aficiones': ['Baile', 'Pádel']}, 3: {'Nombre': 'Julio', 'Aficiones': ['Golf', 'Baile']}, 4: {'Nombre': 'Rosa', 'Aficiones': ['Cocina']}, 5: {'Nombre': 'Alberto', 'Aficiones': ['Cocina', 'Jamón']}})
G.edges(data=True)
EdgeDataView([(1, 2, {'weight': 0.5}), (1, 4, {'weight': 0.1}), (1, 5, {'weight': 0.75}), (2, 3, {'weight': 0.9}), (2, 4, {'weight': 0.01}), (4, 5, {'weight': 0.3})])
Los atributos de ejes y nodos se pueden iterar directamente como si se tratase de un diccionario.
for m, n, w in G.edges(data=True):
print(
f"Eje que conecte el nodo {m} con el nodo {n} y tiene un peso de {w['weight']}."
)
Eje que conecte el nodo 1 con el nodo 2 y tiene un peso de 0.5. Eje que conecte el nodo 1 con el nodo 4 y tiene un peso de 0.1. Eje que conecte el nodo 1 con el nodo 5 y tiene un peso de 0.75. Eje que conecte el nodo 2 con el nodo 3 y tiene un peso de 0.9. Eje que conecte el nodo 2 con el nodo 4 y tiene un peso de 0.01. Eje que conecte el nodo 4 con el nodo 5 y tiene un peso de 0.3.
Visualización avanzada de redes
Existen otras librerías con las que se pueden visualizar redes mucho más grandes de forma muy eficiente e interactiva. En próximos artículos se mostrarán con detalle las principales alternativas.

Información de sesión
import session_info
session_info.show(html=False)
----- matplotlib 3.10.7 networkx 3.5 pandas 2.3.3 session_info v1.0.1 ----- IPython 9.6.0 jupyter_client 7.4.9 jupyter_core 5.9.1 notebook 6.5.7 ----- Python 3.13.9 | packaged by Anaconda, Inc. | (main, Oct 21 2025, 19:16:10) [GCC 11.2.0] Linux-6.14.0-35-generic-x86_64-with-glibc2.39 ----- Session information updated at 2025-11-20 23:58
Bibliografía
Network Science - Albert-László Barabási
https://ericmjl.github.io/Network-Analysis-Made-Simple/01-introduction/03-viz/
Instrucciones para citar
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Introducción a grafos y redes con Python por Fernando Carazo y Joaquín Amat Rodrigo, disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://www.cienciadedatos.net/documentos/pygml01-introduccion-grafos-redes-python.html
¿Te ha gustado el artículo? Tu ayuda es importante
Tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este documento creado por Joaquín Amat Rodrigo tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
-
Compartir: copiar y redistribuir el material en cualquier medio o formato.
-
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
-
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
-
No-Comercial: No puedes utilizar el material para fines comerciales.
-
Compartir-Igual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.
