ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • networkx를 이용하여 multidigraph 그리기
    파이썬 2021. 6. 5. 23:39

    들어가며

     최근에 그래프를 그릴 일이 좀 있었다. Python 기반의 networkx와 matplotlib을 이용하여 그래프를 그리고 시각화를 하려고 했다. networkx는 그래프를 정의하고 시각화하는 다양한 API를 제공하고 있어 쉽게 활용할 수 있었다.

    Networkx: https://networkx.org/documentation/stable/index.html

     

    Software for Complex Networks — NetworkX 2.5 documentation

    NetworkX is a Python package for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks. With NetworkX you can load and store networks in standard and nonstandard data formats, generate many types of random and

    networkx.org

     

    다만, 한 가지 아쉬운 점이 있었는데 노드와 노드 사이를 잇는 엣지가 여러 개 일 경우에 의도대로 그려지지 않았다. 위의 그래프는 아래 코드의 실행 결과이다.

    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

    댓글

Designed by Tistory.