[R] 2. Sentiment analysis with tidy data
반응형
2. Sentiment analysis with tidy data
- 아래 그림은 텍스트 분석의 흐름도 입니다.
- 텍스트 감정을 분석하는 방법 중 하나는 텍스트 데이터가 여러 개별 단어의 조합으로 구성되어 있을 때,
전체 텍스트에서 감정 내용을 개별 단어의 감정 내용의 합으로 간주하는 것입니다.
2. 1. The sentiments
dataset
textdata
라이브러리에 내장되어 있는 사전 데이터(dictionary-based)를 활용해보겠습니다.afinn
: 단어 별 부정과 긍정 사이에 -5에서 5점까지 스코어를 매긴 데이터bing
: 긍/부정을 binary로 나타낸 데이터 (positive, negative)nrc
: positive, negative 외에 세 가지 이상의 감정을 분류한 데이터
- 아래 데이터 예시를 살펴보겠습니다.
library(textdata)
get_sentiments("afinn")
## # A tibble: 2,477 x 2
## word value
## <chr> <dbl>
## 1 abandon -2
## 2 abandoned -2
## 3 abandons -2
## 4 abducted -2
## 5 abduction -2
## 6 abductions -2
## 7 abhor -3
## 8 abhorred -3
## 9 abhorrent -3
## 10 abhors -3
## # … with 2,467 more rows
get_sentiments("bing")
## # A tibble: 6,786 x 2
## word sentiment
## <chr> <chr>
## 1 2-faces negative
## 2 abnormal negative
## 3 abolish negative
## 4 abominable negative
## 5 abominably negative
## 6 abominate negative
## 7 abomination negative
## 8 abort negative
## 9 aborted negative
## 10 aborts negative
## # … with 6,776 more rows
get_sentiments("nrc")
## # A tibble: 13,901 x 2
## word sentiment
## <chr> <chr>
## 1 abacus trust
## 2 abandon fear
## 3 abandon negative
## 4 abandon sadness
## 5 abandoned anger
## 6 abandoned fear
## 7 abandoned negative
## 8 abandoned sadness
## 9 abandonment anger
## 10 abandonment fear
## # … with 13,891 more rows
- 이렇게 사전에 기반한 텍스트 분석은 해당 텍스트에 속한 각 단어들의 감정 점수를 합산하여 그 텍스트의 전체 감정을 찾는 방식입니다.
2. 2. Sentiment analysis with inner join
- 우리는 사전에 기반한 분석 방법으로 접근하기에 필요 없는 용어 제거와 같은 클렌징 작업이 필요할 수도 있습니다.
- 이 때 tidy data라면
anti_join()
등을 활용하는 것처럼 데이터 간의 조인 작업이 필요합니다. - 1장에서 봤듯
janeaustenr
라이브러리의austen_books()
를 활용하겠습니다.
library(janeaustenr)
original_books <- austen_books() %>%
group_by(book) %>%
mutate(
line_number = row_number(),
chapter = cumsum(str_detect(string = text, pattern = regex("^chapter [\\divclx]", ignore_case = TRUE)))
) %>%
ungroup()
original_books
## # A tibble: 73,422 x 4
## text book line_number chapter
## <chr> <fct> <int> <int>
## 1 "SENSE AND SENSIBILITY" Sense & Sensibility 1 0
## 2 "" Sense & Sensibility 2 0
## 3 "by Jane Austen" Sense & Sensibility 3 0
## 4 "" Sense & Sensibility 4 0
## 5 "(1811)" Sense & Sensibility 5 0
## 6 "" Sense & Sensibility 6 0
## 7 "" Sense & Sensibility 7 0
## 8 "" Sense & Sensibility 8 0
## 9 "" Sense & Sensibility 9 0
## 10 "CHAPTER 1" Sense & Sensibility 10 1
## # … with 73,412 more rows
- 이 또한 역시
unnest_tokens()
함수를 사용하여text
컬럼을word
라는 컬럼명을 갖는 단어 단위의 토큰으로 분리하겠습니다.
tidy_books <- original_books %>%
unnest_tokens(
input = text,
output = "word"
)
tidy_books
## # A tibble: 725,055 x 4
## book line_number chapter word
## <fct> <int> <int> <chr>
## 1 Sense & Sensibility 1 0 sense
## 2 Sense & Sensibility 1 0 and
## 3 Sense & Sensibility 1 0 sensibility
## 4 Sense & Sensibility 3 0 by
## 5 Sense & Sensibility 3 0 jane
## 6 Sense & Sensibility 3 0 austen
## 7 Sense & Sensibility 5 0 1811
## 8 Sense & Sensibility 10 1 chapter
## 9 Sense & Sensibility 10 1 1
## 10 Sense & Sensibility 13 1 the
## # … with 725,045 more rows
- 그 다음 nrc 사전(
get_sentiments("nrc")
)에서 joy 라는 감정을 갖는 단어들을 살펴보겠습니다.
nrc_joy <- get_sentiments("nrc") %>%
filter(sentiment == "joy")
nrc_joy
## # A tibble: 689 x 2
## word sentiment
## <chr> <chr>
## 1 absolution joy
## 2 abundance joy
## 3 abundant joy
## 4 accolade joy
## 5 accompaniment joy
## 6 accomplish joy
## 7 accomplished joy
## 8 achieve joy
## 9 achievement joy
## 10 acrobat joy
## # … with 679 more rows
- “Emma” 라는 이름을 갖는 책에서 joy 감정을 나타내는 단어들을 찾고 싶다면?
- 이러한 단어 사전 객체를 우리는 위에서 저장한
tidy_books
객체에inner_join()
하겠습니다.
tidy_books %>%
filter(book == "Emma") %>%
inner_join(nrc_joy, by = "word")
## # A tibble: 4,432 x 5
## book line_number chapter word sentiment
## <fct> <int> <int> <chr> <chr>
## 1 Emma 16 1 happy joy
## 2 Emma 16 1 blessings joy
## 3 Emma 21 1 marriage joy
## 4 Emma 22 1 mother joy
## 5 Emma 24 1 excellent joy
## 6 Emma 25 1 mother joy
## 7 Emma 25 1 affection joy
## 8 Emma 28 1 friend joy
## 9 Emma 33 1 friend joy
## 10 Emma 33 1 friend joy
## # … with 4,422 more rows
- “Emma”에서 joy를 표현하는 단어들의 빈도를 계산하고자 한다면?
count()
함수를 적용하여 확인할 수 있습니다.
tidy_books %>%
filter(book == "Emma") %>%
inner_join(nrc_joy, by = "word") %>%
count(word, sort = TRUE)
## # A tibble: 303 x 2
## word n
## <chr> <int>
## 1 good 359
## 2 young 192
## 3 friend 166
## 4 hope 143
## 5 happy 125
## 6 love 117
## 7 deal 92
## 8 found 92
## 9 present 89
## 10 kind 82
## # … with 293 more rows
- 또한 텍스트에서 감정이 어떻게 변하는 지도 살펴볼 수 있습니다.
- 이번에는 감정의 긍/부정을 나타내는
bing
사전(get_sentiments("bing")
)을 활용해보겠습니다. - 이 때 세션의 정의가 필요하다면 인덱싱을 정의를 해야하는데, 예제에는 그 세션을 80줄을 기준으로 하였습니다.
jane_austen_setiment <- tidy_books %>%
inner_join(get_sentiments("bing"), by = "word") %>%
mutate(index = line_number %/% 80) %>% # x %/% y == floor(x/y) 몫을 제외하고 나머지 소숫점들 버림
count(book, index, sentiment)
jane_austen_setiment
## # A tibble: 1,840 x 4
## book index sentiment n
## <fct> <dbl> <chr> <int>
## 1 Sense & Sensibility 0 negative 16
## 2 Sense & Sensibility 0 positive 32
## 3 Sense & Sensibility 1 negative 19
## 4 Sense & Sensibility 1 positive 53
## 5 Sense & Sensibility 2 negative 12
## 6 Sense & Sensibility 2 positive 31
## 7 Sense & Sensibility 3 negative 15
## 8 Sense & Sensibility 3 positive 31
## 9 Sense & Sensibility 4 negative 16
## 10 Sense & Sensibility 4 positive 34
## # … with 1,830 more rows
- 이를 조금 더 보기 편하게
pivot_wider()
함수를 적용하여 wide format으로 피벗팅 시켜보겠습니다.- 피벗팅에 관련된 포스팅 : 여기!
- 피벗팅 이후에는 positive와 negative의 차이를 가지고 감정을 계산하겠습니다.
- sentiment = positive - negative
jane_austen_setiment_wider <- jane_austen_setiment %>%
pivot_wider(
names_from = "sentiment",
values_from = "n",
values_fill = 0 # 결측이 있다면 0으로 처리
) %>%
mutate(sentiment = positive - negative)
jane_austen_setiment_wider
## # A tibble: 920 x 5
## book index negative positive sentiment
## <fct> <dbl> <int> <int> <int>
## 1 Sense & Sensibility 0 16 32 16
## 2 Sense & Sensibility 1 19 53 34
## 3 Sense & Sensibility 2 12 31 19
## 4 Sense & Sensibility 3 15 31 16
## 5 Sense & Sensibility 4 16 34 18
## 6 Sense & Sensibility 5 16 51 35
## 7 Sense & Sensibility 6 24 40 16
## 8 Sense & Sensibility 7 23 51 28
## 9 Sense & Sensibility 8 30 40 10
## 10 Sense & Sensibility 9 15 19 4
## # … with 910 more rows
- 위의 결과를 가지고 각 책(book)별로 시간의 흐름에 따라(세션에 따라) 긍/부정 감정 추이를 시각화해볼 수 있습니다.
jane_austen_setiment_wider %>%
ggplot(aes(x = index, y = sentiment, fill = book)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ book, nrow = 2, scales = "free_x")
2. 3. Comparing the three sentiment dictionaries
- 이번에는 특정 책 하나를 대상(
filter(book == "Pride & Prejudice")
)으로 하여 세 가지 감정 단어를 모두 사용해보고 비교해보도록 하겠습니다.
pride_prejudice <- tidy_books %>%
filter(book == "Pride & Prejudice")
pride_prejudice
## # A tibble: 122,204 x 4
## book line_number chapter word
## <fct> <int> <int> <chr>
## 1 Pride & Prejudice 1 0 pride
## 2 Pride & Prejudice 1 0 and
## 3 Pride & Prejudice 1 0 prejudice
## 4 Pride & Prejudice 3 0 by
## 5 Pride & Prejudice 3 0 jane
## 6 Pride & Prejudice 3 0 austen
## 7 Pride & Prejudice 7 1 chapter
## 8 Pride & Prejudice 7 1 1
## 9 Pride & Prejudice 10 1 it
## 10 Pride & Prejudice 10 1 is
## # … with 122,194 more rows
- 위 객체를 가지고 세 가지 사전에
inner_join()
함수를 이용하여 적용해보겠습니다.
afinn <- pride_prejudice %>%
inner_join(get_sentiments("afinn"), by = "word") %>%
group_by(index = line_number %/% 80) %>%
summarise(sentiment = sum(value)) %>%
ungroup() %>%
mutate(method = "AFINN")
afinn
## # A tibble: 163 x 3
## index sentiment method
## <dbl> <dbl> <chr>
## 1 0 29 AFINN
## 2 1 0 AFINN
## 3 2 20 AFINN
## 4 3 30 AFINN
## 5 4 62 AFINN
## 6 5 66 AFINN
## 7 6 60 AFINN
## 8 7 18 AFINN
## 9 8 84 AFINN
## 10 9 26 AFINN
## # … with 153 more rows
bing <- pride_prejudice %>%
inner_join(get_sentiments("bing"), by = "word") %>%
mutate(method = "Bing et al.") %>%
count(method, index = line_number %/% 80, sentiment) %>%
pivot_wider(
names_from = "sentiment",
values_from = n,
values_fill = 0
) %>%
mutate(sentiment = positive - negative)
bing
## # A tibble: 163 x 5
## method index negative positive sentiment
## <chr> <dbl> <int> <int> <int>
## 1 Bing et al. 0 7 21 14
## 2 Bing et al. 1 20 19 -1
## 3 Bing et al. 2 16 20 4
## 4 Bing et al. 3 19 31 12
## 5 Bing et al. 4 23 47 24
## 6 Bing et al. 5 15 49 34
## 7 Bing et al. 6 18 46 28
## 8 Bing et al. 7 23 33 10
## 9 Bing et al. 8 17 48 31
## 10 Bing et al. 9 22 40 18
## # … with 153 more rows
nrc <- pride_prejudice %>%
inner_join(get_sentiments("nrc"), by = "word") %>%
filter(sentiment %in% c("positive", "negative")) %>%
mutate(method = "NRC") %>%
count(method, index = line_number %/% 80, sentiment) %>%
pivot_wider(
names_from = "sentiment",
values_from = n,
values_fill = 0
) %>%
mutate(sentiment = positive - negative)
nrc
## # A tibble: 163 x 5
## method index negative positive sentiment
## <chr> <dbl> <int> <int> <int>
## 1 NRC 0 10 31 21
## 2 NRC 1 17 34 17
## 3 NRC 2 22 30 8
## 4 NRC 3 17 60 43
## 5 NRC 4 17 49 32
## 6 NRC 5 12 54 42
## 7 NRC 6 14 68 54
## 8 NRC 7 19 47 28
## 9 NRC 8 11 55 44
## 10 NRC 9 22 43 21
## # … with 153 more rows
- 위 세 가지를 가지고 시각화해본 결과는 아래와 같습니다.
- 아래 결과를 해석하자면, afinn 이나 bing은 조금 더 다양한 감정이 포함되었을거라 보여지는 반면,
nrc는 이 책과 매핑하는 측면에서는 조금 더 긍정적인 단어들이 매핑될 가능성이 높다고 해석할 수 있습니다.
- 아래 결과를 해석하자면, afinn 이나 bing은 조금 더 다양한 감정이 포함되었을거라 보여지는 반면,
bind_rows(afinn, bing, nrc) %>%
ggplot(aes(x = index, y = sentiment, fill = method)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ method, ncol = 1, scales = "free_y")
2. 4. Most common positive and negative words
- bing과 같이 감정과 단어가 모두 있는 사전이 갖는 하나의 이점은 각 감정에 기여하는 단어 수를 분석할 수 있다는 점입니다.
bing_word_counts <- tidy_books %>%
inner_join(get_sentiments("bing"), by = "word") %>%
count(word, sentiment, sort = TRUE)
bing_word_counts
## # A tibble: 2,585 x 3
## word sentiment n
## <chr> <chr> <int>
## 1 miss negative 1855
## 2 well positive 1523
## 3 good positive 1380
## 4 great positive 981
## 5 like positive 725
## 6 better positive 639
## 7 enough positive 613
## 8 happy positive 534
## 9 love positive 495
## 10 pleasure positive 462
## # … with 2,575 more rows
- 긍/부정 각각 상위 10개 단어들을 시각화해본 결과는 아래와 같습니다.
bing_word_counts %>%
group_by(sentiment) %>%
slice_max(n, n = 10) %>%
ungroup() %>%
ggplot(aes(x = reorder(word, n), y = n, fill = sentiment)) +
geom_col(show.legend = FALSE) +
facet_wrap(~ sentiment, scales = "free_y") +
coord_flip() +
labs(x = "Contribution to sentiment", y = NULL)
- 여기서 이상한 부분을 발견할 수 있습니다.
- “miss”라는 단어는 해당 사전에서 부정으로 코딩되었지만, 샘플로 사용한 Jane Austen의 작품에서는 젊은 미혼 여성으로 사용된다고 합니다.
- 이러한 경우 아래와 같이 불용어 처리를 커스텀하게 설정할 수 있습니다.
custom_stop_words <- bind_rows(
stop_words,
tibble(word = c("miss"), lexicon = c("custom"))
)
custom_stop_words %>%
tail()
## # A tibble: 6 x 2
## word lexicon
## <chr> <chr>
## 1 young onix
## 2 younger onix
## 3 youngest onix
## 4 your onix
## 5 yours onix
## 6 miss custom
2. 5. Wordclouds
ggplot2
라이브러리 외에 워드 클라우드를 그리기 위해서는wordcloud
라는 패키지도 활용해볼 수 있습니다.
library(wordcloud)
## 필요한 패키지를 로딩중입니다: RColorBrewer
- 워드클라우드를 통해 알아본 Jane Austen 소설에서 흔한 단어 상위 100개는 아래와 같습니다.
- 그림 내 텍스트의 크기는 단어의 빈도와 비례합니다.
tidy_books %>%
anti_join(stop_words, by = "word") %>%
count(word, sort = TRUE) %>%
with(wordcloud(word, n , max.words = 100))
- 어떤 특정 단어를 그룹으로 나누어 비교하고 싶을 때에는
comparison.cloud()
함수를 사용합니다. - 해당 함수를 적용하기 위해서는 데이터 프레임을 행렬 형태로 변환해줄 필요가 있으며
이 때reshape2
라이브러리에 있는acast()
함수를 사용합니다. - 예제는 아래와 같습니다.
library(reshape2)
##
## 다음의 패키지를 부착합니다: 'reshape2'
## The following object is masked from 'package:tidyr':
##
## smiths
tidy_books %>%
inner_join(get_sentiments("bing"), by = "word") %>%
count(word, sentiment, sort = TRUE) %>%
acast(word ~ sentiment, value.var = "n", fill = 0) %>%
comparison.cloud(
colors = c("grey80", "grey20"),
max.words = 100
)
2. 6. Looking at units beyond just word
- 단어 수준에서 토큰화를 하면 유용한 작업을 할 수 있지만, 때로는 아래와 같이 단어가 아닌 그 이상이 필요할 경우도 있습니다.
tibble(text = prideprejudice) %>%
unnest_tokens(
input = text,
output = "setence",
token = "sentences"
)
## # A tibble: 15,545 x 1
## setence
## <chr>
## 1 "pride and prejudice"
## 2 "by jane austen"
## 3 "chapter 1"
## 4 "it is a truth universally acknowledged, that a single man in possession"
## 5 "of a good fortune, must be in want of a wife."
## 6 "however little known the feelings or views of such a man may be on his"
## 7 "first entering a neighbourhood, this truth is so well fixed in the minds"
## 8 "of the surrounding families, that he is considered the rightful property"
## 9 "of some one or other of their daughters."
## 10 "\"my dear mr."
## # … with 15,535 more rows
- 또한 정규표현식 패턴을 사용하여 토큰을 분할할 수도 있습니다.
austen_chapters <- austen_books() %>%
group_by(book) %>%
unnest_tokens(
input = text,
output = "chapter",
token = "regex",
pattern = "Chapter|CHAPTER [\\dIVXLC]"
) %>%
ungroup()
austen_chapters
## # A tibble: 275 x 2
## book chapter
## <fct> <chr>
## 1 Sense & Sensibi… "sense and sensibility\n\nby jane austen\n\n(1811)\n\n\n\n\…
## 2 Sense & Sensibi… "\n\n\nthe family of dashwood had long been settled in suss…
## 3 Sense & Sensibi… "\n\n\nmrs. john dashwood now installed herself mistress of…
## 4 Sense & Sensibi… "\n\n\nmrs. dashwood remained at norland several months; no…
## 5 Sense & Sensibi… "\n\n\n\"what a pity it is, elinor,\" said marianne, \"that…
## 6 Sense & Sensibi… "\n\n\nno sooner was her answer dispatched, than mrs. dashw…
## 7 Sense & Sensibi… "\n\n\nthe first part of their journey was performed in too…
## 8 Sense & Sensibi… "\n\n\nbarton park was about half a mile from the cottage. …
## 9 Sense & Sensibi… "\n\n\nmrs. jennings was a widow with an ample jointure. s…
## 10 Sense & Sensibi… "\n\n\nthe dashwoods were now settled at barton with tolera…
## # … with 265 more rows
austen_chapters %>%
group_by(book) %>%
summarise(chapters = n()) %>%
ungroup()
## # A tibble: 6 x 2
## book chapters
## <fct> <int>
## 1 Sense & Sensibility 51
## 2 Pride & Prejudice 62
## 3 Mansfield Park 49
## 4 Emma 56
## 5 Northanger Abbey 32
## 6 Persuasion 25
- 이를 가지고 조금 더 활용하면 Austen의 각 소설에서 가장 부정적인 챕터가 무엇인지 확인해볼 수 있습니다.
- 위에서 언급했던 bing 사전에서 부정 의미를 담은 단어 목록을 따로 저장하고, 각 챕터의 단어 수를 저장하겠습니다.
bing_negative <- get_sentiments("bing") %>%
filter(sentiment == "negative")
bing_negative
## # A tibble: 4,781 x 2
## word sentiment
## <chr> <chr>
## 1 2-faces negative
## 2 abnormal negative
## 3 abolish negative
## 4 abominable negative
## 5 abominably negative
## 6 abominate negative
## 7 abomination negative
## 8 abort negative
## 9 aborted negative
## 10 aborts negative
## # … with 4,771 more rows
word_counts <- tidy_books %>%
group_by(book, chapter) %>%
summarise(words = n()) %>%
ungroup()
## `summarise()` has grouped output by 'book'. You can override using the `.groups` argument.
word_counts
## # A tibble: 275 x 3
## book chapter words
## <fct> <int> <int>
## 1 Sense & Sensibility 0 7
## 2 Sense & Sensibility 1 1571
## 3 Sense & Sensibility 2 1970
## 4 Sense & Sensibility 3 1538
## 5 Sense & Sensibility 4 1952
## 6 Sense & Sensibility 5 1030
## 7 Sense & Sensibility 6 1353
## 8 Sense & Sensibility 7 1288
## 9 Sense & Sensibility 8 1256
## 10 Sense & Sensibility 9 1863
## # … with 265 more rows
- 그 다음 각 챕터에서 부정어의 수를 구하고 이를 총 단어 수로 나누어 비율을 계산하면 아래와 같습니다.
tidy_books %>%
semi_join(bing_negative, by = "word") %>%
group_by(book, chapter) %>%
summarise(negative_words = n()) %>%
left_join(word_counts, by = c("book", "chapter")) %>%
mutate(ratio = negative_words/words) %>%
filter(chapter > 0) %>%
slice_max(ratio, n = 1) %>%
ungroup()
## `summarise()` has grouped output by 'book'. You can override using the `.groups` argument.
## # A tibble: 6 x 5
## book chapter negative_words words ratio
## <fct> <int> <int> <int> <dbl>
## 1 Sense & Sensibility 43 161 3405 0.0473
## 2 Pride & Prejudice 34 111 2104 0.0528
## 3 Mansfield Park 46 173 3685 0.0469
## 4 Emma 15 151 3340 0.0452
## 5 Northanger Abbey 21 149 2982 0.0500
## 6 Persuasion 4 62 1807 0.0343
반응형
'tidytext' 카테고리의 다른 글
[R] 한글 형태소 분석 (0) | 2021.07.17 |
---|---|
[R] unnest_tokens() (0) | 2021.07.17 |
[R] 정규표현식 관련 (0) | 2021.07.15 |
[R] stringr 문자열 관련 처리 함수 (0) | 2021.07.14 |
[R] 1. Tidy text format (0) | 2021.07.14 |
TAGS.