연습문제와 해설

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

11. 연습문제 (Exercises)

문제 1. 밀도 — 방향을 버리면 왜 커지는가

방향 네트워크의 밀도는 0.1354였다. as_undirected(mode="collapse")로 무방향화하면 간선이 66개가 된다. 고립자 1024를 제외한 25명의 무방향 밀도를 손으로 계산하고, 방향 밀도 0.1354보다 큰 이유를 설명하라.

아래 해설에 답이 있다. 먼저 풀고 맞춰 볼 것.

문제 2. 상호성 세 가지를 dyad census에서 복원하기

dyad.census 결과 Mut = 22, Asym = 44, Null = 259만 가지고 grecip()의 세 가지 값(dyadic, dyadic.nonnull, edgewise)을 각 항을 전부 써서 손으로 계산하라. 그리고 학부모 상담에서 "이 반 아이들은 서로 잘 통하나요?"라는 질문에 답할 때 어느 값을 써야 하는지와 그 이유를 쓰라.

문제 3. 에러 고치기

아래 코드는 셋 다 에러가 나거나 이상한 값을 준다. 무엇이 문제인지 진단하고 고쳐 쓰라.

library(sna); library(igraph)

# (a)
betweenness(adj, gmode = "digraph")

# (b)
el2 <- read.csv("classroom-edges.csv")
g2  <- graph_from_data_frame(el2, directed = TRUE)

# (c)
closeness(adj, gmode = "digraph")
문제 4. E-I 지수를 다른 기준으로 다시 재기

7절에서 성별 기준 E-I는 −0.8409였다. 이번에는 6.3절의 walktrap 커뮤니티를 집단 기준으로 삼아 E-I를 계산하려 한다.

  1. 계산하기 전에, 그 값이 성별 기준(−0.8409)보다 −1에 더 가까울지 덜 가까울지 예상하고 이유를 쓰라. 근거로 삼을 만한 것: 6.3절에서 walktrap의 Q=0.4387Q = 0.4387이 성별 분할의 Q=0.4086Q = 0.4086보다 크다.
  2. 실제로 계산하는 R 코드를 쓰고 값을 구하라.
  3. 예상과 맞았는가? 틀렸다면 무엇을 빠뜨렸는지 설명하라.
문제 5. 교실 적용 — 세 학생을 어떻게 다르게 볼 것인가

1024, 1030, 1036은 indegree가 모두 0이다. 지표만 보면 똑같이 "지명 못 받은 학생"이다. 그런데 담임으로서 이 셋에게 해야 할 일은 서로 다르다. 세 학생을 구분하는 데 쓸 수 있는 네트워크 지표를 각각 제시하고, 그 지표 값이 왜 다른 행동을 요구하는지 쓰라.

12. 해설과 답 (Solutions)

문제 1 해설 — 밀도

① 무엇을 세는가. 무방향 네트워크에서 가능한 관계 수는 (n2)\binom{n}{2}다. 방향 네트워크의 n(n1)n(n-1)정확히 절반이다 — 방향이 없으면 iji \to jjij \to i가 같은 하나이기 때문이다.

대상nn가능한 관계실제 간선밀도
방향, 26명2626×25=65026\times25 = 6508888/650 = 0.1354
무방향, 26명26(262)=325\binom{26}{2} = 3256666/325 = 0.2031
무방향, 1024 제외25(252)=300\binom{25}{2} = 3006666/300 = 0.2200

② 간선이 왜 88 → 66인가. 상호 지명 22쌍이 각각 화살표 2개 → 간선 1개로 합쳐진다.

유형개수방향 간선무방향 간선
상호(Mut)22쌍22 × 2 = 4422 × 1 = 22
비대칭(Asym)44쌍44 × 1 = 4444 × 1 = 44
66쌍8866
답: 1024를 제외한 무방향 밀도 = 66/300=0.2266/300 = \mathbf{0.22}.

커진 이유는 세 가지가 겹친 것이다.

  1. 분모가 절반이 됐다 (650 → 325). 이게 가장 큰 효과다.
  2. 분자는 절반이 되지 않았다 (88 → 66, 75%로만 줄었다). 비대칭 간선 44개는 합쳐질 짝이 없어 그대로 살아남기 때문이다. 상호성이 낮을수록 무방향화로 밀도가 더 크게 오른다.
  3. 고립자를 뺐다 (325 → 300). 아무 간선도 없는 1024가 분모만 키우고 있었다. 이것만으로 0.2031 → 0.2200이 됐다.
교실 해석밀도를 보고할 때는 반드시 "방향인가 무방향인가, 고립자를 뺐는가"를 같이 써야 한다. 같은 자료가 0.135도 되고 0.220도 된다 — 1.6배 차이다. 다른 학급과 비교할 때 이 조건이 다르면 비교 자체가 무의미하다.
문제 2 해설 — 상호성

① 먼저 재료를 정리한다.

기호
MM상호 — 서로 지명한 쌍22
AA비대칭 — 한쪽만 지명한 쌍44
NN없음 — 둘 다 안 한 쌍259
M+A+NM+A+N모든 쌍325 = (262)\binom{26}{2}
mm전체 방향 간선 = 2M+A2M + A2(22) + 44 = 88 ✔

② 세 가지를 각각 전개한다.

measure=공식전개
"dyadic"M+NM+A+N\dfrac{M+N}{M+A+N}22+25922+44+259=281325\dfrac{22+259}{22+44+259} = \dfrac{281}{325}0.864615
"dyadic.nonnull"MM+A\dfrac{M}{M+A}2222+44=2266\dfrac{22}{22+44} = \dfrac{22}{66}0.333333
"edgewise"2M2M+A\dfrac{2M}{2M+A}4444+44=4488\dfrac{44}{44+44} = \dfrac{44}{88}0.500000

③ 왜 "dyadic"이 이렇게 큰가. 분자에 N=259N = 259가 들어 있다. 아무 관계도 없는 쌍을 "짝이 맞았다"고 세는 것이다. 259가 325의 80%이므로, 관계가 성길수록 이 값은 자동으로 1에 가까워진다. 극단적으로 간선이 하나도 없는 네트워크의 dyadic 상호성은 1.0이다.

답: dyadic = 0.8646, dyadic.nonnull = 0.3333, edgewise = 0.5000.

학부모 상담에서 쓸 값은 edgewise = 0.5다.

이유: 이 값만이 학생 경험으로 번역되는 문장을 준다 — "아이가 친하다고 적은 이름 두 개 중 하나는 그쪽에서도 아이를 적었습니다."
dyadic 0.86은 "86%가 잘 통한다"로 들리지만 실제로는 대부분 '서로 모른다'를 센 것이라 학부모를 오해하게 만든다.
dyadic.nonnull 0.33도 정확한 값이지만 "관계가 있는 쌍 중에서"라는 조건을 설명해야 해서 상담 자리에서 전달하기 어렵다.

문제 3 해설 — 에러 고치기

(a) betweenness(adj, gmode = "digraph")

진단: library(igraph)를 나중에 켰으므로 betweennessigraph의 것이 불린다. igraph에는 gmode 인자가 없다. 또 igraph 함수는 행렬(adj)이 아니라 igraph 객체를 받는다. 에러 두 개가 겹쳐 있다.

고친 코드:
sna::betweenness(adj, gmode = "digraph")     # 행렬을 그대로
# 또는
igraph::betweenness(g, directed = TRUE)      # igraph 객체를

두 값은 완전히 일치한다(최대 차이 0). 어느 쪽을 써도 된다 — 중요한 건 객체 종류와 인자 이름을 짝지어 주는 것이다.

(b) 엣지리스트 읽기

진단: 문제가 두 개다.

  1. colClasses가 없어 from·to정수로 읽힌다. graph_from_data_frame은 정수 ID를 정점 번호로 오해할 수 있다.
  2. vertices =가 없어 1024가 사라진다. 엣지리스트 88행 어디에도 1024가 없기 때문이다. 정점이 26이 아니라 25가 된다.
고친 코드:
el2 <- read.csv("classroom-edges.csv", colClasses = "character")
el2$liking <- as.numeric(el2$liking)
nod <- read.csv("classroom-nodes.csv",
                colClasses = c("character", "logical", "numeric", "numeric"))
g2 <- graph_from_data_frame(el2, directed = TRUE, vertices = nod)

vcount(g2)
[1] 26          # vertices= 없이 하면 25가 나온다

이 실수가 특히 위험한 이유: 에러가 안 난다. 25명짜리 그래프가 조용히 만들어지고, "고립자는 없습니다"라는 정반대 결론이 나온다. 분석에서 가장 중요한 학생 하나가 통째로 빠진 채로.

(c) closeness(adj, gmode = "digraph")

진단: 여기도 igraph가 가리므로 우선 gmode에서 에러가 난다. sna::를 붙여 고쳐도 결과가 전부 0이다 — 1024가 고립돼 있어 모든 학생에게 d=d=\infty인 상대가 있기 때문이다. 에러를 고쳤는데도 답이 틀린, 가장 까다로운 유형이다.

고친 코드:
sna::closeness(adj, gmode = "digraph", cmode = "suminvdir")

교훈: "에러 없이 돌아갔다"는 "맞았다"가 아니다. 결과가 전부 0이거나 전부 같은 값이면 반드시 의심한다.

문제 4 해설 — 커뮤니티 기준 E-I

① 자연스러운 예상 (그리고 이건 틀린다). walktrap 커뮤니티는 간선이 집단 안쪽에 몰리도록 만들어진 분할이다 (모듈러리티를 최대화한다는 게 정확히 그 뜻이다). 반면 성별은 연결과 무관하게 미리 주어진 라벨이다. 게다가 QQ도 walktrap 쪽이 크다.

분할집단 수모듈러리티 QQ
walktrap 커뮤니티50.438705
성별로만 나눈 분할20.408632

그러니 "walktrap 쪽 E-I가 −1에 더 가깝겠다"고 예상하는 게 자연스럽다. 계산해 보면 반대다.

② 계산 코드와 결과.

wt   <- igraph::cluster_walktrap(gu)
memb <- igraph::membership(wt)
names(memb) <- igraph::V(gu)$name

same <- memb[el$from] == memb[el$to]      # 방향 간선 88개 각각에 대해
I <- sum(same);  E <- sum(!same)
c(I = I, E = E, EI = (E - I) / (E + I))
     I      E     EI
76.000 12.000 -0.7273
기준집단 수II (안)EE (밖)E-I
성별2817−0.8409
walktrap57612−0.7273
답: 커뮤니티 기준 E-I = −0.7273. 성별 기준(−0.8409)보다 −1에서 오히려 더 멀다. 예상이 틀렸다.

무엇을 빠뜨렸나 — 집단 수다.

E-I는 집단을 잘게 쪼갤수록 나빠진다. 경계가 늘어나면 "밖으로 나가는 간선"으로 세어지는 것이 늘어나기 때문이다. 성별은 경계가 하나(남|여)뿐이지만, walktrap의 5집단에는 경계가 훨씬 많다. 실제로 넘어간 12개를 뜯어보면 그중 8개는 성별이 같은데도 집단이 달라서 EE로 세어졌다 (예: 1063(여) → 1006(여), 1033(남) → 1051(남)).

따라서 집단 수가 다른 두 분할의 E-I를 직접 비교하면 안 된다. 이건 E-I 지수의 잘 알려진 한계다. 극단적으로 확인해 보면 분명해진다 — walktrap 계층을 2집단에서 자르면 E=0E=0, E-I = −1이 되지만 그 분할은 25명과 1024를 가른 것뿐이라 Q=0Q = 0이다. E-I만 완벽하고 아무것도 설명하지 못한다.

QQ와 E-I가 서로 다른 순위를 준 것도 이 때문이다. QQ는 집단 크기로부터 기대되는 내부 간선 수를 빼서 보정하므로 집단 수에 덜 휘둘린다. 분할끼리 비교할 때는 QQ, 하나의 분할이 얼마나 갈라져 있는지 볼 때는 E-I를 쓴다.

더 중요한 것 — 애초에 커뮤니티 기준 E-I는 "발견"이 될 수 없다. 자기가 만든 분할로 자기를 채점하는 것이라, 알고리즘이 잘 작동한 만큼 좋게 나올 뿐이다. E-I가 뜻을 갖는 것은 성별·성적·거주지처럼 네트워크 바깥에서 온 기준을 쓸 때다. 7절의 −0.8409가 강한 결과인 이유가 바로 이것이다.
문제 5 해설 — 세 학생 구분하기

indegree = 0은 셋이 같지만, 다른 지표를 하나씩만 더 보면 완전히 갈린다.

학생indegree
(받은 지명)
outdegree
(보낸 지명)
coreness컴포넌트
1024000혼자 (크기 1)
1030022본체 (크기 25)
1036032본체 (크기 25)

※ coreness 2인 학생은 학급에서 1030 · 1033 · 1036 셋뿐이고, coreness 0은 1024 하나다. 이 네 명이 학급의 가장 바깥 층이다.

답 — 구분에 쓸 지표와 그에 따른 행동:

지표 ①: outdegree (보낸 지명) — 1024 vs 나머지 둘

  • 1024는 outdegree도 0이다. 설문에 아무것도 적지 않았다. 관계가 없는 게 아니라 응답이 없는 것일 수 있다. → 행동: 분석이 아니라 확인. 그날 출석했는가, 설문지를 받았는가. 이걸 건너뛰고 "고립아"로 해석하면 결석 기록을 심리 진단으로 바꿔 놓는 셈이다.
  • 1030·1036은 지명을 했다. 참여했고, 친하다고 여기는 상대가 있었다. 이건 데이터 결손이 아니라 실제 관측이다.

지표 ②: 누구를 지명했는가 — 1030 vs 1036

for (s in c("1030", "1036")) {
  t <- el$to[el$from == s]
  cat(s, "->", paste0(t, "(ind=", ind[t], ", out=", oud[t], ")", collapse = ", "), "\n")
}
1030 -> 1009(ind=7, out=3), 1078(ind=8, out=3)
1036 -> 1006(ind=7, out=4), 1033(ind=1, out=1), 1042(ind=9, out=4)
  • 1030은 인기 상위권 둘만 골랐다(1009는 받은 지명 7개, 1078은 8개, 둘 다 여학생). → 행동: 이미 지명이 몰려 있는 학생만 바라보고 있다. 여유 있는 다른 학생과의 접점을 만들어 준다.
  • 1036은 다르다. 인기 학생(1006·1042)도 골랐지만 지명이 1개뿐인 1033도 골랐다. 폭이 넓다. 실제로 1036 → 1033은 이 학급에서 1033이 받은 유일한 지명이다. → 행동: 1036은 남을 챙기는 쪽이다. 이 성향을 살릴 자리를 준다.

지표 ③: 지명한 상대의 outdegree — "칸이 차서 못 적은 것"인가?

  • 그럴듯한 가설이다. 이 조사는 최대 6명까지 적게 했으니, 상대의 지명 칸이 이미 찼다면 응답이 없는 게 거부가 아닐 수 있다. 그런데 이 자료에서는 성립하지 않는다. 위 출력의 out=을 보면 상대들은 3~4칸만 썼다 — 두세 칸이 비어 있었다. 6칸을 다 쓴 학생은 학급에서 1027 한 명뿐이고, 그마저 1030·1036의 지명 대상이 아니다. → 결론: "자리가 없어서"는 이 학급에서는 설명이 되지 않는다.
  • 가설을 세우고 데이터로 기각하는 이 과정 자체가 분석이다. 확인하지 않고 "칸이 찼겠지"로 넘어갔다면 잘못된 안심을 했을 것이다.

핵심: 같은 0이라도 "말을 걸지 않은 0"과 "말을 걸었는데 답이 없던 0"은 완전히 다른 상황이다. 전자는 데이터를 의심할 일이고, 후자는 개입할 일이다. indegree 하나만 보면 이 구분이 사라진다 — 그래서 5.5절에서 여러 지표를 같이 보라고 한 것이다.