공부하고 기록하는, 경제학과 출신 개발자의 노트

프로그래밍/이것저것_개발일지

R을 활용한 카카오톡 대화내용 분석

inspirit941 2017. 9. 27. 01:00
반응형

(1) R로 카카오톡 텍스트데이터 분석하기


170825.


사람의 말은 그 사람의 인품을 드러내기도 하고, 의식과 무의식을 세상에 내놓는 하나의 수단이다. 그렇다면, 카카오톡에서 우리가 지인들과 아무 생각 없이 하는 말 속에는 내 무의식이 담겨 있지 않을까? 보통 카카오톡에서 어떤 대화를 많이 하는지, 어떤 감정어를 많이 쓰는지 파악하는 것도 재미있을 것 같았다. 마침 멋쟁이사자처럼 동아리에서도 방학 동안 진행해 볼 프로젝트 주제가 필요했다. 그래서 시작한 카카오톡 텍스트데이터 분석.


목표는 두 가지였다.
1. R에서 카카오톡 텍스트데이터를 분석하고 시각화를 해본다.
    - 워드클라우드
    - 카카오톡 데이터 관련 통계량을 googleVis로 시각화
    - 연관성 높은 단어를 qgraph로 시각화
2. R의 코드내용을 Ruby on Rails로 구현한다.
    - 웹페이지의 사용자가 카카오톡 대화 txt파일을 제출하면, 분석한 결과를 제공하는 웹페이지 구현.




R에서 필요한 패키지들은 아래와 같았다.

library(KoNLP) #한글 형태소 분석 라이브러리. library(stringr) #Regexp를 하기 위해서. 한글을 문자별로, 단어별로 잘라내고 바꾸는 일을 담당 library(wordcloud) #워드클라우드 library(RColorBrewer) #워드클라우드 색깔을 예쁘게 해 주는 걸로 알고 있음 library(rvest) #뭐였지? 잘 기억이 안 난다. library(NIADic) #KoNLP의 한글사전 최신판. SejongDic보다 단어량도 많고 정확도도 높다. library(googleVis) #차트 그리기용. 보기 좋아서 썼다. library(tm) #연관성 검사를 할 때 필요한 라이브러리.  

library(qgraph) #연관성 검사 결과값을 시각화하기 위한 그래프.


카카오톡 PC버전에서 '대화 내보내기' 기능으로 txt파일을 추출했다.





데이터 기본 세팅

textdata<-file("카톡 대화내용파일.txt", encoding='UTF-8') #파일 받기 #R 코딩하면서 한글파일이 워낙 많이 깨져서, 불러올 때마다 인코딩을 UTF-8로 계속 설정해뒀다. data<-readLines(textdata, encoding='UTF-8') head(data) data<-data[-1:-3] # txt파일 첫째~셋째 줄은 필요없는 데이터다. 날려버리자. head(data) #확인 data<-str_replace_all(data,'핸드폰에 저장한 상대방 이름',"본명") #안 하고 그대로 진행해도 무리는 없는데, 나중에 명사들만 따로 추출할 때 쓸모없는 데이터가 많이 생긴다.



#1. 톡방 지분 - 누가 더 많이 톡하는가

person1<-length(data[grep("\\[내 이름",data)])

person2<-length(data[grep("\\[상대방 이름",data)]) #내 카톡과 상대 카톡의 개수를 추출했다. 얼마나 많이 떠들었는가 파악할 수 있다. 카톡량<-c(person1,person2) 이름<-c('내 이름','상대방 이름') 카톡지분<-data.frame(이름,카톡량) #데이터프레임 형태로 만듬 pie1<-gvisPieChart(카톡지분,options=list(width=400,height=300)) 이렇게만 실행했을 때, 구글차트에서는 한글이 깨진다. 해결해 보려고 열심히 구글링하다가 찾게 된 해결코드가 아래와 같다. #구글맵 한글깨짐 방지코드 header <- pie1$html$header header <- gsub("charset=utf-8", "charset=euc-kr", header) pie1$html$header <- header 

plot(pie1) #파이차트 완성.


완성 결과물. googleVis가 확실히 예쁘다. 사실 ggplot2를 구글링해 찾는 것보다 편해서이기도 했다.





#2. 시간대별 카톡량 분석하기

bfnoon<-data[grep("\\[오전",data)] #오전 데이터값만 불러오기

afnoon<-data[grep("\\[오후",data)] #오후 데이터값만 불러오기 timefunc<-function(x){ time1<-x[grep("\\[[가-힣]+ 1[[:punct:]]",x)] #100~159분까지 time2<-x[grep("\\[[가-힣]+ 2[[:punct:]]",x)] #200~259분까지 time3<-x[grep("\\[[가-힣]+ 3[[:punct:]]",x)] time4<-x[grep("\\[[가-힣]+ 4[[:punct:]]",x)] time5<-x[grep("\\[[가-힣]+ 5[[:punct:]]",x)] time6<-x[grep("\\[[가-힣]+ 6[[:punct:]]",x)] time7<-x[grep("\\[[가-힣]+ 7[[:punct:]]",x)] time8<-x[grep("\\[[가-힣]+ 8[[:punct:]]",x)] time9<-x[grep("\\[[가-힣]+ 9[[:punct:]]",x)] time10<-x[grep("\\[[가-힣]+ 10[[:punct:]]",x)] time11<-x[grep("\\[[가-힣]+ 11[[:punct:]]",x)] time12<-x[grep("\\[[가-힣]+ 12[[:punct:]]",x)] return(c(length(time1),length(time2),length(time3), length(time4),length(time5),length(time6),length(time7), length(time8),length(time9),length(time10),length(time11),length(time12))) } #시간대별로 카톡수가 얼마나 되는지 개수 세는 함수. 1~12시까지의 카톡개수를 return한다. Beforenoon<-timefunc(bfnoon) #오전 시간대별 카톡개수 Afternoon<-timefunc(afnoon) #오후 시간대별 카톡개수 time<-c('1시','2시','3시','4시','5시','6시','7시','8시','9시','10시','11시','12시') beforenoon1<-data.frame(time,Beforenoon,Afternoon) #표 형식으로 만들기 위해서 data.frame형태로 합침. 시간대별차트<- gvisColumnChart(beforenoon1,options=list(height=400,weight=500)) #마찬가지로 한글깨짐방지 header <- 시간대별차트$html$header header <- gsub("charset=utf-8", "charset=euc-kr", header) 시간대별차트$html$header <- header 

plot(시간대별차트)






#3. 텍스트분석을 위한 데이터 전처리


텍스트 데이터에서 의미있는 명사들만 파악하기 위한 전처리 과정이다. 

의미없는 단어들을 미리 걸러내야 하는데, 어떤 단어가 의미없는 단어인지 미리 생각해야 한다.


data_mod<-str_replace_all(data,"이모티콘","") %>% str_replace_all("\\[오후","")%>% str_replace_all("\\[오전","")%>% str_replace_all("[ㄱ-ㅎ]+","")%>% #자음만 있는 내용들 다 지우기. 이걸 안 넣었더니 'ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ''ㅇㅇ' 등이 사용 빈도에서 압도적으로 1,2위를 다투고 있었다. 분석에서 그다지 의미있는 데이터는 아니어서 지웠다. str_replace_all("[0-9]+:[0-9]+\\]","")%>% #시간 데이터들 다 지우기. str_replace_all('\\[내 이름',"")%>% str_replace_all('\\[상대방이름',"")%>% str_replace_all("\\[|\\]","") %>% #카톡 텍스트 데이터에 있는 대괄호들을 지우기 위한. str_replace_all("[0-9]+","") %>% #숫자들 다 지우기. 내 카톡의 경우 숫자가 별로 없기도 했고 숫자를 넣어서 말한 내용이 그닥 의미가 없다고 판단했었다. str_replace_all("년|월|일","") %>% #연월일 값 다 지우기 str_replace_all("[가-힣]요","") %>% #txt데이터가 요일별로 나뉘어 있는데, 요일 값을 지우기 위해서 넣었다. str_replace_all('사진',"") %>% #사진이나 이모티콘은 txt데이터에서 '사진', '이모티콘'으로만 처리된다. as.character() 

head(data_mod)

 

구어체로 이루어지는 카카오톡 특성상 세 가지 문제가 발생한다. 
오탈자와 띄어쓰기, 그리고 줄임말.
정밀하게 분석하려면 오탈자도 전부 바꾸어주어야 한다. 
str_replace_all("겟","겠") 처럼 받침에 있는 오타라던지,
str_replace_all("하지말고","하지 말고") 처럼 띄어쓰기 문제가 있는 것들을 전부 바꾸어야 한다.
논문을 쓴다거나 전처리가 정밀해야 하는 경우라면 모든 경우의 수를 고려해야겠지만,
재미로 하는 분석이었으니 이 정도로 끝냈다.


줄임말의 경우는 이렇게 해결했다.

useNIADic() #분석을 위한 사전으로 NIADic을 불러온다. mergeUserDic(data.frame(c("내이름","상대방이름","아무말","개이득"),"ncn")) #사전에 단어를 추가하는 함수. '아무말''개이득' 이라는 두 단어를 사전에 'ncn'(명사)로 등재했다. #사람 이름을 형태소 분석하는 일이 없도록 이름도 사전에 추가한다. 내 이름은 형태소 분석에 명사+동사 처리가 돼서 아주 복잡해졌었다.  

카카오톡에서 많이 쓰는 줄임말이 있다면, 이런 방식으로 사전에 등재하면 된다.


noun <- sapply(data_mod, extractNoun, USE.NAMES=F) %>% unlist() #전처리한 데이터에서 extractNoun함수로 명사만 추출한다. noun2 <- Filter(function(x){nchar(x) >= 2}, noun) #2단어 이상의 명사만 추출 head(noun2) wordFreq <- table(noun2) noundta<-sort(wordFreq, decreasing=TRUE,200) #가장 많이 등장한 단어 순으로 200개 정렬 print(noundta) noundta<-noundta[-1] #앞서 str_replace 때문에 "" 공백이 16000개 정도가 생겨서 가장 많이 등장한 단어 1위다.  

그래서 1위 값만 제거했다.






#4. 워드클라우드 만들기

pal2<-brewer.pal(8,"Dark2") pal<-brewer.pal(12,'Set3') pal<-pal[-c(1:2)] 워드클라우드 색깔을 설정해 주는 명령어인 듯 한데, 어떤 의미인지 자세히는 모른다. 구글에서 검색하다가 찾은 명령어인데 그대로 넣어도 작동했다. png("wordcloud.png",width = 400, height = 300) wordcloud(names(noundta),freq=noundta,min.freq=20,random.order=T,rot.per=0,col=pal,encoding="UTF-8")  

dev.off()

png와 dev.off() 없이 Rstudio로 실행하면 Rstudio에 미리보기 형태로 창이 뜬다.


wordcloud() 내부 명령어


names(noundta)=noundta의 name값들로 워드클라우드를 만들라는 의미. 만약 names()없이 noundta만 넣으면, 해당 단어들의 빈출값(=숫자)로 워드클라우드가 만들어진다. freq= 빈출 정도를 말함. noundta의 숫자값이 곧 빈출 빈도이므로 noundta를 넣어준다. min.freq = 워드클라우드에 반영할 최소 빈출값이 얼마인지 설정할 수 있다. 너무 작을 경우 워드클라우드 결과값이 난잡할 정도로 많이 나오고, 너무 클 경우 워드클라우드가 제대로 이루어지지 않는다. head(noundta)로 단어의 빈출 빈도를 확인하고, 적절한 값으로 스스로 설정하자. random.order = 말 그대로 random order. T로 설정할 경우 단어를 무작위로 선별한다. 현재 데이터 전처리를 하면서 빈출 단어가 높은 순으로 정렬을 해뒀기 때문에 F로 설정할 경우 가장 빈출이 높은 단어부터 처리한다. (내 경우는 상위 빈출값들의 크기가 191, 180, 173 등 엇비슷하다 보니 워드클라우드가 제대로 만들어지지 않아서 random.order=T로 처리했다.) rot.per = 구글에서 찾아본 설명으로는 단어들 사이의 간격이라고 했던 것 같은데, 이 값이 0이 아니면 워드클라우드 내부 단어들이 중구난방으로 생기더라. 직접 값을 바꿔보면 이해가 될 듯) 

col= 색깔 설정. 위에서 정의한 pal을 그대로 썼다.



cf. 혹시 한글깨짐 현상이 나타난다면,

extrafont와 extrafontdb 라이브러리를 우선 설치해 준 다음


library(extrafont) library(extrafontdb) font_import(pattern="NanumGothic") 

wordcloud(names(noundta),freq=noundta,min.freq=35,random.order=T,rot.per=0,col=pal,family="NanumGothic")


라고 해 주면 해결된다. 나눔고딕 폰트로 워드클라우드를 만들 수 있다.


결과는 이렇게 나온다.
* 단어의 빈출 빈도의 범위에 따라 다른 색으로 표시되게끔 되어 있다. 

이를테면 노란색은 150번 이상, 초록색은 70번 이상 80번 미만. 단 어떤 색이 어떤 범주에 들어가는지는 현재까진 나도 모른다.


워드클라우드 결과값. '아무말' 이 1위를 차지했다. 애초에 여기 나열된 단어들에도 아무런 맥락이 없는 것으로 보아 분석은 잘 된 모양이다.




#5. qgraph로 상관성 분석하기.

명사끼리의 연관성 분석, 형용사끼리의 상관성 분석을 따로 진행했다.

(정확히는, 형용사만 있는 것이 아니라 감정이나 느낌을 나타내는 감정어들이다.)
원래 목적은 어떤 명사와 어떤 형용사가 자주 쓰이는지 확인하려 했지만, 너무 어려워서 포기했다.


tt<-paste(unlist(SimplePos22(data_mod))) head(tt,200) SimplePos22 명령어로 텍스트데이터를 명사, 형용사, 부사, 독립언 등등으로 나눈다. #명사만 가져오기 alldta<-str_match_all(tt,"[가-힣]+/[N][C]|[가-힣]+/[N][Q]+")%>% unlist() #형용사만 가져오기 alldta2<-str_match_all(tt,"[가-힣]+/[P][V]+|[가-힣]+/[P][X]+|[가-힣]+/[P][A]+|[가-힣]+/[M][A]+")%>%unlist() N<-str_replace_all(alldta,"/[N][C]","") %>% str_replace_all("/[N][Q]","") %>%unlist() #명사로 추출된 단어들의 분류표인 /NC, /NQ 등을 제거한다. PNM<-str_replace_all(alldta2,"/[P][V]","") %>% str_replace_all("/[P][A]","") %>% str_replace_all("/[M][A]","") %>% str_replace_all("/[P][X]","") %>% unlist() 

#마찬가지로 감정어로 분류한 단어들의 분류표들을 제거한다.



명사들 상관성분석




DtaCorpusNC<-Corpus(VectorSource(N)) myTdmNC<-TermDocumentMatrix(DtaCorpusNC,control = list(wordLengths=c(4,10),     removePunctuation=T,removeNumbers=T,weighting=weightBin)) Encoding(myTdmNC$dimnames$Terms)="UTF-8" #tm패키지에서 제공하는 Corpus를 통해 분류된 단어들의 행렬을 만든다. #Corpus의 원리는 현재 까먹었다. findFreqTerms(myTdmNC, lowfreq=10) #제대로 되었는지 확인 차원에서 입력했다. 제대로 작동했다면 extractnoun함수를 통해 얻은 결과값과 대충 비슷한 형태의 결과가 나온다. mtNC<-as.matrix(myTdmNC) #행렬(matrix)로 변환하는 게 상관성 분석의 핵심이다. mtrowNC<-rowSums(mtNC) mtNC.order<-mtrowNC[order(mtrowNC,decreasing=T)] freq.wordsNC<-sample(mtNC.order[mtNC.order>30],25) freq.wordsNC<-as.matrix(freq.wordsNC) freq.wordsNC 

co.matrix<-freq.wordsNC %*% t(freq.wordsNC)



co.matrix를 통해 나온 결과값이 바로
해당 단어가 다른 단어와 얼마나 많이 연결되어 있는지를 나타내는 값이다.


여기서 행렬의 대각선 값은 자기 자신의 값을 두 번 곱한 것인데, 의미있는 결과값이 아니므로 qgraph에서 반영하지 않을 것이다.


qgraph(co.matrix, labels=rownames(co.matrix), diag=FALSE, layout='spring', 

vsize=log(diag(co.matrix)*2))


qgraph로 나타낸 연관성분석. 어떤 단어가 어떤 단어와 주로 같이 쓰였는지 선의 굵기로 파악할 수 있다.




형용사(감정어) 상관성분석



명사와 같은 원리이고, 데이터 N을 PNM으로 바꿔서 그대로 실행하면 된다. 


DtaCorpusPNM<-Corpus(VectorSource(PNM)) myTdmPNM<-TermDocumentMatrix(DtaCorpusPNM,control = list(wordLengths=c(4,10),weighting=weightBin)) Encoding(myTdmPNM$dimnames$Terms)="UTF-8" findFreqTerms(myTdmPNM, lowfreq=10) mtPNM<-as.matrix(myTdmPNM) mtrowPNM<-rowSums(mtPNM) # mtPNM.order<-mtrowPNM[order(mtrowPNM,decreasing=T)] freq.wordsPNM<-sample(mtPNM.order[mtPNM.order>30],25) freq.wordsPNM<-as.matrix(freq.wordsPNM) 

co.matrix2<-freq.wordsPNM %*% t(freq.wordsPNM)



qgraph(co.matrix2, labels=rownames(co.matrix2), diag=FALSE, layout='spring', 

vsize=log(diag(co.matrix2)*2))


음, 명사보다는 뭐가 뭔지 파악하기가 좀 더 어렵다.


해놓고 보니,

글 도입부에서 거창하게 말했던 '말 속에 담겨 있는 무의식을 파악한다'는 취지가 무색하다. 

제대로 분석을 하기 위해서는 '명사와 감정언 사이의 상관성', '감정어의 긍정, 부정 분석' 등이 더 필요하지만, 

제한된 시간과 R 지식의 부족으로 더 진행하지는 못했다. 아쉬움이 남는 부분이다.

반응형