ナード戦隊データマン

データサイエンスを用いて悪と戦うぞ

ツイートからMBTIの性格タイプ「分析家」かどうかを予測する

16Personalitiesというサイトで性格診断をした人が診断結果をツイートすると、性格タイプと一緒に定型文をシェアします。この定型文とユーザのツイートを利用して、その人が分析家かどうかを予測します。

イデアのフロー

  1. 診断結果の定型文を収集する。
  2. 収集した情報からユーザ名とラベル(性格型)を抽出。
  3. 抽出したユーザ名のツイートを10件つづ取得し、トークン化して結合する。
  4. 分析家かどうかを2値変数(真偽値)に置き換える。
  5. 4を目的変数、3を説明変数としてtf-idf&ロジスティック回帰。
  6. 交差検証する。
  7. 係数を見て分析家のプラス要因とマイナス要因を調べる。
  8. 自分のツイートから予測してみる。

事前準備

  1. TwitterAPI登録をします。
  2. tweepy, json, anaconda, jupyter notebookをインストールします。
  3. data.jsonというファイルにTweet APIのキー情報を格納します。
  4. jupyter notebookを実行します。

Jupyter notebook上で実行する

In[1]:

from __future__ import absolute_import, print_function
import tweepy
import json
import sys
import getopt
import pandas as pd

with open('data.json') as data_file:
    data = json.load(data_file)


def twitter_auth(consumer_key, consumer_secret, access_token,
                 access_token_secret):
    auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
    auth.secure = True
    auth.set_access_token(access_token, access_token_secret)
    return auth


def load_api(consumer_key, consumer_secret, access_token, access_token_secret):
    return tweepy.API(
        twitter_auth(consumer_key, consumer_secret, access_token,
                     access_token_secret),
        parser=tweepy.parsers.JSONParser())

def output_simplify(api, query, count, page, lang):
    results = api.search(q=query, count=count, lang=lang)
    tweets = []
    d = ""
    for j in range(0, page):
        if len(results["statuses"]) == 0:
            return
        for i in range(0, len(results["statuses"])):
            d = results["statuses"][i]
            tweets.append({"user": d["user"]["screen_name"],
                           "text": d["text"]})
        results = api.search(q=query, count=count, max_id=d["id"] - 1, lang=lang)
    return tweets

api = load_api(data["consumer"]["key"], data["consumer"]["secret"],
                   data["token"]["key"], data["token"]["secret"])

In[2]:

personalities = 
pd.DataFrame(output_simplify(api, "\"あなたのタイプは何ですか?\"", 100, 3, "ja"))

In[3]:(実行に2時間以上かかります)

import re
import tinysegmenter
import time
segmenter = tinysegmenter.TinySegmenter()

def get_user_tweets(p_text, p_user):
    p_text = re.sub("\n", "", p_text)
    p_label = re.sub(r'.+“(.+)”.+', r'\1', p_text)
    user_tweets = output_simplify(api, "FROM:@{}".format(p_user), 10, 1, "ja")
    user_tweets = pd.DataFrame(user_tweets)
    tweet_tmp = ""
    for user_tweet in user_tweets["text"]:
        text_tmp = segmenter.tokenize(user_tweet)
        tweet_tmp = tweet_tmp + ' '.join(text_tmp) + ' '
    return (p_label, tweet_tmp)

p_labels = []
p_user_tweets = []
for p_text, p_user in zip(personalities["text"], personalities["user"]):
    label, tweet = get_user_tweets(p_text, p_user)
    p_labels.append(label)
    p_user_tweets.append(tweet)
    time.sleep(30)

data = pd.DataFrame()
data["user"] = personalities["user"]
data["label"] = p_labels
data["tweet"] = p_user_tweets

In[4]:

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np

analyst_labels = ["論理学者", "建築家", "指揮官", "討論者"]

data["analyst"] = data["label"].isin(analyst_labels)

pipe = Pipeline([('feature_extraction',TfidfVectorizer(min_df=7, max_df=9,norm=None)), ("clf",LogisticRegression())])
grid = GridSearchCV(pipe, {'clf__C':[0.001, 0.01, 0.1, 1]}, cv=5)
cross_val_score(grid, data["tweet"], data["analyst"], cv=3)

Out[4]:

array([ 0.80434783,  0.79120879,  0.81318681])

In[5]:

grid.fit(data["tweet"], data["analyst"])
vectorizer = grid.best_estimator_.named_steps["feature_extraction"]
feature_names = np.array(vectorizer.get_feature_names())
coefs = grid.best_estimator_.named_steps["clf"].coef_
coef_pd = pd.DataFrame()
coef_pd["coef"] = coefs.tolist()[0]
coef_pd["feature_name"] = feature_names
display(coef_pd.sort_values("coef",ascending=False))

Out[5]:

coef feature_name
137 0.012821    子供
152 0.008351    拡散
3   0.005750    wwww
18  0.005085    しかし
23  0.003869    そういう
99  0.003830    作り
103 0.003123    価値
141 0.002848    対象
167 0.002309    残念
174 0.002285    状況
65  0.001230    よろ
94  0.001159    上手
35  0.001064    とも
184 0.000549    終わっ
36  0.000536    どっち
70  0.000256    んじゃ
29  0.000238    たっ
183 0.000185    答え
121 0.000115    参考

In[6]:

display(coef_pd.sort_values("coef"))

Out[6]:

 coef    feature_name
138 -0.022697   学校
95  -0.022548   不安
189 -0.020205   英語
106 -0.018699   全員
117 -0.018082   効率
154 -0.017115   描い
8   -0.016969   かい
56  -0.016589   まっ
180 -0.016548   眠い
113 -0.016209   出演
61  -0.015980   みる
171 -0.015863   無料
91  -0.015840   リセット
0   -0.015790   kadokawa
155 -0.015530   擁護者
186 -0.015380   綺麗
123 -0.014755   可愛い
25  -0.014700   そも
160 -0.014634   昨日
2   -0.014632   www
196 -0.014453   話し
122 -0.014451   友人
83  -0.014280   ハロウィン
148 -0.014190   怖い
42  -0.014158   なと
187 -0.014127   練習
20  -0.013968   しっかり
107 -0.013890   公式
12  -0.013786   けも
16  -0.013742   ごい

In[7]:

dummy, my_tweet = get_user_tweets("", "shunsugiyama360")
grid.predict([my_tweet])

Out[7]:

array([False], dtype=bool)

考察

係数の出力が2つあり、前者は分析家としてプラスに働くツイート内の語、後者はマイナスに働く語です。

cross_val_scoreによる交差検証の結果はまずまずですが、この解析には2つの難点があります。サンプルが少ないということと、使っているトークナイザがあまり優秀ではないということです。

また、time.sleepを使っている部分がありますが、これはTwetter APIがRate limitという制限を設けていて、15分内に送れるリクエスト数に制限があるためです。このため、データ取得にはかなりの時間がかかります。

もう一つ気になるのが、wwwwがプラス要因なのに、wwwがマイナス要因であることです。これも、サンプルの少なさに起因した問題の可能性が高いです。

より多くのサンプルを収集したい場合、英語圏のツイートを収集する必要がありますが、文化的バイアスが存在しているため、日本人とは根本的にツイートと性格の対応が違っている可能性があります。

もちろん、実際に予測したい場合は、特定ユーザのツイートを同じ方法でトークン化してpredictを呼ぶだけです。

リンク

  1. 16Personalities - https://www.16personalities.com/ja
  2. Twitter API - https://developer.twitter.com/en/docs/tweets/search/overview