Search
Login
🗞️

파이썬을 활용한 데이터 사이언스 - 정치 성향 분석하기

Created
2022/08/12
Editor
중립적인 제목의 실시간 라이브 뉴스영상에서도 채널의 정치성향에 따라 댓글의 찬반여론 차이가 두드러지게 나타날까?”
이를 알아보기 위해
진보와 보수로 각각 대표되는 서로 다른 정치성향의 두 채널의
정치성향 상관없이 절대 다수가 시청했을 ‘대통령 취임식 라이브 영상’ 댓글을
비교해보기로 했습니다.
댓글 허용 여부, 유튜브 채널 구독자 수 및 해당 영상 조회수를 기준으로 영상을 선정한 결과, 진보 언론사로는 JTBC, 보수 언론사로는 채널 A를 선정하여 영상 댓글을 크롤링하였습니다.

1. 댓글 크롤링

from selenium import webdriver as wd from bs4 import BeautifulSoup import time from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager #저장경로 설정 s = Service("C:/Users/vip/Downloads/chromedriver_win32/chromedriver.exe") driver = wd.Chrome(service = s) #driver = wd.Chrome(executable_path = "C:/Users/vip/Downloads/chromedriver_win32/chromedriver.exe") time.sleep(2) #윈도우 크기 조절 driver.maximize_window() #JTBC뉴스 라이브영상 링크 접속 url = 'https://www.youtube.com/watch?v=ksO9xGYTf1s' driver.get(url) #접속한 페이지의 가장 아래까지 스크롤하기 last_page_height = driver.execute_script("return document.documentElement.scrollHeight") while True : driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);") time.sleep(30.0) new_page_height = driver.execute_script("return document.documentElement.scrollHeight") if new_page_height == last_page_height: break last_page_height = new_page_height #파싱 방법 설정 html_source = driver.page_source soup = BeautifulSoup(html_source, 'lxml') # 스크래핑할 요소 설정(닉네임, 댓글, 공감 수) userID = soup.select('a#author-text') #다른 경로..? userID = soup.select('div#header-author > a > span') comment = soup.select('yt-formatted-string#content-text') positive = soup.select('span#vote-count-middle') str_userID = [] str_comment = [] str_positive = [] #의미없는 값 처리 for i in range(len(userID)): str_tmp = str(userID[i].text) str_tmp = str_tmp.replace('\n', '') str_tmp = str_tmp.replace('\t', '') str_tmp = str_tmp.replace(' ', '') str_userID.append(str_tmp) str_tmp = str(comment[i].text) str_tmp = str_tmp.replace('\n', '') str_tmp = str_tmp.replace('\t', '') str_tmp = str_tmp.replace(' ', '') str_comment.append(str_tmp) str_tmp = str(positive[i].text) str_tmp = str_tmp.replace('\n', '') str_tmp = str_tmp.replace('\t', '') str_tmp = str_tmp.replace(' ', '') str_positive.append(str_tmp) #처리된 데이터 출력 for i in range(len(str_userID)): print(str_userID[i], str_comment[i], str_positive[i]) #데이터프레임 만들기 import pandas as pd pd_data = {"ID":str_userID, "Coment":str_comment, "positive":str_positive} youtube_jtbcnews_pd = pd.DataFrame(pd_data) youtube_jtbcnews_pd youtube_jtbcnews_pd.to_excel("C:/Users/...경로/youtube_jtbcnews_comments.xlsx")
Python
복사

2. 형태소 분석

불용어 설정

분석할 댓글을 직접 모아보았으니 이제는 데이터를 가공해볼 차례입니다. 모은 데이터를 무작정 시각화했다간 띄어쓰기도 제대로 되어 있지 않은 댓글이 그대로 들어갈 수 있으니까요!
먼저, 우리의 분석에서 쓰지 않을 단어를 지정해 데이터에서 삭제해 줄 필요가 있습니다. 이러한 단어들을 바로 불용어라고 지칭하는데, 연구자 마음대로 지정할 수 있답니다! 보통은 ‘은/는/이/가’와 같은 조사나, ‘~합니다’ 같은 어미, ‘사람’, ‘사실’, ‘어이쿠’같이 큰 뜻이 없는 단어들을 불용어로 지정하고는 합니다. 보편적으로 쓰이는 한국어 불용어 예시는 다음과 같습니다.
stop_words.txt
7.5KB
stop_word2.txt
0.7KB
이번 실습에서는 ‘윤석열 대통령 취임식’이 대주제였던 만큼, 위 파일 속 단어들과 함께 두 채널의 댓글에 공통으로 많이 나올 만한 ‘윤석열’, ‘대통령’, ‘대통령님’ 등의 단어도 불용어로 설정했답니다! 또한, 위 파일에 문장 부호는 포함되지 않았으니, 데이터 시각화에서 ‘!’와 같은 기호가 가장 크게 나오길 원하지 않는다면 문장 부호도 별도로 추가해줘야겠죠?

그러면 이것을 어떻게 코드로 정의하나요?

현재 불용어가 txt 파일로 정의되어 있으니 나중에 이것을 사용하려면 한 라이브러리 안에 미리 넣어둬야 합니다. 그러기 위해선 코드로 미리 불용어를 정의해야 하는데, 이때 파일을 잘 살펴보면 단어마다 줄바꿈이 되어 있다는 걸 알 수 있습니다. 우리는 불용어가 아래로 출력되는 형태가 아닌, 리스트처럼 옆으로 배열되어 있는 형태를 원하므로 이 목적에 맞추어 코드를 구현해줍니다.
with open('stop_words.txt', 'r') as file: data = file.read().replace('\n', ' ') with open('stop_word2.txt', 'r') as file: content = file.read().replace('\n', ' ') stop_words = " ".join((data, content '윤석열', '대통령', '대통령님', '대한민국', '취임식', '취임', '대통령이', '~', '나라', '국민', '?!', '.', '!', ':', '...', '?', ',', '‥', '?)..', ')'))
Python
복사
이 단계를 통해 stop_words.txt, stop_word2.txt, 그리고 우리가 추가로 지정한 단어들이 stop_words라는 이름 아래 들어가 있게 됩니다.

형태소 분석

본격적으로 데이터를 정의하기 위해서는 꼭 알아야 할 개념이 있습니다: 바로 형태소입니다.
형태소(morpheme): 의미를 가지는 가장 작은 말의 단위
예를 들어, ‘나는 딥다이브 아티클을 읽고 있다.’라는 문장이 있다고 합시다. 여기서 단어는 무엇일까요? ‘나’, ‘는’, ‘딥다이브’, ‘아티클’, ‘을’, ‘읽고’, ‘있다’로 나눌 수 있겠죠. 보통 단어는 띄어쓰기로 나눠주고, 조사는 다른 단어와 붙어 있더라도 예외적으로 분리성을 인정하여 단어 취급을 해주니까요.
그렇다면 형태소는 무엇일까요? ‘의미’를 가진다는 것이 구체적으로 어떤 뜻일까요?
간단합니다. ‘읽고 있다’는 동사구만 살펴볼게요. 그러면 우리는 자연스럽게 이 구를 ‘읽-’, ‘-고’, ‘있-’, ‘-다’로 좀 더 작게 쪼갤 수 있다는 걸 알게 됩니다. ‘읽고’라는 단어는 ‘읽으며’, ‘쓰고’ 등 다양한 단어로 활용될 수 있으니, ‘읽-’과 ‘-고’라는 글자는 꼭 붙어 있을 필요가 없습니다. 두 글자가 가지고 있는 의미도 다르고요. 우리는 이런 단위를 형태소라고 부릅니다.
세부적으로는, 형태소는 어떤 의미를 가지는지에 따라 실질 형태소형식 형태소로 나뉩니다. 이 사례에서는 행동을 지칭하는 ‘읽-’과 ‘있-’이 실질 형태소, 어미에 해당하는 ‘-고’, ‘-다’가 ‘형식 형태소’에 속한답니다. 자립성에 따라 자립 형태소와 의존 형태소로 나뉘기도 하는데, 우리는 데이터의 내용을 살펴보고 싶으니 형태소가 가지는 의미에 집중하도록 합시다!

그래서,

우리가 지금 하고자 하는 것은 모은 댓글을 ‘실질 형태소’로 분석하는 일입니다.
실질 형태소와 형식 형태소 중 우리가 더 관심이 있는 건 무엇일까요? 당연히, 실질 형태소입니다. 만일 어떤 구가 ‘-고 -다’는 식으로 형식 형태소만 모아 적혀 있다면, ‘읽- 있-’이라는 실질 형태소 구절보다 내용을 알기 어려우니까요. 그렇지만 우리가 댓글을 형태소 단위로 하나하나 분석할 수 없으니, 파이썬 패키지의 힘을 빌리도록 합시다.
형태소 분석을 해주는 대표적인 패키지는 nltk로, Natural Language Toolkit의 약자입니다. 이 패키지에서도 word_tokenize 함수가 바로 형태소 분석을 대신 해줄 친구입니다. 이 과정을 코드로 구현하는 과정은 다음과 같습니다.
import nltk nltk.download('punkt') from nltk import word_tokenize JTBCnltk = word_tokenize(JTBC_str) ChAnltk = word_tokenize(ChA_str)
Python
복사
이 과정에서 우리가 앞에 정의했던 불용어를 써야 합니다. stop_words에 들어가 있는 단어를 제외한 데이터에만 word_tokenizer 함수를 쓰라는 의미로요!
JTBC_nltkr = [word for word in JTBCnltk if not word in stop_words] ChA_nltkr = [word for word in ChAnltk if not word in stop_words]
Python
복사
이렇게 형태소 분석만 하고 넘어가기는 아쉬우니, 패키지가 나눈 형태소 개수를 세 주는 Counter 패키지도 설치해 결과를 살펴볼 수 있습니다.
from collections import Counter JTBCnltkc = Counter(JTBC_nltkr) ChAnltkc = Counter(ChA_nltkr) print(JTBCnltkc) print(ChAnltkc)
Python
복사
그런데, 결과를 보면 이상한 점이 하나 있습니다. 채널 A의 데이터에서 가장 많이 등장한 형태소가 ‘나라를’이라고 출력됩니다. 무엇이 이상한지 눈치 채셨나요?
맞아요, ‘나라를’은 하나의 형태소가 아닙니다! ‘나라’와 ‘를’이라는 두 개의 형태소로 이루어져 있죠. 그러면 패키지가 단어를 잘못 분석한 셈인데, 왜 이런 결과가 (그것도 꽤 빈번히!) 나올까요?
그것은 바로 nltk가 영어를 토대로 개발된 패키지기 때문입니다. 쉽게 말하자면, 우리가 분석하려고 하는 건 영어가 아닌 한국어기 때문에 nltk의 성능이 조금 떨어진다는 것이죠.
이 점을 감안해 개발된 패키지가 바로 KoNLPy입니다. 굉장히 직관적인 이름이죠? 그럼 KoNLPy를 직접 사용해보도록 합시다. KoNLPy는 총 다섯 가지 패키지를 제공하는데, 이번 실습에서 사용할 것은 Mecab입니다.
pip install konlpy !curl -s https://raw.githubusercontent.com/teddylee777/machine-learning/master/99-Misc/01-Colab/mecab-colab.sh | bash from konlpy.tag import Mecab mecab = Mecab()
Python
복사
Mecab 안에는 기본적으로 형태소를 분석해주는 함수 mecab.morphs와, 명사만 분석해주는 함수 mecab.nouns 등이 있습니다. 자, 그럼 여기서 하나 더 생각해봅시다. 데이터를 형태소 단위로 쪼개는 것과 명사 단위로 쪼개는 것, 둘 중 무엇이 데이터의 내용을 더 잘 표현해줄까요?
단연 명사입니다. 형태소 안에는 앞서 얘기한 형식 형태소도 포함돼 있으니 명사의 의미가 좀 더 직접적일 것입니다. 그렇지만, 역시 눈으로 두 차이를 확인해보는 게 가장 편하겠죠? 주어진 데이터에 형태소 분석과 명사 분석을 모두 해서 비교해보겠습니다.
형태소 단위 분석
JTBCM = mecab.morphs(JTBC_str) ChAM = mecab.morphs(ChA_str) JTBC_r = [word for word in JTBCM if not word in stop_words] ChA_r = [word for word in ChAM if not word in stop_words]
Python
복사
명사 단위 분석
JTBC_nouns = mecab.nouns(JTBC_str) ChA_nouns = mecab.nouns(ChA_str) JTBC_nresult = [word for word in JTBC_nouns if not word in stop_words] ChA_nresult = [word for word in ChA_nouns if not word in stop_words]
Python
복사
기본적인 데이터 전처리는 끝났고, 이제 우리가 가공한 데이터를 눈으로 확인해볼 차례입니다!

3. 데이터 시각화 (워드클라우드)

형태소 분석을 마친 후에는 시각화 작업을 진행하였습니다. 분석 결과를 분명히 나타내기에 워드 클라우드가 적합할 것이라 판단하여 워드 클라우드로 데이터를 시각화하였습니다.
워드 클라우드(Wordcloud) : 텍스트 내 단어가 언급된 순으로 크기를 다르게 하여 단어의 중요도를 시각화하는 방법

1) 필요한 라이브러리 설치

워드클라우드를 생성하기에 앞서서 필요한 라이브러리를 설치해줄 것입니다.
우리가 오늘 사용할 라이브러리는 바로 Matplotlib입니다. matplotlib.pyplot 모듈은 MATLAB과 비슷하게 명령어 스타일로 동작하는 함수의 모음입니다. 해당 모듈의 각각의 함수를 사용하여 간편하게 그래프를 만들고 변화를 줄 수 있습니다!
Pillow 모듈은 파이썬 이미지 처리를 담당하는 모듈입니다. 다양한 이미지 파일 형식을 지원하며, 강력한 이미지 처리와 그래픽 기능을 제공하는 이미지 프로세싱 라이브러리입니다.
from wordcloud import WordCloud # matplotlib.pyplot 모듈의 각각 함수를 사용하여 간편하게 그래프를 만들 수 있다. import matplotlib.pyplot as plt # collections 모듈의 counter 클래스는 주어진 단어에 포함된 각 글자의 수를 세어준다. from collections import Counter from konlpy.tag import Okt from PIL import Image import numpy as np
Python
복사

2) mecab.nouns를 활용한 JTBC 댓글 워드클라우드 생성하기

Mecab 내에서 명사만 분석해주는 mecab.nouns 함수를 사용하여 JTBC 댓글 데이터를 명사 단위로 쪼개고, 이를 워드클라우드로 시각화 해주었습니다.
scale, max_font_size 옵션으로 단어의 폰트 크기를 설정해주었고, background_color 옵션을 “white”로 지정함에 따라 배경 색상은 하얀색으로 설정하였습니다. width, height 옵션을 통해 워드클라우드 크기를 설정했으며, random_state 인자를 지정해주어 워드클라우드의 스타일을 고정하였습니다.
wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(JTBC_nouncount) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show()
Python
복사

3) mecab.nouns를 활용한 Channel.A 댓글 워드클라우드 생성하기

같은 방법으로 Channel.A 댓글 데이터를 워드클라우드로 시각화 해주었습니다. 옵션의 기본적인 설정은 앞서 JTBC 댓글을 시각화한 것과 동일하게 지정하였습니다.
wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(ChA_nouncount) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show()
Python
복사

4) Mecab.morphs를 활용한 JTBC 댓글 워드클라우드 생성하기

이제는 Mecab 내에서 데이터를 형태소 단위로 쪼개는 mecab.morphs 함수를 사용하여 JTBC 댓글 데이터를 워드클라우드로 시각화 해보겠습니다.
wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(JTBCMc) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show()
Python
복사

5) Mecab.morphs를 활용한 Channel.A 댓글 워드클라우드 생성하기

같은 방법으로 Channel.A 댓글 데이터 역시 형태소 단위로 쪼개어 워드클라우드를 생성합니다!
wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(ChAc) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show()
Python
복사

6.) nltk를 활용한 JTBC, Channel.A 댓글 워드클라우드 생성하기

이번에는 nltk 패키지를 이용하여 JTBC와 Channel.A 댓글 워드클라우드를 생성해보겠습니다.
##JTBC 댓글 워드클라우드 생성하기 wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(JTBCnltkc) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show() ##Channel A 댓글 워드클라우드 생성하기 wordcloud = WordCloud(font_path='/content/NanumGothic.ttf', scale=2.0, max_font_size=250, background_color ='white', colormap='autumn', width = 700, height = 700, random_state = 43).generate_from_frequencies(ChAnltkc) plt.figure(figsize = (6, 6)) # 최종 워드 클라우드 사이즈 지정 plt.imshow(wordcloud) plt.title("Word Frequency", size = 13) plt.axis('off') # 그래프 축 제거 plt.show()
Python
복사
nltk로 분석한 JTBC 댓글 워드클라우드
nltk로 분석한 Channel A 댓글 워드클라우드
두 사진 모두 공통적으로 “5년 동안”에서 “5년”과 “동안”이나 “나라를”에서 “나라”나 “를”의 형태소를 제대로 분석하지 못한 모습을 확인하실 수 있습니다. 이때 시각적으로 확연한 차이는 두드러지지 않으며, 인명을 기준으로 했을 때는 양쪽 모두 “김건희"가 공통으로 등장했다는 점을 알 수 있습니다.

4. 결과 해석

다음과 같이 해석할 수 있습니다.
1.
유튜브 실시간 라이브 영상은 생각보다 진보나 보수 진영의 충성도를 반영하지는 않는다
2.
대부분 댓글 데이터들의 전반적인 맥락은 비슷하나 특정 키워드의 언급량에서 세부적인 차이가 존재한다