花火大会におけるTwitter民の感情分析

2017年7月29日に行われた2017年度隅田川花火大会。 7月27日午前9時から花火大会翌日の30日午前9時までの隅田川花火大会に関するツイートの時系列での感情分析結果はこんな感じでした。

 f:id:ken787:20170810073024p:plain

ポジティブなツイートほど1に近く,ネガティブなツイートほど-1に近づきます。 なかなかネガティブですね! 以下で分析方法を解説します!

雨の中開催された隅田川花火大会

早速ですが花火大会当日前後の天気を見てみましょう。Pythonを使って気象庁のホームページからデータを引っ張ってきました。以下のサイトを参考にしています。

qiita.com

import pandas as pd

dates = [27,28,29]
df_weather = pd.DataFrame()

for date in dates:
    url = 'http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=44&block_no=47662&year=2017&month=7&day='+str(date)+'&view='
    df = pd.read_html(url)[0].dropna().iloc[date-1,[19,20]].transpose()
    df.index = ['昼(06:00-18:00)', '夜(18:00-翌日06:00)']
    df_weather = pd.concat([df_weather, df],axis=1)
df_weather.columns = dates
df_weather[::-1]

27
28
29
昼(06:00-18:00)
曇時々晴
曇後一時雨、雷を伴う
夜(18:00-翌日06:00)
曇時々雨
雨後一時曇、雷を伴う

花火大会前々日から不安定な天気,そして当日はバッチリと雨が降っていたようですね。それでももちろん花火大会は(強風がない限り)開催されます。 花火大会当日の東京の天気予報が出た頃からTwitter上では様々な発言が飛び交いました。想像に難くないと思いますが概ね以下のようなものです。

  1. 天候不良による花火大会の中止を危惧する声
  2. 天候不良による花火大会の中止を期待する声
  3. 雨天時の開催の是非に関するお知らせ
  4. 脳内妄想ツイート

理系単科大学生である僕のタイムライン上では2番と4番が多かったですね。そもそも触れてない人が殆どでしたが。 それでは,隅田川花火大会前々日から当日までのツイッター民の感情はどのように推移したのでしょうか?

花火大会に関するツイートをしたアカウントの一つ一つを見て行くのはTwitter APIを用いなければならず,その仕様上時間がかかりすぎるためTwitter上の全体的な傾向のみを分析することにします。

実行環境

・  conda 4.3.23
・  python 3.6.0
・  jupyter notebook 4.3.1 その他,numpyやpandasなどのパッケージ,および後に特筆するGetOldTweets-pythonを用いました.最後のコレは上記に環境での使用に当たって注意が必要です.

Twitterからツイートを検索し取得する

Twitter社は,プログラムを用いてTwitter上のデータを収集するためのAPIを公開しています。一回に取得できるツイート数に制限があったり短時間に何回も取得を行えなかったりと使い勝手がよくないと言われています。(こちらがデータをタダで使おうとしてるからですが.)

またこのAPIの最も致命的な点は7日前までのツイートしか取得できないのです.。8月5日を過ぎてしまったためTwitter APIを用いて隅田川花火大会前後のツイートを収集することができません!

そこでHTTPリクエストを用いてツイートを取得するGetOldTweets-pythonを用いました。参考にしたサイト及びGetOldTweets-pythonのgitリポジトリは以下の通りです。

・ stackoverflow.com
・ github.com

なお,このGetOldTweets-pythonはpython3で廃止となった(厳密にはurllibパッケージに統合された)urllib2を用いていますので,python3.6.0の本環境で使用するためにurllib2を使用している部分を書き換えて使用しました。python3以前なら問題ないと思います(検証はしてません)GitHubからクローンしたgot3フォルダを作業ディレクトリに持って来てimportすると使用できます。

それでは,2017-7-27から2017-7-29までの隅田川花火大会に関するツイートを収集しましょう!

以下のメソッドを活用してクエリを作成します。

  • setUsername (str): An optional specific username from a twitter account. Without “@”.
  • setSince (str. “yyyy-mm-dd”): A lower bound date to restrict search.
  • setUntil (str. “yyyy-mm-dd”): An upper bound date to restrist search.
  • setQuerySearch (str): A query text to be matched.
  • setTopTweets (bool): If True only the Top Tweets will be retrieved.
  • setNear(str): A reference location area from where tweets were generated.
  • setWithin (str): A distance radius from “near” location (e.g. 15mi).
  • setMaxTweets (int): The maximum number of tweets to be retrieved. If this number is >- >- unsetted or lower than 1 all possible tweets will be retrieved.

このライブラリの一日は朝9時始まりらしいので指定期間は 2017-7-27 から 2017-7-30, 検索文字列は “#隅田川花火大会 OR 隅田川花火大会 OR (隅田川 AND 花火) OR (隅田川 AND 花火大会)“ と指定します。

英語では,語句の区切りに空白文字を使用しますが日本語では使用しないため文字列検索がどういった挙動を示すかわかりません。そのため大事を取って広めに指定しました。条件に合致するすべてのツイートを集めるため setMaxTweets() は無指定で行きました。

以下スクリプトです.

import got3

since = '2017-07-27'
until = '2017-07-30'
q = '#隅田川花火大会 OR 隅田川花火大会 OR (隅田川 AND 花火) OR (隅田川 AND 花火大会)'

tweetCriteria = got3.manager.TweetCriteria().setSince(since).setUntil(until).setQuerySearch(q)
tweets = got3.manager.TweetManager.getTweets(tweetCriteria)

リクエストが処理されるまで2時間ほどかかりました。せっかく取得したものを失うのが怖いので早急にcsvファイルに退避させます。1日ごとに抜き出した方がいいかもしれません.僕は1時間半越えたあたりまで処理したところを2回失敗してます(泣

f = open('tweet_data.csv', 'w')
for tweet in tweets:
    tweet_list = [str(tweet.id), str(tweet.text.replace(',','')), str(tweet.date)]
    f.write(','.join(tweet_list) + '\n')
f.close()

pandas.DataFrame() にデータを詰めて処理していきます。

import pandas as pd
df_tweets = pd.read_csv('tweet_data.csv', names=['id', 'text', 'date'], index_col='date')

ここで df_tweets.head() にて冒頭を確認したかったのですがたった5ツイートの中に検閲対象のものがありました。 投稿時間について降順に格納されていました。またもしかしたらと思ってidも抽出しましたが,いらない気もするので消します。また,indexになっている投稿時間を datatime 型に変換し,昇順にします。

df_tweets.index = pd.to_datetime(df_tweets.index)
df_tweets = df_tweets[['text']].sort_index(ascending=True)

これで,7月27日から7月29日までの隅田川花火大会に関するツイートが時系列順に揃いました! 引き続き,感情分析を行う準備をしましょう。

ツイートの形態素解析及び印象の評価

取得したツイートはただの文字の集合であるため扱いづらいです。 そこで,ツイートを形態素解析して単語ごとに分け,基本形表記に変換します。

そして得られた単語一つ一つの「ポジティブさ」または「ネガティブさ」を評価します。 形態素解析にはMeCabを使い,単語の「ポジティブさ」の評価はPN Tableという辞書を用いました。PN Tableは単語の印象が-1から+1の実数で表された辞書で,+1に近いほど「ポジティブな」印象,逆に-1に近いほど「ネガティブな」印象を持つとしています。

こういった「ポジティブ」,「ネガティブ」といった性質を極性といい,極性の値をPN値と呼びます。 文章の感情分析には以下のサイトを参考にしました。また,リンク先のPN Tableファイルの文字コードはUTF-8ではないのでnkfコマンドなどを用いてutf-8に変換しておくことをお勧めします。 

必要な処理は以下です

  • テキスト(文章)を形態素解析し,単語の集合を得る。
  • 単語ごとのPN値を求める。
  • 各ツイートの平均のPN値を求める。

上記のサイトを参考に実装しました。

import pandas as pd
import numpy  as np
import MeCab

# 辞書のデータフレームを作成.パスは適宜設定してください.
pn_df = pd.read_csv('./pn_ja.dic', sep=':', encoding='utf-8', names=('Word','Reading','POS', 'PN'))
# 各列の分割
word_list = list(pn_df['Word'])
pn_list   = list(pn_df['PN'])
pn_dict   = dict(zip(word_list, pn_list))

# MeCabインスタンスの作成.引数を無指定にするとIPA辞書になります.
m = MeCab.Tagger('')

# テキストを形態素解析し辞書のリストを返す関数
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_mean(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

では,さきほど抽出したデータ一件一件に対して処理を適用していきましょう。PN Tableに含まれない語句は考慮の対象外とし,PN Tableに含まれる語句が一つもない場合,そのツイートのPN値は0となります。

means_list = []
for tweet in df_tweets['text']:
    dl_old = get_diclist(tweet)
    dl_new = add_pnvalue(dl_old)
    pnmean = get_mean(dl_new)
    means_list.append(pnmean)

えられたPN値の平均値のリストを対応するツイートと統合します。

df_tweets['pn'] = means_list

ツイートの感情の可視化

この三日間(7/27-29)の隅田川花火大会に関するツイートのPN値の平均を出してみます。

df_tweets['pn'].mean()
-0.4940130992684058

取り得る値は-1から1までの実数値なのですが,少々低過ぎやしないですかね? しかし,これはあくまでも単語の印象を元に算出した値です。 そもそも言語が崩壊しているようなツイート,広告ツイートなど予め除外した方がいいツイートはたくさんあるでしょうし,ネットスラングに対する辞書の不完全さも正確な評価が難しい理由の一つでしょう。 また,ツイート数も日によって偏りがあります。

print('三日間全てのツイート:',len(df_tweets))
print('7月29日のツイート:',df_tweets['2017-7-29']['text'].count())
三日間全てのツイート: 81979
7月29日のツイート: 72656

最後に,ツイートの極性の時系列での推移がわかるようにデータを可視化しましょう! 最初に日付はpandasのTimeStampとして格納していたので楽ですね!

import matplotlib.pyplot as plt
%matplotlib inline   # jupyter notebookでmatplotlibを使うためのおまじない

x = df_tweets.index
y = df_tweets.pn
plt.plot(x,y)
plt.grid(True)

f:id:ken787:20170810073024p:plain

こうやって見るとなんか,うーん... ネガティブなツイートが終始多いですね,特に前日にかけてのが.これ,雨が降ったことに対する「ざまぁ!!」も負の印象として扱われているのでしょう。 日頃のタイムライン上でも,ポジティブな内容のツイートって少ないのではないでしょうか? 最後までご覧頂き,ありがとうございました!

プログラミング未経験からでもAIスキルが身につくAidemy Premium




PythonやAIプログラミングを学ぶなら、オンライン制スクールのAidemy Premiumがおすすめです。
「機械学習・ディープラーニングに興味がある」
「AIをどのように活用するのだろう?」
「文系の私でもプログラミング学習を続けられるだろうか?」
少しでも気になることがございましたら、ぜひお気軽にAidemy Premiumの【オンライン無料相談会】にご参加いただき、お悩みをお聞かせください!