1. 분석 주제 (연구문제 설정)

1.1 청와대 국민 청원 게시글과 관련된 주제를 선정한 이유 & 목표

2020년부터 발생한 코로나19 사태는 지금까지 지속되어오고 있습니다. 코로나19는 예상과는 다르게 전세계적으로 퍼지기 시작하였고, 대부분의 사람들의 생활에도 많은 변화를 가져왔습니다. 이로 인해 코로나19 팬데믹이 발생하였고, 전세계적으로 뉴노멀의 시대를 열었습니다. 개개인의 삶에만 많은 변화를 일으킨 것이 아니라, 사람들의 정서에도 많은 영향을 끼치고 있습니다. 대표적으로 기독교에 대한 시선이 좋지 않아졌고, 가게를 운영하는 자영업자들은 손님들이 크게 줄어 문을 닫거나 재정적인 어려움을 겪고있지만, 반대로 배달에 대한 수요가 크게 늘어 배달 대행업체와 온라인 회사들의 배달, 배송 건수가 이전보다 대폭 상승하였습니다. 비 IT 업계는 구조조정을 통해 기존 인력을 줄이고 새로운 인력 고용 또한 줄이고 있지만, 반대로 IT 업계는 호황을 누리며 대규모 IT 인력 채용을 진행하고 있습니다. 코로나19 로 인하여 많은 것들이 변하고 있는 상황 속에서, 저희 팀은 코로나19 발생 이전과 이후의 차이가 있는 것을 주제로 삼고 싶었습니다.

청와대 국민 청원 게시판은 국민들이 청원을 올리고 난 후 30일 안에 20만명 이상의 동의를 얻게 되면, 청와대에서 직접 답변을 하는 시스템을 갖추고 있습니다. 그렇기 때문에 국민 들이 올리는 청원은 국민들의 당시 요구 사항들을 잘 나타낸다고 볼수 있습니다. 이러한 이유로 저희 팀은 청와대 국민 청원 게시글 데이터를 이용하여 분석을 진행하기로 하였습니다. 코로나19는 특별히 자영업자 분들 에게 막대한 경제적 피해를 입히고 있고 정부의 여러 행정 조치들에 공감하지 못하고 있다는 것을 뉴스나 SNS를 통해 흔하게 접할 수 있었습니다. 따라서 코로나19 전후로 자영업/소상공인 키워드를 기준으로 청원 게시글을 분석하기로 하였습니다. 코로나 19 이후 자영업자들의 불만이 더욱 높아졌을 것으로 예상 되는데 이번 분석을 통해 알아보고자 합니다.

2. 데이터 수집 및 가공

2.1 패키지 불러오기

# 크롤링 패키지
library(RSelenium)
library(rvest)
library(httr)
library(xml2)
# data manipulation
library(tidyverse)
library(data.table)
# 그래프 그리는 패키지
library(gridExtra)
library(ggpubr)
library(RColorBrewer)
library(ggplot2)
library(wordcloud2)
library(webshot)
library(htmlwidgets)
# 형태소 분석
library(NLP4kec)
library(rJava)
library(tm)
# 네트워크 맵
library(network)
library(sna)
library(GGally)
# 텍스트 분석
library(tidytext)

2.2 데이터 크롤링

국내 코로나 확진자가 나온 시점에서 1년전인 2019/1/20 부터 1년 후인 2021/1/20 까지의 국민 청원 게시글에 청원번호(id) , 청원 카테고리(category), 청원 제목(title), 청원 만료 날짜(expiryDate), 청원 동의수(numOfAgrees) 총 5가지 데이터를 수집하였습니다. 총 85,856개의 청원 게시글을 수집할 수 있었습니다.

url = "http://www1.president.go.kr/petitions/?c=0&only=2&page="
# 2021/4/14일을 기준
pages <- 1821:12610
newUrl <- paste0(url,pages)
df <- data.frame(matrix(ncol=5,nrow=0))
# eval = F로 보고서 제출시 크롤링 부분은 실행 생략
# 셀리니움 초기 설정
remDr <- remoteDriver(remoteServerAddr = "localhost", port=4445L, browserName = "chrome")
remDr$open()

# 청원목록 긁어오기
while(length(newUrl) > 0){
  # 사이트 띄우기
  remDr$navigate(newUrl[1])
  
  # html 가져오기
  src <- remDr$getPageSource()[[1]]
  html <- read_html(src)
  
  ### 번호 ###
  nodes <- html_nodes(html, 'div.ct_list1 > div.board > div.b_list > div.bl_body > ul.petition_list > li > div.bl_wrap > div.bl_no')
  res <- html_text(nodes)
  no <- gsub("번호 ","",res)
  
  ### 분류 ###
  nodes <- html_nodes(html, 'div.ct_list1 > div.board > div.b_list > div.bl_body > ul.petition_list > li > div.bl_wrap > div.wv_category:not(.sound_only)')
  res <- html_text(nodes)
  category <- gsub("분류 ","",res)
  
  ### 제목 ###
  nodes <- html_nodes(html, 'div.ct_list1 > div.board > div.b_list > div.bl_body > ul.petition_list > li > div.bl_wrap > div.bl_subject > a')
  res <- html_text(nodes)
  title <- gsub("제목 ","",res)
  
  ### 청원 종료일 ###
  nodes <- html_nodes(html, 'div.ct_list1 > div.board > div.b_list > div.bl_body > ul.petition_list > li > div.bl_wrap > div.bl_date')
  res <- html_text(nodes)
  expDate <- gsub("청원 종료일 ","",res)
  
  ### 참여인원 ###
  nodes <- html_nodes(html, 'div.ct_list1 > div.board > div.b_list > div.bl_body > ul.petition_list > li > div.bl_wrap > div.bl_agree')
  res <- html_text(nodes)
  noOfPetition <- gsub("참여인원 ","",res)
  
  # 데이터프레임으로 모으기
  petitions <- data.frame(no,category,title,expDate,noOfPetition)
  # 열 이름이 같아야 데이터 프레임을 합칠 수 있음
  colnames(petitions) <- c("V1","V2","V3","V4","V5")
  df <- rbind(df, petitions)
  
  # 최대한 서버 막히지 않게 n초에 한번씩 시도
  Sys.sleep(3)
  
  # 다음 페이지
  newUrl <- newUrl[-1]
}
remDr$close()
# 파일 내보내기
write.csv(df,"data/petition_data.csv", row.names = F)

2.3 데이터 정제

# 데이터 불러오기
df <- read_csv("data/petition_data.csv")
# 중복 관측값 제거
df <- unique(df)
colName <- c("id", "category", "title", "expiryDate", "numOfAgrees")
colnames(df) <- colName
# 데이터 보여주기
df %>% str
## tibble [85,856 × 5] (S3: tbl_df/tbl/data.frame)
##  $ id         : num [1:85856] 449459 449458 449457 449456 449455 ...
##  $ category   : chr [1:85856] "교통/건축/국토" "행정" "인권/성평등" "보건복지" ...
##  $ title      : chr [1:85856] "불법건축물 양성화 특별법 발의 청원" "‘자의로’ 시험을 거부한 의대생의 구제를 반대합니다" "가습기살균제.정부의 비리와 가해기업,검찰의 유착 관계를 밝히는데 도움부탁드립니다." "강원도 코로나19에 대해서 몇글자 적어봅니다" ...
##  $ expiryDate : Date[1:85856], format: "2021-01-20" "2021-01-20" ...
##  $ numOfAgrees: num [1:85856] 319 10527 1747 530 23488 ...
# 형 변환
df$expiryDate <- as.Date(df$expiryDate)
df$numOfAgrees <- gsub("\\W","",df$numOfAgrees) %>% as.integer

# 코로나 이전과 이후 데이터로 분리
beforeCovid <- df[which(df$expiryDate<"2020-1-20"),]
afterCovid <- df[which(df$expiryDate>="2020-1-20"),]

# 코로나 이전 데이터
beforeCovid %>% str
## tibble [75,162 × 5] (S3: tbl_df/tbl/data.frame)
##  $ id         : num [1:75162] 438768 438767 438766 438765 438764 ...
##  $ category   : chr [1:75162] "저출산/고령화대책" "교통/건축/국토" "교통/건축/국토" "기타" ...
##  $ title      : chr [1:75162] "임신 전기간 단축 근무 시행" "상주영천고속도로 사고(블랙아이스) 투명한 수사를 청원합니다" "서울2호선연장선 (홍대-원종-작전-루원-청라) 적극 추진 청원 드립니다." "*** 5g 통신 요금은 고객에게로 환불되어야 한다." ...
##  $ expiryDate : Date[1:75162], format: "2020-01-19" "2020-01-19" ...
##  $ numOfAgrees: int [1:75162] 759 1357 1152 443 598 19542 123152 6512 343 360 ...
# 코로나 이후 데이터
afterCovid %>% str
## tibble [10,694 × 5] (S3: tbl_df/tbl/data.frame)
##  $ id         : num [1:10694] 449459 449458 449457 449456 449455 ...
##  $ category   : chr [1:10694] "교통/건축/국토" "행정" "인권/성평등" "보건복지" ...
##  $ title      : chr [1:10694] "불법건축물 양성화 특별법 발의 청원" "‘자의로’ 시험을 거부한 의대생의 구제를 반대합니다" "가습기살균제.정부의 비리와 가해기업,검찰의 유착 관계를 밝히는데 도움부탁드립니다." "강원도 코로나19에 대해서 몇글자 적어봅니다" ...
##  $ expiryDate : Date[1:10694], format: "2021-01-20" "2021-01-20" ...
##  $ numOfAgrees: int [1:10694] 319 10527 1747 530 23488 382 3458 430 1473 363 ...

크롤링은 셀레니움을 사용해 진행 하였으며 수집한 데이터는 데이터프레임에 저장하고 날짜 데이터와 청원 동의수 데이터는 각각 날짜 타입의 데이터, 정수 타입의 데이터로 변환하여 주었습니다. 그리고 2020/1/20일 기준으로 코로나 이전과 이후 데이터프레임을 생성하여 데이터를 두개로 나누었습니다. 코로나 이전 데이터는 총 75,162개의 청원 게시글이 있고, 코로나 이후 데이터는 총 10,694개의 청원 게시글이 있습니다. 코로나 이전 1년동안 게시된 청원의 갯수가 코로나 이후 1년동안 게시된 청원의 갯수보다 대 7.5배 차이가 남을 알 수 있습니다.

3. 기술통계

수집한 데이터를 그래프들을 통해 코로나 이전과 이후 데이터가 어떤 모습인지 알아보았습니다.

3.1 청원 동의수 top 5

# 카테고리만 추출하기
category <- unique(afterCovid$category)

# 코로나 이전 청원 Top 5
beforeCovid %>% 
    arrange(desc(numOfAgrees)) %>% 
    top_n(5)
# 코로나 이후 청원 Top 5
afterCovid %>% 
    arrange(desc(numOfAgrees)) %>% 
    top_n(5)

코로나 이전에 동의수를 가장 많이 받은 청원 5개와 코로나 이후에 동의수를 가장 많이 받은 청원 5개를 확인할 수 있었습니다. 코로나 이전에는 정치에 관한 청원들이 동의를 많이 얻었음을 알 수 있습니다. 코로나 이후에는 n번방 사건에 대한 청원과 정치와 관련된 청원들이 많은 동의를 얻었습니다.

3.2 카테고리별 청원 동의수

# 코로나 이전
bca <- ggplot(beforeCovid, aes(category, numOfAgrees , color = category)) + # bca : before covid agrees
  geom_segment(aes(xend=category, yend=0)) + 
  geom_point(size=1.5)+coord_flip() + 
  labs(title="코로나 이전", x ="", y = "청원 동의수") +
  theme(plot.title = element_text(hjust = 0.5), legend.position = "none")+
  theme(axis.text.x = element_text(angle = 30, vjust = 1, hjust=1))
#코로나 이후
aca <- ggplot(afterCovid, aes(category, numOfAgrees , color = category)) + # aca : after covid agrees
  geom_segment(aes(xend=category, yend=0)) + 
  geom_point(size=1.5)+coord_flip() + 
  labs(title="코로나 이후", x ="", y = "청원 동의수") +
  theme(plot.title = element_text(hjust = 0.5), legend.position = "none")+
  theme(axis.text.x = element_text(angle = 30, vjust = 1, hjust=1))

title <- text_grob("카테고리별 청원 동의수", size = 16, face = "bold")

grid.arrange(bca, aca, ncol=2, top=title)

코로나 이전에는 정치개혁 카테고리의 청원들이 가장많은 동의수를 얻었습니다. 코로나 이후에는 여러 카테고리에서 동의수를 얻었는데 인권/성평등, 안전/환경 부분에서 동의수를 많이 얻었음을 볼수있습니다.

3.3 카테고리별 비율

국민 청원 게시글에는 게시자가 지정할 수 있는 카테고리가 17가지 있습니다. 카테고리별로 청원 게시글 갯수와 전체 청원 게시글 갯수와의 비율을 구하여 아래 그래프를 사용하여 나타냈습니다.

# 코로나 이전
b4Ratio <- beforeCovid %>% 
            group_by(category) %>% 
              summarise(freq = n()) %>% 
                mutate(ratio = round(freq / sum(freq),4)*100)
# 코로나 이후
acRatio <- afterCovid %>% 
            group_by(category) %>% 
              summarise(freq = n()) %>% 
                mutate(ratio = round(freq / sum(freq),4)*100)

acBcRatio <- bind_cols(category, b4Ratio$ratio, acRatio$ratio)
colnames(acBcRatio) <- c("category","before","after")

# 그래프를 그리기 위해 코로나 이전, 이후 청원 건수 비율을 long 으로 변경
acBcRatioLong <- pivot_longer(acBcRatio,cols = 2:3, names_to = "period", values_to = "ratio")
# 코로나 이전 이후 순서 정하기
acBcRatioLong$period <- factor(acBcRatioLong$period, levels=c("before","after"), order = T)

# 그래프
acBcRatioLong %>% ggbarplot("category", "ratio",
                          fill = "period", 
                          color = "period",
                          xlab = "",
                          ylab = "",
                          palette = "Paired",
                          position = position_dodge(0.9)) %>% 
                ggpar(x.text.angle = 45, font.family=font) %>% 
                  annotate_figure(top=text_grob("코로나 이전과 이후 카테고리별 비율", face="bold", family=font, size=14))

‘안전/환경’ 카테고리의 비율이 굉장히 높아진걸 알 수 있고 그에 반해 ‘농산어촌’ 카테고리는 비율이 많이 줄은것을 알 수 있습니다. 코로나로 인해 ‘안전/환경’ 카테고리에 이전보다 많은 청원들이 올라왔을 것이라 생각됩니다.

4. 텍스트 분석

4.1 데이터 추출

자영업과 소상공인 키워드로 청원 게시글들을 추출 하였습니다.

# 찾으려는 키워드
pattern <- "자영업|소상공인"
before_cat <- beforeCovid[grep(pattern, beforeCovid$title),]
after_cat <- afterCovid[grep(pattern, afterCovid$title),]
total_cat <- rbind(before_cat, after_cat)

# 데이터 확인
# 코로나 이전 '자영업/소상공인' 키워드 데이터
before_cat %>% head
# 코로나 이후 '자영업/소상공인' 키워드 데이터
after_cat %>% head
# category 확인
sort(unique(before_cat$category))
##  [1] "경제민주화"          "교통/건축/국토"      "기타"               
##  [4] "농산어촌"            "문화/예술/체육/언론" "미래"               
##  [7] "보건복지"            "성장동력"            "안전/환경"          
## [10] "외교/통일/국방"      "육아/교육"           "인권/성평등"        
## [13] "일자리"              "저출산/고령화대책"   "정치개혁"           
## [16] "행정"
sort(unique(after_cat$category))
##  [1] "경제민주화"          "교통/건축/국토"      "기타"               
##  [4] "문화/예술/체육/언론" "미래"                "반려동물"           
##  [7] "보건복지"            "성장동력"            "안전/환경"          
## [10] "육아/교육"           "인권/성평등"         "일자리"             
## [13] "정치개혁"            "행정"
length(unique(before_cat$category))
## [1] 16
length(unique(after_cat$category))
## [1] 14

코로나 이전에는 16개의 카테고리에 자영업/소상공인에 관한 청원들이 게시되었고 코로나 이후에는 14개 카테고리에 청원들이 게시되었습니다.

같은 주제이지만 청원 게시자가 임의로 카테고리를 설정하면서 발생한 상황으로 해석됩니다. 이는 키워드에 대한 분석 시, 하나의 카테고리만을 분석대상으로 잡으면 키워드의 대표성을 보장할 수 없음을 시사합니다. 따라서 저희는 하나의 카테고리가 아닌 키워드를 포함한 모든 카테고리에 대한 분석을 시행했습니다.

4.2 자영업/소상공인 키워드에 대한 기술 통계

기술통계를 위한 데이터 셋을 생성하였습니다. 해당 데이터 셋엔 자영업/소상공인을 포함한 title이 위치한 category, 각 카테고리별 빈도수를 나타낸 freq, 비율을 나타낸 relative, 코로나 전에 발생한 청원인지 혹은 후에 발생한 청원인지 알려주는 period 그리고 각 카테고리 별로 집계된 청원동의 인원수의 합계인 AgreeSum이 있습니다.

# 카테고리별 청원건수 빈도와 비율 확인후 데이터프레임 생성
before_df<- cbind(freq= table(before_cat$category), relative= prop.table(table(before_cat$category)))
before_df<- as.data.frame(before_df)

after_df<- cbind(freq= table(after_cat$category), relative= prop.table(table(after_cat$category)))
after_df<- as.data.frame(after_df)
# 비율기준으로 내림차순 정렬
before_df<- before_df[c(order(-rank(before_df$relative))),]
# 코로나 이전 '자영업/소상공인' 키워드 청원 건수 비율
before_df %>% head
after_df<- after_df[c(order(-rank(after_df$relative))),]
# 코로나 이후 '자영업/소상공인' 키워드 청원 건수 비율
after_df %>% head
## rownames가 카테고리로 되어있기 때문에 카테고리를 변수로 빼고 새로운 행이름 설정
setDT(before_df, keep.rownames = TRUE)[] %>% head
## 변수명이 rn으로 빼진 카테고리 변수명을 category로 재설정
colnames(before_df)[1] <- "category"
## df에 period 변수 생성
before_df<- before_df %>%
  mutate(period="before")

# 코로나 이후 데이터도 동일하게 적용
setDT(after_df, keep.rownames = TRUE)[] %>% head
colnames(after_df)[1] <- "category"
after_df<- after_df %>%
  mutate(period="after")

# 카테고리별 청원동의 인원수 변수 생성 후 before_df, after_df를 total_df로 통합
temp<- data.frame()
temp<- beforeCovid %>%
  group_by(category) %>%
  summarise(AgreeSum= sum(numOfAgrees))
before_df<- merge(before_df, temp, by='category')

temp<- afterCovid %>%
  group_by(category) %>%
  summarise(AgreeSum= sum(numOfAgrees))
after_df<- merge(after_df, temp, by='category')
total_df<- rbind(after_df,before_df)
# 전체 '자영업/소상공인' 키워드 청원 건수와 동의수 데이터 출력
total_df %>% head
# 비율 차이 그래프
ggplot(total_df, aes(x= category, y= relative, fill=period))+
  geom_bar(stat="identity",position= position_dodge2(reverse = TRUE))+
  scale_fill_manual(values=c("#0484bc","#a4d4e4"))+
  labs(title="코로나 전후 자영업 소상공인 키워드에 대한 \n카테고리 별 청원 비율")+
  theme_bw()+
  theme(plot.title = element_text(hjust = 0.5, face='bold', size = 15))+
  theme(axis.text.x = element_text(angle = 90, hjust = 1, vjust = 0.5 ))+
  theme(legend.text = element_text(size = 8))+
  guides(fill=guide_legend(reverse=TRUE))+
  theme(text = element_text(family = "NanumGothic"),
        axis.text.x = element_text(angle = 30, vjust = 1, hjust=1))

일자리 카테고리에 올린 청원 비율이 코로나 이후로 많이 줄어들었고, 반대로 행정 카테고리에 올린 청원 비율이 많이 올라갔습니다.

이는 자영업/소상공인 키워드를 가진 청원의 성격이 일반적인 일자리 개념에서 행정적인 개념으로 옮겨갔음을 의미한다고 볼 수 있겠습니다.

장기화 되가는 사회적 거리두기 방침과 5인 이상 사적모임 금지, 그리고 단축 영업 같은 행정조치로 인해 발생한 상황으로 해석됩니다.

또한 ’자영업/소상공인’과 관련된 청원 게시글에 어떤 차이가 있는지 그래프를 통해 알아보았습니다. 먼저 코로나 이전과 이후로 청원 동의수의 차이를 알아보았고, 다음으로 코로나 이전과 이후로 청원 건수의 차이를 알아보았습니다.

# 동의수 차이
numOfAgreesDiff <- data.frame(period=c("before","after"),
                              numOfAgrees=c(before_cat %>% summarise(sum=sum(numOfAgrees)) %>% unlist, 
                                            after_cat %>% summarise(sum=sum(numOfAgrees)) %>% unlist))
# 동의수 차이 비교 그래프
bna <- ggbarplot(numOfAgreesDiff, x = "period", y = "numOfAgrees", # bna : before num agrees
  fill = "period", 
  color = "period",
  palette = "Paired",
  label = TRUE, 
  label.pos = "out",
  title = "코로나 전후 동의수") +
  theme_pubr(base_family = "NanumGothic")

# 청원건수 차이
numDiff <- data.frame(period=c("before","after"),
                              freq=c(before_cat %>% summarise(freq=n()) %>% unlist, 
                                            after_cat %>% summarise(freq=n()) %>% unlist))
# 청원건수 차이 비교 그래프
ana <- ggbarplot(numDiff, x = "period", y = "freq", # ana : after num agrees
  fill = "period", 
  color = "period",
  palette = "Paired",
  label = TRUE, 
  label.pos = "out",
  title = "코로나 전후 청원건수") +
  theme_pubr(base_family = "NanumGothic")

grid.arrange(bna, ana ,ncol=2)

청원 건수를 보았을 땐 코로나 이전에 청원 건수가 더 많지만 청원 동의수를 보았을 땐 코로나 이후에 압도적으로 동의수가 많음을 알 수 있습니다.

이는 코로나 후에 자영업 청원에 대한 국민적 공감대가 높아졌음을 의미한다고 볼 수 있겠습니다.

이에 대해 더 자세히 살펴보기 위해 시간에 따른 청원 건수와 청원동의수의 변화를 보여주는 그래프를 그려보았습니다.

# 일별 청원 건수
petit_count<- total_cat %>%
  group_by(expiryDate) %>%
  tally() 
# 일별 청원 동의수
petit_ratio<- total_cat %>%
  group_by(expiryDate) %>%
  dplyr::summarize(agreeSum= sum(numOfAgrees))
# 청원건수 대비 청원동의 인원수
petit_rat_cnt<- merge(petit_ratio, petit_count, by="expiryDate") 
petit_rat_cnt$ratCnt<- petit_rat_cnt$agreeSum / petit_rat_cnt$n  

#청원 건수 변화 그래프
a<- ggplot(petit_rat_cnt, aes(expiryDate, n))+
  geom_line()+
  theme_classic()+
  labs(title="자영업 소상공인에 대한  청원 건수 변화")+
  theme(plot.title = element_text(hjust = 0.6, face='bold', size=15))+
  geom_vline(xintercept = as.numeric(petit_rat_cnt$expiryDate[112]),color =  "red", linetype = 2)+
  theme(text = element_text(family = "NanumGothic"))

# 청원 동의 인원 수 변화 그래프
b<- ggplot(petit_rat_cnt, aes(expiryDate, agreeSum))+
  geom_line()+
  theme_classic()+
  labs(title="자영업 소상공인에 대한  청원동의 인원수의 변화")+
  theme(plot.title = element_text(hjust = 0.6, face='bold', size=15))+
  geom_vline(xintercept = as.numeric(petit_rat_cnt$expiryDate[112]),color =  "red", linetype = 2)+
  theme(text = element_text(family = "NanumGothic"))

# 청원 건수 대비 청원 동의 인원 수 변화 그래프
c<- ggplot(petit_rat_cnt, aes(expiryDate, ratCnt))+
  geom_line()+
  theme_classic()+
  labs(title="자영업 소상공인에 대한  청원 건수 대비 청원동의 인원수의 변화")+
  theme(plot.title = element_text(hjust = 0.6, face='bold', size=15))+
  geom_vline(xintercept = as.numeric(petit_rat_cnt$expiryDate[112]),color =  "red", linetype = 2)+
  theme(text = element_text(family = "NanumGothic"))

grid.arrange(a,b,c, nrow=3, ncol=1)

코로나 첫 확진자 발생일 2020년 1/19 이후 처음으로 자영업 청원이 업로드된 2020년 1월 22일을 경계로 설정하였습니다.

마찬가지로 코로나 이전의 데이터가 더 많이 수집되었기 때문에 청원 건수는 코로나 이전에 더욱 많이 접수되었지만, 청원 건수 대비 청원 동의인원 수의 변화그래프를 보았을 때 청원 동의 인원수가 확연히 증가하고 있음을 볼 수 있습니다. 이를 통해 코로나 후에 자영업 청원에 대한 국민적 공감대가 높아졌음을 확인할 수 있습니다.

4.3 불용어 사전 불러오기

청원 게시글 텍스트 데이터에서 의미없는 단어는 삭제시키기 위해 불용어 사전을 사용하였습니다. 그리고 추가로 불용어라 판단되는 단어들을 새로운 불용어 사전에 추가하여 텍스트 분석을 진행 하였습니다.

# 불용어 사전
dic <- read.table(file = "data/한국어불용어100.txt",
                  sep = "\t",
                  fileEncoding = "UTF-8")
dic <- dic[ , 1] %>% as.character()

# 사용자 정의 불용어 사전
delDic <- readLines("data/del.txt")

4.4 형태소 분석

NLP4kec 패키지에 있는 r_parser_r 함수를 사용해 형태소 분석을 진행하였고, 축소된 TF-IDF 단어 행렬과 형태소 분석 결과 값을 돌려주는 textAnalysis함수를 생성하여 분석을 진행하였습니다.

# 형태소 분석 함수
textAnalysis <- function(period){
  df <- period[,3]
  ## 텍스트 공백 제거, 추후 형태소 분석기로 다시 구분
  df<- sapply(df, str_remove_all, '\\s+')
  df<- as.data.frame(df, stringsAsFactors = FALSE)
  
  # 5글자 이상인 title만 추출 
  df <- df[which(nchar(x=df$title)>5),] %>% as.data.frame
  colnames(df) <- "title"

  # 형태소 분석
  # 사용자 정의 감성 사전 추가하여 사용
  Parsed_cat<- r_parser_r(df$title, language= "ko", korDicPath="data/customDic.txt")
  
  # corpus생성
  corp<- VCorpus(VectorSource(Parsed_cat))
  
  # 특수문자 & 숫자 & 불용어 제거
  corp<- tm_map(corp, removePunctuation)
  corp<- tm_map(corp, removeNumbers) 
  
  corp<- tm_map(corp, removeWords, words = c(delDic, dic))
  
  # 문서 단어 행렬 생성 후 단어들에 가중치 부여
  dtmTfIdf <- DocumentTermMatrix(x = corp, 
                                 control = list(removeNumbers = TRUE, 
                                                wordLengths = c(2, Inf),
                                                weighting = function(x) weightTfIdf(x, normalize = TRUE)))

  # 0.99보다 더 희소한 용어들만 제거하여 dtm 차원축소
  dtmTfIdf <- removeSparseTerms(x =  dtmTfIdf, sparse = as.numeric(x = 0.99))

  # dtmTfIdf : 가중치를 부여한 문서 단어 행렬
  # Parsed_cat : 형태소 분석 결과값
  return(list(dtmTfIdf, Parsed_cat))
}

# 형태소 분석
beforeTextData <- textAnalysis(before_cat)
# 가중치를 부여한 문서 단어 행렬
beforeDtmTfIdf <- beforeTextData[1]
# 형태소 분석 결과값
beforeWords <- beforeTextData[2]

afterTextData <- textAnalysis(after_cat)
afterDtmTfIdf <- afterTextData[1]
afterWords <- afterTextData[2]

4.5 워드클라우드

워드클라우드는 빈도수가 높은 순서대로 글자의 크기와 색이 결정되어 어떤 단어들이 데이터에 많이 반복되어 사용되고 있는지 알 수 있습니다.

형태소 분석을 통해 생성한 문서 단어 행렬 데이터를 이용하여 워드클라우드를 그려보았습니다.

# 워드클라우드 그리는 함수 (문서 단어 행렬을 이용하여 그린다)
wc <- function(dtmTfIdf){
  # 단어 빈도수 합치기
  wordsFreq <- dtmTfIdf %>% as.matrix() %>% colSums()
  wordsFreq <- wordsFreq[order(wordsFreq, decreasing = TRUE)]
  print(head(wordsFreq))
  # 단어 빈도표 생성
  wordDf <- data.frame(
                  word = names(x = wordsFreq),
                  freq = wordsFreq,
                  row.names = NULL) %>% 
                  arrange(desc(x = freq))
  
  # 워드클라우드 그리기
  wordcloud2(wordDf,
             fontFamily = 'NanumGothic',
             minRotation = -pi/6, 
             maxRotation = -pi/6,
             rotateRatio = 1,
             shape = "rectangle",
             color = brewer.pal(8, "Dark2"))
}

4.5.1 코로나 이전 워드클라우드

# 가중치가 적용된 단어 빈도
wc(beforeDtmTfIdf[[1]])
##   살리다     대출     죽다     영세     지원   힘들다 
## 71.90391 46.58436 39.37488 31.85273 29.12212 23.39971

4.5.2 코로나 이후 워드클라우드

# 가중치가 적용된 단어 빈도
wc(afterDtmTfIdf[[1]])
##   살리다     지원   코로나     죽다   죽이다     대출 
## 20.85955 19.16702 19.07573 15.99758 13.42832 10.11395

대체적으로 부정적인 단어가 많이 나타나는것을 알 수 있습니다.

코로나 이전과 이후의 워드클라우드에서 찾아볼 수 있는 차이점은 코로나 이후에는 “지원”이란 단어의 크기가 커졌고, “정책”, “긴급”, “학원”, “마트” 등 의 단어들이 새로 나타났음을 볼수있습니다.

4.6 네트워크 맵

코로나 이전 이후로 자영업/소상공인 키워드에 대한 관계성을 살펴보기 위해 네트워크 그래프를 작성하였습니다. network패키지를 통해 네트워크 분석과 객체를 생성하였고 GGally패키지를 사용하여 네트워크 그래프를 그렸습니다. 우선 이전에 생성한 dtmTfIdf를 매트릭스로 바꿔 각 단어들 간의 상관계수를 구하는 함수를 생성하였습니다.

# 상관행렬 생성 함수
getCorTerms <- function(dtmTfIdf){
  corTerms <- dtmTfIdf %>% as.matrix() %>% cor()  
  return(corTerms)
}
# 코로나 이전 상관 행렬
beforeCorTerms <- getCorTerms(beforeDtmTfIdf[[1]])
# 데이터 형식 보기
glimpse(beforeCorTerms)
##  num [1:76, 1:76] 1 -0.0236 0.7535 -0.0276 -0.0235 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:76] "가입자" "개인" "건강" "고용" ...
##   ..$ : chr [1:76] "가입자" "개인" "건강" "고용" ...
# 코로나 이후 상관 행렬
afterCorTerms <- getCorTerms(afterDtmTfIdf[[1]])
# 데이터 형식 보기
glimpse(afterCorTerms)
##  num [1:181, 1:181] 1 -0.01634 -0.00999 -0.01232 -0.01012 ...
##  - attr(*, "dimnames")=List of 2
##   ..$ : chr [1:181] "감독관" "감면" "강력" "강제" ...
##   ..$ : chr [1:181] "감독관" "감면" "강력" "강제" ...

속성에 단어들이 들어가 있고 값에 단어들에 대한 상관계수가 들어갔습니다.

TF-IDF에 대한 상관행렬을 대상으로 network()함수를 사용하여 네트워크 객체를 생성하였습니다. 그 후 상관계수를 통해 상관행렬을 크기를 조정하여 edge의 개수를 줄여 그래프의 가시성을 높였습니다. 상관계수는 그래프의 가시성을 가장 높일 수 있는 계수로 설정하였습니다.

# 네트워크 객체 추출
dim(beforeCorTerms)
## [1] 76 76
getNetTerms <- function(corTerms, range){
  # 상관관계 일정 수치 이상인 데이터만 사용
  # 코로나 이전은 0.2 코로나 이후는 0.45
  corTerms[corTerms <= range] <- 0
  netTerms <- network(x = corTerms, directed = FALSE) # network::network
  return(netTerms)  
}

# 상관계수가 코로나 이전은 0.2, 코로나 이후는 0.45 이상인 데이터만 사용
beforeNetTerms <- getNetTerms(beforeCorTerms, 0.2)
afterNetTerms <- getNetTerms(afterCorTerms, 0.45)

매개 중심성은 노드가 얼마나 단어와 단어를 잘 연결해주는지를 나타내는 용어입니다. 매개 중심성은 sna패키지의 betweenness함수를 활용하여 구하였습니다. 해당 그래프는 다른 노드들과 연결이 많이 되어있는 노드 증 상위 10%에 해당하는 단어들을 금색으로 표현할 수 있도록 설정하였습니다.

# 매개중심성 계산
getBetweennessCentrality <- function(netTerms){
  btnTerms <- betweenness(netTerms) # sna::betweenness
  btnTerms[1:10] #
  
  netTerms %v% 'mode' <- ifelse(test = btnTerms >= quantile(x = btnTerms, probs = 0.90, na.rm = TRUE),
                                yes = 'Top',
                                no = 'Rest')
  return(netTerms)
}

beforeNetTerms <- getBetweennessCentrality(beforeNetTerms)
afterNetTerms <- getBetweennessCentrality(afterNetTerms)

# 네트워크 맵 그리기
nodeColors <- c('Top' = 'gold', 'Rest' = 'lightgrey')
drawNetworkMap <- function(netTerms, corTerms, period){
  # 선의 굵기는 상관계수가 클수록 굵게 설정하였다. 
  set.edge.value(netTerms, attrname = 'edgeSize', value = corTerms * 3)
  ggnet2(
      net = netTerms,
      mode = 'fruchtermanreingold',
      layout.par = list(cell.jitter = 0.001),
      size.min = 3,
      label = TRUE,
      label.size = 3,
      node.color = 'mode',
      palette = nodeColors,
      node.size = sna::degree(dat = netTerms), # sna::degree
      edge.size = 'edgeSize') +
      labs(title = paste0("코로나 ",period," 자영업,소상공인 단어-네트워크맵")) # GGally::ggnet2
}

drawNetworkMap(beforeNetTerms, beforeCorTerms, "이전")

drawNetworkMap(afterNetTerms, afterCorTerms, "이후")

코로나 이전에는 자영업과 관련하여 최저임금 인상, 일자리 창출, 소득 공제, 국민연금 등과 관련한 키워드가 많이 보이는 반면, 코로나 이후에는 영업정지, 집합금지명령, 고용 경영 안정, 거리두기단계 시행과 같이 코로나와 관련된 이슈를 많이 볼수있습니다. 워드 클라우드와 마찬가지로 자영업 키워드에 코로나와 관련된 용어가 상당수 추가 됐음을 볼 수 있습니다. 이는 자영업이 코로나에 불가분한 영향을 받고 있음을 알 수 있습니다.

5. 감성 분석

게시글의 감성을 알기 위해 ’KNU 한국어 감성사전’을 사용하였습니다.

감성사전을 불러와서 긍정사전에는 단어당 1점, 부정사전에서는 단어당 -1점을 부여 하였습니다. 이후 형태소 분석을 해서 나온 데이터를 토크나이징 한 다음 각 단어에 대해 감성점수를 부여 하였습니다.

단어 별로 감성점수를 구한 후 그걸 다시 하나의 문장으로 합치는 과정에서 단어당 감성점수를 모두 더하여 청원 게시글 한개의 감성 점수를 구하였습니다.

코로나 이전과 이후의 감성 점수를 비교하기 위해 중립은 제외하고 긍부정 점수만을 비율로 나타낸 후 파이차트를 그려보았습니다.

# 함수로 만들자
# 감성사전 불러오기
positive <- readLines("data/positive.txt")
negative <- readLines("data/negative.txt")

# 점수 부여
positive <- data.frame(positive, rep(1,length(positive)))
colnames(positive) <- c("word","score")
negative <- data.frame(negative, rep(-1,length(negative)))
colnames(negative) <- c("word","score")

# 감성 사전 하나로 통합
lexicon <- bind_rows(positive, negative) 

# 감성 점수 매칭 함수
matchScore <- function(words){
  df_token <- words %>% as.data.frame
  colnames(df_token)[1] <- "title"
  
  # tidytext 패키지에 unnest_tokens() 를 사용해 토크나이징
  df_token <- unnest_tokens(df_token, input = title,
                          output = word,
                          token = "words",
                          drop = F)
  
  # 토큰화 시킨 단어들과 감성 사전에 단어들을 매칭해서 점수 부여
  df_token <- df_token %>%
    left_join(lexicon, by = "word") %>%
    mutate(polarity = ifelse(is.na(score), 0, score))
  
  # 레이블 주기
  df_token <- df_token %>% mutate(sentiment = ifelse(polarity == 1, "positive", ifelse(polarity == -1, "negative", "neutral")))
  return(df_token)
}

# 감성 분석 함수
sentiAnalysis <- function(words){
  # 감성 점수 매칭하기
  df <- matchScore(words)
  
  # 전체 감성 점수 구하기
  score_df <- df %>%
    group_by(sentiment) %>%
    summarise(score = sum(polarity))
  
  # 절댓값을 취해 감성점수의 총합을 구할 수 있게 함
  score_df$score <- abs(score_df$score)
  # 중립 점수 제거
  score_df <- score_df[-2,]
  score_df$ratio <- round(score_df$score / sum(score_df$score), 4) * 100
  
  ratio <- paste0(score_df$sentiment, "\n","(",score_df$ratio, "%",")")
  fig <- score_df %>% ggpie("ratio", 
                         label = ratio, 
                         fill = "sentiment", 
                         font.family = "NanumGothic",
                         color = "white",
                         lab.pos ="in",
                         lab.font = c(5, "white"),
                         palette = c("#ff6666", "#3399ff")) + rremove("legend")
  
  # df : 감성 점수가 포함된 게시글 
  # fig : 파이 차트 데이터
  return(list(df, fig))
}
# 감성 분석
beforeSenti <- sentiAnalysis(beforeWords)
afterSenti <- sentiAnalysis(afterWords)

# 감성 분석 그래프
beforeFig <- beforeSenti[[2]]
afterFig <- afterSenti[[2]]

# 감성 점수표 함수
getScoreTable <- function(period, periodLabel){
  sentiScore <- period
  scoreTable <- sentiScore %>% 
    group_by(title) %>% 
    summarise(score=sum(polarity))
  
  attach(sentiScore)
  sentiScoreTable <- bind_cols(sentiment, polarity, rep(periodLabel, length(sentiment)))
  colnames(sentiScoreTable) <- c("sentiment","score","period")
  detach(sentiScore)
  return(list(scoreTable, sentiScore))
}
beforeScoreTable <- getScoreTable(beforeSenti[[1]], "before")
afterScoreTable <- getScoreTable(afterSenti[[1]], "after")

# 게시글의 단어별 감성 점수
# 코로나 이전 청원 게시글 단어별 감성 점수
beforeScoreTable[[2]] %>% head
# 코로나 이후 청원 게시글 단어별 감성 점수
afterScoreTable[[2]] %>% head
# 게시글 제목과 감성 점수 테이블
beforeTitleAndScore <- beforeScoreTable[[1]]
# 코로나 이전 청원 게시글 감성점수 데이터프레임
beforeTitleAndScore %>% head
afterTitleAndScore <- afterScoreTable[[1]]
# 코로나 이후 청원 게시글 감성점수 데이터프레임
afterTitleAndScore %>% head
# 감성 비율 그래프 출력
ggarrange(beforeFig, afterFig, labels = c("before covid 19", "after covid 19"), hjust = c(-0.7,-0.9), vjust = 3) %>% 
  annotate_figure(top=text_grob("코로나 전후 청원 게시글 감성", face="bold", family="NanumGothic", size=24))

코로나 이전에는 부정적인 감성이 70.4%를 차지했고 긍정적인 감성이 29.6%를 차지했습니다.

코로나 이후에는 부정적인 감성이 73.76% 이고 긍정적인 감성이 26.24%입니다.

부정적인 감성이 코로나 전후로는 약 3% 증가하였으며 긍정적인 감성은 약 3% 감소했습니다. 이제 이 차이가 통계적으로 유의한 차이인지 검정 해 보고자 합니다.

6. 추론통계

감성분석을 통해 구한 게시글별 감정 점수를 사용하여 코로나 전후로 국민 청원 게시글의 부정적 감성이 더 늘었는지 알아보고자 합니다.

6.1 가설 설정

\(귀무가설 : 자영업,\ 소상공인\ 관련\ 국민\ 청원\ 게시글의\ 감성이\ 코로나\ 이전과\ 이후에\ 차이가\ 없다\)

\(대립가설 : 자영업,\ 소상공인\ 관련\ 국민\ 청원\ 게시글의\ 감성이\ 코로나\ 이전보다\ 이후에\ 더\ 안좋다\)

6.2 정규성 검정

감성점수 데이터에 대한 정규성 검정을 진행하였습니다. qqplot과 히스토그램을 그려보고, 코로나 이전과 이후 청원 게시들의 감성점수들이 정규분포를 따르는지 대해 shapiro.test를 통해서도 정규성을 검증해 보았습니다.

par(mfrow=c(2,2))
# 코로나 전후 감성점수 히스토그램
hist(beforeTitleAndScore$score, main = "before covid histogram", xlab = "sentiment score")
hist(afterTitleAndScore$score, main = "after covid histogram", xlab = "sentiment score")

# 코로나 전후 qqplot
qqnorm(beforeTitleAndScore$score, main = "before covid qqplot")
qqline(beforeTitleAndScore$score, col="green")
qqnorm(afterTitleAndScore$score, main = "after covid qqplot")
qqline(afterTitleAndScore$score, col="green")

shapiro.test(beforeTitleAndScore$score)
## 
##  Shapiro-Wilk normality test
## 
## data:  beforeTitleAndScore$score
## W = 0.52184, p-value < 0.00000000000000022
shapiro.test(afterTitleAndScore$score)
## 
##  Shapiro-Wilk normality test
## 
## data:  afterTitleAndScore$score
## W = 0.84575, p-value = 0.0000000000003198

두 데이터 모두 p-value가 0.05보다 낮으므로 정규분포를 따르지 않습니다.

6.3 두 모집단의 중심 차이에 대한 비모수 검정

데이터가 정규분포를 따르지 않아 t-test를 사용하지 못하여 Wilcoxon rank sum test를 사용하였습니다.

코로나 이후의 감성점수가 더 나빠졌으니 wilcox.test의 alternative에 “greater”이라는 값을 주어 우측검정을 실시하였습니다.

wilcox.test(beforeTitleAndScore$score, afterTitleAndScore$score, alternative="greater")
## 
##  Wilcoxon rank sum test with continuity correction
## 
## data:  beforeTitleAndScore$score and afterTitleAndScore$score
## W = 41700, p-value = 0.03566
## alternative hypothesis: true location shift is greater than 0

6.4 검정 결과

p-value가 0.03566으로 p-value < 0.05 이므로 귀무가설은 기각되고 대립가설이 채택되었습니다.

즉 자영업, 소상공인 관련 국민 청원 게시글의 감성이 코로나 이전보다 이후에 더 안좋아졌다고 해석할 수 있습니다.

7. 결론

이번 분석을 통해 코로나19 전과 후로 청와대 국민 청원 게시판에 게시되는 청원들에 어떤 차이가 있었는지와 사회적 트렌드의 변화를 확인할 수 있었습니다. 그리고 자영업/소상공인에 관한 국민 청원 게시글들의 성격을 확인해보았습니다. 코로나19 이전에도 해당 키워드의 청원들은 상당히 부정적이었습니다. 하지만 코로나19 이후 정부 또는 행정기관을 향한 자영업자들의 감정은 더욱 안좋아졌습니다. 이는 사회적 거리두기, 5인 이상 사적 모임 금지, 영업 시간 단축과 같은 여러 행정 조치들 때문인것으로 생각됩니다.

이번 분석의 한계점으론 분명 코로나19가 자영업자들에게 대부분의 영향을 끼쳤지만 국민 청원 게시글의 감성의 변화의 요인을 오직 코로나19로 두기엔 현대 사회/경제 시스템은 굉장히 얽혀있습니다. 어떤 요인들이 자영업자들의 감성에 영향을 미치는지 추가로 분석 해보고 해당 요인들에 대한 데이터를 가지고 분석을 한다면 더욱 정확한 결과가 나올 수 있을것으로 생각됩니다.