아래 튜토리얼은 Python graph-tool 라이브러리의 is_bipartite()와 is_DAG() 함수에 대해, 기초적인 그래프 이론과 함께 어떻게 사용하면 좋을지, 그리고 파이썬 예제 코드와 상세한 한글 주석을 포함한 설명을 다룹니다.
is_bipartite() 함수: 그래프가 이분그래프인지 판별is_bipartite(g, partition=False, find_odd_cycle=False)이 함수는 주어진 무방향 그래프 g가 이분그래프인지 여부를 판별합니다. 인자별 역할은 다음과 같습니다:
g: 검사할 그래프 객체 (undirected로 취급).partition=False:True로 설정 시, 이분그래프일 경우 “두 파트로의 분할 결과”를 반환합니다.find_odd_cycle=False:True로 설정 시, 만약 이분그래프가 아니라면 “홀수 길이 사이클”의 예시를 찾은 뒤 반환합니다.is_bipartite (bool): 이분그래프인지 여부partition (옵션): 이분그래프일 경우, 각 정점이 어느 파트에 속하는지 나타내는 VertexPropertyMapodd_cycle (옵션): 이분그래프가 아닐 경우, 홀수 길이 사이클에 속한 정점 리스트(예: [v1, v2, v3, ...]) 또는 None다음 예제는 graph-tool이 제공하는 is_bipartite 함수를 이용해 이분그래프 여부를 판별하고, 실제로 이분그래프면 파티션을 시각화하는 과정을 보여줍니다.
import graph_tool.all as gt
def example_is_bipartite():
"""
graph-tool의 is_bipartite() 함수를 이용해 그래프가 이분그래프인지 판단하고,
이분 분할 결과(두 파트)를 얻어 시각화하는 예제 코드.
"""
# 1) 2D 격자형 (10x10) 무방향 그래프 생성
g = gt.lattice([10, 10]) # 10x10 lattice
# 2) 이분그래프 판별, 파티션 정보도 같이 반환 (partition=True)
is_bi, part = gt.is_bipartite(g, partition=True)
print(f"Is bipartite? {is_bi}")
if is_bi:
# part는 VertexPropertyMap, 0 또는 1 등의 값이 들어있음
print("Bipartite partition:", part.a) # 각 정점에 대한 파트 번호
# 시각화: 각 파트마다 다른 색을 부여
# part 값이 0 or 1 등으로 표시된다고 가정
pos = gt.sfdp_layout(g)
gt.graph_draw(
g,
pos=pos,
vertex_fill_color=part, # 파트에 따라 색이 다름
output="example_bipartite_partition.png"
)
print("Bipartite partition draw saved to 'example_bipartite_partition.png'")
else:
print("Given graph is NOT bipartite, so no partition to show.")
if __name__ == "__main__":
example_is_bipartite()
lattice)는 사실 이분그래프 구조임. (체스판처럼 2색으로 칠 가능)is_bipartite 호출 시, partition=True로 설정하면, 두 파트에 대한 정점 PropertyMap이 반환.find_odd_cycle=True로 호출하면, 만약 그래프가 이분그래프가 아니면, “홀수 길이 사이클”의 정점 목록을 반환합니다.is_bipartite(g, find_odd_cycle=True) → (False, None, [2, 5, 4])False, 파티션은 None, 홀수 사이클 예시는 [2,5,4].is_DAG() 함수: 유향 그래프가 DAG(방향성 비순환 그래프)인지 판별is_DAG(g)이 함수는 그래프 g가 방향성 비순환 그래프(DAG)인지 여부를 간단히 True/False로 반환합니다.
g: 검사할 유향 그래프 객체.아래 코드는 무작위로 생성된 유향 그래프가 DAG인지 판별한 뒤, min_spanning_tree를 적용해서 만들어진 서브그래프가 DAG인지 재확인하는 예시를 보여줍니다.
import graph_tool.all as gt
def example_is_DAG():
"""
graph-tool의 is_DAG() 함수를 이용해 유향 그래프가 DAG인지 판별하는 예제 코드.
"""
# 1) 30개 정점을 가지는 무작위 그래프 생성(유향 가능)
g = gt.random_graph(30, lambda: (3, 3)) # (out_deg, in_deg) = (3,3)
# 2) is_DAG()로 DAG 여부 확인
result = gt.is_DAG(g)
print(f"Original graph is DAG? {result}")
# 3) 예: Minimum Spanning Tree (MST) 추출 -> edge filter
# MST는 당연히 cycle이 없어야 하므로 DAG여야 가능(단, 이건 유향 간선 취급이 어떻게 되는지 주의)
# 그래프가 유향인 경우도 min_spanning_tree()가 (가중치가 없으면) 임의의 트리를 뽑아낼 수 있음
tree = gt.min_spanning_tree(g) # MST
# MST에 해당하는 간선만 필터링
g.set_edge_filter(tree)
result_after = gt.is_DAG(g)
print(f"After MST edge filter, graph is DAG? {result_after}")
# 시각화
pos = gt.sfdp_layout(g)
gt.graph_draw(
g,
pos=pos,
output="example_is_dag_mst.png"
)
print("Resulting MST DAG drawn at 'example_is_dag_mst.png'")
if __name__ == "__main__":
example_is_DAG()
random_graph(30, lambda: (3,3))는 (대체로) 유향 간선으로 구성된 그래프를 생성. (유향/무향은 파라미터로 결정 가능)is_DAG(g) 호출로 사이클 존재 여부를 판별.min_spanning_tree(g)를 통해 (유향 그래프여도) 일종의 spanning tree edges를 구하고, set_edge_filter()로 간선 필터링을 적용.is_DAG(g) → True가 나올 가능성이 높음(트리 구조이므로).is_bipartite(g, partition=False, find_odd_cycle=False)
is_DAG(g)
둘 모두 그래프의 구조적 특성을 빠르게 판별하는 함수로, 네트워크 분석에서 매우 유용합니다. 예시로 이분그래프 구조를 시각화하거나, DAG인지 확인하여 위상정렬(topological sort) 가능 여부 등을 확인할 수 있습니다.
이상입니다.
아래 튜토리얼은 그래프 이론에서 중요한 평면 그래프(planar graph) 판별 알고리즘에 대한 논문을 소개하면서, 파이썬 graph-tool 라이브러리를 통한 네트워크 분석 및 시각화의 맥락에서, 핵심 내용을 최대한 쉽게 풀어서 설명합니다. “처음 공부하는 학생”도 이해할 수 있도록, 그래프 이론의 개념과 ‘O(n) 시간 복잡도’ 평면성 검사 기법을 친절하고 상세하게 다루겠습니다.
평면 그래프(planar graph): 그래프를 평면 상에서 그렸을 때, 간선들이 교차 없이 그릴 수 있는 그래프.
Planarity Testing(평면성 판별): 입력으로 주어진 무방향 그래프가 평면 그래프인지 아닌지 판별, 그리고 평면이면 그리는 방법(임베딩)을 구성하는 문제.
Kuratowski 정리: 그래프가 평면성이 아니려면, (K5)나 (K{3,3})의 부분그래프(homeomorph)를 포함해야 한다는 고전적인 정리.

선행 연구: Hopcroft-Tarjan, Lempel-Even-Cederbaum(LEC) + PQ-tree(Booth & Lueker), Shih-Hsu의 PC-tree, de Fraysseix-Rosenstiehl의 캐릭터라이제이션 등 다양하고 복잡한 O(n) 알고리즘 존재.
(\star) 본 논문(“On the Cutting Edge: Simplified O(n) Planarity by Edge Addition”) 은,
Biconnected component: 그래프에서 어떤 두 개의 정점을 삭제해도 해당 컴포넌트가 끊어지지 않는 부분 (간단히 말해, cut-vertex를 통해 다른 부분과 분리될 수 없는 맥락).
Edge addition으로 그래프를 만들며, 어떤 간선을 추가하면, 기존에 cut-vertex로 분리되어 있던 biconnected components가 하나로 합쳐질 수 있음.
예시:
각 단계에서, 앞으로도 다른 간선을 추가해야 할 “외부에서 활동이 필요한” (externally active) 정점들은 항상 외부얼굴(external face)에 남아야 한다.
두 biconnected component가 merge될 때, 한쪽을 뒤집어(Flip)서 externally active 정점들이 외부에 남도록 함.
예시:
즉, “각 단계에서 어떤 biconnected component들을 어떻게 merge & flip해야 하는지”를 효율적으로 찾아주고, 실제로 간선 추가(embedding)를 하는 알고리즘의 메인 루틴.
graph_tool.topology.planarity_test() 등으로 제공하지는 않지만, 여러 가지 topology 함수 제공.주의: 아래 코드는 논문 속 pseudo-code를 파이썬 식으로 단순 모사한 것이며, 실제 동작하려면 G̃라는 임베딩 구조를 상세히 구현해야 합니다.
def planarity_check(graph):
"""
논문 Boyer-Myrvold(2004) 스타일 edge-addition planarity 검사(의사코드).
실제로 구현하려면 G~ (임베딩 구조), Walkup/Walkdown, Flip, Merge 등이 필요.
"""
n = graph.num_vertices()
# (1) DFS & lowpoint 계산
parent, order, lowpt = dfs_with_lowpoint(graph)
# (2) G~ (임베딩 구조) 초기화
# - 각 정점에 "backedgeFlag", "pertinentRoots", "separatedDFSChildList" 등 초기화
G_tilde = init_embedding_structure(graph, parent, order, lowpt)
# (3) 역순으로 정점 처리
for v in reversed(range(n)):
# (4) Tree edges: v -> 각 child c
for c in children_of(v, parent):
embed_tree_edge(G_tilde, v, c)
# (6) Back edges: v -> 자손 w
# Walkup 호출: 자손 w를 "pertinent" 표시
for w in find_descendant_back_edges(v, graph):
walkup(G_tilde, v, w)
# (9) 각 child c에 대해 Walkdown
for c in children_of(v, parent):
walkdown(G_tilde, v, c) # 임베딩 시도
# (11) 검사: 안 박힌 back edge가 있으면 non-planar
for w in find_descendant_back_edges(v, graph):
if not is_embedded(G_tilde, v, w):
# (12) Kuratowski subgraph isolate
subg = isolate_kuratowski_subgraph(G_tilde, graph, v)
return (False, subg)
# (14) 임베딩 복원(ShortCircuit 제거 + flip orientation 정리)
final_embedding = recover_planar_embedding(G_tilde)
return (True, final_embedding)
def walkup(G_tilde, v, w):
"""
back edge (v, w)에 대해 'w'와 w->...->v 경로 상 biconnected comp. root들을
pertinent 표시.
"""
# 1) w.backedgeFlag = v
# 2) x, y = w (서로 다른 방향)
# 3) 두 방향으로 external face path 따라가기 ...
# stopping 조건 등등
pass
def walkdown(G_tilde, v, c):
"""
biconnected component root = vc.
back edges (v, w) 임베딩 시도하며 merge & flip.
"""
# merge stack = []
# two pass (양방향):
# while w != vc:
# if w.backedgeFlag == v:
# # merge stack 전부 merge
# # embed (v, w)
# ...
pass
def merge_biconnected_component(G_tilde, root_edge_info):
"""
merge stack pop : (r, direction_in, rc, rc_out)
biconnected comp flip여부 결정, adjacency link등 업데이트
"""
pass
def embed_back_edge(G_tilde, v_root, w):
"""
(v_root, w) 간선 임베딩
-> 새 proper face 형성
-> externall active 유지 등
"""
pass
def isolate_kuratowski_subgraph(G_tilde, graph, v):
"""
- 'edge addition' 불가능해진 시점에서
- figure 11의 minor A~E 중 어떤 상황인지 판단
- homeomorph subgraph (K_5 or K_{3,3}) 구성
- 반환
"""
pass
아래 튜토리얼은 graph-tool 라이브러리에서 제공하는 평면성(planarity) 검사 함수 is_planar()와, 평면 그래프를 최대 평면 그래프(maximal planar graph)로 만드는 함수 make_maximal_planar() 의 사용법, 그리고 내부 알고리즘(특히 Boyer–Myrvold 알고리즘)을 이해하는 데 도움이 되는 기본 원리들을 최대한 친절하게 설명한 것입니다.
평면 그래프(planar graph)란, 2차원 평면에서 간선이 서로 교차하지 않도록 그릴 수 있는 그래프를 말합니다.
그래프의 평면성을 검사(planarity test)하려면, 과거엔 Hopcroft–Tarjan 알고리즘, PQ-tree, PC-tree 등 여러 복잡한 알고리즘이 존재했지만,
Boyer–Myrvold(2004)는 "edge addition" 접근을 사용해 (O(n)) 시간에 평면성을 판별하고, 비평면이면 Kuratowski subgraph도 추출하는 단순화된 방법을 제시했습니다.
graph-tool 라이브러리는 이 Boyer–Myrvold 알고리즘을 기반으로 is_planar() 함수를 구현해 두었으며, 결과로 평면성 여부, 임베딩(embedding) 정보, Kuratowski edge 등을 얻을 수 있습니다.
추가로, make_maximal_planar() 함수는 어떤 평면 그래프를 간선 추가를 통해 ‘최대 평면 그래프(maximal planar graph)’ 로 만드는 (더 이상 간선을 추가하면 교차가 생기는) 확장을 수행합니다.
is_planar() 함수: 그래프 평면성 검사is_planar(g, embedding=False, kuratowski=False)
g: 검사할 Graph 객체(무방향).embedding: True이면, 평면일 때 정점별로 간선을 어떤 순서(시계방향/반시계방향)로 나열해야 하는지 임베딩 정보를 반환.kuratowski: True이면, 비평면인 경우 평면성을 깨는 Kuratowski subgraph를 이루는 에지들의 property map(표시=1)을 반환.is_planar (bool): 평면성 여부embedding (선택, VertexPropertyMap): 각 정점에 대해, “시계방향으로 이어진 out-edges의 인덱스 순서” 리스트kuratowski (선택, EdgePropertyMap): Kuratowski subgraph를 이루는 에지면 1, 아니면 0.장점:
1. (O(n)) 시간
2. 구현 간단
3. Kuratowski 서브그래프도 추가로 얻기 가능
아래는 graph-tool에서 is_planar()를 사용하는 간단 예시입니다.
import graph_tool.all as gt
import numpy as np
def example_is_planar_basic():
# 1) 임의 그래프(삼각분할) 만들기
# triangulation()은 임의 2D 점들의 델로네 삼각분할 -> 보통 planar
coords = np.random.rand(100, 2) # 100개의 (x,y) 난수
g, pos = gt.triangulation(coords) # g: 그래프, pos: 위치 정보
# 2) 평면성 검사 : 임베딩 정보도 뽑기(embedding=True)
planar, embedding = gt.is_planar(g, embedding=True)
# 결과 출력
print("Is planar?", planar) # 대체로 True일 확률 높음
# 임베딩 정보: 각 정점에 대해, 나가는(혹은 incident) edges의 시계방향순 인덱스 목록
if planar:
for v in g.vertices():
emb_list = list(embedding[v]) # 임베딩된 간선 인덱스의 시계방향 순서
print(f"Vertex {int(v)} embedding order:", emb_list)
# 3) 시각화 : planar_layout() 등으로도 layout을 얻을 수 있지만,
# triangulation 이미 pos를 줌. 그래프 그리기
gt.graph_draw(g, pos=pos, output="example_is_planar_basic.svg")
실행해보면, 대부분 triangulation 그래프는 평면이므로 planar == True. 이 경우, embedding[v] 에서 각 간선 인덱스의 순서 정보를 확인할 수 있습니다.
def example_is_planar_kuratowski():
# 1) 임의로 non-planar 그래프 만들기 (ex. K_5)
g = gt.Graph()
g.add_vertex(5)
# 완전그래프(5)
for i in range(5):
for j in range(i+1,5):
g.add_edge(g.vertex(i), g.vertex(j))
# 2) 평면성 검사 : kuratowski=True
planar, kur_map = gt.is_planar(g, kuratowski=True)
print("Is planar?", planar) # False
# 3) kuratowski subgraph를 시각화 위해 edge_property bool적용
# True이면, 이 에지는 K-subgraph의 일부
g.set_edge_filter(kur_map, inverted=False)
# Layout
# non-planar라 layout이 완벽히 평면적으로 안되지만 시각화 목적
pos = gt.sfdp_layout(g)
# 필터된 서브그래프(=Kur-subgraph)만 draw
gt.graph_draw(g, pos=pos, output="example_kuratowski_subgraph.svg")
kur_map은 에지 프로퍼티 맵(edge property map). kur_map[e] == 1이면 Kuratowski subgraph의 일부.K_5 전체가 Kuratowski이므로 거의 모든 에지가 1로 표시.make_maximal_planar() 함수: 최대 평면 그래프 만들기make_maximal_planar(g)
g: 반드시 biconnected planar graph(단절점 없이 연결된 평면 그래프)이어야 하며, num_vertices() >= 3.import graph_tool.all as gt
def example_make_maximal_planar():
# 1) 간단한 평면 그래프 생성: 10 x 10 격자(lattice)
# 보통 lattice는 연결은 되지만 (0,0) ~ (9,9)로 cut-vertex 발생 -> biconnected 아님
# -> graph-tool doc 예시에서는 biconnected "라고 가정" ->
# lattice(10,10)은 actually biconnected가 아님.
# 그냥 예시로 써보면, 한번 시도해본다.
g = gt.lattice([10, 10])
# 우선 is_planar로 테스트
p = gt.is_planar(g)[0] # embedding=False, kuratowski=False
print("Before make_maximal_planar, is_planar:", p) # True
# 2) make_maximal_planar 시도
# g가 biconnected 가정이 아니면 에러 or behavior undefined일 수 있음.
gt.make_maximal_planar(g) # 평면임을 가정 & biconnected라고 가정
# 3) 다시 평면성 검사
p = gt.is_planar(g)[0]
print("After make_maximal_planar, is_planar:", p) # True
# 4) 간선 수 출력 -> 3V - 6 = 3*100 - 6 = 294
print("num_vertices:", g.num_vertices(), " num_edges:", g.num_edges())
# 5) 시각화
pos = gt.planar_layout(g) # planar embedding 기반 layout
gt.graph_draw(g, pos, output="maximal_planar.png")
print("Saved as 'maximal_planar.png'")
실제로 lattice(10,10) 예시는 biconnected가 아닐 수 있으므로, strict 모드에서는 make_maximal_planar()가 동작 안할 수도 있습니다(또는 특정 예외). 하지만 doc 예시에선 이런 식으로 사용법을 보여주고 있습니다.
진짜 biconnected planar 그래프 예시로는 원형 그래프(cycle) or 다각형 + chords, 델로네 삼각분할해서 안에 추가 간선을 제거한 평면 그래프 등등을 만들면 좋습니다.
is_planar():make_maximal_planar():graph-tool 을 쓰면, 이와 같은 복잡한 로직을 한두 줄로 간단히 호출 가능하므로, 네트워크 분석, 기하학적 그래프 시각화 등에서 매우 편리합니다.
boyer_myrvold_planar_test 문서: 링크 아래 내용은 “Reciprocity of Networks with Degree Correlations and Arbitrary Degree Sequences”라는 논문( arXiv:0706.3372 )의 일부를 기반으로 작성되었습니다. 이 논문은 네트워크(그래프)에서 관측되는 reciprocity(호혜성)가 단순한 무작위 효과로부터 기인하는지, 아니면 네트워크의 더 깊은 구조적 특성을 반영하는지를 이론적으로 분석하며, 특히 노드 차수 간 상관관계(degree correlations)를 어떻게 고려하면 호혜성이 달라지는지를 다루고 있습니다.
우리가 다루는 네트워크(그래프)에는 종종 단방향 연결(unidirectional edges)뿐 아니라 양방향 연결(bidirectional edges), 즉 서로를 연결하는 "쌍방 링크"가 섞여 있습니다. 예를 들어,
이러한 양방향 링크(Reciprocal)의 비율을 reciprocity(또는 호혜성)이라 하며, 고전적 정의로는
[
r \;=\; \frac{L{\leftrightarrow}}{L}\,,
]
여기서 (L{\leftrightarrow})는 양방향 연결의 총 개수, (L)은 네트워크의 모든 (방향성) 링크 수를 뜻합니다.
하지만 실제 네트워크에는 단순히 "랜덤"해서 생긴 호혜성 말고도, 노드 차수의 분포 및 상관관계가 reciprocity에 크게 영향을 줄 수 있다고 논문은 주장합니다. 예컨대, 입력 차수(in-degree)와 출력 차수(out-degree)가 특정한 통계적 성질(예: 특정 분포, 특정 상관관계 등)을 가질 때, 이들의 조합이 커지거나 작아지는 식으로 reciprocity가 결정될 수 있다는 것이죠.
본 논문에서는 통계적(enumeration) 기법을 통해, 주어진 네트워크가 (1) 임의의 차수분포를 가지되, (2) 노드 차수 간 특정 상관관계를 가지고, (3) 노드쌍(에지쌍)에 대해서도 특정한 2-노드 차수 상관(예: source 노드의 out-degree와 target 노드의 in-degree가 상관됨 등)을 가질 때, 예상되는 reciprocity가 어느 정도인지(이론적 식) 제시합니다.
이 논문의 핵심 아이디어는 다음과 같습니다:
in-degree = k_i^in, out-degree = k_i^out라 하자. (편의상 논문에서는 ((k_i, k_o))처럼 표기)이를 통해, 각 링크가 쌍방(양방향)으로 존재할 확률을 계산하면, 네트워크 전반의 호혜성 (r)를 구할 수 있다는 것.
논문에서 제시되는 대표 식 중 하나:
[
r{1n2n} \;=\; \frac{1}{L}\sum{k,q} L(k \to q) \cdot \frac{L(k \leftarrow q)}{N(k) \, N(q)},
]
이는 "노드 단의 상관(1-node correlation)" + "에지쌍(2-node correlation)" 모두 고려한 식.
(논문 표기: (r_{1n2n}) )
논문에서는 상관관계가 없거나(또는 단순한 경우)면 이 식이 간단히 줄어들어서, 예컨대 in-degree와 out-degree가 독립이라면 (1-node correlation이 없는 경우)나, 2-node 중 어떤 것만 상관 있고 다른 것은 무시하면, reciprocity (r)가 어떠어떠한 간단 식으로 정리되는지를 보여줍니다.
대표적으로,
1-node correlation만 있는 경우:
[
r{1n} \;=\; \frac{\langle k_i k_o \rangle^2}{\langle k_i \rangle^4}
\frac{L}{N^2}.
]
(대략 이런 꼴: (r{1n} = \frac{L}{N^2}\times \frac{\langle k_i k_o\rangle^2}{\langle k_i\rangle^4}))
2-node (out-out) correlation만 있는 경우:
[
r{2n:o/o} = \frac{1}{L}\sum{k_o,q_o} L(k_o \to q_o) \frac{L(k_o \leftarrow q_o)}{N(k_o) N(q_o)}
]
(이것도 특정 분포함이 주어지면 단순화할 수 있음.)
이러한 식들로부터, 원하는 수준의 상관관계를 부여한 "랜덤 네트워크"에 대한 호혜성 기대값을 이론적으로 구할 수 있게 됩니다.
논문에서는 이 식들을 실제 네트워크(예: 무역 네트워크, 신경망, 위키피디아 웹 등)에 적용하여 "차수 분포와 상관관계만"으로도 관측된 호혜성을 얼마나 설명할 수 있는지 분석했습니다.
결과적으로, "차수 기반의 상관관계만"으로도 호혜성이 어느 정도까지 설명 가능한지를 보여주고, 실제론 추가적인 구조가 들어가야(모듈성, 커뮤니티, 계층성 등) 호혜성 전체를 설명할 수 있다는 결론.
파이썬 graph-tool 라이브러리와 연동하여, 만약 우리가 어떤 방향성 그래프 g를 가지고 있고, 그 네트워크의 차수 분포나 상관관계를 측정했을 때, 그때의 "이론적 reciprocity"를 계산하려면 다음과 같은 과정을 거칠 수 있습니다.
import graph_tool.all as gt
from collections import Counter
def get_inout_degs(g):
"""
각 노드별 (in-degree, out-degree)를 추출한다.
g: graph_tool.Graph (directed)
returns: list of (in_degree, out_degree)
"""
degs = []
for v in g.vertices():
in_deg = v.in_degree()
out_deg = v.out_degree()
degs.append((in_deg, out_deg))
return degs
N(k)와 링크 상관 L(k->q) 계산def measure_degree_distribution(g, degs):
"""
degs: list of (in_deg, out_deg)
반환값:
N_dict: {(in_deg, out_deg): 개수}
L_dict: {(in_deg, out_deg) -> (in_deg2, out_deg2): 개수}
"""
# N_dict 계산
N_dict = Counter(degs)
# L_dict 계산: 인접 행렬(혹은 adjacency) 순회
L_dict = Counter()
for s in g.vertices():
s_k = (s.in_degree(), s.out_degree())
for t in s.out_neighbors():
t_k = (t.in_degree(), t.out_degree())
L_dict[(s_k, t_k)] += 1
return N_dict, L_dict
이렇게 (s_k, t_k) 식으로 카운팅하면, (\textstyle L(k \to q))에 대응하는 자료구조가 생김.
[r{1n2n} = \frac1L \sum{k,q} L(k\to q) \frac{L(k\leftarrow q)}{N(k)\,N(q)}. ]
def reciprocity_1n2n(g, N_dict, L_dict):
"""
이론식 r_{1n2n} = 1/L sum_{k,q} [ L(k->q)* L(k<-q) / (N(k)*N(q)) ]
단, k, q는 (in_deg, out_deg) 튜플
"""
L = g.num_edges()
val_sum = 0.0
for (k, q), L_kq in L_dict.items():
# L(k->q) = L_kq
# L(k<-q) = L_dict.get((q, k), 0)
L_qk = L_dict.get((q, k), 0)
if L_qk == 0:
continue
# N(k) = N_dict[k], N(q) = N_dict[q]
Nk = N_dict[k]
Nq = N_dict[q]
val_sum += L_kq * L_qk / (Nk * Nq)
return (1.0 / L) * val_sum
이렇게 하면, 논문에서 언급된 r1n2n이론값을 계산 가능. 만약 실제 그래프의 reciprocity는 단순히 (양방향 에지 수)/(전체 에지 수)로 계산이 가능:
def real_reciprocity(g):
"""
실제 호혜성 = (양방향 edge 개수) / (전체 edge 개수)
"""
L = g.num_edges()
L_bi = 0
for e in g.edges():
s = e.source()
t = e.target()
# 만약 t->s도 있다면...
if g.edge(t, s) is not None:
L_bi += 1
# 이런 식은 e,t 쌍에 대해 2번 counting됨 -> L_bi/2 가 진짜 양방향 개수
# 하지만 reciprocity는 L_bi / L (양방향의 '방향성 에지' 개수로 치면 2* #양방향임.)
# 일반적으로 r = (양방향 연결 쌍수) / L
# => (L_bi/2) / L
return (L_bi/2.0) / L
reciprocity_1n() : 1-node correlation만 고려한 식reciprocity_2n_o_o(): 2node (out-out) correlation만 고려reciprocity_2n_i_o(): 2node (in-out) correlation만 고려graph-tool 등에서 실제 그래프 g의 in/out-degree를 추출하여, 제시된 식에 따라 이론값을 계산한 뒤, 실제 r과 비교해볼 수 있음. 이처럼, 네트워크 이론에서 reciprocity는 단순한 무작위 성분이 아닌, 다양한 차수 상관관계의 구조적 결과물이기도 하며, 차수 정보만으로도 꽤 정확히 예측할 수 있음을 보여주는 결과라고 요약할 수 있습니다.
호혜성(Reciprocity) 정의
[
r = \frac{L{\leftrightarrow}}{L},
]
여기서 (L{\leftrightarrow})는 양방향 에지 쌍의 개수.
일반 식 (1-node + 2-node 상관관계 모두 있을 때)
[
r{1n2n} \;=\; \frac{1}{L} \sum{k,q} L(k\to q)\; \frac{L(k\leftarrow q)}{N(k)\;N(q)}.
]
( (N(k)): 차수가 k인 노드 수, (L(k\to q)): 차수 k->q인 링크 수 )
1-node 상관만 있는 경우
[
r_{1n} \;=\; \frac{\langle k_i\,k_o\rangle^2}{\langle k_i\rangle^4} \;\times\; \frac{L}{N^2}
]
(정확한 형태는 논문 내 파트 참고)
2-node (out-out) 상관만 있는 경우
[
r{2n:o/o} \;=\; \frac{1}{L}\sum{k_o,q_o}\; L(k_o\to q_o)\; \frac{L(k_o\leftarrow q_o)}{\,N(k_o)\,N(q_o)\,}
]
import graph_tool.all as gt
from collections import defaultdict
def calculate_reciprocity_1n2n(g):
"""
논문 arXiv:0706.3372 식 (3)에 해당하는,
'1-node + 2-node' correlation을 모두 고려한 r_{1n2n} 추정.
"""
# 1) 전체 edge 수
L = g.num_edges()
if L == 0:
return 0.0
# 2) 각 vertex의 in/out deg -> (in_deg, out_deg)
deg_map = {} # v -> (in_deg, out_deg)
for v in g.vertices():
deg_map[int(v)] = (v.in_degree(), v.out_degree())
# 3) N_dict[(k_in, k_out)] = 해당 차수를 가진 노드 수
from collections import Counter
N_dict = Counter(deg_map.values())
# 4) L_dict[((k_in1,k_out1),(k_in2,k_out2))] = #링크수
L_dict = Counter()
for e in g.edges():
s, t = int(e.source()), int(e.target())
sk = deg_map[s]
tk = deg_map[t]
L_dict[(sk, tk)] += 1
# 5) 식 계산
val_sum = 0.0
for (k, q), L_kq in L_dict.items():
# k->q : L_kq
# k<-q : L_qk
L_qk = L_dict.get((q, k), 0)
if L_qk == 0:
continue
# N(k) = N_dict[k], N(q) = N_dict[q]
Nk = N_dict[k]
Nq = N_dict[q]
val_sum += L_kq * L_qk / (Nk*Nq)
# r_{1n2n} = (1/L)*val_sum
return val_sum / float(L)
def real_reciprocity(g):
"""
실제 네트워크의 reciprocity 계산
"""
L = g.num_edges()
if L == 0:
return 0.0
count_bi = 0
for e in g.edges():
s, t = e.source(), e.target()
if g.edge(t, s) is not None:
count_bi += 1
# count_bi는 양방향 edge '쌍' 중 한쪽 방향만 센 것 => 실제 bidir edge 개수= count_bi/2
# r = (bidirectional 개수) / L
return (count_bi / 2.0) / L
def example_reciprocity_arxiv0706():
# 1) 임의의 directed graph 생성 or load
g = gt.random_graph(50, lambda: (2,2), directed=True)
# (노드 50개, 평균차수 대략 2)
# 2) 실제 호혜성
r_real = real_reciprocity(g)
# 3) 논문의 r_{1n2n} 식으로 예측값 계산
r_theo = calculate_reciprocity_1n2n(g)
print("Actual reciprocity (real) =", r_real)
print("Theoretical (1n+2n) reciprocity =", r_theo)
if __name__ == "__main__":
example_reciprocity_arxiv0706()
위 코드는 (1) 임의 방향 그래프를 만들고, (2) 실제 호혜성 r_real, (3) 논문의 1node+2node correlation 기반 이론값 r_theo를 출력하는 예시입니다. 실제 네트워크 예시로 대체해볼 수도 있습니다.
실무적 의의:
아래 내용은 파이썬 graph-tool 라이브러리에서 제공하는 edge_reciprocity() 함수에 대한 설명과, 이를 활용한 초보자도 이해하기 쉬운 튜토리얼을 담고 있습니다. 특히 “Reciprocity(호혜성)” 지표가 무엇이며, graph-tool을 통해 어떻게 계산하는지, 그리고 어떻게 활용할 수 있는지를 단계별로 자세히 정리해보겠습니다.
graph_tool.topology.edge_reciprocity(g, weight=None) 함수는 네트워크(그래프)의 호혜성을 계산합니다.
방향 그래프(directed graph)에서, 양방향 연결(서로를 잇는 쌍방 링크)이 얼마나 많이 존재하는지를 나타내는 척도입니다.
전통적인 정의로는,
[
r \;=\; \frac{L{\leftrightarrow}}{L}\,,
]
여기서 (L{\leftrightarrow})는 양방향(서로를 잇는) 에지 쌍의 개수, (L)은 총 방향성 에지의 개수입니다.
예를 들어, (u \to v) 에지가 있고, 동시에 (v \to u) 에지가 있으면, 양방향 연결 하나가 존재한다고 합니다.
edge_reciprocity()는 가중치 옵션도 지원합니다. import graph_tool.all as gt
def demo_edge_reciprocity_basic():
# 1) 간단한 방향 그래프 g 생성
g = gt.Graph(directed=True)
# 2) 노드 2개 추가
v0 = g.add_vertex()
v1 = g.add_vertex()
# 현재 g.num_vertices() == 2
# 3) 일방향 간선 v0->v1 추가
e1 = g.add_edge(v0, v1)
# 4) 호혜성 계산
r1 = gt.edge_reciprocity(g)
print("초기 호혜성(양방향 연결 없음) =", r1) # 예상: 0.0
# 5) 반대방향 v1->v0 간선 추가
e2 = g.add_edge(v1, v0)
# 6) 다시 호혜성 계산
r2 = gt.edge_reciprocity(g)
print("양방향 연결 생긴 후 호혜성 =", r2) # 예상: 1.0
if __name__=="__main__":
demo_edge_reciprocity_basic()
v0->v1만 있어서 양방향 연결이 하나도 없으므로 r = 0.0.v1->v0를 추가하면 두 노드가 서로 이어지므로, 모든 에지가 사실상 양방향 쌍(1쌍)뿐이므로 r = 1.0.아래 예시는 무작위로 그래프를 생성하고, 가중치도 임의 부여하여 호혜성을 계산해보는 코드입니다:
import graph_tool.all as gt
import numpy as np
import random
def demo_edge_reciprocity_weighted():
# 1) directed=True 로 임의 그래프 생성
g = gt.random_graph(10, lambda: (random.randint(1,3),random.randint(1,3)), directed=True)
# 위처럼 하면 대략 10개 노드, 임의의 average out-degree(1~3)
# 2) 간선 weight property 만들기
w_prop = g.new_edge_property("double")
for e in g.edges():
w_prop[e] = random.uniform(0.5, 2.0) # 0.5 ~ 2.0 사이 임의값
# 3) 가중치 기반 호혜성 계산
r = gt.edge_reciprocity(g, weight=w_prop)
print("Weighted edge reciprocity =", r)
if __name__=="__main__":
demo_edge_reciprocity_weighted()
gt.edge_reciprocity(g, weight=w_prop)로 가중치 포함 reciprocity를 얻습니다.edge_reciprocity(g)는 내부적으로 모든 에지를 훑어보며, 해당 에지의 반대방향이 존재하는지 검사하여, 양방향 여부를 판별합니다.O(E)로 표기. 즉 (E = L)은 에지 개수. 문서 예시에서처럼 g = gt.collection.data["pgp-strong-2009"] 라는 그래프를 불러온 후:
import graph_tool.all as gt
def demo_pgp_network():
g = gt.collection.data["pgp-strong-2009"] # PGP 네트워크 (directed)
# 호혜성 계산
r = gt.edge_reciprocity(g)
print("PGP strong(2009) network reciprocity:", r)
if __name__=="__main__":
demo_pgp_network()
0.692196963163...이상으로, 파이썬 graph-tool 라이브러리의 edge_reciprocity() 함수 사용법을 처음부터 코드 예시와 함께 살펴보았습니다.
edge_reciprocity(g, weight=None): 그래프 G에 대해 (가중치가 있으면 weight를 적용하여) 호혜성 값을 계산해주는 편리한 함수.추가로, 필요하다면 gt.is_planar(), gt.min_spanning_tree() 등과 조합해서, 네트워크의 평면성, MST, 커뮤니티 등 다양한 분석과 함께 호혜성을 해석해볼 수 있습니다.
(끝)
아래 내용은 여행하는 외판원 문제(Travelling Salesman Problem, TSP)에 관한 긴 위키피디아 내용을 바탕으로, 처음 공부하는 학생도 이해할 수 있도록 한글로 친절하고 자세하게 설명하며, 파이썬 예제 코드를 포함한 튜토리얼을 제공하는 형태로 작성되었습니다. 특히 도시·교통 빅데이터 관점에서 TSP가 왜 중요한지, 어떤 이론적/실용적 배경이 있는지, 그리고 간단한 파이썬 예시 코드를 통해 TSP를 어떻게 구현(또는 근사해결)할 수 있는지를 살펴보겠습니다.
여행하는 외판원 문제(Travelling Salesman Problem, TSP)는 다음과 같이 정의됩니다:
“서로 다른 (n)개의 도시가 주어지고, 각 도시 쌍 사이의 이동 비용(거리)이 주어졌다고 할 때, 어떤 외판원이 모든 도시를 정확히 한 번씩 방문하고 다시 출발 도시로 돌아오는 경로 중, 그 이동 비용(거리) 합이 최소가 되는 경로(해밀턴 순환)를 찾아라.”
[
\begin{aligned}
\text{minimize}& \quad \sum{i=1}^{n}\sum{\substack{j=1 \ j \neq i}}^{n} c{ij} x{ij},\
\text{subject to}& \quad \sum{\substack{i=1 \ i\neq j}}^{n} x{ij} = 1\quad(\forall j), \
& \quad \sum{\substack{j=1 \ j\neq i}}^{n} x{ij} = 1\quad(\forall i), \
& \quad \sum{i \in Q}\sum{\substack{j \in Q \ j\neq i}} x{ij} \le |Q|-1 \quad (\forall Q \subset {1,\dots,n}, |Q|\ge2 ),\
& \quad x{ij}\in {0,1}.
\end{aligned}
]
실제로는, 크기가 큰 TSP에 대한 ‘완벽해’를 구하기보다는 근사·휴리스틱 알고리즘을 통해 빠르게 (때론 최적 대비 1~3% 이내) 해를 얻습니다.
아래는 파이썬에서 간단히 2-Opt를 통해 TSP 경로를 개선하는 예제 코드입니다.
(단, 여기서는 그래프 형태보다는 편의상 도시 간 거리 행렬을 이용합니다.)
import math
import random
def calc_tour_length(tour, dist_matrix):
"""
tour: 도시 방문 순서 리스트, 예: [0, 2, 1, 3, 0]
dist_matrix: dist_matrix[i][j] = i->j 거리(혹은 비용)
"""
length = 0.0
for i in range(len(tour) - 1):
length += dist_matrix[tour[i]][tour[i+1]]
return length
def two_opt_swap(tour, i, k):
"""
tour[i..k] 구간을 뒤집어서 (i, i+1) 및 (k, k+1) 구간을 바꿔새 경로 생성
"""
new_tour = tour[0:i]
new_tour += reversed(tour[i:k+1])
new_tour += tour[k+1:]
return list(new_tour)
def solve_tsp_2opt(dist_matrix):
"""
2-Opt 로컬 서치 기반 TSP 해결 (단, 시작도시=0 고정)
dist_matrix: NxN 형태의 거리행렬
"""
n = len(dist_matrix)
# 1) 임의 혹은 간단한 초기 순회 생성: [0 -> 1 -> 2 -> ... -> n-1 -> 0]
curr_tour = list(range(n))
curr_tour.append(0)
best_dist = calc_tour_length(curr_tour, dist_matrix)
improved = True
while improved:
improved = False
# 2) 모든 (i, k) 쌍에 대해 swap 시도
for i in range(1, n-1):
for k in range(i+1, n):
if k - i == 1:
continue # swap 이득이 없을 가능성 높음
new_tour = two_opt_swap(curr_tour, i, k)
new_dist = calc_tour_length(new_tour, dist_matrix)
if new_dist < best_dist:
curr_tour = new_tour
best_dist = new_dist
improved = True
break # 이중 for문 탈출위한 조치
if improved:
break
return curr_tour, best_dist
# -- 시연 예시 --
if __name__=="__main__":
# 도시 5개, 무작위 distance matrix 생성(대칭, triangle inequality 대충 만족)
n_cities = 5
coords = [(random.random()*100, random.random()*100) for _ in range(n_cities)]
# 유클리드 거리행렬
dist = [[0]*n_cities for _ in range(n_cities)]
for i in range(n_cities):
for j in range(n_cities):
if i==j:
dist[i][j]=0
else:
dx = coords[i][0] - coords[j][0]
dy = coords[i][1] - coords[j][1]
dist[i][j] = math.sqrt(dx*dx + dy*dy)
# 2opt로 순회계획
route, length = solve_tsp_2opt(dist)
print("2-Opt 결과 경로:", route)
print("경로 길이:", length)
calc_tour_length(): 순회(tour)의 길이(=모든 구간 거리 합)를 계산.two_opt_swap(): 구간 [i..k]를 뒤집어 경로를 수정하는 스왑 연산.solve_tsp_2opt():[0, 1, 2, ..., n-1, 0] 기본 순회를 초기해로 설정(간단).dist를 만든 뒤 2-Opt를 실행.이 간단한 2-Opt는 인스턴스가 커지면 시간이 많이 걸릴 수 있지만( (\approx O(n^2)) ~ (O(n^3))로 추정 ), 중간 규모(수백~수천 도시) 정도에서는 충분히 빠른 편이어서 TSP 근사해로 자주 활용됩니다.
(끝)
아래 튜토리얼은 그래프 툴(graph-tool) 라이브러리에서 제공하는 tsp_tour() 함수를 활용해, 간단한 TSP (여행하는 외판원 문제) 경로를 구하는 방법을 단계별로 설명합니다. 도시, 교통 빅데이터, 네트워크 분석 관점에서 TSP는 여러 용례(물류/배송 경로 최적화, 공정 라우팅 등)에서 자주 등장합니다. 이 튜토리얼에서는 초보자도 따라할 수 있도록 파이썬 코드를 포함해 최대한 정성스럽고 실용적으로 구성했습니다.
tsp_tour() 함수 개요함수: graph_tool.topology.tsp_tour(g, src, weight=None)
목적:
인자:
1로 간주.반환:
tour: numpy.ndarray 형태, 정점 번호의 리스트. 예: [0, 1, 2, ... 0]시간 복잡도: (O(E + V \log V)) (Boost 문서 기준)
tsp_tour 간단 사용법아래 예제에서는 graph-tool로 무작위 격자 그래프를 만든 뒤, tsp_tour()로 투어를 구합니다.
import graph_tool.all as gt
import numpy as np
def example_tsp_tour():
# 1) 무방향 그래프 g 만들기
# 예) 10x10 격자 (lattice)
g = gt.lattice([10, 10]) # 100개 정점, 2D 격자식 연결, 무방향
# 2) 시작 정점(src) 지정: 일단 0번 정점 (g.vertex(0) 객체)
src_vertex = g.vertex(0)
# 3) tsp_tour 호출
# - weight=None: 모든 엣지 비용=1 로 처리 (default)
# - weight=some_edge_prop: 가중치 적용 가능
tour_array = gt.tsp_tour(g, src=src_vertex, weight=None)
# tour_array: numpy.ndarray, 예) [ 0, 1, 2, ..., 0 ]
print("TSP Tour (vertex indices):", tour_array)
print("Number of visited vertices (including the last repeated src):", len(tour_array))
# 4) 간단히 길이 계산 (모든 엣지=1 이므로, = 투어에 포함된 엣지 수)
# graph-tool은 undirected -> tsp_tour 로 반환된 경로의 간선수 = len(tour_array)-1
# => 2D lattice에서 실제 '최단'과 비교: 여기서는 단순히 예시.
tour_length = len(tour_array) - 1 # weight=None -> 그냥 edge=1씩
print("Approx. TSP tour length:", tour_length)
# 5) 시각화 (optional)
# planar_layout은 일반적으로 planar 그래프에서만 쓰이지만
# 여기서는 lattice 구조가 충분히 planar -> 레이아웃 대충 나타냄
pos = gt.planar_layout(g)
# 만약 layout이 proper하지 않으면 다른 layout 함수 예: sfdp_layout(등)
# 그래프 그리기: 투어에 사용된 간선을 강조
# - 먼저 edge property map 생성하여, 투어 간선이면 color=red, 아니면 gray
ecolor = g.new_edge_property("string", val="gray") # 기본회색
# 'tour_array' 인접한 정점 쌍마다 edge를 찾아서 color=red
for i in range(len(tour_array)-1):
v1 = tour_array[i]
v2 = tour_array[i+1]
# v1->v2 또는 v2->v1 edge 찾아서 ecolor[e]="red"
# (그래프가 undirected 이므로 어느방향 edge든 동일)
e = g.edge(v1, v2)
if e is not None:
ecolor[e] = "red"
# draw
gt.graph_draw(g, pos=pos,
vertex_text=g.vertex_index,
edge_color=ecolor,
output_size=(600, 600),
output="lattice_tsp_tour.png")
print("Graph with TSP tour drawn to 'lattice_tsp_tour.png'")
if __name__=="__main__":
example_tsp_tour()
gt.lattice([10,10]): 10x10 정점을 가진 2차원 격자 그래프 생성 (무방향).src_vertex = g.vertex(0): 정점 0을 출발·귀환 지점으로 지정.tour_array = gt.tsp_tour(g, src_vertex): TSP 투어를 근사적으로 구함. tour_array는 numpy 배열 형태로 순회 순서의 정점 번호가 들어 있음. 맨 끝 원소가 다시 0(시작점).weight=some_eprop로 지정 가능.e = g.edge(v1, v2)로 해당 엣지를 찾고 color를 "red"로 설정.tsp_tour()는 Boost Graph Library의 metric TSP approximate 알고리즘을 활용하며, g가 무방향이며(undirected), 삼각 부등식을 만족(메트릭)해야 정확히 2-approx 성립.tsp_tour() 함수는 그래프 툴 라이브러리를 활용해 TSP 근사경로를 쉽게 구할 수 있게 해줍니다. (튜토리얼 끝)
아래 튜토리얼은 그래프 이론에서의 그래프 색칠(Graph Coloring) 개념을 체계적으로 살펴보며, 수식과 개념, 원리를 친절하고 정성스럽게 소개하는 내용입니다. 도시와 교통 빅데이터, 그리고 네트워크 분석과도 밀접히 연관되어 있으며, 처음 공부하는 학생도 확실히 이해할 수 있도록 꼼꼼히 설명해볼게요.
그래프 색칠이란, 간단히 말해 그래프의 원소(정점, 혹은 간선, 혹은 면)에 대해 특정 ‘색’(또는 레이블)을 할당하되, 일정한 제약조건을 만족하도록 하는 기법을 의미합니다. 예를 들어,
이 중 보통 “그래프 색칠”이라고 하면 ① 번의 정점 색칠을 의미하는 경우가 많습니다.
가장 흔히 다루는 것은 정점 색칠입니다.
그래프 (G)에 대해, “(t)가지 색으로 색칠하는 경우의 수”를 세는 함수를 색칠 다항식 (P(G,t))라고 합니다.
그래프 색칠 문제가 쉽지 않은 이유:
결론적으로, 그래프 색칠은 (1) 이론적 깊이와 (2) 실용적 응용 모두에서 큰 의미를 가지는 주제이며, “어떻게 색칠할 것인가?”와 “최소 색은 몇 개인가?”라는 질문은 NP-완전 문제로서, 대규모 그래프에서는 적절한 휴리스틱 접근이 필수적입니다.
아래 튜토리얼에서는 graph-tool 라이브러리에 내장된 sequential_vertex_coloring 함수를 사용해 그래프의 정점을 순차적으로 색칠하는 방법을 배워봅니다. 도시, 교통 빅데이터를 예로 들어 직관적으로 이해하며, 초보자도 쉽게 따라할 수 있도록 최대한 상세한 한글 설명과 함께 파이썬 코드를 준비했습니다.
정점 색칠(Vertex Coloring) 은 그래프에서 인접한(직접 연결된) 두 정점이 같은 색을 갖지 않도록 색을 할당하는 작업입니다.
- 예: "교차로"를 정점으로, "도로"를 간선으로 보는 교통망 그래프를 생각해보면, 교차로마다 색을 배정하되 인접 교차로(도로로 직접 연결된 교차로)는 다른 색을 써야 하는 상황이 있을 수 있습니다(예: 신호등 스케줄 분배, 혹은 자원할당).
sequential_vertex_coloring 함수는 이름 그대로 그래프의 정점을 정해진 순서대로(Sequential) 하나씩 색칠하면서, 이미 색칠된 이웃 정점들과 겹치지 않는 색을 찾아 할당합니다.
order) 정점들을 한 번씩 방문이 알고리즘은 탐욕적(greedy) 정점 색칠 중 하나로, 주어진 순서에 따라 결과가 달라질 수 있습니다. 최적의 색(최소 색 개수)을 보장하지는 않지만, 구현이 간단하고 빠릅니다.
sequential_vertex_coloring공식 문서에 따르면 함수 시그니처는 아래와 같습니다:
sequential_vertex_coloring(g, order=None, color=None)
VertexPropertyMap 형태. VertexPropertyMap(int 타입) 반환: 색 정보를 담은 VertexPropertyMap
문서상 (\mathcal{O}(V \cdot \Delta \cdot C))로 표시됩니다.
해석: 각 정점을 색칠할 때, 이미 색칠된 이웃들의 색을 확인(최대 (\Delta)번)하고, 어느 색을 쓸지 결정하는 데 (C) 정도 비교가 가능하기 때문에 (\mathcal{O}(V\Delta C))가 됩니다.
graph_tool에서 자주 쓰이는 샘플인 lattice(격자) 그래프를 구성해보겠습니다. 10x10 격자는 100개 정점, 그리고 인접성이 높은 구조(각 정점은 상하좌우와 연결)입니다. 간단한 Sequential coloring으로 어떻게 색칠되는지 확인해봅시다.
import graph_tool.all as gt
def example_sequential_vertex_coloring():
# 1) 10 x 10 격자(lattice) 그래프 생성
# 도시/교통 관점: 10개 가로줄과 10개 세로줄이 교차 → 교차점이 정점 → 각 교차점이 상하좌우로 연결된 도로
g = gt.lattice([10, 10]) # 10x10 Lattice (정점 100개)
# 2) 순차 정점 색칠
# order=None → 내부적으로 정점 id 순서(0,1,2,...)로 순회
colors = gt.sequential_vertex_coloring(g)
# 3) 결과 확인
# colors.a 는 numpy.ndarray로, 각 정점의 색(정수)이 기록됨
print("색칠 결과(각 정점별 할당 색):")
print(colors.a)
# 4) 총 몇 가지 색이 사용되었는지
used_colors = set(colors.a)
print("사용된 색의 종류 개수:", len(used_colors))
# 5) 시각화 예시
# 각 정점의 색상으로 표시해 볼 수 있음 (파이썬 matplotlib 등에 매핑)
# graph_tool.graph_draw 함수에서 vertex_fill_color 파라미터로 넘길 수 있다.
pos = gt.sfdp_layout(g) # 간단 레이아웃
gt.graph_draw(g,
pos=pos,
vertex_fill_color=colors, # color map
vertex_size=8,
output_size=(400,400),
output="lattice_sequential_coloring.png")
if __name__ == "__main__":
example_sequential_vertex_coloring()
g = gt.lattice([10, 10]) colors = gt.sequential_vertex_coloring(g) colors.a set(colors.a) vertex_fill_color=colors로 그래프를 그리면 정점별로 다른 색이 적용됨. int → rgba 변환이 필요할 수 있습니다.실행 결과:
색칠 결과(각 정점별 할당 색):
[0 1 0 1 0 1 ... 1 0 1 0 1 ... ]
사용된 색의 종류 개수: 2
만약 “교차로를 정점, 도로를 간선”으로 하는 그래프가 있다고 가정합시다. 신호등 조정이나 교차로별 자원할당(?) 등이 필요할 때, 인접 교차로끼리는 서로 간섭 때문에 다른 ‘채널’을 사용해야 한다고 보면, 정점 색칠이 됩니다.
예를 들어:
import graph_tool.all as gt
def city_traffic_coloring_example():
g = gt.Graph(directed=False)
# 가상의 교차로(정점) 6개 추가
vlist = [g.add_vertex() for _ in range(6)]
# 임의의 도로(간선) 연결: 교차로0-1, 1-2, 2-3, 2-4, 3-5, 4-5 등
edge_list = [(0,1), (1,2), (2,3), (2,4), (3,5), (4,5)]
for s, t in edge_list:
g.add_edge(vlist[s], vlist[t])
# sequential_vertex_coloring
colors = gt.sequential_vertex_coloring(g)
# 결과 출력
print("교차로별 색상:", colors.a)
print("사용 색 개수:", len(set(colors.a)))
# 시각화
pos = gt.sfdp_layout(g)
gt.graph_draw(g,
pos=pos,
vertex_text=g.vertex_index,
vertex_fill_color=colors,
output_size=(300,300),
output="city_traffic_coloring.png")
if __name__ == "__main__":
city_traffic_coloring_example()
이 코드를 실행하면 6개의 교차로가 순서대로 색칠되어, 인접 교차로는 다른 색을 사용하게 됩니다. 순차 색칠이기 때문에, 정점 id가 0,1,2,3,4,5 순이라면 특정 greedy 결과가 나옵니다.
장점
단점
sequential_vertex_coloring 은 간단한 순차(탐욕) 방식으로 정점을 색칠해주는 함수입니다.이상으로, graph-tool 라이브러리의 sequential_vertex_coloring 함수 사용 예시와 함께, 간단한 튜토리얼을 완성했습니다. 실제 응용 시에는 정점 순서를 어떻게 정하느냐가 색칠 품질에 큰 영향을 미치므로, 필요에 따라 고급 휴리스틱을 도입하면 좋겠습니다.
아래 튜토리얼에서는 graph-tool 라이브러리가 제공하는 find_vertex, find_vertex_range, find_edge, find_edge_range 함수를 다룹니다. 이 함수들은 그래프 내 정점 혹은 간선의 프로퍼티(property) 값을 손쉽게 검색할 수 있는 유틸리티 기능입니다. 도시, 교통 빅데이터와 같은 네트워크 그래프 상황에서 "특정 속성(예: 인구 수, 교차로 혼잡도, 도로 폭 등)"을 만족하는 정점(혹은 간선)을 찾아 필터링하거나, 시각화, 분석에 활용할 때 유용합니다.
find_vertex(g, prop, match)g에서, 정점 프로퍼티 prop의 값이 match와 정확히 일치하는 정점들을 찾아 리턴합니다.prop 은:VertexPropertyMap(정점 프로퍼티 맵, 예: 각 정점에 할당된 값)"in", "out", "total" 중 하나 → 이는 "차수(degree)"를 의미합니다."in": 들어오는 차수(in-degree)"out": 나가는 차수(out-degree)"total": 총 차수(in+out-degree, 무방향 그래프에서는 out=in=total)예: find_vertex(g, "out", 3) → 아웃 차수가 3인 정점들 찾기
find_vertex_range(g, prop, range)g에서, 정점 프로퍼티 prop의 값이 range[0] <= prop[v] <= range[1] 범위에 있는 정점들을 리턴합니다.prop은 위와 동일하게 VertexPropertyMap 이거나 "in", "out", "total" 차수를 의미할 수 있습니다.range 는 [lower_bound, upper_bound] 형태의 길이 2짜리 튜플/리스트예: find_vertex_range(g, "in", [2, 5]) → 인 차수가 2 이상, 5 이하인 정점들 찾기
find_edge(g, prop, match)g에서, 간선 프로퍼티 prop의 값이 match와 정확히 일치하는 간선들을 찾아 리턴합니다.prop은 반드시 EdgePropertyMap이어야 합니다. (문자열 "in", "out" 등은 사용 불가)find_edge(g, capacity_prop, 10) → 간선 "용량(capacity)"이 10인 간선만 필터find_edge_range(g, prop, range)g에서, 간선 프로퍼티 prop의 값이 range[0] <= prop[e] <= range[1] 범위에 있는 간선들을 리턴합니다.prop은 EdgePropertyMap만 가능find_edge_range(g, weight_prop, [5, 10]) → 간선 가중치(weight)가 5~10 사이인 간선들 찾기아래 예시에서는 정점 수 6개, 간선 수 7개인 무방향 그래프를 만들고, 정점/간선 프로퍼티를 간단히 설정하여 find_vertex*, find_edge* 함수를 실습해 봅니다.
import graph_tool.all as gt
import numpy as np
def example_find_vertex_and_edge():
# 1) 그래프 생성(무방향)
g = gt.Graph(directed=False)
# 2) 정점 6개 추가
vlist = [g.add_vertex() for _ in range(6)] # vlist[0] ~ vlist[5]
# 3) 간선 구성: 예: 0-1, 1-2, 2-3, 2-4, 3-5, 4-5, 0-2
edge_list = [(0,1), (1,2), (2,3), (2,4), (3,5), (4,5), (0,2)]
for s, t in edge_list:
g.add_edge(vlist[s], vlist[t])
# 4) 정점 프로퍼티 생성: "value"라는 int타입
# (도시/교통 예시: 교차로 혼잡도/신호강도 등)
vprop = g.new_vertex_property("int")
# 임의로 값 넣어보기 (0~9 사이 난수)
for v in g.vertices():
vprop[v] = np.random.randint(0, 10)
# 5) 간선 프로퍼티 생성: "weight"라는 float타입
# (도시/교통 예시: 도로 길이, 비용 등)
eprop = g.new_edge_property("float")
for e in g.edges():
eprop[e] = np.random.uniform(1.0, 20.0) # 1.0~20.0 사이 실수
# 6) 정점 색인 출력
print("[Vertex indices]:", [int(v) for v in g.vertices()])
# 정점 프로퍼티 값 출력
print("[Vertex values (vprop)]:", [vprop[v] for v in g.vertices()])
# 간선 프로퍼티 값 출력
for e in g.edges():
print(f"Edge ({int(e.source())}-{int(e.target())}), weight={eprop[e]:.2f}")
# === 예시 1: find_vertex ===
# "vprop[v] == match" -> match=5인 정점 찾기
match_val = 5
found_v = gt.find_vertex(g, vprop, match_val)
print(f"\n[vprop == {match_val}] -> 정점 목록:", [int(x) for x in found_v])
# === 예시 2: find_vertex_range ===
# "1 <= vprop[v] <= 4" 인 정점 찾기
found_v_range = gt.find_vertex_range(g, vprop, [1, 4])
print(f"[1 <= vprop <= 4] -> 정점 목록:", [int(x) for x in found_v_range])
# === 예시 3: find_edge ===
# "weight == match"는 float이므로 정확히 일치시키기 어렵지만,
# 여기서는 임의로 'weight==some_value'를 예시로
# -> test_value와 거의 같은(절대 오차 0.001 이하) 간선 수동 필터
# (여기서는 그냥 예시이므로, 매칭되는 게 없을 수도..)
test_value = 10.0
# EdgePropertyMap은 float이므로, find_edge 내부로는 '정확히 일치'를 집어넣으면 잘 안 맞을 가능성↑
# -> 간단 예시에선 pass (또는 근사 비교를 위해 별도 수동 filter)
# === 예시 4: find_edge_range ===
# "weight 5.0 ~ 10.0" 사이의 간선
low, high = 5.0, 10.0
found_e_range = gt.find_edge_range(g, eprop, [low, high])
print(f"\n[{low:.1f} <= weight <= {high:.1f}] 인 간선 목록:")
for e in found_e_range:
print(f" Edge ({int(e.source())}-{int(e.target())}), weight={eprop[e]:.2f}")
# === 예시 5: "in", "out", "total" 사용해보기 ===
# 무방향 그래프에서는 "in"=="out"=="total" 차수가 같음
# out-degree==2 인 정점
out2_vertices = gt.find_vertex(g, "out", 2)
print(f"\nout-degree==2 인 정점 목록:", [int(v) for v in out2_vertices])
# total-degree 범위 [2, 3]
deg_range_vertices = gt.find_vertex_range(g, "total", [2, 3])
print("total-degree가 2~3 인 정점 목록:", [int(v) for v in deg_range_vertices])
if __name__ == "__main__":
example_find_vertex_and_edge()
vlist에 저장edge_list)vprop), 간선 프로퍼티(eprop) vprop (int형): 무작위 0~9 정수 eprop (float형): 무작위 1.0~20.0 실수find_vertex(g, vprop, match_val) vprop[v] == match_val인 정점들 찾음find_vertex_range(g, vprop, [1,4]) 1 <= vprop[v] <= 4 범위에 해당하는 정점 찾음find_edge_range(g, eprop, [5.0, 10.0]) 5.0 <= eprop[e] <= 10.0 범위에 해당하는 간선 목록"in", "out", "total" gt.find_vertex(g, "out", 2) → 아웃 차수가 2인 정점gt.find_vertex_range(g, "total", [2,3]) → 전체 차수가 2~3 사이인 정점실행하면, 무작위 프로퍼티로 인해 매번 결과는 다르지만, 다음과 같은 콘솔 로그 형태가 나옵니다:
[Vertex indices]: [0, 1, 2, 3, 4, 5]
[Vertex values (vprop)]: [7, 3, 7, 9, 0, 4]
Edge (0-1), weight=12.35
Edge (1-2), weight=5.37
Edge (2-3), weight=19.58
Edge (2-4), weight=7.61
Edge (3-5), weight=4.99
Edge (4-5), weight=10.82
Edge (0-2), weight=8.13
[vprop == 5] -> 정점 목록: []
[1 <= vprop <= 4] -> 정점 목록: [1, 5]
[5.0 <= weight <= 10.0] 인 간선 목록:
Edge (1-2), weight=5.37
Edge (2-4), weight=7.61
Edge (0-2), weight=8.13
out-degree==2 인 정점 목록: [0, 3]
total-degree가 2~3 인 정점 목록: [0, 1, 3, 4, 5]
(이는 예시일 뿐, 매번 난수에 따라 다릅니다.)
find_vertex_range(g, congest_prop, [50,100]) → 혼잡도 50~100인 교차로만 모아서 시각화, 혹은 우선적 처리(예: 방범 카메라 배치)find_edge_range(g, traffic_prop, [0, 500]) → 교통량 500 이하인 도로 = 비교적 한적한 경로graph_tool.util의 find_vertex, find_vertex_range, find_edge, find_edge_range 함수들은 그래프에서 특정 프로퍼티값(혹은 차수)와 정확히 일치하거나 범위에 들어가는 정점/간선을 빠르게 필터링할 수 있게 해줍니다.
도시/교통 빅데이터(그래프 구조) 분석 시, "속성으로 정점/간선을 골라내고, 그 서브셋만 집중 분석/시각화" 할 때 굉장히 편리합니다.
find_vertex* vs find_edge* 사용*_rangeprop 자리에 "in", "out", "total" 활용실무에서도 이 방법들을 응용하여, 대규모 그래프에서 특정 조건을 만족하는 노드·링크만을 추출해 보고서를 작성하거나, 추가 분석, 시각화 시에 필터링 작업을 용이하게 해줄 것입니다.
이상으로 graph_tool.util의 핵심 유틸 함수들에 대한 초보자용 튜토리얼을 마칩니다.