Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

花火大会における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 その他,numpypandasなどのパッケージ,および後に特筆する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に変換しておくことをお勧めします.

www.statsbeginner.net

必要な処理は以下です

  • テキスト(文章)を形態素解析し,単語の集合を得る.
  • 単語ごとの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.49401309926840586

取り得る値は-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「DQN」で3色オセロ「トリコロール」の学習に挑戦

どーも! まじすけです🎉
今回は最近話題の強化学習、DQNに挑戦してみました。

以前「AINOW」というAIのキュレーションメディアにてDQNについての記事を書いたので、よろしければ見てみてくださいm(_ _)m
ainow.ai

今回はこのDQNを使って3色オセロのトリコロールを学習しました!

トリコロールとは?

f:id:majisuke:20170805195611p:plain
トリコロールは青・赤・白の3色で行うボードゲームです。

青と赤それぞれの裏面が白になっています。

f:id:majisuke:20170805195628p:plain
ひっくり返るルールはオセロと同じで、縦・横・斜めで自分の色のコマ同士で挟むことでひっくり返ります。

ただ記憶力が高くなければ白がひっくり返った時に何色になるかを覚えることができません。特に本プログラムでは初期配置の白コマの裏面はランダムになっています。なので、最初の時点ですでに運ゲー要素があります。

DQNの学習

今回はこちらのサイトを参考に実装しました。
機械学習の理論を理解せずに tensorflow で オセロ AI を作ってみた 〜導入編〜 - Qiita

まずはオセロとDQNのプログラムをこちらからダウンロードしてみてください。

$ git clone https://github.com/sasaco/tf-dqn-reversi

DQNはAI(エージェント)が状況を把握して行動します。行動に対する報酬"Q値"をエージェントに与えることで、より適した行動を取れるようになります。初めはランダムに行動した結果を保存しますが、勝った時に得られる報酬を参考にそれぞれの一手に対するQ値が定まります。

本プログラムではDQNのエージェント同士を戦わせて学習していきます。
まずは黒ターンと白ターンのAIを用意します

#train.py
#もろもろインポート
import copy
from Reversi import Reversi
from dqn_agent import DQNAgent

#オセロ開始の合図
if __name__ == "__main__":
    # 繰り返しの学習回数の設定
    n_epochs = 100
    # オセロの環境を構築
    env = Reversi()
    # playerID    
    playerID = [env.Black, env.White, env.Black]
    players = []
    # player[0]= 黒のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))
    # player[1]= 白のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))

ここから学習の記述になります。コマを置く、勝敗が決まる、というオセロの一連の流れをn_epochs回リピートして行動のモデルを保存します。
後にAIと戦う際に学習したAIが後攻になるので、後攻の勝敗の結果のみ保存します。

#train.py
    for e in range(n_epochs):
        env.reset() # 最初は初期化
        terminal = False
        while terminal == False: # 1エピソードが終わるまでループ
            for i in range(0, len(players)): 
                state = env.screen
                targets = env.get_enables(playerID[i])
                if len(targets) > 0: # どこかに置く場所がある場合
                    for tr in targets: #すべての手をトレーニングする
                        tmp = copy.deepcopy(env)
                        tmp.update(tr, playerID[i])
                        #終了判定
                        win = tmp.winner()
                        end = tmp.isEnd()
                        #次の状態
                        state_X = tmp.screen
                        target_X = tmp.get_enables(playerID[i+1])
                        if len(target_X) == 0:
                            target_X = tmp.get_enables(playerID[i])
                        # 両者トレーニング
                        for j in range(0, len(players)):
                            reword = 0
                            if end == True:
                                if win == playerID[j]:
                                    # 勝ったら報酬1を得る
                                    reword = 1
                            # 勝敗を保存する    
                            players[j].store_experience(state, targets, tr, reword, state_X, target_X, end)
                            players[j].experience_replay()

                    # 行動を選択  
                    action = players[i].select_action(state, targets, players[i].exploration)
                    # 行動を実行
                    env.update(action, playerID[i])
                    # ログの記述
                    loss = players[i].current_loss
                    Q_max, Q_action = players[i].select_enable_action(state, targets)
                    print("player:{:1d} | pos:{:2d} | LOSS: {:.4f} | Q_MAX: {:.4f}".format(
                             playerID[i], action, loss, Q_max))

                # 行動を実行した結果
                terminal = env.isEnd()     
        # 試合ごとの勝敗を記述               
        w = env.winner()                    
        print("EPOCH: {:03d}/{:03d} | WIN: player{:1d}".format(
                         e, n_epochs, w))
    # 後攻のplayer2のデータのみを保存する。
    players[1].save_model()

f:id:majisuke:20170806113353p:plain
実行するとこんな感じにどんどん学習していきます。(結構時間かかります)

DQNとの戦いの実装

次は実際に学習したAIと戦うプログラムです。コマンドライン上でコマンドを書きながらAIとオセロをします。argparseを使用することで、保存しているモデルをエージェントに追加します。

#FightWithAI.py
#もろもろインポート
import argparse
from Reversi import Reversi
from dqn_agent import DQNAgent

if __name__ == "__main__":
    # argparseでコマンドラインの引数を解釈
    parser = argparse.ArgumentParser()
    parser.add_argument("-m", "--model_path")
    parser.add_argument("-s", "--save", dest="save", action="store_true")
    parser.set_defaults(save=False)
    args = parser.parse_args()
    # オセロ環境を構築してmodelsのフォルダから学習したモデルをロードする
    env = Reversi()
    agent = DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols)
    agent.load_model(args.model_path)

これ以下のほとんどのコードがオセロの実行プログラムReversi.pyと記述が同じなので、AIの実装部分のみ紹介します。保存されている学習結果を参考に次の手を決めています。

#FightWithAI.py
 print("*** AIターン● ***")
        env.print_screen()
        enables = env.get_enables(2)
        if len(enables) > 0:
            # 学習結果の中で最も報酬が高い一手を選択
            qvalue, action_t = agent.select_enable_action(env.screen, enables) 
            print('>>>  {:}'.format(action_t))              
            env.update(action_t, 2)
        else:
            print("パス")

f:id:majisuke:20170806113536p:plain
実行すると、このようにAIと戦えます。(画像はトリコロールでの実行結果)

オセロをトリコロールに書き換え

最後にオセロのプログラムReversi.pyをトリコロールに書き換えます。

まずは初期のマスが8*8と大きく時間がかかってしまうので、5*5に変えます。
そしてトリコロール独特のコマである「黒の裏」と「白の裏」を作っておきます。

import os
import numpy as np
import random # 初期の白コマにランダムを付与するため

class Reversi:
    def __init__(self):
        # parameters
        self.name = os.path.splitext(os.path.basename(__file__))[0]
        self.Blank = 0
        self.Black = 1
        self.White = 2
        self.Blackrev = 3 # 黒の裏のコマを追加
        self.Whiterev = 4 # 白の裏のコマを追加
        # 学習時間を早めるために5*5の25マスでプレイ #
        self.screen_n_rows = 5 
        self.screen_n_cols = 5 
        self.enable_actions = np.arange(self.screen_n_rows*self.screen_n_cols)
        # variables
        self.reset()

初期のコマの配置の際、センターのコマの色をランダムで決めます。
20~25行目

    def reset(self):
        """ 盤面の初期化 """
        # reset ball position
        self.screen = np.zeros((self.screen_n_rows, self.screen_n_cols))
        self.set_cells(7, self.White)
        self.set_cells(11, self.Black)
        self.set_cells(17, self.Black)
        self.set_cells(13, self.White)
        self.set_cells(12, random.randint(3,4)) # 初期の白コマはランダムで決まる

黒と白以外のコマは「裏」と表記することにします。
40~50行目

    def print_screen(self):
        """ 盤面の出力 """
        i = 0
        for r in range(self.screen_n_rows):
            s1 = ''
            for c in range(self.screen_n_cols):
                s2 = ''
                if self.screen[r][c] == self.Blank:
                    s2 = '{0:2d}'.format(self.enable_actions[i])
                elif self.screen[r][c] == self.Black:
                    s2 = '●'
                elif self.screen[r][c] == self.White:
                    s2 = '○'
                else:
                    s2 = '裏' # 出力が白黒なので、ひっくり返った時は「裏」にする
                s1 = s1 + ' ' + s2
                i += 1
            print(s1)

マスの数を変えたので、コマをおける場所の定義も変更します。
60~80行目

    def put_piece(self, action, color, puton=True):
        
        """ ---------------------------------------------------------"""

        t, x, y, l = 0, action%5, action//5, [] # 盤面の変更にともないコマの動きを変更
        for di, fi in zip([-1, 0, 1], [x, 4, 4-x]):
            for dj, fj in zip([-5, 0, 5], [y, 4, 4-y]):

裏に返す処理の記述です。黒と白だけであれば塗り替えるだけで良いのですが、反転するという動作に変更します。
110~120行目

if puton:
            """ ひっくり返す石を場合分けする """
            for i in l:
                if self.get_cells(i) == 3:
                    self.set_cells(i, 1)
                elif self.get_cells(i) ==4:
                    self.set_cells(i, 2)
                elif self.get_cells(i) ==1:
                    self.set_cells(i, 3)
                elif self.get_cells(i) ==2:
                    self.set_cells(i, 4)

これでトリコロールに書き換え完了です。

実戦!!

それでは学習数100回のAIと戦ってみました!

f:id:majisuke:20170806113717p:plain

負けちゃいましたね...

もうちょっと自分の腕を磨いて再チャレンジします。

メンヘラ炸裂!? 西野カナの歌詞から感情の時系列データを抽出してみた

f:id:majisuke:20170805151412j:plain
どーも! まじすけです✨
今回は以下のリンクを参考に、pythonで曲の歌詞から曲中の感情の動きを可視化してみました。
www.statsbeginner.net

「会いたくて 会いたくて」を初め、女性に圧倒的人気を誇る西野カナさん。
よくメンヘラの代名詞とも言われる彼女の曲を可視化してみたら面白いんじゃないかな?ということでやってみました。

f:id:majisuke:20170809141353p:plain

ちなみに「会いたくて 会いたくて」はこんな感じに出ます。(浮き沈みが激しい...)

準備

用意するものはこちら!

  • 西野カナの曲の歌詞
  • python3系
  • MeCab
  • PN Table

日本語の形態素解析MeCabを使うので、使ったことがない人は以下のリンクを参考にしてください。
qiita.com

続いて日本語の感情のネガポジ判定をしてくれる辞書PN Tableをダウンロードしてください。
Macで使う方は辞書が文字化けしてしまうためUTF-8に変換しなければいけません。以下のリンクを参考にファイルをUTF-8形式に変換してください。
kawatama.net

実装

それでは実装に移ります✌︎('ω'✌︎ ) 今回は西野カナさんの「if」を使ってみました!

#まずはもろもろインポート
import re
import csv
import time
import pandas as pd
import matplotlib.pyplot as plt
import MeCab
import random
%matplotlib inline
# 歌詞の読み込み
title = 'nishino'
f = open(title+'.txt')
lyrics_df = f.read()  # ファイル終端まで全て読んだデータを返す
f.close()
# MeCabインスタンス作成
m = MeCab.Tagger('')

このとき歌詞のテキストファイルはプログラムと同じフォルダに入れてください。*歌詞のテキストファイルに関して後で読み取れない部分を省くので、改行などがあっても構いません。
MeCabが上手くいってるかチェックしてみましょう。

print(m.parse(lyrics_df))

f:id:majisuke:20170805144009p:plain

こんな感じに出ればひとまずokです!
続いてネガポジ判定の辞書を読み込みます。

# PN_TABLE辞書の読み込み
pn_df = pd.read_csv('pn_ja.dic',\
                    sep=':',
                    encoding='utf-8',
                    names=('Word','Reading','POS', 'PN')
                   )

次に2つの関数をつくります。
一つはMeCabが解析したあとにでてくる余分な2行を削除して、必要な情報だけを残します。
もう一つはMeCabから作った新しいデータにPN_TABLEの情報を付与します。さらにPN_TABLEに入っていない文字列を削除します。

# テキストを形態素解析して辞書のリストを返す関数
def get_diclist(lyrics):
    parsed = m.parse(lyrics)      # 形態素解析結果(改行を含む文字列として得られる)
    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)

# 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))

# 形態素解析結果の単語ごと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]) # pnの中身があれば追加
        else:
            pn = 'notfound' # 中身がなければnotfound
        word['PN'] = pn
        diclist_new.append(word)
    pn_list = []
    textlist = []
    for word in diclist_new:
        pn = word['PN']
        text = word['BaseForm']
        if pn != 'notfound': # notfoundじゃなかったら追加
            pn_list.append(pn)
            textlist.append(text)
    return(pn_list, textlist)

最後にそれぞれの関数を実行し、pandasのDataFrameを使って文字とそのPN値を対応させたデータを作ります。

dl_old = get_diclist(lyrics_df)
pn_list, text_list= add_pnvalue(dl_old)
    
# 本文、PN値を格納したデータフレームを作成
aura_df = pd.DataFrame({'text':text_list,
                        'PN':pn_list,
                       },
                       columns=['text', 'PN']
                      )

結果

ここでデータを可視化してみましょう。

plt.plot(pn_list)
plt.title(title)
plt.show

f:id:majisuke:20170805144858p:plain

こんな感じにプロットできました。かなりマイナスの要素が強い曲でしたね。
歌詞というのは曲の中で時間とともに流れていくので、"感情の時系列データ"を抽出したとも言えるかもしれませんね。

MySQLをjupyter notebookのkernelに入れる方法

f:id:matsume_goods:20170808213817p:plain

目次

  1. いきさつ
  2. 前準備
  3. 実践

方法だけ知りたければ3番までスキップしてください。

1.いきさつ: MySQL を jupter notebook で動かしたい

pythonで機械学習を勉強する際にデータベースの勉強が必要になりまして、sqlをjupyter notebook上で動かす必要が出てきました。
そこでjupyter notebookのkernelをいじっていたのですが、少し躓いてしまったので備忘録程度にjupyterへのMySQLの入れ方をまとめておきます。
なお、実行環境はubuntu16.04です。

注意
この記事はMySQLをjupyter notebookのkernelに入れることを目的としていますが、pythonでデータベースを扱う際にはMySQL-pythonをinstallして、
pythonのカーネルでMySQLdbをimport
したほうが実用的です。MySQL-pythonについては下記のサイトを参考にしてください。

MySQLを操作する | Make.

MySQL Python tutorial - programming MySQL in Python

2. 前準備: mysql-serverをインストール

そもそもmysqlサーバーのユーザー設定が無いと意味がないので、ターミナルで

sudo apt-get install mysql-server

を実行。当然、元から入っている人はスルーです。
また、この際にpasswordの設定画面が出るので、設定しましょう。
詳しくは下記の記事を参考に。

Ubuntu で MySQL - Qiita

3. 実践: MySQLをjupyter notebookのkernelに入れる

github.com

ここから実際にインストールしていきます。
いろいろ悩みまして、ipython用のパッケージでもjupyterとの互換性があるということなので上記のパッケージを使いました。
基本は上記の"README"を参考にしましたが、適宜"ipython"の部分を"jupyter"に読み替えていきます。

基本やることは、

  1. ipython_mysql_kernelのインストール
  2. Configのjsonファイルを作成
  3. kernelのjsonファイルを作成、jupyterのkernelにファイルを追加

この3フェーズに別れます。

1. ipython_mysql_kernelのインストール

ターミナルで

sudo pip install git+https://github.com/mmisono/ipython_mysql_kernel

を実行します。

2. Configのjsonファイルを作成

ここで少し躓きました。というのも、ここを飛ばしてしまってもjupyter notebookでmysqlが選択できるようにはなるのですが、mysqlサーバーへのログインがうまく行かないためdead kernelと表示されてしまったからです。

f:id:matsume_goods:20170808213240p:plain

画像1: ログインできず、死んだことにされるkernel。ターミナルの方のログを見るとログインでうまく行っていないことが確認できる。

ということで、先ほど作ったユーザー設定をまとめたConfigファイルを作ります。
ホームディレクトリに
.ipython
というフォルダがあるので、そのフォルダに
mysql_config.json
というファイルを作り、内容を

{
    "user"     : "root",
    "port"     : "3306",
    "host"     : "127.0.0.1",
    "charset"  : "utf8",
    "password" : "(設定したpassword)"
}

mysql_config.json

とします。
これでjupyterによるログイン時にこのユーザー設定が優先されるようになります。

3. kernelのjsonファイルを作成、jupyterのkernelにファイルを追加

今度は現在のディレクトリ(どこでも良い)に
mysql
というフォルダを作成し、そのフォルダに
kernel.json
を作り、内容を

{
    "argv": [
        "python", 
        "-m",
        "ipython_mysql_kernel",
        "-f",
        "{connection_file}"
    ],
    "display_name": "mysql",
    "language": "mysql"
}

kernel.json

とします。そうしたらターミナルからフォルダmysqlがあるディレクトリで

sudo jupyter kernelspec install mysql

を実行します。

usr/local/share/jupyter/kernels

mysqlがあればkernelの追加は完了です。

jupyter notebookのkrenelが動くか確認

f:id:matsume_goods:20170808213315p:plain

画像2: jupyter notebookのkernelにMySQLが入っているかを確認する。

うまく行っていれば上記のようにmysqlを選択できるので、適当にいじってみましょう。


以上になります。
今回の躓きはjupyterに執着しすぎていてipythonの互換性を見落としていたことを起因としたものでした。そのうちjupyterのSQLkernelを作ったほうが良いのだろうか・・・。

【音声認識 超入門】 固定長音声データの分類

概要

以前から音声認識には興味があったので, その第一歩として, Yes と No の固定長の音声を機械学習を用いて分類しました. (どっちも2秒)
今回作成したソースコードはgithub上げときました.

環境

macOS, python3 (anaconda)

必要なものをインストールする.

portaudioとpyaudioをインストールする.

brew install portaudio
pip install pyaudio

他のOSの場合など詳しくはここを見てね.

soundfileをインストールする.

pip install soundfile

データを作る

今回は自分の声を分類する分類器を作りたいから, 録音するプログラムから作りました.
録音するプログラムにimportしているrecorderのソースコードもgithubに上げたから気になったら見てください.
下のプログラムで連続して録音しました. (YesとNo, 20回ずつ)

import sys
import pyaudio
import wave

import recorder

cnt = 0
print('input prefix of output filename:')
fname = input()
print('input number to start with:')
cnt = int(input())
rec = recorder.WaveRecorder()
while True:
    print('Press enter to start recoding. Type end to finish recording.')
    if input() == 'end':
        break
    rec.record('{}_{}.wav'.format(fname, cnt))
cnt += 1

モデルの学習

分類器は, Random Forest, Gradient Boosting, Support Vector, KNNの4つを試しました. まず, それぞれのwaveデータから一次元データを作成し, くっつける.

n_datas = 20
X = []
y = []
for i in range(n_datas):
    no, _ = sf.read('no_{}.wav'.format(i))
    yes, _ = sf.read('yes_{}.wav'.format(i))
    no = no[:, 0] #2chで録音したので片方だけ取ってくる
    yes = yes[:, 0]
    X.append(no)
    X.append(yes)
    y.append(0) #Noはクラス0
    y.append(1) #Yesはクラス1

X = np.array(X)
y = np.array(y)

次に説明変数をFFTの絶対値と位相にする.

X_fft = np.array([np.fft.fft(x) for x in X])
X_fft = np.fft.fft(X)
X = np.array([np.hstack((x.real**2+x.imag**2, np.arctan2(x.real, x.imag))) for x in X_fft])

そしてcross_val_scoreで5分割して交差検証, 正答率を表示.

#ここをGradientBoostingClassifier(), SVC()やKNeighborsClassifier()に変える
clf = RandomForestClassifier()
scores = cross_val_score(clf, X, y, cv=5)
print('score:{:.3f} (+/-{:.3f})'.format(scores.mean(), scores.std()*2))

結果

Random Forest の結果は0.975 (+/-0.100), Gradient Boostingは1.000 (+/-0.000), SVCは0.700 (+/-0.339), KNNは0.675 (+/-0.436)でした. この結果から, Gradient Boostingが最も良いと考えられます.

データ数がそれぞれ20個ずつしか無いのに思ったより精度が高かったです.
この理由は,
・ 分類クラスが2クラスだけ
・ 雑音がほぼなかった ・ trainもtestも自分の声しか入っていないので周波数とか似てる
だと思います.

実行するとわかると思いますが, Gradient Boostingは3手法の中では1番学習に時間がかかっています. 学習時, 全ての弱分類器が独立でないBoostingという手法を使っているのが原因だと考えられます. ただ, 学習には時間がかかっても, 実際に使うときには短時間で計算できるため, これを使っても問題はないです.

遊び

実際に学習したモデルで, 新たに録音した音声を分類させてみる.

#全データで学習
clf.fit(X, y)

rec = recorder.WaveRecorder()
yesno = ['No', 'Yes']
while True:
    print('Press enter to start recording. Type end to finish recording.')
    if input() == 'end':
        break

    rec.record('output.wav')
    wav, _ = sf.read('output.wav')
    wav = np.array(wav[:, 0])
    wf = np.fft.fft(wav)
    wav = np.hstack((wf.real**2+wf.imag**2, np.arctan2(wf.real, wf.imag)))
    pred = clf.predict(np.array([wav]))
    print(yesno[int(pred)])

実行すると次のようになった.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.
end

Yes, Yes, No, No, Yes, Noの順に発声したので全て正しく分類されていました. この程度のデータ数で自分の声だけならこれだけちゃんとできるのであれば, 家電とか声で操作するの一から作ってもそこまで難しくないかも!?

【教師あり学習】怠惰で強力なアルゴリズム!?k-NN【分類】

k-NNとは?

突然ですが、皆さんは巡回セールスマン問題(TSP)は知っていますか。
最短経路問題の一つですが、その経路決定アルゴリズムの一つに最近傍法というのがあります。
これは現在いる地点から一番近い地点へと経路を決定するアルゴリズムですが、機械学習にも似たようなアルゴリズムが存在します。
それがk近傍法、k-NNと略されるアルゴリズムです。

教師あり学習

k-NNは教師あり学習のアルゴリズムの一つです。
さて、教師あり学習とはなんでしょうか。
教師あり学習は主に未知のデータや将来のデータを予測できるように既知のデータを用いて学習をする機械学習の種類の一つです。
既知のデータから学習するためそのデータを教師に見立てて教師データと呼ぶことにします。
教師ありデータでは予測したいデータを目標変数、その予測に使うデータを説明変数と言います。
目標変数と説明変数の間になにか関係があって、それを数式、あるいは関数として表せないかという問題を扱う分野です。 他にも教師なし学習や強化学習が機械学習の主な種類としてあげられます。

分類と回帰

教師あり学習には大きく分けて二つの分野が存在します。
分類回帰です。 違いは目標変数が離散値であるか連続値であるか、ということです。

目標変数が離散値をとる場合、教師あり学習で予測したいものは入力したデータがどの値に分類されるかということです。
よって目標変数が離散値である場合を分類(Classification)と言います。

目標変数が連続値をとる場合、目標変数と説明変数との間には数学的関係が存在するのではないか、私たちの多くはそう考えることでしょう。
回帰(Regression)はそうした数学的関数を識別関数として探索し、その識別関数からデータを予測しようとすることを指します。

“怠惰"学習

k-NNは怠惰学習(lazy learner)という学習アルゴリズムの代表例です。
怠惰というのは教師データから識別関数、つまり入力されるデータを直接分類する関数を学習しないということです。
ではどのように分類を行っているのかというと、入力されたデータと類似している上位k個の教師データを参照し、そのデータグループの中で一番多く分類されているクラスに分類するという方式を取っています。
機械学習のアルゴリズムですが教師データを丸暗記するような挙動をとる特徴的なアルゴリズムなんです。

Pythonによるk-NN分類器実装

k-NNを使った分類予測をPythonを使って実装してみます。
機械学習ではscikit-learnというPythonのモジュールが便利です。
まずはプログラムの全容から。

import requests
import io
import pandas as pd

from sklearn import preprocessing
from sklearn.model_selection import train_test_split

from sklearn.neighbors import  KNeighborsClassifier

import matplotlib.pyplot as plt

vote_data_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/voting-records/house-votes-84.data"
s = requests.get(vote_data_url).content
vote_data = pd.read_csv(io.StringIO(s.decode('utf-8')),header=None)
vote_data.columns = ['Class Name',
                     'handicapped-infants',
                     'water-project-cost-sharing',
                     'adoption-of-the-budget-resolution',
                     'physician-fee-freeze',
                     'el-salvador-aid',
                     'religious-groups-in-schools',
                     'anti-satellite-test-ban',
                     'aid-to-nicaraguan-contras',
                     'mx-missile',
                     'immigration',
                     'synfuels-corporation-cutback',
                     'education-spending',
                     'superfund-right-to-sue',
                     'crime',
                     'duty-free-exports',
                     'export-administration-act-south-africa']

label_encode = preprocessing.LabelEncoder()
vote_data_encode = vote_data.apply(lambda x: label_encode.fit_transform(x))
X = vote_data_encode.drop('Class Name', axis=1)
Y = vote_data_encode['Class Name']
X_train, X_test, y_train, y_test = train_test_split(X,Y, random_state=50)

training_accuracy = []
test_accuracy = []

neighbors_settings = [i for i in range(1, 30)]
for n_neighbors in neighbors_settings:
    clf = KNeighborsClassifier(n_neighbors = n_neighbors)
    clf.fit(X_train, y_train)
    training_accuracy.append(clf.score(X_train, y_train) * 100)
    test_accuracy.append(clf.score(X_test, y_test) * 100)

plt.plot(neighbors_settings, training_accuracy,label='教師用データの正解率')
plt.plot(neighbors_settings, test_accuracy,label='テスト用データの正解率')
plt.ylabel('正解率')
plt.xlabel('分類に使ったデータの数')
plt.legend()
plt.show()

モジュールの読み込み

import requests
import io
import pandas as pd

from sklearn import preprocessing
from sklearn.model_selection import train_test_split

from sklearn.neighbors import  KNeighborsClassifier

import matplotlib.pyplot as plt

今回のプログラムで使うデータはオンラインに公開されているデータを使うため取得するために標準モジュールのrequestsとioを使っています。
pandasは機械学習に使うデータとして扱いやすくするよう加工するために用います。
sklearnはscikit-learnのことで機械学習のためのモジュールです。
このモジュールから様々なアルゴリズムを読み込むことができますが今回はKNNのみです。
最後にグラフを表示させるためにmatplotlibを読み込んでいます。

データの読み込みと前処理

# dataの参照URL
vote_data_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/voting-records/house-votes-84.data"
# data取得
s = requests.get(vote_data_url).content
# dataを扱いやすいように加工
vote_data = pd.read_csv(io.StringIO(s.decode('utf-8')),header=None)
# dataには名前が付いてないので自分でつける
vote_data.columns = ['Class Name',
                     'handicapped-infants',
                     'water-project-cost-sharing',
                     'adoption-of-the-budget-resolution',
                     'physician-fee-freeze',
                     'el-salvador-aid',
                     'religious-groups-in-schools',
                     'anti-satellite-test-ban',
                     'aid-to-nicaraguan-contras',
                     'mx-missile',
                     'immigration',
                     'synfuels-corporation-cutback',
                     'education-spending',
                     'superfund-right-to-sue',
                     'crime',
                     'duty-free-exports',
                     'export-administration-act-south-africa']

# データの中身は文字列なので数字に変更
label_encode = preprocessing.LabelEncoder()
vote_data_encode = vote_data.apply(lambda x: label_encode.fit_transform(x))
# 説明変数
X = vote_data_encode.drop('Class Name', axis=1)
# 目標変数
Y = vote_data_encode['Class Name']
# 教師データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(X,Y, random_state=50)

今回のデータはUCI Machine Learning RepositoryにあるCongressional Voting Records datasetを用いました。
これはアメリカ合衆国下院議会において議員が共和党員か民主党員かという立場と議員が政策に対してどのように投票したかということをまとめたデータセットです。

詳しくは以下のリンクを参照してください。 UCI Machine Learning Repository: Congressional Voting Records Data Set

取得してきたデータセットはClass Nameと名付けた項は'democrat'(民主党員)、'republican'(共和党員)のどちらかが、それ以外の項は'y'(yes)、'n'(no)、そして'?‘のどれかが入っています。
このままだと使いづらいので('democrat’, ‘republican’) = (0, 1)、(‘?’, ‘n’, ‘y’) = (0, 1, 2)というように数字に変換します。

今回のデータでは分類したい目標はClass Name、そのために使うデータはそれ以外として分類を行うことにします。
つまり、分類を予測したいものは民主党員であるか、共和党員であるかということで、そのために使うデータは教育や保険などの13の政策に対して議員が賛成、反対のどちらに票を投じたのかという投票記録です。
本当はここでデータの選択をするべきですが、入門なのでそこは気にせずやっていきます。

scikit-learnのtrain_test_split関数を使って教師データとテストデータに分割します。
こうして得られたX_trainとy_trainを使って学習をします。

k-NNによる分類と正解率の表示

# 教師データに対する予測の正解率を格納
training_accuracy = []
# テストデータに対する予測の正解率を格納
test_accuracy = []

# kの値を格納(グラフのプロットに用いるためリストを用意)
neighbors_settings = [i for i in range(1, 30)]
for n_neighbors in neighbors_settings:
    # KNN分類器を作成(k=n_neighbors)
    clf = KNeighborsClassifier(n_neighbors = n_neighbors)
    # 教師データを用いて学習
    clf.fit(X_train, y_train)
    # k=n_neighborsのときのKNNの教師データに対する予測の正解率をリストに送る
    training_accuracy.append(clf.score(X_train, y_train) * 100)
    # k=n_neighborsのときのKNNのテストデータに対する予測の正解率をリストに送る
    test_accuracy.append(clf.score(X_test, y_test) * 100)

# 以下、グラフ表示のための処理
plt.plot(neighbors_settings, training_accuracy,label='教師用データの正解率')
plt.plot(neighbors_settings, test_accuracy,label='テスト用データの正解率')
plt.ylabel('正解率')
plt.xlabel('分類に使ったデータの数')
plt.legend()
plt.show()

k-NN分類器の実装そのものはとてもシンプルです。
clf = KNeighborsClassifier() たったこれだけです。

自分で実装する場合はそれなりに数学の知識が必要になりますが、とにかくこれで分類器が作成できました。
続いてfitメソッドを用いて分類器に学習させます。
これで分類器がどのようなデータをどう分類すればいいのか学習します。

あとはデータを使って予測をさせるだけです。
今回は正解率をグラフにプロットするためにscoreメソッドを用いていますが、分類の予測結果が欲しい場合はpridictメソッドを使います。

実行結果

f:id:trickortreatcat338:20170729181526p:plain

画像を見てみると正解率にブレはあるものの、おおよそkの値が3か4で一番正解率が高く、kの値が大きくなるに従って正解率が下がっているのがわかります。
k-NNにおいては、kの値が大きくなるとデータの類似度がそんなに高くないデータも類似データとして識別処理に使われることになります。
その結果として、分類クラスの境界線が曖昧になってしまうという問題が発生します。
kの値は参照するデータなどにより変えるのが一般的です。

また、正解率がおおよそ90%を超えているのも画像から読み取れますね。
かなり直感的なように思えるこのアルゴリズムですが、機械学習として強力なアルゴリズムであることが分かっていただけたでしょうか?

参考文献

今回記事を作成するにあたり、以下の本を参考にさせていただきました。
Amazon CAPTCHA

Twitterからデータセットを作るfor機械学習

前置き

さて、皆さんは機械学習で重要なものって何があると思いますか?

実行するコードの質や、その中で使うパッケージ、実行するマシンのスペックも大きく結果に影響してくるでしょう。

しかしそれらすべてを手に入れられる最高のものを用意したとしても、やりたいことに関するデータセットがなければ何もできないのです!

それでは機械学習で使うデータをどうやって手に入れればいいのでしょうか?

まず第一に様々な企業や研究者などがデータセットを公開してくれているものがあります。これらは機械学習のために作られたものなので、扱いやすく信頼性もある程度高いと思います。しかし公開されているデータの中に自分が使いたいものがあるとは限りません。もしほしいデータがなかったらどうすればいいのか。

f:id:sack_kazu:20170730215245p:plain

自分で作ればいいのです。

そもそも近年ディープラーニングが隆盛してきた理由はマシン性能の向上と、インターネットの普及による大量のデータ入手が可能になったからでした。

インターネット上には膨大な量のさまざまな情報があふれており、これを活用しない手はありません。

さて今回どんなデータセットを作るのかというと、電車の遅延と気象条件の組み合わせた物にしようと思います。
気象情報を説明変数にして、電車が遅延するかを目的変数にするイメージです。

気象情報のデータは気象庁から入手できるのですが、電車の遅延情報をまとめたデータは公開されていないようです。(企業主催のコンペなどで特別に公開されたりはしていたようですが。)

というわけで今回は鉄道会社の公式ツイッターからデータを作ることにします。


twitter.com

今回東急電鉄さんのツイートを利用させてもらいます。

環境について

使用言語
・python3系
使用パッケージ(インストールが必要なもの)
・pandas:DataFrameを使うため
・requests_oauthlib:ツイッターの認証のため

本題

気象データ

まず、気象庁から過去の気象情報をダウンロードします。
 
気象庁|過去の気象データ・ダウンロード

地点は東京、時別値で項目は降水量、現地気圧、相対湿度を選んでみました。

ここが厄介なのですが、ここで一度にダウンロードできるデータ量が制限されており、この三つの項目を選ぶと5か月分くらいしか取得できません。なので期間をずらして必要な分だけ取得しましょう。今回は2016/4/30~2017/7/28(3回分)取得してみました。


まずは気象データのほうを成型していきます。
どんなデータ形式になってるかとりあえず見てみましょう。

import pandas as pd
import codecs

with codecs.open("data0.csv", "r", "Shift-JIS", "ignore") as file:
df =pd.read_table(file, delimiter=",")

df

f:id:sack_kazu:20170730210211p:plain

列の名前がUnnamed:1…となっているのでこれを使って要らない列を削除していきます。降水に関しては量が0でも、現象無し情報が0の行があるので、現象無し情報の方を降水の情報として用います。湿度や気圧に関しては品質情報や均質番号はいらないので消します。

さらに列名がわかりづらいので上4行を消して新しい列名を付けます。

これらの作業を各ファイルから読み取ったDataFrameに行い、最後に一つのDataFrameにまとめます。

最後に日時のデータを参照しやすいように単純な数字に変換しておきます。

これで気象情報の方は完成です。

import codecs

def YmdHMS2(d_time):#日時を数字の列に直す関数
    n_time = time.strptime(d_time, '%Y/%m/%d %H:%M:%S')
    return int(time.strftime("%Y%m%d%H%M%S", n_time))

a_df = []
list = [1,3,4,6,7,9,10]#要らない列番号

for i in range(3):#三個ファイルを開き、それぞれの列を整理する
    with codecs.open("data{}.csv".format(i), "r", "Shift-JIS", "ignore") as file:
        b_df =pd.read_table(file, delimiter=",")
        
    b_df = b_df.drop([0,1,2,3])
    for i in list:#要らない列を削除
        b_df = b_df.drop(['Unnamed: {}'.format(i)],axis=1)
    b_df.columns = ['time','rain','humidity','puressur']
    b_df = b_df.reset_index(drop=True)

    a_df.append(b_df)

df = pd.concat([a_df[2],a_df[1],a_df[0]], ignore_index=True)#3つのDataFrameを結合
for i in df.index:
    df.time[i] = YmdHMS2(df.time[i])
df

f:id:sack_kazu:20170730210642p:plain 

遅延情報の取得

次にツイッターから遅延情報を取得していきましょう。
ツイッターからデータを取得する方法については以下のサイトを主に参考にしました。

TwitterAPI でツイートを大量に取得。サーバー側エラーも考慮(pythonで) | コード7区

指定の路線が遅延したかとその時間のデータを作ります。
アカウントのタイムラインを時系列順に取得し、特定の路線名と「遅れ」の文字が含まれていたツイートの時間に遅延があったと判断します。

# -*- coding: utf-8 -*-

from requests_oauthlib import OAuth1Session
import json
import datetime, time, sys, calendar
import pandas as pd

CK = '##############################'                             # Consumer Key 自分のものを入力
CS = '##################################################'         # Consumer Secret
AT = '##########################################################' # Access Token
AS = '##################################################'         # Accesss Token Secert

session = OAuth1Session(CK, CS, AT, AS)
max_id = 891290216417210368#取得を始めるツイートのid。ツイートへのリンクの末尾の長い数字
res_text = []

N = 40 # Nx100個のツイートを取得(50くらいからエラーになる?仕様は調べてません)
line = "東横線" # 調べる路線
 
url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'

def YmdHMS(created_at):#分以下の情報を切り捨てて数字の列に変換する関数
    time_utc = time.strptime(created_at, '%a %b %d %H:%M:%S +0000 %Y')
    unix_time = calendar.timegm(time_utc)
    time_local = time.localtime(unix_time)
    return int(time.strftime("%Y%m%d%H00", time_local))

for i in range(N):
    res = session.get(url, params = {'user_id':1199983754, 'count':100, 'max_id':max_id})
    res_text_semi = json.loads(res.text) 
  
    if res.status_code != 200:# ステータスコード確認
        print ("Twitter API Error: %d" % res.status_code)
        sys.exit(1)
    
    for tweet in res_text_semi:#今回取得した最古のツイートのid取得
        if tweet['id'] < max_id:
            max_id = tweet['id']

    res_text.extend(res_text_semi)


df1 = pd.DataFrame(columns=["time","delay"])

    
for tweet in reversed(res_text):
    if tweet['text'].find('遅れ') != -1 and tweet['text'].find(line) != -1:#遅れていたらdelayを1にする。
        delay = 1
    else:
        delay = 0
        
    t_df = pd.DataFrame([[YmdHMS(tweet['created_at']),delay]],columns=["time","delay"])
    df1 = df1.append(t_df,ignore_index = True)


df1 = df1.drop_duplicates(['time'])#時間が重複している行を削除
df1

f:id:sack_kazu:20170802004949p:plain
最後に気象データと遅延データを組み合わせます。
気象データは毎時の値がありますが、遅延データはそうではないので、気象データに遅延データをくっつけるようにします。
そしてその間の値は、一度遅延したら次に遅れのないツイートが来るまで遅延していたものとします。
そして最後にcsvファイルとして書き出します。

merged = pd.merge(df,df1,on='time',how='left')#時間をキーに気象データを残す形で結合
flag = False #遅延状態かのフラグ

for i in merged.index:
    if merged.delay[i] == 1:
        flag = True
    elif merged.delay[i] == 0:
        flag = False
    else:
        if flag == True:
            merged.delay[i] = 1
        else:
            merged.delay[i] = 0            

merged.to_csv('delay_data.csv')

f:id:sack_kazu:20170802004920p:plain
こうして無事電車の遅延情報と気象情報を合わせたデータセットができました。
次回はこのデータセットを使って実際に機械学習をやる予定です。