networkD3
패키지는 htmlwidget
패키지를 바탕으로 D3 기반의 네트워크 시각화를 가능하게 해준다.
기존의 igraph 패키지로는 만족스러운 모양새의 그래프를 만들기 힘들다고 생각해서 gephi와 d3를 사용하고 있었는데 d3 기반의 시각화를 R로 빠르게 구현할 수 있어서 관심을 가지게 되었다.
물론 세세한 부분까지 변경하는 것은 조금 무리가 있을 것 같지만 처음 d3로 force layout을 만들 때 고생했던 것을 생각하면 간단한 코드로 이렇게 force layout을 구현할 수 있다는 것 만으로도 상당히 유용하게 사용할 수 있을 것 같다.
이렇게 생성한 그래프는 html 형태로 export 할 수 있고, RMarkdown 문서에 바로 첨부할 수도 있다
http://blog.thomsonreuters.com/index.php/mobile-patent-suits-graphic-of-the-day/
위 링크는 모바일 관련 소송에 대한 그래픽 자료이다. 해당 데이터를 d3를 이용해 네트워크 형태로 시각화한 자료는 여기에서 확인할 수 있다. 이 글에서는 link데이터를 가져와서 networkD3 패키지로 시각화 하는 작업을 진행할 것이다
library(networkD3)
library(dplyr)
##
## Attaching package: 'dplyr'
##
## The following objects are masked from 'package:stats':
##
## filter, lag
##
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
시작하기 전에 네트워크 시각화를 하는데 필요한 데이터의 형태에 대해서 알아볼 필요가 있다.
네트워크 형태로 데이터를 시각화할 때 필요한 데이터는 두 가지가 있다
우선 노드(Node 또는 Vertex)에 대한 정보가 필요하고
이 각각의 노드를 어떻게 연결할 것인지(Link 또는 Edge)에 대한 정보가 필요하다
networkD3
패키지에서 기본적으로 제공하는 데이터 중에서 소설 레미제라블의 인물 관계도를 나타내는 MisLinks
와 MisNodes
데이터를 가지고 살펴보자
data(MisLinks, MisNodes)
head(MisNodes)
## name group size
## 1 Myriel 1 15
## 2 Napoleon 1 20
## 3 Mlle.Baptistine 1 23
## 4 Mme.Magloire 1 30
## 5 CountessdeLo 1 11
## 6 Geborand 1 9
이름에서부터 느껴지듯이 MisNodes
는 Node에 대한 데이터다
각각의 노드를 구별하는 열이 존재하고(여기서는 name
열)
해당 노드가 그룹화 되어있는 경우 그룹을 표시해준다(group
)
또 필수적이지는 않지만 각각의 노드가 가지는 값(중요도, 크기 등)이 있을 경우에 해당 값을 추가한다(size
)
head(MisLinks)
## source target value
## 1 1 0 1
## 2 2 0 8
## 3 3 0 10
## 4 3 2 6
## 5 4 0 1
## 6 5 0 1
MisLinks
는 노드를 연결하는 정보에 대한 데이터이다
한 행은 source
로 부터 target
으로 연결되는 선 하나를 나타낸다
각 source
와 target
열의 값은 Node 데이터의 인덱스(0부터 시작)를 나타낸다
따라서 source = 1, target = 0은 source = Napoleon, target = Myriel을 의미한다
그러면 다시 돌아와서 우리가 처리할 데이터를 확인해보자
데이터는 JSON 형태로 되어있다
links = '[
{"source": "Microsoft", "target": "Amazon", "type": "licensing"},
{"source": "Microsoft", "target": "HTC", "type": "licensing"},
{"source": "Samsung", "target": "Apple", "type": "suit"},
{"source": "Motorola", "target": "Apple", "type": "suit"},
{"source": "Nokia", "target": "Apple", "type": "resolved"},
{"source": "HTC", "target": "Apple", "type": "suit"},
{"source": "Kodak", "target": "Apple", "type": "suit"},
{"source": "Microsoft", "target": "Barnes & Noble", "type": "suit"},
{"source": "Microsoft", "target": "Foxconn", "type": "suit"},
{"source": "Oracle", "target": "Google", "type": "suit"},
{"source": "Apple", "target": "HTC", "type": "suit"},
{"source": "Microsoft", "target": "Inventec", "type": "suit"},
{"source": "Samsung", "target": "Kodak", "type": "resolved"},
{"source": "LG", "target": "Kodak", "type": "resolved"},
{"source": "RIM", "target": "Kodak", "type": "suit"},
{"source": "Sony", "target": "LG", "type": "suit"},
{"source": "Kodak", "target": "LG", "type": "resolved"},
{"source": "Apple", "target": "Nokia", "type": "resolved"},
{"source": "Qualcomm", "target": "Nokia", "type": "resolved"},
{"source": "Apple", "target": "Motorola", "type": "suit"},
{"source": "Microsoft", "target": "Motorola", "type": "suit"},
{"source": "Motorola", "target": "Microsoft", "type": "suit"},
{"source": "Huawei", "target": "ZTE", "type": "suit"},
{"source": "Ericsson", "target": "ZTE", "type": "suit"},
{"source": "Kodak", "target": "Samsung", "type": "resolved"},
{"source": "Apple", "target": "Samsung", "type": "suit"},
{"source": "Kodak", "target": "RIM", "type": "suit"},
{"source": "Nokia", "target": "Qualcomm", "type": "suit"}
]'
jsonlite
패키지의 fromJSON
함수를 이용해서 json데이터를 data.frame으로 변환시킨다
link_df = jsonlite::fromJSON(links)
이렇게 가져온 데이터는 link만을 담고 있기 때문에 node는 link의 source와 target 전체에서 unique한 항목만 추출한다
그리고 link에서 source와 target은 node의 인덱스를 가지고 표현해야 하기 때문에
node_df
라는 data.frame에 0부터 시작하는 인덱스 번호를 부여하고
다시 link_df
에 left_join
해서 source와 target 각각의 인덱스를 가져온다
# node의 index 숫자는 0부터 시작해야 한다
# dplyr::row_number()가 1부터 숫자를 매기기 때문에 거기서 1씩을 빼도록 한다
node_df = data.frame(node = unique(c(link_df$source, link_df$target))) %>%
mutate(idx = row_number()-1)
# node_df에서 index값을 가져와서 source와 target에 해당하는 index 값을 저장한다
link_df = link_df %>%
left_join(node_df %>% rename(source_idx = idx), by=c('source' = 'node')) %>%
left_join(node_df %>% rename(target_idx = idx), by=c('target' = 'node'))
## Warning in left_join_impl(x, y, by$x, by$y): joining factor and character
## vector, coercing into character vector
## Warning in left_join_impl(x, y, by$x, by$y): joining factor and character
## vector, coercing into character vector
완성된 데이터는 다음과 같은 형태를 가진다
node_df
## node idx
## 1 Microsoft 0
## 2 Samsung 1
## 3 Motorola 2
## 4 Nokia 3
## 5 HTC 4
## 6 Kodak 5
## 7 Oracle 6
## 8 Apple 7
## 9 LG 8
## 10 RIM 9
## 11 Sony 10
## 12 Qualcomm 11
## 13 Huawei 12
## 14 Ericsson 13
## 15 Amazon 14
## 16 Barnes & Noble 15
## 17 Foxconn 16
## 18 Google 17
## 19 Inventec 18
## 20 ZTE 19
link_df
## source target type source_idx target_idx
## 1 Microsoft Amazon licensing 0 14
## 2 Microsoft HTC licensing 0 4
## 3 Samsung Apple suit 1 7
## 4 Motorola Apple suit 2 7
## 5 Nokia Apple resolved 3 7
## 6 HTC Apple suit 4 7
## 7 Kodak Apple suit 5 7
## 8 Microsoft Barnes & Noble suit 0 15
## 9 Microsoft Foxconn suit 0 16
## 10 Oracle Google suit 6 17
## 11 Apple HTC suit 7 4
## 12 Microsoft Inventec suit 0 18
## 13 Samsung Kodak resolved 1 5
## 14 LG Kodak resolved 8 5
## 15 RIM Kodak suit 9 5
## 16 Sony LG suit 10 8
## 17 Kodak LG resolved 5 8
## 18 Apple Nokia resolved 7 3
## 19 Qualcomm Nokia resolved 11 3
## 20 Apple Motorola suit 7 2
## 21 Microsoft Motorola suit 0 2
## 22 Motorola Microsoft suit 2 0
## 23 Huawei ZTE suit 12 19
## 24 Ericsson ZTE suit 13 19
## 25 Kodak Samsung resolved 5 1
## 26 Apple Samsung suit 7 1
## 27 Kodak RIM suit 5 9
## 28 Nokia Qualcomm suit 3 11
데이터를 모두 준비하고 나면 forceNetwork 함수로 그래프를 생성할 수 있다
함수의 동작을 위해 최소한 필요한 옵션은 다음과 같다
Links에는 link가 담긴 data.frame
Nodes에는 node가 담긴 data.frame
Source에는 Links에서 source의 index가 담긴 열 이름
Target에는 Links에서 target의 index가 담긴 열 이름
NodeID에는 Nodes에서 node이름이 담긴 열 이름
Group은 Nodes에서 group이름이 담긴 열 이름(없으면 NodeID와 동일하게)
forceNetwork(Links = link_df, Nodes = node_df,
Source = 'source_idx', Target = 'target_idx',
NodeID = 'node', Group = 'node')
bounded
옵션을 TRUE로 하면 Node들이 특정 범위 밖으로 나가지 못하는 범위가 생긴다
opacityNoHover
옵션을 TRUE로 하면 마우스를 node에 올리지 않아도 label이 보인다
forceNetwork(Links = link_df, Nodes = node_df,
Source = 'source_idx', Target = 'target_idx',
NodeID = 'node', Group = 'node',
bounded = TRUE,
opacityNoHover = TRUE)
zoom = TRUE
로는 zoom을 가능하게 한다
forceNetwork(Links = link_df, Nodes = node_df,
Source = 'source_idx', Target = 'target_idx',
NodeID = 'node', Group = 'node',
bounded = TRUE,
opacityNoHover = TRUE,
zoom = TRUE)
fontSize는 폰트 크기
linkDistance는 link의 길이
opacity는 전체적인 투명도를 조절한다
forceNetwork(Links = link_df, Nodes = node_df,
Source = 'source_idx', Target = 'target_idx',
NodeID = 'node', Group = 'node',
opacityNoHover = TRUE,
zoom = TRUE,
fontSize = 15,
linkDistance = 75,
opacity = 0.9)