安倍首相は常にネガティブ!?!?

実行結果
f:id:ryo0927:20180627204643p:plain
初めまして、Aidemy研修生の加賀美です。
今回は、自然言語処理(MeCab)とTwitter APIを使い、安倍首相のツイートを感情分析してみました。

・対象者

PythonでTwitter APiを使ってみたい人

MeCabを使って日本語の感情分析をやってみたい人

では、早速Tweitter APIを利用して感情分析をしてみましょう!!

目次

実装

Tweitter APIのkey取得

Twitterデータは、APIを使って取得します。APIを使用するために必要なkeyは以下の4つです。

・Consumer Key
・Consumer Secret
・Access Token
・Access Token Secret

これらのkeyを取得する手順の仕方については、以下記事でわかりやすくまとめられていますので、ご参照ください。
www.randpy.tokyo

ツイートを取得する

以下のコードで、特定のユーザのツイートを取得します。


#今回使用するモジュール
import neologdn
import re
import json
from requests_oauthlib import OAuth1Session
import requests
import MeCab
import pandas as pd
import re
from statistics import mean
from bs4 import BeautifulSoup
from datetime import datetime
import matplotlib.pyplot as plt
%matplotlib inline

#カンマの間に取得したkeyを入れてください
consumer_key = ''
consumer_secret = ''
access_token = ''
access_token_secret = ''

#APIの認証
twitter = OAuth1Session(consumer_key,consumer_secret,access_token,access_token_secret)

#ユーザのツイートを取得
url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'

#取得するユーザを指定 @の後の名前
screen_name = "AbeShinzo"

#パラメータ設定 max_id: どのツイートから取得するか決める
params = {'screen_name': screen_name,'count': 100,
         'max_id':1008645666061500419,'include_rts':False,
          'exclude_replies':True,'contributor_details':False}

#リクエストを投げる
res = twitter.get(url,params = params)

#テキスト・時間・IDを追加する場所
tweet_list = []
time_ = []
tweet_id = []

#ツイートを取得 今回は制限に引っかからないようにしていますが、念の為、制限まで行ったらストップするようにしています。’if limit == 1の数字を変更するとより多くのツイートを取得できます。’
for j in range(1):
    res = twitter.get(url,params = params)
    if res.status_code == 200:
        #APIの残りを表示
        limit = res.headers['x-rate-limit-remaining']
        print ("API remain: " + limit)
        if limit == 1:
            #15分間停止させる
            sleep(60*15)
        
        timeline = json.loads(res.text)
        #各ツイートの本文を'tweet_list'に追加
        #各ツイートの投稿日を'time_'に追加
        for i in range(len(timeline)):
            if i != len(timeline)-1:
                tweet_list.append(timeline[i]['text']+'\n')
                time_.append(timeline[i]['created_at'])
                tweet_id.append(timeline[i]['id'])
            else:
                tweet_list.append(timeline[i]['text']+'\n')
                time_.append(timeline[i]['created_at'])
                tweet_id.append(timeline[i]['id'])
                
                params['max_id'] = timeline[i]['id']-1
# 日付の整形 Twitter APIから得られる日付は米国の時間のため日本時間に直す
time_list = []
def make_convert_date_format(src_format, dst_format):
    def convert_date_format(s):
        return datetime.strftime(datetime.strptime(s, src_format),dst_format)

    return convert_date_format

convert_date_format = make_convert_date_format('%a %b %d %H:%M:%S %z %Y', '%Y-%m-%d')
for i in range(len(time_)):
    time_list.append(convert_date_format(time_[i]))
    
# ツイートからURLを削除、不要な文字を消す
def format_text(text):
    text = re.sub(r"(https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)", "", text)
    text = re.sub("\d", "", text)
    text = re.sub("\s", "", text)
    text = re.sub("\n","",text)
    text = re.sub("、","",text)
    text = re.sub("#","",text)
    text = re.sub("[()]","",text)
    text = re.sub("・","",text)
    text = re.sub("。","",text)
    text = re.sub("「","",text)
    text = re.sub("」","",text)
    text = re.sub("[a-zA-Z0-9]","",text)
    text = re.sub("@","",text)
    return text

neo_tweet = []
tweet_c = []
for i in range(len(tweet_list)):
    neo_tweet.append(neologdn.normalize(tweet_list[i]))
    tweet_c.append(format_text(neo_tweet[i]))

データフレーム にして確認。paramsのmax_idを変更するとそのツイートよりも前のツイートを取得します。


#データフレームを作成
df = pd.DataFrame({'tweet_list':tweet_list,'time':time_list,'tweet_id':tweet_id})
print(df.head())
f:id:ryo0927:20180621001903p:plain
実行結果

ツイートを形態素解析する

先に、以下のリンク先から単語感情極性対応表をダウンロードしてローカルに保存してください。
PN Table


#PN Tableを読み込み
pn_df = pd.read_csv('pn_ja.dic.txt',sep=':',encoding='shift-jis',
                    names=('Word','Reading','POS', 'PN'))
# MeCabインスタンス作成
m = MeCab.Tagger('')  # 指定しなければIPA辞書

# PN Tableをデータフレームからdict型に変換しておく
word_list = list(pn_df['Word'])
pn_list = list(pn_df['PN'])  # 中身の型はnumpy.float64
pn_dict = dict(zip(word_list, pn_list))

# テキストを形態素解析して辞書のリストを返す関数
def get_diclist(text):
    parsed = m.parse(text)      # 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'BaseForm':l[7]}
        diclist.append(d)
    return(diclist)

# 形態素解析結果の単語ごとdictデータにPN値を追加する関数
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base])
        else:
            pn = 'notfound'            # その語がPN Tableになかった場合
        word['PN'] = pn
        diclist_new.append(word)
    return(diclist_new)

# 各ツイートのPN平均値をとる関数
def get_pnmean(diclist):
    pn_list = []
    for word in diclist:
        pn = word['PN']
        if pn != 'notfound':
            pn_list.append(pn)  # notfoundだった場合は追加もしない            
    if len(pn_list) > 0:        # 「全部notfound」じゃなければ
        pnmean = mean(pn_list)
    else:
        # 全部notfoundならゼロにする
        pnmean = 0
    return(pnmean)

# 各ツイートをPN値に変換
pnmeans_list = []
for tw in tweet_c:
    dl_old = get_diclist(tw)
    dl_new = add_pnvalue(dl_old)
    pnmean = get_pnmean(dl_new)
    pnmeans_list.append(pnmean)

PN値に欠損値がないか確認

PN値とは、そのツイートがPositiveかNegativeかどうかを判断する数値です。この数値は+1 ~ -1の範囲で表現されます。+1に近ければポジティブ、-1に近ければネガティブと判断することができます。


a = pd.Series(pnmeans_list)
print(a.head(25))

f:id:ryo0927:20180623033225p:plain
22、23行目に0がありましたこれを確認してみます。
安倍首相のツイートをクリックして後ろの数字をTweet_IDの数字に変更すると該当するツイートに飛ぶことができます。


tn1 = tweet_id[22]
tn2 = tweet_id[23]
print('Tweet_ID 22行目: '+str(tn1))
print('Tweet_ID 23行目: '+str(tn2))

f:id:ryo0927:20180621004243p:plain
22行目のツイート
f:id:ryo0927:20180621004339p:plain
23行目のツイート
f:id:ryo0927:20180621004429p:plain
なぜPN値が0になってしまうのかというと、MeCabという日本語の形態素解析エンジンを使用しているため、英語に対応していないからです。

英語のツイートを除外する

PN値が0の、PN値・ツイート・ツイートID・投稿日を除外して別のリストに保存する


# PN値が0のものを削除
pnl = []
twl = []
tl = []
il = []
for i in range(len(pnmeans_list)):
    if pnmeans_list[i] != 0:
        pnl.append(float('{:.4}'.format(pnmeans_list[i])))
        twl.append(tweet_c[i])
        tl.append(time_list[i])
        il.append(tweet_id[i])

データフレーム にして22、23行目を確認


df1 = pd.DataFrame({'tweet':twl,'time':tl,'tweet_id':il,'PN値':pnl})
print(df1.iloc[22])
print(df1.iloc[23])

f:id:ryo0927:20180623033937p:plain
上の画像を見ると、英語のツイートがなくなっていることが確認できます。

PN値を見て、ポジティブかネガティブか判断する

さて、ここからが本題です。PN値の平均を出して、普段ポジティブとネガティブのツイートどちらが多いのか確認していきます。


#データフレームの作成
df = pd.DataFrame({'tweet':twl,'time':tl,'tweet_id':il,'PN値':pnl})

# 全体のPN値の平均を計算
_mean = df['PN値'].mean()
print('平均: '+str('{0:.4}'.format(_mean)))
pnl_max = max(pnl)
print('最大値: '+str(pnl_max))
pnl_min = min(pnl)
print('最小値: '+str(pnl_min))

print()
# PN値が0以上のものを抜き出す
for i in range(len(il)):
    if pnl[i] >= 0:
        print('PN値0以上: '+str(df.iloc[i]))
print()

# PN値が最小のものを抜き出す
for i in range(len(pnl)):
    if pnl[i] == pnl_min:
        print('PN値最小:'+str(df.iloc[i]))
print()

#ツイート毎にPN値をプロットする
plot_df = pd.DataFrame(pnl,index=tl)
plot_df.plot.bar(figsize=(20,10))
plt.tick_params(labelsize = 15)
plt.tight_layout()
plt.show()

f:id:ryo0927:20180623035750p:plain
f:id:ryo0927:20180627203500p:plain
ほう、グラフにするとかなりわかりやすいですね。12/20日以外は常にネガティブなツイートをしているようですね(笑)

よし、完成!じゃないんです。まだ、正規化を行っていません。
上で定義した、「def get_diclist(text)」を下記のように変更してください。


# ストップワードを取得
def stopwords():
    target_url = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
    r =requests.get(target_url)
    soup=BeautifulSoup(r.text, "html.parser")
    stop_=str(soup).split()
    return stop_

# 正規化
def dictionary_words(text):
    stop_word = stopwords()
    parsed = m.parse(text)# 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られている
        d = {'Surface':l[0], 'POS1':l[1], 'BaseForm':l[7]}
        if d['POS1'] == '名詞' or '動詞' or '形容詞' or '副詞':
            if not d['BaseForm'] in stop_word:
                diclist.append(d)
    return (diclist)

そして、下記も変更してください


# 各ツイートをPN値に変換
pnmeans_list = []
for tw in tweet_c:
    dl_old = dictionary_words(tw) #ここを変更
    dl_new = add_pnvalue(dl_old)
    pnmean = get_pnmean(dl_new)
    pnmeans_list.append(pnmean)

すると,,,,
f:id:ryo0927:20180623040014p:plain
f:id:ryo0927:20180627204643p:plain
グラフの見え方とPN値が変わりましたね。
正規化で「名詞・動詞・形容詞・副詞」だけを抽出することによって、それ以外の品詞にPN値が引っ張られないようにすることができます。

PN値が最大、最小のツイートを確認する

PN値が最大のツイートを見てみましょう
f:id:ryo0927:20180621033503p:plain
すごいポジティブ!!(笑)
「新しい」、「創っていってくれる」、「全力」、「支援」というキーワードがPN値を上げてくれる要因になったのかなと思いました。

次にPN値が最小のツイートを見てみます
f:id:ryo0927:20180621033642p:plain
いや、どんだけブルガリアに行きたくないんだ(笑)
「凍った」、「昼食後」がPN値を下げているのかなと思いました。

感想

今回は特定のユーザのツイートを取得してみましたが、次はキーワードやハッシュタグを取得して感情分析を行って行きたいと思っています。
自分でプログラムを組むと勉強したことの復習や、勉強で出てこなかった部分がとても勉強になりました。
「もう少しコードが短くなるよ!」「こんなやり方があるよ!」などありましたらぜひ教えてください!