ネガポジ分析アプリを開発する(アプリ編)
本日はネガポジ分析アプリを開発するのアプリ編について記載します。
これまで環境構築、CI/CDとやってきましたので、そちらもよければご参照ください。
1.アプリの概要
自分の気になるキーワードがTwitter上でポジティブな言葉としてTweetされているのか、あるいはネガティブな言葉としてTweetされているのかをAIを使って分析し過去からの推移を表示します。
また、キーワードを含むTweetのうち「いいね」「リツイート」が高いものをPickUpして表示することで、ポジティブ/ネガティブという側面からばずったTweetを分析します。
2.画面解説(ネガポジグラフ)
下記は「機械学習エンジニア」というキーワードで分析した画面です。
直近10日間のうち毎日100tweetを分析してポジティブな内容=高い点数としてグラフで推移を表しています。
機械学習エンジニアはポジティブな発言もネガティブな発言も両方同じくらいあるようです。
3.画面解説(ネガポジTweet)
グラフの下にはポジティブなTweetとネガティブなTweetを紹介しています。
ポジティブなツイートを見ると機械学習という言葉と一緒に「面白そう」、「大好物」という言葉が使われており、機械学習をポジティブに捉えているTweetとなっています。
一方でネガティブなTweetは「難しい」という言葉が何回も出ており、ネガティブと捉えてしまったと思います。
ただ実際にはこのTweetは「難しいけどどうすれば?」というQに対してAも記載しているのでネガティブな要素も含んでいるけど解決策も提示しています。
このあたりをネガティブTweetとして表示するかは今後検討が必要かと思います。
4.画面解説(いいね&リツイートグラフ)
ネガポジ分析の次は「いいね」と「リツイート」の推移を表示しています。
10/26の「いいね」が322と他より多くなっています。このTweetを見ていきましょう。
5.画面解説(いいね&リツイートTweet)
いいねが一番多かったのはネガポジ分析でも登場したQA形式のTweetでした。
機械学習が難しいという皆が持っている悩みについて、解決策を提示したからたくさん「いいね」がついたと思いました。
以上がネガポジ分析アプリの説明となります。
これからこのアプリのソース解説をしていきます。
6.環境
Python 3.9.7 [開発言語]
Flask 2.0.2 [PythonのWebアプリ用ライブラリ]
mecab-python3 1.0.4 [自然言語処理AI用のライブラリ]
numpy 1.21.3 [Pythonのデータ加工用のライブラリ]
pandas 1.1.5 [Pythonのデータ加工用のライブラリ]
tweepy 3.10.0 [TwitterAPIを利用するためのライブラリ]
以上が主要な環境となります。
その他も含めた全てのパッケージ一覧を下記に載せておきます。
certifi==2021.10.8
charset-normalizer==2.0.7
click==8.0.3
cycler==0.10.0
Flask==2.0.2
gunicorn==20.1.0
idna==3.3
itsdangerous==2.0.1
Jinja2==3.0.2
kiwisolver==1.3.2
MarkupSafe==2.0.1
mecab-python3==1.0.4
numpy==1.21.3
oauthlib==3.1.1
pandas==1.1.5
pyparsing==2.4.7
PySocks==1.7.1
python-dateutil==2.8.2
pytz==2021.3
requests==2.26.0
requests-oauthlib==1.3.0
six==1.16.0
tweepy==3.10.0
unidic-lite==1.0.8
urllib3==1.26.7
Werkzeug==2.0.2
7.HTML(キーワード入力)
まず、キーワード入力の部分です。
<section>
<h2>{{keyword}}人気分析</h2>
<p>キーワードに該当するTweetの内容を分析して点数化(ポジティブな内容=高い点数)
各Tweetの点数から日ごとの平均値でグラフで表示</p>
<div class="row">
<div class="chart-container">
<canvas id="canvas1" width="600" height="300"></canvas>
</div>
</div>
</section>
8.HTML(ネガポジグラフ)
次にネガポジ分析のグラフ部分です。
<section>
<h2>{{keyword}}人気分析</h2>
<p>キーワードに該当するTweetの内容を分析して点数化(ポジティブな内容=高い点数)
各Tweetの点数から日ごとの平均値でグラフで表示</p>
<div class="row">
<div class="chart-container">
<canvas id="canvas1" width="600" height="300"></canvas>
</div>
</div>
</section>
上記はcanvasタグを設置しただけで描画しているのは下記になります。
var ctx = document.getElementById('canvas1');
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: {{datelist | tojson}},
datasets: [{
label: 'Negative/Positive',
data: {{pnlist | tojson}},
borderColor: '#fff',
}],
},
options: {
y: {
min: -1,
max: 1,
},
},
});
9.HTML(ネガポジTweet)
<section>
<div id="tweet1" class="tweet">
<div id="positive-tweet" class="p-twitter-box">
<div class="p-twitter-title p-twitter-brown"><i class="fab fa-twitter"></i><img border="0" width="16px" height="16px" src="/static/img/up_arrow.png"> Positive Tweet</div>
{% for Ptweet in Plist %}
<blockquote class="twitter-tweet">
<a href="{{ Ptweet }}"></a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
{% endfor %}
</div>
<div id="negative-tweet" class="p-twitter-box">
<div class="p-twitter-title p-twitter-brown"><i class="fab fa-twitter"></i><img border="0" width="16px" height="16px" src="/static/img/down_arrow.png"> Negative Tweet</div>
{% for Ntweet in Nlist %}
<blockquote class="twitter-tweet">
<a href="{{ Ntweet }}"></a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
{% endfor %}
</div>
</div>
</section>
HTML上にある{% for Ptweet in Plist %}~{% endfor %}と
{% for Ntweet in Nlist %}~{% endfor %}はFlaskの書き方です。
このテンプレートHTMLに引数で渡したPlistとNlist(TweetのURL)を埋め込んでいます。
10.HTML(いいね&リツイートグラフ)
<section>
<h2>いいね&リツイート分析</h2>
<p>キーワードに該当するTweetのうち日ごとにもっとも「いいね」と「リツイート」が多いものを表示<br/>
<div class="row">
<div class="chart-container">
<canvas id="canvas2"></canvas>
</div>
</div>
</section>
グラフの描画部分は下記となります。
var ctx2 = document.getElementById('canvas2');
var myChart2 = new Chart(ctx2, {
type: 'line',
data: {
labels: {{datelist2 | tojson}},
datasets: [{
label: 'Like',
data: {{favmaxlist | tojson}},
borderColor: '#bab9a0',
}, {
label: 'Retweet',
data: {{RTmaxlist | tojson}},
borderColor: '#baa0a1',
}],
},
options: {
y: {
min: 0,
max: {{max}},
},
},
});
11.HTML(いいね&リツイートTweet)
<section>
<div id="tweet2" class="tweet">
<div id="like-tweet" class="p-twitter-box">
<div class="p-twitter-title p-twitter-brown"><i class="fab fa-twitter"></i><img border="0" width="16px" height="16px" src="/static/img/like.png"> Best Liked Tweet</div>
{% for favtweet in favlist %}
<blockquote class="twitter-tweet">
<a href="{{ favtweet }}"></a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
{% endfor %}
</div>
<div id="retweet-tweet" class="p-twitter-box">
<div class="p-twitter-title p-twitter-brown"><i class="fab fa-twitter"></i><img border="0" width="16px" height="16px" src="/static/img/retweet.png"> Best Reweeted Tweet</div>
{% for RTtweet in RTlist %}
<blockquote class="twitter-tweet">
<a href="{{ RTtweet }}"></a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
{% endfor %}
</div>
</div>
</section>
12.Python(HTML呼び出し部分)
最初にHTMLを開く際は引数は全て初期化して渡します。
@app.route('/')
def root():
return render_template('index.html',keyword='',
datelist=['',''],pnlist=[0,0],
datelist2=['',''],max=1,
favmaxlist=[0,0],RTmaxlist=[0,0],
Plist=[],Nlist=[],
favlist=[],RTlist=[])
キーワード入力して分析したときは下記となります。
@app.route('/q', methods=['GET'])
def query():
keyword=request.args.get('keyword')
tw = TwitterAnalays()
df_tweets = tw.analays(keyword)
datelist, pnlist, pnmax = makePNchart(df_tweets, 'pn')
datelist2, favmaxlist, max = makePNchart(tw.favmaxlist,'fav')
RTdatelist, RTmaxlist, RTmax = makePNchart(tw.RTmaxlist,'RT')
if max < RTmax:
max=RTmax
favlist = makeTwUrl(tw.favlist)
RTlist = makeTwUrl(tw.RTlist)
Plist = makeTwUrl(tw.Plist)
Nlist = makeTwUrl(tw.Nlist)
return render_template('index.html',
keyword=keyword,
datelist=datelist,pnlist=pnlist,
datelist2=datelist2,max=max,
favmaxlist=favmaxlist,RTmaxlist=RTmaxlist,
Plist=Plist,Nlist=Nlist,
favlist=favlist,RTlist=RTlist)
TwitterAnalays()がTwitterを取得してネガポジ分析するクラスです。
中の処理ではまず、TwitterAPIを使い、キーワードでTweetを取得します。
取得したTweetを入れる変数tweet_dataには、「Tweetの日時」「Tweet本文」「いいね数」「リツイート数」「Tweetのユーザ名」を保持します。
#TwitterAPI認証用
consumer_key = 'キーはご自身のを入力してください'
consumer_secret = 'キーはご自身のを入力してください'
access_token = 'キーはご自身のを入力してください'
access_secret = 'キーはご自身のを入力してください'
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_secret)
api = tweepy.API(auth)
#ツイート取得
tweet_data = []
dt = datetime.datetime.now()
for dummy in range(10):
dt = dt - datetime.timedelta(days=1)
tweets = tweepy.Cursor(api.search,q = keyword, since=dt.strftime("%Y-%m-%d_00:00:00_JST"), until=dt.strftime("%Y-%m-%d_23:59:59_JST"))
for tweet in tweets.items(100):
tweet_data.append([tweet.id,tweet.created_at + datetime.timedelta(hours=9),tweet.text.replace('\n',''),tweet.favorite_count,tweet.retweet_count,tweet.user.screen_name])
次に取得したTweetを1件ずつ分析します。
Tweetを分析するためにまずは、mecabを使い単語単位で区切ります。
引数のmecab = mecabインスタンス、text = Tweet1件、です。
def __get_diclist(self, mecab, text):
parsed = mecab.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[3]}
diclist.append(d)
return(diclist)
次に分解した単語1語ずつ辞書を検索してポジティブ、ネガティブの点数をつけます。
引数のdiclist_old = 上で作った単語単位に分けた1Tweet、pn_dict = 各語のポジティブ、ネガティブを管理するマスタ。
def __add_pnvalue(self, diclist_old, pn_dict):
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値の平均を計算します。
引数のdictlist = 上で求めたTweet1件に含まれる単語単位のPN値
def __get_mean(self, dictlist):
pn_list = []
for word in dictlist:
pn = word['PN']
if pn!='notfound':
pn_list.append(pn)
if len(pn_list)>0:
pnmean = np.mean(pn_list)
else:
pnmean=0
return pnmean
上記の「__get_diclist」「__add_pnvalue」「__get_mean」を呼びだし一連の処理をしているのは下記となります。
def __add_dailyPN(self, df_tweets, pn_dict, mecab):
# means_listという空のリストを作りそこにツイートごとの平均値を求める
means_list = []
for tweet in df_tweets['text']:
if len(tweet)>4 and tweet.startswith('RT @') == False:
dl_old = self.__get_diclist(mecab, tweet)
dl_new = self.__add_pnvalue(dl_old, pn_dict)
pnmean = self.__get_mean(dl_new)
means_list.append(pnmean)
else:
means_list.append(np.nan)
各TweetのPN値を求めたら、標準化を行い、日毎の平均値を求めます。
この日次のPN値の平均をHTML側に渡し、ポジティブ、ネガティブの推移グラフを描画する流れになります。
# means_listをnumpy配列に変換
means_list = np.copy(means_list)
df_tweets['pn'] = means_list
df_tweets = df_tweets.dropna(subset=['pn'])
# means_listを用いて標準化を行う
x_std = (df_tweets['pn'] - df_tweets['pn'].mean()) / df_tweets['pn'].std()
df_tweets['pn'] = x_std
df_tweets.index = pd.to_datetime(df_tweets.index)
df_tweets['pn'] = df_tweets['pn'].astype(float)
df_tweets = df_tweets.resample('D').mean().interpolate()
return df_tweets
ポジティブTweet、ネガティブTweetについては日毎の平均値を作成する前に下記のようにPN値でソートしたものを表示しています。
self.Plist = df_tweets.sort_values('pn',ascending=False)
self.Nlist = df_tweets.sort_values('pn',ascending=True)
ここで作成したPlistとNlistを下記関数に渡すことでTweetのURLを作成できます。
TweetのURLは形が決まっているので、TweetしたユーザとTweetのID(どちらもTwitterAPIでTweetを取得した時にとれます)をセットするとURLを作成できます。
今回のアプリではリストから最大3件のURLを作成しています。
def makeTwUrl(tweetList):
url_list = []
for tweet in tweetList.itertuples():
url = 'https://twitter.com/{}/status/{}?ref_src=twsrc%5Etfw'.format(tweet.name,tweet.id)
url_list.append(url)
#print(url)
if len(url_list) == 3:
break
return url_list
13.さいごに
上記では「機械学習エンジニア」というキーワードを使いましたが、
他にも、有名人の名前を入れると人気推移のような見方もでき、面白いものができました。
ただ、難点がTwitterAPIに制限があり、頻繁にキーワードを変えて分析できないということです。
11/3追記
TwitterAPIの上限に達した場合は下記画面のように利用可能になる時刻を表示するように修正しました。
いつまで公開するか未定ですがリンクを張っておきます。
よかったらご確認ください。