사전기반 감정분석이란 텍스트의 감정 형태는 사용된 표현(어휘, 어구, 문형, 축약어, 이모티콘)들의 합이라는 전제하에 감정사전을 이용하는 방법이라 할 수 있습니다. 따라서, 하나의 텍스트가 어떠한 감정을 표현하는지 분석하기 위해서 각각의 표현을 감정어휘 사전에 따라 분류하고 출현 빈도수를 합하는 과정을 거치게 됩니다.
사전기반 감정분석
사전기반 감정분석은 감정사전에 등재되어있는 표현 분류에 따라 우리가 분석하고자 하는 텍스트에 출현하는 표현 찾아 분류하는 것으로 시작합니다. 그렇다면, 감정 표현이 분류되어있는 사전을 어떻게 이용할 수 있을까요. 가령, 긍정의 의미 또는 부정의 의미를 뜻하는 표현들이 정리되어있는 사전들도 그 종류와 쓰임이 다양합니다. 어떠한 사전을 어떻게 이용하느냐가 사전기반 감정분석에서 중요한 지점입니다. R에서 이용할 수 있는 감정사전도 다양합니다. 하지만, 본 강의에서는 군산대학교 소프트웨어융합공학과에서 만든 “KNU 한국어 감성사전”을 이용해 감정분석을 하는 방법을 살펴보겠습니다. “KNU 한국어 감성사전”은 본 수업의 e-class 강의 콘텐츠에서 다운로드 가능합니다. “KNU 한국어 감성사전” 깃허브 출처: https://github.com/park1200656/KnuSentiLex
knu_sentiment_lexicon.csv
감성사전의 구조는 감정 단어 word
와 감정의 강도를 표현한 polarity
로 구성되어 있습니다.
사전기반 감정분석은 텍스트의 각 표현들의 감정 상태를 표시하는 것부터 시작합니다. 그리고 앞서 말했듯, 감정사전의 표현들은 감정상태에 따라 분류되어 있죠. 예를 들어, 힘찬
그리고 희망적
은 긍정적 감정 표현으로 분류되어 있고, 힘들여
와 희망이 없는
은 부정적 감정 표현으로 분류되어 있죠. 즉, 감정사전의 표현은 감정 상태와 쌍을 이루고 있습니다. 이러한 표현과 감정의 쌍으로 이루어진 감정사전을 우리가 분석하고자 하는 텍스트를 포함한 데이터셋과 결합해주는 방식으로 감정분석을 진행할 겁니다.
library(tidyverse)
## -- Attaching packages --------------------------------------- tidyverse 1.3.0 --
## √ ggplot2 3.3.3 √ purrr 0.3.4
## √ tibble 3.1.0 √ dplyr 1.0.4
## √ tidyr 1.1.2 √ stringr 1.4.0
## √ readr 1.3.1 √ forcats 0.5.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
dic <- read_csv("knu_sentiment_lexicon.csv")
## Parsed with column specification:
## cols(
## word = col_character(),
## polarity = col_double()
## )
dic
## # A tibble: 14,854 x 2
## word polarity
## <chr> <dbl>
## 1 ㅡㅡ -1
## 2 ㅠㅠ -1
## 3 ㅠ_ㅠ -1
## 4 ㅠ -1
## 5 ㅜㅡ -1
## 6 ㅜㅜ -1
## 7 ㅜ_ㅜ -1
## 8 ㅜ.ㅜ -1
## 9 ㅜ -1
## 10 ㅗ -1
## # ... with 14,844 more rows
“KNU 감정사전”은 14,854 행과 2개의 열로 이루어진 데이터 프레임의 형식을 가지고 있습니다. 첫번째 열은 “word”인데, 즉 사전에 수록된 14,854 표현들이 열거되어 있습니다. 두번째 열은 “polarity”로서 각 표현의 감정 점수가 규정되어 있죠. 가령 “힘찬 기운이” 라는 표현은 “2” 즉 긍정적 감정으로 “힘이 없음을”이라는 표현은 “-1” 즉, 부정적 감정으로 분류되어 있는 것을 볼 수 있습니다.
# 매우 긍정 단어 (+2)
dic %>%
filter(polarity == 2) %>%
arrange(word)
## # A tibble: 2,602 x 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
# 긍정 단어 (+1)
dic %>%
filter(polarity == 1) %>%
arrange(word)
## # A tibble: 2,269 x 2
## word polarity
## <chr> <dbl>
## 1 "(-;" 1
## 2 "(^-^)" 1
## 3 "(^^)" 1
## 4 "(^^*" 1
## 5 "(^_^)" 1
## 6 "(^o^)" 1
## 7 "*^^*" 1
## 8 "/^o^\\" 1
## 9 ":'-(" 1
## 10 ":-(" 1
## # ... with 2,259 more rows
# 중립 단어 (0)
dic %>%
filter(polarity == 0) %>%
arrange(word)
## # A tibble: 154 x 2
## word polarity
## <chr> <dbl>
## 1 :p 0
## 2 8-) 0
## 3 B-) 0
## 4 가까스로 0
## 5 가라앉다 0
## 6 가라앉지 않은 0
## 7 가르침을 받아 0
## 8 가리지 않고 0
## 9 감싸고 달래다 0
## 10 강구하다 0
## # ... with 144 more rows
# 부정 단어 (-1)
dic %>%
filter(polarity == -1) %>%
arrange(word)
## # A tibble: 5,030 x 2
## word polarity
## <chr> <dbl>
## 1 -_-^ -1
## 2 (-_-) -1
## 3 (;_;) -1
## 4 (^_^; -1
## 5 (T_T) -1
## 6 (ㅡㅡ) -1
## 7 )-: -1
## 8 :-D -1
## 9 :-P -1
## 10 :) -1
## # ... with 5,020 more rows
# 매우 부정 단어 (-2)
dic %>%
filter(polarity == -2) %>%
arrange(word)
## # A tibble: 4,799 x 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
감정 표현을 나타낸 word
는 한 단어로 구성된 단일어, 두 개 이상의 단어가 결합된 복합어, ^^
,ㅠㅠ
와 같은 이모티콘으로 구성됩니다. polarity
는 +2
에서 -2
까지 5가지 정수로 되어 있습니다. 긍정 단어는 +
, 부정 단어는 -
로 표현됩니다. 긍정과 부정 중 어느 한쪽으로 판단하기 어려운 중성 단어는 0
으로 표현됩니다.
그리고, 중요한 부분이 있습니다. “KNU 감정사전”에는 “힘이 있게”나 “힘이 없어”와 같은 수식어구로서의 감정 표현도 수록되어 있다는 점인데요. 따라서 이러한 사전을 이용한 감정분석은 보다 면밀하고 섬세한 분석 및 결과 해석이 필요합니다.
“KNU 감정사전”의 표현은 총 14,854개입니다. 긍정 표현 4,871개, 부정 표현 9,829개, 중성 표현 154개로 구성됩니다.
dic %>%
mutate(sentiment = ifelse(polarity >= 1, "pos",
ifelse(polarity <= -1, "neg", "neu"))) %>%
count(sentiment)
## # A tibble: 3 x 2
## sentiment n
## * <chr> <int>
## 1 neg 9829
## 2 neu 154
## 3 pos 4871
library(readxl)
library(lubridate)
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
cv <- read_excel("bigkinds_corona_vaccine.xlsx", sheet = 1)
cv_hls_type <- cv %>%
select(DATE, COMPANY, HEADLINE, TEXT) %>%
filter(!duplicated(HEADLINE)) %>%
mutate(DATE = ymd(DATE)) %>%
mutate(HEADLINE = str_remove_all(HEADLINE, "\\[[[:print:]]+\\]|\\<[[:print:]]+\\>")) %>%
filter(str_detect(HEADLINE, "아스트라|AZ|화이자")) %>%
mutate(type = ifelse(str_detect(HEADLINE, "AZ|아스트라"),
"AZ", "Pfizer"))
cv_hls_type
## # A tibble: 1,006 x 5
## DATE COMPANY HEADLINE TEXT type
## <date> <chr> <chr> <chr> <chr>
## 1 2021-03-22 중부일보 " 고령층에도 본격화된 AZ 백신 접종"~ "고령층에 대해 접종이 유보됐던 아스트라제~ AZ
## 2 2021-03-22 중앙일보 "AZ \"코로나백신, 美 임상서 효과 79%~ "아스트라제네카(AZ)가 미국에서 진행한 ~ AZ
## 3 2021-03-22 중앙일보 "\"AZ 접종 후 희귀 혈전 발생 20대 구~ "최근 아스트라제네카(AZ) 백신을 접종한~ AZ
## 4 2021-03-22 세계일보 "예방접종위 “AZ 백신과 혈전 연관성 없어 ~ "아스트라제네카 백신 샘플을 살펴보는 의료~ AZ
## 5 2021-03-22 조선일보 "文, 아스트라 접종 하루 전날 “백신 가짜뉴~ "문재인 대통령이 22일 아스트라제네카 코~ AZ
## 6 2021-03-22 조선일보 " 예방접종위 “아스트라, 혈전과 연관 없어.~ "질병관리청 예방접종전문위원회(예방접종위)~ AZ
## 7 2021-03-22 동아일보 "예방접종위 “AZ 백신과 혈전 연관성 없다”~ "보건 감염병 분야 전문가로 구성된 예방접~ AZ
## 8 2021-03-22 세계일보 "AZ 백신 신뢰 회복하고 대상자 불안감 해소~ "충북지역에서 2분기 코로나19 백신 접종~ AZ
## 9 2021-03-22 조선일보 "존슨 英총리 “저도 아스트라 백신 맞았어요”~ "보리스 존슨(왼쪽) 영국 총리가 19일(~ AZ
## 10 2021-03-22 동아일보 "같은 75세 이상인데 요양병원 환자는 AZ,~ "65세 이상 고령자에 대한 신종 코로나바~ AZ
## # ... with 996 more rows
unnest_tokens()
함수를 이용해 샘플 텍스트
library(tidytext)
library(RcppMeCab)
# 띄어쓰기 기준으로 1-gram으로 토큰화
cv_hls_unigram <- cv_hls_type %>%
rowid_to_column() %>%
unnest_tokens(input = TEXT,
output = ngram,
token = "words",
drop = FALSE)
cv_hls_unigram
## # A tibble: 49,304 x 7
## rowid DATE COMPANY HEADLINE TEXT type ngram
## <int> <date> <chr> <chr> <chr> <chr> <chr>
## 1 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 고령층에~
## 2 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 대해
## 3 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 4 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 유보됐던~
## 5 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 아스트라제네~
## 6 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ az
## 7 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 백신
## 8 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 9 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 시작됐다~
## 10 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 요양병원과~
## # ... with 49,294 more rows
cv_hls_unigram %>% count(ngram, sort=T)
## # A tibble: 10,307 x 2
## ngram n
## <chr> <int>
## 1 코로나 1385
## 2 백신 1151
## 3 19 960
## 4 일 871
## 5 화이자 472
## 6 아스트라제네카 469
## 7 백신을 465
## 8 미국 350
## 9 접종 327
## 10 앵커 264
## # ... with 10,297 more rows
# 띄어쓰기 기준으로 2-gram으로 토큰화
cv_hls_bigram <- cv_hls_type %>%
rowid_to_column() %>%
unnest_tokens(input = TEXT,
output = ngram,
token = "ngrams",
n = 2,
drop = FALSE)
cv_hls_bigram
## # A tibble: 48,298 x 7
## rowid DATE COMPANY HEADLINE TEXT type ngram
## <int> <date> <chr> <chr> <chr> <chr> <chr>
## 1 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 고령층에 대해~
## 2 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 대해 접종이~
## 3 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 접종이 유보됐~
## 4 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 유보됐던 아스~
## 5 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 아스트라제네카~
## 6 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ az 백신
## 7 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 백신 접종이~
## 8 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 접종이 시작됐~
## 9 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 시작됐다 요양~
## 10 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트~ AZ 요양병원과 요~
## # ... with 48,288 more rows
cv_hls_bigram %>% count(ngram, sort=T)
## # A tibble: 29,252 x 2
## ngram n
## <chr> <int>
## 1 코로나 19 921
## 2 19 백신 331
## 3 감염증 코로나 214
## 4 코로나바이러스 감염증 214
## 5 신종 코로나바이러스 213
## 6 일 현지시간 151
## 7 65 세 140
## 8 코로나 백신 133
## 9 백신 접종 119
## 10 백신 접종이 116
## # ... with 29,242 more rows
1-gram 자료와 2-gram 자료를 기사 별로 결합
cv_hls_tidy <- bind_rows(cv_hls_unigram,
cv_hls_bigram) %>%
arrange(rowid) %>%
rename(word=ngram)
cv_hls_tidy
## # A tibble: 97,602 x 7
## rowid DATE COMPANY HEADLINE TEXT type word
## <int> <date> <chr> <chr> <chr> <chr> <chr>
## 1 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 고령층에~
## 2 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 대해
## 3 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 4 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 유보됐던~
## 5 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 아스트라제네~
## 6 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ az
## 7 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 백신
## 8 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 9 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 시작됐다~
## 10 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 요양병원과~
## # ... with 97,592 more rows
left_join()
함수를 이용한 표현에 감정 점수 부여하기 사전기반 감정분석은 tidy
데이터 프레임을 이용하는 것에서부터 시작합니다. 구체적으로, 한 행당 하나의 단어 혹은 어구씩으로 tidy
데이터 프레임 화 되어 있는 헤드라인 데이터를 감정사전과 결합하는 방식으로 감정분석이 진행됩니다. 그럼 우선 예제를 통해 데이터 셋 결합 방법을 살펴봅시다.df <- tibble(sentence = c("디자인 예쁘고 마감도 좋아서 만족스럽다.",
"디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다."))
df
## # A tibble: 2 x 1
## sentence
## <chr>
## 1 디자인 예쁘고 마감도 좋아서 만족스럽다.
## 2 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다.
df_tidy <- df %>%
unnest_tokens(input = sentence,
output = word,
token = "words",
drop = FALSE)
df_tidy
## # A tibble: 12 x 2
## sentence word
## <chr> <chr>
## 1 디자인 예쁘고 마감도 좋아서 만족스럽다. 디자인
## 2 디자인 예쁘고 마감도 좋아서 만족스럽다. 예쁘고
## 3 디자인 예쁘고 마감도 좋아서 만족스럽다. 마감도
## 4 디자인 예쁘고 마감도 좋아서 만족스럽다. 좋아서
## 5 디자인 예쁘고 마감도 좋아서 만족스럽다. 만족스럽다
## 6 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 디자인은
## 7 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 괜찮다
## 8 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 그런데
## 9 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 마감이
## 10 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 나쁘고
## 11 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 가격도
## 12 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 비싸다
토큰화한 각 표현에 감정 점수를 부여하겠습니다. dplyr
패키지의 left_join()
함수를 이용해 word
기준으로 감정사전을 결합하면 각 단어 혹은 어구에 감정 점수가 부여됩니다. 감정사전에 없는 표현은 polarity
의 값이 NA
가 되는데, 이때는 0
을 부여합니다.
left_join
left_join
위에서 볼 수 있듯, left_join()
함수는 두 tidy
데이터 간에 공통 요소 (여기에서는 각 어휘, word
)들을 골라 각 요소와 짝지어져 있는 변수들을 추가하는 방식으로 두 데이터를 결합하는 기능을 합니다. df
객체의 word
변수에 있는 각 어휘들, 그리고 dic
객체의 word
변인의 요소들 중 공통되는 것들에 그 어휘들과 짝지어져 있는 polarity
변수의 점수를 부여하고 나머지에는 NA
를 부여하지만 이를 0
으로 변환하는 방식인 것이죠.
이러한 left_join()
함수를 이용하면 tidy
방식의 헤드라인 데이터를 KNU 감정사전
과 결합하여 감정 점수를 부여할 수 있습니다.
df_tidy
## # A tibble: 12 x 2
## sentence word
## <chr> <chr>
## 1 디자인 예쁘고 마감도 좋아서 만족스럽다. 디자인
## 2 디자인 예쁘고 마감도 좋아서 만족스럽다. 예쁘고
## 3 디자인 예쁘고 마감도 좋아서 만족스럽다. 마감도
## 4 디자인 예쁘고 마감도 좋아서 만족스럽다. 좋아서
## 5 디자인 예쁘고 마감도 좋아서 만족스럽다. 만족스럽다
## 6 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 디자인은
## 7 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 괜찮다
## 8 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 그런데
## 9 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 마감이
## 10 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 나쁘고
## 11 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 가격도
## 12 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 비싸다
dic
## # A tibble: 14,854 x 2
## word polarity
## <chr> <dbl>
## 1 ㅡㅡ -1
## 2 ㅠㅠ -1
## 3 ㅠ_ㅠ -1
## 4 ㅠ -1
## 5 ㅜㅡ -1
## 6 ㅜㅜ -1
## 7 ㅜ_ㅜ -1
## 8 ㅜ.ㅜ -1
## 9 ㅜ -1
## 10 ㅗ -1
## # ... with 14,844 more rows
dic %>% filter(word =="예쁘고")
## # A tibble: 1 x 2
## word polarity
## <chr> <dbl>
## 1 예쁘고 2
df_sentiment <- df_tidy %>%
left_join(dic, by = "word") %>%
mutate(polarity = ifelse(is.na(polarity), 0, polarity))
df_sentiment
## # A tibble: 12 x 3
## sentence word polarity
## <chr> <chr> <dbl>
## 1 디자인 예쁘고 마감도 좋아서 만족스럽다. 디자인 0
## 2 디자인 예쁘고 마감도 좋아서 만족스럽다. 예쁘고 2
## 3 디자인 예쁘고 마감도 좋아서 만족스럽다. 마감도 0
## 4 디자인 예쁘고 마감도 좋아서 만족스럽다. 좋아서 2
## 5 디자인 예쁘고 마감도 좋아서 만족스럽다. 만족스럽다 2
## 6 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 디자인은 0
## 7 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 괜찮다 1
## 8 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 그런데 0
## 9 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 마감이 0
## 10 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 나쁘고 -2
## 11 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 가격도 0
## 12 디자인은 괜찮다. 그런데 마감이 나쁘고 가격도 비싸다. 비싸다 -2
cv_hls_tidy # "코로나" 및 "백신" 키워드를 포함한 뉴스기사 헤드라인 데이터의 tidy data frame
## # A tibble: 97,602 x 7
## rowid DATE COMPANY HEADLINE TEXT type word
## <int> <date> <chr> <chr> <chr> <chr> <chr>
## 1 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 고령층에~
## 2 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 대해
## 3 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 4 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 유보됐던~
## 5 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 아스트라제네~
## 6 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ az
## 7 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 백신
## 8 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 접종이
## 9 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 시작됐다~
## 10 1 2021-03-22 중부일보 " 고령층에도 본격화된 ~ 고령층에 대해 접종이 유보됐던 아스트라~ AZ 요양병원과~
## # ... with 97,592 more rows
cv_hls_senti <- cv_hls_tidy %>%
left_join(dic %>% filter(word!="3"), by = "word") %>%
mutate(polarity = ifelse(is.na(polarity), 0, polarity))
cv_hls_senti
## # A tibble: 97,602 x 8
## rowid DATE COMPANY HEADLINE TEXT type word polarity
## <int> <date> <chr> <chr> <chr> <chr> <chr> <dbl>
## 1 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 고령층에~ 0
## 2 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 대해 0
## 3 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 접종이 0
## 4 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 유보됐던~ 0
## 5 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 아스트라제~ 0
## 6 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ az 0
## 7 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 백신 0
## 8 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 접종이 0
## 9 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 시작됐다~ 0
## 10 1 2021-03-22 중부일보 " 고령층에도 본격화~ 고령층에 대해 접종이 유보됐~ AZ 요양병원과~ 0
## # ... with 97,592 more rows
left_join()
함수를 이용해서 cv_hls_tidy
객체를 dic
객체와 결합하는 방식으로 감정 어휘를 정리할 수 있습니다. 자, 그러면 각 헤드라인의 감정 점수는 어떻게 될까요? HEADLINE
별로 감정 점수를 합산하는 방식으로 각 헤드라인의 감정 점수를 계산할 수 있습니다.cv_hls_score <- cv_hls_senti %>%
group_by(type, rowid, TEXT) %>%
summarise(score = sum(polarity)) %>%
ungroup
## `summarise()` has grouped output by 'type', 'rowid'. You can override using the `.groups` argument.
cv_hls_score
## # A tibble: 1,006 x 4
## type rowid TEXT score
## <chr> <int> <chr> <dbl>
## 1 AZ 1 "고령층에 대해 접종이 유보됐던 아스트라제네카(AZ) 백신 접종이 시작됐다. 요양병원과 요양시설의 65세~ 0
## 2 AZ 2 "아스트라제네카(AZ)가 미국에서 진행한 신종 코로나바이러스 감염증(코로나19) 백신 임상 3상시험에서 ~ 0
## 3 AZ 3 "최근 아스트라제네카(AZ) 백신을 접종한 후 희귀 뇌혈전이 발견된 20대 코로나1차 대응요원 상태에 대~ 0
## 4 AZ 4 "아스트라제네카 백신 샘플을 살펴보는 의료진. 뉴시스 질병관리청 예방접종전문위원회(예방접종위)가 22일 ~ 0
## 5 AZ 5 "문재인 대통령이 22일 아스트라제네카 코로나 백신 안정성 문제와 관련해 “백신 불안감을 부추기는 가짜뉴~ -1
## 6 AZ 6 "질병관리청 예방접종전문위원회(예방접종위)가 22일 아스트라제네카 백신과 혈관과의 연관성이 낮다고 확인하~ 0
## 7 AZ 7 "보건 감염병 분야 전문가로 구성된 예방접종전문위원회가 최근 ‘혈전 생성’ 논란이 일었던 아스트라제네카(~ 0
## 8 AZ 8 "충북지역에서 2분기 코로나19 백신 접종을 앞두고 아스트라제네카(AZ) 백신에 대한 불안감이 커지는 추~ -1
## 9 AZ 9 "보리스 존슨(왼쪽) 영국 총리가 19일(현지 시각) 런던 세인트토머스병원에서 ‘아스트라제네카' 코로나 ~ 0
## 10 AZ 10 "65세 이상 고령자에 대한 신종 코로나바이러스 감염증(코로나19) 백신 접종이 23일 시작된다. 일반 ~ 0
## # ... with 996 more rows
감정 점수 높은 기사 살펴보기
# 긍정적 기사
cv_hls_score %>%
select(score, TEXT) %>%
arrange(-score)
## # A tibble: 1,006 x 2
## score TEXT
## <dbl> <chr>
## 1 7 "세계보건기구(WHO)의 백신 전문가들은 19일(현지시간) “코로나19 자체가 혈소판 감소 및 혈전을 유발할 수 있다는 점을 ~
## 2 6 "우리 정부가 2000만명분을 확보한 노바백스의 코로나 백신이 3상 임상시험 결과에서 뛰어난 예방 효과가 있는 것으로 나타났다~
## 3 5 "미국 노바백스가 개발 중인 코로나 백신 후보 물질이 3상 임상 시험 최종 분석 결과 96% 이상의 유효성이 확인됐다고 회사 ~
## 4 5 "<앵커> \r\n\r\n \r\n\r\n미국 제약사인 모더나가 자사가 개발 중인 코로나19 백신의 예방 효과가 94.5%로 ~
## 5 5 "미국 제약회사 화이자와 함께 신종 코로나바이러스(코로나 19) 백신 개발에 성공한 독일 바이오엔테크의 설립자는 터키 이민자 ~
## 6 4 "독일 보건 당국 산하 자문위원회가 제약사 아스트라제네카와 옥스퍼드대가 공동 개발한 코로나 예방 백신에 대해 ’65세 미만에만~
## 7 4 "영국의 제약사 아스트라제네카와 옥스퍼드대는 오늘 공동 개발 중인 코로나19 백신이 평균 70%의 예방 효과를 보였다고 밝혔습~
## 8 4 "<앵커> \r\n\r\n \r\n\r\n아스트라제네카 백신에 이어서 화이자 백신도 국내 허가의 첫 단계인 전문가 검증을 통과~
## 9 4 "◀ 앵커 ▶\r\n\r\n앞으로 가야 할 길은 멀지만 이 길에는 백신과 치료제가 함께 할 겁니다.\r\n\r\n다만 이 바이~
## 10 4 "정세균 국무총리는 원래 “3분기에 들어올 예정이었던 화이자 물량 일부를 2월로 앞당겨 도입하는 프로젝트를 민관이 협력해 특별~
## # ... with 996 more rows
# 부정적 기사
cv_hls_score %>%
select(score, TEXT) %>%
arrange(score)
## # A tibble: 1,006 x 2
## score TEXT
## <dbl> <chr>
## 1 -7 "최근 아스트라제네카 코로나 백신을 맞은 의료진 사이에서 “접종 후 이상 반응이 예상보다 훨씬 강하다”는 경험담이 잇따르고 있~
## 2 -5 "아스트라제네카(AZ) 백신을 맞은 한 전문의가 “부작용이 너무 심해서 웃음이 나왔다”며 접종 후기를 공개했다.\r\n유튜브 ~
## 3 -5 "2만여 명에 대한 코로나19 1차 백신 접종에서 이상 반응 의심 사례 97건이 추가 신고됐습니다.\r\n\r\n질병관리청은 ~
## 4 -4 "“집값 확인 순간 기절할 뻔” 16일부터 확인할 수 있었던 공동주택 공시가격에 대한 세종시 박 모(68) 씨의 반응이다. 3~
## 5 -4 "EU(유럽연합) 4대 회원국인 독일 프랑스 이탈리아 스페인이 15일(현지 시각) 아스트라제네카(AZ) 코로나 백신의 접종을 ~
## 6 -4 "최근 아스트라제네카(AZ) 백신 접종 후 이상반응을 보이거나 사망하는 사례가 잇따르면서 백신에 대한 시민들의 불신이 커지고 ~
## 7 -4 "경북 김천에서 아스트라제네카 백신을 접종한 50대 여성이 쓰러져 병원 중환자실에서 치료를 받고 있다. \r\n \r\n방역당~
## 8 -4 "국민의당 안철수 대표는 22일 “정부가 허락한다면, 정치인이자 의료인의 한 사람으로서 먼저 아스트라제네카(AZ) 백신을 맞을~
## 9 -4 "“신종 코로나바이러스 감염증(코로나19) 백신을 다른 나라보다 한 달 정도 늦게 접종하는 건 부작용 가능성을 감안하면 큰 문~
## 10 -4 "영국에 이어 미국에서도 제약사 화이자의 코로나19 백신을 맞은 뒤 알레르기 반응을 보이는 부작용 사례가 발생했다. \r\n ~
## # ... with 996 more rows
감정 점수 빈도 구하기
cv_hls_score %>%
count(score)
## # A tibble: 14 x 2
## score n
## * <dbl> <int>
## 1 -7 1
## 2 -5 2
## 3 -4 8
## 4 -3 15
## 5 -2 48
## 6 -1 98
## 7 0 540
## 8 1 196
## 9 2 68
## 10 3 17
## 11 4 8
## 12 5 3
## 13 6 1
## 14 7 1
cv_hls_score %>% count(type)
## # A tibble: 2 x 2
## type n
## * <chr> <int>
## 1 AZ 486
## 2 Pfizer 520
# 긍정, 중립, 부정으로 분류하는 변수 추가
cv_hls_score <- cv_hls_score %>%
mutate(sentiment = ifelse(score >= 1, "긍정",
ifelse(score <= -1, "부정", "중립")))
# `sentiment`별 빈도와 비율 구하기
frequency_score <- cv_hls_score %>%
group_by(type) %>%
count(sentiment) %>%
mutate(ratio = n/sum(n)*100)
frequency_score
## # A tibble: 6 x 4
## # Groups: type [2]
## type sentiment n ratio
## <chr> <chr> <int> <dbl>
## 1 AZ 긍정 134 27.6
## 2 AZ 부정 92 18.9
## 3 AZ 중립 260 53.5
## 4 Pfizer 긍정 160 30.8
## 5 Pfizer 부정 80 15.4
## 6 Pfizer 중립 280 53.8
frequency_score %>%
ggplot(aes(x = type, y = ratio, fill = sentiment)) +
geom_col() +
geom_text(aes(label = paste0(round(ratio, 1), "%")),
position = position_stack(vjust = 0.5)) +
xlab("")
중립 기사는 제외하고, 각 백신 별로 긍정적/부정적 기사 중 가장 자주 사용된 표현을 10개씩 추출해 비교하는 막대 그래프를 만들어봅시다.
# 토큰화 된 데이터에 기사별 감정 정보 추가하기
frequency_word <- cv_hls_tidy %>%
left_join(cv_hls_score) %>%
count(type, sentiment, word, sort=T)
## Joining, by = c("rowid", "TEXT", "type")
frequency_word
## # A tibble: 51,837 x 4
## type sentiment word n
## <chr> <chr> <chr> <int>
## 1 Pfizer 중립 코로나 412
## 2 Pfizer 중립 백신 357
## 3 AZ 중립 코로나 352
## 4 AZ 중립 백신 301
## 5 Pfizer 중립 19 301
## 6 Pfizer 중립 코로나 19 294
## 7 AZ 중립 일 266
## 8 AZ 중립 아스트라제네카 246
## 9 AZ 중립 19 242
## 10 Pfizer 중립 일 241
## # ... with 51,827 more rows
# 긍정 댓글 고빈도 단어
frequency_word %>%
filter(sentiment == "긍정")
## # A tibble: 15,500 x 4
## type sentiment word n
## <chr> <chr> <chr> <int>
## 1 Pfizer 긍정 코로나 234
## 2 Pfizer 긍정 19 177
## 3 Pfizer 긍정 코로나 19 171
## 4 AZ 긍정 코로나 167
## 5 Pfizer 긍정 백신 165
## 6 AZ 긍정 19 122
## 7 AZ 긍정 백신 121
## 8 AZ 긍정 코로나 19 119
## 9 Pfizer 긍정 일 117
## 10 AZ 긍정 아스트라제네카 105
## # ... with 15,490 more rows
# 부정 댓글 고빈도 단어
frequency_word %>%
filter(sentiment == "부정")
## # A tibble: 10,627 x 4
## type sentiment word n
## <chr> <chr> <chr> <int>
## 1 AZ 부정 백신 130
## 2 Pfizer 부정 코로나 114
## 3 AZ 부정 코로나 106
## 4 AZ 부정 아스트라제네카 91
## 5 AZ 부정 일 81
## 6 Pfizer 부정 백신 77
## 7 Pfizer 부정 일 73
## 8 Pfizer 부정 화이자 64
## 9 AZ 부정 19 61
## 10 AZ 부정 접종 61
## # ... with 10,617 more rows
library(tidyr)
sentiment_wide <- frequency_word %>%
filter(sentiment != "중립") %>%
pivot_wider(names_from = sentiment,
values_from = n,
values_fill = list(n = 0))
sentiment_wide
## # A tibble: 23,771 x 4
## type word 긍정 부정
## <chr> <chr> <int> <int>
## 1 Pfizer 코로나 234 114
## 2 Pfizer 19 177 57
## 3 Pfizer 코로나 19 171 56
## 4 AZ 코로나 167 106
## 5 Pfizer 백신 165 77
## 6 AZ 백신 121 130
## 7 AZ 19 122 61
## 8 AZ 코로나 19 119 58
## 9 Pfizer 일 117 73
## 10 AZ 아스트라제네카 105 91
## # ... with 23,761 more rows
# 로그 오즈비 구하기
sentiment_wide <- sentiment_wide %>%
mutate(log_odds_ratio = log(((`긍정` + 1) / (sum(`긍정` + 1))) /
((`부정` + 1) / (sum(`부정` + 1)))))
sentiment_wide
## # A tibble: 23,771 x 5
## type word 긍정 부정 log_odds_ratio
## <chr> <chr> <int> <int> <dbl>
## 1 Pfizer 코로나 234 114 0.467
## 2 Pfizer 19 177 57 0.874
## 3 Pfizer 코로나 19 171 56 0.857
## 4 AZ 코로나 167 106 0.204
## 5 Pfizer 백신 165 77 0.508
## 6 AZ 백신 121 130 -0.318
## 7 AZ 19 122 61 0.438
## 8 AZ 코로나 19 119 58 0.463
## 9 Pfizer 일 117 73 0.219
## 10 AZ 아스트라제네카 105 91 -0.106
## # ... with 23,761 more rows
sentiment_top10 <- sentiment_wide %>%
group_by(type, sentiment = ifelse(log_odds_ratio > 0, "긍정", "부정")) %>%
slice_max(abs(log_odds_ratio), n = 10)
sentiment_top10
## # A tibble: 51 x 6
## # Groups: type, sentiment [4]
## type word 긍정 부정 log_odds_ratio sentiment
## <chr> <chr> <int> <int> <dbl> <chr>
## 1 AZ 함께 15 0 2.53 긍정
## 2 AZ 명이 11 0 2.24 긍정
## 3 AZ 정 11 0 2.24 긍정
## 4 AZ 전문가 22 1 2.20 긍정
## 5 AZ 평균 10 0 2.15 긍정
## 6 AZ 정확한 19 1 2.06 긍정
## 7 AZ 내용은 방송으로 9 0 2.06 긍정
## 8 AZ 방송으로 9 0 2.06 긍정
## 9 AZ 방송으로 확인하시기 9 0 2.06 긍정
## 10 AZ 임상시험 9 0 2.06 긍정
## # ... with 41 more rows
sentiment_top10 %>%
ggplot(aes(x = reorder_within(word, log_odds_ratio, type),
y =log_odds_ratio,
fill = sentiment)) +
geom_col() +
scale_x_reordered() +
coord_flip() +
labs(x = NULL) +
facet_wrap(~type, scales = "free")