Sankey Diagram 은 화살표 너비가 흐름 속도에 비례(the width of the arrows is proportional to the flow rate)하는 흐름 다이어그램의 한 유형(a type of flow diagram)입니다. Sankey Diagram은 또한 에너지 계정, 지역 또는 국가 차원의 자재 흐름 계정 및 비용 분석을 시각화할 수 있습니다. 다이어그램은 종종 재료 흐름 분석의 시각화에 사용됩니다. 


Sankey Diagram은 시스템 내의 주요 전송 또는 흐름(major transfers or flows)을 강조합니다. 그들은 흐름에 대한 가장 중요한 기여의 위치를 찾는데 도움을 줍니다. 그것들은 종종 정의된 시스템 경계 내에서 보존된 양을 보여줍니다. [1]

 

 

Python에서 Sankey Diagram 을 그리는 모듈로 HoloViews 와 plotly 가 있는데요, HoloViews 가 구문이 좀더 직관적이어서 이해하기가 쉽습니다. 

 

이번 포스팅에서는 Python의 HoloViews 모듈을 사용해서 Jupyter Notebook에서 Sankey Diagram 을 시각화하는 방법을 소개하겠습니다. [2]

 

HoloViews 모듈은 백본으로 Matplotlib 과 Bokeh 를 사용하므로, 만약 이들 모듈이 설치되어 있지 않다면 pip install 로 먼저 이들 모듈을 설치해주어야 합니다. 

 

 

## in a command line

$ pip install bokeh
$ pip install holoviews

 

Sankey elements represent flows and their quantities in proportion to one another. The data of a Sankey element defines a directed, acyclic graph, making it a specialized subclass of the Graph element. The width of the lines in a Sankey diagram represent the magnitudes of each edge. Both the edges and nodes can be defined through any valid tabular format including pandas dataframes, dictionaries/tuples of columns, NumPy arrays and lists of tuples.

The easiest way to define a Sankey element is to define a list of edges and their associated quantities:

 

Sankey 요소는 흐름과 흐름의 양을 서로 비례하여 나타냅니다. Sankey 요소의 데이터는 방향성 있는 비순환 그래프(directed, acyclic graph)를 정의하여 그래프 요소의 특수 하위 클래스로 만듭니다. Sankey Daigram 에서 선의 너비는 각 Edge의 크기를 나타냅니다. Edge와 Node 모두 pandas DataFrame, Dictionaries/Tuples of columns, NumPy Array 및 Lists of Tuples을 포함한 유효한 표 형식을 통해 정의할 수 있습니다. 이게 바로 Holoviews 가 편리하고 직관적인 이유예요. 

 

Sankey 요소를 정의하는 가장 쉬운 방법은 list of edges과 관련 수량을 정의하는 것입니다. 아래 예에서는 A, B node에서 X, Y, Z node 로의 흐름의 관계와 양을 리스트 형식으로 정의한 것입니다. [2]

 

import holoviews as hv
from holoviews import opts, dim
hv.extension('bokeh')

# defining a list of edges and their associated quantities
sankey = hv.Sankey([
    ['A', 'X', 10],
    ['A', 'Y', 20],
    ['A', 'Z', 30],
    ['B', 'X', 60],
    ['B', 'Y', 5],
    ['B', 'Z', 40]]
)

sankey.opts(width=800, height=500)

sankey diagram using holoviews module in python

 

 

 

아래의 예시는 2010년 왕립학회 정책 보고서 "The Scientific Century: securing our future prosperity"에 설명된 영국 박사과정 학생들의 진로에 대한 간단한 데이터 세트를 Sankey Diagram으로 그려본 것입니다. 우리는 정수 지수로 열거된 노드와 각 경력 단계 사이에 흐르는 백분율을 정의합니다. 마지막으로 "To"라는 레이블을 붙이는 대상 노드에 의해 값과 색상에 대한 단위를 가진 차원을 정의합니다. [2]

 

위의 예에서는 nodes의 이름을 그대로 사용하였다면, 아래의 예에서는 nodes에 대해서 enumerate() 함수를 사용해서 index 정수를 매핑해서 holoveiws Dataset 객체로 만들어 주고, 이를 edges 의 관계를 정의할 때 index 정수를 사용하였습니다. 

 

nodes = ["PhD", "Career Outside Science",  "Early Career Researcher", "Research Staff",
         "Permanent Research Staff",  "Professor",  "Non-Academic Research"]
for i, j in enumerate(nodes):
    print(i, ':', j)
    
# 0 : PhD
# 1 : Career Outside Science
# 2 : Early Career Researcher
# 3 : Research Staff
# 4 : Permanent Research Staff
# 5 : Professor
# 6 : Non-Academic Research

 

 

Label의 위치를 오른쪽으로 바꾸어 주었으며 (label_position='right'), cmap, edge_color, node_color 로 edge와 node 의 색깔도 다르게 설정해주었습니다. 색깔이 다르니 가독성이 한결 좋아졌습니다. 

 

## the edges are expressed as integer node indexes and labels are provided separately.
## We can explicitly define the set of nodes as a Dataset of indexes and labels as key 
## and value dimensions respectively.
nodes = ["PhD", "Career Outside Science",  "Early Career Researcher", "Research Staff",
         "Permanent Research Staff",  "Professor",  "Non-Academic Research"]
nodes = hv.Dataset(enumerate(nodes), 'index', 'label')

edges = [
    (0, 1, 53), 
    (0, 2, 47), 
    (2, 6, 17), 
    (2, 3, 30), 
    (3, 1, 22.5), 
    (3, 4, 3.5), 
    (3, 6, 4.), 
    (4, 5, 0.45)   
]

value_dim = hv.Dimension('Percentage', unit='%')
careers = hv.Sankey((edges, nodes), ['From', 'To'], vdims=value_dim)

careers.opts(
    opts.Sankey(labels='label', 
                label_position='right', # adjust the label_position from "right" to "left"
                width=900, height=500, 
                cmap='Set1',
                edge_color=dim('To').str(), # setting edge color
                node_color=dim('index').str()) # setting node color
)

sankey diagram using holoviews in python

 

 

 

Interactive Diagram 이어서 커서를 가져다대면 아래의 화면 캡쳐와 같이 해당 Edge 가 하이라이드 되며 해당 Edge의 From note, To note, Percentage 등의 캡션 정보를 팝업으로 볼 수 있습니다.  (아래는 화면 캡쳐이므로 Interactive 기능 없음. Jupyter Notebook에서 실행해야지 Interactive 기능 사용 가능함)

 

 

[ Reference ]

 

[1] Introduction to Sankey Diagram: https://en.wikipedia.org/wiki/Sankey_diagram

[2] Sankey Element using HoloViews: https://holoviews.org/reference/elements/bokeh/Sankey.html

[3] R로 Sankey Diagram 시각화 하기: https://rfriend.tistory.com/220

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 방향을 가진 네트워크 상에서 유량, 에너지, 물자 등의 '네트워크(network) 기반'의 흐름(flow)을 시각화하는데 유용한 Sankey Diagram 에 대해서 알아보겠습니다. 

 

Sankey Diagram 은 얼핏보면 평행좌표그림(parallel coordinate plot)과 비슷한 면이 있습니다.  하지만 Sankey Diagram은 Node -> Edge 로의 관계, 경로가 있다는 점, 경로의 두께를 weight 에 따라서 다르게 한다든지, 색깔을 부여할 때도 반투명(alpha)하게 한다든지 해서 평행좌표그림 대비 다릅니다. (평행좌표그림은 그냥 다변량 변수들을 y축 높이를 같게 해서 옆으로 x를 죽 늘어놓은 형태. 경로 개념 없음. 선 두께 동일) 

 

아래 이미지는 구글에서 Sankey Diagram 이라고 키워드 검색했을 때 나오는 이미지들을 화면캡쳐한 것입니다.  아래 이미지를 보면 '아, 이거~ ' 싶으시죠?

 

 

* 이미지 출처 : Google 검색

 

 

자, 그럼

 

 - R의 riverplot package(* author : January Weiner) 를 가지고

 - minard 데이터셋(* source : Charles Joseph Minard)을 사용해서
   (R riverplot package에 내장되어 있음)

 

나폴레옹 군대가 러시아로 진군했다가 퇴각했던 경로를 시각화해보겠습니다.

 

 

분석의 재미를 더하기 위해 "minard" 데이터셋에 대한 역사적인 배경을 간략히 알아보고 넘어가겠습니다.

 

때는 바야흐로 1812년, 유럽이 되겠습니다.  프랑스의 나폴레옹은 유럽의 상당 국가를 점령했으며, 영국을 침탈하기 위해 백방으로 쌈질을 걸었지만 번번히 실패하고 있던 상황이었습니다.  이에 화가 난 나폴레옹은 '바다를 건너가서 싸우는 게 승산이 낮으니 차라리 유럽 본토와 영국과의 무역을 봉쇄해서 영국의 피를 말리자. 그래, 바로 이거야. 내 사전에 불가능은 없어!' 라는 계획을 세우게 됩니다.

 

그러나 but,

 

나폴레옹의 프랑스가 이미 과도하게 힘이 세졌기 때문에 견제가 필요하다가 생각한 러시아의 알렉산더 짜르는 '나폴레옹, 내가 니 봉이냐?  영국 다음에 러시아 공격할거지?  내가 누구 좋으라고 니 말을 따라?' 라면서 나폴레옹의 영국과의 무역 폐쇄령을 쌩까고 게기게 됩니다. 

 

이에 발끈한 나폴레옹은 프랑스 대군을 모집해서 러시아 짜르의 못되고 괴씸한(?) 버릇을 고쳐주고자, 본떼를 보여주고자 1812년 10월, 겨울이 코앞인 시점에 러시아 모스코바로 진격을 하게 됩니다. 이때만 해도 정말 나폴레옹 군대는 "진격의 거인" 이었습니다.

 

그러나 but,

 

러시아 알렉산더 짜르가 대책없이 나폴레옹에게 대든게 아니었습니다.  러시아 짜르는 러시아 군과 국민에게 아주 간단한(?) 전략의 명령을 내립니다.  "프랑스군이 진격하는 곳의 모든 것을 태워서 프랑스군이 아무것도 탈취하지 못하도록 하고, 싸우지는 말고 퇴각하라.  전투 전략 끝!"

 

아마, 나폴레옹의 프랑스군은 처음 며칠은 러시아로 무혈입성하는 것에 신이 났을지도 모릅니다.

 

그러나 but,

 

10월이 11월이 되고, 그 담에 12월이 되면서 추위와 배고픔과 질병에 프랑스 군인들의 대부분이 죽어나갔습니다.  진격의 거인 나폴레옹은 러시아 짜르의 "불태우고 후퇴" 전략에 속수무책으로 당하면서 거의 전멸을 당하게 됩니다.

 

 

 

아래의 그림이 나폴레옹 프랑스 군대가 러시아로 진격했다가 퇴각한 진로를 지도 상에 표기한 것입니다.

 

* 출처 : https://robots.thoughtbot.com/analyzing-minards-visualization-of-napoleons-1812-march

 

 

 

여기까지의 이야기를 토대로 숫자와 그래프를 가지고 한눈에 실감할 수 있는 시각화를 Charles Joseph Minard 이라는 분이 아래와 같이 했습니다.

 

[ Napoleon army march : minard ]

* 출처 : https://robots.thoughtbot.com/analyzing-minards-visualization-of-napoleons-1812-march

 

 

 

위의 2차원의 minard visualization에는 다양한 차원의 정보가 알차게(!) 들어있는데요,


 - (1) 프랑스군의 진격과 퇴각 경로 (advance and retreat path and direction)

        : 연한 색깔이 진격(advance), 검정 색깔이 퇴각(retreat)

        : 도시 이름은 좌표에 따라 text로 표기

        : 프랑스 군이 세갈래(하나의 큰 줄기, 두 개의 얇은 줄기)로 나누어 진격한 것도 선이 갈라지게 표현

 - (2) 프랑스군의 인명 손실 규모 (loss of life at a time and location)
        : 선의 두께(line width), 처음에는 몽둥이처럼 두꺼웠던 선이 퇴각 마무리 시점에는 실처럼 가늘게 됨

 

 - (3) 온도 (temperature)

        : 그림 하단에 퇴각 시점의 온도를 그리 넣음.  최저 -30도씨까지 떨어졌음.  얼어죽기 딱 좋은 날씨. -_-;

 

 - (4) 강 (river)

        : 하단에 얇은 수직 선으로 나폴레옹군이 퇴각 시점에 맞닥트려 시련을 더해 준 강(river)을 그려 넣음. 

 

 

이렇게 많은 알찬 정보를 저 위의 시각화 하나에 오롯히 담아 냈습니다!!!  이해하기 쉽죠?!

 

 

이걸 데이터로 나타내 보면 아래와 같습니다.  @@;  

이게 무슨 소린가, 데이터가 뭘 말해주려고 하나.... 눈 돌아가지요? 

위의 그래프로 보면 단박에 이해되는 것을 아래의 숫자로 보면 한숨 나오고 갑갑하지요? ㅋㅋ

 

> install.packages("riverplot")
> library(riverplot)

 

> data( minard )
> minard
$edges
    ID1  ID2  Value direction
1    A1   A2 422000         A
2    A2   A3 400000         A
3    A3   A4 320000         A
4    A4   A5 320000         A
5    A5   A6 300000         A
6    A6   A7 280000         A
7    A7   A8 240000         A
8    A8   A9 210000         A
9    A9  A10 180000         A
10  A10  A11 175000         A
11  A11  A12 145000         A
12  A12  A13 140000         A
13  A13  A14 127100         A
14  A14  A15 100000         A
15  A15  A16 100000         A
16  A16   R1 100000         A
17   R1   R2 100000         R
18   R2   R3  98000         R
19   R3   R4  97000         R
20   R4   R5  96000         R
21   R5   R6  87000         R
22   R6   R7  55000         R
23   R7   R8  37000         R
24   R8   R9  24000         R
25   R9  R10  20000         R
26  R10  R11  50000         R
27  R11  R12  50000         R
28  R12  R13  48000         R
29  R13  R14  20000         R
30  R14  R15  12000         R
31  R15  R16  14000         R
32  R16  R17   8000         R
33  R17  R18   4000         R
34  R18  R19  10000         R
35  R19  R19  10000         R
36   A3 A4.2  60000         A
37 A4.2 A5.2  40000         A
38 A5.2 A6.2  33000         A
39 A6.2  R10  30000         R
40   A2 A3.1  22000         A
41 A3.1  R18   6000         A

$nodes
       ID Longitude Latitude
A1     A1      24.0     54.9
A2     A2      24.5     55.0
A3     A3      25.5     54.5
A4     A4      26.0     54.7
A5     A5      27.0     54.8
A6     A6      28.0     54.9
A7     A7      28.5     55.0
A8     A8      29.0     55.1
A9     A9      30.0     55.2
A10   A10      30.3     55.3
A11   A11      32.0     54.8
A12   A12      33.2     54.9
A13   A13      34.4     55.5
A14   A14      35.5     55.4
A15   A15      36.0     55.5
A16   A16      37.6     55.8
R1     R1      37.7     55.7
R2     R2      37.5     55.7
R3     R3      37.0     55.0
R4     R4      36.8     55.0
R5     R5      35.4     55.3
R6     R6      34.3     55.2
R7     R7      33.3     54.8
R8     R8      32.0     54.6
R9     R9      30.4     54.4
R10   R10      29.2     54.3
R11   R11      28.5     54.2
R12   R12      28.3     54.3
R13   R13      27.5     54.5
R14   R14      26.8     54.3
R15   R15      26.4     54.4
R16   R16      25.0     54.4
R17   R17      24.4     54.4
R18   R18      24.2     54.4
R19   R19      24.1     54.4
A4.2 A4.2      26.6     55.7
A5.2 A5.2      27.4     55.6
A6.2 A6.2      28.7     55.5
A3.1 A3.1      24.6     55.8

$cities
   Longitude Latitude           Name
1       24.0     55.0          Kowno
2       25.3     54.7          Wilna
3       26.4     54.4       Smorgoni
4       26.8     54.3      Moiodexno
5       27.7     55.2      Gloubokoe
7       28.5     54.3     Studienska
8       28.7     55.5        Polotzk
9       29.2     54.4           Bobr
10      30.2     55.3        Witebsk
11      30.4     54.5         Orscha
13      32.0     54.8       Smolensk
14      33.2     54.9    Dorogobouge
15      34.3     55.2          Wixma
16      34.4     55.5          Chjat
17      36.0     55.5        Mojaisk
18      37.6     55.8         Moscou
19      36.6     55.3      Tarantino
20      36.5     55.0 Malo-Jarosewii

 

 

* source : R riverplot package, author : January Weiner, data source : Charles Joseph Minard

 

 

 Sankey Diagram에서 사용하는 node, edge라는 용어를 이해하기 위해서, 두 개체 간 쌍을 이룬 관계 (mathematical structures used to model pairwise relations between objects)를 다루는 Graph Theory에 대해서 간략히 짚고 넘어가겠습니다.

 

아래 그래프처럼 점(Node or Point or Vertice)과 선(Edge or Link or Line or Arc)으로 개체 간의 관계를 나타내는 그래프로 나타내어 연구하는 수학, 컴퓨터 과학 분야가 Graph theory입니다.  최적화(optimization) 할 때도 네트워크 그래프 많이 쓰곤 합니다.

 

 

 

 

Sankey Diagram 그리려면

 

  - Nodes : 개체들의 ID, Longitude, Latitude, Labels

  - Edges : 개체 간 관계를 나타내는 ID 1, ID 2, Value (or weight) 

 

정보가 필요합니다.

 

> str(minard)
List of 3
 $ edges :'data.frame':	41 obs. of  4 variables:
  ..$ ID1      : chr [1:41] "A1" "A2" "A3" "A4" ...
  ..$ ID2      : chr [1:41] "A2" "A3" "A4" "A5" ...
  ..$ Value    : num [1:41] 422000 400000 320000 320000 300000 280000 240000 210000 180000 175000 ...
  ..$ direction: Factor w/ 2 levels "A","R": 1 1 1 1 1 1 1 1 1 1 ...
 $ nodes :'data.frame':	39 obs. of  3 variables:
  ..$ ID       : chr [1:39] "A1" "A2" "A3" "A4" ...
  ..$ Longitude: num [1:39] 24 24.5 25.5 26 27 28 28.5 29 30 30.3 ...
  ..$ Latitude : num [1:39] 54.9 55 54.5 54.7 54.8 54.9 55 55.1 55.2 55.3 ...
 $ cities:'data.frame':	18 obs. of  3 variables:
  ..$ Longitude: num [1:18] 24 25.3 26.4 26.8 27.7 28.5 28.7 29.2 30.2 30.4 ...
  ..$ Latitude : num [1:18] 55 54.7 54.4 54.3 55.2 54.3 55.5 54.4 55.3 54.5 ...
  ..$ Name     : Factor w/ 20 levels "Bobr","Chjat",..: 5 18 15 9 4 16 13 1 19 12 

 

 

 

 

R riverplot package를 사용해서 드디어 Sankey diagram을 그려보겠습니다.  R script는 riverplot package(* author : January Weiner)에 있는 예제 script를 그대로 인용하였습니다. (날로 먹는 듯한 이 기분..^^;) 

 

 

> ############################
> # Sankey diagram 
> # using R riverplot package 
> # minard data (list format)
> ############################
> 
> # install.packages("riverplot")
> library(riverplot)
> data( minard )
> 
> nodes <- minard$nodes
> edges <- minard$edges
> colnames( nodes ) <- c( "ID", "x", "y" )
> colnames( edges ) <- c( "N1", "N2", "Value", "direction" )
> 
> 
> # color the edges by troop movement direction
> edges$col <- c( "#e5cbaa", "black" )[ factor( edges$direction ) ]
> 
> # color edges by their color rather than by gradient between the nodes
> edges$edgecol <- "col"
> 
> # generate the riverplot object and a style
> river <- makeRiver( nodes, edges )
> style <- list( edgestyle= "straight", nodestyle= "invisible" )
> 
> # plot the generated object
> plot( river, lty= 1, default_style= style )
> 
> # Add cities
> with( minard$cities, points( Longitude, Latitude, pch= 19 ) )
> with( minard$cities, text( Longitude, Latitude, Name, adj= c( 0, 0 ) ) )
> # Add title
> title("Sankey Diagram - Napoleon army march, minard")

 

* R script author : January Weiner

 

 

다음번 포스팅에서는 igraph package를 사용해서 방향성 있는 가중 네트워크를 시각화해보겠습니다.

 

이번 포스팅이 도움이 되었다면 아래의 '공감 ~♡'를 꾸욱 눌러주세요. ^^

 

[Reference]

 - R riverplot package manual : https://cran.r-project.org/web/packages/riverplot/riverplot.pdf

 - On minards visualization : https://robots.thoughtbot.com/analyzing-minards-visualization-of-napoleons-1812-march

 

 

728x90
반응형
Posted by Rfriend
,