バズったツイートへのリアクションを感情分析してみる【Google Natural Language API / Python】

## 概要
バズったツイートへのリアクション（リプライ、引用RT）を、GoogleのNatural Language APIで感情分析してみました。

前日にバズったツイートに対して↑を実行した結果を返すbotも作ったので、よければのぞいてみてください。

https://twitter.com/echo_sentiment

実装方法を簡単に書くと、

Twitter APIで指定したRT数以上のツイートを抽出
↓
抽出したツイートに対するリプライと引用RTを取得し整形
↓
Google Natural Language APIへ投げてスコアを測定
↓
（スコアを元にツイート内容を出し分け）
↓
（AWS Lambda + CloudWatch Eventsで定期実行）

となっています（カッコ内はbotを実装する上で必要となる手順です）。

それでは、以下で具体的な実装を紹介します。
なお、プログラムはPythonで書いています。

## 実装
### ツイート取得まわり
[Tweepy](https://www.tweepy.org/)を使ってツイートの取得を行います。
以下の操作では、取得するツイートにRT（引用RTを除く）が含まれないよう`exclude:retweets`オプションをつけて行います。

まず、指定したRT数以上のツイートを抽出します。
次にそのツイートへのリプライを取得するのですが、特定のツイートへのリプライのみを取得するエンドポイントは残念ながらありません。
そのため、
1. バズったツイートをしたユーザへのリプライ全体を、`@screen_name`をクエリとして取得
2. バズったツイートに対してのリプライか否かを、`in_reply_to_status_id`を用いて判定
の手順で実現しました。

そして引用RTの取得です。
引用RTの実体はツイートのURLの埋め込みであるため、バズったツイートのツイートIDをクエリとして絞り込むことができます。

```python
auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)

api = tweepy.API(auth)

tweet_id_str = []
screen_name = []
reply_texts_rows = []

today = datetime.today() + timedelta(hours=9)
yesterday = today - timedelta(days=1)

# 前日のバズったツイートを取得
for status in api.search(q='min_retweets:2000 exclude:retweets until:' + datetime.strftime(yesterday, '%Y-%m-%d'), lang='ja', count=30):
    tweet_id_str.append(status._json['id_str'])
    screen_name.append(status._json['user']['screen_name'])

    query_reply = '@' + status._json['user']['screen_name'] + ' exclude:retweets'

    row = []
    # リプライ取得
    for status_reply in api.search(q=query_reply, lang='ja', count=100):
        if status_reply._json['in_reply_to_status_id'] == status._json['id']:
            row.append(format_text(status_reply._json['text']))
        else:
            continue

    query_quote = status._json['id_str'] + ' exclude:retweets'

    # 引用RT取得
    for status_quote in api.search(q=query_quote, lang='ja', count=100):
        if status_quote._json['id_str'] == status._json['id_str']:
            continue
        else:
            row.append(format_text(status_quote._json['text']))

    reply_texts_rows.append(row)
```

取得したツイートには、検索クエリで用いた`@screen_name`や引用元のURLが混ざってしまっているため、正規表現を用いてこれらを取り除きます。

```python
def format_text(text):
    text=re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-…]+', "", text)
    text=re.sub('\n', " ", text)
    text=re.sub(r'@?[!-~]+', "", text)
    return text
```
最後に整形したツイートをひとつのリストにまとめれば、感情判定をするためのデータの準備は完了です。

### 感情分析
準備したデータをGoogle Natural Language APIに投げ、感情分析を行います。
リストのデータをループを回して1つずつ投げるとAPIの無料枠を割とすぐに超えてしまうため、あらかじめ結合しておきます。

データをAPIに投げて判定を返すanalyze_sentiment関数は、ほぼ[サンプルプログラム](https://cloud.google.com/natural-language/docs/analyzing-sentiment?hl=ja)と同じコードです。


```python
count = 0
for reply_texts_row in reply_texts_rows:
    reply_texts = " ".join(reply_texts_row)
    result = analyze_sentiment(reply_texts)

    # 以下botでのツイート用
    description = judge_description(result)
    message = datetime.strftime(yesterday, '%Y-%m-%d') + '(' + str(count + 1) + ')\nリアクション分析結果：' + description + 'https://twitter.com/' + screen_name[count] + '/status/' + tweet_id_str[count]
    api.update_status(message)
    count += 1
```

```python
def analyze_sentiment(content):
    client = language_v1.LanguageServiceClient()

    if isinstance(content, six.binary_type):
        content = content.decode('utf-8')

    type_ = enums.Document.Type.PLAIN_TEXT
    document = {'type': type_, 'content': content}

    response = client.analyze_sentiment(document)
    sentiment = response.document_sentiment
    return sentiment.score, sentiment.magnitude
```
感情分析のスコアを得るための実装はここまでです。

最後に、スコアを元にbotでツイートする内容を決めるための関数に渡してあげます。
以下の判定は私の所感によるところが大きいので、文面から「スコアはこんなもんだったんだな」と読み取る程度にしていただければ幸いです。
スコアについての詳細は、[公式のドキュメント](https://cloud.google.com/natural-language/docs/basics?hl=ja#interpreting_sentiment_analysis_values)をご覧ください。

```python
def judge_description(result):
    if -0.1 < result[0] < 0.1:
        if result[1] < 20:
            message = "ニュートラル😐"
        elif 20 <= result[1] < 40:
            message = "ポジティブ・ネガティブがやや交錯🔥"
        elif 40 <= result[1] < 60:
            message = "🔥ポジティブ・ネガティブが交錯🔥"
        elif 60 <= result[1] < 80:
            message = "🔥🔥ポジティブ・ネガティブが多く交錯🔥🔥"
        elif 80 <= result[1]:
            message = "🔥🔥🔥ポジティブ・ネガティブが非常に多く交錯🔥🔥🔥"

        return message
        
    else:
        if result[0] >= 0.1:
            sentiment = "ポジティブ"
            flag = 0
        elif result[0] <= -0.1:
            sentiment = "ネガティブ"
            flag = 1
        
        if 0.1 <= abs(result[0]) < 0.3:
            prefix = "やや"
            suffix = "🙂" if flag == 0 else "🙁"
        elif 0.3 <= abs(result[0]) < 0.5:
            prefix = ""
            suffix = "😊" if flag == 0 else "😖"
        elif 0.5 <= abs(result[0]) < 0.7:
            prefix = "とても"
            suffix = "🥰" if flag == 0 else "😰"
        elif 0.7 <= abs(result[0]):
            prefix = "非常に"
            suffix = "🎉" if flag == 0 else "😱"

        return prefix + sentiment + suffix
```

賛否両論があるツイート（特に政治系）に対してのリアクションを分析すると、スコアはニュートラルに近いものの感情強度(magnitude)の値が大きくでる傾向があったため、あえて判定を加えてみました。

プログラムをAWS Lambdaにデプロイし、CloudWatch Eventsで定期実行してあげればbotの完成です。
（Lambdaにデプロイする際、Google Natural Language APIを動かすための環境変数の設定が必要なので、サービス アカウントキーをデプロイファイルに含め、コンソールで環境変数として指定してあげます。）
[Cloud9でのデプロイ](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloud9-deploy-lambda-external-libraries/)がお手軽だったので、これくらいの簡単なプログラムをLambdaに乗せてみたい！という場合におすすめです。

プログラムの全体はgithubに置いています。
https://github.com/matsuri0828/tweet_emotion

以上、お読みいただきありがとうございました。