チャットログからその人のプロフィール情報を抽出することができるか

#目的
* 面白そうな[キャンペーン](https://zine.qiita.com/event/collaboration-cotoha-api/?utm_source=qiita&utm_medium=article_banner)があったので
* 人は意図しない間に、どの程度自分のプロフィールをさらけ出してしまっているのか気になったので


#環境とかデータとか
##分析用API
流石に自分で1から分析を行うには知見が足りないので、今回はNTTコミュニケーションズさんが公開している[COTOHA API](https://api.ce-cotoha.com/contents/index.html)を使用しました。

##元データ
今回はチャットログをベースにしようと考えています。
国内ではチャットと言えばLINEが主流ですが、なんとLINEにはチャットログエクスポート機能がついています。
今回はLINEでエクスポートしたチャットログを使って分析をしていきます。
念の為本人に事前承諾を頂いております。

#ファイルがでかすぎた
普段はあまりお互いのプロフィールには触れないような当たり障りのない世間話しかしておりませんが、そこそこ大きめのファイルでした。まずはこのファイルを整形するところから始めます。

実物ではありませんが、LINEのチャットログはこのような構成になっています。

```:元ファイルサンプル
2019/12/22 Sun
17:00　bowtin [Sticker]
17:01  hogehogekun [Sticker]
17:02  hogehogekun 今日暇だったらラーメン食べようよ

2019/12/23 Mon
05:00  bowtin ごめん寝てた
05:00  bowtin [Sticker]
08:35  hogehogekun 許さない
   ：
   ：
```

まず、以下の情報を排除しました。

* 投稿日、曜日、時刻情報
* 投稿者の名前
* 自分の投稿（相手の投稿のみが分析対象
* [Sticker][Photo]などのシステム関連の文字列

結果として、以下のようになりました。

```:整形後のファイル
今日暇だったらラーメン食べようよ
許さない
```

1チャット1行になっているので、目視で見ても比較的わかりやすいです。
整形後のファイルの行数は20500程度でした。

#ファイルを500行ずつぐらいに区切る
整形後ファイルの時点で20500行と、かなり大きめのチャットログでした。
このままAPIを叩くとエラーが帰ってきてしまったので、500行ずつぐらいのファイルに小分けにしました。
（glob使えばよかった...。）

```python:filesplitter.py

with open(file=r'\path\to\file\sample_chatlog.txt', mode='r', encoding='utf-8') as old_file:
    lines = old_file.readlines()

    for i in range(0, 21000, 500):
        line_count = 0 + i
        while line_count <= i + 500:
            with open(file=r'\path\to\file\splitted_file' + str(i) + '.txt', mode='a+', encoding='utf-8') as new_file:
                new_file.write(lines[line_count + i])
                line_count += 1
```

もうちょっとうまい書き方があるとは思いますが、とりあえずファイルを分割することが目的だったのでこれでヨシとします。


#COTOHA APIを使ったユーザ属性推定
COTOHA APIは色々なAPIを公開されていますが、今回はその中の[「ユーザ属性推定」](https://api.ce-cotoha.com/contents/reference/apireference.html#user)を利用しました。本APIはまだベータ版（2020/02/19現在）とのことです。

では、1つ目のファイルの中身をすべてAPIに渡してみます。

```:1つ目のファイルの推定結果
{"civilstatus": "未婚", "earnings": "1M-3M", "gender": "女性", "hobby": ["INTERNET", "MUSIC", "PAINT", "TRAVEL", "TVGAME"], "moving": ["BUS", "WALKING"], "occupation": "大学生"},
```

これはすごい。8割ぐらいは合ってます。

続けていきます。

```2つ目
{"civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["COOKING", "FORTUNE", "GOURMET", "INTERNET", "MUSIC", "TRAVEL"], "location": "関東", "moving": ["RAILWAY", "WALKING"]},
```
今度は少し違った結果が出てきました。
てか、"earnings":"-1M"ってなんだ？年収にマイナスとかあるのか！？
追記："-1M"なのではなく"0-1M"という解釈ではないかというコメントを頂きました。確かにそうかもしれません！「1M未満」または「1M以下」ってことですね。

あと、今回は地域の情報が入ってました。抽出できる情報は元データによって微妙に異なるようです。

というわけで、あとは40個ぐらいのファイルをひたすらAPIに渡していきました。
上記のようなレスポンスがひたすら帰ってくるので、帰ってきたものもすべて1つのファイルに溜め込むようにしてみました。

実際に使ったコードはこちらです。

```python:main.py
#APIへのリクエストに関する基本情報
BASE_URL = 'https://api.ce-cotoha.com/hogehoge/'
CLIENT_ID = 'YOUR ID'
CLIENT_SECRET = 'YOUR SECRET'
TOKEN_SERVER_URL = 'https://api.ce-cotoha.com/hogehoge/'


#APIアクセストークンの取得を行う関数（一定時間ごとにアクセストークンは無効化されるような仕様ですかね...違ってたらすみません）
def authorization():
    payload = {
        'grantType': 'client_credentials',
        'clientId': CLIENT_ID,
        'clientSecret': CLIENT_SECRET
    }
    headers = {
        'content-type': 'application/json'
    }
    response = requests.post(TOKEN_SERVER_URL, data=json.dumps(payload), headers=headers)
    auth_info = response.json()

    return auth_info['access_token']


#APIにリクエストを投げる関数（引数は文字列のリスト）
def make_request(original_string_list):
    headers = {
        'Content-Type': 'application/json',
        'charset': 'UTF-8',
        'Authorization': 'Bearer ' + authorization()
    }

    payload = {
        'document': original_string_list,
        'type': 'kuzure' #チャットログ等の崩れた文章の場合にはそれ用のモードがある模様です
    }

    response = requests.post(BASE_URL, data=json.dumps(payload), headers=headers)

    jsonified_response = response.json()
    return jsonified_response['result']


if __name__ == '__main__':
    #40個ぐらいある元ファイルのファイル名をリストにする（今回はファイル名＋番号という形で規則性をもたせています）
    file_list = ['splitted_file' + str(i) + '.txt' for i in range(0, 21000, 500)]

    #ファイル名の一覧からファイル名を1つ取得して内容を読み込む
    for a_file in file_list:
        lines = []
        with open(file=(r'path\to\file' + a_file), mode='r', encoding='utf-8') as file:
            lines = file.readlines()
            file.close()
        
        #読み込んだ内容をそのままCOTOHA APIに投げ、結果をファイルに保存する
        with open(file=r'path\to\file\result.txt', mode='a+', encoding='utf-8') as file:
            file.write(json.dumps(parse(lines)))
            file.close()
            sleep(1) #短時間にリクエストを投げすぎると迷惑が掛かりそうなので1秒待機
```


#結果
というわけで一部抜粋ですが、結果の一覧はこんな感じになりました。同じageでも多少のばらつきがみられますね。

```python:ユーザ属性抽出結果（一部抜粋）.py
[
{"age": "20-29歳", "civilstatus": "未婚", "gender": "女性", "hobby": ["COOKING", "FORTUNE", "INTERNET", "TVCOMMEDY"], "moving": ["OTHER", "WALKING"]},
{"age": "20-29歳", "civilstatus": "未婚", "gender": "女性", "hobby": ["GOURMET", "INTERNET", "SMARTPHONE_GAME", "PAINT", "TVGAME"], "moving": ["CYCLING", "OTHER", "RAILWAY", "WALKING"]},
{"civilstatus": "未婚", "earnings": "1M-3M", "gender": "女性", "hobby": ["COOKING", "GOURMET", "INTERNET", "SHOPPING", "TRAVEL"], "moving": ["CYCLING"]},
{"age": "30-39歳", "civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["ANIMAL", "CAMERA", "COLLECTION", "COOKING", "INTERNET", "SHOPPING"]},
{"age": "20-29歳", "civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["COOKING", "FORTUNE", "GOURMET", "PAINT"], "location": "東海", "moving": ["RAILWAY"]},
{"age": "20-29歳", "civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["ANIMAL", "DRAMA", "GYM", "SMARTPHONE_GAME", "MUSIC", "TVGAME"], "moving": ["RAILWAY", "WALKING"]}
]
```

このままだとわけわかめなので、集計してみたいと思います。
上記の結果をpythonのdictとしてハードコードしちゃいます。

```python:parse_result.py
results = [ 
  {"age": "20-29歳", "civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["ANIMAL", "DRAMA", "GYM", "SMARTPHONE_GAME", "MUSIC", "TVGAME"], "moving": ["RAILWAY", "WALKING"]},
  {"age": "30-39歳", "civilstatus": "未婚", "earnings": "-1M", "gender": "女性", "hobby": ["ANIMAL", "CAMERA", "COLLECTION", "COOKING", "INTERNET", "SHOPPING"]}
  #以下略
]


from collections import Counter
import itertools

#collectionsを使うと最頻値を取り出すことができます
print(Counter([data['age'] for data in dict_array if 'age' in data]).most_common()[0][0])
print(Counter([data['location'] for data in dict_array if 'location' in data]).most_common()[0][0])
print(Counter([data['gender'] for data in dict_array if 'gender' in data]).most_common()[0][0])
print(Counter([data['civilstatus'] for data in dict_array if 'civilstatus' in data]).most_common()[0][0])
print(Counter([data['earnings'] for data in dict_array if 'earnings' in data]).most_common()[0][0])

#リスト内にリストがある形式なので、とりあえずすべて平坦な一本のリストに放り込んだあと、最頻値を取り出しています
print(Counter(list(itertools.chain.from_iterable([data['hobby'] for data in dict_array if 'hobby' in data]))).most_common()[0][0])
```

最頻値のまとめはこんな感じになりました。

```:最頻値
20-29歳
関東
女性
未婚
1M-3M
INTERNET
```


#考察
かなり精度は高いと思います。
少なくともこの方とのチャットで「結婚しているかどうか」なんていう話題は出していないはずですし、当たり前ですが「あなたは女性ですか？」なんていう安直な問いもしていません。年収の話はちょっとしたかも。

ちなみに他の方とのチャットでも少し試してみましたが、概ねあたっておりました。

将来的にはマッチングアプリなどで、チャットログからその人の本当のプロフィールをある程度見破ることができるかもしれませんね！
偏差なども考慮すれば、普段キャラ使いを分けてる人は「使い分けている」という事実が判明しそうです。

結論として、意外と人はチャットで自分のプロフィールを垂れ流してしまっていることがわかりましたが、VTuberやいわゆるネカマの方等、キャラ作りを徹底されている方はわかりにくいと思います。