인공지능/그래프

[그래프 ML] 파이썬 NetworkX 시작하기

백관구 2024. 3. 8. 17:22
반응형

 

※ 출처 : 그래프 머신러닝 (클라우디오 스타밀레 외, 김기성·장기식 옮김)

 

1. 그래프(Graph) 뜻

* 개체 간의 관계를 설명하는 데 사용되는 수학적 구조
* 소셜 네트워크(팔로우), 지도(길로 이어진 도시), 생물학적 구조, 웹 페이지, 프로세스 등 여러 분야에 사용됨

 

2. 라이브러리 설치

jupyter, networkx, snap-stanford, matplotlib, pandas, scipy

 

3. 단순무향그래프(Simple Undirected Graph)

G=(V,E)G=(V,E)

* VV : 꼭지점(vertex) 또는 노드(node)의 집합
V={v1,...,vn}V={v1,...,vn}

* EE : 두 꼭지점 간의 연결을 나타내는 간선(edge)으로 순서가 없는 순서쌍의 집합
E={(vk,vw),...,(vi,vj)}E={(vk,vw),...,(vi,vj)}
  - 각 간선 간에 순서가 없으므로, (vk,vw),(vw,vk)(vk,vw),(vw,vk)는 같은 간선을 나타냄

import networkx as nx

G = nx.Graph()  # 무향그래프
V = {"서울", "부산", "인천", "대구"}  # 문자열, 클래스, 다른 networkx 그래프와 같이 해시가능한 객체
E = [
    ("서울", "부산"),
    ("서울", "인천"),
    ("서울", "대구"),
    ("부산", "대구")
]
G.add_nodes_from(V)
G.add_edges_from(E)

print(f"V = {G.nodes}")
print(f"E = {G.edges}")
V = ['부산', '서울', '대구', '인천']
E = [('부산', '서울'), ('부산', '대구'), ('서울', '인천'), ('서울', '대구')]

 

 

3.1. 기본 속성

* 그래프 위수(order) |V||V|
  - 노드의 개수

* 그래프 크기(size) |E||E|
  - 간선의 개수

* 노드 차수(degree)
  - 해당 노드에 연결된 간선의 개수

* 노드 근방(neighbor)
  - 노드에 연결된 모든 노드  VV의 부분집합

print(f"그래프 위수: {G.number_of_nodes()}")
print(f"그래프 크기: {G.number_of_edges()}")
print(f"노드 차수: { {v: G.degree(v) for v in G.nodes} }")
print(f"노드 근방: { {v: list(G.neighbors(v)) for v in G.nodes} }")
그래프 위수: 4
그래프 크기: 4
노드 차수: {'부산': 2, '서울': 3, '대구': 2, '인천': 1}
노드 근방: {'부산': ['서울', '대구'], '서울': ['부산', '인천', '대구'], '대구': ['서울', '부산'], '인천': ['서울']}

* 노드 근방그래프(neighborhood graph) 또는 에고그래프(ego graph)
  - 노드에 연결된 노드와 연결된 모든 변으로 구성된 GG의 부분그래프(subgraph)

neighborhood_graph_busan = nx.ego_graph(G, "부산")

print(f"노드: {neighborhood_graph_busan.nodes}")
print(f"간선: {neighborhood_graph_busan.edges}")
노드: ['부산', '서울', '대구']
간선: [('부산', '서울'), ('부산', '대구'), ('서울', '대구')]

 

4. 그래프 종류

유향그래프(Directed Graph)
다이그래프(Digraph)
가중그래프(Weighted Graph) 다중그래프(Multigraph)

 

4.1. 유향그래프(Directed Graph)

G=(V,E)G=(V,E)

* VV : 꼭지점 또는 노드의 집합
V={v1,...,vn}V={v1,...,vn}
  - 머리(Head) : 출발 노드
  - 꼬리(Tail) : 도착 노드

* EE : 두 꼭지점 간의 연결을 나타내는 간선으로 순서가 있는 순서쌍의 집합
E={(vk,vw),...,(vi,vj)}E={(vk,vw),...,(vi,vj)}
  - 각 간선 간에 순서가 있으므로, (vk,vw),(vw,vk)(vk,vw),(vw,vk)는 다른 간선을 나타냄
    . (vk,vw)(vk,vw) : vkvkvwvw
    . (vw,vk)(vw,vk) : vwvwvkvk

* 노드 차수
  - 입력차수(Indegree) deg(v)deg(v) : 노드로 도착하는 간선의 개수
  - 출력차수(Outdegree) deg+(v)deg+(v) : 노드에서 출발하는 간선의 개수

G = nx.DiGraph()  # 유향그래프
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울"),
    ("인천", "서울"),
    ("대구", "서울"),
    ("부산", "인천"),
    ("대구", "부산"),
    ("대구", "인천")
]
G.add_nodes_from(V)
G.add_edges_from(E)

print(f"입력차수: { {v: G.in_degree(v) for v in G.nodes} }")
print(f"출력차수: { {v: G.out_degree(v) for v in G.nodes} }")
입력차수: {'부산': 1, '서울': 3, '대구': 0, '인천': 2}
출력차수: {'부산': 2, '서울': 0, '대구': 3, '인천': 1}

 

4.2. 다중그래프(Multigraph)

* 한 노드에서 출발해서 같은 노드로 도착하는 간선이 여러 개인 다중 간선이 있는 그래프

G=(V,E)G=(V,E)

* VV : 노드의 집합
* EE : 간선의 다중집합
  ※ 다중집합 : 집합의 각 원소에 다중 인스턴스를 허용하는 집합

* 유향다중그래프(Directed Multigraph)
  - EE 순서가 있는 순서쌍의 다중집합
* 무향다중그래프(Undirected Multigraph)
  - EE 순서가 없는 순서쌍의 다중집합

directed_multi_graph = nx.MultiDiGraph()  # 유향다중그래프
undirected_multi_graph = nx.MultiGraph()  # 무향다중그래프

V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울"),
    ("부산", "서울"),
    ("인천", "서울"),
    ("대구", "서울"),
    ("대구", "서울"),
    ("부산", "인천"),
    ("대구", "부산"),
    ("대구", "인천")
]
directed_multi_graph.add_nodes_from(V)
undirected_multi_graph.add_nodes_from(V)
directed_multi_graph.add_edges_from(E)
undirected_multi_graph.add_edges_from(E)

 

4.3. 가중그래프(Weighted Graph)

* 간선가중그래프(Edge-weighted Graph)
  - 일반적인 가중그래프
G=(V,E,w)G=(V,E,w)
  - VV : 노드의 집합
  - EE : 간선의 집합
  - w:ER : 각 간선 eE를 실수 가중값으로 대응시키는 가중 함수

* 노드가중그래프(Node-weighted Graph)
G=(V,E,w)
  - V : 노드의 집합
  - E : 간선의 집합
  - w:VR : 각 노드 vV를 실수 가중값으로 대응시키는 가중 함수

G = nx.DiGraph()
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울", 13),
    ("인천", "서울", 7),
    ("대구", "서울", 10),
    ("부산", "인천", 17),
    ("대구", "부산", 5),
    ("대구", "인천", 15)
]
G.add_nodes_from(V)
G.add_weighted_edges_from(E)

 

5. 다분그래프(Multipartite Graph)

5.1. k분그래프(kth-partite Graph)

* k개의 노드 집합으로 분할할 수 있는 그래프
* 간선은 서로 다른 집합에 대해서만 허용되며, 같은 집합에 속한 노드 안에서는 허용되지 않음
* 응용 예시
  - 신용카드 거래 그래프 분석
  - 문서와 그 안의 개체들을 이용한 이분그래프(bipartite graph)로 문서를 처리하고 정보를 구조화

import pandas as pd
import numpy as np

n_nodes = 10
n_edges = 12

bottom_nodes = [ith for ith in range(n_nodes) if ith%2==0]  # [0, 2, 4, 6, 8]
top_nodes = [ith for ith in range(n_nodes) if ith%2==1]  # [1, 3, 5, 7, 9]
iter_edges = zip(
    np.random.choice(bottom_nodes, n_edges),
    np.random.choice(top_nodes, n_edges)
)
edges = pd.DataFrame([{"source": a, "target": b} for a, b in iter_edges])

B = nx.Graph()
B.add_nodes_from(bottom_nodes, bipartite=0)
B.add_nodes_from(top_nodes, bipartite=1)
B.add_edges_from([tuple(x) for x in edges.values])

from networkx.drawing.layout import bipartite_layout  # 그래프 그리기

pos = bipartite_layout(B, bottom_nodes)
nx.draw_networkx(B, pos=pos)

이분그래프(Bipartite Graph)

 

6. 인접행렬(Adjacency Matrix)

* 원소 Mij의 값은 노드 i에서 노드 j로 가는 간선이 있을 때에는 1, 없을 때에는 0인  정방행렬(|V|×|V|)

* 무향그래프
  - 간선의 방향이 없기 때문에 대칭이 됨

G = nx.Graph()  # 무향그래프
V = {"서울", "부산", "인천", "대구"}  # 문자열, 클래스, 다른 networkx 그래프와 같이 해시가능한 객체
E = [
    ("서울", "부산"),
    ("서울", "인천"),
    ("서울", "대구"),
    ("부산", "대구")
]
G.add_nodes_from(V)
G.add_edges_from(E)

print("Pandas DataFrame:\n", nx.to_pandas_adjacency(G))  # Pandas DataFrame 인접행렬
print("Numpy Array:\n", nx.to_numpy_array(G))  # Numpy Array 인접행렬
Pandas DataFrame:
        부산 서울 대구 인천
부산 0.0   1.0   1.0   0.0
서울 1.0   0.0   1.0   1.0
대구 1.0   1.0   0.0   0.0
인천 0.0   1.0   0.0   0.0
Numpy Array:
[[0. 1. 1. 0.]
 [1. 0. 1. 1.]
 [1. 1. 0. 0.]
 [0. 1. 0. 0.]]

* 유향그래프
  - 대칭이 보장되지 않음

G = nx.DiGraph()  # 유향그래프
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울"),
    ("인천", "서울"),
    ("대구", "서울"),
    ("부산", "인천"),
    ("대구", "부산"),
    ("대구", "인천")
]
G.add_nodes_from(V)
G.add_edges_from(E)

print("Pandas DataFrame:\n", nx.to_pandas_adjacency(G))  # Pandas DataFrame 인접행렬
print("Numpy Array:\n", nx.to_numpy_array(G))  # Numpy Array 인접행렬
Pandas DataFrame:
        부산 서울 대구 인천
부산 0.0   1.0   0.0   1.0
서울 0.0   0.0   0.0   0.0
대구 1.0   1.0   0.0   1.0
인천 0.0   1.0   0.0   0.0
Numpy Array:
[[0. 1. 0. 1.]
 [0. 0. 0. 0.]
 [1. 1. 0. 1.]
 [0. 1. 0. 0.]]

* 다중그래프
  - 같은 노드 쌍을 연결하는 간선이 여러 개 있을 수 있으므로, 1보다 큰 값을 가질 수 있음

G = nx.MultiDiGraph()  # 유향다중그래프
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울"),
    ("부산", "서울"),
    ("인천", "서울"),
    ("대구", "서울"),
    ("대구", "서울"),
    ("부산", "인천"),
    ("대구", "부산"),
    ("대구", "인천")
]
G.add_nodes_from(V)
G.add_edges_from(E)

print("Pandas DataFrame:\n", nx.to_pandas_adjacency(G))  # Pandas DataFrame 인접행렬
print("Numpy Array:\n", nx.to_numpy_array(G))  # Numpy Array 인접행렬
Pandas DataFrame:
        부산 서울 대구 인천
부산 0.0   2.0   0.0   1.0
서울 0.0   0.0   0.0   0.0
대구 1.0   2.0   0.0   1.0
인천 0.0   1.0   0.0   0.0
Numpy Array:
[[0. 2. 0. 1.]
 [0. 0. 0. 0.]
 [1. 2. 0. 1.]
 [0. 1. 0. 0.]]

* 가중그래프
  - 인접행렬의 값이 두 노드를 연결하는 간선의 가중값과 같음

G = nx.DiGraph()
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울", 13),
    ("인천", "서울", 7),
    ("대구", "서울", 10),
    ("부산", "인천", 17),
    ("대구", "부산", 5),
    ("대구", "인천", 15)
]
G.add_nodes_from(V)
G.add_weighted_edges_from(E)

print("Pandas DataFrame:\n", nx.to_pandas_adjacency(G))  # Pandas DataFrame 인접행렬
print("Numpy Array:\n", nx.to_numpy_array(G))  # Numpy Array 인접행렬
Pandas DataFrame:
        부산 서울 대구 인천
부산 0.0  13.0  0.0  17.0
서울 0.0    0.0  0.0    0.0
대구 5.0  10.0  0.0  15.0
인천 0.0    7.0  0.0    0.0
Numpy Array:
[[ 0. 13. 0. 17.]
 [ 0.   0. 0.   0.]
 [ 5. 10. 0. 15.]
 [ 0.   7. 0.   0.]]

 

7. 간선 리스트(Edge List)

* 변 |E|의 크기에 대한 리스트
* 각 원소는 간선이 출발하는 노드와 도착하는 노드의 쌍으로 표현

G = nx.DiGraph()
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울", 13),
    ("인천", "서울", 7),
    ("대구", "서울", 10),
    ("부산", "인천", 17),
    ("대구", "부산", 5),
    ("대구", "인천", 15)
]
G.add_nodes_from(V)
G.add_weighted_edges_from(E)

print(nx.to_pandas_edgelist(G))
   source target weight
0 부산    서울   13
1 부산    인천   17
2 대구    서울   10
3 대구    부산     5
4 대구    인천   15
5 인천    서울     7

 

8. 그래프 그리기

* 노드는 원으로, 간선은 두 노드를 연결하는 선으로 표현

8.1. NetworkX

* networkx.draw 함수를 통해 그래프 개체를 그릴 수 있음

import networkx as nx


def draw_graph(graph, nodes_position: dict, weight):  # nodes_position: {노드 이름: 노드의 위치 좌표(직교좌표계의 2개의 값을 갖는 배열)}
    nx.draw(graph, nodes_position,
            with_labels=True, font_family="Malgun Gothic", font_size=10,  # 각 노드에 이름을 표시
            node_size=400,  # 노드의 크기
            edge_color="grey",  # 간선의 색상
            arrowsize=30)  # 간선의 화살표 크기(유향그래프인 경우)
    if weight:
        edge_labels = nx.get_edge_attributes(graph, "weight")
        nx.draw_networkx_edge_labels(graph, nodes_position, edge_labels=edge_labels)


G = nx.DiGraph()
V = {"서울", "부산", "인천", "대구"}
E = [
    ("부산", "서울", 13),
    ("인천", "서울", 7),
    ("대구", "서울", 10),
    ("부산", "인천", 17),
    ("대구", "부산", 5),
    ("대구", "인천", 15)
]
G.add_nodes_from(V)
G.add_weighted_edges_from(E)

nodes_position = {
    "서울": [1, 1],
    "부산": [1, 0],
    "인천": [0, 1],
    "대구": [0.5, 0]
}
draw_graph(G, nodes_position, True)

* 노드 레이아웃 종류
  - NetworkX는 레이아웃에 따라 각 노드의 위치를 자동으로 계산하는 기능 지원
  - networkx.drawing.layout 라이브러리에 있는 bipartite_layout, circular_layout, kamada_kawai_layout, planar_layout, random_layout, spring_layout, shell_layout, spectral_layout, spiral_layout 레이아웃 활용 가능
  - 복잡한 그래프는 Gephi를 사용하는 것을 추천!

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx
from networkx.drawing.layout import bipartite_layout, circular_layout, kamada_kawai_layout, planar_layout, random_layout, spring_layout, shell_layout, spectral_layout, spiral_layout


n_nodes = 10
n_edges = 20
nodes = np.arange(n_nodes)  # [0, ..., 9]

fig, subs = plt.subplots(nrows=3, ncols=3, figsize=(9, 9))
for sub, layout in zip(subs.flatten(),
                       [bipartite_layout, circular_layout, kamada_kawai_layout, planar_layout, random_layout, spring_layout, shell_layout, spectral_layout, spiral_layout]):
    iter_edges = zip(
        np.random.choice(nodes[:5], n_edges),
        np.random.choice(nodes[5:], n_edges)
    )
    edges = pd.DataFrame([{"source": a, "target": b} for a, b in iter_edges])
    
    G = nx.Graph()
    G.add_nodes_from(nodes)
    G.add_edges_from([tuple(x) for x in edges.values])
    
    if layout.__name__=="bipartite_layout":
        pos = layout(G, nodes=nodes[:5])
    else:
        pos = layout(G)
    nx.draw(G, pos=pos, ax=sub, with_labels=True, font_size=10, node_size=100)
    sub.set_title(layout.__name__)

NetworkX 그래프 레이아웃

 

반응형