[NewsSentiment] News Sentiment Analysis Part3: Sentiment Analysis
in ToyCode on NewsSentiment
개요
News Sentiment Analysis를 합니다.
필요한 라이브러리를 가져옵니다. nltk 라이브러리는 자연어 처리 용도로 쓰입니다. TweetTokenizer는 hashtag를 자르지 않고 살려두는 점에서 word_tokenize와 다릅니다. nltk에 내장된 vader (Valence Aware Dictionary for Sentiment Reasoning) sentiment analyzer를 가져오고, 의견과 관련된 corpus를 가져옵니다.
import os
import csv
import nltk
from nltk.tokenize import TweetTokenizer
from nltk.corpus import opinion_lexicon
from nltk.sentiment.vader import SentimentIntensityAnalyzer
import pandas as pd
import sqlite3
San Francisco 연준의 Shapiro, Sudhof, Wilson이 2017년에 쓴 Measuring News Sentiment 논문을 참고하여 만들었습니다. 단어 dictionary는 그들이 사용한 것들을 그대로 사용합니다.
금융 분야 sentiment analysis의 대가들 중 Tim Loughran과 Bill McDonald라는 사람들이 만들어 둔 단어 뭉치들이 있습니다. 광범위한 단어들을 포함하고 있어 학술 연구에서 많이 쓰입니다. 단어 하나에 여러 가지 데이터가 엮여 있어 dictionary 형태로 가져옵니다. 단어별로 positive와 negative 표시를 해서 단어별 sentiment를 기록한 dictionary를 줍니다.
# Parsing은 text 받아오면서 끝냄. Sqlite DB에 있는 text 그대로 가져오면 사용 가능함
lexicon_dir = ""
# lexicon 가져오기 ( lexicon: {'word1' : 'score1', 'word2' : 'score2', ... } )
def load_lm_lexicon():
# Loughran McDonald
fn = os.path.join(lexicon_dir, "LoughranMcDonald_MD2018.csv")
# Example: [('Word', 'AARDVARK'), ('Sequence Number', '1'), ('Word Count', '277'), ('Word Proportion', '1.48E-08'),
# ('Average Proportion', '1.24E-08'), ('Std Dev', '3.56E-06'), ('Doc Count', '84'), ('Negative', '0'), ('Positive', '0'),
# ('Uncertainty', '0'), ('Litigious', '0'), ('Constraining', '0'), ('Superfluous', '0'), ('Interesting', '0'), ('Modal', '0'),
# ('Irr_Verb', '0'), ('Harvard_IV', '0'), ('Syllables', '2'), ('Source', '12of12inf')]
reader = csv.DictReader(open(fn))
words2weights = {}
for r in reader:
# Assume: positive != 0 --> positive word, negative != 0 --> negative word
pos_score = 1. if r['Positive'] != "0" else 0.
neg_score = 1. if r['Negative'] != "0" else 0.
sentiment_score = pos_score - neg_score
# upper -> lower
w = r['Word'].lower()
# positive / negative labeling
if sentiment_score:
words2weights[w] = sentiment_score
return words2weights
nltk에 내장된 것으로, 의견 관련 단어들로 알려져 있습니다.
def load_hl_lexicon():
# Hu and Liu 2004 opinion lexicon : nltk 내장
words2weights = {w: 1.0 for w in opinion_lexicon.positive()}
words2weights.update({w: -1.0 for w in opinion_lexicon.negative()})
return words2weights
Shapiro, Sudhof, Wilson (2017)에서 테스트한 결과는 VADER 단어 모음 중 경제와 관련이 있는 단어들을 사용하면 결과가 더 좋았다고 합니다. 그들이 가공하여 제공한 경제 관련 VADER 단어 모음을 가져옵니다. 단어 - sentiment dictionary로 만듭니다.
lexicon_dir = ""
def load_news_vader_lexicon():
# 샌프란시스코 연준의 measuring news sentiment 논문에서 가공한 vader lexicon
fn = os.path.join(lexicon_dir, "ns.vader.sentences.20k.csv")
df = pd.read_csv(fn)
words2weights = dict(zip(df['word'].values, df['sentiment'].values))
return words2weights
3개 dictionary를 합친 최종 dictionary 입니다.
def combine_lexicons(lexicons):
# input(list): [lm_lexicon, hl_lexicon, vader lexicon]
# and returns the union
lexicons.reverse()
words2weights = {}
for lex in lexicons:
for w in lex:
words2weights.setdefault(w, 0.0)
words2weights[w] += lex[w]
return words2weights
뉴스 기사 text를 전부 단어 단위로 잘라 주고(token화) 소문자로 만듭니다. 단어별로 dictionary에서 sentiment 값을 찾아서 text 내에서 sentiment 평균치를 구합니다.
def lexicon_scoring(text, lexicon):
# text 받아온 것 전부 token화 (단어 단위로 자른다는 느낌) 하고 이모티콘이나 문장 부호 같은 것 아니면 전부 소문자화
words = TweetTokenizer(preserve_case=False).tokenize(text)
# words에서 단어별로 체크 후 합치기. w.lower()만 하면 key 없어버리면 None이니 key 없으면 0.0 처리
score = sum([lexicon.get(w.lower(), 0.0) for w in words])
score = score/len(words)
return score
부정어를 고려해야 합니다. Bad는 나쁘다는 말이지만 Not Bad면 나쁘다는 의미가 아니게 됩니다. 문맥에 부정어가 있는지 체크하기 위해 현재 보고 있는 word 앞 3단어까지 체크합니다. 부정어가 발견되면 sentiment 값에 -1을 곱해서 돌려주고 아니면 그대로 갑니다.
NEGATION_WORDS = set(nltk.sentiment.vader.NEGATE)
def negated_lexicon_scoring(text, lexicon):
words = TweetTokenizer(preserve_case=False).tokenize(text)
score = 0.0
for i, w in enumerate(words):
# 문맥에 부정어 있는지 봐야 하니까 체크 중인 word 앞 3단어 체크
context = words[max(0, i-3):i]
nega_adjust = 1.0
# negation word(부정어) 있음 (ex: bad가 not bad가 되어서 부정적인 뜻이 아니게 됨)
if set(context) & NEGATION_WORDS:
nega_adjust = -1.0
score += (nega_adjust * lexicon.get(w.lower(), 0.0))
score = score / len(words)
return score
이전부터 많이 했던 작업입니다. 이미 sentiment 값을 구한 것들은 다시 구할 필요가 없으니 sentiment 값을 아직 구하지 않은 최신 데이터만 계산하도록 합니다.
# news_sentiment db에 있는 식별자들과 news_text db에 있는 것들 비교해서 news_sentiment db에 없는 것만 추가할 것
con = sqlite3.connect('news_sentiment.db')
news_sentiment_data = pd.read_sql('select source, topic, title, publish_date, link, keywords, text from news_sentiment', con)
con.close()
con = sqlite3.connect('news_text.db')
news_text_data = pd.read_sql('select source, topic, title, publish_date, link, keywords, text from news_text', con)
con.close()
# sentiment score 저장하는 db인 news_sentiment에 포함되어 있지 않은 news_text_data만 점수 내서 집어넣을 것임
df_target = pd.merge(news_text_data, news_sentiment_data, how='outer', indicator='Exist')
df_target= df_target.loc[df_target['Exist']=='left_only']
df_target = df_target.loc[:, ['source','topic','title','publish_date','link','keywords','text']]
df_target = df_target.reset_index(drop=True)
뉴스 기사마다 sentiment를 계산합니다. 부정어 관련 조정을 한 것과 안 한 경우 둘 다 구합니다.
# combined lexicon
lex = combine_lexicons([load_lm_lexicon(), load_hl_lexicon(), load_news_vader_lexicon()])
# 부정어 조정 안 함
sent_score = []
# 부정어 조정함
sent_score_adj = []
for i in range(len(df_target)):
try:
# column index 6: news text
sent_score.append(lexicon_scoring(df_target.iloc[i,6], lex))
sent_score_adj.append(negated_lexicon_scoring(df_target.iloc[i,6], lex))
#print(i)
# text 없는 경우 sentiment score 0으로 대체
except ZeroDivisionError as e:
sent_score.append(0)
sent_score_adj.append(0)
#print(e)
news_text_data_with_sentiment_score = df_target
news_text_data_with_sentiment_score['sent_score'] = sent_score
news_text_data_with_sentiment_score['sent_score_adj'] = sent_score_adj
con = sqlite3.connect('news_sentiment.db')
news_text_data_with_sentiment_score.to_sql('news_sentiment', con, if_exists='append', index_label='id')
con.close()