티스토리 뷰

(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 지식의 부족으로 더 진행하지는 못했다. 아쉬움이 남는 부분이다.

댓글
  • 프로필사진 ㅊㅇㅈ > useNIADic()
    Backup was just finished!
    983012 words dictionary was built.
    > mergeUserDic(data.frame(c("내이름","상대방이름","아무말","개이득"),"ncn"))
    4 words were added to dic_user.txt.
    경고메시지(들):
    'mergeUserDic' is deprecated.
    Use 'buidDictionary()' instead.
    See help("Deprecated")
    > noun <- sapply(data_mod, extractNoun, USE.NAMES=F) %>% unlist()
    50건 이상의 경고들을 발견되었습니다 (이들 중 처음 50건을 확인하기 위해서는 warnings()를 이용하시길 바랍니다).
    > noun2 <- Filter(function(x){nchar(x) >= 2}, noun)

    안녕하세요 저는 님과 다르게 단체채팅방을 분석하는데
    여기서부터 오류가 나네요 ㅜㅜ 어떻게 해야할까요?

    2017.12.05 23:00
  • 프로필사진 inspirit noun2<-Filter(function(x){nchar(x)>=2},noun) 여기서 오류가 나는 건가요? 오류 메세지가 뭔지 알 수 있을까요? 2017.12.06 01:25
  • 프로필사진 김현민 안녕하세요~ 올려주신 포스트 보고 단톡방을 분석해보고 있습니다^^
    마지막 qgraph에서 한글이 깨질땐 어떻게 하는게 좋을까요? 완전 초보인데 이렇게 따라해보니 어느정도 되는것같아 신기하네요!!
    2017.12.14 22:55
  • 프로필사진 inspirit 저도 이걸 예전에 했다 보니 기억이 잘 안나네요...;;; 한글 때문에 속 많이 썩었었는데.

    우선 qgraph 실행 전에
    Sys.setlocale("LC_CTYPE","ko_KR.UTF-8")
    Sys.setlocale("LC_TIME","ko_KR.UTF-8")
    Sys.setlocale("LC_COLLATE","ko_KR.UTF-8")
    Sys.setlocale("LC_MONETARY","ko_KR.UTF-8")
    Sys.setlocale("LC_MESSAGES","ko_KR.UTF-8")
    Sys.setlocale("LC_PAPER","ko_KR.UTF-8")
    Sys.setlocale("LC_MEASUREMENT","ko_KR.UTF-8")

    얘네 전부 실행한 다음 해보시구요

    그래도 안 되면
    library(extrafontdb)
    font_import(pattern="NanumGothic")
    로 한글 글꼴을 설치해보신 다음

    x11()
    pdf.options(family = "Korea1deb")
    qgraph(co.matrix, filetype='pdf',width=800,height=600, filename='qgraphNC',
    labels=rownames(co.matrix),
    diag=FALSE,
    layout='spring',
    vsize=log(diag(co.matrix)*2))
    dev.off()
    형태로 한번 해보시겠어요?

    이 방식은 Rstudio에 파일이 뜨는 게 아니라 pdf형태로 저장합니다. pdf형태로
    저장하는 방식이라서, 만약 그대로 한글이 깨진 채 pdf파일이 나올 경우 구글에 'R 그래프 한글 깨짐' 처럼 검색하시면 해결하실 수 있을 거에요.

    qgraph가 검색결과 제일 안 뜹니다 ㅜㅜ 그래도 저때 제가 어찌어찌 구글링으로 해결했었으니.. 답은 분명 있을 거예요!
    2017.12.16 16:46
  • 프로필사진 토기님a 카카오자료를엔길수있는자료를주세요 2017.12.23 20:10
  • 프로필사진 초보자 ㅠ 3번 텍스트분석을 위한 데이터전처리 탭에서요..
    noundata <- sort(wordFreq, decreasing = TRUE, 200) 이걸 입력하고 출력해봤는데 저는 전체 데이터가 출력되는데
    혹시 200개 출력되시나요?
    2018.05.14 09:38
  • 프로필사진 inspirit941 네 저는 200개 출력됩니다. 위에 쓴 코드는 포스팅 시점에서 전부 제가 동작하는 걸 확인하고 올린 내용입니다.
    전체 개수가 몇 개인지 모르겠네요.
    만약 전체 개수가 200개가 안 된다면 당연히 전체가 출력되겠죠..???
    2018.05.15 20:55 신고
  • 프로필사진 dong 안녕하세요. 정말 많은 공부가 되었습니다! 예전 게시물임에도 불구하고 혹시나 하는 마음에 질문을 드려봅니다!

    제 R에서도 plot까지 결과가 잘 나오는데요, plot 탭에서 export기능 클릭하여 사용했을때 node의 둘레가 우글우글하면서 화질이 깨집니다 ㅠㅠ 글쓴이님이 올리신 그림처럼 선명하지가 않습니다. 혹시 글쓴이님은 어떻게 plot을 저장하셨나요?

    갑사합니다 :)
    2018.08.20 23:16
  • 프로필사진 호호호호 혹시 분석하신 카카오톡 방 텍스트파일 좀 받을 수 잇을까여?.. 제꺼로 해보니 에러가 떠서 안되네요 형식이 다른건지... 2019.01.09 16:52
  • 프로필사진 It직종입니다 질문있습니다!
    예를들어 단톡방에 철수가 있다면
    철수가 가장 많이 쓴 단어들을 워드클라우드로 하려면 어케해야할까요??
    2019.08.26 18:53
댓글쓰기 폼