(Actualización de este post: http://micaminomaster.com.co/grafo-algoritmo/libreria-grafos-dinamicos/)
Me gustaría compartirles un módulo que realicé en Python 3.5 acerca de Grafos. Durante las siguientes semanas voy a trabajar bastante con esta herramienta y sería de gran ayuda un módulo que me ayude a administrar todas estas tareas.
Debido a que voy a trabajar con algoritmos, no puedo usar la herramienta networkx de Python. Solo utilizo su herramienta gráfica la cual es muy potente.
Utilizo el módulo numpy como excelente herramienta para manejar las estructuras de datos de matrices.
El módulo está disponible en mi repositorio público: https://github.com/arturoverbel/graph_python.
Módulo de características de un Grafo
Las características más importantes como son el manejo de nodo y de los arcos, debe ser lo más básico de este módulo. La siguiente clase abarca estos aspectos:
import numpy as np class Graph: source = [] target = [] weight = [] vertex = [] undirected = 0 def __init__(self, source=[], target=[], weight=[], directed=True): self.source = np.array(source) self.target = np.array(target) self.weight = np.array(weight) self.directed = directed self.set_vertex() def print_r(self): print("Source: ", self.source) print("Target: ", self.target) print("Weight: ", self.weight) print("Vertex: ", self.vertex) def set_vertex(self): vertex = np.unique(self.source) vertex2 = np.unique(self.target) self.vertex = np.unique(np.concatenate([vertex, vertex2])) return self.vertex def get_weight(self, n1, n2): return self.weight[np.logical_and(self.source == n1, self.target == n2)] def export(self): array_export = [(int(self.source[i]), int(self.target[i]), self.weight[i]) for i in range(self.source.size)] return array_export
En esta clase se definen los nodos fuentes y nodos destinos junto con su peso, las funciones son:
- print_r( ): Imprime los valores de nodos fuentes, nodos destino, pesos de cada arco y nos los nodos como tal (vertex)
- set_vertex( ): Dependiendo de los nodos fuentes y destino, realiza una lista de los nodos existentes del grafo, importantes para usarlo en los algoritmos.
- get_weight(n1, n2): Dado dos nodos encontrar el peso de su arco.
- export( ): Exportar todos los datos de arco en una matriz
La clase principal, de la cual hereda la anterior es la siguiente:
from _graph.GraphAPSP import GraphAPSP import matplotlib.pyplot as plt import networkx as nx import numpy as np class GraphPro(GraphAPSP): def __init__(self, source=[], target=[], weight=[], directed=True): GraphAPSP.__init__(self, source, target, weight, directed) @staticmethod def creategraph(total_nodes, pro_edges, weights, directed=True): source = np.array([]) target = np.array([]) weight = np.array([]) for i in range(total_nodes): for k in range(i+1, total_nodes): if k == i: continue p = 1 - pro_edges has_edge = np.random.choice(2, 1, p=[p, pro_edges])[0] if not has_edge: continue probabilities = np.zeros(len(weights)) probabilities = probabilities + (1 / len(weights)) w = np.random.choice(weights, 1, p=probabilities)[0] source = np.append(source, i) target = np.append(target, k) weight = np.append(weight, w) if not directed: source = np.append(source, k) target = np.append(target, i) weight = np.append(weight, w) return GraphPro(source, target, weight) def draw(self, with_weight=True): Gr = nx.DiGraph() Gr.add_weighted_edges_from(self.export()) pos = nx.spring_layout(Gr) list_edges = list(Gr.edges()) last = () if self.last_vertex_modified.size > 0: last = (int(self.last_vertex_modified[0]), int(self.last_vertex_modified[1]) ) list_edges.remove(last) nx.draw(Gr, pos=pos, with_labels=True, edgelist=list_edges, node_size=600) if with_weight: edge_labels = dict([((u, v,), d['weight']) for u, v, d in Gr.edges(data=True)]) nx.draw_networkx_edge_labels(Gr, pos=pos, edgelist=list_edges, edge_labels=edge_labels) if len(last) > 0: nx.draw_networkx_edges(Gr, pos=pos, edgelist=[last], width=2.0, edge_color='b') plt.axis('off') plt.show()
Este módulo genera grafos aleatoriamente pasando datos como el número de nodos, la probabilidad de que exista un arco y los pesos que pueda tener, además de si es un grafo dirigido. Tiene además una función para dibujar el grafo. El atributo de self.last_vertex_modified lo veremos más adelante.
Hasta aquí podemos mostrar una apariencia de un grafo generado con este módulo:
from _graph.GraphPro import GraphPro as g import os os.system('clear') print("<--------Test ------->\n") weights = [1, 2, 3, 4, 5] graph = g.creategraph(6, .75, weights, directed=False) graph.print_r() graph.draw()
El resultado:

Figura 1. Imagen generada con el módulo de grafos aleatorios
El resultado en consola es:
Source: [ 0. 1. 0. 2. 0. 3. 0. 4. 1. 2. 1. 5. 2. 3. 2. 4. 2. 5. 3. 5.] Target: [ 1. 0. 2. 0. 3. 0. 4. 0. 2. 1. 5. 1. 3. 2. 4. 2. 5. 2. 5. 3.] Weight: [ 5. 5. 3. 3. 2. 2. 1. 1. 4. 4. 2. 2. 4. 4. 1. 1. 5. 5. 4. 4.] Vertex: [ 0. 1. 2. 3. 4. 5.]
Este es otro ejemplo solamente cambiando el número de nodos de 6 a 20, con la siguiente linea de código:
from _graph.GraphPro import GraphPro as g import os os.system('clear') print("<--------Test ------->\n") weights = [1, 2, 3, 4, 5] graph = g.creategraph(20, .4, weights, directed=False) graph.print_r() graph.draw(with_weight=False)
El resultado:

Figura 2. Creación de grafo aleatorio con 20 nodos y una propabilidad de arco de 40%
Módulo de Grafo dinámicos
Tenemos la clase DynamicGraph con propiedades de un grafo dinámico. En este caso sol otenemos cuatro funciones.
import numpy as np from _graph.Graph import Graph class DynamicGraph(Graph): def __init__(self, source=[], target=[], weight=[], directed=True): Graph.__init__(self, source, target, weight, directed) self.last_vertex_modified = np.array([]) def dynamic_decreasing_random_vertex(self): count_max = 100 flag = 0 while True: source = np.random.choice(self.vertex, 1)[0] choisen = self.target[source == self.source] if choisen.size != 0: target = np.random.choice(choisen, 1)[0] break flag = flag + 1 if flag >= count_max: return -2 return self.dynamic_decreasing_vertex(source, target) def dynamic_decreasing_vertex(self, source, target): index = np.where(np.logical_and(self.source == source, self.target == target))[0][0] returned = np.array([]) self.source = np.delete(self.source, index) returned = np.append(returned, source) self.target = np.delete(self.target, index) returned = np.append(returned, target) self.weight = np.delete(self.weight, index) self.last_vertex_modified = returned return returned def dynamic_incremental_random_vertex(self, weights=[1, 2, 3, 4, 5, 6, 7, 8, 9]): count_max = 100 flag = 0 while True: source = np.random.choice(self.vertex, 1)[0] index_for_target = np.invert(np.logical_or(np.in1d(self.vertex, self.target[source == self.source]), self.vertex == source)) choisen = self.vertex[index_for_target] if choisen.size != 0: target = np.random.choice(choisen, 1)[0] break flag = flag + 1 if flag >= count_max: return -2 w = np.random.choice(weights) return self.dynamic_incremental_vertex(source, target, w) def dynamic_incremental_vertex(self, source, target, weight=1): returned = np.array([]) self.source = np.append(self.source, source) returned = np.append(returned, source) self.target = np.append(self.target, target) returned = np.append(returned, target) self.weight = np.append(self.weight, weight) returned = np.append(returned, weight) self.last_vertex_modified = returned return returned
- dynamic_incremental_vertex( source, target, weight=1): Agrega un nuevo arco al grafo dado un nodo fuente y destino con un peso específico.
- dynamic_incremental_random_vertex( weights=[*] ): Agrega un nuevo arco con fuente y destino aleatorios, el peso también es aleatorio y puede ser especificado el conjunto
- dynamic_decreasing_vertex( source, target): Elimina un arco existente del grafo dado un nodo fuente y destino.
- dynamic_decreasing_random_vertex( weights=[*] ): Elimina un arco con fuente y destino aleatorios dentro del grafo.
Las dos funciones que realizan el proceso de actualización del arco en el grafo, tambén actualiza un atributo llamado self.last_vertex_modified para identificar en la instancia el último elemento modificado. Esto lo usamos al momento de dibujar un grafo. Veamoslo en acción con el siguiente código:
from _graph.GraphPro import GraphPro as g import os os.system('clear') print("<--------Test Create------->\n") weights = [1, 2, 3, 4, 5] graph = g.creategraph(6, .75, weights) graph.print_r() print("-------Incremental-----") data = graph.dynamic_incremental_random_vertex(weights) graph.print_r() graph.draw() print() print("------------------------")
En el código generamos aleatoriamente un grafo de seis nodos, con una probabilidad de arco de 75% y unos pesos específicos. Utillizamos la función que agrega un nuevo arco aleatoriamente. Tenemos el siguiente resultado:

Figura 3. Grafo dinámico creciente por arco, generado aleatoriamente
Como se puede observar en la figura, el arco agregado es (3, 1) con peso de 2.0, el resultado por consola es:
<--------Test Create-------> Source: [ 0. 0. 0. 1. 1. 2. 2. 3. 4.] Target: [ 1. 2. 4. 4. 5. 3. 4. 5. 5.] Weight: [ 1. 2. 1. 3. 4. 4. 2. 2. 3.] Vertex: [ 0. 1. 2. 3. 4. 5.] -------Incremental----- Source: [ 0. 0. 0. 1. 1. 2. 2. 3. 4. 3.] Target: [ 1. 2. 4. 4. 5. 3. 4. 5. 5. 1.] Weight: [ 1. 2. 1. 3. 4. 4. 2. 2. 3. 2.] Vertex: [ 0. 1. 2. 3. 4. 5.] ------------------------
Conclusiones
Con esto, tenemos todo lo necesario para trabajar con los grafos, ahora solo queda aplicar los algoritmos correspondientes. El código final está disponible en https://github.com/arturoverbel/graph_python. Utilizando Python 3.5
Comentarios