텍스트 마이닝 감정 분석 넥플릭스 [지금 우리 학교는]

     지금 우리 학교는은  전세계  Netflilx에서 매우 Hot 하다.   나도 처음 나오자 마자 정주행 했던 것이다.    역시  한국 좀비 영화가 재미가 있다.  이유는 앞의 이야기를 예측을 할 수 없기 때문이다.   R에서 텍스트 마이닝에서 감정 분석 하는 것이 있다.      

Nexflix 화면

사람들의 반응은 어떠 하였는가 ?    나는 이것 보고 나서,  유튜브나, 넷플릿스 다른 것이 재미 없었다.    


만약에 R4.2버전에서  KoNLP가 설치 되어 있지 않았다면 아래의 패이지를 참고해서 설치 한다. 


https://rdmkyg.blogspot.com/2022/06/r-42-windows-rjava-kolnp.html

이버 댓글을 수집하는 패키지인 "N2H4" 패키지로  Naver News 의  댓글을 데이터화 시켰다.  여기에서 댓글 자체가 영화 평과 같다고 생각 하였다. 

일단,  필요한 패키지는 모두 불러오자.
# 라이브러리 불러오기 

## 불필요 객체 지우기  
  rm(list = ls())

## 패키지 불러오기
  pkgs <- c(
    # Data munipulate packages   
    "dplyr",
    "rio",
    "stringr",
    "httr",
    "ggwordcloud",
    "readr",
    "readxl",

    # crawl Packages
    "N2H4",
        
    # text mining packages
    "tidytext",
    "ggwordcloud",
    "KoNLP",
    "tidyr",
    "textclean",
    
    # graphic packages
    "ggplot2"
  )

  # 패키지 동시에 적용
  sapply(pkgs, require, character.only = T)

KoNLP의  사전을 불러온다. 

  useNIADic()
## Backup was just finished!
## 1213109 words dictionary was built.

아래는 네이버,  뉴스기사의 댓글을 가져온 것이다.
    
# 지금 우리 학교는 데이터 url   
  url  = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=032&aid=0003126541"
  url1 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=052&aid=0001698211"
  url2 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=437&aid=0000288469"
  url3 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=020&aid=0003408896"
  url4 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=028&aid=0002577686"
  url5 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=052&aid=0001696962"
  url6 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=025&aid=0003170581"
  url7=  "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=079&aid=0003603645"
  url8 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=366&aid=0000791610"
  url9 = "https://news.naver.com/main/read.naver?mode=LSD&mid=shm&sid1=103&oid=214&aid=0001176617"
  
# 지금 우리 학교는 이라는 컨텐츠를 하나로 묶는다. 
  url_vt <-  c(url, url1, url2, url3, url4, url5, url6, url7, url8, url9)   
  rm(url, url1, url2, url3, url4, url5, url6, url7, url8, url9)
  
# 네이버 뉴스 댓글 가지고 오기   
  comment_get_fun <- function(x){

    comments <- getAllComment(url_vt[x]) %>% select(contents)
  
    return(list(comments))  
  }

  x = 1:length(url_vt)

# 뉴스 댓글 가져오는 함수 가져오기   
  mylist <- sapply(x, comment_get_fun)
raw_news_comment <-  tibble(do.call(rbind, mylist))

  head(raw_news_comment)
## # A tibble: 6 × 1
##   contents                                                                     
##   <chr>                                                                        
## 1 "우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재하기까지를 생각…
## 2 "짱개들은 좀비도이제 강시가 원조라고 우기겠군"                               
## 3 "도대체 댓글 24시간 내내 쓰는 놈들은 누구일까 ㅋㅋㅋㅋㅋㅋㅋㅋ"              
## 4 "하 🐕중국인들 넘쳐나네 ㅋㅋㅋㅋ 시진핑에 어머니 받칠 애들 ㅋㅋㅋㅋㅋㅋ"     
## 5 "짱OOO들 열심히 퍼다가 리뷰질 하드라.\n같쟎은 그 종자. 짱개"                 
## 6 "넷플 중국인 15억 뒤지는드라마 한편 만들면 전세계1위가능"

여기에서 뉴스기사를 가져올때,  가장 핵심인 함수는 "N2H4" 패키지의 getAllComment 라는 함수이며, 함수 안에 URL을 삽입하면, 댓글을 가져온다.

아래는 감정사전을 가져오는 것이다. 
  url = "https://drive.google.com/u/0/uc?id=14t6CqgzfNpJjU45W8HFmLBlHaI_FjOUw&export=download"
  GET(url, write_disk(tf <- tempfile(fileext = ".csv")))
  dic <- read_csv(tf) 
## Rows: 14854 Columns: 2
  rm(url,tf)

감정사전은  군산대학교 소프트웨어 융합 공학과에서 만든 'KNU 한국어 감성사전'을 이용하였다.  아래는 긍정 단어와,   부정단어가 있다.

# 긍정단어
  dic %>% 
    filter(polarity == 2) %>% 
    arrange(word)
## # A tibble: 2,602 × 2
##    word              polarity
##    <chr>                <dbl>
##  1 가능성이 늘어나다        2
##  2 가능성이 있다고          2
##  3 가능하다                 2
##  4 가볍고 상쾌하다          2
##  5 가볍고 상쾌한            2
##  6 가볍고 시원하게          2
##  7 가볍고 편안하게          2
##  8 가볍고 환하게            2
##  9 가운데에서 뛰어남        2
## 10 가장 거룩한              2
## # … with 2,592 more rows
# 부정 단어
  dic %>% 
    filter(polarity == -2) %>% 
    arrange(word) 
## # A tibble: 4,799 × 2
##    word            polarity
##    <chr>              <dbl>
##  1 가난                  -2
##  2 가난뱅이              -2
##  3 가난살이              -2
##  4 가난살이하다          -2
##  5 가난설음              -2
##  6 가난에                -2
##  7 가난에 쪼들려서       -2
##  8 가난하게              -2
##  9 가난하고              -2
## 10 가난하고 어렵다       -2
## # … with 4,789 more rows
댓글 데이터 베이스 전처리 한다.   일단 번호를 주고,  데이터 공백을 없애주고,  특수문자와 영문을 뺀 한글 2자리수 이상의 단어(word) 만 추려서,  감성 분석을 하였다. 


 댓글 데이터 전처리 하기   
  news_comment <- raw_news_comment %>% 
    mutate(id = row_number(),
           reply = str_squish(contents))
  
# 데이터 토콘화 
  word_comment <- news_comment %>% 
    unnest_tokens(input = reply,
                  output = word,
                  token = "words",
                  drop = F)%>% 
    filter(str_detect(word, "[가-힣]")) %>%   
    filter(str_count(word) >= 2)
  
  word_comment %>% 
    select(word, reply)
## # A tibble: 12,980 × 2
##    word           reply                                                        
##    <chr>          <chr>                                                        
##  1 우리네         우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  2 인생사가       우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  3 반영되어       우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  4 안타깝게       우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  5 봤네요         우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  6 현재의         우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  7 우리가         우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  8 존재하기까지를 우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
##  9 생각하여       우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
## 10 인류가         우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재…
## # … with 12,970 more rows

감정 점수를 부여는 댓글의 문장과 감성사전을 join(매칭) 하여,  점수를 부여 하였고,   2점 보다 높으면,  긍정인 글로 정리 하였고,  -2점 인 것은 부정으로 정리 하였다. 

 # 감정점수 부여
  word_comment <- word_comment %>% 
    left_join(dic, by ="word") %>% 
    mutate(polarity = ifelse(is.na(polarity), 0 , polarity))
  
  # 감정 분류 하기 
  word_comment <- word_comment %>% 
    mutate(sentiment = ifelse(polarity ==  2, "pos",
                              ifelse(polarity == -2, "neg", "neu")))

 word_comment %>% 
   count(sentiment)
## # A tibble: 3 × 2
##   sentiment     n
##   <chr>     <int>
## 1 neg         139
## 2 neu       12684
## 3 pos         157

 중립단어를 제외 하고,  가장 자주 사용되는 단어,  긍정 10개,  부정 10개 씩 정리하여 추출 하여, 그래프를 그렸다. 

 # 중립 단어를 제외하고, 긍정 단어와 부정 단어중 
 # 가장 자주 사용된 단어를 10개씩 추출
 top10_sentiment <- word_comment %>% 
   filter(sentiment != "neu") %>% 
   count(sentiment, word) %>% 
   group_by(sentiment) %>% 
   slice_max(n, n =10)
 
 
 top10_sentiment %>% 
   ggplot(aes(x = reorder(word,n),
              y = n, 
              fill = sentiment
   )) +
   geom_col() +
   coord_flip() +
   geom_text(aes(label = n), hjust = -0.3)+
   facet_wrap(~ sentiment, scales = "free") +
   scale_y_continuous(expand = expansion(mult = c(0.05, 0.15))) +
   labs(x = NULL) +
   theme(text = element_text(family="nanumgothic"))
지금 우리학교는 긍정인 단어와 부정인 단어


텍스트 마이닝에서 빠질수 없는 wordcloud를 그렸다.  너무, 솔직히, 진짜, 그냥 단어가 들어 갔다.   너무, 솔직히, 진짜의 부사어는 긍정이나, 부정어에서 충분히 같이 쓸 수 있는 단어이다. 


# 지금 우리 학교는의 wordcloud 그리기     
 word_comment %>% 
   count(word, sort = T) %>% 
 ggplot(aes(label = word,
            size =n,
            color = n)) +
   geom_text_wordcloud(seed = 1004) +
   scale_radius(limits = c(3, NA), # 최소, 최대 단어 빈다
                range = c(3, 30) #  최소 최대 글자 크기
   ) +
   scale_color_gradient(low = "#66aaf2",
                        high= "#004EA1") +
   theme_minimal()

지금 우리학교는 워드 클라우드


 주인공 이름 보다는  영화를 보고 느꼈던 이야기를 적었다,  오징어라는 글자도 간혹보인다.  이것과 오징어 게임과 일부 비교한 것이 보인다. 

# 댓글별 감정 점수 구하기 
  score_comment <- word_comment %>% 
   group_by(id, reply) %>% 
   summarise(score = sum(polarity)) %>% 
   ungroup()

 score_comment %>% 
   select(score, reply)
## # A tibble: 1,283 × 2
##    score reply                                                                  
##    <dbl> <chr>                                                                  
##  1    -1 우리네 인생사가 반영되어 안타깝게 봤네요. 현재의 우리가 존재하기까지를…
##  2     0 짱개들은 좀비도이제 강시가 원조라고 우기겠군                           
##  3     0 도대체 댓글 24시간 내내 쓰는 놈들은 누구일까 ㅋㅋㅋㅋㅋㅋㅋㅋ          
##  4     0 하 🐕중국인들 넘쳐나네 ㅋㅋㅋㅋ 시진핑에 어머니 받칠 애들 ㅋㅋㅋㅋㅋㅋ 
##  5     1 짱OOO들 열심히 퍼다가 리뷰질 하드라. 같쟎은 그 종자. 짱개              
##  6     0 넷플 중국인 15억 뒤지는드라마 한편 만들면 전세계1위가능                
##  7     0 엄청나게 뛰어제끼는 한국좀비. 너무정신사나워..                         
##  8     0 훠훠훠 역쉬 k방역 그리고 k학폭 입니다.                                 
##  9    -1 왜케 넷플릭스는 잔인한 장면과 피칠갑화면... 엽기적 내용, 싸이코 등장인…
## 10    -1 예고편 보는데 찬혁이 나와서 깜놀..                                     
## # … with 1,273 more rows

# 긍정 댓글 
  score_comment %>% 
    select(score, reply) %>% 
    arrange(-score)
## # A tibble: 1,283 × 2
##    score reply                                                                  
##    <dbl> <chr>                                                                  
##  1     6 "정말 재미있게 봤음. 오징어게임보다 더 재미 있게 봤다. 대한민국 드라마…
##  2     6 "이런거 그만봐요. 마인드컨트롤로 조종 되는거에요. 행복하고 유쾌하고 즐…
##  3     6 "김대중 대통령님 감사합니다 . . 수구꼴통들이 국가 부도 사태에 빠뜨려놓…
##  4     6 "DJ..창조적인 지식은 높은 부가가치를 창출한다. 우리나라는 높은 교육수… 
##  5     5 "탑스타 간판없이 무명으로 꾸린 영화가 대박난다는건 좋은 현상이다. 영화…
##  6     5 "예전에 보지않던 좀비물인데 코로나이후 새롭게 보임. 몰입도도 좋고 재미…
##  7     5 "좀비 영화의 새로운 연출은 좋다 . 일부 아닌것도 있지만 좋은것이 더 많… 
##  8     5 "재미있게 잘 봤어요 학생들 연기하느라 고생했어요 당신들 고생덕에 즐거… 
##  9     5 "한류 재도약의 근간은 민주주의 바탕이 있기때문에 가능하다.제작의 창의… 
## 10     4 "썩 재미있던데 웹툰 본 사람들이 일부 비교하면서 어쩌구저쩌구 많이 하더…
## # … with 1,273 more rows
# 부정 댓글
  score_comment %>% 
    select(score, reply) %>% 
    arrange(score)  
## # A tibble: 1,283 × 2
##    score reply                                                                  
##    <dbl> <chr>                                                                  
##  1    -8 좀비 너무보기에 혐오스럽다! ~~~학교라는곳을 좀비랑 연계해 굳이 이런 영…
##  2    -8 넷플 한국드라마 특징이 엽기적으로 잔인하고 더럽게 잔인하고 끝까지 잔인…
##  3    -5 좀비물은 인간형상을 극도로 왜곡하고 인간이 인간을 공격하고 죽이는 것이…
##  4    -5 이런 기사에 안좋은 글을쓰시는 분들은 스스로의 망상에 빠져있는 분들이거…
##  5    -5 ㅋㅋㅋㅋㅋ 오징어게임 지옥 우리학교는 전부 1위 하면서 어째 한국에 대한…
##  6    -5 공포감 확대와 강조는 이해하나 굳이 내장까지 먹고 아비규환속 살아 남은 …
##  7    -5 글쎄 ㅠㅠ 캐릭터들에 좀더 애정을 가지고 만들었으면 좋았겠다는 생각이들…
##  8    -5 반전이 없는게 반전임. 너무 많이, 너무 안타깝게 죽어서. 살아 남은 사람… 
##  9    -4 혐오스럽다 딱봐도 불건전                                               
## 10    -4 잼있지만 쓸데없는 로맨스나 신파는 좀 줄이고 8~10부작 정도가 적당. 그리…
## # … with 1,273 more rows

위에는 댓글별로 점수를 구하였고,  물론 점수가 높은 것은 긍정 댓글이고,  점수가 낮은 것은 부정 댓글이다.  

# 감정 점수 빈도 구하기 
  score_comment %>% 
    count(score) %>% 
    print(n =Inf)
## # A tibble: 13 × 2
##    score     n
##    <dbl> <int>
##  1    -8     2
##  2    -5     6
##  3    -4     5
##  4    -3    12
##  5    -2    81
##  6    -1    87
##  7     0   923
##  8     1    46
##  9     2    96
## 10     3     9
## 11     4     7
## 12     5     5
## 13     6     4
  # 감정 분류하고 막대 그래프 만들기 
  score_comment <- score_comment %>% 
    mutate(sentiment = ifelse(score >= 1, "pos",
                              ifelse(score <=-1, "neg", "neu") ))
  
  frequency_score <- score_comment %>% 
    count(sentiment) %>% 
    mutate(ratio = n/sum(n)*100)
  
  frequency_score  
## # A tibble: 3 × 3
##   sentiment     n ratio
##   <chr>     <int> <dbl>
## 1 neg         193  15.0
## 2 neu         923  71.9
## 3 pos         167  13.0
  # 감정분류 막대 그래프 그리기
  frequency_score %>% 
    ggplot(aes(x=sentiment, 
               y = n,
               fill = sentiment)) +
    geom_col() +
    geom_text(aes(label = n, vjust = -0.3)) +
    scale_x_discrete(limits = c("pos", "neu", "neg"))

지금 우리학교는 긍정, 부정, 중립의 비중


  위와 같이 긍정, 부정, 중립의 갯수를 막대 그래프로 볼 수 있다.   눈으로만 보았을 경우 중립의 비중이 높고,  긍정과, 부정의 비중은  적다.   구체적인 비중을 보고 싶으면 아래와 같이 그래프를 그리면 된다. 

 댓글 감정비율로 누적 막대 그래프 만들기 
  # frequency_score 에는 샘플 데이터 contry 처럼 x 축을 구성 할 변수가 없다
  # 이때는 임의의 값을 넣은 더비변수를 활용함 (dummy variable)
  frequency_score$dummy <- 0
  frequency_score
## # A tibble: 3 × 4
##   sentiment     n ratio dummy
##   <chr>     <int> <dbl> <dbl>
## 1 neg         193  15.0     0
## 2 neu         923  71.9     0
## 3 pos         167  13.0     0
  # 감정을 비율 막대 그래프로 그린다. 
  frequency_score %>% 
    ggplot(aes(x = dummy, 
               y =ratio,
               fill= sentiment)) +
    geom_col()+
    geom_text(aes(label = paste0(round(ratio,1), "%")),
              position = position_stack(vjust =0.5)) +
    theme(axis.text.x = element_blank(),
          axis.title = element_blank(),
          axis.ticks = element_blank())


지금 우리학교는 긍정, 중립, 부정 비중 [네이버 댓글 기준]


   데이터 점수를 보고 긍정적인 고빈도 단어와 부정적인 고빈도 단어를 보자  

 감정 변수별 주요 단어 보기 
# 로그 오즈비를 이용해 긍정 댓글 과 부정 댓글에 상대적으로 어떤
# 단어가 자주 사용되었는지 알아 보자. 
  
  comment <- score_comment %>% 
    unnest_tokens(input = reply,
                  output = word,
                  token =  "words",
                  drop = F) %>% 
    filter(str_detect(word, "[가-힣]")) %>%   
    filter(str_count(word) >=2)
  
# 감정 및 단어별 빈도 구하기ㅣ 
  frequency_word <- comment %>% 
    count(sentiment, word, sort = T)  

  
# 긍정 댓글 고빈도 단어
  frequency_word %>% 
    filter(sentiment == "pos")
## # A tibble: 1,988 × 3
##    sentiment word         n
##    <chr>     <chr>    <int>
##  1 pos       너무        23
##  2 pos       좀비        18
##  3 pos       재미        16
##  4 pos       드라마      11
##  5 pos       재미있게    11
##  6 pos       우리나라    10
##  7 pos       대한민국     9
##  8 pos       그냥         8
##  9 pos       좋고         8
## 10 pos       지금         8
## # … with 1,978 more rows
# 부정 댓글 고빈도 단어
  frequency_word %>% 
    filter(sentiment == "neg")
## # A tibble: 2,400 × 3
##    sentiment word       n
##    <chr>     <chr>  <int>
##  1 neg       너무      32
##  2 neg       그냥      23
##  3 neg       좀비      21
##  4 neg       진짜      14
##  5 neg       지옥      12
##  6 neg       노잼      10
##  7 neg       그리고     9
##  8 neg       연기       9
##  9 neg       지겹다     9
## 10 neg       하는       9
## # … with 2,390 more rows

부정적인 조건을 두고,  "너무"가 포함된 글을 보았다. 

score_comment %>% 
    filter(sentiment == "neg") %>% 
    filter(str_detect(reply,"너무"))
## # A tibble: 32 × 4
##       id reply                                                   score sentiment
##    <int> <chr>                                                   <dbl> <chr>    
##  1    63 밖에는 물고뜯고 피철철 난린데 좀비 피해온 교실에있는 …     -3 neg      
##  2    94 게임하는건 좋지만 학교에서 저리 위험한 행동은 자제해야…    -2 neg      
##  3   135 이게 왜 열풍인지 모르겠다 애들 연기 너무 못하고 특히 …     -3 neg      
##  4   170 오징어 만큼은 안됨. 극의 긴장감이 중간 중간 너무 떨어…     -3 neg      
##  5   196 배우들 연기 너무 못해서 아쉬움 처음에는 일반인 데리고 …    -1 neg      
##  6   253 쓸데없이 1818을 너무많이함 ㅜㅜ 연기가안됨                 -2 neg      
##  7   276 너무 지루하고 재미없어 빨리보기로 그냥 넘기고 말았다. …    -3 neg      
##  8   297 몰입도 떨어지는 지루한 스토리가 곳곳에 있던데 런닝타임…    -1 neg      
##  9   351 좀비 너무보기에 혐오스럽다! ~~~학교라는곳을 좀비랑 연…     -8 neg      
## 10   355 잔인하고 악함이 극에달한 저런 컨텐츠가 유행하는 현실이…    -1 neg      
## # … with 22 more rows

부정에는 잔인함과,  배우 연기 이야기, 그리고 지루한 스토리가 나왔다.   물론 나도 내용이 길어서 조금은 지루 하였다.   그래도 시청자 세계 1등을 현재(2022-02-06) 까지 기록 하고 있다.  

감정 변수별 주요 단어를  긍정이 부정 보다  강조를 많이 한것과,   부정이 긍정보다 많이 강조한  로그 오즈비를 계산하기 위해서,  단어별로,  wide form 으로 변환 하였다. 

# wid_form으로 변환 하여, 로그 오즈비를 구한다. 
  comment_wide <- frequency_word %>% 
    filter(sentiment != "neu") %>% 
    pivot_wider(names_from = sentiment,
                values_from = n,
                values_fill = list(n = 0) )
  
  comment_wide
## # A tibble: 4,016 × 3
##    word       neg   pos
##    <chr>    <int> <int>
##  1 너무        32    23
##  2 그냥        23     8
##  3 좀비        21    18
##  4 재미         2    16
##  5 진짜        14     7
##  6 지옥        12     0
##  7 드라마       7    11
##  8 재미있게     0    11
##  9 노잼        10     0
## 10 우리나라     2    10
## # … with 4,006 more rows

로그 오즈비를 구한 다음,  긍정 10개와  부정 10개를 추출 하여,  긍정과 부정을 비교 분석 하였다. 
지금 우리학교는 로그 오즈비 비교분석



"Do It 쉽게 배우는 R 텍스트 마이닝"의 코드를 많이 참조 하여, 작성 되었다.    
그런데  신문기사에 대한 평가는 아래와 같았다. 

이것은 댓글을 적지 않아도,  평을 낼 수 있다.   대부분이 좋다고 평하였다. 

영화 내용은 그렇다 치더라도,   기사 제목이 아래와 같아서 좋다고 평한 것 같다. 


'지금 우리 학교는', 미국 포함 전세계 1위···'오징어게임' 열풍 잇나




댓글 없음:

댓글 쓰기

css cheat sheet 클래스 선택자, margin(마진), display , center 조정 간단한 구성 요소

 앞에서는 html의 간단한 sheet를 소개 하였습니다.   html은  주로 골격을 나타나는 것이라, 디자인을 하는데는 css로 하여야 합니다.  아래 코드와 같이 css 관련 하여 매우 간단하게 코딩 하겠습니다.  body 부분의 css 코딩  ...