자료 읽기 — 세 가지 입력 형식

R 실습 — 손으로 돌려 보기STAGED+ 스터디

2.1 세 가지 입력 형식 (Three Input Formats)

현장에서 만나는 관계 자료는 사실상 세 모양 중 하나다.

형식생김새언제 쓰나
인접행렬
adjacency
26 × 26, A[i,j]=1A[i,j]=1이면 iijj를 지명사람 수가 적을 때. sna 함수에 그대로 넣을 수 있어 제일 편하다
엣지리스트
edge list
from, to, liking 3열 × 88행사람이 많을 때(행렬은 대부분 0이라 낭비). 가중치를 붙이기 쉽다
노드표
node attributes
name, female, isei08_m, isei08_f 4열 × 26행관계가 아니라 사람의 속성. 위 둘 중 하나와 반드시 짝을 이룬다

세 파일을 메모장으로 열면 이렇게 생겼다. 앞 몇 줄만, 그리고 앞 여섯 명까지만 옮겼다.

classroom-adjacency.csv 인접행렬
"","1003","1006","1009","1012","1015","1018", …
"1003",0,0,0,0,0,1, …
"1006",0,0,1,0,0,0, …
"1009",0,1,0,0,0,0, …
classroom-edges.csv 엣지리스트
"from","to","liking"
"1003","1018",3
"1003","1051",3
"1003","1072",4
"1006","1009",5
classroom-nodes.csv 노드표
"name","female","isei08_m","isei08_f"
"1003",FALSE,NA,25.71
"1006",TRUE,14.64,33.76
"1009",TRUE,28.48,37.22
초록으로 칠한 두 곳이 같은 사실을 적은 것이다 — 행렬에서 1003 행·1018 열의 1, 엣지리스트에서 "1003","1018",3 줄. "1003이 1018을 지명했다"는 한 가지를 형식만 바꿔 적었다. 따옴표는 그 칸이 숫자가 아니라 문자열이라는 표시고, read.csv()가 읽을 때 알아서 벗겨 낸다.

같은 관계를 세 형식이 어떻게 적는지 나란히 놓으면 이렇다. 여섯 명(1003 · 1006 · 1009 · 1012 · 1015 · 1018)만 잘라 온 것이다.

① 인접행렬 6 × 6 발췌
100310061009101210151018
1003000001
1006001000
1009010000
1012000010
1015000100
1018000000
② 엣지리스트
fromtoliking
100310183
100610095
100910062
101210155
101510122
⋮ 나머지 83줄
③ 노드표
namefemale
1003FALSE
1006TRUE
1009TRUE
1012TRUE
1015TRUE
1018FALSE

행렬의 색칠한 칸 다섯 개 = 엣지리스트의 색칠한 줄 다섯 개 = 아래 그림의 화살표 다섯 개다. 셋은 같은 관계를 적는 세 가지 방법일 뿐이다.

여섯 명만 떼어 그린 미니 네트워크
여섯 명만 떼어 그린 그림. 1006 ⇄ 1009, 1012 ⇄ 1015는 서로 지명해 화살표가 둘, 1003 → 1018은 한쪽만 지명해 화살표가 하나다.
왜 형식이 셋이나 되나 — 26명이면 행렬은 262=67626^2 = 676칸인데 그중 11은 88칸뿐이다(밀도 0.135). 나머지 588칸은 전부 0이다. 학급이 커지면 행렬은 n2n^2으로 불어나지만 엣지리스트는 관계 수만큼만 길어진다. 그래서 설문은 엣지리스트로 걷고, 계산은 행렬로 한다. 노드표는 둘 중 어느 쪽과도 짝을 이룬다.
D <- "발표/6장/자료/statnet_실습"

# ① 인접행렬 — 첫 열이 이름이므로 row.names = 1
adj <- as.matrix(read.csv(file.path(D, "classroom-adjacency.csv"),
                          row.names = 1, check.names = FALSE))

# ② 엣지리스트 — ID를 숫자로 읽으면 나중에 깨진다. 반드시 문자로
el <- read.csv(file.path(D, "classroom-edges.csv"), colClasses = "character")
el$liking <- as.numeric(el$liking)

# ③ 노드표
nod <- read.csv(file.path(D, "classroom-nodes.csv"),
                colClasses = c("character", "logical", "numeric", "numeric"))

dim(adj);  nrow(el);  nrow(nod)
[1] 26 26
[1] 88
[1] 26
가장 흔한 초보 실수 두 개
  1. ID를 숫자로 읽는 것. 학생 번호 1003이 정수가 되면 igraph가 이를 "1003번째 정점"으로 오해해 정점 1003개짜리 그래프를 만든다. colClasses = "character"를 반드시 쓴다.
  2. 행렬 이름과 노드표 이름의 순서가 다른 것. 확인 한 줄이면 된다.
    identical(rownames(adj), nod$name)
    [1] TRUE
    FALSE가 나오면 nod <- nod[match(rownames(adj), nod$name), ]로 맞춘다. 이걸 안 맞추면 성별이 엉뚱한 학생에게 붙는다 — 그림은 멀쩡히 그려지므로 눈치채기 어렵다.

2.2 객체 만들기 — network와 igraph (Creating Objects)

어느 형식이 어느 함수로 이어지는지부터 보면 아래 코드가 한눈에 들어온다.

인접행렬adjacency.csv
network(adj, directed = TRUE) graph_from_adjacency_matrix(adj, mode = "directed") 읽을 때 row.names = 1로 첫 열을 이름으로 넘겨 둔다
엣지리스트edges.csv
graph_from_data_frame(el, vertices = nod) network(as.matrix(el[, 1:2]), matrix.type = "edgelist") 노드표를 같이 주지 않으면 아무와도 이어지지 않은 1024가 빠져 25명이 된다
노드표nodes.csv
set.vertex.attribute(net, "female", nod$female) V(g)$female <- nod$female 관계가 아니라 속성 — 반드시 위 둘 중 하나에 얹는다
# statnet 계보
net <- network::network(adj, directed = TRUE)
network::set.vertex.attribute(net, "female", nod$female)

# igraph 계보 — 행렬에서
g <- igraph::graph_from_adjacency_matrix(adj, mode = "directed")
igraph::V(g)$female <- nod$female

# igraph 계보 — 엣지리스트 + 노드표에서 (가중치까지 한 번에)
gw <- igraph::graph_from_data_frame(el, directed = TRUE, vertices = nod)

network::network.size(net); network::network.edgecount(net)
[1] 26
[1] 88
igraph::vcount(g); igraph::ecount(g)
[1] 26
[1] 88
graph_from_data_frame(vertices = nod)가 왜 좋은가 — 엣지리스트만 주면 아무에게도 지명받지 않고 아무도 지명하지 않은 학생이 사라진다. 이 학급의 1024번이 정확히 그런 학생이다. vertices=로 노드표를 같이 주면 26명이 모두 살아남고, 속성도 자동으로 붙는다.

2.3 intergraph으로 오가기 (Converting Between Packages)

g2 <- intergraph::asIgraph(net)     # network → igraph
n2 <- intergraph::asNetwork(g)      # igraph → network

igraph::vcount(g2); igraph::ecount(g2)
[1] 26
[1] 88

노드 26·간선 88이 그대로다. 속성도 따라온다. "igraph에만 있는 커뮤니티 탐지를 쓴 뒤, 결과를 sna 그림에 얹는" 식의 왕복이 실제로 자주 필요하다.

확인해 보면 놀라는 것 — 행렬 → igraph → 행렬로 되돌린 뒤 비교하면 FALSE가 나온다.
B <- igraph::as_adjacency_matrix(g, sparse = FALSE)
identical(adj, B)
[1] FALSE
all(adj == B)                # 값은?
[1] TRUE
storage.mode(adj); storage.mode(B)
[1] "integer"
[1] "double"
값은 완전히 같고 저장 타입만 다르다. identical()은 타입까지 보므로 FALSE가 나온 것이다. 자료가 망가진 게 아니다. 이런 비교에는 all(x == y)all.equal()을 쓴다.