이 글에 대해서
이 분석결과는 금일(2014.12.11) 서울대학교 통계학과 학과 세미나 강의자료로 활용한 직후에 공개하는 자료로서, 텍스트 분석을 위해 R을 사용하는 연구자나 학생들에게 좋은 시작점이 되었으면 하는 바램에서 주제를 정해 직접 분석한 후 공개하게 되었음. 개인적으로는 독서를 하지 않고 데이터 분석을 통해 핵심만을 취하는 용도로 오용되지 않았으면 하는 바램이며, 다시한번 이문열 평역 삼국지 책을 열어보는 계기가 되었으면 함.ㅋ
## ## -------------- ## ___ _ _ _ _ _ ____ ## |_ _|_ __ | |_ _ __ ___ __| |_ _ ___| |_(_) ___ _ __ | |_ ___ | _ \ ## | || '_ \| __| '__/ _ \ / _` | | | |/ __| __| |/ _ \| '_ \ | __/ _ \ | |_) | ## | || | | | |_| | | (_) | (_| | |_| | (__| |_| | (_) | | | | | || (_) | | _ < ## |___|_| |_|\__|_| \___/ \__,_|\__,_|\___|\__|_|\___/|_| |_| \__\___/ |_| \_\ ## ## _____ _ _ _ _ ## |_ _|____ _| |_ / \ _ __ __ _| |_ _ ___(_)___ ## | |/ _ \ \/ / __| / _ \ | '_ \ / _` | | | | / __| / __| ## | | __/> <| |_ / ___ \| | | | (_| | | |_| \__ \ \__ \ ## |_|\___/_/\_\\__| /_/ \_\_| |_|\__,_|_|\__, |___/_|___/ ## |___/ ## -------------- ## \ ## \ ## \ ## |\___/| ## ) ( ## =\ /= ## )===( ## / \ ## | | ## / \ ## \ / ## jgs \__ _/ ## ( ( ## ) ) ## (_( ##
사용 데이터
- 이문열 평역 삼국지 10권 텍스트 파일
- 인터넷 검색을 통해 획득한 파일로 텍스트 파일 공유는 불가능함
- 제 1 기 : 환제의 즉위부터 조조가 패권을 차지하는 약 30년
- 제 2 기 : 조비가 위를 건국하기까지의 약 15년
- 제 3 기 : 제갈공명이 사망하기까지의 약 15년
- 제 4 기 : 위가 멸망하는 302년까지로 분류함
- 60명의 장수 데이터에 개인적으로 관심 있던 '초선', '왕윤'을 포함 시킴
사용 패키지 소개
-
stringi : ICU라는 유니코드 텍스트 전처리 관련 라이브러리를 R 패키지화 했음. 텍스트 처리에 매우 유용한 함수들을 다수 포함하고 있으며, 처리 속도도 빠르다.
-
extrafont : font 관리 패키지
-
ggplot2 : 대표적인 시각화 패키지
-
corrplot : 다양한 변수별 상관관계 시각화 방법론을 포함하고 있음.
-
data.table : data.frame의 동생격인 패키지로 data.frame 의 모든 기능을 포함하고 추가적인 유용한 처리 옵션을 가지고 있다. 매우 빠르게 동작하기 때문에 대용량 데이터 분석시 필수 패키지임.
-
igraph : 대표적인 SNA 분석 및 시각화 패키지
분석을 위한 전처리
- 텍스트 읽어들임
- 문장 단위로 쪼갬
- 챕터, 권 정보 처리
- 몇번째 문장에서 2권의 두번째 챕터가 시작되는가?
library(stringi)
library(Ruchardet)
library(extrafont)
library(ggplot2)
library(corrplot)
library(data.table)
library(Hmisc)
library(igraph)
#ggplot2 폰트 고정
theme_set(theme_bw(base_family = "Un Dotum"))
#전체 10권의 데이터 로딩
sam_1_v <- readLines("sam/sam_1.txt", encoding = detectFileEncoding("sam/sam_1.txt"))
sam_2_v <- readLines("sam/sam_2.txt", encoding = detectFileEncoding("sam/sam_2.txt"))
sam_3_v <- readLines("sam/sam_3.txt", encoding = detectFileEncoding('sam/sam_3.txt'))
sam_4_v <- readLines("sam/sam_4.txt", encoding = detectFileEncoding('sam/sam_4.txt'))
sam_5_v <- readLines("sam/sam_5.txt", encoding = detectFileEncoding('sam/sam_5.txt'))
sam_6_v <- readLines("sam/sam_6.txt", encoding = detectFileEncoding('sam/sam_6.txt'))
sam_7_v <- readLines("sam/sam_7.txt", encoding = detectFileEncoding('sam/sam_7.txt'))
sam_8_v <- readLines("sam/sam_8.txt", encoding = detectFileEncoding('sam/sam_8.txt'))
sam_9_v <- readLines("sam/sam_9.txt", encoding = detectFileEncoding('sam/sam_9.txt'))
sam_10_v <-readLines("sam/sam_10.txt",encoding = detectFileEncoding('sam/sam_10.txt'))
#삼국지 모든 권을 하나의 리스트로
sam_all <- list(book_1=sam_1_v, book_2=sam_2_v, book_3=sam_3_v, book_4=sam_4_v, book_5=sam_5_v, book_6=sam_6_v, book_7=sam_7_v,
book_8=sam_8_v, book_9=sam_9_v, book_10=sam_10_v)
#각 책권을 문장으로 분리한다.
#문장 분리를 위해 stringi 패키지의 stri_split_boundaries 함수를 사용한다.
sam_all_sentences <- lapply(sam_all, function(book){
book_full <- paste0(book,collapse = "")
stri_split_boundaries(book_full, stri_opts_brkiter(type="sentence"))[[1]]
})
#챕터 텍스트 정보
chapter_str_1 <- c('서사', '창천에 비키는 노을',
'누운 용 엎드린 범', '영웅, 여기도 있다',
'고목의 새싹은 흙을 빌어 자라고',
'황건의 회오리 드디어 일다',
'복사꽃 핀 동산에서 형제가 되고', '도적을 베어 공을 이루다',
'걷히지 않는 어둠',
'장락궁의 피바람', '여우 죽은 골에 이리가 들고',
'차라리 내가 저버릴지언정')
chapter_str_2 <- c('가자, 낙양으로', '데운술이 식기전에',
'낙양에는 이르렀건만', '어제의 동지, 오늘의 적',
'장성은 강동에 지고', '천하를 위해 내던진 미색',
'두 이리 연환계에 걸리다' ,'큰 도적은 죽었으나',
'드디어 패자의 길로', '연못을 떠나 대해로',
'복양성의 풍운 ', '서주는 봄바람만', '궁한 새는 쫓지 않으리',
'서도의 회오리')
chapter_str_3 <- c('안겨오는 천하', '풍운은 다시 서주로',
'우리를 벗어나는 호랑이', '다져지는 또 하나의 기업',
'교룡 다시 연못에 갇히다', '전공은 호색에 씻겨가고',
'천자의 꿈은 수춘성의 잿더미로',
'스스로 머리칼을 벰도 헛되이', '꿈은 다시 전진 속에 흩어지고',
'가련하다 백문루의 주종',
'아직은 한의 천하', '교룡은 다시 창해로',
'원가도 중원을 향하고', '급한 불길은 잡았으나')
chapter_str_4 <- c('살아서는 한의 충신 죽어서는 한의 귀신', '자옥한 전진, 의기를 가리우고',
"드높구나 춘추의 향내여\\(1\\)",
"다섯관을 지나며 여섯 장수를 베다", "아직도 길은 멀고",
"다시 이어진 도원의 의", "아깝다 강동의 손랑",
"장강에 솟는 또 하나의 해", "양웅 다시 관도에서 맞붙다",
"하북을 적시는 겨울비", "이제는 형주로",
"조각나는 원가", "마침내 하북도 조조의 품에",
"높이 솟는 동작대")
#5권은 완전한 책이 아님
chapter_str_5 <- c('용이 어찌 못 속의 물건이랴', '다시 다가오는 초야의 인맥',
'드디어 복룡의 자취에 닿다',
'와룡선생 ', '삼고초려와 삼분천하의 계', '높이 이는 장강의 물결')
chapter_str_6 <- c('계략과 계략 꾀와 꾀', '오가는 사항계로 전기는 무르익고',
'전야, 그 현란함이여',
'모든 것을 갖추었으되 동풍이 없구나',
'혼일사해의 꿈은 동남풍에 타 버리고', '화용도를 끊기엔 옛 은의가 무거워라',
'한바탕 힘든 싸움 누구를 위함이었던고', '교룡은 드디어 삼일우를 얻고',
'다시 이는 두 집안 사이의 불길', '형주는 못 찾고 미인만 바쳤구나',
'주유가 시상으로 되돌아갔다', '비상을 재촉하는 또 하나의 날개',
'이는 회오리', '젊은 범 묵은 용')
chapter_str_7 <- c('발톱 잃고 쫓겨가는 젊은 범', '서천은 절로 다가오고',
'서쪽으로 뻗는 왕기',
'장강엔 다시 거센 물결이 일고',
'바뀐 깃발 돌려세운 칼끝', '새끼 봉은 땅에 떨어지고',
'무너져 내리는 서천의 기둥', '서량의 풍운아 다시 일어나다',
'서천엔 드디어 새로운 해가 뜨고',
'장강을 뒤덮는 호기', '위공도 서쪽으로 눈을 돌리고',
'한중이 떨어지니 불길은 장강으로',
'장하구나 장문원, 씩씩하다 감흥패',
'왕자와 술사들', '허창을 태우는 한신들의 충의')
chapter_str_8 <- c('드디어 터진 한중 쟁탈전', '정군산 남쪽에서 한팔 꺾였네',
'가름나는 한중의 주인', '유비, 한중왕이 되다',
'불길은 서천에서 형주로', '빛나구나, 관공의 무위',
'옛 맹세를 어찌할거나', '흙으로 돌아가고',
'콩깍지를 태워 콩을 볶누나 조조가',
'한의 강산은 마침내 위에게로',
'한스럽다, 익덕도 관공을 따라가고', '벌벌 떠는 동오의 산천',
'원수를 갚아도 한은 더욱 깊어가고')
chapter_str_9 <- c('강남의 서생 칠백 리 촉영을 불사르다', '긴 꿈은 백제성에서 지고',
'촉과 오 다시 손을 잡다',
'위는 오에 맡기고 촉은 남만으', '두 번 사로잡고 두 번 놓아주다',
'네 번을 사로잡혀도',
'독룡동천에서 은갱동으로', '꺾일 줄 모르는 자유의 넋',
'맹획은 드디어 꺾이고 공명은 성도로',
'조비의 죽음과 출사표', '나이 일흔에 오히려 기공을 세웠네 ',
'오리새끼를 놓아 주고 봉을 얻다',
'치솟는 촉의 기세 흔들리는 중원', '다시 쓰이게 된 사마의의 매운 첫솜씨',
'한스럽구나, 가정의 싸움',
'다시 올려지는 출사표')
chapter_str_10 <- c('왕쌍을 베어 진창의 한은 씻었으나', '세번째로 기산을 향하다',
'이번에는 사마의가 서촉으로',
'공명 네번째 기산으로 나아가다',
'다섯번째 기산행도 안으로부터 꺾이고', '여섯번째 기산으로',
'꾸미는 건 사람이되 이루는 건 다만 하늘일 뿐',
'큰 별 마침내 오장원에 지다', '공명은 충무후로 정군산에 눕고',
'시드는 조위', '그 뒤 10년', '홍한의 꿈 한줌 재로 흩어지는구나',
'패업도 부질없어라. 조위도 망하고',
'나뉜 것은 다시 하나로', '결사 ')
#챕터 타이틀을 list형태로 결합
all_chapter_title <- list(chapter_str_1, chapter_str_2, chapter_str_3, chapter_str_4,
chapter_str_5, chapter_str_6,chapter_str_7, chapter_str_8,
chapter_str_9, chapter_str_10)
#책 권 인덱스 생성 및 하나의 vector로 책을 결합
book_end <- 0
book_idx <- c()
all_book_sentence <- c()
for(book in sam_all_sentences){
book_start <- book_end + 1
book_end <- book_start + length(book) - 1
book_idx <- c(book_idx, book_start)
all_book_sentence <- append(all_book_sentence, book)
}
#마지막 인덱스 정보 입력
book_idx[length(book_idx) + 1] <- length(all_book_sentence)
#책 챕터 인덱스 생성
all_chapter_idx <- list()
for(b_idx in 1:(length(book_idx) - 1)){
all_chapter_idx[[b_idx]] <- sapply(all_chapter_title[[b_idx]], function(chap_tit){
which(grepl(chap_tit, all_book_sentence[book_idx[b_idx]:book_idx[b_idx + 1] - 1]))[1] + book_idx[b_idx]
})
}
## 60명 장수의 이름파일 읽어들이기
sam_names <- readLines("names.txt",encoding = "UTF-8")
#각 권 제목
book_title <- c('도원에 피는 의', '구름처럼 이는 영웅', '헝클어진 천하',
'칼 한자루 말 한 필로 천리를 닫다', '세번 천하를 돌아봄이여',
'불타는 적벽', '가자 서촉으로', '솔밭처럼 갈라선 천하',
'출사표, 드높아라 충신의 매운 얼이여',
'오장원에 지는 별')
텍스트에 대한 일반 통계
- 문장 길이 분포
## 종합
# 10권의 문장 정보를 문자 벡터로 가지고 있음
# all_book_sentence
length(all_book_sentence)
## [1] 55249
head(all_book_sentence)
## [1] " 삼국지1권 나관중 지음 이문열 평역 도원에 피는 의 서사 티끌 자윽한 이 땅 일을 한바탕 긴 봄꿈이라 이를 수 있다면, 그 한바 탕 꿈을 꾸미고 보태 이기함 또한 부질없는 일이 아니겠는가. " ## [2] "사람은 같은 냇물에 두 번 발을 담글 수 없고, 때의 흐름은 다만 나아갈 뿐 되돌 아오지 않는 것을, 새삼 지나간 날 스러진 삶을 돌이켜 길게 적어 나감 도. " ## [3] "마찬가지로 헛되이 값진 종이를 버려 남의 눈만 어지럽히는 일이 되 지 않겠는가. " ## [4] "그러하되 꿈속에 있으면서 그게 꿈인 줄 어떻게 알며,흐름 속에 함께 흐르며 어떻게 그 흐름을 느끼겠는가. " ## [5] "꿈이 꿈인 줄 알려면 그 꿈에서 깨어나야 하고, 흐름이 흐름인 줄 알려면 그 흐름에서 벗어나야 한다. " ## [6] "때로 땅끝에 미치는 큰 앎과 하늘가에 이르는 높은 깨달음이 있어 더러 깨어나고 또 벗어나되, 그 같은 일이 어찌 여느 우리에게까지도 한결같 을 수가 있으랴. "
# 리스트 구조형태로 all_book_sentence에서 챕터의 시작 위치 정보를 가지고 있음
# all_chapter_idx
print(all_chapter_idx[1])
## [[1]] ## 서사 창천에 비키는 노을 ## 2 27 ## 누운 용 엎드린 범 영웅, 여기도 있다 ## 402 781 ## 고목의 새싹은 흙을 빌어 자라고 황건의 회오리 드디어 일다 ## 1177 1553 ## 복사꽃 핀 동산에서 형제가 되고 도적을 베어 공을 이루다 ## 1890 2295 ## 걷히지 않는 어둠 장락궁의 피바람 ## 2729 3126 ## 여우 죽은 골에 이리가 들고 차라리 내가 저버릴지언정 ## 3668 4168
# 주요 등장 인물들에 대한 리스트
# sam_names
head(sam_names)
## [1] "조조" "공명" "사마소" "유비" "여포" "사마의"
#권별 문장 길이 분포
sentence_lengths <- lapply(sam_all_sentences, function(book){
nchar(book)
})
to_df <- data.frame(book=c(), len=c())
for(book_idx in 1:length(sentence_lengths)){
to_df <- rbind(to_df, data.frame(book=names(sentence_lengths[book_idx]), len=sentence_lengths[[book_idx]]))
}
to_df$book <- factor(to_df$book, levels=c("book_1", "book_2", "book_3","book_4","book_5",
"book_6","book_7","book_8","book_9","book_10"))
#ggplot(to_df, aes(len)) + geom_density(aes(fill=book), alpha=0.6) + scale_x_continuous(breaks=seq(1,300,by = 20))
ggplot(to_df, aes(len)) + geom_density(aes(fill=book), alpha=0.6) + scale_x_continuous(breaks=seq(1,300,by = 20)) + facet_grid(book~.)
- 1~10권 모두 유사한 분포를 따르고 있다.
- 이문열 작가는 이 소설 전체에서 일반적으로 한 문장당 30문자를 사용했다(공백 포함).
- 1권은 다소 문장 길이 분포의 첨도가 상대적으로 낮았으나 점점 집필을 계속할 수록 첨도가 높아지는 경향을 보인다.
장수 출현 정도
#이름과 등장 횟수
nm_cnts <- sapply(sam_names, function(nm){
sum(grepl(nm, all_book_sentence))
})
ordered_freq <- nm_cnts[order(nm_cnts,decreasing = T)]
pander(data.frame(ordered_freq))
ordered_freq
조조 5281
유비 3194
공명 2099
여포 1254
원소 1172
장비 1047
손권 842
관우 833
동탁 699
주유 693
사마의 675
마초 623
조운 608
위연 532
손책 523
손견 454
강유 425
원술 379
장합 368
황충 347
유표 343
맹획 322
조비 299
이각 298
조인 254
서황 242
유장 230
곽사 229
허저 221
왕윤 215
육손 214
관흥 209
동승 205
조진 202
헌제 187
등애 182
감녕 176
초선 154
장포 153
왕평 144
조예 127
조홍 127
사마소 109
곽회 109
종회 108
하후무 100
공융 79
사마사 73
도겸 72
조상 72
마충 69
손호 51
사마염 48
영제 48
유선 29
소제 26
제갈각 26
제갈탄 24
손침 21
손휴 18
공손연 16
장요 1
prop_ordered_nm <- prop.table(ordered_freq)
ggplot(data.frame(nm=names(ordered_freq), ratio=ordered_freq), aes(reorder(nm, ratio), ratio)) +
geom_histogram(stat='identity') + xlab("장수") + ylab("빈도") + coord_flip()
ggplot(data.frame(nm=names(prop_ordered_nm), ratio=prop_ordered_nm), aes(reorder(nm, ratio), ratio)) +
geom_histogram(stat='identity') + xlab("장수") + ylab("비율") + coord_flip()
- 역시 숫자보다는 시각화 된 결과가 더 직관적임
- 빈도보다는 상대적인 중요성을 보기 위해서 비율로 시각화 해보는 것도 중요함
각 장수별 산포도(Dispertion plot)
- x축 : 소설 시간
- 출현 빈도순 정렬
#1권에서 인물 분포
for(nm in c("조조", "유비", "관우", "장비")){
n_times <- seq(1:length(sam_all_sentences$book_1))
character_grep <- grepl(nm, sam_all_sentences$book_1)
w_cnt <- rep(NA, length(n_times))
w_cnt[character_grep] <- 1
plot(w_cnt, main=sprintf("'%s' 산포도", nm),
xlab='', ylab=sprintf("총 %d 회 출현", sum(character_grep)), type="h", ylim=c(0,1), yaxt='n')
}
#1~ 10권 60명 장수 분포 모두를 플로팅한다.
#pdf("DP.pdf",width = 10, height = 6, onefile = T)
for(nm in names(ordered_freq)){
n_times <- seq(1:length(all_book_sentence))
character_grep <- grepl(nm, all_book_sentence)
w_cnt <- rep(NA, length(n_times))
w_cnt[character_grep] <- 1
plot(w_cnt, main=sprintf("'%s' 산포도", nm),
xlab='', ylab=sprintf("총 %d 회 출현", sum(character_grep)), type="h", ylim=c(0,1), yaxt='n', xaxt='n')
axis(1, at = sapply(all_chapter_idx, `[`, 1), labels = book_title, cex.axis=0.6, las=2)
}
#dev.off()
- 도원결의를 하는 1권의 경우 “유비/관우/장비"의 출현 위치가 유사함
- 1~10권 전체의 시간 분포를 볼때 등장인물들의 생존기간을 소설 시간으로 유추해 볼 수 있다.
장수간 유사도 계산(활동 시점 관점)
- 특정 챕터들에 대해 출현빈도가 높은 장수들은 동시대 장수들일 것이다.
- 챕터에서 나온 빈도를 기준으로 두 장수들간의 상관관계를 계산하자!
\[\Large r = \frac{\sum (x_{i} – \bar{x})(y_{i}-\bar{y})}{\sqrt{\sum(x_{i}-\bar{x})^2}\sqrt{\sum(y_{i}-\bar{y})^2}}\]
all_idx <- vector(mode="numeric")
for(bi in all_chapter_idx){
all_idx <- append(all_idx,bi)
}
chp_cnt_lst <- list()
for(nm in names(ordered_freq)){
n_times <- seq(1:length(all_book_sentence))
character_grep <- grepl(nm, all_book_sentence)
w_cnt <- rep(0, length(n_times))
w_cnt[character_grep] <- 1
w_cnt_dt <- data.table(w_cnt=w_cnt)
w_cnt_dt[,chapter_idx:=cut2(1:nrow(w_cnt_dt), cuts=all_idx)]
chp_word_cnts <- w_cnt_dt[,list(w_cnt_sum=sum(w_cnt)),by=chapter_idx]$w_cnt_sum
chp_cnt_lst[[nm]] <- chp_word_cnts
}
#같은 length를 가지고 같은 타입을 포함하고 있는 리스트 객체는 쉽게 data.frame이나 data.table로 변환 가능하다.
chp_cnt_dt<- as.data.frame(chp_cnt_lst)
# general_dists <- dist(scale(t(chp_cnt_dt)),method = "euclidean")
#
# clust <- hclust(general_dists)
# plot(clust)
# rect.hclust(clust,k=5)
cor_obj <- cor(chp_cnt_dt)
corrplot(cor_obj,order = "hclust", addrect=5)
- 상관관계 기반 상관도표이며, 장수명은 유사도에 따라 클러스터링해서 보여준다.
- 대부분 상관관계가 높은 쌍은 비슷한 시기에 왕성한 활동을 했던 장수를 보여준다.
장수들간 유사도 계산(상호 언급 관점)
- 한 문장에서 함께 출현한 빈도가 높은 장수들에 대한 유사도 측정(Jaccard Coefficient)
- binary로 입력된 값들을 기반으로 두 오브젝트 A,B사이의 유사도를 다음과 같이 계산 할 수 있다.
\[Jaccard Coefficient = \Large{\frac{A \cap B}{A \cup B}}\]
sent_cnt_lst <- list()
for(nm in names(ordered_freq)){
n_times <- seq(1:length(all_book_sentence))
character_grep <- grepl(nm, all_book_sentence)
w_cnt <- rep(0, length(n_times))
w_cnt[character_grep] <- 1
sent_cnt_lst[[nm]] <- w_cnt
}
sent_cnt_dt <- as.data.frame(sent_cnt_lst)
general_dist <- dist(t(sent_cnt_dt),method='binary')
clust <- hclust(general_dist)
plot(clust)
- 실제 우리가 알고 있는 정보와 많은 부분 일치한다.
g <- graph.adjacency(1 - as.matrix(general_dist), weighted=T, mode ="undirected")
g <- simplify(g)
layout1 <- layout.circle(g)
plot.igraph(g,layout=layout1, edge.width=E(g)$weight * 130)
마치며
- 추가 분석 과제
- 저자 판별(형태소 분석 필요 : KoNLP)
- 분석 목적에 맞는 텍스트 전처리의 유연성 필요
- R은 텍스트 통계 분석의 최고 환경
참고문헌
- Text Analysis with R for Students of Literature, Matthew Jockers, 2014
발표자료
by : 전희원 (http://freesearch.pe.kr)
이문열 평역 소설 삼국지 텍스트 데이터 분석 – 장수간의 관계 그리고 초선 by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.