RedandWhiteDays

赤、白、ときどき黒猫

DAY6 : TwitterのStreaming APIを使ってリプライに自動返信するBotをPython3で作成する

ポアソン分布を用いたサッカーの得点予想モデルにより、試合結果の予測を行えるようになった。この予想結果に簡単にアクセスできるように、TwitterのStreaming APIを利用した自動返信機能を本ブログのツイッターアカウントに搭載することにした。


したがって今回は、Python3でTwitterのStreaming APIを利用して、自分宛てのリプライに返信するbotを実装するまでの過程を記す。


python3でtwitter APIを利用するにはいくつかライブラリがあるようだが、
Python3でのtwitter投稿など - Qiitaの記事を参考に、sixohsix氏が開発したライブラリを活用することにした。このライブラリのインストールや簡単な使い方は上の記事で簡単に説明されている。また実際にTwitter Developersでアプリ登録して、Access Tokenなどを取得してそれをプログラムに組み込むまでのステップは、[Python] Python3でTwitterの検索APIを利用する - YoheiM .NETなどのサイトに載っているコードを参考にすれば分かりやすいだろう。今回は自動返信botということでREST APIではなくStreaming APIを使うことになる。この使い方などはPythonでTwitterのStreamingを取得する - kasajeiのブログに詳しい。


さて、今回のリプライ自動返信botには以下のような機能をつけることを考えた。
まず第一にリプライで指定された試合の予想スコアを返信する機能だ。試合の指定方法はプレミアリーグ公式推奨の #SUNNEW というハッシュタグ形式を採用することにした。
もうひとつ今節と次節でどんな試合があるのか、ハッシュタグの一覧を取得する機能。これは'last'で最新10試合の試合結果とそのハッシュタグを、'next'で次の10試合の試合結果とそのハッシュタグを取得できるようにした。


これらの機能を実装するためにそれぞれの試合の情報が一行ずつ記載されているcsvファイルを作成し、そこのデータをもとにbotが自動返信する仕組みをとった。
その作成のために必要になるのが今シーズンの試合一覧だ。これが纏められているcsvファイルなどを探したがなかなか見つけられなかったので、公式の2015/16 Fixture List Released | Barclays Premier Leagueから捨てpythonスクリプトにてcsvファイルを自前で用意した。


上の手順により作成されたcsvファイル(E0fix.csv)に、England Football Results Betting Odds | Premiership Results & Betting Oddsからダウンロードできるプレミアリーグの試合結果のcsvファイル(E0.csv)を参考にして、PredictionやResultを書き込むスクリプトを書いた。試合があるたびにE0.csvは更新されていくので、最新のE0.csvを参考にE0fix.csvファイルを更新していくことになる。内部で得点予想モデルの計算もしているので分かりにくいとは思うが、一応コードを載せておく。

import pandas as pd
import numpy as np
import math


def get_abb(team):
    abb={'Bournemouth':'BOU','Aston Villa':'AVL','Arsenal':'ARS','West Ham':'WHU','Chelsea':'CHE','Swansea':'SWA','Everton':'EVE','Watford':'WAT','Leicester':'LEI','Sunderland':'SUN','Man United':'MUN','Tottenham':'TOT','Newcastle':'NEW','Southampton':'SOU','Norwich':'NOR','Crystal Palace':'CRY','Stoke':'STK','Liverpool':'LIV','West Brom':'WBA','Man City':'MCI'}
    return abb[team]

def predict_goal(lam):
    g1=math.floor(lam)
    g2=math.ceil(lam)
    p1=mypoisson(lam,g1)
    p2=mypoisson(lam,g2)
    if p1>p2:
        return g1,p1
    else:
        return g2,p2

def mypoisson(lam,x):
    return math.exp(-lam)*math.pow(lam,x)/math.factorial(x)


#open csv
fix=pd.read_csv('csvdata/E0fix.csv',index_col=0)#fix for fixture
res=pd.read_csv('csvdata/E0.csv')#res for result

#update result
num=res.shape[0]
for i in range(num):
    HomeTeam,AwayTeam,FTHG,FTAG,HTHG,HTAG=res.ix[i,['HomeTeam','AwayTeam','FTHG','FTAG','HTHG','HTAG']]
    HomeTeam=get_abb(HomeTeam)
    AwayTeam=get_abb(AwayTeam)
    index=HomeTeam+AwayTeam
    Result=HomeTeam+' '+str(FTHG)+' - '+str(FTAG)+' ('+str(HTHG)+' - '+str(HTAG)+') '+AwayTeam
    SResult=str(FTHG)+' - '+str(FTAG)
    fix.ix[index,'Result']=Result
    fix.ix[index,'SResult']=SResult

teams=list(res["HomeTeam"].head(10))+list(res["AwayTeam"].head(10))
df=pd.DataFrame(index=teams,columns=['Hs','Hg','As','Ag'])

#calc Hs,Hg,Ag,As
for team in teams:
    home=(res[res.HomeTeam==team]).ix[:,['FTHG','FTAG']]
    away=(res[res.AwayTeam==team]).ix[:,['FTHG','FTAG']]
    homemean=home.mean()
    awaymean=away.mean()
    homemean.index = ['Hs', 'Hg']
    awaymean.index=['Ag','As']
    df.ix[team,:]=pd.concat([homemean, awaymean])
df=df.sort_index()

#calc mean
mean=res.ix[:,['FTHG','FTAG']].mean()

#calc HA,HD,AA,AD
for i in range(df.shape[0]):
    df.ix[i,'Hs']=df.ix[i,'Hs']/mean['FTHG']
    df.ix[i,'Hg']=df.ix[i,'Hg']/mean['FTAG']
    df.ix[i,'As']=df.ix[i,'As']/mean['FTAG']
    df.ix[i,'Ag']=df.ix[i,'Ag']/mean['FTHG']
df.columns=['HA','HD','AA','AD']


#update Prediction,SPrediction
for i in range(380):
    if isinstance(fix.ix[i,'Result'],str)==False:#if 'Result'==nan, update Prediction
        HomeTeam=fix.ix[i,'HomeTeam']
        AwayTeam=fix.ix[i,'AwayTeam']
        
        Hlam=mean['FTHG']*df.ix[HomeTeam,'HA']*df.ix[AwayTeam,'AD']
        Alam=mean['FTAG']*df.ix[HomeTeam,'HD']*df.ix[AwayTeam,'AA']
        preFTHG=predict_goal(Hlam)
        preFTAG=predict_goal(Alam)
        Prediction=get_abb(HomeTeam)+' '+str(preFTHG[0])+' - '+str(preFTAG[0])+' ({:.2%}) '.format(preFTHG[1]*preFTAG[1])+get_abb(AwayTeam)
        SPrediction=str(preFTHG[0])+' - '+str(preFTAG[0])
        fix.ix[i,'Prediction']=Prediction
        fix.ix[i,'SPrediction']=SPrediction

fix.to_csv('csvdata/E0fix.csv')


まずResultを更新してから、Resultの記入されていない行のPredictionを更新する、という形にすることで、もう結果が出ている試合のPredictionは更新されず、試合当時の予測をそのまま残せるようにしてあるのが工夫点だ。

更新されたE0fix.csvはこんな感じ。

f:id:redandwhite:20160127134148p:plain


このE0fix.csvを読み取ってリプライに返信していくことになる。
リプライのコードは以下のようになった。

import json
import pandas as pd
from twitter import *

with open("secret.json") as f:
    secretjson=json.load(f)

fix=pd.read_csv('E0fix.csv',index_col=0)
for comminggame in range(380):
        if isinstance(fix.ix[comminggame,'Result'],str)==False:
                    break

replylast='\n'
replynext='\n'

for i in range(comminggame-10,comminggame):
    replylast=replylast+fix.ix[i,'SResult']+' #'+fix.index[i]+'\n'
for i in range(comminggame,comminggame+10):
    replynext=replynext+fix.ix[i,'SPrediction']+' #'+fix.index[i]+'\n'

print(replylast)
print(replynext)
print(len(replylast),len(replynext))

auth=OAuth(secretjson["access_token"],secretjson["access_token_secret"],secretjson["consumer_key"],secretjson["consumer_secret"])
tweet=Twitter(auth=auth)
stream=TwitterStream(auth=auth,domain="userstream.twitter.com")

for msg in stream.user():
    if 'in_reply_to_screen_name' in msg and 'text' in msg:
        if msg['in_reply_to_screen_name']=='1879redandwhite':
            
            hashtag=''
            if len(msg['entities']['hashtags'])!=0:
                hashtag=msg['entities']['hashtags'][0]['text']
            text=(msg['text'].split(' '))[1]

            if hashtag in fix.index:
                Date=fix.ix[hashtag,'Date']
                HomeTeam=fix.ix[hashtag,'HomeTeam']
                AwayTeam=fix.ix[hashtag,'AwayTeam']
                Prediction=fix.ix[hashtag,'Prediction']
                Result=fix.ix[hashtag,'Result']

                reply='\n'+HomeTeam+' vs '+AwayTeam+'\n'+'Date: '+Date+'\n'+'Prediction: '+str(Prediction)+'\n'+'Result: '+str(Result)+'\n'

            elif text=='last':
                reply=replylast
            
            elif text=='next':
                reply=replynext

            else:
                reply='could not find any match.'

            status='@'+msg['user']['screen_name']+' '+reply
            tweet.statuses.update(status=status,in_reply_to_status_id=msg['id'])
            print(status)


リプライの文言は最後のif節によって定義されており、最優先で認識するのが一番目のハッシュタグで、そのハッシュタグが特定の試合を指定していた場合、その試合の詳細なデータが返ります。ハッシュタグが認識されなかった場合で、リプライの一番目の単語が'last'か'next'の場合、最新10試合の試合結果、もしくは次10試合の予測が返ります。
その他はelse節で処理されます。

気をつけて欲しいのがリプライを送る際の区切り文字が半角スペースでないといけない、という点。
なるべく走らせるようにはしているものの、現状このプログラムは私の気が向いたときにしか動いていない。次回はこのbotをherokuで安定して運用できるようにしたい。