テストエンジニアが機械学習してみた備忘録

広島とジト目が好きなテストエンジニアが機械学習に手を出した備忘録。

Twitterのデータから読み解くJaSST'18 Tokyo

gratk.hatenablog.jp

前回の記事で触れたように、今回のJaSSTはTwitterで #jasst のハッシュタグを追いかけるだけのエア参加でした。 せっかくなのでツイートの内容を分析してみようと思います。

環境

データ収集

何はともあれデータ収集です。Twitter APIを下記の記事を参考に叩くようにします。

RTを除くフィルタをかけて検索すれば、ツイート一覧を取得できるので、そのデータを収集します。ついでなのでユーザID、ファボ数、RT数くらいは取っておくと使えるかもしれないので保存します。

以下のクラスを定義して

import json
import re
from datetime import timedelta

import pandas as pd
from requests_oauthlib import OAuth1Session

class Twitter():
    def __init__(self, c_key, c_secret, token, t_secret):
        self.session = OAuth1Session(c_key, c_secret, token, t_secret)
        self.search_metadata = None
        self.tweets = None

        # ツイート情報を格納するデータフレーム
        self.columns = ['date', 'user', 'retweet', 'favorite', 'text', 'tweet_id']
        self.df = pd.DataFrame(columns=self.columns)

    def execute_search_query(
        self, query, *, max_id=None, count=100,
        since=None, until=None
    ):
        url = 'https://api.twitter.com/1.1/search/tweets.json'

        # 検索用パラメータ
        params = {
            'q': query,
            'result_type': 'recent',
            'count': count
        }
        if max_id is not None:
            params['max_id'] = max_id 
        if until is not None:
            params['until'] = until 
        if since is not None:
            params['since'] = since

        # 検索結果を取得
        response = self.session.get(url, params=params)

        if response.status_code == 200:
            result = json.loads(response.text)
            self.search_metadata = result['search_metadata']
            self.tweets = result['statuses']
        else:
            print(f"Error: {response.status_code}")

    def get_tweets(self, query, since, until):
        max_id = None
        total_count = 0
        while True:
            print(f"{total_count} tweets")
            self.execute_search_query(
                query, max_id=max_id, since=since, until=until
            )
            if self.tweets:
                total_count += len(self.tweets)
            else:
                break

            # 必要な情報の抽出
            # ツイート時間はJSTに変換
            rows = []
            for tweet in self.tweets:
                date = pd.to_datetime(tweet['created_at'])
                date += timedelta(hours=9)
                user = tweet['user']['screen_name']
                retweet = tweet['retweet_count']
                favorite = tweet['favorite_count']
                tweet_id = tweet['id']
                text = tweet['text']

                row = [
                    date, user, retweet, favorite, text, tweet_id
                ]
                rows.append(row)
            self.df = self.df.append(pd.DataFrame(rows, columns=self.columns))
            self.df = self.df.reset_index(drop=True)
            
            # 次のクエリで指定するmax_idを特定
            if 'next_results' in self.search_metadata:
                next_results = self.search_metadata['next_results']
                max_id = re.sub(r'^\?max_id=', '', next_results)
                max_id = re.sub(r'\&.*', '', max_id)
            else:
                print("next_results is not exist")
                break

以下のように叩いて「jasst」を含む(大文字小文字問わず)ツイートを、3/7(1日目)の9:00から3/10の9:00までの3日分取得します。

consumer_key = {自分で取得した値}
consumer_secret = {自分で取得した値}
access_token = {自分で取得した値}
access_token_secret = {自分で取得した値}

client = Twitter(consumer_key, consumer_secret, access_token, access_token_secret)
since = '2018-03-07_09:00:00_JST'
until = '2018-03-10_09:00:00_JST'
query = 'jasst exclude:retweets'

client.get_tweets(query, since=since, until=until)

データを眺めてみる

1時間毎のツイート数をプロットしてみました。 実際にセッションが行われている期間はやはり活発的にツイートされていますが、 夜間はそんなに呟かれていないようです。

f:id:gratk:20180310210337p:plain

形態素解析する

ツイートの内容を分析するには、各ツイートから単語等を抽出する必要があります。 こういった処理を形態素解析と呼び、日本語の形態素解析MeCabというツールがメジャーなようです。 以下の記事を参考に環境を整えて形態素解析をかけてみます。

今更ながらPythonとMeCabで形態素解析してみた - イノベーション エンジニアブログ

import pandas as pd
import MeCab

# 収集したツイートの情報は、ツイート本文の改行を削除した上でpd.to_csv()でcsvにしておいた
df = pd.read_csv('tweet_info.csv', index_col=0)

# なぜかツイート本文が改行されている行があったので削除しておく
df = df.dropna()

# 形態素解析して出現した単語をカウントする
keywords = {}
mecab = MeCab.Tagger()

for i in df.index:
    try:
        words = mecab.parse(df.loc[i, 'text'])
    except:
        print(f"Index: {i}")
        print(df.loc[i, 'text'])
        raise
    for word_info in words.split('\n'):
        word_info = word_info.split('\t')
        if len(word_info) < 2:
            break
        else:
            word_type = word_info[1].split(',')[0]
            if word_type == '名詞':
                word = word_info[0]
                if word not in keywords.keys():
                    keywords[word] = 1
                else:
                    keywords[word] += 1

keywords = pd.DataFrame(
    [[word, count] for word, count in keywords.items()],
    columns=['word', 'count']
)

# 出現数の降順でソートして出力
keywords.sort_values('count', ascending=False)

結果は以下の通り、なかなか残念な感じです。

「#」はハッシュタグを多分に含むせいでしょうね。あとはURLの一部っぽいものも多いですね。「自動」と「化」が分割されているのはちょっと残念。上記の記事によると辞書登録ができるようなので、辞書登録+明らかにノイズっぽいものは対象外にします。

なかなかそれっぽくなってきました。前回触れたように、今回のJaSSTはGoggleのテスト自動化の話が1番インパクトあったようなので、「自動化」はもちろん「開発」「コード」といった比較的上流工程よりの単語も多く出ているようです。下流工程よりの話では「探索的テスト」が1番多いですね。確か探索的テストのセッションが2〜3個あった気がするのでその影響でしょうか。

私見ですが、自動化を突き詰めた先にある形の1つは今回のGoogleのような「マニュアルテストがない世界」ですが、「スクリプトテストがなくなり、探索的テストが残る世界」も1つの形かなと思ってます。スクリプトテストは単純作業になりがちなので、そこは自動化してしまって、人間は想像力を働かせる必要のある探索的テストに注力する世界ですね。企業のスタイルやプロダクトの性質によって様々な形があって然るべきと思いますが、「自動化+探索的テスト」は受けが広い世界でもあるかなと思います。

その他に気になったことは「Flaky」が思いの外少なかったことですかね。「Flaky」自体は去年のICSTにおけるMicco氏の講演でも出てきたようですが、日本のソフトウェアテスト業界で広まったのは今回のJaSSTかなと思うので、今後Flakyなテストに関する話題は増えるのかもしれませんが、今回のJaSST中にTwitterでバズったわけではなさそうです。

まとめ

LDAなどでトピック分析にもチャレンジしようかなと思いましたが、ツイート数も少ないし力尽きたので今回はこの辺で。