streamlit-agraph: 파이썬에서 그래프 네트워크 시각화하기

Streamlit 으로 interacitve graph network viewer 만들기

t.k.woo
네이버 쇼핑 개발 블로그
18 min readMar 10, 2023

--

[서론] 그래프 시각화를 어떻게 하는게 좋을까?

데이터를 다루는 개발을 하다보면 다른 데이터와의 연결성을 고려하게 되고 결국 점(node)과 선(edge)으로 이루어지는 그래프 데이터를 다루게 된다.

눈오리와 관련된 키워드 사이의 관계를 찾아내고 싶어서 만들어 본 그래프. 이렇게 시각화하면 관련 키워드가 군집 형태로 생성되는 것을 한 눈에 파악하기 쉽다.

문제는 내가 만든 그래프가 정말 잘 만들어진 데이터인지 확인할 수 있는 방법이 마땅하지 않다는 점이다.
예를 들어 어떤 노드가 중요한 노드인지 두 노드 사이의 유사도가 어떻게 구성되는지 알려면 아래와 같은 방법이 있다.

  1. 주피터 노트북에 edge list 뽑아서 값을 하나씩 뽑아보거나,
  2. edge list 를 networkx 로 변환해서 networkx.drawing 을 사용할 수 있다.

두 방법 모두 당연하게도 단점이 존재한다. 1번의 경우 전체 구조를 파악할 수 없어서 degree나 density 등의 지표를 따로 뽑아봐야하고 2번의 경우 그래프가 큰 경우 렌더링에 오랜 시간이 걸리고 특정 edge 나 노드만 확인하기 어렵다는 점이다.

그래프에서 각 노드들의 관계를 확인하고 인사이트를 얻어내고 싶지만 전체 구조를 확인해야 보다 나은 시각을 얻을 수 있다.
networkx 의 pydot 으로 그린 키워드 그래프, 노드가 겹치지 않게 렌더링하다보니 이미지 사이즈가 굉장히 커지게 되고 결국 이미지를 생성해내는데 오랜 시간이 걸리는 것을 경험할 수 있다. 만들어진 이미지는 static 이어서 당연히 특정 노드나 엣지의 정보를 확인할 수 없다.

가장 좋은 해결책은 렌더링된 graph 상태에서 사용자와 interactive 하게 동작하는 뷰어가 필요하다는 점이다. 즉 전체 구조를 보여주면서도 마우스 입력을 받아 사용자가 원하는 edge 나 node 만 선택해서 볼 수 있는 시스템이 필요하다.

복잡한 네트워크를 시각화하는 경우 상호작용 없이 군집이나 세부 노드, 엣지 정보를 파악하기 힘들다.

데이터와 상호작용이 가능하려면 전용 프로그램을 만들거나 웹 프론트 기술을 사용하는 것이 바람직하다. 자바스크립트에는 vis.js 라는 그래프 뷰어 라이브러리가 존재하기 때문에 이를 사용하는 파이썬 라이브러리를 찾으면 우려하는 두 문제가 모두 해결될 수 있다.

파이썬에서 vis.js 를 사용하는 대표적인 패키지는 pyvis 라는 패키지를 예로 들 수 있다.

이 툴은 개발자가 생성한 네트워크 구조를 vis.js 로 변환시켜서 html 을 생성해준다. 또한 html 을 읽어서 주피터노트북에서 바로 렌더링 할 수 있도록 기능을 지원하고 있다.

개발 중간 과정에서 데이터를 확인하기 위한 용도로 아주 적합하기 때문에 개발자가 사용하기 좋은 도구라고 생각한다. python 에서 graph 데이터를 표현하기 위한 defacto 라이브러리인 networkx 와도 잘 호환된다.

pyvis 의 경우 개발자 스스로에게는 사용하기 좋은 툴이지만 데모 페이지를 제작해서 타인에게 공유하는 용도로는 다소 어려움이 있다. (ML 개발자와 데이터 개발자가 데모 페이지를 타 개발자, 기획자에게 전달해야할 일은 상당히 빈번하게 일어난다.) 물론 html 파일을 서빙하는 방법이 있지만 추가적인 웹 서버를 제작해야하고 그래프의 실제 데이터를 표로 확인하고 싶은 경우 복잡한 과정이 수반된다. (글을 적으면서 생각해보니 pyvis 로 html 을 만들고 streamlit 에서 html 을 로딩하는 방법도 있다는 생각을 하는 중…)

Streamlit 은 데모 페이지 수요를 만족시켜주는 아주 훌륭한 툴이다. 23년 현재에는 pynecone 과 같은 더 새로운 웹 어플리케이션 도구가 만들어지고 있지만 streamlit 이 2018년부터 만들어져 오면서 사용하기 쉬우면서 높은 안정성을 가지고 있어 파이썬 개발자들 사이에서 신뢰성 높게 사용되고 있다. (사실상 파이썬 데모페이지 계의 defacto 가 아닐까..)

이 streamlit 과 vis.js 가 연동된다면 그래프를 그리는 방법도 간단하면서 먼저 설명만 사용목적들을 모두 충족할 수 있을 것이라고 생각했다. 그리고 찾아본 결과 vis.js 를 streamlit 에 연결해주는 라이브러리를 찾아서 만들어본 결과 너무 만족스러운 품질로 뷰어를 제작해서 사용할 수 있었다.

https://github.com/ChrisDelClea/streamlit-agraph

문제는 이 streamlit-agraph 는 API 문서가 거의 없는 상태이기 때문에 사용하기에 약간 번거롭다. 물론 매우 직관적으로 만들어져있기 때문에 간단한 네트워크는 바로 사용할 수 있지만 디테일한 변경은 vis.js 문서를 참고해야한다.

이 글을 작성하는 목적은 streamlit-agraph 를 잘 사용하기 위해 부족한 개발문서를 보충하는데 있다.

이 글이 유용한 대상

  • 파이썬으로 interactive graph viewer 를 만들고 싶은 개발자
  • 평소 streamlit 으로 데모 웹 어플리케이션을 만들고 있는 개발자

Streamlit과 graph, streamlit-agraph 설명

설치

pip install streamlit==1.17.0
pip install streamlit-agraph==0.0.45 # 낮은 버전의 경우 호환이 되지 않는 함수들이 있다.

기초적인 사용법

import streamlit
from streamlit_agraph import agraph, Node, Edge, Config, TripleStore

nodes = []
edges = []
# node 를 정의하고
nodes.append( Node(id="Spiderman",
label="Peter Parker",
size=25,
shape="circularImage",
image="http://marvel-force-chart.surge.sh/marvel_force_chart_img/top_spiderman.png")
) # includes **kwargs
nodes.append( Node(id="Captain_Marvel",
size=25,
shape="circularImage",
image="http://marvel-force-chart.surge.sh/marvel_force_chart_img/top_captainmarvel.png")
)
# edge 를 정의해서
edges.append( Edge(source="Captain_Marvel",
label="friend_of",
target="Spiderman",
# **kwargs
)
)

# config 와 함께
config = Config(width=750,
height=950,
directed=True,
physics=True,
hierarchical=True,
# **kwargs
)

# graph 를 그리면 끝!
return_value = agraph(nodes=nodes,
edges=edges,
config=config)

기초 예제 코드를 보면 알겠지만 너~무 간단하다. network 를 구성하는 node, edge 만 정의하고 함수를 실행시키면 시각화가 완료된다. 심지어 image 까지 multimodal 로 보여줄 수 있기 때문에 적은 노력으로 만족스러운 결과물을 얻을 수 있다.

문제는 각 object 가 어떤 파라미터로 제어되는지 알아야한다는 점인데 개발문서가 정리되어 있지 않기 때문에 내가 만들어보면서 사용했던 파라미터들을 정리해본다.

Node

Node(
id=1234, # node 의 id
title='title of node', # 타이틀, 마우스를 hovering 할 때 여기 적힌 값이 표시된다.
image='http://image/url.jpg', # image url, 기본 기능에서는 외부 접근 가능한 url 만 인식 가능하다. server 의 local file path 는 불가능하다.
label='name of node', # label 에 적힌 값이 화면에 노출된다.
color='#ACDBC9', # RGB color 색상
shape='circularImage', # node shape, [image, circularImage, diamond, dot, star, triangle, triangleDown, hexagon, square and icon] 가 있지만 주로 circularImage 나 dot 을 사용하게 된다.
size=25, # node 크기
)

Edge

Edge(
source=1234, # source node id
label='label of node', # label, weight 값을 시각화하기 위해 label 로 넣는 방법도 있다.
target=1234, # target node id
title='edge title', # TripleStore() 에서 사용되고 hovering 시 값이 표시된다.
width=1, # width
font={'color': '#B0B0B0'}, # font
labelHighlightBold=False
)

라이브러리 코드를 보면 알겠지만 fontwidth 같은 파라미터는 **kwargs 에 포함된다. 상세한 파라미터를 이해하기 위해서는 앞서 설명한 vis.js doc 을 살펴보면 찾을 수 있다.

visjs nodes: https://visjs.github.io/vis-network/docs/network/nodes.html
visjs edges: https://visjs.github.io/vis-network/docs/network/edges.html

라이브러리에 설명되지 않은 파라미터는 vis.js 문서에서 확인하고 사용할 수 있다.

TripleStore

그래프에서 node 와 edge 를 각각 정의할 수도 있지만 실제 파이썬에서는 networkx 와 같은 라이브러리를 통해 이미 생성된 네트워크가 있을 것이다. TripleStore 는 edge list 를 바로 agraph 로 변환해줄 수 있는 편리한 도구라고 생각하면 된다.

import networkx as nx

# Assuming the graph G has already been created.
G = nx.from_pandas_edgelist(df_edge, 'source', 'target', 'weight')

store = TripleStore()
for edge in sub_G.edges(data=True):
score = edge[-1]['weight']
store.add_triple(edge[0], f"{score:.3f}", edge[1])

edges = list(store.getEdges())
nodes = list(store.getNodes())

Config

그래프를 화면에 그리기 위한 화면 파라미터와 네트워크 구조를 어떻게 보여줄지에 대한 파라미터를 정의하는 오브젝트이다.

config = Config(
width=750, # 그래프를 그릴 캔버스 사이즈
height=950, # 그래프를 그릴 캔버스 사이즈
directed=False, # directed or undirected graph 를 그릴 수 있다.
physics=True, # 그래프 랜더링 후 node 의 움직임을 허용할 것인지
hierarchical=False, # 그래프가 tree 구조인 경우
nodeHighlightBehavior=True, # node highlight
highlightColor='#F7A7A6',
collapsible=True,
staticGraphWithDragAndDrop=False,
link={'labelProperty': 'label', 'renderLabel': True},
staticGraph=False
)

streamlit-agraph 의 파라미터 항목이 매우 많기 때문에 config 를 능동적으로 생성할 수 있도록 config builder 를 제공한다. 실험을 위해 1번 정도 사용해보게 되지만 최종 서비스하는 코드에는 불필요한 코드라고 생각한다.

from streamlit_agraph.config import Config, ConfigBuilder

# 1. Build the config (with sidebar to play with options) .
config_builder = ConfigBuilder(nodes)
config = config_builder.build()

# 2. If your done, save the config to a file.
config.save("config.json")

ConfigBuilder 는 streamlit-agraph 에서 공개한 공식 app 에서 확인해볼 수 있다.

https://marvelous-graph.streamlit.app/

이 앱의 sidebar 부분이 ConfigBuilder() 로 만들어진 항목이다.

화면에 그리기, agraph

agraph(nodes=nodes, edges=edges, config=config)

이전 과정에서 만든 node, edge, config 로 agraph 를 통해 간단히 그려주면 아래와 같은 그래프를 출력할 수 있게된다.

Node, edge 의 title 에 입력된 값은 마우스 hover 를 통해 확인할 수 있다.

streamlit 실행코드는 아래처럼 사용할 수 있다.

streamlit run --server.port 11111 viewer.py

추가로 꼭 필요한 팁

[Tips 1.] 빠른 로딩과 핵심 데이터 시각화를 위한 데이터 관리

파이썬에서 그래프를 다룰 때 일반적으로 networkx 를 사용하기 때문에 이 라이브러리에서 제공하는 데이터를 주로 사용하게 된다. 가장 간단하게는 edge list 로 다른 라이브러리(pandas) 와 커뮤니케이션이 가능하므로 아래와 같이 사용할 수 있겠다.

df_edge = pd.read_parquet(edge_path)

# | | source | target | weight |
# |---:|---------:|---------:|---------:|
# | 0 | 14750 | 17085 | 0.911821 |
# | 1 | 14750 | 17086 | 0.816762 |
# | 2 | 17085 | 17086 | 0.878161 |
# | 6 | 14750 | 20829 | 0.996924 |
# | 7 | 17085 | 20829 | 0.916394 |

G = nx.from_pandas_edgelist(df_edge, 'source', 'target', 'weight')

store = TripleStore()
for edge in G.edges(data=True):
score = edge[-1]['weight']
store.add_triple(edge[0], f"{score:.3f}", edge[1])

nodes = list(store.getNodes())
edges = list(store.getEdges())

agraph(nodes=nodes, edges=edges, config=config)

가장 간단히 사용할 수 있는 방법이지만 이 방법은 node attribute(label, title 등) 을 표현하기 어렵다. 따라서 edge list 만 관리하는 것이 아니라 node list 도 따로 관리해주어야한다.

Node list 예시

|   index | name                                                           |   cluster_id | img_path               |
|--------:|:---------------------------------------------------------------|-------------:|:-----------------------|
| 22677 | [디자인스킨] 패브릭레더 폴더매트240 2세트(컬러선택) | 105417 | ./image/9393/82091.jpg |
| 1047 | 7DW9A8789 콩지래빗 백설 공주 동화 마녀 미니 토끼 인형 장난감 | 283695 | ./image/3342/34670.jpg |
| 1922 | AG 체리와쥬리의 공부방 랜덤 데일리 시리즈 스쿨룩 패션 | 100404 | ./image/2687/34390.jpg |
| 21004 | 디즈니클래식돌 모아나 브러쉬 디즈니프린세스 구체관절인형 마루인형 선물포장 | 108472 | ./image/1335/30848.jpg |
| 10081 | 퓨어메이트 뉴브리지 마스크 화이트 (소형) (50매) | 90478 | ./image/7925/84399.jpg |

streamlit 은 page 기반 라이브러리이기 때문에 이벤트가 일어날 때 전체 프로그램이 리로딩된다. 만약 파일로 node, edge 관리한다면 이 파일들을 읽을 때 병목이 일어날 수 있다. 따라서 이 부분을 캐싱해주면 큰 도움이 된다.

@st.cache
def read_node_and_edge():
node_path = '../data/node.parquet'
edge_path = '../data/edge.parquet'
df_node = pd.read_parquet(node_path)
df_edge = pd.read_parquet(edge_path)
return df_node, df_edge

또는 streamlit.session_state 로 관리할 수 있다.

if 'df_node' not in st.session_state:
st.session_state['df_node'] = pd.read_parquet(node_path)

참고로 데이터가 상당히 크다면 DB 를 활용해서 sqlalchemy 를 사용하는 방법이 편하다.

[Tips 2.] 이미지 file path 로 읽기

streamlit-agraph, streamlit-aggrid 와 같은 패키지들에서는 이미지를 지원하긴 하지만 기본적으로 외부 url 경로만을 지원하고 있다.

Node(id=1234, label='Node A', image='http://image/url.jpg')

하지만 ML 개발자나 데이터 엔지니어가 파이썬에서 주로 다루는 이미지는 로컬 컴퓨터나 서버의 file system 에 저장되어 있어 파일경로로 가지고 있을 확률이 크다. 이 경우 image=’/path/to/image.jpg’와 같은 형태로 입력해보면 이미지가 출력되지 않는 문제가 있다.

아래와 같은 방법으로 해결할 수 있다.

import base64

local_img_prefix = 'data:image/jpg;base64,'
with open(img_path, 'rb') as img_file:
img = local_img_prefix + base64.b64encode(img_file.read()).decode('utf8')

Node(id=1234, label='Node A', image=img)

해결방법은 아래 링크들에서 참고했다.

local_img_prefix 에서 jpg 부분은 file extension 을 나타내는 부분이므로 png 등 사용하는 파일확장자에 맞게 변경해서 사용하면 된다.

--

--