[RedditSentiment] Reddit Sentiment Analysis


개요

Sentiment Analysis 도구입니다.

Reddit의 포스팅과 댓글을 가지고 sentiment analysis를 직접 구현해 보겠습니다. 주식 및 투자에 관련된 subreddit 중 참여자 수가 가장 많은 wallstreetbets, stocks, StockMarket, investing 4개 subreddit을 사용합니다. 각 subreddit 안에서도 일종의 tag가 있습니다. 예를 들어, wallstreetbets에는 daily discussion, weekly discussion, discussion 등의 tag가 있습니다. 모든 글을 다 체크하는 것은 그다지 효율적이지 않으니 인기글 위주로 체크할 것입니다. 대상 주식은 시가 총액 3억 달러 이상인 주식들로 제한합니다. 언급된 횟수 기준 상위 30개 주식의 언급 횟수와 sentiment analysis 결과를 출력합니다.

import praw
import time
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import squarify
from nltk.sentiment.vader import SentimentIntensityAnalyzer

필요한 패키지를 가져옵니다. praw는 Reddit API 사용을 편리하게 하는 패키지입니다. Sentiment Analysis는 VADER (Valence Aware Dictionary and sEntiment Reasoner) 모델을 사용합니다. 단어들에 따라, 부정적인지 긍정적인지에 따라 가중치를 부여하는 방식으로 간편하게 사용할 수 있어서 많이 쓰입니다. 특히 소셜 미디어에 특화되어 있다고 알려져 있습니다.

# adding wsb/reddit flavour to vader to improve sentiment analysis, score: 4.0 to -4.0
new_words = {
    'citron': -4.0,  
    'hidenburg': -4.0,        
    'moon': 4.0,
    'highs': 2.0,
    'mooning': 4.0,
    'long': 2.0,
    'short': -2.0,
    'call': 4.0,
    'calls': 4.0,    
    'put': -4.0,
    'puts': -4.0,    
    'break': 2.0,
    'tendie': 2.0,
     'tendies': 2.0,
     'town': 2.0,     
     'overvalued': -3.0,
     'undervalued': 3.0,
     'buy': 4.0,
     'sell': -4.0,
     'gone': -1.0,
     'gtfo': -1.7,
     'paper': -1.7,
     'bullish': 3.7,
     'bearish': -3.7,
     'bagholder': -1.7,
     'stonk': 1.9,
     'green': 1.9,
     'money': 1.2,
     'print': 2.2,
     'rocket': 2.2,
     'bull': 2.9,
     'bear': -2.9,
     'pumping': -1.0,
     'sus': -3.0,
     'offering': -2.3,
     'rip': -4.0,
     'downgrade': -3.0,
     'upgrade': 3.0,     
     'maintain': 1.0,          
     'pump': 1.9,
     'hot': 1.5,
     'drop': -2.5,
     'rebound': 1.5,  
     'crack': 2.5,}

vader = SentimentIntensityAnalyzer()
vader.lexicon.update(new_words)

wallstreetbets와 Reddit에서 종종 쓰이는 단어들을 VADER 모델의 단어 목록에 추가합니다.

blacklist = ['RAW','THE','BUT','USA','AUG','UK','STAY','SEP','IRS','EDIT','YOLO','IT','DOWAM','JUL','ROPE','CEO','WTF',
             'TL;DR','AYYMD','HQ','BE','DD','I','FOMO','MACD','FD','PRAY','IL','RIP','CPU','OCT','EZ','LGMA','A','OUT',
             'RH','BIG','OTM','SEC','STILL','NOW','ATM','RISEIPA','CA','NOV','TYS','RIDE','LPT','TICK','FOR','LMFAO',
             'EOD','PR','COD','OPEN','SO','POS','PS','BEZOS','OP','ATH','ELON','AT','TOS','DEC','BMW','IV','USD',
             'GG','ARE','URL','FL','HAS','ON','EV','US','PC','GO','NOT','U','CFO','GOAT','ITM','WSB','IMO','KYS','CAN',
             'ALL','FEB','LOL','ROFL','IPO','SEE','AH','IS','RED','ISIS','FIFA','OR','DJIA','DR','FBI','DOJ','JAN','SSN',
             'LOVE','LMAO','OK','SEPT','ER','GDP','ONE','Mar','PDT','GOD','PT','PDFUA','TL','MILF','GOOD','CTO','PM','ICE',
             'AM','BY','FDA','BTFD']

말 그대로 blacklist입니다. 분석에 아무 도움이 되지 않는 단어들로, 제외할 것입니다.

# https://www.nasdaq.com/market-activity/stocks/screener
# NYSE NASDAQ AMEX, Mega Large Medium Small Cap (mktcap >= $300m)
stockcode = pd.read_csv('US_Tickers_20210402.csv')
stockcode = stockcode['Symbol'].to_list()

시가 총액 3억 달러 이상인 주식 티커만 가져옵니다. NYSE NASDAQ AMEX 기준으로 4098개가 존재합니다.

start_time = time.time()

# API 호출 정보 
with open("RedditAPI.txt", "r") as f:
    lines = f.readlines()
    username = lines[0].strip()
    password = lines[1].strip()
    client_id = lines[3].strip()
    client_secret = lines[4].strip()
    user_agent = lines[8].strip()

# reddit API wrapper: praw
reddit = praw.Reddit(user_agent=user_agent,
    client_id=client_id,
    client_secret=client_secret,
    username=username,
    password=password)

Reddit API 호출에 필요한 bot의 username, password, client id, client secret key, user agent를 가져오고 praw 패키지에 먹입니다.

# program parameters

# sub-reddit to search: 참여자 수가 많은 4개 subreddit
subs = ['wallstreetbets','stocks','StockMarket','investing']     
# posts flairs to search || None flair is automatically considered: subreddit 안에 있는 카테고리
post_flairs = {'Daily Discussion','Weekend Discussion' ,'Discussion','Company News','Trades','Company Analysis' ,'Fundamentals/DD','Valuation','Opinion'}    
goodAuth = {'AutoModerator'}   # authors whom comments are allowed more than once
uniqueCmt = True                # allow one comment per author per symbol
ignoreAuthP = {'example'}       # authors to ignore for posts 
ignoreAuthC = {'example'}       # authors to ignore for comment 
upvoteRatio = 0.70         # upvote ratio for post to be considered, 0.70 = 70% (추천 많이 받은 글만 판정)
ups = 10       # define # of upvotes, post is considered if upvotes exceed this # (추천 10개 필요)
limit = 1000     # define the limit, comments 'replace more' limit 
upvotes = 2     # define # of upvotes, comment is considered if upvotes exceed this # (댓글은 추천 2개 필요)
picks = 30     # define # of picks here, prints as "Top ## picks are:"  (top 30개 뽑기)
picks_ayz = 30   # define # of picks for sentiment analysis (top 30개 sentiment analysis)

posts, count, c_analyzed, tickers, titles, a_comments = 0, 0, 0, {}, [], {}
cmt_auth = {}

# subreddit 목록에서 반복
for sub in subs:
    subreddit = reddit.subreddit(sub)
    hot_python = subreddit.hot()    # posting은 hot 먼저
    
    # Extracting comments, symbols from subreddit
    for submission in hot_python:
        flair = submission.link_flair_text 
        author = submission.author.name         
        
        # posting 추천 비율, 추천 수, 카테고리, 작성자 필터링 
        if submission.upvote_ratio >= upvoteRatio and submission.ups > ups and (flair in post_flairs or flair is None) and author not in ignoreAuthP:   
            # 댓글 최신순
            submission.comment_sort = 'new'     
            comments = submission.comments
            titles.append(submission.title)
            posts += 1
            try: 
                # 댓글 추가 소환 1000회 제한
                submission.comments.replace_more(limit=limit)   
                for comment in comments:
                    # try except for deleted account?
                    try: auth = comment.author.name
                    except: pass
                    c_analyzed += 1
                    
                    # 댓글 추천 수, 작성자 필터링
                    if comment.score > upvotes and auth not in ignoreAuthC: 
                        # 공백으로 구분
                        split = comment.body.split(" ")
                        for word in split:
                            # $ 표시 제거
                            word = word.replace("$", "")        
                            # upper = ticker, length of ticker <= 5, excluded words,                     
                            if word.isupper() and len(word) <= 5 and word not in blacklist and word in stockcode:
                                
                                # unique comments, try/except for key errors
                                # 적절한 작성자가 아닐 경우 (작성자 1인은 티커 1개에 대한 댓글 1개까지만 체크)
                                # 이미 나온 작성자면 그 댓글은 분석 목적으로 추가 안 할 것
                                if uniqueCmt and auth not in goodAuth:
                                    try: 
                                        if auth in cmt_auth[word]: break
                                    except: pass
                                    
                                # counting tickers
                                # ticker의 count 추가(언급 수 추가됨), 댓글 text 추가, 작성자 추가
                                # ticker가 기존에 언급되지 않았던 경우에는 ticker 자체가 추가되면서 count 1로 잡힘
                                if word in tickers:
                                    tickers[word] += 1
                                    a_comments[word].append(comment.body)
                                    cmt_auth[word].append(auth)
                                    count += 1
                                else:                               
                                    tickers[word] = 1
                                    cmt_auth[word] = [auth]
                                    a_comments[word] = [comment.body]
                                    count += 1   
            except Exception as e: print(e)
            
# sorts the dictionary
# 언급 많은 순으로 정렬
symbols = dict(sorted(tickers.items(), key=lambda item: item[1], reverse = True))
top_picks = list(symbols.keys())[0:picks]
time = (time.time() - start_time)

# print top picks
print("It took {t:.2f} seconds to analyze {c} comments in {p} posts in {s} subreddits.\n".format(t=time, c=c_analyzed, p=posts, s=len(subs)))
print("Posts analyzed saved in titles")
#for i in titles: print(i)  # prints the title of the posts analyzed

print(f"\n{picks} most mentioned picks: ")
times = []
top = []
for i in top_picks:
    print(f"{i}: {symbols[i]}")
    times.append(symbols[i])
    top.append(f"{i}: {symbols[i]}")
   
# Applying Sentiment Analysis
scores, s = {}, {}

# Sentiment Analysis 대상 ticker만 가져옴
picks_sentiment = list(symbols.keys())[0:picks_ayz]

# 언급 많은 순으로 정렬된 ticker 목록에서 댓글에 vader 기법 적용해서 sentiment analysis
for symbol in picks_sentiment:
    stock_comments = a_comments[symbol]
    for cmnt in stock_comments:
        score = vader.polarity_scores(cmnt)
        
        # ticker가 s에 있으면 ticker - cmnt에 점수 달아줌
        # ticker가 s에 없으면 ticker 자체에 새로 cmnt-score 달아줌
        if symbol in s:
            s[symbol][cmnt] = score
        else:
            s[symbol] = {cmnt:score}      
        
        # ticker가 scores에 있으면 점수 나온 것 더해줌
        # ticker가 scores에 없으면 ticker:점수 형태로 넣음 
        if symbol in scores:
            for key, _ in score.items():
                scores[symbol][key] += score[key]
        else:
            scores[symbol] = score
            
    # calculating avg
    for key in score:
        scores[symbol][key] = scores[symbol][key] / symbols[symbol]
        scores[symbol][key]  = "{pol:.3f}".format(pol=scores[symbol][key])

# print sentiment analysis
print(f"\nSentiment analysis of top {picks_ayz} picks:")
df = pd.DataFrame(scores)
df.index = ['Bearish', 'Neutral', 'Bullish', 'Total/Compound']
df = df.T
print(df)

# weighted average of sentiment --> reddit index
# top[i], df['Total/Compound'][i] check. 
# 30 stocks
reddit_idx = 0
referred = 0

for i in range(30):
    reddit_idx += int(top[i].split()[1]) * float(df.iloc[i,3])
    referred += int(top[i].split()[1])
reddit_idx = float(reddit_idx / referred)
    
print("Today's Reddit Index: " + str(reddit_idx))

# Date Visualization
# most mentioned picks    
plt.figure(figsize = (16, 12))
squarify.plot(sizes=times, label=top, alpha=.7 )
plt.axis('off')
plt.title(f"{picks} most mentioned picks")
plt.show()

# Sentiment analysis
df = df.astype(float)
colors = ['red', 'springgreen', 'forestgreen', 'coral']
df.plot(kind = 'bar', color=colors, title=f"Sentiment analysis of top {picks_ayz} picks:")
plt.show()





© 2021.03. by JacobJinwonLee

Powered by theorydb