• Home

데이터 분석가의 KPI 그리고 소프트웨어 개발자의 KPI

약 9년의 소프트웨어 엔지니어 생활에서 데이터 분석분야로 업무 분야를 바꾼 뒤 약 3년 정도의 시행착오 끝에 현재 데이터 분석가로 탈바꿈을 했고, 어느정도 안정궤도에 올랐다는 생각이 들어 관련 내용을 정리하고자 이렇게 글을 써본다.

“데이터를 분석한다”

많은 사람들이 위 문장을 사용하고 인식하는데 매우 큰 차이를 보이고 있다. 단순히 데이터 전처리 및 단어 카운팅을 하는것을 분석행위라 하는 것 부터, 시각화, 고급 모델링 및 통계분석 추론하는 행위까지 포함하고 있는 문장이기 때문이다. 이 때문에 데이터 분석가라는 직업 자체가 매우 폄하되거나 과대 포장되는 경우가 상당히 많을 것으로 생각된다. 하지만 이 글에서는 “분석한다”라는 의미는 “의사결정에 도움이 되거나 자동화를 위해 데이터에서 통계적으로 유의미한 정보를 추출하는 행위”로 정의하도록 하겠다.

이런 이유 때문에 다수의 소프트웨어 개발자들이 데이터 전처리를 하면서 간단하게 분석을 수행하기도 하고, 오히려 고급 통계분석이나 데이터마이닝을 업무에 적용해 스스로가 데이터 분석가로 거듭나는 사람들이 종종 있다. 반면에 데이터 분석가로 탈바꿈 하고 싶지만 그렇게 하지 못하는 소프트웨어 개발자들도 많이 봐왔다.

소프트웨어 개발자에서 데이터 분석가로 성공적으로 업종 변경을 하는건 경험자의 한사람으로 봤을때 그리 간단한 문제가 아니다. 흡사 오랫동안 오른손을 주로 사용해서 조각을 해온 사람에게 왼손만 사용하게 하는 것과 같은데 예외 사항으로 조각을 위한 도구를 만들때는 오른손을 사용하게 해서 필요한 다양한 도구를 만드는 시간을 단축할 수 있게 하는 상황과 매우 유사하다.

변신을 위해 몇가지 유념해야 될 사항이 있다.

  1. 그동안 주력 기술과 경력을 쌓았던 소프트웨어 개발 능력이 앞으로는 주력이 되서는 안된다. 이걸 너무도 잘해 계속 성과로 내세우다가는 영영 데이터 분석 업무로 갈 수 없다.
  2. 소프트웨어 개발은 본인의 분석 업무를 매우 쾌적하게 만들어줄 환경과 빠른 분석을 위한 보조 수단으로 활용한다.
  3. 데이터 분석가의 KPI와 소프트웨어 개발자의 KPI는 다르다.

위 세번째 항목이 매우 중요한데, 데이터 분석 업무의 궁극적인 목적은 의사 결정을 위한 유의미한 정보 발견이다. 이는 특정 기능을 하는 소프트웨어를 제작하는 행위가 목적인 소프트웨어 개발과는 매우 다른 목적이다. 몇몇 이를 혼동해서 특정 기능을 수행하는 통계 알고리즘을 직접 구현하는데 80%의 시간을 소모하고 20%의 시간을 데이터 분석에 할애하는 개발자들이 종종 있는데, 이는 데이터 분석 업무에서는 도박행위하고 같다. 이 행위는 필자도 수년전 직접 시행착오를 했던 부분인데, 그당시 Neural Network를 Erlang이라는 언어로 직접구현(노드를 worker로 할당해 학습을 분산시키는 개념)하고 데이터 분석을 하고자 했는데, 알다시피 Neural Network와 같은 데이터를 입력으로 받고 데이터로 모델링을 하는 학습 알고리즘의 경우 디버깅이 매우 어렵다. 게다가 Neural Network 자체가 블랙박스 모형이라는 점도 큰 장애물로 작용을 했다. 이렇게 80% 이상의 프로젝트 시간을 구현에 허비하고 남은 20%의 시간만 실제 데이터 분석에 투자하는 과오를 범하고 말았다. 결국 프로젝트는 앞으로 이렇게 하지 말아야 겠다는 교훈만을 남긴채 실패하고 말았다.

이 당시 나는 개발자적인 만족감에 도취되 Erlang언어로 Neural Network를 직접 구현하는 만행을 저질렀다. 이런 만족감 이면에는 통계분석의 경험 부족을 개발 결과물로 어느정도 보완하고자 하는 무의식적인 의도가 있었던 것이 아니였을까 회상해 본다. 이 당시 부족한 경험과 실력이지만 그당시 가능했던 통계분석 방법으로 데이터 분석 목적 달성을 위해 노력했다면 어땠을까 하는 생각을 가끔씩 해보기도 하나 개발자에서 데이터 분석가로 가기 위해 어느정도 피할 수 없는 시행착오가 아니였을까 하는 생각이 들기도 한다.

데이터 분석을 하는데, 소프트웨어 개발 기술 및 이해정도는 요즘같은 시기에 매우 큰 힘이된다. 하지만 데이터 분석의 KPI를 잊은채 소프트웨어 개발 결과물을 내세우고 싶은 유혹 또한 있기 마련이다. 이런 유혹을 떨칠 수 있는 사람만이… 다른 능력이 있더라도 데이터 분석 업무의 시행착오를 묵묵히 견뎌낼 수 있는 사람이 데이터 분석 업무로 성공적인 안착을 할 수 있다고 생각한다. 물론 데이터 분석 KPI를 달성하기 위해 새로 습득해야 될 지식과 경험이 생각보다 매우 많다는 함정이 있지만 말이다.

흥미롭게도 소프트웨어 개발과는 다르게 데이터 분석에서 소프트웨어 혹은 코드조각은 그저 중간 결과물에 지나지 않는다. 한번의 분석을 위해 수백라인의 R코드를 작성하지만 분석 결과가 무의미 하다면 코드는 쓰레기통에 들어가고 만다. 코드가 가치가 있으려면 데이터 분석 결과가 유의미 해야되고 이를 배치잡으로 동작시킬 필요가 있을때 비로소 분석 코드가 소프트웨어로 되살아날 기회가 생긴다. 따라서 분석과정에서는 개발과정과 같이 심혈을 기울여 최적화된 코드를 미리 작성할 필요가 없으며 이미 존재하는 알고리즘을 재구현할 이유도 없는 것이다. 프로토타이핑을 하듯이 데이터 분석코드는 작성할 필요가 있고, 이미 존재하는 알고리즘은 사용과 해석에 필요한 지식을 습득하는데 투자를 하고 가져다 쓰는게 가장 좋다. 이는 데이터 분석 및 모델링에 ‘은제탄환(Silver Bullet)’이 존재하지 않기 때문이며, 목적에 맞는 탄환을 빠르게 찾아서 선택사용하는게 데이터 분석 KPI를 달성하는데 가장 좋은 접근방법이기 때문이다.

“직접 만들지 않고 이렇게 가져다 쓰기만 하면 내가 얻는게 뭐가 되나요?” 하고 소프트웨어 개발자 관점의 질문을 할 수 있다고 생각하는데, 데이터 분석을 하다보면 해석과 추론을 위해 알아야 될 통계지식이 너무도 많이 요구된다. 그 지식을 실무에 적용 시키는 훈련이 곧 학습이 되고 경험이 되는 것이다. 흥미롭게도 위와 같은 질문 자체가 아직 소프트웨어 개발의 KPI에서 벗어나지 못했다는것을 의미하기도 한다.

어제의 엄청난 방문자수는 블랙스완? – 블로그 방문자수 예측 모형 –

이 블로그는 호스팅을 받기 때문에 트래픽에 대한 어느정도의 예상치는 알고 있어야 나중에 곤욕을 당하지 않는다. 바로 어제(2014.12.12) 이문열 삼국지 텍스트 분석 포스팅의 엄청난 인기 때문에 약 세번의 트래픽 리셋이 있었고, 거기다 추가적으로 매일 4G의 웹 트래픽을 더 구입해서 임시적으로나마 내년 2월 중순까지 버텨 보기로 했다(트래픽 최소 구입 단위가 4G였으며, 기간은 무조건 호스팅 계약 종료시까지였다).

아래와 같은 메시지를 어제 하루동안 세번을 받았다. ㅜㅜ

exceed


그동안 미뤄 오던 블로그 방문자 수 예측(forecasting) 모형을 만들어 보고자 한다. 그리고 어제의 방문자 수가 과거 패턴으로 볼때 발생확률이 어느정도인지도 계산해보자!

필자의 경우 GA(http://www.google.com/analytics/)을 2011년부터 적용해오고 있어서 방문자 time series 분석을 하기엔 크게 어렵지 않았다. 그리고 GA의 API를 이용하기 위해서 rga 패키지를 사용했다.

suppressPackageStartupMessages({
library(rga)
library(xts)
library(forecast)
})
rga.open(instance="ga", where="./ga.rga")


daily_visits_src <- ga$getData(
  key,
  start.date = "2011-01-01",
  end.date = "2014-12-13",
  metrics = "ga:users",
  dimensions = "ga:date",
  sort = "ga:date",
  batch = TRUE
)

daily_visits <- daily_visits_src 

데이터를 읽어왔으니 본격적으로 시계열 모형을 만들자.
문제를 좀더 간단히 하기 위해서 주단위 방문자 수를 예측하는 모형을 만들어본다.

library(forecast)

daily_xts <- xts(daily_visits$users, order.by = daily_visits$date,frequency = 7)

weekly_sum <- apply.weekly(daily_xts, sum)

weekly_ts <- ts(as.numeric(weekly_sum), start = c(2011, 1),frequency = 52)

plot(stl(weekly_ts, s.window="periodic"))

plot of chunk times

decompose를 보면 몇가지 흥미로운 사실을 알 수 있다.

  • 년단위 seasonality가 있으나 그 정도가 크지 않다는 것.
    • 회사 생활과 학교를 병행하고 있어서 방학때 포스팅이 많은 이유인지 방문자가 여름과 겨울에 많은 패턴이 보인다.
    • 주별 평균 포스팅 갯수와 방문자들의 상관관계가 높을 것으로 예상되나 주별 포스팅 숫자는 WordPress DB를 확인해야 되서 이 데이터 분석은 내년 2월 호스팅 서비스 선택시점까지 미뤄본다.
  • 한창 KoNLP를 개발하고 개선하던 시점에 블로그 방문자가 많았다는 것
  • 이번주 (2014-12-07 ~ 2014-12-13) 기록 사상 역대 최고의 방문자수를 기록 했다는 것
weekly_ar <- auto.arima(weekly_ts)

tsdiag(weekly_ar)

plot of chunk times2

Seasonal ARIMA 모형으로 모형 피팅을 한다. 이때 auto.arima 함수는 최적의 p,d,q 파라메터를 AIC 혹은 다른 모델 적합 관련 함수로 자동으로 선택해 주기 때문에 아주 편함. 물론 ACF, PACF같은 함수로 적절한 파라메터로 서치를 하는 작업도 필요하나 auto.arima는 이런 작업 결과와 큰 차이를 보여주지 않기 때문에 그냥 사용한다.

무엇보다 모델 검정이 중요하다. Ljung-Box 검정으로 확인해보니 잔차의 시차간 상관관계가 없다는 귀무가설을 지지하는 결과를 보여주고 실제 ACF로 볼때 잔차의 상관 패턴이 보이지 않아 S-ARIMA에서 뽑을 수 있는 패턴은 제대로 뽑혔다는 것을 확인해 볼 수 있다.

accuracy(weekly_ar)
##                    ME     RMSE      MAE         MPE     MAPE      MASE
## Training set 14.54806 168.7241 127.5051 -0.09397117 11.94629 0.9204207
##                    ACF1
## Training set 0.03437268

모형의 평균 에러는 12% 정도(training set only)

hist(weekly_ar$residuals,breaks = 30)

plot of chunk unnamed-chunk-3

dist <- sample(weekly_ar$residuals,10000,replace = TRUE)

idx <- which.max(dist[order(dist,decreasing = T)] >= weekly_ar$residuals[length(weekly_ar$residuals)])

pval <- idx/10000

잔차의 분포를 보니 정규분포는 아닌것으로 보여 prediction interval에 큰 의미를 부여할 필요는 없다는 것을 알 수 있다.
그리고 잔차의 분포를 생성하기 위해 bootstrap 방식으로 리샘플링 해서 최근 1주의 트래픽이 발생할 확률값을 계산했다(p-value : 0.0001).
확률값으로 알 수 있는 것처럼 만번에 한번 발생할까 말까한 '블랙스완'에 가까운 방문자수라 할 수 있으며, 약 2주 정도는 금주 트래픽 효과가 지속될 것으로 보인다.

plot(forecast(weekly_ar,h = 7,level=0, fan=FALSE))

plot of chunk unnamed-chunk-4

그리고 마지막으로 호스팅 재계약인 2월 중순까지 방문자수 예측을 해봤다.

모델 예측 결과로는 금주 많은 트래픽이 몰렸다고 해서 성급하게 고사양의 호스팅 서비스를 계약하는건 시기상조라는 이야기를 해주고 있으며 내년에도 금년과 유사한 트래픽 상품을 계약하거나 안전하게 한단계 정도 높은 서비스를 계약하는게 나을거라 예상해본다. 그러나 최종 결정은 호스팅 회사에서 제공하는 일별 트래픽 데이터를 기반으로 2월 초 정도에 분석해 상품을 결정할 예정이다(방문자와 트래픽 사용량의 관련성 파악과 방문자 1인당 사용 트래픽량 시뮬레이션 등등).


분석 결과를 요약하면 아래와 같다.

  • 어제 성급하게 상위 상품으로 계약 갱신을 했다면 낭비가 발생 했을 것이며, 2만원 정도로 현재의 서비스 만기까지 4G 트래픽을 더 구입한건 현재 내가 할 수 있는 범위에서 최적의 선택이였다.
  • 주 혹은 개월 단위로 트래픽을 자신이 원하는 기간동안 더 구매할 수 있는 호스팅 서비스를 찾아볼 생각이다. 이는 고 방문자수 영향의 지속 기간이 최대 2주로 모델링 결과에서 볼 수 있기 때문이다.
  • 호스팅 재계약 시점인 내년 2월 초에 실제 트래픽 데이터를 받아서 통합 분석해보자!

이문열 평역 소설 삼국지 텍스트 데이터 분석 – 장수간의 관계 그리고 초선

이 글에 대해서

이 분석결과는 금일(2014.12.11) 서울대학교 통계학과 학과 세미나 강의자료로 활용한 직후에 공개하는 자료로서, 텍스트 분석을 위해 R을 사용하는 연구자나 학생들에게 좋은 시작점이 되었으면 하는 바램에서 주제를 정해 직접 분석한 후 공개하게 되었음. 개인적으로는 독서를 하지 않고 데이터 분석을 통해 핵심만을 취하는 용도로 오용되지 않았으면 하는 바램이며, 다시한번 이문열 평역 삼국지 책을 열어보는 계기가 되었으면 함.ㅋ

## 
##  -------------- 
##  ___       _                 _            _   _               _          ____  
## |_ _|_ __ | |_ _ __ ___   __| |_   _  ___| |_(_) ___  _ __   | |_ ___   |  _ \ 
##  | || '_ \| __| '__/ _ \ / _` | | | |/ __| __| |/ _ \| '_ \  | __/ _ \  | |_) |
##  | || | | | |_| | | (_) | (_| | |_| | (__| |_| | (_) | | | | | || (_) | |  _ < 
## |___|_| |_|\__|_|  \___/ \__,_|\__,_|\___|\__|_|\___/|_| |_|  \__\___/  |_| \_\
##                                                                                
##  _____         _        _                _           _     
## |_   _|____  _| |_     / \   _ __   __ _| |_   _ ___(_)___ 
##   | |/ _ \ \/ / __|   / _ \ | '_ \ / _` | | | | / __| / __|
##   | |  __/>  <| |_   / ___ \| | | | (_| | | |_| \__ \ \__ \
##   |_|\___/_/\_\\__| /_/   \_\_| |_|\__,_|_|\__, |___/_|___/
##                                            |___/            
##  --------------
##       \
##         \
##           \
##             |\___/|
##             )     (
##            =\     /=
##              )===(
##             /     \
##             |     |
##            /       \
##            \       /
##       jgs   \__  _/
##               ( (
##                ) )
##               (_(
## 

사용 데이터

  • 이문열 평역 삼국지 10권 텍스트 파일
    • 인터넷 검색을 통해 획득한 파일로 텍스트 파일 공유는 불가능함
  1. 제 1 기 : 환제의 즉위부터 조조가 패권을 차지하는 약 30년
  2. 제 2 기 : 조비가 위를 건국하기까지의 약 15년
  3. 제 3 기 : 제갈공명이 사망하기까지의 약 15년
  4. 제 4 기 : 위가 멸망하는 302년까지로 분류함
  • 60명의 장수 데이터에 개인적으로 관심 있던 '초선', '왕윤'을 포함 시킴

사용 패키지 소개

  • stringi : ICU라는 유니코드 텍스트 전처리 관련 라이브러리를 R 패키지화 했음. 텍스트 처리에 매우 유용한 함수들을 다수 포함하고 있으며, 처리 속도도 빠르다.

  • extrafont : font 관리 패키지

  • ggplot2 : 대표적인 시각화 패키지

  • corrplot : 다양한 변수별 상관관계 시각화 방법론을 포함하고 있음.

  • data.table : data.frame의 동생격인 패키지로 data.frame 의 모든 기능을 포함하고 추가적인 유용한 처리 옵션을 가지고 있다. 매우 빠르게 동작하기 때문에 대용량 데이터 분석시 필수 패키지임.

  • igraph : 대표적인 SNA 분석 및 시각화 패키지


분석을 위한 전처리

  1. 텍스트 읽어들임
  2. 문장 단위로 쪼갬
  3. 챕터, 권 정보 처리
    • 몇번째 문장에서 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~.)

plot of chunk init_analysis

  • 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()

plot of chunk freq_ratio_plot

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()

plot of chunk freq_ratio_plot

  • 역시 숫자보다는 시각화 된 결과가 더 직관적임
  • 빈도보다는 상대적인 중요성을 보기 위해서 비율로 시각화 해보는 것도 중요함

각 장수별 산포도(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')
}

plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion

#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)  
}

plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion plot of chunk dispersion