ツイートの性格分析による本のマッチングシステム

# はじめに
こんにちは。もっちゃそと申します。情報系大学の学部4年生です。
8月末から[Weekend Engineer(WE)](#weekend-engineerとは)に参加し、アイデア出し＆開発の経験を積んでいます。
その活動の中で、予め分析しておいた本の性格情報とツイートの性格情報を比較することで、
オススメの本をサジェストするシステムを作りました。

# デモ動画
まず、自分のツイートで分析してみます。
・・・not foundでした。本の数や性格のパターンが少ないためよく出てきます...。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Qiita記事素材1 <a href="https://t.co/gK4UJh7HOj">pic.twitter.com/gK4UJh7HOj</a></p>&mdash; mocchaso (@ks_mocchaso) <a href="https://twitter.com/ks_mocchaso/status/1051046696950456320?ref_src=twsrc%5Etfw">2018年10月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<br/>

もう一度ダメ元でやってみたら、性格パターンが見つかりました。
同じユーザで結果が変わってしまっています。
同じ性格情報を持つ本が複数あって別の性格パターンが出力に選ばれるのなら
納得なんですが、「not found → 一致」は謎です...。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Qiita記事素材2 <a href="https://t.co/3ZI8bBOQEi">pic.twitter.com/3ZI8bBOQEi</a></p>&mdash; mocchaso (@ks_mocchaso) <a href="https://twitter.com/ks_mocchaso/status/1051046754559225856?ref_src=twsrc%5Etfw">2018年10月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<br/>

検索対象を渡辺さんに変えてみると、一致する性格パターンが見つかりました。
何回か試すと出力は変わりましたが、これはパターン候補が複数のケースでした。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Qiita記事素材3 <a href="https://t.co/s5xjE9nejs">pic.twitter.com/s5xjE9nejs</a></p>&mdash; mocchaso (@ks_mocchaso) <a href="https://twitter.com/ks_mocchaso/status/1051046851292450816?ref_src=twsrc%5Etfw">2018年10月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# きっかけ
[当コミュニティ](#weekend-engineerとは)では以下のステップに従って経験を積んでいきます。

1. アイデア出し：渡辺さん、開発：メンバー(@Mocchaso)
2. アイデア出し・開発：メンバー + 渡辺さんのヘルプ
3. アイデア出し・開発：メンバー + 基本ヘルプ無し

ステップ1では、渡辺さんが持っているアイデアから1つ選択して開発する流れとなっています。
Pythonをもっと鍛えたいのと、APIの使い方を勉強したかったので、このシステムを選択しました。

# 仕組み

### システム図
<img width="545" alt="本のサジェスト_システム図.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/259787/1cfcbde2-fdec-4830-f833-2c8caca5828a.jpeg">

### 本のデータをDL
青空文庫から、坊ちゃん、走れメロス、注文の多い料理店、人間失格、怪人二十面相をDLしました。
今回は手動で取得しましたが、自動化するプログラムを書けるともっと良かったかなと思います。

### 性格分析
IBM Watsonの一種であるPersonality Insightsを用いています。
[Big Five Model](https://console.bluemix.net/docs/services/personality-insights/models.html#models)という性格の特性論に従って性格分析をしてくれるAPIです。
以下の処理により、分析結果をJSON形式で取得することができます。

```python
def get_personality(text):
    api_version = ""
    api_username = ""
    api_password = ""
    api_url = "https://gateway.watsonplatform.net/personality-insights/api"
    
    personality_insights = PersonalityInsightsV3(
        version = api_version,
        username = api_username,
        password = api_password,
        url = api_url
    )
    
    profile = personality_insights.profile(
        content = text,
        content_type = "text/plain",
        accept = "application/json",
        content_language = "ja",
        accept_language = "en",
        raw_scores = True,
    )
    
    return profile
```

### ツイートを取得
Twitter APIを用いて取得した後、正規表現で不要な情報を取り除いています。

```python
from requests_oauthlib import OAuth1Session
from twitter import Twitter, OAuth

#APIキーの設置
CONSUMER_KEY =  ''
CONSUMER_SECRET = ''
ACCESS_TOKEN = ''
ACCESS_SECRET = ''

t = Twitter(auth=OAuth(
    ACCESS_TOKEN,
    ACCESS_SECRET,
    CONSUMER_KEY,
    CONSUMER_SECRET
))
    
twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET)
url = "https://api.twitter.com/1.1/search/tweets.json"
userTweets = []

def get_userstweets_again(screen_name, max_id):
    max_id = max_id
    count = 200 #一度のアクセスで何件取ってくるか
    aTimeLine = t.statuses.user_timeline(screen_name = screen_name, count=count, max_id=max_id)
    for tweet in aTimeLine:
        userTweets.append(tweet['text'])

def get_userstweets(screen_name):
    number_of_tweets = 0
    count = 200 #一度のアクセスで何件取ってくるか
    aTimeLine = t.statuses.user_timeline(screen_name = screen_name, count=count)
    for tweet in aTimeLine:
        number_of_tweets += 1
        userTweets.append(tweet['text'])
        if number_of_tweets >= 200:
            max_id = tweet["id"]
            print(max_id)
            get_userstweets_again(screen_name, max_id)
    return userTweets
            
            
def get_shaped_tweets(tweets_list):
    shaped_tweets = []
    rm_replie = re.compile(r'@([A-Za-z0-9_]+)')
    rm_url = re.compile(r'https?://t.co/([A-Za-z0-9_]+)')
    rm_hashtag = re.compile(r'#(\w+)')
    for tweet in tweets_list:
        shape = rm_replie.sub('', tweet)
        shape = rm_url.sub('', shape)
        shape = rm_hashtag.sub('', shape)
        shape = shape.replace('&gt;', '>').replace('&lt;', '<').replace('&amp;', '&').replace('\n', ' ')
        shaped_tweets.append(shape)
    return shaped_tweets

```

### 性格パターンの抽出
ユーザのツイートを性格分析して得られた5つの要素の内、上から2番目まで高い要素を抽出して性格パターンとします。候補(同じ性格パターンを持つ本)が複数存在する場合は、ランダムで選択します。
例：`{"Openness": 0.82, "Conscientiousness": 0.63, "Extraversion": 0.81, "Agreeableness": 0.67, "Emotional range": 0.74}` -> 性格パターンは`{"Openness", "Extraversion"}`

```python
def main(my_user_name):
    tweets = get_userstweets(my_user_name)
    tweets = get_shaped_tweets(tweets)

    ## ユーザのツイートを性格APIで分析する

    tweets_joined = " ".join(tweets) # 集めた複数のツイートを結合
    user_personality_dict = OrderedDict()

    tweet_personality_dict = get_personality(tweets_joined) # ツイートを結合したデータをAPIに入力
    # ツイートから分析した性格の名前
    personality_name = [big5["name"] for big5 in tweet_personality_dict["personality"]]
    # 性格の値。小数点第2位で四捨五入した後、decimal.Decimalからfloatに変換している。
    # 参考サイト(四捨五入)：https://note.nkmk.me/python-round-decimal-quantize/
    personality_percentile = [float(Decimal(str(big5["percentile"])).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)) \
                            for big5 in tweet_personality_dict["personality"]]

    # ユーザのツイートの分析結果
    all_tweets_big5 = OrderedDict(zip(personality_name, personality_percentile))

    # { my_user_id: { personality_name1: percentile1, personality2: percentile2, ... } }
    user_personality_dict[my_user_name] = all_tweets_big5
    # print("finished analyzing all({}) tweets.".format(len(tweets)))

    # Agreeableness: 協調性, Conscientiousness: 真面目さ, Emotional range: 精神的安定性, Extraversion: 外向性, Openness: 開放性
    # 参考サイト：https://note.nkmk.me/python-math-factorial-permutations-combinations/
    big5_list = ["Agreeableness", "Conscientiousness", "Emotional range", "Extraversion", "Openness"]

    # big5から2つの性格を選ぶ組み合わせを列挙する
    category_patterns = [set(pat) for pat in itertools.combinations(big5_list, 2)]

    # ユーザのツイートのカテゴリ分けを記録する辞書
    user_tweet_category_table = OrderedDict()

    # ユーザのツイートの分析結果を、値の大きい順にソートする
    sorted_result = sort_personality(user_personality_dict[my_user_name])

    # big5のうち大きい順から2番目までの値の性格名のみを取得し、その本のカテゴリとする(例：{Openness, Extraversion})
    # この時、複数の性格の順番を考慮させないために、setで用意する
    # 2つを組み合わせているため、1つだけの場合よりも幅広くカテゴリ分けできそう
    tweet_category = {big5_elm[0] for big5_elm in sorted_result[0:2]}
    user_tweet_category_table[my_user_name] = tweet_category


    user_personality = user_tweet_category_table[my_user_name]
```

### 一致する性格パターンの本をサジェスト
ユーザのツイートを分析した結果を、事前に分析しておいたの本の性格情報と比較し、
性格パターンが一致する本を出力結果とします。

```python
def main():
    ### 上記の続き

    suggest_book = ""
    img_path = "./static/images/nothing.jpg" # templates/index.htmlを基準にしたパス
    if user_personality == {'Openness', 'Extraversion'}:
        candidates = ['chumonno_oi_ryoriten', 'hashire_merosu']
        suggest_book = random.choice(candidates)
        img_path = "./static/images/" + suggest_book + ".jpg"
    elif user_personality == {'Openness', 'Emotional range'}: 
        suggest_book = 'bocchan'
        img_path = "./static/images/" + suggest_book + ".jpg"
    elif user_personality == {'Openness', 'Agreeableness'}:
        candidates = ['kaijin_nijumenso', 'ningen_shikkaku']
        suggest_book = random.choice(candidates)
        img_path = "./static/images/" + suggest_book + ".jpg"
    else:
        suggest_book = "not found a suggested book..."

    return suggest_book, img_path
```

上記のmain関数から返ってきた値をFlask側で取得し、ブラウザ上に表示します。
Flaskを用いて、簡易的なWebアプリケーションとして実装しています。
この時、それぞれの本に対応した画像も併せて出力されます。

```python
@app.route('/result', methods=['POST'])
def add_user():
    title = "分析結果"
    my_user_name = request.form['my_user_name']
    if not my_user_name:
        return redirect('/')
    result, result_img = analyze_books.main(my_user_name)
    return render_template('index.html', title=title, result=result, result_img=result_img, my_user_name=my_user_name)
```

# 最後に
### 感想
JSONファイルやAPIの操作が初めてだったため苦戦しましたが、非常に勉強になりました。
課題としては、出力する本の候補が見つからない(not found)点です。
本の種類を増やして性格パターンを全て網羅させれば防げるはず・・・。

また、後半の実装の時間をとれず、ツイート取得やFlaskの実装を渡辺さんに任せてしまいました。
次回はステップ2なので、特に時間配分に気を付けたいと思います。

※コード全体はこちらをご参照ください
→ [ツイートの性格分析による本のマッチングシステム](https://github.com/seven0525/match_books_by_tweets)

(初めての記事投稿意外と楽しかった)

### Weekend Engineerとは
渡辺大智さん (@ahpjop) 率いる開発コミュニティ。
「どんなアイデアもカタチにする」ことに焦点を置いて活動しています。
<b>[ホームページはこちら。](https://yutonakano.github.io/)</b>このホームページも、WEのメンバーが制作しました。