데이터 사이언스

네트워크 데이터 분석 - Networkx

skypainter 2024. 6. 24. 12:43

목차

     

     

    네트워크 데이터 분석 - 서론

    목차 " data-ke-type="html">HTML 삽입미리보기할 수 없는 소스  이번 포스트에서는 네트워크 데이터 분석의 기초가 되는 개념들을 다뤄보겠습니다.1. 그래프의 구성 요소네트워크 데이터란 두 관측치

    sanghn.tistory.com

    지난 포스트에 이어서 이번 포스트에서는 네트워크 데이터 분석에 유용한 python 패키지인 Networkx에 대해서 다뤄보겠습니다.

    1. Undirected network 생성

    먼저 networkx를 import 해줍니다.

    import networkx as nx
    %matplotlib inline

    nx.Graph()는 undirected graph G를 생성시킵니다.
    이제 add_node 또는 add_nodes_from 매서드를 통해 node를 G에 넣어줍니다.
    비슷하게 add.edge 또는 add_edges_from 매서드를 통해 G의 edge들을 설정해줍니다. 

    G = nx.Graph()
    
    # add a node called 'a'
    G.add_node('a')
    
    # adding nodes from a list
    nodes_to_add = ['b', 'c', 'd', 'f']
    G.add_nodes_from(nodes_to_add)
    
    # add edge from 'a' to 'b'
    G.add_edge('a', 'b')
    
    #  add edges from a list
    edges_to_add = [('a', 'c'), ('b', 'c'), ('c', 'd'), ('c', 'f')]
    G.add_edges_from(edges_to_add)
    
    # draw the graph
    nx.draw(G, with_labels=True)

    마지막으로 설정한 node와 edge들에 따라 네트워크가 잘 생성되었는지 확인하기 위해서 nx.draw 매서드를 통해 네트워크를 시각화합니다. 여기서 with_labels 파라매터를 True로 설정해주면, node의 이름들이 plot에 나타납니다.

    네트워크가 잘 혀성되었지만, node의 이름이 검정색이라 잘 안 보입니다. 아래의 코드를 통해 시각화와 관련된 파라매터들을 더 설정해주었습니다.

    nx.draw(G,
            with_labels=True,
            node_color='blue',
            node_size=1600,
            font_color='white',
            font_size=16,
            )

    2. Node와 edge

    G.nodes()와 G.edges를 통해 각각 전체 node와 edge의 list를 불러올 수 있습니다.

    # List all of the nodes
    print(G.nodes()) ## ['a', 'b', 'c', 'd', 'f']
    
    # List all of the edges
    print(G.edges()) ## [('a', 'b'), ('a', 'c'), ('b', 'c'), ('c', 'd'), ('c', 'f')]

    비슷하게 G.number_of_nodes() 와 G.number_of_edges로 각각 node의 개수와 edge의 개수를 확인할 수 있습니다.

    print(G.number_of_nodes())  ## node의 개수: 5
    
    print(G.number_of_edges())  ## edge의 개수: 5

    G.neighbors('c') 함수를 사용하면 c와 인접한 node를 확인할 수 있습니다.
    그러나 결과를 직접 print 해보면 아래와 같이 iterator를 print 함을 알 수 있습니다.
    이는 networkx가 효율성 문제로 인해서 대부분 iterator의 형태로 값을 반환하기 때문입니다.

    # list of neighbors of node 'c'
    print(G.neighbors('c')) ## dict_keyiterator object at 0x0000026764FA49A0>

    따라서 각 값을 확인하고 싶다면 for 구문을 사용하여야 합니다.

    for neighbor in G.neighbors('c'):
        print(neighbor) ## a b d f

    아니면 list() 함수를 사용하여, 값들을 리스트화할 수도 있습니다.

    neighbors_c = list(G.neighbors('c'))
    print(neighbors_c) ## ['a', 'b', 'd', 'f']

    G.has_node 또는 in operator로 특정 node의 존재 유무를 알 수 있습니다.
    마찬가지로, has_edge 또는 in operator로 특정 edge의 존재 유무를 알 수 있습니다. Edge의 경우, tuple의 형태여야합니다.

    print(G.has_node('a'))  ## True
    print(G.has_node('g'))  ## False
    print('d' in G.nodes)  ## True
    
    print(G.has_edge('a', 'b'))  ## True
    print(G.has_edge('a', 'd')) ## False
    print(('c', 'd') in G.edges) ## True

    마지막으로, G.degree(node) 매서드는 특정 node의 degree 값을 반환합니다. 네트워크 분석할 떄 그 네트워크의 중심이 되는 node들의 degree가 중요하므로, 자주 쓰이는 매서드중 하나입니다.

    print(G.degree('c'))  ## c의 degree: 4

    3. Adjacency list

    이제  adjacency list를 이요하여 네트워크를 구성해봅시다. 아래의 첨부 파일은 삼국지의 인물들의 우호관계 네트워크의 일부분입니다. 한글로 된 파일이기 때문에 utf-8 인코딩을을 파라매터로 넣어줘야합니다.

    three_kingdoms.adjlist
    0.00MB

    파일을 print 해보면 다음과 같습니다. 첫 열의 node를 기준으로 인접한 노드들이 두번째 열부터 나열되어있습니다. 우리는 undirected graph를 이용하기 때문에 반대 방향의 edge는 생략해도 됩니다.

    print(open('three_kingdoms.adjlist', encoding="utf-8").read())
    
    """
    output:
    유비 관우 장비
    관우 장비 관평
    조조 순욱 허저
    순욱 허저
    여포 진궁
    장비
    관평
    진궁
    허저
    """

    이제 nx.read_adjlust() 매서드를 통해 네트워크를 구성해줍니다.

    SG = nx.read_adjlist('three_kingdoms.adjlist')

    그리고 nx.draw를 통해서 네트워크를 시각화합니다. Node의 이름이 한글이면 깨지기 때문에 font_family 설정을 해줘야 합니다.

    import matplotlib.font_manager as fm 
    font_name = fm.FontProperties(fname="c:/Windows/Fonts/malgun.ttf").get_name()
    
    nx.draw(SG, node_size=700, with_labels=True, font_family=font_name)

    유비의 친구의 수 (degree)를 확인해보면 2로 잘 나옵니다.

    SG.degree('유비') ## '유비' node의 degree: 2

    마지막으로 mutual friends를 return하는 함수를 생각해봅시다. Node a와 node b 의 mutual friends란, node a 와 b 모두와 동시에 인접한 노드들을 말합니다. 즉, a와 b의 공통의 친구인 셈이죠.
    List comprehension을 이용하여 아래와 같이 함수를 정의해보았습니다.

    def mutual_friends(G, node_1, node_2):
      result = [node for node in G.neighbors(node_1) if node in G.neighbors(node_2)]
      return result

    결과를 확인해보면 오류 없이 잘 시행됩니다. 유비와 관우와 동시에 친구인 것은 장비뿐이고, 조조와 유비 사이에는 공통의 친구가 없는 것이 확인됩니다.

    SG = nx.read_adjlist('three_kingdoms.adjlist')
    assert mutual_friends(SG, '유비', '관우') == ['장비']
    assert mutual_friends(SG, '유비', '조조') == []

    4. Directed network

    위에서는 undirected network만 다루었으니, 마지막으로 directed network를 다뤄보았습니다.
    Directed network는 nx.DiGraph() 로 생성합니다. 그리고 undirected의 경우와 마찬가지로, edge를 추가해주면 됩니다. 여기서 중요한 점은 directed이기 때문에, 양방향으로 연결된 경우, 양방향의 edge를 모두 넣어줘야한다는 점입니다. 네트워크를 시각화 하면 아래와 같이 화살표 link로 edge가 표현되어 방향성을 알 수 있습니다.

    D = nx.DiGraph()
    
    D.add_edges_from([(1,2),(2,3),(3,2),(3,4),(3,5),(4,5),(4,6),(5,6),(6,4),(4,2)])
    
    nx.draw(D, with_labels=True)

    Node 1에서 node 2로 가는 방향의 edge는 존재하기 때문에 True가 출력되지만, node 2에서 node 1로 가는 edge는 존재하지 않기 때문에 D.has_edge(2,1)의 출력값이 False임을 확인할 수 있습니다.

    print(D.has_edge(1,2))  ## True
    
    print(D.has_edge(2,1))  ## False

    Directed의 경우, 인접한 neighbors는 두가지로 나뉩니다. 하나는 successors로, 해당 node에서부터 출발하여 연결되는 node들의 집합입니다. 다른 하나는 predecessors로, 다른 node에서 출발하여 해당 node로 연결된 node들의 집합입니다. 위의 그림에서 예를 들어보면, node 2의 successors는 node 3 하나이고, predecessors는 node 1,3,4 임을 확인할 수 있습니다.
    마찬가지로, degree도 둘로 나뉩니다. in_degree는 predecessors의 개수이고, out_degree는 successors의 개수입니다.
    Directed network에서 degree 매서드는 in_degree와 out_degree의 합입니다. 따라서 위의 예시에서 node 2의 degree는 in_degree 3 + out_degree 1 = 4 가 됩니다.

    print('Successors of 2:', list(D.successors(2))) ## Successors of 2: [3]
    
    print('Predecessors of 2:', list(D.predecessors(2))) ## Predecessors of 2: [1, 3, 4]
    
    print(D.in_degree(2))  ## 3
    
    print(D.out_degree(2))  ## 1
    
    print(D.degree(2)) ## 4

     

    다음 포스트에서는 homophily와 path 등 좀 더 다양한 네트워크의 특성에 대해서 다뤄보도록 하겠습니다.