ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 종로대첩과 의미연결망
    잡다R 2020. 6. 19. 18:51
    종로 대첩과 의미연결망


     이번 시간에는 텍스트 마이닝 기법 중 하나인 의미연결망을 진행해보도록 하겠습니다. 의미연결망은 사회연결망 구조를 텍스트 데이터에 접목시킨 것으로 단어 사이의 관계나 의미를 도출하는 분석법이라고 합니다. 의미연결망 작업은 어떻게 진행되며 결과에 대한 해석은 어떤 식으로 이루어지는지, 직접 데이터를 가지고 두들겨보고 이야기해보도록 하겠습니다. 이번 데이터는 일명 종로대첩이죠, 지난 415총선 때 가장 뜨거운 격전지였던 종로구에서 두 후보자가 종로구 국회의원선거 후보자 토론회에서 만나 이야기 나눈 내용을 담고 있습니다.

     종로구 후보자 토론회에 대한 의미연결망 분석은 이미 서울대 모 교수님께서 진행하셨습니다. 그리고 그 내용이 jtbc 이규연의 스포트라이트라는 시사 프로그램에서 이미 다뤄졌습니다. 241회에 종로대첩을 다루며 의미연결망으로 후보자 발언을 살펴보는 대목이 나오는데요. 저희는 어떻게 할거냐면 R을 이용한 의미연결망을 그려보는데 최대한 정답에 가깝게, 그러니까 이규연의 스포트라이트에 나온 의미연결망을 최대한 따라가려고 노력하도록 하겠습니다. 함께 의미연결망을 그려보고, 결과에 대한 해석은 교수님의 인터뷰 내용을 빌려 살펴보는 시간을 가져보도록 하겠습니다.

     한편 굉장히 민감한 데이터가 될 수도 있겠습니다. 다만 미리 말씀드리는데 어떠한 정치적인 목적이 없음을 알립니다. 오롯이 의미연결망 학습을 위한 이유에서 글을 썼으니 불편한 부분이 있더라도 너그럽게 봐주세요 ^-^

     시작하기에 앞서 이 글 역시 대단히 대단하신 분의 도움을 많이 받았습니다. 감사드리며, 의미연결망에 대한 쉽고 친절하고 자세하고 정확한 정리 글은 아래 블로그를 참고해주세요.


     저희는 역시 빠르게 의미연결망을 진행해보도록 하겠습니다. 먼저, 데이터를 불러와주세요. 데이터는 종로구 국회의원선거 후보자 토론회에서 나온 후보자들의 발언입니다. 데이터는 후보자들의 머리말과 공통질문에 대한 답변, 서로 상대방 후보자들에게 묻는 질문 및 답변, 맺음말로 이루어져 있으며, 각자 총 23개의 발언이 있습니다. 이미 결과는 났지만, 토론회에서 호칭을 후보라고 하니까 저도 이낙연 후보, 황교안 후보라는 호칭을 사용하겠습니다. 먼저 데이터를 받아올게요.

    종로구후보자토론회.txt

    데이터 출처 : 종로구 선관위 종로구 국회의원선거 후보자 토론회 중 후보자 발언

    debate <- readr::read_lines("종로구후보자토론회.txt")
    debate[c(1,3)]
    ## [1] "황교안: 안녕하십니까? 국민 여러분. 그리고 종로구민 여러분. 지금 우한 코로나 사태로 인해서 정말 힘드시죠? 어려우실 줄로 압니다. 지금 대한민국은 정말 총체적 위기 상황에 빠져 있습니다. 경제는 최악의 평가를 받고 있습니다. 우리 건국 이래에 가장 어려운 상황이라는 평가를 받고 있습니다. 이번 총선은 이런 경제를 살리느냐 아니면 조국을 살리느냐 하는 평가가 이루어지는 선거라고 생각합니다. 국가 재건 수준의 대수술로 망가진 경제를 살려놓아야 합니다. 지난 3년간의 이 정권은 총체적 난국을 초래했습니다. 그럼에도 불구하고 자화자찬으로 일관하고 있습니다. 무책임한 정권입니다. 이러한 정권에게 다시 기회를 줄 수 없다고 생각합니다. 거짓말로 일관하면서 정의와 공정도 무너뜨린 제 2의 조국같은 세력들에게 다시 한번 국민을 대변할 기회를 준다는 것은 있을 수 없는 일이라고 생각합니다. 종로 주민 여러분들께서 정말 준엄한 심판을 해주실 것을 당부를 드립니다."                                                                                                                                    
    ## [2] "이낙연: 존경하는 국민 여러분. 사랑하는 종로구민 여러분. 얼마나 힘드십니까. 여러분께서 코로나19 등으로 얼마나 깊은 고통과 불편을 겪으시는지 잘 압니다. 그러한 여러분을 뵐 때마다 저는 가슴이 미어집니다. 특히, 저를 보시자마자 친정 큰오빠를 보는 것 같다며 울먹이신 삼청동 가게 주인을 저는 잊을 수가 없습니다. 그러나 저는 국민 여러분께 감사드립니다. 방역에 협조해주셔서 감사드립니다. 특히, 의료진 여러분, 헌신적으로 진찰과 치료에 임해주셔서 감사합니다. 약사 여러분 감사합니다. 무엇보다도 치료제 개발에 임해주시는 제약회사 임직원 여러분 고맙습니다. 그리고 나보다 더 어려운 이웃을 위해서 돼지저금통을 채워 이웃돕기에 나서겠다며 다시 문을 여신 동묘 시장 포장마차 여러분 고맙습니다. 식당 문을 닫지 않도록 하기 위해서 쥐꼬리만한 월급 쪼개서 더 많이 먹어드리겠다는 알바 노조의 과식투쟁, 눈물납니다. 그러한 국민 여러분이 계시기 때문에 저는 코로나 전쟁에서 우리 대한민국이 반드시 이겨내리라는 확실을 가지게 되었습니다. 이 고통의 계곡 국민 한분도 낙오되지 않도록 노력하겠습니다."

     데이터를 보면 이런 식으로 데이터가 들어가 있는 것을 확인할 수 있습니다. 황교안 후보와 이낙연 후보의 발언을 나눠서 담아볼게요.

    hwang <- NULL
    lee   <- NULL
    
    for (i in 1:length(debate)) {
      
      if (substring(debate[i], 1, 3) == "황교안") {
        hwang <- c(hwang, substring(debate[i], 4))
      } else if (substring(debate[i], 1, 3) == "이낙연") {
        lee <- c(lee, substring(debate[i], 4))
      }
      
    }

     나눠서 담는 이유는 당연하게도 후보자 개개인의 의미연결망을 진행해야 결과에 대한 해석이 가능하기 때문입니다. 따로 담았으면 이제 텍스트 데이터 전처리를 진행하도록 하겠습니다. stringr 라이브러리를 불러올게요.

    # 텍스트 데이터 전처리를 위한 라이브러리 장착
    library(stringr)

     이번 텍스트 데이터는 크게 더럽지 않습니다. 주석 등 데이터를 어지럽게 하는 텍스트가 사용되지 않아서 그렇습니다. 짧게 전처리를 진행해볼게요.

     먼저 두 후보 모두 23개의 발언을 했는데, 그때마다 데이터에는 콜론이 쓰였습니다. 예를 들면 ‘황교안:’, ‘이낙연 질문1:’ 이런 식으로 말이죠. 질문+숫자, 답변+숫자와 같은 표현과 콜론은 지워줘도 무방한 표현일 것입니다.

    # 먼저, 질문, 답변으로 된 부분 정리
    hwang <- str_replace_all(hwang, "질문[[:digit:]]", "")
    hwang <- str_replace_all(hwang, "답변[[:digit:]]", "")
    lee <- str_replace_all(lee, "질문[[:digit:]]", "")
    lee <- str_replace_all(lee, "답변[[:digit:]]", "")
    
    # 콜론 정리
    lee <- str_replace_all(lee, ":", "")
    hwang <- str_replace_all(hwang, ":", "")

     그리고 또 하나는 황교안 후보의 발언 중 이낙연 후보의 대답 한 마디가 소괄호 형태로 들어가 있는데, 그것도 제거해주기로 합니다.

    # 황교안 후보의 발언 중 이낙연 후보의 한 마디가 소괄호 형태로 들어가 있음
    check <- lapply(hwang, function(x) (
      
      str_extract_all(x, "\\([[:print:]]{1,10}\\)")
      
    ))
    
    table(unlist(check))
    ## 
    ## (창신, 숭인지역) 
    ##                1
    hwang <- str_replace_all(hwang, "\\([[:print:]]{1,10}\\)", "")

     이제 전처리 작업 중 일어날 수 있었던 2회 이상의 공백을 tm 라이브러리의 stripWhitespace() 함수로 지워주도록 합니다.

    # Whitespace 제거
    library(tm)
    lee <- stripWhitespace(lee)
    hwang <- stripWhitespace(hwang)

     이러면 데이터 전처리가 끝났습니다. 전처리가 이렇게 짧다니. 정말 좋네요 ㅎ.ㅎ 하나 말씀드릴 건 이번 글에 KoSpacing은 사용하지 않도록 하겠습니다. 글이 길어지는 걸 피하기 위함도 있구요, 제가 직접 타이핑한 데이터이기 때문에 저는 저를 믿어보도록 할게요ㅋㅋ 저를 못 믿으시는 (나쁜)분들은 KoSpacing 라이브러리를 알려드린 지난 글을 참고하셔서 KoSpacing도 함께 적용해보아요ㅠㅠ

     그럼 이제 문단으로 존재하는 각 발언들을 문장별로 쪼개겠습니다. 문장별로 쪼개는 이유는 단어의 관계를 더 잘 파악하기 위해서 입니다. 같은 문단에서 쓰인 단어보다 같은 문장에서 쓰인 단어들의 관계를 파악하는 것이 훨씬 더 밀접한 관계가 있다고 간주할 수 있겠죠? 예를 들어 다음과 같은 두 문장이 존재합니다.

     ‘원숭이는 바나나를 좋아한다. 코끼리는 과자를 좋아한다.’

     두 문장을 나누지 않고 동시에 보면 원숭이, 바나나, 코끼리, 과자라는 단어들은 서로 같은 비중의 관계로 연결되어 있다고 할 것입니다. 하지만 저희가 원하는 건 그게 아니라 원숭이와 바나나의 관계가 높고, 다른 한편으로 코끼리와 과자의 관계가 높은 결과일 것입니다. 이렇게 더 자세하게 밀접한 관계를 따지기 위해 두 후보의 발언을 다시 문장으로 담아주도록 할게요.

     문단에서 문장은 어떻게 쪼개는 방법은 KoSpacing 설명 글에서 언급한 방식을 이용하겠습니다. 마침표를 기준으로 문장을 나누는 방법이죠. 그런데 온점을 마침표가 아닌 소수점 등으로 사용한 경우 문제가 될 수 있습니다. 이런 경우가 있는지 한번 살펴보겠습니다.

    # 이낙연 후보
    check <- lapply(lee, function(x) (
      
      str_extract_all(x, "[[:print:]]{1,10}[[:digit:]]{1,}\\.[[:digit:]]{1,}[[:print:]]{1,10}")
      
    ))
    
    table(unlist(check))
    ## 
    ## 다 써서 출산율을 2.4명까지 끌어올린 것 종입니다. 세종은 1.8명을 넘어서고 있습 
    ##                                      1                                      1
    #황교안 후보
    check <- lapply(hwang, function(x) (
      
      str_extract_all(x, "[[:print:]]{1,10}[[:digit:]]{1,}\\.[[:digit:]]{1,}[[:print:]]{1,10}")
      
    ))
    
    table(unlist(check))
    ## 
    ##    자하문로 지하에 1.2km 구간의 대형   한국의 출산율은 0.92%, 지난해 통계입 
    ##                                     1                                     1

     저런. 이낙연 후보 발언에서는 2.4명, 1.8명, 황교안 후보의 발언에서는 1.2km, 0.92%라는 소수점 표현이 있습니다. 합치면 총 4개나 있네요. 문제가 될 수 있으므로 이 두 표현들을 치환해주기로 합니다. 치환은 임의로 아무거나 해주셔도 좋지만, 저는 가장 안전한 문자로 치환하도록 하겠습니다. 바로 콜론이죠. 콜론은 앞서 한번 일괄삭제를 거쳤기 때문에 안전한 치환문자일 것입니다.

    lee <- str_replace_all(lee, "2.4", "2:4")
    lee <- str_replace_all(lee, "1.8", "1:8")
    hwang <- str_replace_all(hwang, "1.2", "1:2")
    hwang <- str_replace_all(hwang, "0.92", "0:92")

     이제 문장을 나눠담아 봅니다. 마침표를 기준으로 문장을 구분하는 방법은 다음과 같습니다. 마침표를 제거하면서 담았으니 다시 써주는 작업까지 포함했습니다.

    lee_speak <- NULL
    
    for (i in 1:length(lee)) {
      split.txt <- strsplit(lee[i], split = '\\.')
      
      for (j in 1:length(split.txt[[1]])) {
        answkd <- paste(split.txt[[1]][j], '.', sep = '')
        if (substring(answkd, 1, 1) == ' ') {
          answkd <- substring(answkd, 2)
        }
        lee_speak <- c(lee_speak, answkd)
      }
    }

     똑같이 황교안 후보의 발언도 문장별로 나눠보아요.

    hwang_speak <- NULL
    
    for (i in 1:length(hwang)) {
      split.txt <- strsplit(hwang[i], split = '\\.')
      
      for (j in 1:length(split.txt[[1]])) {
        answkd <- paste(split.txt[[1]][j], '.', sep = '')
        if (substring(answkd, 1, 1) == ' ') {
          answkd <- substring(answkd, 2)
        }
        hwang_speak <- c(hwang_speak, answkd)
      }
    }

     자 그럼 제대로 들어갔는지 확인해보도록 합니다.

    lee_speak %>% head(4)
    ## [1] "존경하는 국민 여러분."                                                     
    ## [2] "사랑하는 종로구민 여러분."                                                 
    ## [3] "얼마나 힘드십니까."                                                        
    ## [4] "여러분께서 코로나19 등으로 얼마나 깊은 고통과 불편을 겪으시는지 잘 압니다."
    hwang_speak %>% head(4)
    ## [1] "안녕하십니까? 국민 여러분."                                         
    ## [2] "그리고 종로구민 여러분."                                            
    ## [3] "지금 우한 코로나 사태로 인해서 정말 힘드시죠? 어려우실 줄로 압니다."
    ## [4] "지금 대한민국은 정말 총체적 위기 상황에 빠져 있습니다."

     잘 들어가 있네요 좋습니다. 그러면 이제 KoNLP 라이브러리를 통해서 명사 단어를 빼낼 수 있도록 작업하겠습니다. KoNLP를 부르고 ‘종로’, ‘코로나’ 등의 단어를 세종 단어사전에 추가해주도록 합니다. 아래와 같이 입력하면 원하는 단어를 명사로 인식할 수 있어요.

    # 명사 추출
    library(KoNLP)
    user_dictionary <- data.frame(term = c("종로", "한국", "황후보", "이후보", "문재인", "더불어민주당", "미래통합당", "드라이브스루", "코로나", "거리두기", "긴급재난지원금", "3권분립", "규정", "숲공원"), tag = 'ncn')
    buildDictionary(ext_dic = 'sejong', user_dic = user_dictionary, replace_usr_dic = F)
    ## 371525 words dictionary was built.

     다음으로 롱포맷을 위한 두 라이브러리를 불러옵니다.

    library(tidyverse)
    library(reshape2)

     데이터에 '코로나19', '3권분립' 등 숫자와 함께 의미를 갖는 단어가 존재하기 때문에, 단어를 뽑아낼 때 한글, 숫자+한글, 한글+숫자의 표현도 카운트할 수 있게 정규표현을 써주겠습니다. [0-9가-힣-0-9] 표현이 될 수 있겠네요. 반드시 문자가 포함되기 때문에 숫자만 명사로 카운트될거라는 우려는 필요없을 거에요. 그럼 먼저 이낙연 후보의 발언을 가지고 품사 분석과 의미연결망을 진행하겠습니다.

    # 이낙연 후보자의 단어
    lee_n <- SimplePos09(lee_speak) %>% 
      melt() %>% 
      as_tibble() %>% 
      mutate(noun = str_match(value, '([0-9가-힣0-9]+)/N')[,2]) 
    
    lee_n %>% head
    ## # A tibble: 6 x 4
    ##   value            L2          L1 noun    
    ##   <fct>            <chr>    <int> <chr>   
    ## 1 존경/N+하/X+는/E 존경하는     1 존경    
    ## 2 국민/N           국민         1 국민    
    ## 3 여러분/N         여러분       1 여러분  
    ## 4 ./S              .            1 <NA>    
    ## 5 사랑/N+하/X+는/E 사랑하는     2 사랑    
    ## 6 종로구민/N       종로구민     2 종로구민

     다음의 데이터 프레임에서 value는 KoNLP의 SimplePos09() 함수가 열심히 품사분석을 진행한 결과이구요. L2는 분석에 쓰인 어절의 원문 그대로를, L1은 몇 번째 문장인지, noun은 명사만 뽑아냈을 때의 결과입니다. 얼추 잘 뽑아냈지만 문제가 있어요. 코로나를 명사 단어로 추가했는데도 코로나가 단독으로 쓰이면 인식하지 못합니다.

    lee_n %>% 
      filter(grepl('코로나', L2))
    ## # A tibble: 11 x 4
    ##    value           L2            L1 noun      
    ##    <fct>           <chr>      <int> <chr>     
    ##  1 코로나19/N      코로나19       4 코로나19  
    ##  2 코/N+로나/J     코로나        14 코        
    ##  3 코로나19/N      코로나19      16 코로나19  
    ##  4 코로나19가/N    코로나19가    22 코로나19가
    ##  5 코로나/N+의/J   코로나의      27 코로나    
    ##  6 코로나19/N+의/J 코로나19의    28 코로나19  
    ##  7 코로나19로/N    코로나19로    31 코로나19로
    ##  8 코로나19로/N    코로나19로    36 코로나19로
    ##  9 코/N+로나/J     코로나        45 코        
    ## 10 코로나/N+로/J   코로나로      52 코로나    
    ## 11 코/N+로나/J     코로나        52 코

     또한 ’코로나19’라는 표현과 겹치면서 ’코로나’라는 명사의 카운트가 잘 들어가지 못하는 모습입니다. 이낙연 후보의 발언에서 코로나는 총 11번 쓰였지만, 카운트는 2개밖에 못했습니다.

    check <- lapply(lee, function(x) (
      
      str_extract_all(x, "코로나")
      
    ))
    
    table(unlist(check))
    ## 
    ## 코로나 
    ##     11
    lee_n %>% 
      filter(noun == '코로나') %>% 
      count(n = n())
    ## # A tibble: 1 x 2
    ##       n    nn
    ##   <int> <int>
    ## 1     2     2

     이처럼 실제 빈도와 카운트에서 차이가 나타나는 문제를 해결해보겠습니다. 아울러 비슷한 문제가 나타나는 다른 단어들도 함께 문제 해결을 하겠습니다. 단어 정리는 빈도와 카운트의 차이가 큰 것 위주이고, 이규연의 스포트라이트에 나온 의미연결망 키워드를 중심으로 진행했습니다.

    for (i in 1:nrow(lee_n)) {
      # '코로나%' 표현을 '코로나'로 통일
      if (grepl('코로나', lee_n$L2[i])) {
        lee_n$noun[i] <- '코로나'
        # '여성'과 '여성들'의 표현이 같다고 판단
      } else if (grepl('여성들', lee_n$L2[i])) {
        lee_n$noun[i] <- '여성'
        # '안정'으로 시작하는 단어를 찾기 위한 정규표현 '^'
      } else if (grepl('^안정', lee_n$L2[i])) {
        lee_n$noun[i] <- '안정'
        # '국민'과 '국민들'의 표현이 같다고 판단
      } else if (grepl('국민들', lee_n$L2[i])) {
        lee_n$noun[i] <- '국민'
      } else if (grepl('성공', lee_n$L2[i])) {
        lee_n$noun[i] <- '성공'
      } else if (grepl('감사', lee_n$L2[i])) {
        lee_n$noun[i] <- '감사'
      } else if (grepl('청년', lee_n$L2[i])) {
        lee_n$noun[i] <- '청년'
      }
    }

     분산된 표현을 정리했으니 필요없는 것은 쳐내보겠습니다. 한 글자 단어들은 제거해도 될만한 표현밖에 없다고 판단해서 제거하고, 빈도가 4 이상인 표현을 가져오겠습니다. 그 중에서도 필요없는 표현은 제거해서 한데 담아줍니다.

    lee_over4 <- lee_n %>% 
      count(noun, sort=TRUE) %>% 
      filter(str_length(noun) >= 2 &
               !(noun %in% c('그것', '때문', '이것', '저희', '특히', '거기')) &
               n >= 4) 

     그러면 빈도가 4회 이상이고, 정리된 단어들만 따로 뽑아줄게요. 아래와 같이 column은 select로 noun과 L1만 뽑아주면 됩니다.

    ga <- SimplePos09(lee_speak) %>% 
      melt() %>% 
      as_tibble() %>% 
      mutate(noun = str_match(value, '([0-9가-힣0-9]+)/N')[,2]) %>% 
      na.omit() %>% 
      filter(noun %in% lee_over4$noun) %>%
      select(4, 3)
    
    ga %>% head
    ## # A tibble: 6 x 2
    ##   noun      L1
    ##   <chr>  <int>
    ## 1 국민       1
    ## 2 여러분     1
    ## 3 여러분     2
    ## 4 여러분     4
    ## 5 고통       4
    ## 6 여러분     5

     이러면 의미연결망을 위한 단어 정리는 완료되었습니다. 아래와 같이 입력해서 이분 그래프를 만들어보아요.

    # 이분 그래프 만들기
    library(igraph)
    # 데이터 프레임을 그래프 형식으로 바꿈
    LNY <- graph_from_data_frame(ga)
    # 노드 특성(문장)으로 type을 구분
    V(LNY)$type <- bipartite_mapping(LNY)$type
    # 근접행렬과 전치행렬을 곱해 단어 사이의 관계를 계산
    lees_words <- as_incidence_matrix(LNY) %*% t(as_incidence_matrix(LNY))
    # 행렬의 주대각선을 0으로 치환
    diag(lees_words) <- 0
    # 행렬의 점들을 선으로 잇는 인접행렬로 변환
    LNY <- graph_from_adjacency_matrix(lees_words)

     이 작업은 단어끼리 어느 정도의 관계가 있는지 볼 수 있는 행렬을 만든 것입니다. 자세한 내용은 위에서 드린 kini님의 글, 최대한 친절하게 쓴 R로 사회연결망 분석하기를 참고해주세요. 우리는 아주 간략히 살펴보면 다음과 같습니다.

    lees_words[1:6, 1:6]
    ##        국민 여러분 고통 감사 치료제 우리
    ## 국민      0     14    2    0      0    5
    ## 여러분   14      0    2    2      1    3
    ## 고통      2      2    0    0      0    0
    ## 감사      0      2    0    0      0    0
    ## 치료제    0      1    0    0      0    2
    ## 우리      5      3    0    0      2    0

     저희가 만든 행렬의 일부분이고, 읽는 방법은 대충 감이 오실거라고 믿어요. 살짝 엿보니까 단어 ’국민’과 ’여러분’의 관계가 가장 높습니다. 그도 그럴 것이 발언에서 두 단어를 합쳐 ’국민 여러분’이라고 말하는 표현이 많기 때문입니다. 두 단어의 관계는 어떠한 관계보다 가장 밀접합니다. 이런 느낌으로다가 만들어진 행렬을 그래프로 그려보도록 합니다. 그림을 그리기 위한 tidygraph와 ggraph 라이브러리를 받아와 주시고요

    library(tidygraph)
    library(ggraph)

     아래와 같이 명령어를 입력해주시면 저희가 바라던 의미연결망 그래프가 완성됩니다.

    LNY %>%
      as_tbl_graph(directed = FALSE) %>%
      activate(nodes) %>%
      # 고유벡터 중심성 구하기
      mutate(eigen = centrality_eigen(),
             group = group_infomap()) %>%
      # 레이아웃을 mds로
      ggraph(layout = 'mds') +
      # 노드를 선으로 연결결
      geom_edge_link(color='grey50', alpha = .1) +
      # 연결선 사이즈를 최소 0.2에서 최대 1.5로 적용
      scale_edge_width(range = c(0.2, 1.5)) +
      # 노드에 점찍기
      geom_node_point(aes(size = eigen), 
                      color = 'steelblue') +
      # 점 사이즈를 최소 2에서 최대 10으로 적용
      scale_size(range = c(2,10)) +
      geom_node_text(aes(label = name), 
                     size = 4, 
                     fontface = 'bold',
                     repel = TRUE) +
      theme_graph() +
      theme(legend.position = 'none')

     이쁘게 그림을 뽑았으니 여기에 대한 해석을 들어보겠습니다. 이규연의 스포트라이트에서 이낙연 후보의 의미연결망을 다음과 같이 설명합니다.


    이낙연 후보의 의미연결망을 보면 '현안에 대한 구체적인 대상과 해결 방식을 제시하는' 모습이다. 한편 경제를 이야기할 때 비교적 '정부의 추경 이야기를 많이' 하는 모습이며, '황교안 후보와는 다른 시각'을 가지고 있다는 것을 확인 가능하다.


     이러한 설명에 동의하시나요? 평창동, 송현동 등 구체적인 지명이 눈에 보이는 게 꼭 설명을 뒷받침하는 것 같네요.


     그러면 이제 황교안 후보의 의미연결망도 살펴보도록 합시다. 각 명령문에 대한 설명은 앞서 말씀드렸으니, 설명은 최대한 스킵하고 갈게요.

    hwang_n <- SimplePos09(hwang_speak) %>% 
      melt() %>% 
      as_tibble() %>% 
      mutate(noun = str_match(value, '([0-9가-힣0-9]+)/N')[,2]) 

     황교안 후보의 단어는 분명 이낙연 후보의 것과는 다를 것입니다. 이번에도 어떤 단어들이 부족한지 살펴보고 정리했습니다.

    for (i in 1:nrow(hwang_n)) {
      if (grepl('코로나', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '코로나'
      } else if (grepl('선거라고', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '선거'
      } else if (grepl('국민들', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '국민'
      } else if (grepl('청년', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '청년'
      } else if (grepl('주민', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '주민'
      } else if (grepl('위기', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '위기'
        # '일'과 '일자리'는 다르므로, '일하다'는 표현으로 쓰인 단어를 '일'로 통일
      } else if (grepl('일할', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '일'
      } else if (grepl('중요한', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '중요'
      } else if (grepl('활용', hwang_n$L2[i])) {
        hwang_n$noun[i] <- '활용'
      }
    }

     마찬가지로 한 글자 단어들은 제거해도 될만한 표현밖에 없다고 판단하고, 빈도가 4회 이상인 단어 중 ‘때문’, ‘이것’, ’저희’를 제거합니다.

    hwang_over4 <- hwang_n %>% 
      count(noun, sort=TRUE) %>% 
      filter(str_length(noun) >= 2 &
               !(noun %in% c('때문', '이것', '저희')) &
               n >= 4) 
    
    na <- SimplePos09(hwang_speak) %>% 
      melt() %>% 
      as_tibble() %>% 
      mutate(noun = str_match(value, '([0-9가-힣0-9]+)/N')[,2]) %>% 
      na.omit() %>% 
      filter(noun %in% hwang_over4$noun) %>%
      select(4, 3)

     이분 그래프를 만듭니다.

    HGA <- graph_from_data_frame(na)
    
    V(HGA)$type <- bipartite_mapping(HGA)$type
    hwangs_words <- as_incidence_matrix(HGA) %*% t(as_incidence_matrix(HGA))
    diag(hwangs_words) <- 0
    HGA <- graph_from_adjacency_matrix(hwangs_words)

     이제 황교안 후보의 의미연결망을 그려보겠습니다.

    HGA %>%
      as_tbl_graph(directed = FALSE) %>%
      activate(nodes) %>%
      mutate(eigen = centrality_eigen(),
             group = group_infomap()) %>%
      ggraph(layout = 'mds') +
      geom_edge_link(color='grey50', alpha = .1) +
      scale_edge_width(range=c(0.2, 1.5)) +
      geom_node_point(aes(size = eigen), 
                      color = 'violet') +
      scale_size(range = c(2,10)) +
      geom_node_text(aes(label = name), 
                     size = 4, 
                     fontface = 'bold',
                     repel = TRUE) +
      theme_graph() +
      theme(legend.position = 'none')

     황교안 후보의 의미연결망에 대한 해석은 다음과 같이 나타났습니다.


    ‘코로나도 있지만 … 중요도가 크지는 않’아 보인다. 또한 경제 이야기로 ’정부의 경제 실책에 대한 것들을 집중적으로 이야기하면서, 일자리를 어떻게 확보할 것인가’를 이야기한다. 한편, 황교안 후보의 단어들이 ’뭉쳐져 있는’데, ’핵심 단어들만 얘기하고 그 이상 아이디어가 뻗어나가지 않는다는 것으로’ 보이며 ‘전략적으로 선택되어진 단어들만 던지고, 깊이 있는 이야기들을 던지지 못하는 것 같다.’


     설명에 동의하시나요? 여러분의 생각은 어떠신지 궁금합니다. 위 설명에 대해서는 결과론적인 원색한 비판이라고 생각하실지 모르겠습니다. 텍스트 데이터 분석 같은 경우는 예측보다 결과에 대한 해석이 주 목적이고, 사람에 따라 다른 해석이 존재하니까요. 저희는 서울대 모 교수님의 전문성을 믿고 '이렇게 해석하면 되는구나' 알아가는 것으로 마무리하도록 해요.

     지금까지 종로구 후보자 토론회 발언을 중심으로 의미연결망을 진행하고, 그에 대한 해석도 들어봤습니다. 앞서 말씀드린 대로 이규연의 스포트라이트에 나온 의미연결망을 최대한 따라가려고 노력한 글이다보니, 저희가 그린 의미연결망과 프로그램의 의미연결망 간에 차이가 있는 점 이해해주세요. 끝으로 가장 중요한 이 글의 목적이 의미연결망 분석법에 대한 학습 외에 다른 것은 없음을 다시 말씀드리며, 여기서 글 마치겠습니다. 

    감사합니다.

    :)


Designed by Tistory.