Excelに整理した「アンケート自由記述」を分析しよう！（その1）　～様々な可視化を自動に～

>20211130：Word-cloud記述に誤りあり、修正。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/9be646e6-0412-9655-c7dd-7aafb5508d5b.png)

##はじめに
[**過去の記事**](https://qiita.com/hima2b4/items/46688daebfc4c98d729f)で、テキスト（自然言語）の分析や可視化を紹介しました。分析の手順としてはザっと以下のような内容です。
>1. 自由記述であれ何であれ、テキスト（自然言語）をガサっとtxtテキストファイルに放り込む。
2. 「。」でセンテンスに分割。
3. 形態素分析。
4. WordCloud、出現語カウントグラフ、共起ネットワーク…等を描画

　　
アンケート等で得た結果を全般として把握したい場合は、上記の方法でいいですが、User単位で**表形式でまとめられた形式は崩さず、満足度等の情報があればそれらも活かして分析を進めたい**ですね。
　
自然言語処理は様々なサイトで紹介されていますが、なぜか不思議とテキストデータを取り込んで…というものばかり。
**表形式のデータを取り込み、データフレーム化して自然言語処理**を進めたいということで、やってみましたという記事です。
**所定の表形式データを取り込むだけで、ザっと記事の上に貼付したようなキャプチャのような可視化ができます。**


##実行条件など
>・**Google colab**で実行
・**手元の表形式データ（アンケート自由記述結果）**で実行
※使用したデータは開示できませんが、データ形式は以下です。カラムはUserID、comment、csの3つ。commentは自由記述回答、csは満足度（1～6）です。

|userID|comment|cs|
|---|---|---|
|U001  |総合的には満足してますが、○○が細かく調整できたらもっと使いやすいと思います。|4  |
|U002  |○○ボタンが押しにくいです。|2  |
|U003  |特にありません。|3  |
|U004  |既存品と取付位置が合わないことがつらいです。|1  |


##備忘
- 実行内容は[**『以前の記事：自然言語を可視化･分析できるライブラリ「nlplot」はすごいよ』**](https://qiita.com/hima2b4/items/46688daebfc4c98d729f)をベースとしています。
- コード操作を不要にするため、ファイル指定やフォームなど、GoogleColabの機能を活用した。
- 満足度（CS）High/Low設定は、コード操作なく、数値設定フォームで変更できるようにした。
- 取り込んだデータをデータフレームに格納、形態素の結果もデータフレームに結合した。
- 品詞は一般名詞・動詞・形容詞（動詞と形容詞は基礎型）を抽出対象とした。
- 分かち書きは、スペース区切り（語A 語B 語C）とカンマ区切り [語A,語B,語C] の2パターン。TF-IDF計算はスペース区切り、Word Cloudは カンマ区切り、nlplotはどちらでもいけたと思います。1パターンだけで対応したかったが…私の修行が足りないのだと思います。
- 満足度（1-6）を、3以下：不満足／4以上：満足とし、TF-IDFによるWord Cloudは、全データ/満足高データ/満足低データで描画した。（word2vecでベクトル化した語の可視化も同様）
- word2vecでベクトル化した語の可視化（PCA）では、抽出語数が少ないと描画できないとはじかれることがありました。このような場合は「Word2Vecモジュール（語の最小出現数やベクトル生成手法等の設定）」のmin_count数を調整してください。（これはコード操作必要）

##ライブラリのインストール

```Python:nlplotをインストール
pip install nlplot
```

```Python:日本語フォント
#日本語フォントをインストール
!apt-get -y install fonts-ipafont-gothic
```

```Python:MeCabのインストール
#Mecabのインストール
!pip install mecab-python3==0.996.5
```

```Python:matplotlib日本語化
#matplotlib日本語化
!pip install japanize-matplotlib
```

```Python:ライブラリインポート
from pathlib import Path
import pandas as pd
import re
import MeCab
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
```

##ファイル & CS_High/Low_Level & ストップワード指定
※CSは顧客満足度です。

```Python:
#@title csvファイル（UTF-8）を指定してください
from google.colab import files
#print('csvファイル(UTF-8）を指定してください')
uploaded = files.upload()
```
```Python:
#@title CS_High/Low設定（上：設定以上をCS高/下：設定以下をCS低） { run: "auto" }
CS_High_level_x_or_More = 4 #@param {type:"number"}
CS_Low__level_x_or_Less = 3 #@param {type:"number"}
```

```Python:
#@title ストップワード設定
stop_words = ["し", "い", "ある", "おる", "せる", "ない", "いる", "する", "の", "よう", "なる", "それ", "そこ", "これ", "こう", "ため", "そう", "れる", "られる"]
```
※GoogleColabのフォームを使うと表示は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/edaac113-d5d7-ae2b-9840-c621c95907ea.png)


##モジュール構築
```Python:
#@title データフレーム格納＆欠損値削除
if len(uploaded.keys()) != 1:
    print("アップロードは１ファイルにのみ限ります")
else:
    target = list(uploaded.keys())[0]

df = pd.read_csv(target)

df.dropna(subset=['comment'], inplace=True)
df.head()
```

```Python:
#@title 形態素解析（一般名詞・動詞:基礎型・形容詞:基礎型）＆カンマ･スペース区切りをデータフレームに格納
#形態素解析（一般名詞・動詞・形容詞（動詞と形容詞は基礎型）を抽出対象とした）
#スペース区切り分かち書き
def mecab_analysis(text):

    t = MeCab.Tagger('-Ochasen')

    node = t.parseToNode(text)

    words = []
     
    while node:
        if node.surface != "":  # ヘッダとフッタを除外

            word_type = node.feature.split(',')[0]
            sub_type = node.feature.split(',')[1]
            features_ = node.feature.split(',')

            #品詞を選択
            if word_type in ["名詞"]: 
                if sub_type in ['一般']:
                    word = node.surface
                    words.append(word)

            #動詞、形容詞[基礎型]を抽出（名詞のみを抽出したい場合は以下コードを除く）
            elif word_type in ['動詞','形容詞'] and not (features_[6] in stop_words):
                words.append(features_[6])

        node = node.next

        if node is None:
            break

    return " ".join(words)

#カンマ区切り分かち書き
def mecab_analysis2(text):

    t = MeCab.Tagger('-Ochasen')

    node = t.parseToNode(text)

    words2 = []

    while(node):

        if node.surface != "":  # ヘッダとフッタを除外
            word_type = node.feature.split(',')[0]
            sub_type = node.feature.split(',')[1]
            features_ = node.feature.split(',')

            if word_type in ['名詞']:  # 名詞をリストに追加する
                if sub_type in ['一般']:
                    words2.append(node.surface)

            #動詞、形容詞[基礎型]を抽出（名詞のみを抽出したい場合は以下コードを除く）
            elif word_type in ['動詞','形容詞'] and not (features_[6] in stop_words):
                    words2.append(features_[6])

        node = node.next
        if node is None:
            break
    return words2

#形態素結果をリスト化し、データフレームdf1に結果を列追加する
df['words'] = df['comment'].apply(mecab_analysis)
df['words2'] = df['comment'].apply(mecab_analysis2)

#表示
df
```
※表示の一部抜粋
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/591ce264-7684-31d1-9de2-8e400140aae6.png)


```Python:
#@title 形態素結果を層別しデータフレームに格納
#全データをデータフレームに格納
all_words=' '.join(df['words'])
df_all = pd.Series(all_words)

#CSが高いuserの声（words）をデータフレームに格納
drop_index = df.index[df['cs'] <=CS_Low__level_x_or_Less]
#条件にマッチしたIndexを削除
df_high = df.drop(drop_index)
all_high_words=' '.join(df_high['words'])
df_all_high_words = pd.Series(all_high_words)

#CSが低いuserの声（words）をデータフレームに格納
drop_index2 = df.index[df['cs'] >=CS_High_level_x_or_More]
#条件にマッチしたIndexを削除
df_low = df.drop(drop_index2)
all_low_words=' '.join(df_low['words'])
df_all_low_words = pd.Series(all_low_words)

print('CS高ユーザーの声(Words）：')
print(df_all_high_words)
print('CS低ユーザーの声(Words）：')
print(df_all_low_words)
```

```Python:
#@title ワード出現回数カウント（表示する場合は#外す）
#カンマ区切り分かち書きしたワードをリスト化
words_list = df.words2.tolist()
words_list = sum(words_list,[])

from collections import Counter

#出現回数を集計し、最頻順にソートし、resultに格納
words_count = Counter(words_list)
result = words_count.most_common()

#出現回数結果を画面に出力
#for word, cnt in result:
#    print(word, cnt)
```

#nlpot
```Python:
#@title uni-gram表示
import nlplot

npt = nlplot.NLPlot(df, target_col='words')

# top_nで頻出上位単語, min_freqで頻出下位単語を指定できる
#stopwords = npt.get_stopword(top_n=0, min_freq=0)

npt.bar_ngram(
    title='uni-gram',
    xaxis_label='word_count',
    yaxis_label='word',
    ngram=1,
    top_n=50,
#    stopwords=stopwords,
)
```
```Python:
#@title tree map表示
npt.treemap(
    title='Tree of Most Common Words',
    ngram=1,
    top_n=30,
#    stopwords=stopwords,
)
```
```Python:
#@title Word Cloud表示（nlpot）
npt.wordcloud(
    max_words=100,
    max_font_size=100,
    colormap='tab20_r',
#    stopwords=stopwords,
)
```
```Python:
#@title Word Distribution表示（単語数の分布）
# 単語数の分布
npt.word_distribution(
    title='number of words distribution',
    xaxis_label='count',
)
```
```Python:
#@title Build Graph（共起ネットワーク）表示
# ビルド（データ件数によっては処理に時間を要します）※ノードの数のみ変更
npt.build_graph(min_edge_frequency=1,
                #stopwords=stopwords,
                )

display(
    npt.node_df.head(), npt.node_df.shape,
    npt.edge_df.head(), npt.edge_df.shape
)

npt.co_network(
    title='Co-occurrence network',
)
```
```Python:
#@title Sunburst表示
npt.sunburst(
    title='All sentiment sunburst chart',
    colorscale=True,
    color_continuous_scale='Oryel',
    width=800,
    height=600,
    #save=True
)
```

##Word Cloud
```Python:
#@title TF-IDFマトリクス作成＆データフレーム格納
# ライブラリインポート
from sklearn.feature_extraction.text import TfidfVectorizer

# TF-IDFのベクトル処理
vectorizer = TfidfVectorizer(use_idf=True)
tfidf = vectorizer.fit_transform(df['words'] )

# TF-IDF値を「センテンス×ワード」マトリクスをデータフレーム化
df_tfidf = pd.DataFrame(tfidf.toarray(), columns=vectorizer.get_feature_names(), index=df['words'])
#display(df_tfidf)
```

```Python:
#@title Word Cloud by word_count（All Data）：🔲型 → #maskの#外すと🍩型に

#wordcloud取込用にresultを辞書型ヘ変換
dic_result = dict(result)

#Word Cloudで画像生成（#max_words, width, heightは任意設定）
from wordcloud import WordCloud

#画像データダウンロード（biwakoの画像リンクもあり。変更する場合は#調整）
import requests

url = "https://github.com/hima2b4/Word-Cloud/raw/main/donuts.png"
#url = "https://github.com/hima2b4/Word-Cloud/raw/main/biwa.png"

file_name = "donuts.png"
#file_name = "biwa.png"

response = requests.get(url)
image = response.content

with open(file_name, "wb") as f:
    f.write(image)

#ライブラリインポート
from PIL import Image
import numpy as np

#Word Cloudで画像生成（#max_words, width, heightは任意設定）
custom_mask = np.array(Image.open('donuts.png'))
wordcloud = WordCloud(background_color='white',
                      max_words=125,
                      #mask=custom_mask,
                      font_path='/usr/share/fonts/truetype/fonts-japanese-gothic.ttf',
                      width=1000,
                      height=600,
                      ).fit_words(dic_result)

#生成した画像の表示
plt.figure(figsize=(15,10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```

```Python:
#@title Word Cloud with TF-IDF（All DATA)：🔲型 → #maskの#外すと🍩型に
# TF-IDF計算
tfidf_vec2 = vectorizer.fit_transform(df_all).toarray()[0]
# TF-IDFを辞書化
tfidf_dict2 = dict(zip(vectorizer.get_feature_names(), tfidf_vec2))
# 値が正のkeyだけ残す
tfidf_dict2 = {k: v for k, v in tfidf_dict2.items() if v > 0}

#Word Cloudで画像生成（#max_words, width, heightは任意設定）
wordcloud = WordCloud(background_color='white',
                      max_words=125,
                      #mask=custom_mask,
                      font_path='/usr/share/fonts/truetype/fonts-japanese-gothic.ttf',
                      width=1000,
                      height=600,
                      ).generate_from_frequencies(tfidf_dict2)

#生成した画像の表示
plt.figure(figsize=(15,10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```
```Python:
#@title Word Cloud with High CS Data (TF-IDF)：🔲型 → #maskの#外すと🍩型に
tfidf_vec = vectorizer.fit_transform(df_all_high_words).toarray()[0]
# TF-IDFを辞書化
tfidf_dict = dict(zip(vectorizer.get_feature_names(), tfidf_vec))
# 値が正のkeyだけ残す
tfidf_dict = {k: v for k, v in tfidf_dict.items() if v > 0}

#Word Cloudで画像生成（#max_words, width, heightは任意設定）
wordcloud = WordCloud(background_color='white',
                      max_words=125,
                      #mask=custom_mask,
                      font_path='/usr/share/fonts/truetype/fonts-japanese-gothic.ttf',
                      width=1000,
                      height=600,
                      ).generate_from_frequencies(tfidf_dict)

#生成した画像の表示
plt.figure(figsize=(15,10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```
```Python:
#@title Word Cloud with Low CS Data (TF-IDF)：🔲型 → #maskの#外すと🍩型に
tfidf_vec = vectorizer.fit_transform(df_all_low_words).toarray()[0]
# TF-IDFを辞書化
tfidf_dict = dict(zip(vectorizer.get_feature_names(), tfidf_vec))
# 値が正のkeyだけ残す
tfidf_dict = {k: v for k, v in tfidf_dict.items() if v > 0}

#Word Cloudで画像生成（#max_words, width, heightは任意設定）
wordcloud = WordCloud(background_color='white',
                      max_words=125,
                      #mask=custom_mask,
                      font_path='/usr/share/fonts/truetype/fonts-japanese-gothic.ttf',
                      width=1000,
                      height=600,
                      ).generate_from_frequencies(tfidf_dict)

#生成した画像の表示
plt.figure(figsize=(15,10))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()
```

##Visualization of word2vec
```Python:
#@title Word2Vecモジュール（語の最小出現数やベクトル生成手法等の設定）
from gensim.models import word2vec

# size : 中間層のニューロン数・数値に応じて配列の大きさが変わる。数値が多いほど精度が良くなりやすいが、処理が重くなる。
# min_count : この値以下の出現回数の単語を無視
# window : 対象単語を中心とした前後の単語数
# iter : epochs数
# sg : skip-gramを使うかどうか 0:CBOW 1:skip-gram

model = word2vec.Word2Vec(df['words2'],
                          size=200,
                          min_count=5,
                          window=5,
                          iter=20,
                          sg = 1)    # sg=1:skip-gram使用

model2 = word2vec.Word2Vec(df_high['words2'],
                          size=200,
                          min_count=5,
                          window=5,
                          iter=20,
                          sg = 1)    # sg=1:skip-gram使用

model3 = word2vec.Word2Vec(df_low['words2'],
                          size=200,
                          min_count=2,
                          window=5,
                          iter=20,
                          sg = 1)    # sg=1:skip-gram使用
```
```Python:
#@title ベクトル化した各語彙確認
#ベクトル化したテキストの各語彙確認
model.wv.index2word
```
```Python:
#@title PCA表示（ALL Data)
#PCA実行
from sklearn.decomposition import PCA

pca = PCA(n_components=2)
values = pca.fit_transform(model.wv.vectors)
#print(values.shape)
#print(values)

#PCA可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values, model.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('PCA on word2vec embeddings（All_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```
```Python:
#@title PCA表示（CS_High Data)
pca = PCA(n_components=2)
values2 = pca.fit_transform(model2.wv.vectors)

#PCA可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values, model2.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('PCA on word2vec embeddings（CS_High_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```
```Python:
#@title PCA表示（CS_Low Data)
pca = PCA(n_components=2)
values3 = pca.fit_transform(model3.wv.vectors)

#PCA可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values, model3.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('PCA on word2vec embeddings（CS_Low_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```
※全データ／CS高データ／CS低データ、それぞれで可視化
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/a72b6ea5-2131-dcaf-1bc0-57148eec3aee.png)


```Python:
#@title t-SNE表示（All Data)
#t-SNE実行
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
values4 = tsne.fit_transform(model.wv.vectors)

#t-SNE可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values4, model.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('t-SNE on word2vec embeddings（All_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```
```Python:
#@title t-SNE表示（CS_High Data)
#t-SNE実行
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
values5 = tsne.fit_transform(model2.wv.vectors)

#t-SNE可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values5, model2.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('t-SNE on word2vec embeddings（CS_High_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```
```Python:
#@title t-SNE表示（CS_Low Data)
#t-SNE実行
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2, random_state=0)
np.set_printoptions(suppress=True)
values6 = tsne.fit_transform(model3.wv.vectors)

#t-SNE可視化
import matplotlib.pyplot as plt
import japanize_matplotlib
import seaborn as sns

sns.set(font="IPAexGothic")
plt.rcParams["font.size"] = 16
plt.tight_layout()
fig = plt.figure(figsize=(12,6))
for value, word in zip(values6, model3.wv.index2word):
    plt.plot(value[0], value[1], marker='')
    plt.annotate(word, (value[0], value[1]))
    plt.title('t-SNE on word2vec embeddings（CS_Low_Data）', fontsize=25)
    #plt.xlim(-0.02,0.04)
    #plt.ylim(-0.02,0.04)
    plt.xticks(fontsize= 14)
    plt.yticks(fontsize= 14)
plt.show()
```

#最後に
コードは冗長的と思いますが、なんとかやりたいことはできました。
表形式に整理された自由記述回答で分析を行いたいケースは多いと思います。
表形式を崩さなければ、満足度など他の属性との関係もみられますし、元データに戻ることも容易ですね。

今回はGoogleColabのフォームを多用しました。コードが全くみえない状態にもできます。
これらを使われる方にとっては、コードが見えただけで引く方もおられるかもしれませんので、これはいいです。
あらかじめ、取込むデータの形式には従わないといけませんが、基本的にはランタイムでセルを実行するだけで勝手に可視化してくれるので便利です。

今回のコードは、満足度数値があることを前提に処理を進めていますので、（あたりまえですが）満足度数値がない場合は該当セルの処理は進みませんが、該当セルは飛ばして上のセルから順番に実行すれば他のセルは進められると思います。

最後まで読んでいただき、ありがとうございました。


#参考サイト

https://data-analytics.fun/2020/03/12/word2vec/

https://zenn.dev/megane_otoko/articles/013_tf_idf_trial

https://axross-recipe.com/recipes/34

https://www.takapy.work/entry/2020/05/17/192947

https://boxcode.jp/nlplot%E3%81%8C%E5%87%84%E3%81%84%EF%BC%81%E8%87%AA%E7%84%B6%E8%A8%80%E8%AA%9E%E3%82%92%E5%8F%AF%E8%A6%96%E5%8C%96%E3%83%BB%E5%88%86%E6%9E%90%E3%81%A7%E3%81%8D%E3%82%8Bpython%E3%83%A9%E3%82%A4

https://boxcode.jp/python%e3%81%a7%e6%84%9f%e6%83%85%e5%88%86%e6%9e%90%ef%bc%88%e5%bd%a2%e6%85%8b%e7%b4%a0%e8%a7%a3%e6%9e%90%e6%ba%96%e5%82%99%e7%b7%a8%ef%bc%89-mecab

https://www.cloudnotes.tech/entry/polka_word2vec

http://wordcloudjp.com/