-
networkx를 이용하여 multidigraph 그리기파이썬 2021. 6. 5. 23:39
들어가며
최근에 그래프를 그릴 일이 좀 있었다. Python 기반의 networkx와 matplotlib을 이용하여 그래프를 그리고 시각화를 하려고 했다. networkx는 그래프를 정의하고 시각화하는 다양한 API를 제공하고 있어 쉽게 활용할 수 있었다.
Networkx: https://networkx.org/documentation/stable/index.html
다만, 한 가지 아쉬운 점이 있었는데 노드와 노드 사이를 잇는 엣지가 여러 개 일 경우에 의도대로 그려지지 않았다. 위의 그래프는 아래 코드의 실행 결과이다.
import networkx as nx import matplotlib.pyplot as plt MG = nx.MultiDiGraph() edges = [ ("A", "B", 0.5, "A to B 1"), ("A", "B", 0.75, "A to B 2"), ("A", "B", 3, "A to B 3"), ("A", "A", 1, "A to A"), ("B", "C", 0.5, "B to C"), ] for node1, node2, weight, label in edges: MG.add_edge(node1, node2, weight=weight, label=label) plt.subplot(111) nx.draw(MG, with_labels=True) plt.show()
원래 의도했던 그래프는 아래와 같은 모양이다.
그래서 직접 networkx, matplotlib을 이용하여 위와 같은 모양으로 그래프를 그려보려고 한다.
우선 node를 먼저 그려본다.
import networkx as nx import matplotlib.pyplot as plt MG = nx.MultiDiGraph() edges = [ ("A", "B", 0.5, "A to B 1"), ("A", "B", 0.75, "A to B 2"), ("A", "B", 3, "A to B 3"), ("B", "C", 0.5, "B to C"), ] for node1, node2, weight, label in edges: MG.add_edge(node1, node2, weight=weight, label=label) nx.draw_networkx_nodes(MG, nx.circular_layout(MG)) nx.draw_networkx_labels(MG, nx.circular_layout(MG)) plt.show()
다음으로는 각각의 노드 사이에 엣지를 그려주려고 한다. 아이디어는 두 노드를 잇는 직선과 수직인 직선을 구한 뒤 그 직선 위에 있는 점들중에서 일정 거리만큼 떨어진 점들을 찾아서 두 노드와 그 점을 이어줄 것이다. 우선 두 노드 사이의 점을 찾아본다.
import networkx as nx import matplotlib.pyplot as plt MG = nx.MultiDiGraph() edges = [ ("A", "B", 0.5, "A to B 1"), ("A", "B", 0.75, "A to B 2"), ("A", "B", 3, "A to B 3"), ("B", "C", 0.5, "B to C"), ] for node1, node2, weight, label in edges: MG.add_edge(node1, node2, weight=weight, label=label) layout = nx.circular_layout(MG) nx.draw_networkx_nodes(MG, layout) nx.draw_networkx_labels(MG, layout) for node1, node2, index in MG.edges: x1, y1 = layout[node1] x2, y2 = layout[node2] mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 plt.plot(mid_x, mid_y, 'o-') plt.show()
circular_layout 함수는 각 node의 x, y 좌표를 반환하고 그 형태는 Dict[str, List[float, float]]로 이루어져 있다. 즉 layout[node]를 조회하면 node의 x, y좌표를 얻을 수 있다.
{'A': array([1.00000000e+00, 1.98682151e-08]), 'B': array([-0.50000007, 0.86602542]), 'C': array([-0.49999993, -0.86602544])}
MG.edges에는 그래프의 edge가 (node1, node2, node1에 등록된 index)의 형태로 저장되어있다.
[('A', 'B', 0), ('A', 'B', 1), ('A', 'B', 2), ('B', 'C', 0)]
위의 코드를 실행시키면 아래와 같이 각 노드 사이의 중점을 그릴 수 있다.
이제 처음 계획대로 두 노드를 지나는 직선에 수직이면서 일정 거리만큼 떨어진 점들을 찾아줄 것이다. 아래처럼 가, 나 점을 그리고 A와 가를 잇고 가와 B를 이어서 엣지를 표현할 것이다.
가, 나의 좌표는 아래와 같이 구할 수 있다.
아래는 이것을 코드로 표현한 것이다.
import math import networkx as nx import matplotlib.pyplot as plt MG = nx.MultiDiGraph() edges = [ ("A", "B", 0.5, "A to B 1"), ("A", "B", 0.75, "A to B 2"), ("A", "B", 3, "A to B 3"), ("B", "C", 0.5, "B to C"), ] for node1, node2, weight, label in edges: MG.add_edge(node1, node2, weight=weight, label=label) layout = nx.circular_layout(MG) nx.draw_networkx_nodes(MG, layout) nx.draw_networkx_labels(MG, layout) history = {} for node1, node2, index in MG.edges: x1, y1 = layout[node1] x2, y2 = layout[node2] mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 factor = history.get(tuple({node1, node2}), 0) theta = math.atan((y2 - y1) / (x2 - x1)) mid_x -= factor * math.sin(theta) mid_y += factor * math.cos(theta) next_factor = -factor if factor > 0 else -factor + 0.2 history[tuple({node1, node2})] = next_factor plt.plot(mid_x, mid_y, 'o-') plt.show()
이제 각각의 노드와 그려놓은 점들을 이어서 엣지를 표현하고 점 위에 텍스트를 배치해서 엣지의 label을 나타내려고 한다.
위의 코드에서 plt.plot(mid_x, mid_y, 'o-') 부분을 아래처럼 바꿀것이다.
plt.annotate( "", (x2, y2), xytext=(mid_x, mid_y), arrowprops=dict( arrowstyle="->", connectionstyle=f"arc3,rad={factor / 2}" ) ) plt.annotate( "", (mid_x, mid_y), xytext=(x1, y1), arrowprops=dict( arrowstyle="-", connectionstyle=f"arc3,rad={factor / 2}" ) ) plt.text(mid_x, mid_y, MG[node1][node2][index]['label'] )
이제 label의 각도를 선의 기울기와 같게 바꿔주고 각 곡선의 시작점과 끝점을 좀더 부드럽게 보이도록 변경해주면 된다.
import math import networkx as nx import matplotlib.pyplot as plt MG = nx.MultiDiGraph() edges = [ ("A", "B", 0.5, "A to B 1"), ("A", "B", 0.75, "A to B 2"), ("A", "B", 3, "A to B 3"), ("B", "C", 0.5, "B to C"), ] for node1, node2, weight, label in edges: MG.add_edge(node1, node2, weight=weight, label=label) layout = nx.circular_layout(MG) nx.draw_networkx_nodes(MG, layout) nx.draw_networkx_labels(MG, layout) history = {} for node1, node2, index in MG.edges: x1, y1 = layout[node1] x2, y2 = layout[node2] mid_x = (x1 + x2) / 2 mid_y = (y1 + y2) / 2 factor = history.get(tuple({node1, node2}), 0) theta = math.atan((y2 - y1) / (x2 - x1)) mid_x -= factor * math.sin(theta) mid_y += factor * math.cos(theta) next_factor = -factor if factor > 0 else -factor + 0.2 history[tuple({node1, node2})] = next_factor plt.annotate( "", (x2, y2), xytext=(mid_x, mid_y), arrowprops=dict( arrowstyle="->", connectionstyle=f"arc3,rad={factor / 2}", shrinkB=15 ) ) plt.annotate( "", (mid_x, mid_y), xytext=(x1, y1), arrowprops=dict( arrowstyle="-", connectionstyle=f"arc3,rad={factor / 2}", shrinkA=15 ) ) plt.text(mid_x, mid_y, MG[node1][node2][index]['label'], rotation=math.degrees(theta), rotation_mode='anchor', verticalalignment='center', horizontalalignment='center', backgroundcolor='white', ) plt.show()
'파이썬' 카테고리의 다른 글
파이썬 싱글턴(Singleton) 패턴 (0) 2021.04.25