聰明不如鈍筆
총명불여둔필
assignment KindeR

최대한 친절하게 쓴 R로 사회연결망 분석하기(feat. tidygraph, ggraph)


사회 연결망(社會連結網) 또는 소셜 네트워크(영어: Social Network)는 사회학에서 개인, 집단, 사회의 관계를 네트워크로 파악하는 개념이다. 즉 개인 또는 집단이 네트워크의 하나의 노드(node)이며, 사회연결망은 이 각 노드들 간의 상호의존적인 관계(tie)에 의해 만들어지는 사회적 관계 구조를 말한다. 모든 노드들은 네트워크 안에 존재하는 개별적인 주체들이고, 타이(tie)는 각 노드들 간의 관계를 뜻한다.

─ 위키피디아 '사회 연결망'


바야흐로 소셜네트워크서비스(SNS) 전성시대라 따로 설명이 필요없을지 모르지만 사회 연결망은 이렇게 정의할 수 있습니다. 그리고 R는 사회 연결망 분석(SNA·Social Network Analysis)에 있어서도 뛰어난 기량을 갖추고 있습니다.


이 포스트에서는 tidygraph, ggraph 패키지를 활용해 △최근 1년간 가수 아이유 씨와 피처링한 뮤지션 △수도권 지하철 △프로배구 남자부 학맥을 분석하도록 하겠습니다.


물론 많은 분들이 R 관련 검색어로 이 포스트에 찾아오셨겠지만, 혹시나 모르시는 분이 계실까 노파심에 말씀드리면 R는 통계 계산과 그래픽에 활용하는 무료 소프트웨어 환경(R is a free software environment for statistical computing and graphics)"입니다.


무료라는 건 누구나 이 프로그램을 내려받아서 설치할 수 있다는 뜻입니다. 2018년 9월 현재 최신 버전은 3.5.1이며 마이크로소프트(MS) 윈도용은 이 페이지에서 다운로드 받으실 수 있습니다.


이 포스트는 '최대한 친절하게'를 목표로 하고 있지만 프로그램 설치법까지 모르시는 분이 아니 계실 것이라고 생각해 따로 설명하지는 않겠습니다.


설치를 마친 다음 R를 실행하면 아래 같은 화면이 나타납니다. 창 위에 써 있는 것처럼 이 창을 'R 콘솔(Console)'이라고 부릅니다.



스크린샷을 찍기 전 저는 "print('Hello, World!')"라고 입력했습니다. R는 기본적으로 이렇게 '>' 뒤에 명령어(함수)를 입력하는 방식으로 작동합니다. 저는 화면에 'Hello, World!'를 출력해 달라고 입력했고 결과도 그렇게 나왔습니다.


이렇게 입력한 명령어를 코드(code)라고 부르고 코드를 활용하는 걸 코딩(coding)이라고 합니다. 코딩은 프로그래밍이라고 일컫기도 합니다.


이렇게 프로그래밍에 쓰는 소프트웨어를 흔히 프로그래밍 언어라고 부릅니다. 언어니까 문법이 존재하고 각 언어별 문법에 맞도록 코드를 입력하면 프로그램을 만들 수 있습니다. 위에 등장한 "Hello, World!" 프로그램은 코딩을 배울 때 맨 처음에 제일 많이 쓰는 예제입니다.


프로그래밍 언어는 대부분 기본 기능 이외에 기능을 확장할 수 있도록 별도 꾸러미를 마련하고 있습니다. R에서는 이런 꾸러미를 패키지(package)라고 부릅니다. 위에서 tidygraph, ggraph 패키지라고 말씀드린 건 그런 까닭입니다.


원래 R에서 SNA를 진행하려고 할 때 제일 많이 쓰는 패키지는 igraph입니다. 이 포스트에서도 이 패키지 기능을 일부 활용하게 됩니다.


그래도 주(主)가 되는 건 tidygraph와 ggraph입니다. igraph 대신 이 두 패키지를 고른 건 기본 R 문법을 깔끔하게(tidy) 가다듬은 tidyverse 생태계에서 작동하기 때문입니다. (이 포스트가 속한 '최대한 친절하게 쓴 R' 시리즈는 기본적으로 tidyverse 생태계 소개가 목적입니다.)


R에서 패키지를 설치할 때는 'install.packages()'라는 함수를 씁니다. 우리는 tidygraph, ggraph를 내려받을 거니까 이렇게 입력하면 됩니다.

install.packages(c('tidygraph', 'ggraph'))


여기서 'c()' 함수는 '사슬같이 잇다(concatenate)'는 뜻에서 유래한 함수로 데이터 여러 개를 묶을 때 씁니다. 만약 두 패키지를 차례로 설치하고 싶으시다면 그냥 이렇게 입력하셔도 됩니다.

install.packages('tidygraph')
install.packages('ggraph')


이렇게 입력하신 다음 엔터를 치시면 R가 어떤 (미러) 사이트에서 패키지를 다운로드 할지 묻습니다. 여기서는 어떤 사이트에서 내려받아도 크게 관계가 없습니다. 그냥 한 번 더 엔터를 치시고 설치를 진행합니다.


R에서는 패키지를 받았다고 곧바로 활용할 수 있게 되는 건 아닙니다. 이 패키지를 메모리에 읽어들이는 과정이 필요합니다. 이때 쓰는 함수는 'library()'입니다. library()는 한 번에 패키지 하나만 불러오기 때문에 따로 입력하셔야 합니다.

library('tidygraph')
library('ggraph')


만약 이전에 install.packages()로 패키지를 내려받으신 적이 있다면 그다음에는 설치 과정 없이 바로 library()로 불러들여서 쓰시면 됩니다. 


어느덧 R 함수를 벌써 네 개 - print(), install.packages(), c(), library() - 배우셨습니다.



피처링 데이터를 불러오자

아래 첨부 파일에는 가수 아이유 씨와 최근 1년 동안 공동 작업한 뮤지션 그리고 그 뮤지션과 공동 작업한 뮤지션 정보가 들어 있습니다. (아이유 씨를 고른 건 이 글을 쓰는 동안 이 분 노래가 들렸기 때문일 뿐 특별한 이유는 없습니다.)


featuring.csv


이 파일은 확장자에서 확인하실 수 있는 것처럼 CSV(Comma Separated Value·쉼표로 구분한 값)라는 형식입니다.


CSV는 기본적으로 텍스트 파일입니다. 표에서 열에 해당하는 내용을 쉼표로 구분했기 때문에 이런 이름이 붙었습니다. 행은 물론 행으로 구분합니다.


컴퓨터에 마이크로소프트(MS) 엑셀이 깔려 있다면 다른 엑셀 파일을 열듯이 이 파일을 열어 내용을 확인해 보실 수 있습니다.


R에는 CSV 파일을 불러올 수 있도록 'read.csv()'라는 함수가 들어있습니다. 그래서 이렇게 입력하면 이 파일을 R로 불러오실 수 있습니다.

read.csv('featuring.csv')


파일을 잘 불러왔나요? 혹시 아래 에러 메시지를 출력하지 않았나요?


Error in file(file, "rt") : cannot open the connection

In addition: Warning message:

In file(file, "rt") :

  cannot open file 'temperature.csv': No such file or directory



R에서 이런 메시지를 출력했다면 파일 위치를 지정하지 않았기 때문입니다. R는 파일 위치를 별도로 지정하지 않으면 작업 디렉토리(폴더)에서 파일을 불러오게 됩니다.


MS 윈도에서는 '(내) 문서'가 기본 작업 폴더입니다. 만약 한 번도 작업 폴더를 바꾸신 적이 없다면 이 CSV 파일을 문서 안으로 옮기셔야 합니다.


물론 작업 폴더 자체를 바꿀 수도 있습니다. 작업 폴더를 바꾸는 기본적인 방법은 메뉴에서 File - Change dir...를 선택하는 겁니다. 그러면 폴더 선택창이 뜰 겁니다. 파일을 다른 위치에 받으셨다면 그리로 작업 폴더를 바꾸시면 됩니다.


명령어를 쓰시는 쪽이 편하시다면 'setwd()'를 활용하셔도 됩니다.


이렇게 폴더 설정이 끝났다면 이번에는 read.csv()가 정상 작동할 겁니다[각주:1].

read.csv('featuring.csv')


파일 내용이 주루룩 나타났면 성공입니다. 단, read.csv()는 그저 파일을 읽어오라는 뜻이라 화면에 내용을 출력할 뿐 메모리에 담아두지는 않습니다. 앞으로 계속 이 내용을 쓰고 싶으면 컴퓨터 메모리에 빈 방(공간)을 만들어 데이터를 담아둬야 합니다.


코딩에서는 이런 방을 '변수'라고 부릅니다. R에서 변수에 어떤 값을 넣으라고 할 때는 '<-' 또는 '=' 기호를 씁니다.


이번에는 feat라는 변수에 데이터를 넣도록 하겠습니다. 이렇게 쓰면 됩니다.

feat <- read.csv('featuring.csv')


이번에는 아무 반응이 없는 게 정상입니다. 그냥 방에 자료를 넣어두라는 내용일 뿐이니까요. 대신 데이터 맨 첫 여섯 줄만 보여주는 'head()' 함수를 써서 내용이 잘 들어와 있는 걸 확인할 수 있습니다.

head(feat)
##       from       to
## 1   아이유 G-DRAGON
## 2 에픽하이   아이유
## 3 에픽하이     오혁
## 4   아이유     오혁
## 5    HIGH4   아이유
## 6 에픽하이     MINO


이렇게 데이터가 있다고 바로 SNA를 진행할 수 있는 건 아닙니다. 현재 이 자료는 '데이터 프레임'이라는 클래스(형식)입니다. 어떤 자료가 어떤 형식인지는 class() 함수로 확인할 수 있습니다.

class(feat)
## [1] "data.frame"


데이터 프레임은 MS 엑셀처럼 행과 열이 있는 표 형태 자료라고 보시면 됩니다. 우리가 다루는 데이터는 대부분 데이터 프레임입니다.


SNA를 진행하려면 이 데이터 프레임을 (당연히) 그래프 형식으로 바꿔야 합니다. tidygraph에서는 as_tbl_graph()가 데이터 프레임 등을 그래프 형식으로 바꾸는 구실을 합니다.

fg <- as_tbl_graph(feat)


이번에도 데이터 형식을 확인해야겠죠? 

class(fg)
## [1] "tbl_graph" "igraph"


데이터 형식이 두 가지로 나왔습니다. tbl_graph는 tidygraph에서 쓰는 그래프 형식입니다. 이때 tbl은 'tibble'을 줄인 말입니다.


tibble 혹은 tbl_df는 R에서 기본적으로 제공하는 데이터 프레임을 tidyverse에서 활용하기 쉽도록 업그레이드한 형태라고 이해하시면 됩니다.


뒤에 igraph가 따라 나온 건 만약 tidygraph를 불러오지 않아서 tbl_df를 이해하지 못하는 환경에서는 이 데이터를 igraph 데이터 형식으로 이해하겠다는 뜻입니다.


이건 그만큼 igraph가 SNA 분야에서 유명한 패키지이기 때문이기도 하고, tidygraph가 igraph에 뿌리를 두고 있고 때문이기도 합니다.


그러면 tbl_graph는 어떻게 생겼는지 테이터 내용을 보도록 하겠습니다. R에서 데이터(변수) 내용을 보고 싶을 때는 그냥 데이터 이름만 치면 됩니다.

fg
## # A tbl_graph: 9 nodes and 12 edges
## #
## # A directed acyclic simple graph with 1 component
## #
## # Node Data: 9 x 1 (active)
##   name
##   <chr>
## 1 아이유
## 2 에픽하이
## 3 HIGH4
## 4 MINO
## 5 사이먼 도미닉
## 6 G-DRAGON
## # ... with 3 more rows
## #
## # Edge Data: 12 x 2
##    from    to
##   <int> <int>
## 1     1     6
## 2     2     1
## 3     2     7
## # ... with 9 more rows


node(노드)와 edge(엣지)라는 개념이 등장했습니다.


기억력이 좋으신 분은 이 글 맨 처음에 나온 인용문에 노드가 등장했다는 걸 기억하고 계실지 모르겠습니다. 이 인용문에서 타이(tie)에 해당하는 개념이 여기서는 엣지입니다.


feat에서는 그럼 뭐가 노드고, 뭐가 엣지일까요?


그렇습니다. 뮤지션이 각각 노드고, 이들이 피처링한 관계가 엣지가 됩니다.


tidygraph에서는 노드에 번호로 코드를 붙인 다음 이 번호를 연결하는 방식으로 엣지를 정리합니다.



그림을 그립시다

말로만 설명하면 이 네트워크 그래프가 어떻게 생겼는지 잘 감을 잡기 어렵습니다. 실제로 그래프를 그려보겠습니다.


R에서 그래프를 그리는 가장 기본 함수는 plot()입니다. 그러면 이렇게 쓰면 그래프가 나타날 겁니다.

plot(fg)


plot()는 네트워크를 시각화하는 함수가 아니라 R에 들어 있는 기본 시각화 함수입니다. tidyverse 생태계에서는 ggplot2 패키지에 들어 있는 ggplot() 함수가 시각화 기능을 담당합니다. 네트워트 시각화에서는 ggraph()가 바로 ggplot()이 담당하던 구실을 합니다.


아직 ggplot()에 익숙하지 않으시다면 '최대한 친절하게 쓴 R로 그래프 그리기(feat. ggplot2)' 포스트를 읽으시면 다음 부분을 이해하시는 데 도움이 될 수 있습니다.


지금 우리가 그린 그래프는 일단 아래 같은 코드로 시작할 수 있습니다. 아래 코드를 말로 풀어 설명하면 fg라는 데이터를 가지고 와서 각 노드를 점으로 찍고, 엣지는 선으로 연결하라는 뜻입니다.  

ggraph(fg) +  geom_node_point()  +  geom_edge_link()


모양이 같은 건 알겠는데 뭔가 심심하죠? 이건 ggplot()나 ggraph()나 기본 코드에 살을 붙여 가는 방식으로 그래프를 완성하기 때문입니다. 차차 살을 붙이도록 하겠습니다.


직접 따라서 코드를 입력하신 분은 아래 경고 메시지를 마주하셨을 겁니다.  

## Using `nicely` as default layout


ggraph()는 레이아웃(layout)을 지정하도록 되어 있는데 따로 지정하지 않았으니 nicely 레이아웃을 자동으로 선택했다는 뜻입니다.


ggraph에서는 nicely 이외에도 △circle △dh △drl △fr △gem △graphopt △grid △kk △lgl △mds △kk △lgl △mds △ramdomly △star 같은 레이아웃을 제공합니다


 같은 데이터가 레이아웃 설정에 따라 어떻게 변하는지는 아래 GIF를 통해 확인하실 수 있습니다.


 

이 외에도 특정한 자료 형태에서만 가능한 레이아웃도 있습니다.


ggraph()에서 활용 가능한 전체 레이아웃이 궁금하신 분은 제작자가 만든 이 ggraph() 레이아웃 소개 페이지를 방문해 보시면 도움이 될 겁니다.


이 포스트에서는 기본적으로 'kk' 레이아웃을 사용할 예정입니다.



파이프 또는 %>%

tidyverse 생태계를 상징하는 기능 가운데 하나는 파이프(pipe)입니다.


금속이나 플라스틱으로 만든 파이프가 유체(액체 또는 기체)를 다른 곳으로 연결하는 것처럼 코딩에서 파이프는 데이터를 한 함수에서 다른 함수로 보내는 구실을 합니다.


tidyverse 생태계에서는 '%>%' 표시로 파이프를 나타냅니다.


위에서 feat라는 데이터를 as_tbl_graph()라는 함수로 보낼 때는 'as_tbl_graph(feat)'라고 썼습니다. 파이프를 사용해 이를 표현하려면 'feat %>% as_tbl_graph'라고 표현하면 됩니다.


파이프를 연결하면 유체를 계속 다른 곳으로 보낼 수 있는 것처럼 %>%를 이어 쓰면 데이터를 계속 다른 함수로 보낼 수 있습니다. 이렇게 말입니다.

feat %>%
  as_tbl_graph() %>%
  ggraph(layout='kk') + 
  geom_node_text(aes(label=name)) +
  geom_edge_link(aes(start_cap = label_rect(node1.name), end_cap = label_rect(node2.name)))


여기서 알 수 있는 사실 가운데 하나는 %>%로 코드를 연결할 때는 줄(행)도 바꿀 수 있는 점입니다. 그건 '+'도 마찬가지입니다.


이렇게 파이프를 쓰면 일단 따로 변수(여기서는 fg)를 한 번 더 만들지 않고도 데이터를 처리할 수 있다는 장점이 있습니다.


또 데이터 흐름을 눈으로 볼 수 있기 때문에 코드를 직관적으로 이해하는 데도 도움을 받을 수 있습니다.


그래도 이번 코드가 어렵긴 어렵습니다.


앞서 본 것과 이번 코드가 다른 첫 번째 요소는 일단 geom_node_point() 대신에 geom_node_text()를 썼다는 점입니다. 이 함수는 노드 자리에 글씨를 쓰라는 뜻입니다.


이렇게만 쓰면 R가 어떤 글씨를 써야 할지 모르겠죠? 우리가 선택한 텍스트는 레이블(label)인데 이 자리에 노드 이름(name)을 쓰라고 R에게 알려주는 게 바로 'label=name'이 담당하는 기능입니다.


그러면 그 앞에 aes를 붙인 건 뭘까요?


여기서 aes는 '미적인'이라는 뜻을 나타내는 영어 낱말 'aesthetic'을 줄인 말입니다. ('에스테틱' 아시죠?)


ggplot() 계열에서 저 말은 특정한 값을 지정하는 게 아니라 변수에 따라 값을 달리하라는 뜻입니다. node마다 name이 다를 테니까 이렇게 aes를 붙인 겁니다.


그런 이유로 geom_edge_link() 안에 들어 있는 요란한 표현 앞에도 aes가 붙어 있습니다.


안에 있는 내용은 각 노드 이름이 들어 갈 수 있도록 여백을 주고 선을 그리라는 내용을 담고 있습니다. 실제 그래프를 보면 뮤지션 이름이 들어갈 만큼 선이 비어 있습니다.



처음에는 아이유 씨 때문에 이 그래프를 그렸는데 그리고 나니 어쩐지 에픽하이가 제일 중요한 노드처럼 보입니다. 이건 어떻게 확인할 수 있을까요?



중심성(centrality)

SNA에서는 어떤 노드가 (상대적으로) 중요한지 '중심성'을 가지고 판단합니다. 중심성에는 다양한 종류가 있습니다.


이번에도 위키피디아 도움을 받아보겠습니다.


그래프 이론에서 중심성(中心性, centrality)이란 그래프 혹은 사회 연결망에서 꼭짓점(vertex) 혹은 노드(node)의 상대적 중요성을 나타내는 척도이다. 이 중심성은 지수로 계산되는데, 이 중심성 지수는 그 계산 방법에 따라 크게 연결 중심성(degree centrality), 근접 중심성(closeness centrality), 매개 중심성(betweenness centrality), 고유벡터 중심성(eigenvector centrality)이 주로 쓰인다.


애석하게도 각 중심성에 대한 설명은 없네요.


그래서 구글링 끝에 '[네트워크 이론] 다양한 중심성(Centrality) 척도들'이라는 포스트를 발견했습니다. 이 포스트 내용을 토대로 간단히 정리하면 각 중심성별 개념을 정리해 보겠습니다.


가장 기본이 되는 건 연결(degree) 중심성입니다. 연결 중심성은 한 노드에 연결된 모든 엣지 개수입니다.


그런데 이 개수가 많다고 무조건 중요한 노드는 아닙니다. 페이스북에서 친구가 무조건 많다고 유명인사는 아닐 겁니다. 유명인사인 친구가 많을수록 유명인사겠죠. 이렇게 한 노드와 연결된 다른 노드의 중요성까지 따져서 중심성을 계산한 결과가 고유벡터(eigenvector) 중심성입니다. 


문제는 한 노드가 중심성이 아주 높게 나올 경우 이와 연결된 다른 노드도 덩달아 지나치게 중심성이 올라갈 수 있다는 점입니다.


그래서 중심성을 다른 노드로 전달할 때 일정 부분 제약을 줄 필요가 있습니다. 이렇게 바깥으로 나가는 영향력을 제한한 중심성이 페이지랭크(pagerank)입니다. (구글에서 검색 결과를 노출할 때 쓰는 알고리즘을 일컫는 페이지 랭크 맞습니다.) 


여기까지는 한 노드가 다른 노드와 얼마나 연결된 상태인지를 가지고 중심성을 따집니다. 위키피디아에 나온 매개(betweenness) 중심성과 근접(closeness) 중심성은 접근법이 조금 다릅니다. 


매개 중심성은 노드 사이 최단 경로를 가지고 계산합니다. 만약 그래프에 존재하는 노드 두 개를 최단 거리로 연결한다고 할 때 유독 특정한 노드를 거쳐야 하는 일이 많다면 이 노드가 중요하다고 보는 겁니다.


근접 중심성도 거리 기준입니다. 근접 중심성은 이름 그대로 어떤 노드가 중요하다면 다른 노드에서 최단 거리로 접근할 수 있을 것이라는 가정에 뿌리를 두고 있습니다.


이렇게 개념을 봐도 감이 잘 오시지 않죠? 아래 표는 실제로 이 피처링 네트워크에서 중심성을 구한 결과입니다.


각 항목 첫 번째 행(제목)을 선택하시면 해당 항목 순서에 따라 다시 정렬하니까 한 번 여러분 예상과 실제 결과가 맞는비 비교해 보세요. 


 뮤지션  연결  고유벡터  페이지랭크  매개  근접
 에픽하이  6  1  .0678  0  .0588
 사이먼 도미닉  1  .715  .110  0  .0156
 더콰이엇  0  .715  .204  0  .0139
 MINO  2  .715  .0774  0  .0179
 아이유  2  .514  .135  3  .0179
 오혁  0  .446  .135  0  .0139
 수현  0  .294  .0774  0  .0139
 HIGH4  1  .151  .0678  0  .0200
 G-DRAGON  1  .151  .125  0  .0139


이 단락 처음에 링크한 포스트를 찾아 보신 분이라면 행렬 계산이 기다리고 있어 당황하셨을지 모릅니다.


중심성 계산 기본 원리에는 행렬 계산이 들어가지만 제가 직접 이걸 계산한 건 아닙니다. (당연히) tidygraph에 들어 있는 함수가 계산했습니다.



계산 결과를 더할 때는 mutate()

ggraph()는 tidyverse 생태계 문법을 따른다고 했습니다. 이 생태계에서 데이터 처리에 쓰는 패키지가 바로 dplyr입니다. 데이터를 분석할 때 dplyr에서 제일 많이 쓰는 함수는 아래 여섯 가지입니다.


▌dplyr 주요 함수 

 함수  기능  사용법
 arrange()  행 정렬  arrange(데이터, 변수 이름)
 filter()  행 추출  filter(데이터, 조건)
 group_by()  행 결합  group_by(조건)
 mutate()  열 추가  mutate(데이터, 새 변수 = 계산식)
 select()  열 선택  select(데이터, 변수 이름 또는 인덱스)
 summarise()  행 요약  summarise(데이터, 새 변수 = 계산식)


이 포스트는 SNA를 다루고 있는 만큼 dplyr()에 대해서는 최대한 친절하게 소개하지 않습니다. dplyr가 궁금하신 분은 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트를 참고하시면 도움이 될 수 있습니다.


그래도 mutate()에 대해서는 설명이 필요합니다. 영어 낱말을 많이 아시는 분들은 'mutate'를 들으면 '돌연변이를 만들다'는 뜻을 곧바로 떠올리실 겁니다. dplyr에서 mutate()가 만드는 돌연변이는 '계산 결과'입니다.


예컨대 이런 겁니다. 야구에서 타율을 계산하는 식은 '안타÷타수'입니다. 만약 어떤 데이터에 안타 숫자만 있는 열이 있고, 타수만 있는 열이 따로 있다고 칩시다. 그러면 '안타÷타수'를 계산한 결과를 담은 열을 만들고 이 열에 '타율'이라는 이름을 붙일 수 있을 겁니다. 이렇게 계산 결과로 새로운 열을 만들 때 mutate(타율=안타/타수)처럼 쓸 수 있습니다.


우리가 지금 계산하려고 하는 건 '중심성'입니다. 중심성은 직접 계산 식을 쓸 필요가 없습니다. 각각 계산에 필요한 함수가 이미 tidygraph에 들어있기 때문입니다.


• 매개 중심성: centrality_betweenness()

• 근접 중심성: centrality_closeness()

• 고유벡터 중심성: centrality_eigen()

• 페이지랭크: centrality_pagerank()

• 연결 중심성: centrality_degree()


만약 이 피처링 데이터(변수 이름이 feat라는 거 잊지 않으셨죠?)에서 매개 중심성을 구하려 한다면 코드를 이렇게 쓰면 됩니다. 결과는 위에서 우리가 살펴본 것처럼 나옵니다.

feat %>% 
  as_tbl_graph() %>% 
  mutate(bet= centrality_betweenness()) %>%
  as_tibble %>%
  arrange(desc(bet))
## # A tibble: 9 x 2
##   name            bet
##   <chr>         <dbl>
## 1 아이유            3
## 2 에픽하이          0
## 3 HIGH4             0
## 4 MINO              0
## 5 사이먼 도미닉     0
## 6 G-DRAGON          0
## 7 오혁              0
## 8 더콰이엇          0
## 9 수현              0


이 코드에는 낯선 함수 두 개가 숨어 있습니다. 먼저 as_tibble()은 어떤 자료를 tibble 형식으로 만들라는 뜻입니다.


한 번 더 말씀드리자면 tibble은 데이터 프레임 tidyverse 버전이라고 보시면 됩니다. 지금 데이터는 tbl_graph 형식이기 때문에 이를 tibble로 바꾸는 겁니다.


arrange()는 표에서 나온 것처럼 특정 열을 기준으로 데이터를 정렬하라는 뜻입니다.


여기서는 노드별 매개 중심성을 계산한 bet 열을 기준으로 내림차순(descending)으로 정렬하라고 입력했습니다. 실제 결과도 그렇게 나왔습니다.


만약 중심성 여러 개를 동시에 계산하고 싶을 때는 어떻게 해야 할까요?


그냥 쉼표(,) 뒤에 계속 계산하고 싶은 결과를 연결하면 그만입니다. 이렇게 말입니다.

feat %>% 
  as_tbl_graph() %>% 
  mutate(bet=centrality_betweenness(),
             clo=centrality_closeness()) %>%
  as_tibble


아, SNA에서는 때로 중심성보다 더 궁금한 게 있습니다. 바로 '거리'입니다.


세상 사람은 여섯 단계만 거치면 모두 안다는 말 들어보셨죠? 여기서 여섯 단계가 바로 거리입니다.


tidygraph는 이런 거리를 계산할 수 있도록 graph_mean_distance()라는 함수도 가지고 있습니다.

feat %>% as_tbl_graph() %>%
  with_graph(graph_mean_dist())
## [1] 1.2


이 피처링 네트워크에서는 0.2 단계만 거치면 모두가 아는 사이입니다. 원래 자기하고 남이 기본적으로 1단계이니까요.


자료만 충분하다면 물론 아래처럼 큰 네트워크도 그릴 수 있습니다. 이 그림은 2016년 7월부터 지난해 7월까지 피처링 결과를 정리한 그림입니다.



※이렇게 노드가 많은 사회연결망을 그려보고 싶으신 분은 '피처링 관계도를 그려봅시다(feat. tidygraph, ggraph)' 포스트를 참고해 보셔도 좋습니다. 2018년 12월부터 2019년 11월까지 데이터를 기준으로 같은 그래프를 그리는 방법을 알아보았습니다. 



친한 사람끼리는 더 친하다

피처링 네트워크는 SNA 개념 익히기 차원에서 아주 간단하게 데이터를 한 번 만들어 본 겁니다. 


이번에는 좀 큰 데이터로 옮겨보겠습니다.


바로 수도권 전철 노선입니다. 전철도 역을 노드로 노선을 엣지로 생각해서 SNA를 진행할 수 있습니다.


아래 있는 CSV 파일에 수도권 전철 노선 자료가 들어 있습니다.


subway.csv


이 데이터를 읽으려면 어떤 함수를 써야 할까요? 그렇습니다. read.csv()입니다.


이 자료를 subway라는 변수에 넣고 첫 여섯 번째 줄을 불러오겠습니다.

subway <- read.csv('subway.csv')
head(subway)
##         from         to line
## 1     소요산     동두천    1
## 2     동두천       보산    1
## 3       보산 동두천중앙    1
## 4 동두천중앙       지행    1
## 5       지행       덕정    1
## 6       덕정       덕계    1


내친 김에 ggraph()를 활용해 그래프도 그려보겠습니다. 엣지는 호선별(line)로 색깔을 다르게 그리고, 노드는 색깔이 25% 회색이고 크기는 1인 점으로 표시하라는 코드라는 것 아시겠죠?

subway %>% as_tbl_graph() %>%
  ggraph(layout='kk') + 
  geom_edge_link(aes(color=line)) + 
  geom_node_point(color='gray25', size=1)


실제 역 위치를 반영해 그린 노선도가 익숙하기 때문에 이 그래프는 조금 이상하게 보이는 것도 사실. 그래도 이 그래프 역시 수도권 전철 노선을 나타내고 있습니다.


이 노선도 사회연결망이니까 중심성을 계산할 수 있을 터. 실제로 고유벡터 중심성을 계산하면 이렇습니다.

subway %>% as_tbl_graph() %>%
  mutate(eig=centrality_eigen()) %>%
  as_tibble %>% arrange(desc(eig))
## # A tibble: 605 x 2
##    name       eig
##    <chr>    <dbl>
##  1 회기     1.000
##  2 청량리   0.883
##  3 중랑     0.579
##  4 왕십리   0.359
##  5 외대앞   0.263
##  6 제기동   0.237
##  7 상봉     0.192
##  8 삼봉     0.167
##  9 상왕십리 0.104
## 10 망우     0.101
## # ... with 595 more rows


이 결과에 동의하십니까? 회기역 청량리역 중랑역이 정말 수도권 전철에서 세 손가락 안에 들 정도로 중요한 역일까요? 저는 동의하기가 힘듭니다.


이런 결과가 나온 건 중심성을 계산할 때 연결고리 그러니까 엣지를 모두 똑같이 취급했기 때문입니다. 실제로 어떤 역이 (덜) 중요하다고 평가할 때는 승하차 인원이 중요한데 현재 이 계산 결과는 이를 전혀 반영하고 있지 못합니다.


그래서 서울시 홈페이지에서 확인할 수 있는 가장 최신(2017년 7월) 역간 승객 이동 정보를 내려받아 이번 포스트에 맞도록 조금 손질했습니다. 아래 있는 게 바로 그 파일입니다.


metro.csv


이번에도 파일을 불러들이고 처음 여섯 줄을 확인하는 작업부터 진행하겠습니다.

metro <- read.csv('metro.csv')
head(metro)
##   from             to total
## 1 가능       가락시장   103
## 2 가능 가산디지털단지   593
## 3 가능           가양   102
## 4 가능         가재울     1
## 5 가능           가정     2
## 6 가능   가정중앙시장     5


조금 재미있습니다. 가능역은 경기 의정부시 가능동에 있고 가락시장역은 서울 송파구 가락동에 있습니다. 따라서 두 역을 곧바로 연결할 수는 없습니다.


그런데 이 자료가 이렇게 둘을 연결하고 있는 건 가능역에서 타서 가락시장에서 내린 승객 숫자를 집계하고 있기 때문입니다. 어떤 데이터 특성이 있는지 아시겠죠?


이번에도 중심성을 구해보겠습니다. 위에서 고유벡터 중심성을 썼으니 이번에도 같은 중심성을 알아보겠습니다. 일단 먼저 원래 하던 대로.

metro %>% as_tbl_graph() %>%
  mutate(eig=centrality_eigen()) %>%
  as_tibble %>% 
  arrange(desc(eig))
## # A tibble: 578 x 2
##    name               eig
##    <chr>            <dbl>
##  1 가산디지털단지 0.00184
##  2 사당           0.00184
##  3 서울역         0.00184
##  4 신도림         0.00184
##  5 신림           0.00184
##  6 영등포         0.00184
##  7 개봉           0.00184
##  8 고속터미널     0.00184
##  9 대림           0.00184
## 10 부천           0.00184
## # ... with 568 more rows


결과가 이상합니다. 상위 10개 역 결과가 (겉보기에) 모두 똑같습니다.


이렇게 나온 제일 큰 이유는 그냥 출발역과 도착역을 연결한 것만 차이일 뿐 역과 역을 일대일로 연결했다는 점에서는 차이가 나지 않기 때문입니다.


이럴 때 네트워트 특성을 정확하게 이해하려면 가중치(weights)라는 개념이 필요합니다.


이 네트워크에서 강남역은 도착하는 사람(387만2879명)도 제일 많고, 출발하는 사람(371만9395명)도 제일 많은 역입니다.


그런데 지금은 거꾸로 도착하는 사람(2831명)도 제일 적고, 출발하는 사람(3817명)도 제일 적은 달월역과 강남역 사이에 차이가 없습니다.


그렇다면 이렇게 이동하는 인원을 가중치로 주면 될 겁니다.


tidygraph에서 중심성 계산에 가중치를 줄 때는 'centrality_종류(weights=변수)' 형태로 쓰면 됩니다.


따라서 고유벡터 중심성 계산에 이동 인원(total)을 가중치로 주고 싶으면 코드는 이렇게 됩니다.

metro %>% as_tbl_graph() %>%
  mutate(eig=centrality_pagerank(weights=total)) %>%
  as_tibble %>% 
  arrange(desc(eig))
## # A tibble: 578 x 2
##    name           eig
##    <chr>        <dbl>
##  1 강남       0.0136 
##  2 고속터미널 0.0106 
##  3 잠실       0.0105 
##  4 홍대입구   0.0104 
##  5 서울역     0.00923
##  6 사당       0.00753
##  7 신림       0.00722
##  8 건대입구   0.00711
##  9 신도림     0.00701
## 10 선릉       0.00694
## # ... with 568 more rows


어떻습니까? 이 쪽이 우리가 상식적으로 알고 있는 전철역 특징을 더 잘 설명한다고 생각하지 않으십니까? 중심성을 계산할 때 때로 가중치를 줄 필요가 있다는 점 잊지마세요.



자료가 이상해요…

지금까지 우리가 다룬 CSV 파일은 SNA를 진행하기 쉽도록 모두 제가 from, to 형태로 정리했습니다.


실제로 SNA를 진행할 때 우리가 접하게 되는 자료는 이렇지 않은 일이 더 많습니다. 데이터를 정제하고 준비하는 데 전체 데이터 분석 시간 중 80%를 쓴다는 이야기가 있을 정도입니다.


만약 우리가 어떤 사람 다섯 명(가~마)이 어떤 고교와 대학을 나왔는지 정리할 일이 있다면 아마 다음처럼 데이터를 마련할 가능성이 큽니다. 아, data.frame()은 다음에 나오는 자료를 데이터 프레임 형태로 정리하라는 함수입니다.

school <- data.frame(사람=c('가', '나', '다', '라', '마', '바'),
                     고교=c('1', '2', '3', '1', '2', '3'),
                     대학=c('a', 'b', 'a', 'b', 'a', 'b'))
school
##   사람 고교 대학
## 1   가    1    a
## 2   나    2    b
## 3   다    3    a
## 4   라    1    b
## 5   마    2    a
## 6   바    3    b


이럴 때는 SNA를 실시하려면 노드를 어떻게 구분해야 할까요? 모를 때는 일단 원래 하던 대로 해보는 것도 나쁘지 않습니다.

school %>% 
  as_tbl_graph() %>% 
  ggraph(layout='kk') + 
  geom_edge_link(aes(start_cap = label_rect(node1.name), end_cap = label_rect(node2.name))) +
  geom_node_text(aes(label=name))


왜 이런 결과가 나왔는지 아시겠나요? 그렇습니다. tidygraph가 첫 두 열에서만 엣지를 찾았기 때문입니다. 그래서 사람 여섯 명이 출신 고교에 따라 짝을 지었습니다.


이 그래프를 완성하려면 적어도 대학까지는 짝을 찾아줘야 합니다. 어떻게 하면 될까요?


다른 방법도 물론 있겠지만 이 글은 초보자용이니까 초보자 관점에서 접근해 보겠습니다.


데이터 프레임에서 특정한 위치에 있는 자료만 골라내고 싶을 때는 '변수[행, 열]'처럼 쓰면 됩니다. 


지금 우리가 다루고 있는 변수 이름은 'school'입니다. 여기서 첫 번째, 두 번째 열만 뽑아 내면 사람과 고교만 뽑아낼 수 있을 겁니다.


이렇게 두 열을 묶을 때는 어떤 함수를 써야 할까요? 네, c()입니다.


정말 그렇게 나오는지 확인해보겠습니다.

school[, c(1, 2)]
##   사람 고교
## 1   가    1
## 2   나    2
## 3   다    3
## 4   라    1
## 5   마    2
## 6   바    3

물론 이 결과를 'school_고교'라는 변수에 따로 담아둘 수도 있습니다. 마찬가지로 첫 번째, 세 번째 열만 뽑아서 이 역시 'school_대학'에 담아 놓겠습니다.


이제 school_고교와 school_대학을 합치면 첫 번째 열에는 사람이, 두 번째 열에는 학교가 나오는 데이터를 만들 수 있습니다.


이렇게 자료를 합치려면 행(row) 단위로 묶어야 합니다. 이럴 때 쓰는 함수가 바로 rbind()입니다. 그러니까 rbind(school_고교, school_대학)이라고 쓰면 두 변수를 다시 합칠 수 있습니…


아닙니다. rbind()로 두 변수를 묶을 때는 열 이름이 모두 똑같아야 합니다. 현재 두 데이터는 첫 번째 열은 '사람'으로 똑같지만 두 번째 열에는 각각 '고교'와 '대학'이 들어 있습니다. 이 역시 똑같이 바꿔줘야 합니다. 


R에서 어떤 열 이름을 확인하거나 수정할 때 쓰는 함수는 names()입니다.  

names(school_고교)
## [1] "사람" "고교"


두 개가 동시에 나왔습니다. 우리는 두 번째에 있는 고교만 다른 표현('학교')으로 바꾸고 싶은 상태입니다.


이럴 때는 어떻게 하면 될까요? 두 번째에 있는 걸 선택해야 하니까 'names(school_고교)[2]'라고 쓰면 됩니다.


여기에 새로운 값을 입력하려면? '<-'를 쓰시면 그만입니다. (물론 '='도 됩니다.)

names(school_고교)[2] <- '학교'


실제로 우리가 원하는 대로 바뀌었는지 확인해 보려면 다시 names()를 써서 이름을 확인하면 그만입니다.

names(school_고교)
## [1] "사람" "학교"


잘 나왔습니다. school_대학도 마찬가지로 두 번째 열 이름을 학교로 바꾸겠습니다. 그리고 나서 rbind()로 두 변수를 묶어서 다시 school에 넣겠습니다.

names(school_대학)[2] <- '학교'
school <- rbind(school_고교, school_대학)


지금까지 우리는 데이터 앞 부분을 보여주는 head()만 사용했는데 사실 뒤에서부터 보여주는 tail()도 있습니다. 뒤에서 열 줄을 한 번 볼까요?

tail(school, 10)
##    사람 학교
## 3    다    3
## 4    라    1
## 5    마    2
## 6    바    3
## 7    가    a
## 8    나    b
## 9    다    a
## 10   라    b
## 11   마    a
## 12   바    b


숫자로 구분한 고교와 알파벳으로 구분한 대학이 잘 섞여 있습니다. 이제 다시 그래프를 그려보겠습니다. 

school %>% 
  as_tbl_graph() %>% 
  ggraph(layout='kk') + 
  geom_edge_link(aes(start_cap = label_rect(node1.name), end_cap = label_rect(node2.name))) +
  geom_node_text(aes(label=name))


축하드립니다. 여러분은 방금 tidygraph를 통해 이분(Bipartite) 그래프를 그리는 데 성공하셨습니다.


그리고 실제로 데이터를 정제하는 게 생각보다 퍽 귀찮고 까다로운 과정이라는 것도 느끼셨을 겁니다. 애석하게도 이제부터 더욱 귀찮고 까다로운 일이 기다리고 있습니다.



사람끼리만 그래프를 그리고 싶어요

물론 위에서 그린 그래프도 의미가 없는 건 아니지만 학교는 빼고 사람끼리만 데이터를 그리고 싶을 때가 있습니다. 그럴 땐 어떻게 해야 할까요?


역시 다른 방법도 있겠지만 SNA 세계에서 제일 유명한 R 패키지인 igraph 도움을 받는 게 그나마 수월합니다. (분명 '그나마'라고 붙였습니다.)

install.packages('igraph')
library('igraph')


tidygraph에 있는 as_tbl_graph() 함수도 데이터를 igraph 형식으로 바꿔주기는 하지만 기왕 igraph를 불러들였으니 이 패키지에 있는 함수를 써서 그래프 형식으로 바꿔 보겠습니다.


우리가 쓰고 있는 school은 데이터 프레임 형식. 그래서 graph_from_data_frame()을 씁니다.

sg <- graph_from_data_frame(school)
sg
## IGRAPH f26a707 DN-- 11 12 -- 
## + attr: name (v/c)
## + edges from f26a707 (vertex names):
##  [1] 가->1 나->2 다->3 라->1 마->2 바->3 가->a 나->b 다->a 라->b 마->a
## [12] 바->b


igraph에는 이분 그래프 안에서 노드가 어떤 특징을 가지고 있는지 알려주는 bipartite_mapping() 함수가 들어 있습니다. 이 그래프에 이 함수를 적용하면 이런 결과를 나타냅니다.

bipartite_mapping(sg)
## $res
## [1] TRUE
## 
## $type
##  [1] FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE


여기서 FALSE는 뭐고 TRUE는 뭘까요?


여기서 일단 알고 계셔야 하는 건 igraph에서는 지금까지 우리가 노드라고 부르던 걸 꼭지점(vertex)라고 표현한다는 것. 그래서 노드를 확인하고 싶을 때 V()라는 함수를 씁니다.

V(sg)
## + 11/11 vertices, named, from b20d023:
##  [1] 가 나 다 라 마 바 1  2  3  a  b


이 결과하고 위에서 나온 type을 비교하면 igraph가 신기하게도 노드 특성을 구분했다는 사실을 알 수 있습니다. 사람은 FALSE, 학교는 TRUE라고 구분한 겁니다.


이 결과 자체를 노드에 붙여 보겠습니다. R에서는 어떤 변수 안에 들어 있는 열을 (위에서 보신 것처럼) '$' 기호로 구분합니다.


그래서 꼭지점 데이터에 type이라는 열을 만들되 이 열은 노드 특성에 따라서 구분하라는 명령은 이렇게 입력할 수 있습니다.

V(sg)$type <- bipartite_mapping(sg)$type


igraph에는 또 as_incidence_matrix()라는 함수도 들어 있습니다. 지금까지 배운 걸 토대로 추론하면 이 함수는 데이터를 'incidence matrix' 형태로 바꾸라는 뜻이 될 겁니다. 'incidence matrix'는 '근접 행렬'이라는 뜻입니다.


이 행렬이 어떻게 생겼는지 궁금하니까 한번 함수를 직접 실행해 보겠습니다.

as_incidence_matrix(sg)
##    1 2 3 a b
## 가 1 0 0 1 0
## 나 0 1 0 0 1
## 다 0 0 1 1 0
## 라 1 0 0 0 1
## 마 0 1 0 1 0
## 바 0 0 1 0 1


어라? 사람과 학교를 행과 열에 따로 구분했습니다. 이때 사람을 열, 학교를 행에 배치하고 싶을 때는 어떻게 해야 할까요?


이를 전치행렬이라고 부르는데 R에서는 t() 함수를 통해 구할 수 있습니다.

t(as_incidence_matrix(sg))
##   가 나 다 라 마 바
## 1  1  0  0  1  0  0
## 2  0  1  0  0  1  0
## 3  0  0  1  0  0  1
## a  1  0  1  0  1  0
## b  0  1  0  1  0  1


고교 수학 시간에 행렬 공부를 열심히 하신 분이라면 행렬과 전치행렬을 곱하면 재미있는 결과가 나온다는 걸 아실 겁니다. R에서 행렬을 곱할 때 쓰는 기호는 '%*%'입니다.

as_incidence_matrix(sg) %*% t(as_incidence_matrix(sg))
##    가 나 다 라 마 바
## 가  2  0  1  1  1  0
## 나  0  2  0  1  1  1
## 다  1  0  2  0  1  1
## 라  1  1  0  2  0  1
## 마  1  1  1  0  2  0
## 바  0  1  1  1  0  2


사람과 사이가 어떤 연결 상태인지를 알려주는 결과가 나왔습니다.


한 가지 아쉬운 점이 있다면 자신과 자신을 두 번 연결했다는 점입니다. (자신은 자신과 같은 고교와 대학을 나왔으니까요.) 이걸 0으로 바꿔줘야겠죠?


R에는 이렇게 행렬 대각선에 0을 넣어주는 diag() 함수도 들어 있습니다.

sm <- as_incidence_matrix(sg) %*% t(as_incidence_matrix(sg))
diag(sm) <- 0
sm
##    가 나 다 라 마 바
## 가  0  0  1  1  1  0
## 나  0  0  0  1  1  1
## 다  1  0  0  0  1  1
## 라  1  1  0  0  0  1
## 마  1  1  1  0  0  0
## 바  0  1  1  1  0  0


이제 뭔가 다시 작업을 할 수 있을 것 같은 느낌적인 느낌입니다.


다시 tidygraph를 활용해보겠습니다. as_tbl_graph()는 행렬도 그래프로 바꿀 수 있으니 그냥 쓰던 대로 쓰시면 됩니다.

sm %>% as_tbl_graph() %>%
  ggraph(layout='kk') + 
  geom_edge_link(aes(start_cap = label_rect(node1.name), end_cap = label_rect(node2.name))) +
  geom_node_text(aes(label=name))


이렇게 우리는 또 한 고비를 넘어섰습니다. 이제 우리는 사람과 단체가 함께 있는 데이터가 있을 때 사람만 뽑아내는 방법을 익혔습니다. 위에서 진행한 과정을 잘 생각해 보시면 단체만 뽑아낼 수도 있다는 사실도 잘 알고 계시리라 믿습니다.



프로배구 남자부 데이터를 돌려보자

실제 이분 데이터로 SNA를 해보도록 하겠습니다. 아래 파일에는 프로배구 남자부 7개 구단 2018~2019 시즌 등록 선수 101명 출신 학교 정보가 들어 있습니다.


kovo.csv


이미 여러분은 키보드에 read.csv()와 head()를 치고 계실 줄 믿습니다. 파일을 열고 확인하는 게 기본이니까요.

k <- read.csv('kovo.csv')
head(k)
##     선수         고교   대학
## 1 곽승석       동성고 경기대
## 2 김학민       수성고 경희대
## 3 심홍석 경북사대부고 홍익대
## 4 정지석       송림고       
## 5 임동혁   제천산업고       
## 6 김규민     벌교상고 경기대


어디서 많이 본 형태입니다. 그냥 위에서 데이터를 처리한 방식을 그대로 응용하면 될 겁니다.

k_고교 <- k[, c(1, 2)]
k_대학 <- k[, c(1, 3)]
names(k_고교)[2] <- '학교'
names(k_대학)[2] <- '학교'
k <- rbind(k_고교, k_대학)


잘 아시는 것처럼 아직은 노드를 선수와 학교로 구분하기 전입니다. 이 상태 그대로 중심성을 계산하면 어떤 결과가 나올까요? 고유벡터 중심성을 한번 계산해 보겠습니다.

k %>% as_tbl_graph() %>%
  mutate(eig=centrality_eigen()) %>%
  arrange(desc(eig)) %>%
  as_tibble
## # A tibble: 141 x 2
##    name       eig
##    <chr>    <dbl>
##  1 경기대   1    
##  2 송림고   0.716
##  3 한양대   0.401
##  4 성균관대 0.393
##  5 송명근   0.360
##  6 안우재   0.360
##  7 이민규   0.360
##  8 이민욱   0.360
##  9 하현용   0.360
## 10 인창고   0.278
## # ... with 131 more rows


여기서 퀴즈 하나: 송명근(25·OK저축은행) 안우재(24·한국전력) 이민규(26·OK저축은행) 이민욱(23·삼성화재) 하현용(36·KB손해보험)은 어떤 학교를 나왔을까요?


이들 모두 송림고-경기대 동문입니다. 고유벡터 중심성 계산에서 한 노드가 높게 나오면 다른 노드까지 덩달아 중심성이 올라간다는 건 이런 뜻입니다.


이런 문제점을 최소화하려고 나온 중심성 지표가 바로 페이지랭크였습니다. 페이지랭크를 계산하면 이런 결과를 얻을 수 있습니다.

k %>% as_tbl_graph() %>%
  mutate(pr=centrality_pagerank()) %>%
  arrange(desc(pr)) %>%
  as_tibble
## # A tibble: 141 x 2
##    name             pr
##             
##  1 경기대       0.0363
##  2 성균관대     0.0363
##  3 인하대       0.0344
##  4 한양대       0.0325
##  5 송림고       0.0288
##  6 경희대       0.0269
##  7 경북사대부고 0.0175
##  8 광주전자공고 0.0175
##  9 인창고       0.0175
## 10 홍익대       0.0175
## # ... with 131 more rows


사람과 학교가 뒤섞여 있을 때는 학교가 더 중요하지 않을까요? 만약 그렇게 생각하신다면 이 그래프에서는 페이지랭크가 더 의미있는 중심성 지표라고 할 수 있습니다.


이제 노드에서 선수만 분리하는 작업을 진행하겠습니다. 이번에도 위에서 쓴 코드를 그대로 응용하면 그만입니다.

kg <- graph_from_data_frame(k)
V(kg)$type <- bipartite_mapping(kg)$type
km <- as_incidence_matrix(kg)
km <- km %*% t(km)
diag(km) <- 0
km %>% as_tbl_graph()
## # A tbl_graph: 101 nodes and 1554 edges
## #
## # A directed simple graph with 1 component
## #
## # Node Data: 101 x 1 (active)
##   name  
##   <chr> 
## 1 곽승석
## 2 김학민
## 3 심홍석
## 4 정지석
## 5 임동혁
## 6 김규민
## # ... with 95 more rows
## #
## # Edge Data: 1,554 x 3
##    from    to weight
##   <int> <int>  <dbl>
## 1     1     6      1
## 2     1    13      1
## 3     1    20      2
## # ... with 1,551 more rows


선수만 잘 나왔습니다. 재미있는 건 엣지에 가중치(weight)가 생겼다는 점입니다. 고교 또는 대학만 동문은 사이는 1, 두 학교 모두 동문일 때는 2가 가중치에 들어갔을 겁니다. 


학교 없이 선수만 대상으로 페이지랭크를 구하면 어떤 결과가 나올까요?

km %>% as_tbl_graph() %>%
  mutate(pg=centrality_pagerank()) %>%
  arrange(desc(pg)) %>%
  as_tibble
## # A tibble: 101 x 2
##    name       pg
##    <chr>   <dbl>
##  1 강민웅 0.0149
##  2 신으뜸 0.0149
##  3 하현용 0.0144
##  4 송명근 0.0144
##  5 안우재 0.0144
##  6 이민욱 0.0144
##  7 이민규 0.0144
##  8 신동광 0.0138
##  9 우상조 0.0138
## 10 손주형 0.0134
## # ... with 91 more rows


전에는 나오지 않았던 강민웅(33·한국전력)과 신으뜸(31·우리카드)가 제일 중요한 인물이 됐습니다. 두 선수 모두 송림고 - 성균관대 출신이라는 공통점이 있습니다.


그렇다면 이렇게 비슷한 특징이 있는 노드끼리 그룹으로 묶을 수 있겠죠?


tidygraph에는 이럴 때 쓸 수 있도록 group_infomap() 함수가 들어 있습니다. 한번 그루핑을 해보겠습니다.

km %>% as_tbl_graph() %>%
  mutate(cm=group_infomap()) %>%
  arrange(desc(cm)) %>%
  as_tibble
## # A tibble: 101 x 2
##    name      cm
##    <chr>  <int>
##  1 함형진     8
##  2 박준혁     8
##  3 한정훈     8
##  4 안준찬     8
##  5 하승우     8
##  6 조국기     8
##  7 정지석     7
##  8 임동혁     7
##  9 김지한     7
## 10 허수봉     7
## # ... with 91 more rows


내림차순 정렬을 선택한 건 그래야 전체 커뮤니티 숫자를 확인하기가 쉽기 때문입니다. 총 8개로 그룹을 나눴다는 사실을 알 수 있습니다.


이 그룹을 어떻게 나눴는지 그래프로 확인해 볼까요?

km %>% as_tbl_graph() %>%
  mutate(pg=centrality_pagerank(),
         cm=group_infomap()) %>%
  ggraph(layout='lgl') + 
  geom_edge_link(aes(width=weight), alpha=.8) +
  scale_edge_width(range=c(0.2, 2)) +
  geom_node_point(aes(size=pg, color=as.factor(cm)))


결과를 보기 전에 코드를 조금 보면 이번에는 지금까지 쓰던 kk 대신 'lgl'이라는 레이아웃을 써봤습니다.


엣지 두께는 가중치에 0.2~2 사이에서 바뀌도록 설정했습니다. 엣지 투명도는 80%.


노드 크기는 페이지랭크에 따라 달리 그립니다. 노드 색깔은 커뮤니티로 구분하도록 했습니다.


여기서 cm에 as.factor()라는 함수를 적용한 건 이 숫자가 그냥 그룹을 구분하는 요소(factor)일 뿐 숫자 자체로 의미가 있는 건 아니기 때문입니다.


만약 이 숫자가 요소일 뿐이라고 알려주지 않으면 R는 숫자에 따라 그러데이션을 적용합니다.



결과가 예쁘게 잘 나왔습니다. 남자 배구 선수 101명은 이런 사이로 엮여 있습니다.


그래프 평균 거리를 계산하면 약 2.5가 나옵니다. 학교만 따졌을 때도 남자 배구 선수는 1.5 단계만 거치면 서로 전부 아는 사이라고 할 수 있는 겁니다.


저는 예전에 이 기법을 활용해 프로야구 선수 및 코칭 스태프를 분석한 적이 있습니다.


그때는 고려대가 제일 중요한 학교, 김경기 당시 SK 코치(현 SPOTV 해설위원·50)이 제일 중요한 인물이었습니다.


이번에 프로배구 남자부를 선택한 건 일단 규모가 작기 때문이고 또 이세호 강남대 교수(KBS 해설위원)께서 '프로배구 선수의 사회연결망 구조와 자원교환'이라는 논문을 쓰셨던 게 기억났기 때문입니다.


지금까지 긴 글 읽어주시느라 대단히 고생 많으셨습니다. 저로서도 이 시리즈를 쓰면서 처음으로 포스트를 나누고 싶은 유혹을 느낄 만큼 길었습니다.


혹시 이 포스트에 잘못된 내용이 있어나 이해하기 어려운 부분이 있으면 언제든 알려주세요. 함께 공부하면서 바로잡도록 하겠습니다.


  1. 그냥 한번만 특정 위치에서 파일을 불러오고 싶으시다면 file.choose() 함수를 쓰는 방법도 있습니다. 이번에는 read.csv()를 쓰고 있으니까 read.csv(file.choose())처럼 쓰시면 됩니다. [본문으로]

댓글,

KindeR | 카테고리 다른 글 더 보기