【自然言語処理】Slackコミュニティにおける各メンバーの発言を可視化してみた

## 本記事について

この記事では、Slackコミュニティで各メンバーの発言内容をWordcloudで可視化する手法について紹介します。

ソースコードは[こちら](https://github.com/sota0121/slack-msg-analysis)にあります :octocat: 

あわせて読みたい：[【自然言語処理】Slackコミュニティで今週盛り上がった話題を可視化してみた](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4)

## 目次

1. 使い方と出力例
2. Slackからメッセージを取得
3. 前処理：テーブル作成/クリーニング/形態素解析/正規化/ストップワード除去
4. 前処理：重要語句抽出(tf-idf)
5. Wordcloudで可視化処理
6. おまけ

<font color=red>※前処理については、今後別記事にまとめたいと思います</font>

## 1. 使い方と出力例

### 1.1. 使い方

詳細は、[README](https://github.com/sota0121/slack-msg-analysis/blob/master/README.md)のGetting started を参照ください。
流れは、こんな感じです。

1. `docker-compose up -d` で仮想環境構築
2. `docker exec -it ds-py3 bash` でシェルに入る
3. `run_wordcloud_by_user.sh` を実行

### 1.2. 出力例

実際に出力した例です。それぞれ異なるメンバーの発言をWordcloudにしています。

![anim_.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181972/d2f5acc6-847d-1624-da35-12fc20bf38fb.gif)


## 2. Slackからメッセージを取得

こちらの記事を参照してください。

[【自然言語処理】Slackコミュニティで今週盛り上がった話題を可視化してみた - 2. Slackからメッセージを取得](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#2-slack%E3%81%8B%E3%82%89%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E5%8F%96%E5%BE%97)


## 3. 前処理：テーブル作成/クリーニング/形態素解析/正規化/ストップワード除去

[こちらの記事](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4)と同一の内容ですので、省略します。
詳細は、リンク先をご参照ください。

- [前処理：メッセージマートテーブル作成](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#3-%E5%89%8D%E5%87%A6%E7%90%86%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%83%9E%E3%83%BC%E3%83%88%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E4%BD%9C%E6%88%90)
- [前処理：クリーニング](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#4-%E5%89%8D%E5%87%A6%E7%90%86%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0)
- [前処理：形態素解析(Janome)](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#5-%E5%89%8D%E5%87%A6%E7%90%86%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90janome)
- [前処理：正規化](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#6-%E5%89%8D%E5%87%A6%E7%90%86%E6%AD%A3%E8%A6%8F%E5%8C%96)
- [前処理：ストップワード除去](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#7-%E5%89%8D%E5%87%A6%E7%90%86%E3%82%B9%E3%83%88%E3%83%83%E3%83%97%E3%83%AF%E3%83%BC%E3%83%89%E9%99%A4%E5%8E%BB)


## 4. 前処理：重要語句抽出(tf-idf)

### 4.1. tf-idfとは？

tf-idfとは、ある文書に存在する各単語について、
「その文書のコンテクストを理解する上で重要か？」という観点でスコアリングする為の指標だと言えます。

詳細は、[こちらの記事](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#81-tf-idf%E3%81%A8%E3%81%AF)をご参照ください。

### 4.2. tf-idfによる単語のスコアリング処理実装

#### 4.2.1. 何を文書・全文書とするか？

今回の目的は、あるメンバーの発言の特徴を見ることです。
ゆえに、 **Slackコミュニティ内の全投稿に対して、一人のメンバーがどのような特徴をもっているか** が分かるようにすべきと考えました。

したがって、

- **全文書** ：これまでの全チャンネル・全ユーザーの全ての投稿
- **1文書** ：あるメンバーの全ての投稿

としてtf-idfを計算しました。

#### 4.2.2. 実装

簡単に処理の流れを書きます。

1. メッセージを発言したメンバーごとにグルーピング
2. 1グループのメッセージ群を1文書として、tf-idfを計算
3. tf-idfのスコアが閾値以上の単語を抽出する（辞書として出力）

```python:important_word_extraction.py
import pandas as pd
import json
from datetime import datetime, date, timedelta, timezone
from pathlib import Path
from sklearn.feature_extraction.text import TfidfVectorizer
JST = timezone(timedelta(hours=+9), 'JST')

# メッセージをユーザーごとにグルーピング
def group_msgs_by_user(df_msgs: pd.DataFrame) -> dict:
    ser_uid = df_msgs.uid
    ser_wktmsg = df_msgs.wakati_msg
    # 重複なしのuid一覧を取得
    ser_uid_unique = df_msgs.drop_duplicates(subset='uid').uid
    # 重複なしuidごとにグルーピング
    dict_msgs_by_user = {}
    for uid in ser_uid_unique:
        # 当該uidに該当する全wktmsgを取得
        extracted = df_msgs.query('uid == @uid')
        # key, value を出力用の辞書に追加
        dict_msgs_by_user[uid] = ' '.join(extracted.wakati_msg.dropna().values.tolist())        
    return dict_msgs_by_user

# tf-idfスコアを参照しながら重要単語を抽出し辞書として返す
def extract_important_word_by_key(feature_names: list, bow_df: pd.DataFrame, uids: list) -> dict:
    # > 行ごとにみていき、重要単語を抽出する(tfidf上位X個の単語)
    dict_important_words_by_user = {}
    for uid, (i, scores) in zip(uids, bow_df.iterrows()):
        # 当該ユーザーの単語・tfidfスコアのテーブルを作る
        words_score_tbl = pd.DataFrame()
        words_score_tbl['scores'] = scores
        words_score_tbl['words'] = feature_names
        # tfidfスコアで降順ソートする
        words_score_tbl = words_score_tbl.sort_values('scores', ascending=False)
        words_score_tbl = words_score_tbl.reset_index()
        # extract : tf-idf score > 0.001
        important_words = words_score_tbl.query('scores > 0.001')
        # 当該ユーザの辞書作成 'uid0': {'w0': 0.9, 'w1': 0.87}
        d = {}
        for i, row in important_words.iterrows():
            d[row.words] = row.scores
        # 当該ユーザの辞書にワードが少なくとも一つ以上ある場合のみテーブルに追加
        if len(d.keys()) > 0:
            dict_important_words_by_user[uid] = d
    return dict_important_words_by_user

# ユーザー単位で重要単語を抽出する
def extraction_by_user(input_root: str, output_root: str) -> dict:
    # ---------------------------------------------
    # 1. load messages (processed)
    # ---------------------------------------------
    msg_fpath = input_root + '/' + 'messages_cleaned_wakati_norm_rmsw.csv'
    print('load: {0}'.format(msg_fpath))
    df_msgs = pd.read_csv(msg_fpath)
    # ---------------------------------------------
    # 2. group messages by user
    # ---------------------------------------------
    print('group messages by user and save it.')
    msgs_grouped_by_user = group_msgs_by_user(df_msgs)
    msg_grouped_fpath = input_root + '/' + 'messages_grouped_by_user.json'
    with open(msg_grouped_fpath, 'w', encoding='utf-8') as f:
        json.dump(msgs_grouped_by_user, f, ensure_ascii=False, indent=4)
    # ---------------------------------------------
    # 4. 全文書を対象にtf-idf計算
    # ---------------------------------------------
    print('tfidf vectorizing ...')
    # > 全文書にある単語がカラムで、文書数（=user）が行となる行列が作られる。各要素にはtf-idf値がある
    tfidf_vectorizer = TfidfVectorizer(token_pattern=u'(?u)\\b\\w+\\b')

    bow_vec = tfidf_vectorizer.fit_transform(msgs_grouped_by_user.values())
    bow_array = bow_vec.toarray()
    bow_df = pd.DataFrame(bow_array,
                        index=msgs_grouped_by_user.keys(),
                        columns=tfidf_vectorizer.get_feature_names())
    # ---------------------------------------------
    # 5. tf-idfに基づいて重要単語を抽出する
    # ---------------------------------------------
    print('extract important words ...')
    d_word_score_by_uid = extract_important_word_by_key(tfidf_vectorizer.get_feature_names(), bow_df, msgs_grouped_by_user.keys())
    # ---------------------------------------------
    # 6. uid => uname 変換
    # ---------------------------------------------
    print('ユーザーごとの重要単語群のキーをuidからunameに変換 ...')
    user_tbl = pd.read_csv('../../data/020_intermediate/users.csv')
    d_word_score_by_uname = {}
    for uid, val in d_word_score_by_uid.items():
        # 発言者のuidでunameを検索（アクティブユーザーでない場合存在しない可能性あり）
        target = user_tbl.query('uid == @uid')
        if target.shape[0] != 0:
            uname = target.iloc[0]['uname']
        else:
            continue
        print('uname: ', uname, 'type of uname: ', type(uname))
        d_word_score_by_uname[uname] = val
    return d_word_score_by_uname
```

#### 4.2.3. 出力した辞書

次章で説明するWordcloudでは、`{ "word": score }` という辞書を入力することで、スコアに応じて単語の表示サイズを変化させるWordcloudを生成することが出来ます。

[【自然言語処理】Slackコミュニティで今週盛り上がった話題を可視化してみた](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4)

という記事では、「期間」でグルーピングした場合のWordcloudを出力しました。

本記事では、「メンバー」でグルーピングした訳ですが、 **出力する辞書は同じ形式** にしています。

そうすることで、 **「tf-idfのスコアリング処理」以外は全て同じ処理** で実現することができます。 [DRY](https://qiita.com/yatmsu/items/b4a84c4ae78fd67a364c) に行きたいですよね。

今回実際に出力した辞書はこちらです。
（ユーザー名は伏せています）

```json:important_word_tfidf_by_user.json
{
    "USER_001": {
        "参加": 0.1608918987478819,
        "環境": 0.15024077008089046,
        "良品": 0.1347222699467748,
        "node": 0.1347222699467748,
        "説明": 0.13378417526975775,
        "サイバーセキュリティ": 0.12422689899152742,
        "r": 0.12354794954617476,
        "選択": 0.11973696610170319,
        "置換": 0.11678031479185731,
        "最終": 0.11632792524420342,
        "講座": 0.11467215023122095,
        "公開": 0.11324407267324783,
        "解析": 0.11324407267324783,
        "期限": 0.11100429535028021,
        "書き方": 0.10628494383660991,
        "ディープラーニング": 0.10229478898619786,
        :
    },
    "USER_002": {
        "データ": 0.170245452132736,
        "参加": 0.15825283334154341,
        "講座": 0.13785592895847276,
        "お願い": 0.1265412327351908,
        "募集": 0.12204781908784276,
        "記事": 0.1197561921672133,
        "環境": 0.11083230914864184,
        "食品": 0.1091835225326696,
        "共有": 0.10371152197590257,
        "コロナ": 0.10081254351124691,
        "輪読": 0.10025885742434383,
        "企画": 0.09899869065055528,
        "開発": 0.09571338092513401,
        "目標": 0.09253887576557392,
        "仕事": 0.09094257214685446,
        "プロジェクト": 0.08910924912513929,
        "情報": 0.08772258523428605,
        "言語": 0.08636683271048684,
        "channel": 0.08295159680178281,
        "リリース": 0.0818876418995022,
        "youtube": 0.07956948308804826,
        "チーム": 0.07956948308804826,
        "基本": 0.07444492553072463,
        :
    },
    :
}
```


## 5. Wordcloudで可視化処理

こちらの記事をご参照ください。

[【自然言語処理】Slackコミュニティで今週盛り上がった話題を可視化してみた - 9. Wordcloudで可視化処理](https://qiita.com/masso/items/41630aa02f1fd6cfa0a4#9-wordcloud%E3%81%A7%E5%8F%AF%E8%A6%96%E5%8C%96%E5%87%A6%E7%90%86)


## 6. おまけ

### 特に参考にした記事

- [自然言語処理における前処理の種類とその威力 | Qiita](https://qiita.com/Hironsan/items/2466fe0f344115aff177)
- [NLP Pipeline 101 With Basic Code Example — Feature Extraction](https://medium.com/voice-tech-podcast/nlp-pipeline-101-with-basic-code-example-feature-extraction-ea9894ed8daf)

その他の参考資料（大量）は、[こちら :octocat: ](https://github.com/sota0121/slack-msg-analysis/blob/master/references/REFERENCE_LINK.md)にまとめています。

### 宣伝

今回は、Data Learning Guild というSlackコミュニティのデータを利用させていただいています。
Data Learning Guild はデータ分析人材が集まるオンラインコミュニティです。気になる方は、こちらをチェックしてみてください。

[データラーニングギルド公式ホームページ](https://data-learning.com/guild)

[データラーニングギルド 2019 Advent Calendar](https://qiita.com/advent-calendar/2019/data-learning-guild)