뉴스기사 분석 예시

질문: 코로나 백신과 관련한 뉴스보도에서 중요하게 다뤄지는 의제 혹은 이슈는 무엇인가?

방법: “코로나 백신” 관련 뉴스기사를 수집한 후 헤드라인에서 자주 등장하는 단어들을 추출하여 내용을 분석한다.

데이터 프레임 이용하기

빅카인즈에서 CSV 형태의 뉴스 데이터를 불러온다.

# 코로나 백신 관련 뉴스: 키워드 검색 - "코로나 + 백신"

# 중앙지, 지역종합지, 방송사  
# 지난 6개월 기간
# 제목 혹은 본문에 키워드 포함
# 중복 기사 제거 (분석제외)

# 엑셀파일 다운로드 폴더 확인
getwd()
## [1] "D:/Dropbox/2021_Class/DM_BD/DM_BD_R"
# 엑셀파일 불러오기

# install.packages("readxl")
library(readxl)
cv <- read_excel("bigkinds_corona_vaccine.xlsx", sheet = 1)
class(cv)
## [1] "tbl_df"     "tbl"        "data.frame"
head(cv)
## # A tibble: 6 x 11
##   DATE   COMPANY  BYLINE HEADLINE PERSON PLACE ORG   KEYWORD FEATURE TEXT  URL  
##   <chr>  <chr>    <chr>  <chr>    <chr>  <chr> <chr> <chr>   <chr>   <chr> <chr>
## 1 20210~ 중부일보 중부일보~ "[사설] 고~ <NA>   이스라엘~ 유럽의약~ 고령층,본격~ az,유럽,~ "고령층~ www.~
## 2 20210~ 중앙일보 홍주희(h~ "AZ \"코~ <NA>   미국,美~ AP통신~ AZ,코로나~ 아스트라제네~ "아스트~ http~
## 3 20210~ YTN      이은지 "[생생경제]~ 박세리,김~ 주내,미~ 차영,S~ SK,성공,~ 공모주,차영~ "■ 방~ http~
## 4 20210~ YTN      이은지 "[생생경제]~ 우다야 라~ 서울,중~ YTN,~ 코로나의무검~ 노동자,김혜~ "■ 방~ http~
## 5 20210~ 중앙일보 황수연(p~ "\"AZ 접~ 나상훈,서~ <NA>  의대,유~ AZ,접종,~ 혈전증,az~ "최근 ~ http~
## 6 20210~ 전남일보 서울=김선~ "이용빈, \~ 이용빈,송~ 인도,(~ 정부,세~ 이용빈,폭력~ 미얀마,광주~ "더불어~ http~

데이터 정제

지금부터 tibble이라는 데이터 프레임의 형식으로 이루어진 뉴스기사 데이터를 텍스트 분석을 위한 전처리 방식에 대해서 알아보도록 하겠습니다. 이를 위해 필요한 함수를 제공하는 tidyverse 패키지에 대해서 살펴보도록 하겠습니다.

mutate(): 기존 변수를 이용하여 생성한 새로운 변수를 데이터 열에 추가

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()
cv_hls_unique <- cv %>% 
  select(DATE, COMPANY, HEADLINE) %>% 
  filter(!duplicated(HEADLINE)) 
  
names(cv_hls_unique)
## [1] "DATE"     "COMPANY"  "HEADLINE"
cv_hls_unique$DATE[1:10]
##  [1] "20210322" "20210322" "20210322" "20210322" "20210322" "20210322"
##  [7] "20210322" "20210322" "20210322" "20210322"
class(cv_hls_unique$DATE)
## [1] "character"
library(lubridate) # 날짜 정보를 다루기 위한 함수를 제공
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
cv_hls_unique %>% mutate(date = ymd(DATE))
## # A tibble: 15,890 x 4
##    DATE    COMPANY  HEADLINE                                          date      
##    <chr>   <chr>    <chr>                                             <date>    
##  1 202103~ 중부일보 "[사설] 고령층에도 본격화된 AZ 백신 접종"         2021-03-22
##  2 202103~ 중앙일보 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\""~ 2021-03-22
##  3 202103~ YTN      "[생생경제] SK바사. 공모주라고 무조건 성공은 아니다. 투자 시 유의할 점"~ 2021-03-22
##  4 202103~ YTN      "[생생경제] 코로나의무검사보다 근로환경개선이 더 시급(우다야 라이 민주노총 이주노조위~ 2021-03-22
##  5 202103~ 중앙일보 "\"AZ 접종 후 희귀 혈전 발생 20대 구급대원 증상 호전돼\""~ 2021-03-22
##  6 202103~ 전남일보 "이용빈, \"미얀마 군부 폭력의 희생자에 대한 인도적 지원 시급\""~ 2021-03-22
##  7 202103~ 동아일보 "“선택적 분노 김제동 선생” 신간 리뷰 삭제 논란"   2021-03-22
##  8 202103~ 중앙일보 "김제동 책 비판 리뷰 삭제 논란 \"욕설도 아닌데 검열하냐\""~ 2021-03-22
##  9 202103~ 중앙일보 "LH 두고 \"부동산 적폐\"→\"누적된 관행\"  일주일만에 말바뀐 文"~ 2021-03-22
## 10 202103~ YTN      "[더뉴스] [리얼미터] 문 대통령 지지율 '최저치'...LH 사태 여파"~ 2021-03-22
## # ... with 15,880 more rows
# ymd 함수의 기능은?
cv_hls_date <- cv_hls_unique %>% 
  mutate(date = ymd(DATE))
cv_hls_date
## # A tibble: 15,890 x 4
##    DATE    COMPANY  HEADLINE                                          date      
##    <chr>   <chr>    <chr>                                             <date>    
##  1 202103~ 중부일보 "[사설] 고령층에도 본격화된 AZ 백신 접종"         2021-03-22
##  2 202103~ 중앙일보 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\""~ 2021-03-22
##  3 202103~ YTN      "[생생경제] SK바사. 공모주라고 무조건 성공은 아니다. 투자 시 유의할 점"~ 2021-03-22
##  4 202103~ YTN      "[생생경제] 코로나의무검사보다 근로환경개선이 더 시급(우다야 라이 민주노총 이주노조위~ 2021-03-22
##  5 202103~ 중앙일보 "\"AZ 접종 후 희귀 혈전 발생 20대 구급대원 증상 호전돼\""~ 2021-03-22
##  6 202103~ 전남일보 "이용빈, \"미얀마 군부 폭력의 희생자에 대한 인도적 지원 시급\""~ 2021-03-22
##  7 202103~ 동아일보 "“선택적 분노 김제동 선생” 신간 리뷰 삭제 논란"   2021-03-22
##  8 202103~ 중앙일보 "김제동 책 비판 리뷰 삭제 논란 \"욕설도 아닌데 검열하냐\""~ 2021-03-22
##  9 202103~ 중앙일보 "LH 두고 \"부동산 적폐\"→\"누적된 관행\"  일주일만에 말바뀐 文"~ 2021-03-22
## 10 202103~ YTN      "[더뉴스] [리얼미터] 문 대통령 지지율 '최저치'...LH 사태 여파"~ 2021-03-22
## # ... with 15,880 more rows
library(ggplot2) #시각화 예시
cv_hls_date %>% 
  count(date) %>% 
  ggplot(aes(x=date, y=n)) +
  geom_col() + 
  scale_x_date(date_breaks = "1 month", date_labels = "%b")

cv_hls_date %>% 
  count(DATE) %>% 
  ggplot(aes(x=DATE, y=n)) +
  geom_col() 

select()filter(), mutate()dplyr 패키지의 함수를 이용해서 데이터 처리하는 과정을 살펴봄.

tidytext

자, 이제는 “tidy” 데이터가 무엇인지, 그 원리를 살펴보고 이를 기반으로 한 어휘 빈도수 분석에 효율적인 함수를 제공하는 “tidytext” 패키지를 소개해드리도록 하겠습니다. 특히, 이 패키지의 unnest_tokens() 함수는 텍스트 전처리에 매우 편리한 기능을 제공합니다.

자 우선, tidytext 패키지와 텍스트 전처리에 필요한 stringr 패키지를 R세션에 설치해 봅시다.

#install.packages("tidytext")
library(tidytext)
library(stringr)

앞서 우리는 어휘 빈도수 분석을 위해서, 여러 전처리 과정을 거쳐왔습니다. 예를 들어, 문자열에서 구두점, 숫자, 알파벳 이외의 문자 등을 정규 표현을 이용해서 매칭하고 지워주거나 바꿔주는 전처리 작업을 했었죠. 그리고 이렇게 전처리 과정을 거친 텍스트를 단어 단위로 분석하기 위해서 빈칸을 기준으로 문자열을 쪼개는 작업을 거쳤습니다. 이를 위해 str_split()이라는 함수를 사용했었죠.

tidytext 패키지의 unnest_tokens() 함수는 이 과정을 한번에 그리고 편리하게 수행할 수 있도록 해주는 기능을 수행합니다. apostrophe의 축약형을 제외한 텍스트의 구두점은 자동적으로 삭제하고, 알파벳을 소문자로 변환하고 빈칸을 기준으로 단어 단위로 텍스트를 쪼개서 그 결과값을 ‘tidy’ 데이터로 만들어 줍니다. 결국, 텍스트를 토큰 단위로 쪼개서 한 행에 하나의 토큰, 여기에선 한 단어씩 위치하게하는 데이터 프레임 형식으로 변환하는 기능을 수행하는 거죠.

tidy 데이터란 각 행에 하나의 토큰(여기에선 하나의 단어)만 위치하도록 구성된 테이블을 말합니다. 결국 tidy한 데이터를 만든다는 것은 토큰의 단위를 정하고 텍스트 데이터를 토큰화하여 각 행이 하나의 토큰으로 구성되게 만든다는 것이죠. 이렇게 tidy 데이터를 구성한다는 것은 각 행이 하나의 단어씩 가지고 있기 때문에 이후 어휘 빈도수 분석 또는 사전기반 감정분석을 수행할 때 편리하게 연계 작업할 수 있다는 장점이 있습니다.

자, 다음의 R코드는 unnest_tokens() 함수를 이용해서 cv_hls_date라는 기사 데이터를 토큰화한 과정을 보여주는데요.

cv_hls_date %>% arrange(date) # dplyr의 arrange() 함수는 해당 변수를 순차적으로 정렬 (작은 것부터); 시간의 경우 오래된 데이터부터 정렬
## # A tibble: 15,890 x 4
##    DATE    COMPANY  HEADLINE                                          date      
##    <chr>   <chr>    <chr>                                             <date>    
##  1 202009~ 매일신문 7조8천억 4차 추경 국회 문턱 넘었다                2020-09-22
##  2 202009~ 매일신문 경북 안동시, '안동형 일자리 창출 모델' 본격 추진 시동~ 2020-09-22
##  3 202009~ KBS      추경 처리 통신비 선별지원, 돌봄지원 중학생 확대   2020-09-22
##  4 202009~ 세계일보 文대통령 “방역 방해행위, 강력한 조치 취할 것”     2020-09-22
##  5 202009~ YTN      [나이트포커스] 왔다 갔다 한 '국민 통신비 지원'...최종 지원 대상은?~ 2020-09-22
##  6 202009~ 대전일보 코로나19 청정지역은 없다...안정세에 다시 방심     2020-09-22
##  7 202009~ 중앙일보 35~64세 통신비 빠지고 중학생도 돌봄 지원, 4차 추경 국회 통과...추석 전 지급~ 2020-09-22
##  8 202009~ 울산매일 여야, 4차 추경 합의 통신비 돌봄비 독감백신 ‘선별지원’~ 2020-09-22
##  9 202009~ 조선일보 7조8148억원 푼다 59년만에 4차추경안 통과          2020-09-22
## 10 202009~ 한겨레   7조8000억 규모 4차 추경 국회 본회의 통과          2020-09-22
## # ... with 15,880 more rows
cv_hls_date %>% 
  arrange(date) %>% 
  slice(1) %>% 
  unnest_tokens(word, HEADLINE, token = "words") 
## # A tibble: 10 x 4
##    DATE     COMPANY  date       word  
##    <chr>    <chr>    <date>     <chr> 
##  1 20200922 매일신문 2020-09-22 7     
##  2 20200922 매일신문 2020-09-22 조    
##  3 20200922 매일신문 2020-09-22 8     
##  4 20200922 매일신문 2020-09-22 천억  
##  5 20200922 매일신문 2020-09-22 4     
##  6 20200922 매일신문 2020-09-22 차    
##  7 20200922 매일신문 2020-09-22 추경  
##  8 20200922 매일신문 2020-09-22 국회  
##  9 20200922 매일신문 2020-09-22 문턱  
## 10 20200922 매일신문 2020-09-22 넘었다
cv_hls_date %>% 
  arrange(date) %>% 
  slice(1) %>% 
  unnest_tokens(word, HEADLINE, token = "regex",
                pattern=" ")  # dplyr의 slice() 함수는 특정 범위의 행(들)만 추출해서 보여주는 기능을 함
## # A tibble: 6 x 4
##   DATE     COMPANY  date       word    
##   <chr>    <chr>    <date>     <chr>   
## 1 20200922 매일신문 2020-09-22 7조8천억
## 2 20200922 매일신문 2020-09-22 4차     
## 3 20200922 매일신문 2020-09-22 추경    
## 4 20200922 매일신문 2020-09-22 국회    
## 5 20200922 매일신문 2020-09-22 문턱    
## 6 20200922 매일신문 2020-09-22 넘었다

이 코드는 cv_hls_date는 데이터를 unnest_tokens()함수를 이용하여, HEADLINE라는 변수의 문자열을 단어 단위로 쪼개서 word라는 새로운 변수에 저장해주라는 명령을 수행합니다. 여기에서 토큰화 단위는 즉, 문자열을 쪼개고자 하는 기본 단위는 token이라는 인자를 “words”로 설정했죠. 이를 통해 단어 단위로 문자열을 토큰화하라는 설정이 취해진 것입니다.

unnest_tokens() 함수의 기능은 문자열을 단어 단위로 토큰화하라는 것 이외에 정규 표현식을 이용해서 텍스트를 토큰화하도록 할 수 있다는 점인데요. 따라서 텍스트를 쪼개는 토큰의 단위를 다양하게 설정할 수 있는 장점이 있습니다. 예를 들어서, 우리가 다루는 기사 데이터에 경우, 키워드에 단어들이 쉼표로 구분되어 있어 이를 기준으로 토큰화 방법이 필요하고, 이 때 unnest_regex()는 유용한 기능을 합니다.

자, 이러한 tidytext 방식의 토큰화를 거치면, 뉴스 텍스트가 어휘 단위로 구분된 새로운 데이터를 얻게 되는데요. 이를 이용하면 어휘 빈도수 분석이 매우 용이합니다.

cv_tidy_count <- cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token="regex", pattern=" ") %>% 
  count(word, sort = TRUE) 
cv_tidy_count
## # A tibble: 40,833 x 2
##    word         n
##    <chr>    <int>
##  1 백신      4182
##  2 코로나    3477
##  3 접종      1755
##  4 코로나19   664
##  5 대통령     587
##  6 문         555
##  7 첫         442
##  8 “코로나    435
##  9 美         424
## 10 화이자     394
## # ... with 40,823 more rows
cv_hls_date %>% 
  unnest_regex(word, HEADLINE, pattern=" ") %>% 
  count(word, sort = TRUE) 
## # A tibble: 40,833 x 2
##    word         n
##    <chr>    <int>
##  1 백신      4182
##  2 코로나    3477
##  3 접종      1755
##  4 코로나19   664
##  5 대통령     587
##  6 문         555
##  7 첫         442
##  8 “코로나    435
##  9 美         424
## 10 화이자     394
## # ... with 40,823 more rows
cv_word_count <- cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token="words") %>% 
  count(word, sort = TRUE) 
cv_word_count
## # A tibble: 27,411 x 2
##    word       n
##    <chr>  <int>
##  1 코로나  5979
##  2 백신    5428
##  3 접종    2237
##  4 19      1144
##  5 1       1015
##  6 대통령   996
##  7 명       945
##  8 3        890
##  9 2        834
## 10 美       684
## # ... with 27,401 more rows

dplyr의 count() 함수는 해당 변수 즉 벡터를 구성하는 값들의 빈도수를 계산합니다. sort 인자는 빈도수를 내림차순을 정렬할 수 있게 합니다.

단어 빈도수가 데이터 형식으로 저장된 cv_word_count의 내용을 인덱싱

cv_word_count
## # A tibble: 27,411 x 2
##    word       n
##    <chr>  <int>
##  1 코로나  5979
##  2 백신    5428
##  3 접종    2237
##  4 19      1144
##  5 1       1015
##  6 대통령   996
##  7 명       945
##  8 3        890
##  9 2        834
## 10 美       684
## # ... with 27,401 more rows
cv_word_count[1:5,]  
## # A tibble: 5 x 2
##   word       n
##   <chr>  <int>
## 1 코로나  5979
## 2 백신    5428
## 3 접종    2237
## 4 19      1144
## 5 1       1015
cv_word_count[,1:2] # 데이터 프레임 인덱싱 방법, 대괄호에 쉼표 앞에 숫자 또는 그 범위는 해당 행의 범위, 쉼표 뛰는 해당 열의 범위
## # A tibble: 27,411 x 2
##    word       n
##    <chr>  <int>
##  1 코로나  5979
##  2 백신    5428
##  3 접종    2237
##  4 19      1144
##  5 1       1015
##  6 대통령   996
##  7 명       945
##  8 3        890
##  9 2        834
## 10 美       684
## # ... with 27,401 more rows

위의 방법으로 우리는 “코로나”와 “백신”을 포함한 기사 텍스트를 단어 단위로 토큰화한 결과의 어휘 빈도수를 확인할 수 있습니다. 여기에서 가장 많이 등장하는 단어들이 대체로 그 이유가 이해가 됩니다만, 몇 단어들은 타당하지 않거나 맥락에서 벗어나 있는 경우들도 있습니다. 또한 한자 문자열이 제거되지 않은 점도 해결해야 함.

막대 그래프 시각화

library(ggplot2)

cv_word_count %>% 
  top_n(20, n) %>% 
  ggplot(aes(x=reorder(word,n), y=n)) +
  geom_col() +
  coord_flip() + 
  geom_text(aes(label=n), hjust=-0.1, size=2) +
  labs(x=NULL) +
  theme_bw()

Wordcloud Visualization

library(wordcloud)
## Loading required package: RColorBrewer
set.seed(419)

wordcloud(words = cv_word_count$word, 
                 freq = cv_word_count$n, 
                 min.freq = 100, 
                 random.order =FALSE,
                 rot.per = 0.1,
                 scale = c(3, 0.3),
                 colors = brewer.pal(8, "Dark2"))

형태소 분석기를 활용한 토큰화 및 단어 빈도수 분석

library(RcppMeCab)

cv_hls_date$HEADLINE[1:10]
##  [1] "[사설] 고령층에도 본격화된 AZ 백신 접종"                                                  
##  [2] "AZ \"코로나백신, \u7f8e 임상서 효과 79% 혈전 위험 증가 없어\""                            
##  [3] "[생생경제] SK바사. 공모주라고 무조건 성공은 아니다. 투자 시 유의할 점"                    
##  [4] "[생생경제] 코로나의무검사보다 근로환경개선이 더 시급(우다야 라이 민주노총 이주노조위원장)"
##  [5] "\"AZ 접종 후 희귀 혈전 발생 20대 구급대원 증상 호전돼\""                                  
##  [6] "이용빈, \"미얀마 군부 폭력의 희생자에 대한 인도적 지원 시급\""                            
##  [7] "“선택적 분노 김제동 선생” 신간 리뷰 삭제 논란"                                          
##  [8] "김제동 책 비판 리뷰 삭제 논란 \"욕설도 아닌데 검열하냐\""                                 
##  [9] "LH 두고 \"부동산 적폐\"→\"누적된 관행\"  일주일만에 말바뀐 \u6587"                       
## [10] "[더뉴스] [리얼미터] 문 대통령 지지율 '최저치'...LH 사태 여파"
RcppMeCab::pos(cv_hls_date$HEADLINE[1:10])
## $`[사설] 고령층에도 본격화된 AZ 백신 접종`
##  [1] "[/SSO"      "사설/NNG"   "]/SSC"      "고령/NNG"   "층/XSN"    
##  [6] "에/JKB"     "도/JX"      "본격/XR"    "화/XSN"     "된/XSV+ETM"
## [11] "AZ/SL"      "백신/NNG"   "접종/NNG"  
## 
## $`AZ "코로나백신, \u7f8e 임상서 효과 79% 혈전 위험 증가 없어"`
##  [1] "AZ/SL"      "\"/SY"      "코로나/NNP" "백신/NNG"   ",/SC"      
##  [6] "\u7f8e/NNG" "임상/NNG"   "서/JKB"     "효과/NNG"   "79/SN"     
## [11] "%/SY"       "혈전/NNG"   "위험/NNG"   "증가/NNG"   "없/VA"     
## [16] "어/EC"      "\"/SY"     
## 
## $`[생생경제] SK바사. 공모주라고 무조건 성공은 아니다. 투자 시 유의할 점`
##  [1] "[/SSO"       "생생/MAG"    "경제/NNG"    "]/SSC"       "SK/SL"      
##  [6] "바사/NNG"    "./SY"        "공모주/NNG"  "라고/VCP+EC" "무조건/MAG" 
## [11] "성공/NNG"    "은/JX"       "아니/VCN"    "다/EF"       "./SF"       
## [16] "투자/NNG"    "시/NNG"      "유의/NNG"    "할/XSV+ETM"  "점/NNG"     
## 
## $`[생생경제] 코로나의무검사보다 근로환경개선이 더 시급(우다야 라이 민주노총 이주노조위원장)`
##  [1] "[/SSO"      "생생/MAG"   "경제/NNG"   "]/SSC"      "코로나/NNP"
##  [6] "의/JKG"     "무/XPN"     "검사/NNG"   "보다/JKB"   "근로/NNG"  
## [11] "환경/NNG"   "개선/NNG"   "이/JKS"     "더/MAG"     "시급/NNG"  
## [16] "(/SSO"      "우다/NNP"   "야/JKV"     "라이/NNG"   "민주/NNG"  
## [21] "노총/NNG"   "이/NR"      "주/NNBC"    "노조/NNG"   "위원장/NNG"
## [26] ")/SSC"     
## 
## $`"AZ 접종 후 희귀 혈전 발생 20대 구급대원 증상 호전돼"`
##  [1] "\"/SY"      "AZ/SL"      "접종/NNG"   "후/NNG"     "희귀/XR"   
##  [6] "혈전/NNG"   "발생/NNG"   "20/SN"      "대/NNBC"    "구급대/NNG"
## [11] "원/NNG"     "증상/NNG"   "호전/NNG"   "돼/XSV+EC"  "\"/SY"     
## 
## $`이용빈, "미얀마 군부 폭력의 희생자에 대한 인도적 지원 시급"`
##  [1] "이용빈/NNP"  ",/SC"        "\"/SY"       "미얀마/NNP"  "군부/NNG"   
##  [6] "폭력/NNG"    "의/JKG"      "희생자/NNG"  "에/JKB"      "대한/VV+ETM"
## [11] "인도적/NNG"  "지원/NNG"    "시급/NNG"    "\"/SY"      
## 
## $`“선택적 분노 김제동 선생” 신간 리뷰 삭제 논란`
##  [1] "“/SSO"     "선택/NNG"   "적/XSN"     "분노/NNG"   "김제동/NNP"
##  [6] "선생/NNG"   "”/SSC"     "신간/NNG"   "리뷰/NNP"   "삭제/NNG"  
## [11] "논란/NNG"  
## 
## $`김제동 책 비판 리뷰 삭제 논란 "욕설도 아닌데 검열하냐"`
##  [1] "김제동/NNP"    "책/NNG"        "비판/NNG"      "리뷰/NNP"     
##  [5] "삭제/NNG"      "논란/NNG"      "\"/SY"         "욕설/NNG"     
##  [9] "도/JX"         "아닌데/VCN+EC" "검열/NNG"      "하/XSV"       
## [13] "냐/EC"         "\"/SY"        
## 
## $`LH 두고 "부동산 적폐"→"누적된 관행"  일주일만에 말바뀐 \u6587`
##  [1] "LH/SL"       "두/VV"       "고/EC"       "\"/SY"       "부동산/NNG" 
##  [6] "적폐/NNG"    "\"→\"/SY"   "누적/NNG"    "된/XSV+ETM"  "관행/NNG"   
## [11] "\"/SY"       "일/NR"       "주일/NNBC"   "만/NNB"      "에/JKB"     
## [16] "말/NNG"      "바뀐/VV+ETM" "\u6587/NNG" 
## 
## $`[더뉴스] [리얼미터] 문 대통령 지지율 '최저치'...LH 사태 여파`
##  [1] "[/SSO"        "더/MAG"       "뉴스/NNG"     "]/SSC"        "[/SSO"       
##  [6] "리얼미터/NNP" "]/SSC"        "문/NNG"       "대통령/NNG"   "지지율/NNG"  
## [11] "'/SY"         "최저치/NNG"   "'.../SY"      "LH/SL"        "사태/NNG"    
## [16] "여파/NNG"
cv_hls_date %>% 
  slice(1:10) %>% 
  unnest_tokens(word, HEADLINE, token = pos)
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
## # A tibble: 164 x 4
##    DATE     COMPANY  date       word      
##    <chr>    <chr>    <date>     <chr>     
##  1 20210322 중부일보 2021-03-22 [/sso     
##  2 20210322 중부일보 2021-03-22 사설/nng  
##  3 20210322 중부일보 2021-03-22 ]/ssc     
##  4 20210322 중부일보 2021-03-22 고령/nng  
##  5 20210322 중부일보 2021-03-22 층/xsn    
##  6 20210322 중부일보 2021-03-22 에/jkb    
##  7 20210322 중부일보 2021-03-22 도/jx     
##  8 20210322 중부일보 2021-03-22 본격/xr   
##  9 20210322 중부일보 2021-03-22 화/xsn    
## 10 20210322 중부일보 2021-03-22 된/xsv+etm
## # ... with 154 more rows

에러가 날 경우

cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos)
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
## # A tibble: 229,232 x 4
##    DATE     COMPANY  date       word      
##    <chr>    <chr>    <date>     <chr>     
##  1 20210322 중부일보 2021-03-22 [/sso     
##  2 20210322 중부일보 2021-03-22 사설/nng  
##  3 20210322 중부일보 2021-03-22 ]/ssc     
##  4 20210322 중부일보 2021-03-22 고령/nng  
##  5 20210322 중부일보 2021-03-22 층/xsn    
##  6 20210322 중부일보 2021-03-22 에/jkb    
##  7 20210322 중부일보 2021-03-22 도/jx     
##  8 20210322 중부일보 2021-03-22 본격/xr   
##  9 20210322 중부일보 2021-03-22 화/xsn    
## 10 20210322 중부일보 2021-03-22 된/xsv+etm
## # ... with 229,222 more rows

명사만 선택

cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos) %>% 
  filter(str_detect(word, "[/]n|[/]sl"))
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
## # A tibble: 124,889 x 4
##    DATE     COMPANY  date       word      
##    <chr>    <chr>    <date>     <chr>     
##  1 20210322 중부일보 2021-03-22 사설/nng  
##  2 20210322 중부일보 2021-03-22 고령/nng  
##  3 20210322 중부일보 2021-03-22 az/sl     
##  4 20210322 중부일보 2021-03-22 백신/nng  
##  5 20210322 중부일보 2021-03-22 접종/nng  
##  6 20210322 중앙일보 2021-03-22 az/sl     
##  7 20210322 중앙일보 2021-03-22 코로나/nnp
##  8 20210322 중앙일보 2021-03-22 백신/nng  
##  9 20210322 중앙일보 2021-03-22 美/nng    
## 10 20210322 중앙일보 2021-03-22 임상/nng  
## # ... with 124,879 more rows

태그 정보 제거

cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos) %>% 
  filter(str_detect(word, "[/]n|[/]sl")) %>% 
  mutate(word = str_remove(word, "[/]\\w+"))
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
## # A tibble: 124,889 x 4
##    DATE     COMPANY  date       word  
##    <chr>    <chr>    <date>     <chr> 
##  1 20210322 중부일보 2021-03-22 사설  
##  2 20210322 중부일보 2021-03-22 고령  
##  3 20210322 중부일보 2021-03-22 az    
##  4 20210322 중부일보 2021-03-22 백신  
##  5 20210322 중부일보 2021-03-22 접종  
##  6 20210322 중앙일보 2021-03-22 az    
##  7 20210322 중앙일보 2021-03-22 코로나
##  8 20210322 중앙일보 2021-03-22 백신  
##  9 20210322 중앙일보 2021-03-22 美    
## 10 20210322 중앙일보 2021-03-22 임상  
## # ... with 124,879 more rows

‘코로나’ 및 ‘백신’ 단어 제거

cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos) %>% 
  filter(str_detect(word, "[/]n")) %>% 
  mutate(word = str_remove(word, "[/]\\w+")) %>% 
  filter(!str_detect(word, "코로나|백신"))
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
## # A tibble: 110,108 x 4
##    DATE     COMPANY  date       word 
##    <chr>    <chr>    <date>     <chr>
##  1 20210322 중부일보 2021-03-22 사설 
##  2 20210322 중부일보 2021-03-22 고령 
##  3 20210322 중부일보 2021-03-22 접종 
##  4 20210322 중앙일보 2021-03-22 美   
##  5 20210322 중앙일보 2021-03-22 임상 
##  6 20210322 중앙일보 2021-03-22 효과 
##  7 20210322 중앙일보 2021-03-22 혈전 
##  8 20210322 중앙일보 2021-03-22 위험 
##  9 20210322 중앙일보 2021-03-22 증가 
## 10 20210322 YTN      2021-03-22 경제 
## # ... with 110,098 more rows

단어 빈도수 세기

cv_hls_tidy <- cv_hls_date %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos) %>% 
  filter(str_detect(word, "[/]n")) %>% 
  mutate(word = str_remove(word, "[/]\\w+")) %>% 
  filter(!str_detect(word, "코로나|백신"))
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
cv_hls_tidy %>% count(word, sort=TRUE)
## # A tibble: 10,011 x 2
##    word       n
##    <chr>  <int>
##  1 접종    3338
##  2 명      1727
##  3 대통령  1186
##  4 만      1066
##  5 확진     733
##  6 월       707
##  7 일       704
##  8 문       703
##  9 방역     701
## 10 美       683
## # ... with 10,001 more rows

막대그래프 만들기

cv_hls_tidy %>% 
  count(word, sort=T) %>% 
  top_n(20, n) %>% 
  ggplot(aes(x = reorder(word, n), y = n)) +
  geom_col() +
  coord_flip() +
  geom_text(aes(label = n), size = 2,
            hjust=-0.1) +
  labs(x=NULL) +
  theme_bw()

집단 비교 분석

시기별 백신 보도 비교: 백신 접종 개시 전과 후의 보도 태도

cv_hls_period <- cv_hls_tidy %>% 
  mutate(period = ifelse(date < as.Date("2021-02-26"),
                         "접종전","접종후")) %>% 
  group_by(period) %>% 
  count(word, sort=T) %>% 
  top_n(20, n) %>% 
  arrange(period)
cv_hls_period
## # A tibble: 41 x 3
## # Groups:   period [2]
##    period word       n
##    <chr>  <chr>  <int>
##  1 접종전 접종    2344
##  2 접종전 명      1252
##  3 접종전 대통령  1066
##  4 접종전 만       862
##  5 접종전 문       638
##  6 접종전 방역     635
##  7 접종전 월       627
##  8 접종전 美       596
##  9 접종전 文       588
## 10 접종전 일       572
## # ... with 31 more rows

막대그래프 비교

cv_hls_period %>% 
  ggplot(aes(x=reorder(word, n), y=n, fill=period)) +
  geom_col() +
  coord_flip() +
  facet_wrap(~period)

# 그래프별로 y축 설정하기

cv_hls_period %>% 
  ggplot(aes(x=reorder(word, n), y=n, fill=period)) +
  geom_col() +
  coord_flip() +
  facet_wrap(~period, scales="free_y") #y축 통일하지 않음음

막대그래프 축 정렬하기

# `reorder_within()` 축 순서를 항목별로 따로 구할 때 
cv_hls_period %>% 
  ggplot(aes(x=reorder_within(word, n, period), 
             y=n, fill=period)) + 
  geom_col() +
  coord_flip() +
  facet_wrap(~period, scales="free_y")

# 항목별 표시 제거하기
cv_hls_period %>% 
  ggplot(aes(x=reorder_within(word, n, period), 
             y=n, fill=period)) + 
  geom_col() +
  coord_flip() +
  facet_wrap(~period, scales="free_y") +
  scale_x_reordered() + # 항목이름 제거
  scale_fill_discrete("시기") +
  labs(x=NULL,y="빈도수")

집단 간 차이 비교 2: 오즈비 - 상대적으로 중요한 단어 비교

아스트라제네카 vs. 화이자

cv_hls_az <- cv_hls_date %>% 
  filter(str_detect(HEADLINE, "AZ|아스트라")) %>% 
  mutate(type="AZ")

cv_hls_pf <- cv_hls_date %>% 
  filter(str_detect(HEADLINE, "화이자")) %>% 
  mutate(type="Pfizer")

cv_hls_type <- bind_rows(cv_hls_az, cv_hls_pf) %>% 
  unnest_tokens(word, HEADLINE, token = RcppMeCab::pos) %>% 
  filter(str_detect(word, "[/]n")) %>% 
  mutate(word = str_remove(word, "[/]\\w+")) %>% 
  filter(!str_detect(word, "코로나|백신"))
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
# long form 데이터
cv_hls_type %>% count(type, word, sort=T)
## # A tibble: 1,646 x 3
##    type   word         n
##    <chr>  <chr>    <int>
##  1 Pfizer 화이자     569
##  2 AZ     아스트라   337
##  3 AZ     접종       249
##  4 Pfizer 접종       205
##  5 AZ     카         198
##  6 AZ     제         178
##  7 Pfizer 명          83
##  8 Pfizer 승인        83
##  9 Pfizer 효과        72
## 10 AZ     세          70
## # ... with 1,636 more rows
# wide form 데이터
cv_hls_type %>% count(type, word, sort=T) %>%
  pivot_wider(names_from = type, 
              values_from = n)
## # A tibble: 1,261 x 3
##    word     Pfizer    AZ
##    <chr>     <int> <int>
##  1 화이자      569    49
##  2 아스트라     33   337
##  3 접종        205   249
##  4 카           17   198
##  5 제           16   178
##  6 명           83    44
##  7 승인         83    41
##  8 효과         72    38
##  9 세           29    70
## 10 모더         63     7
## # ... with 1,251 more rows
cv_hls_type %>% count(type, word, sort=T) %>%
  pivot_wider(names_from = type, 
              values_from = n,
              values_fill = list(n = 0)) # NA값을 0으로 치환
## # A tibble: 1,261 x 3
##    word     Pfizer    AZ
##    <chr>     <int> <int>
##  1 화이자      569    49
##  2 아스트라     33   337
##  3 접종        205   249
##  4 카           17   198
##  5 제           16   178
##  6 명           83    44
##  7 승인         83    41
##  8 효과         72    38
##  9 세           29    70
## 10 모더         63     7
## # ... with 1,251 more rows
cv_frequency_wide <-  cv_hls_type %>% 
  count(type, word, sort=T) %>%
  pivot_wider(names_from = type, 
              values_from = n,
              values_fill = list(n = 0))
cv_frequency_wide
## # A tibble: 1,261 x 3
##    word     Pfizer    AZ
##    <chr>     <int> <int>
##  1 화이자      569    49
##  2 아스트라     33   337
##  3 접종        205   249
##  4 카           17   198
##  5 제           16   178
##  6 명           83    44
##  7 승인         83    41
##  8 효과         72    38
##  9 세           29    70
## 10 모더         63     7
## # ... with 1,251 more rows

오즈비 구하기

오즈비(odds ratio)는 어떤 사건이 A 조건에서 발생할 확률이 B 조건에서 발생할 확률에 비해 얼마나 더 큰지를 나타낸 값.

  1. 각 단어 비중 나타내는 변수 추가: 우선, 각 집단별로 ’각 단어의 빈도’를 ’모든 단어 빈도의 합’으로 나눈다.
cv_frequency_wide <- cv_frequency_wide %>% 
  mutate(ratio_az = ((AZ+1)/(sum(AZ+1))),
         ratio_pf = ((Pfizer+1)/sum(Pfizer+1)))
cv_frequency_wide
## # A tibble: 1,261 x 5
##    word     Pfizer    AZ ratio_az ratio_pf
##    <chr>     <int> <int>    <dbl>    <dbl>
##  1 화이자      569    49  0.0102   0.107  
##  2 아스트라     33   337  0.0690   0.00639
##  3 접종        205   249  0.0511   0.0387 
##  4 카           17   198  0.0406   0.00338
##  5 제           16   178  0.0366   0.00319
##  6 명           83    44  0.00919  0.0158 
##  7 승인         83    41  0.00858  0.0158 
##  8 효과         72    38  0.00797  0.0137 
##  9 세           29    70  0.0145   0.00563
## 10 모더         63     7  0.00163  0.0120 
## # ... with 1,251 more rows
  1. 오즈비 변수 추가: 각 단어가 집단에서 차지하는 비중을 그 단어가 다른 집단에서 차지하는 비중으로 나누면 됨.
cv_frequency_wide <- cv_frequency_wide %>% 
  mutate(odds_ratio = ratio_az/ratio_pf)
cv_frequency_wide
## # A tibble: 1,261 x 6
##    word     Pfizer    AZ ratio_az ratio_pf odds_ratio
##    <chr>     <int> <int>    <dbl>    <dbl>      <dbl>
##  1 화이자      569    49  0.0102   0.107       0.0954
##  2 아스트라     33   337  0.0690   0.00639    10.8   
##  3 접종        205   249  0.0511   0.0387      1.32  
##  4 카           17   198  0.0406   0.00338    12.0   
##  5 제           16   178  0.0366   0.00319    11.4   
##  6 명           83    44  0.00919  0.0158      0.583 
##  7 승인         83    41  0.00858  0.0158      0.544 
##  8 효과         72    38  0.00797  0.0137      0.581 
##  9 세           29    70  0.0145   0.00563     2.57  
## 10 모더         63     7  0.00163  0.0120      0.136 
## # ... with 1,251 more rows
  1. 정렬해서 상대적 중요 단어 살펴보기
cv_frequency_wide %>% arrange(odds_ratio) # Pfizer 그룹에서 빈번한 단어
## # A tibble: 1,261 x 6
##    word     Pfizer    AZ ratio_az ratio_pf odds_ratio
##    <chr>     <int> <int>    <dbl>    <dbl>      <dbl>
##  1 기술         15     0 0.000204  0.00301     0.0680
##  2 탈취         13     0 0.000204  0.00263     0.0777
##  3 화이자      569    49 0.0102    0.107       0.0954
##  4 국정원       10     0 0.000204  0.00207     0.0989
##  5 이스라엘     10     0 0.000204  0.00207     0.0989
##  6 日           10     0 0.000204  0.00207     0.0989
##  7 증시          9     0 0.000204  0.00188     0.109 
##  8 트럼프        9     0 0.000204  0.00188     0.109 
##  9 미화원        8     0 0.000204  0.00169     0.121 
## 10 일본          8     0 0.000204  0.00169     0.121 
## # ... with 1,251 more rows
cv_frequency_wide %>% arrange(-odds_ratio) # AZ 그룹에서 빈번한 단어
## # A tibble: 1,261 x 6
##    word     Pfizer    AZ ratio_az ratio_pf odds_ratio
##    <chr>     <int> <int>    <dbl>    <dbl>      <dbl>
##  1 혈전          0    26  0.00551 0.000188      29.4 
##  2 대통령        0    19  0.00408 0.000188      21.7 
##  3 카           17   198  0.0406  0.00338       12.0 
##  4 제           16   178  0.0366  0.00319       11.4 
##  5 고령          2    29  0.00613 0.000563      10.9 
##  6 미만          0     9  0.00204 0.000188      10.9 
##  7 출하          0     9  0.00204 0.000188      10.9 
##  8 아스트라     33   337  0.0690  0.00639       10.8 
##  9 논란          1    18  0.00388 0.000376      10.3 
## 10 제네          2    25  0.00531 0.000563       9.42
## # ... with 1,251 more rows
  1. 막대그래프 만들기
# 오즈비가 가장 높거나 낮은 단어 추출하기
?rank
## starting httpd help server ... done
cv_frequency_wide %>% filter(rank(odds_ratio)==1)
## # A tibble: 1 x 6
##   word  Pfizer    AZ ratio_az ratio_pf odds_ratio
##   <chr>  <int> <int>    <dbl>    <dbl>      <dbl>
## 1 기술      15     0 0.000204  0.00301     0.0680
cv_frequency_wide %>% filter(rank(-odds_ratio)==1)
## # A tibble: 1 x 6
##   word  Pfizer    AZ ratio_az ratio_pf odds_ratio
##   <chr>  <int> <int>    <dbl>    <dbl>      <dbl>
## 1 혈전       0    26  0.00551 0.000188       29.4
cv_frequency_wide %>% 
  filter(rank(odds_ratio) <= 20 | rank(-odds_ratio) <= 20)
## # A tibble: 42 x 6
##    word     Pfizer    AZ ratio_az ratio_pf odds_ratio
##    <chr>     <int> <int>    <dbl>    <dbl>      <dbl>
##  1 화이자      569    49  0.0102  0.107        0.0954
##  2 아스트라     33   337  0.0690  0.00639     10.8   
##  3 카           17   198  0.0406  0.00338     12.0   
##  4 제           16   178  0.0366  0.00319     11.4   
##  5 모더         63     7  0.00163 0.0120       0.136 
##  6 고령          2    29  0.00613 0.000563    10.9   
##  7 혈전          0    26  0.00551 0.000188    29.4   
##  8 제네          2    25  0.00531 0.000563     9.42  
##  9 대통령        0    19  0.00408 0.000188    21.7   
## 10 논란          1    18  0.00388 0.000376    10.3   
## # ... with 32 more rows
# 그룹 표시
cv_frequency_type <- cv_frequency_wide %>% 
  filter(rank(odds_ratio) <= 20 | rank(-odds_ratio) <= 20) %>% 
  mutate(type = ifelse(odds_ratio > 1, "AZ", "Pfizer"),
         n = ifelse(odds_ratio > 1, AZ, Pfizer))

# 시각화
cv_frequency_type %>% 
  filter(rank(odds_ratio) <= 20 | rank(-odds_ratio) <= 20) %>% 
  ggplot(aes(x=reorder_within(word, n, type), 
             y=n, fill=type)) + 
  geom_col() +
  coord_flip() +
  facet_wrap(~type, scales="free") + # "free" 그래프별로 x & y축
  scale_x_reordered() + # 항목이름 제거
  scale_fill_discrete("시기") +
  labs(x=NULL,y="빈도수")