「Twitterデータを使って、何か面白いことができないか」の第二弾です。
前回:
「停電」に関するツイートをpythonで収集して、WordCloudで可視化してみた
https://qiita.com/pocket_kyoto/items/0f43c9fdce87bddd31cf
今回は、前回と同様の方法で、Twitterデータを収集し、
「クッパ姫」に関する2018/9/24のツイートを可視化し、「クッパ姫」と共起する語のバースト検出を試してみました。
※この記事は2018/09/24に執筆しました。「クッパ姫」の元ネタがツイートされたのが2018/09/20(木)。その後、2018/09/23(日)に日本でTwitterのトレンド入りし、2018/09/24(月)には、様々なスーパーマリオキャラの女体化に関するツイートがトレンド入りしました。
クッパ姫って?
クッパ姫とは、『スーパーマリオ』の敵役クッパが「スーパークラウン」によって女体化(ピーチ姫化)した姿の二次創作のことです。(pixivより)
↓この漫画の4コマ目が元ネタのクッパ姫です。
詳細は「クッパ姫」で検索してみてください。
2018/09/23(日)以降に、日本で爆発的な人気を得ました。
「クッパ姫」に関するツイートを収集する
収集方法は、基本的に前回とほぼ同じなので、分析に興味がある人は、クッパ姫に関するツイートを時系列で比較してみるまで飛んでください。
一応、前回から少し修正した箇所があるので、全体を再度解説したいと思います。
まずは、ライブラリの読み込みなど、ツイート収集の準備です。ここは前回と一緒です。
# Twitterデータ収集用のログインキーの情報
KEYS = { # 自分のアカウントで入手したキーを記載
'consumer_key':'*********************',
'consumer_secret':'*********************',
'access_token':'*********************',
'access_secret':'*********************',
}
# Twitterデータの収集(収集準備)
import json
from requests_oauthlib import OAuth1Session
twitter = OAuth1Session(KEYS['consumer_key'],KEYS['consumer_secret'],KEYS['access_token'],KEYS['access_secret'])
ツイート収集用の関数は、以下のように修正しました。
ツイート場所は今回使わないため、デフォルトの引数(None)を設定できるようにしました。
また、1回あたり最大100ツイートしか検索できないので、for文で繰り返しリクエストする必要があるのですが、Twitterデータ取得関数の外で管理した方がスマートだったので、そのように修正しました。
このあたりは、参考[2]の書き方を踏襲しました。
# Twitterデータ取得関数
def getTwitterData(key_word, latitude=None, longitude=None, radius=None, mid=-1):
url = "https://api.twitter.com/1.1/search/tweets.json"
params ={'q': key_word, 'count':'100', 'result_type':'recent'} #取得パラメータ
if latitude is not None: # latitudeのみで判定
params = {'geocode':'%s,%s,%skm' % (latitude, longitude, radius)}
params['max_id'] = mid # midよりも古いIDのツイートのみを取得する
req = twitter.get(url, params = params)
if req.status_code == 200: #正常通信出来た場合
tweets = json.loads(req.text)['statuses'] #レスポンスからツイート情報を取得
# 最も古いツイートを取るための工夫(※もっと良い書き方ありそう)
user_ids = []
for tweet in tweets:
user_ids.append(int(tweet['id']))
if len(user_ids) > 0:
min_user_id = min(user_ids)
else:
min_user_id = -1
# メタ情報
limit = req.headers['x-rate-limit-remaining'] if 'x-rate-limit-remaining' in req.headers else 0
reset = req.headers['x-rate-limit-reset'] if 'x-rate-limit-reset' in req.headers else 0
return {'tweets':tweets, 'min_user_id':min_user_id, 'limit':limit, 'reset':reset}
else: #正常通信出来なかった場合
print("Failed: %d" % req.status_code)
return {}
上記の関数を連続で実行するための制御関数(getTwitterDataRepeat)を作成しました。
15分間に450回(180回?)のリクエスト制限に引っかからないように、制限に引っかかりそうになったら自動で待機します。
# Twitterデータ連続取得
import datetime, time
def getTwitterDataRepeat(key_word, latitude=None, longitude=None, radius=None, mid=-1, repeat=10):
tweets = []
for i in range(repeat):
res = getTwitterData(key_word, latitude, longitude, radius, mid)
if 'tweets' not in res: #エラーとなった場合は離脱
break
else:
sub_tweets = res['tweets']
for tweet in sub_tweets:
tweets.append(tweet)
if int(res['limit']) == 0: # 回数制限に達した場合は休憩
# 待ち時間の計算. リミット+5秒後に再開する
now_unix_time = time.mktime(datetime.datetime.now().timetuple()) #現在時刻の取得
diff_sec = int(res['reset']) - now_unix_time
print ("sleep %d sec." % (diff_sec+5))
if diff_sec > 0:
time.sleep(diff_sec + 5)
mid = res['min_user_id'] - 1
print("ツイート取得数:%s" % len(tweets))
return tweets
このように実装することで、リクエストの上限を気にせず、ツイートの収集が自動で可能となります。
あとは、時間帯別でツイートを分けて収集したかったので、以下のようなスクリプトを回しました。
# クッパ姫に関するツイートを時間別に取得する
tweet_princess_koopas = {}
mid = -1
for t in range(24):
tweets = getTwitterDataRepeat("クッパ姫", mid=mid, repeat=10)
old_tweet = tweets[-1] # 収集した中で最も古いツイート
key = YmdHMS(old_tweet["created_at"]) # YmdHMS関数の詳細は前回記事
tweet_princess_koopas[key] = tweets # 最も古いツイートの時刻をキーとして保存する
mid = old_tweet["id"] - 10000000000000 # 約1時間遡って収集する
出力:
ツイート取得数:590
KEY : 2018/09/24 19:38:23
ツイート取得数:603
KEY : 2018/09/24 18:58:01
:
(中略)
:
ツイート取得数:300
KEY : 2018/09/24 04:25:53
ツイート取得数:307
KEY : 2018/09/24 03:42:16
ツイートを1時間ずつ遡りながら収集したかったので、最も古いツイートのmidから10,000,000,000,000を引いています。この10,000,000,000,000という値は、机上で概算で求めた値です。この値をそのまま信じるならば、1時間に10兆ツイート近くされていることになるので驚きです。ちなみに概算がズレて約40分間隔でのツイート取得となってしまったのですが、気にせず先に進みます。
「クッパ姫」に関するツイートを時系列で比較してみる
ここまでで、「クッパ姫」を含むツイートを時系列で収集することができました。
まずはデータを理解するために、単語の出現頻度を時系列で可視化したいと思います。可視化方法はWordCloudを用いました。実装は前回記事の通りです。
for k, v in sorted(tweet_princess_koopas.items()):
print("%s~ のツイート" % k)
freq_dict = CountWord(v)
DrawWordCloud(freq_dict, "%s~ のツイート" % k)
今回の話題の中心である「クッパ」「マリオ」「ピーチ」などのキャラクター名や、「RT」や「https:/t.co/xx(画像のリンクなど)」といったTwitter独自の表記が安定して上位に出現していることが分かります。
上位に出現する語の大部分は変わらないのですが、あるキャラクターの名前だけは、突如上位にランクインしました。**「キングテレサ」**です。
「キングテレサ姫」って?
キングテレサ姫とは、『ルイージマンション』シリーズの敵役キングテレサが「スーパークラウン」によって女体化(ピーチ姫化)した姿の二次創作のことで、クッパ姫の派生形です。(pixivより)
↓オリジナルかは分かりませんが、みずしー@oshiruko_mizseeさんの作品が有名です。
ツリ目、キバがある、舌を出している などの身体的特徴や、
幽霊、目が合うと動けなくなる などのキャラクター設定が女体化に合っていたため、爆発的に流行ったものと思われます。
「キングテレサ」に関するツイートの激増を、バースト検出してみる
今回収集したデータセットと、バースト検出と呼ばれる手法を用いて、「キングテレサ」という語を含むツイートの激増を検出してみたいと思います。
バースト検出という手法に関しては、書籍では、「ウェブデータの機械学習 (機械学習プロフェッショナルシリーズ)」に詳しくまとめられているようですが、ネットには解説記事が少ないです。
今回は、自然言語処理に関する研究室として著名な、東北大学 乾・鈴木研究室の解説記事を参考として、バースト検出手法の実装・適用に挑戦してみたいと思います。
今回は、Moving Average Convergence Divergence (MACD) という指標を用いて、バースト検出に取り組んでみました。
バースト検出手法としては、Kleinbergが2002年に発表した手法がベースラインとして、よく用いられるようですが、He and Parker が2010年に発表したMACDのほうがシンプル、かつ計算量が少ないようです。
↓MACDの解説については、乾・鈴木研究室のものが分かりやすいので、そのまま引用したいと思います。
【MACDの解説】
ある時刻におけるMACDは,
MACD = (時系列値の過去f期間の移動指数平均) - (時系列値の過去s期間の移動指数平均)
Signal = (MACD値の過去t期間の移動指数平均)
Histgram = MACD - Signal
ここで,f, s, tはパラメータ(f < s)で,これらをまとめてMACD(f, s, t)と書きます. 今回の実験では,He and Parker (2010) の実験でも用いられていた MACD(4, 8, 5) を採用しました. MACDをテクニカル指標として用いる時は,「Signal < MACD」の状態を上げトレンド,「MACD < Signal」の状態を下げトレンドとし,Histgramがトレンドの強さを表すと言われています. 今回は,15分間の期間をひとまとまりとして(15分足),その期間内にツイッター上に出現した単語の出現頻度を15で割った値,つまり出現速度[回/分]を観測値として,MACDによるトレンド分析を行いました. MACDの計算に必要な移動指数平均の値は逐次計算が可能で,今回のトレンド分析はストリーミング・アルゴリズムとして実装できるため,ビッグデータからのトレンド分析に適していると考えています.
上記の解説内容から、MACDを以下のように実装しました。(間違えてたら、すみません。。)
# Moving Average Convergence Divergence (MACD) の計算
class MACDData():
def __init__(self,f,s,t):
self.f = f
self.s = s
self.t = t
def calc_macd(self, freq_list):
n = len(freq_list)
self.macd_list = []
self.signal_list = []
self.histgram_list = []
for i in range(n):
if i < self.f:
self.macd_list.append(0)
self.signal_list.append(0)
self.histgram_list.append(0)
else :
macd = sum(freq_list[i-self.f+1:i+1])/len(freq_list[i-self.f+1:i+1]) - sum(freq_list[max(0,i-self.s):i+1])/len(freq_list[max(0,i-self.s):i+1])
self.macd_list.append(macd)
signal = sum(self.macd_list[max(0,i-self.t+1):i+1])/len(self.macd_list[max(0,i-self.t+1):i+1])
self.signal_list.append(signal)
histgram = macd - signal
self.histgram_list.append(histgram)
このプログラムを用いて、「ヨッシー」「クッパ」「キングテレサ」「マリオ」「ピーチ」「ルイージ」の6キャラクターについて、バースト検出してみました。
上記の関数へデータを代入するプログラム(折り込み)
# データ取得(べた書き)
yossi_list = []
teresa_list = []
koopa_list = []
mario_list = []
peach_list = []
ruiji_list = []
for k, v in sorted(tweet_princess_koopas.items()):
freq_dict = CountWord(v)
if "ヨッシー" in freq_dict:
yossi_list.append(freq_dict["ヨッシー"])
else:
yossi_list.append(0)
if "キングテレサ" in freq_dict:
teresa_list.append(freq_dict["キングテレサ"])
else:
teresa_list.append(0)
if "クッパ" in freq_dict:
koopa_list.append(freq_dict["クッパ"])
else:
koopa_list.append(0)
if "マリオ" in freq_dict:
mario_list.append(freq_dict["マリオ"])
else:
mario_list.append(0)
if "ピーチ" in freq_dict:
peach_list.append(freq_dict["ピーチ"])
else:
peach_list.append(0)
if "ルイージ" in freq_dict:
ruiji_list.append(freq_dict["ルイージ"])
else:
ruiji_list.append(0)
# 正規化
yossi_av_list = [elem/sum(yossi_list) for elem in yossi_list]
koopa_av_list = [elem/sum(koopa_list) for elem in koopa_list]
teresa_av_list = [elem/sum(teresa_list) for elem in teresa_list]
mario_av_list = [elem/sum(mario_list) for elem in mario_list]
peach_av_list = [elem/sum(peach_list) for elem in peach_list]
ruiji_av_list = [elem/sum(ruiji_list) for elem in ruiji_list]
# 計算(He and Parker(2010)と同じパラメータ)
f = 4
s = 8
t = 5
yossi_macd_data = MACDData(f,s,t)
yossi_macd_data.calc_macd(yossi_av_list)
koopa_macd_data = MACDData(f,s,t)
koopa_macd_data.calc_macd(koopa_av_list)
teresa_macd_data = MACDData(f,s,t)
teresa_macd_data.calc_macd(teresa_av_list)
mario_macd_data = MACDData(f,s,t)
mario_macd_data.calc_macd(mario_av_list)
peach_macd_data = MACDData(f,s,t)
peach_macd_data.calc_macd(peach_av_list)
ruiji_macd_data = MACDData(f,s,t)
ruiji_macd_data.calc_macd(ruiji_av_list)
可視化プログラム(折り込み)
# 結果の可視化
import numpy as np
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
from matplotlib.font_manager import FontProperties
fp = FontProperties(fname=r'C:\WINDOWS\Fonts\meiryo.ttc', size=10) #日本語対応
x = np.array(sorted(tweet_princess_koopas.keys()))
y1 = np.array(yossi_macd_data.histgram_list)
y2 = np.array(koopa_macd_data.histgram_list)
y3 = np.array(teresa_macd_data.histgram_list)
y4 = np.array(mario_macd_data.histgram_list)
y5 = np.array(peach_macd_data.histgram_list)
y6 = np.array(ruiji_macd_data.histgram_list)
plt.plot(x, y1, marker="o")
plt.plot(x, y2, marker="+", markersize=10, markeredgewidth=2)
plt.plot(x, y3, marker="s", linewidth=1)
plt.plot(x, y4, marker="o")
plt.plot(x, y5, marker="+", markersize=10, markeredgewidth=2)
plt.plot(x, y6, marker="s", linewidth=1)
plt.xticks(rotation=90)
plt.title("バースト検出結果", fontproperties=fp)
plt.xlabel("日時", fontproperties=fp)
plt.ylabel("バースト検出結果", fontproperties=fp)
plt.legend(["「ヨッシー」","「クッパ」", "「キングテレサ」","「マリオ」", "「ピーチ」","「ルイージ」"], loc="best", prop=fp)
可視化結果は以下の通りです。
何となくそれっぽい結果になりました。「キングテレサ」は、他のキャラクターと比較しても、バースト傾向が強い(瞬間的に激増している)ことが分かります。
16:00~18:00までが上昇トレンドで、18:15頃がピーク。それ以降は、下降トレンドであると読み取れます。
まとめと今後
今回は、「クッパ姫」をテーマにバースト検出に取り組んでみました。
前回は地域ごとでのツイート内容の違いを分析してみましたが、時系列でのツイート内容の推移の分析も結構使えそうな感触でした。
Twitterデータの他の活用方法についても、引き続き探っていこうと思います。
次回:
「指定アカウントのツイートをWordCloudで可視化する方法」
https://qiita.com/pocket_kyoto/items/e823f3319d41076f7899
参考
[1]
クッパ姫(pixiv)
https://dic.pixiv.net/a/%E3%82%AF%E3%83%83%E3%83%91%E5%A7%AB
[2]
スタバのTwitterデータをpythonで大量に取得し、データ分析を試みる その1
https://qiita.com/kenmatsu4/items/23768cbe32fe381d54a2
[3]
キングテレサ姫(pixiv)
https://dic.pixiv.net/a/%E3%82%AD%E3%83%B3%E3%82%B0%E3%83%86%E3%83%AC%E3%82%B5%E5%A7%AB
[4]
東北大学 乾・鈴木研究室 Project 311 / Trend Analysis
http://www.cl.ecei.tohoku.ac.jp/index.php?Project%20311%2FTrend%20Analysis
[5]
Dan He and D. Stott Parker(2010)
「Topic Dynamics: An Alternative Model of 'Bursts' in Streams of Topics」
https://dollar.biz.uiowa.edu/~street/HeParker10.pdf