【Python/TwitterAPI】コンテンツのトピック抽出(gensim LDA)とWordCloudによる可視化

# 1. はじめに
qiitaの記事は2つ目になります。
機械学習の勉強もまだ数か月のため技術的に未熟な投稿ではありますが、
TwitterAPI(v2)の使用方法などについて少しでも初学者の方の参考になれば幸いです。

最初の投稿はこちら。この頃よりは多少成長してる、はずです。

https://qiita.com/kawanago_py/items/d93e59d795d504ce5cba



また今回の記事ではAPIの申請方法等は扱っておりませんので、
そちらを知りたい方は以下のようなページを参考に申請をお願い致します。
[【2022年度最新版】Twitter APIの申請方法【画像解説付き】](https://toxublog.com/blog/twitter_api_apply/)

## 1-1. 分析の概要と目的
本分析では任天堂から発売されている「Nintendo Swich」のゲーム **「Splatoon3」について、
TwitterAPIで取得したゲームユーザーのTweetから、注目されるキーワードを調査**するものです。

個人的な内容も含みますが、今回の分析を行う目的は以下の通りです。
**【本分析の目的】**：**<font color="red">「Splatoon3」のアクティブユーザーの中で注目されているワードを可視化する</font>**
**【個人的な目的】**：APIを利用した分析を経験する
　　　　　　　　　自然言語を含むデータの分析を経験する
　　　　　　　　　既に成形されたデータではなく、データの取得と前処理を経験する

## 1-2. 分析の方針/流れ
今回の分析では大まかに以下のような流れで進めていきます。
- 「Splatoon」の公式アカウントのフォロワーを抽出
- 抽出したフォロワーの中で、自己紹介欄に「スプラ」などを含むユーザーを再度抽出
- 抽出したユーザーを対象に、ゲーム発売日以降のツイートを取得
- ツイートから単語のみを抜き出して分かち書き、ベクトルに変換
- LDAモデルを作成してトピック分析
- ワードクラウドによる可視化

## 1-3. 分析時の環境
|  | version |
|:-:|:-:|
| tweepy  |  '4.10.1' |
| python  |  '3.9.12' |



# 2. 実装
上記の分析方針の通りにデータの取得から可視化までを行います。


## 2-1. ライブラリ/APIの準備
```import.py
import tweepy
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
import re
import neologdn
import time
import datetime as dt
import MeCab
import unidic
import gensim
from gensim import corpora
import matplotlib.pyplot as plt
from wordcloud import WordCloud
from PIL import Image
```
```client.py
# "---"は自身のkey/tokenを入れる
consumer_key        = "-----------------------"
consumer_secret     = "-----------------------"
access_token        = "-----------------------"
access_token_secret = "-----------------------"
bearer_token        = "-----------------------"

def build_client():
    client = tweepy.Client(bearer_token        = bearer_token,
                           consumer_key        = consumer_key,
                           consumer_secret     = consumer_secret,
                           access_token        = access_token,
                           access_token_secret = access_token_secret,)    
    return client

#clientを作成
client = build_client()
```
いずれ自動化を予定していたので今回はclientの準備に関数を定義しましたが、
直接`client = tweepy.Client(bearer_token = "---", , , )`としても問題ありません。

今回の分析では**リファレンスの見やすさ**、**レスポンスの中身**からv2のclientを利用しています。
各バージョンのメソッドなどは以下のリファレンスを参考にしました。

https://docs.tweepy.org/en/stable/client.html

https://docs.tweepy.org/en/stable/api.html



## 2-2. フォロワーデータの取得
今回はゲームのアクティブユーザーを抽出する方法として、
**ゲームの公式アカウントのフォロワーから抽出**する方法を選択しました。

それだけだと対象となるユーザーが多すぎる、かつアクティブでないユーザーを多く含むため、
**自己紹介欄に特定のワードを含むユーザーのみを抽出**の対象とします。

ユーザー抽出の方法として他に、「ゲームに登場するキーワードを事前に設定し、
そのワードを含むツイートをしているユーザーを抽出する」という方法も検討しましたが、
事前知識に頼らず、一般化した方法を探るという意図で上記のような方法を選択しています。

```get_followers_info.py
# "username"を指定してそのフォロワーの"id","name","username","description"を含むデータフレームを返す
# "n_loop"(default=5)でfor文の繰り返し数を指定(1回あたり14*1000ユーザー)

# "id" --> アカウントごとにシステム上で割り振られている数字の番号
# "name" --> ユーザー自身が設定する名前
# "screen_name", "username" --> ユーザ自身が設定する「@------」の文字列の部分(api[v1]:screen_name, api[v2]:username)
# "description" --> ユーザ自身が記述する自己紹介文

def get_followers_info(target_username, n_loop=5):
    #一度に取得できるユーザー数の上限1,000に設定
    max_results = 1000
    
    #"data_frame"のカラムになるリストを作成
    user_id = []
    username = []
    name = []
    description = []
    
    #入力した"username"から"user_id"を取得
    target_id = client.get_user(username=target_username).data.id
    #IDで指定したアカウントのフォロワー情報を取得
    target_info = client.get_users_followers(id=target_id,               #ユーザーID
                                             user_auth=True,             #これをやらないと認証されない
                                             user_fields="description",  #"description(自己紹介文)"を含んで取得
                                             max_results=max_results)    #一度に取得するアカウント数(上限1,000)
    
    #次の情報取得時に必要な"next_token"を一時的にtxtで保存
    next_token = target_info.meta["next_token"]  #metaからtokenを取得
    f = open("./data/token.txt", "w")                 #txtファイルをwriteモードで開く(最初は新規作成される)
    f.write(next_token)                          #tokenの情報をファイルに書き込み
    f.close()
    
    #"ID","username","name","description"をそれぞれ取得
    for i in range(max_results): 
        user_id.append(target_info.data[i].id)
        username.append(target_info.data[i].username)
        name.append(target_info.data[i].name)
        description.append(target_info.data[i].description)
    
    #一回目の"get_users_followers"で取得した"next_token"を使い、取得を繰り返す
    for _ in range(n_loop):
        #15分に15回のリクエストまでしか許可されていないため、rangeを14に設定
        for i in range(14):
            f = open("./data/token.txt", "r")  #tokenファイルをreadモードで開く
            next_token = f.read()              #next_tokenを読み込み
            f.close()
            
            #上で読み込んだtokenをpagination_tokenに設定
            target_info = client.get_users_followers(id=target_id,
                                                     user_auth=True,
                                                     user_fields="description",
                                                     max_results=max_results,
                                                     pagination_token=next_token)
            
            #最新のnext_tokenを上書き
            next_token = target_info.meta["next_token"]
            f = open("./data_token.txt", "w")
            f.write(next_token)
            f.close()
            
            #最新の各情報をリストに格納
            for i in range(max_results): 
                user_id.append(target_info.data[i].id)
                username.append(target_info.data[i].username)
                name.append(target_info.data[i].name)
                description.append(target_info.data[i].description)
        
        #15回(2週目以降は14回)分格納したリストをdata_frameに変換
        df_list=list(zip(user_id, username, name, description))
        data = pd.DataFrame(data=df_list, columns=["user_id", "username", "name", "description"])
        #自己紹介のないデータを除去
        data = data[data["description"] != ""]  
        #"description"に"spla"または"スプラ"を含む行だけを抽出
        data = data[data["description"].str.contains("spla|スプラ")].reset_index(drop=True)
        #csvに出力
        data.to_csv("./data/followers_data.csv", index=False)
        
        #進捗の確認用
        print(dt.datetime.now())
        print(data.shape)
        #15分間停止
        time.sleep(901)
    
    #返り値としてdata_frameを出力
    return data
```
```followers_data.py
#スプラトゥーンの公式アカウントのフォロワーを抽出
target_username = "SplatoonJP"
followers_data= get_followers_info(target_username, 2)
```

## 2-3. 対象アカウントのtweetを取得
上記コードで抽出したユーザーを対象に、ゲーム発売日以降のツイートを取得します。
ゲーム発売日以降とした理由は以下の通りです。
・**発売日以前では「Splatoon2」に関するツイートを含む可能性が高くなるため**
・**主に発売日以降の感想や不満点を抽出することを目的としているため**

またリプライにはユーザー同士のゲームに関係のない会話(例：フォローありがとうございます)  
などを大量に含んでしまうため、**ツイート取得時にはリプライを除く**こととしました。

同様に対象ユーザー以外のツイートを取得することになるために**RTも除く**こととします。

```utc_to_jst.py
#created_atを日本時間に変更する
def utc_to_jst(timestamp_utc):
    datetime_utc = dt.datetime.strptime(timestamp_utc, "%Y-%m-%d %H:%M:%S%z")
    datetime_jst = datetime_utc.astimezone(dt.timezone(dt.timedelta(hours=+9)))
    timestamp_jst = dt.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S')
    return timestamp_jst
```
```get_tweets.py
#対象アカウントのツイートを取得(発売日の9/9以降)
#上記で取得したフォロワーのデータフレームを入力として受け取る
def get_tweets(dataframe):
    tweet_id = []    #各tweetに割り振られるID
    time = []        #tweetが投稿された日時
    tweet = []       #tweet本文
    like_count = []  #tweetについたお気に入り数
    rt_count = []    #tweetについたretweet数
    rp_count = []    #tweetについたreply数
    
    for i in range(dataframe.shape[0]):
        try:
            user_tweets = client.get_users_tweets(id = dataframe.iloc[i, 0],
                                                  user_auth = True,
                                                  max_results = 100,
                                                  exclude = ["retweets", "replies"],  #RTとreplyを除く
                                                  tweet_fields = ["created_at", "public_metrics"],  #投稿時間とその他を取得するように設定
                                                  start_time = "2022-09-09T00:00:00Z")  #2022/9/9以降のデータを取得
            
            #各ユーザーに対して、取得したtweet数の回数分、データをリストに格納
            for i in range(user_tweets.meta["result_count"]):
                tweet_id.append(user_tweets.data[i].id)
                time.append(user_tweets.data[i].created_at)
                tweet.append(user_tweets.data[i].text)
                like_count.append(user_tweets.data[i].public_metrics["like_count"])
                rt_count.append(user_tweets.data[i].public_metrics["retweet_count"])
                rp_count.append(user_tweets.data[i].public_metrics["reply_count"])
        except:
            pass
    
    #各リストからdata_frameに変換
    df_list=list(zip(tweet_id, time, tweet, like_count, rt_count, rp_count))
    tweets_data = pd.DataFrame(data=df_list, columns=["tweet_id", "time", "tweet", "like_count", "rt_count", "rp_count"])
    #timeを日本時間に変換
    tweets_data["time"] = tweets_data["time"].map(lambda x : utc_to_jst(str(x)))
    #CSVに出力
    tweets_data.to_csv("./data/tweets_data.csv", index=False)
      
    return tweets_data
```
```tweets_data.py
#tweetデータを取得
tweets_data = get_tweets(followers_data)
```

## 2-4. テキストクレンジング
「#スプラ」など単語を不要にかつ大量に含む可能性があるため**ハッシュタグは全て除き**、  
また今回は**日本語のみを対象**とするため、URL等の除去はせずに直接以下のように処理します。

上記の処理について、分析の対象をTwitter全体として「Splatoon」自体の注目度を調べる場合は、
全てのハッシュタグを含めて単語数を集計する必要があるかと思います。

また今回は**コンテンツの特性上、日本語以外で表記された単語はほとんど出現せず、
分析の中では重要度が低いものだと事前知識から判断**し、上記のような処理を行いました。
他コンテンツの分析では絵文字や英数字を含むかの検討が必要だと思います。

```cleansing_text.py
#クレンジング用の関数を定義
#入力されたテキストの日本語文のみが出力として返される
def cleansing_text(text):
    tmp = re.sub(r"#[^\s]*", "", text, 10)                    #ハッシュタグ「#------」を10個まで除去
    tmp = neologdn.normalize(tmp.lower())                     #半角/全角の統一(カナは全角)、小文字に統一、～も消える
    cleansed_text = re.sub(r"[^ぁ-んァ-ン一-龥ー]+", "", tmp)  #"ひらがな","カタカナ","漢字","ー"のみを抽出
    return cleansed_text
```
```cleaned.py
#クレンジングの関数をtweetに適用し、["cleaned"]として列を追加
tweets_data["cleaned"] = tweets_data["tweet"].map(lambda x : cleansing_text(x))
#クレンジング後に空白になってしまった行(日本語以外の投稿、URLのみ、ハッシュタグのみなど)を削除
tweets_data = tweets_data[tweets_data["cleaned"] != '']
#tweets_data.csvを上書き
tweets_data.to_csv("./data/tweets_data.csv", index=False)
```

## 2-5. 単語の分割
最終的にワードクラウドを出力にあたって、
日本語だと大量の助詞を含むため**テキストから抽出するのは名詞のみ**としました。

感情分析を行う場合には助詞以外を対象として抽出する必要がありそうです。

```mecab_analyzer.py
# 入力されたtextから名詞だけを抜き出しリストにして返す
tagger = MeCab.Tagger()
def mecab_analyzer(text):
    text_list = []
    res = tagger.parse(text).splitlines()[:-1]
    for line in res:
        spl = line.split(",")[0].split("\t")
        if spl[1] == "名詞":
            text_list.append(spl[0])
    return " ".join(text_list)
```
```CountVectorizer.py
#テキストを名詞のみ抽出したリストに変換
text_data = []
for i in range(tweets_data.shape[0]):
    text_data.append(mecab_analyzer(tweets_data["cleaned"][i]))

#countvectorizerでベクトル表現へ変換
vec = CountVectorizer(min_df=15)  #出現回数が15回以下の単語を除く
cv_text = vec.fit_transform(text_data)
#変換後の単語数を確認
cv_text.shape
```
上記の出力は`(13744, 477)`となり、**477単語**が抽出されたことが分かります。

今回は**出現回数15回以上**を条件として抽出しましたが、
ここは実際に何度か出力を繰り返して期待した単語があるかを確認して決めています。

より正確にゲームユーザーのみを抽出、関連ツイードだけ取得できていれば、
この出現回数(または割合)を増やしても注目ワードを抽出できると思います。

また定義した`mecab_analyzer`は本来、
`vec = CountVectorizer(analyzer=mecab_analyzer)`として直接使用できるのですが、
`["ス","プ","ラ",...]`のように1文字ずつ出力されてしまったため、別に実行しました。
ここの挙動については後ほど調べて改善したいと思います。

```term_freq.py
#出現回数の多い単語と出現回数を50個出力
term_freq = np.array(cv_text.sum(axis=0))[0]
index = term_freq.argsort()[::-1][:50]
for i, n in enumerate(index):
    print(i, vec.get_feature_names_out()[n], term_freq[n])
```

![top50_0_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/57604b53-9346-d83d-a2ef-4c7948fa9834.png "top_0_45")

今回のベクトル変換でCountVectorizerを使用しましたが、
正確にアクティブユーザーのみを抽出、関連ツイートのみを取得していた場合には、
tf-idfを用いて重み付けした方が、よりコンテンツ内の注目ワードを抽出できたかも知れません。

この時点で集計したツイートデータの中には、
ゲームに関連しないツイートも散見されたためCountVectorizerを使用しました。

## 2-6. LDAによるトピック抽出/可視化
LDA(潜在ディリクレ配分法)によりテキストからトピックを抽出します。
学習時はひとまず**トピック数を3に設定**しました。

```LDA3.py
#ldaによるトピック抽出

#テキストの単語をリストにして格納(["今日 雨 屋内", "明日 晴れ"] --> ["今日", "雨", "屋内"], ["明日", "晴れ"])
text_data_list = [txt.split(" ") for txt in text_data]
#cv_textに出現する単語のリストを作成(["今日", "雨", "明日", "晴れ"])
vec_list = list(vec.vocabulary_.keys())

#text_data_listの中で、vec_listに出現するものだけを抽出
#vec_listを(["今日", "雨", "明日", "晴れ"] --> ["今日", "雨"], ["明日", "晴れ"])
#vec_listには出現回数15回未満の単語は含まれていない
noun_list = []
for text in text_data_list:
    tmp_list = []
    for noun in text:
        if noun in vec_list:
            tmp_list.append(noun)
    noun_list.append(tmp_list)

#lda学習用のdictionaryとcorpusを作成
dictionary = corpora.Dictionary(noun_list)                 #出現単語を辞書として作成
corpus = [dictionary.doc2bow(noun) for noun in noun_list]  #corpusを作成([[(0, 1), (1, 1), (2, 1)], ...])
#ldaのインスタンスを作成(num_topicsでトピック数を指定)
num_topics = 3
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=num_topics, id2word=dictionary)
```
```importance_noun.py
#各トピックに含まれる単語を重要度の高いものから10個出力する

#格納用のデータフレームを作成
importance_noun = pd.DataFrame()

#トピックごとに重要単語10個を列をして追加
for i in range(num_topics):
    #argsortは昇順がデフォなので"-"を付けて逆順にする
    #重要単語のindexをリストに格納
    index = np.argsort(-lda.get_topics())[i][:10]
    word = []
    for l in index:
        #indexから重要単語をwordリストに格納
        word.append(dictionary.id2token[l])
    #抽出したwordリストをdata_frameの列に追加
    importance_noun["topic" + str(i)] = word
    
#結果の出力
importance_noun
```

![importance_noun.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/1ad3e77f-a5c5-24a2-931d-6285ebe3a157.png "importance_noun")
トピック分類された単語を見てみると、**同様の単語が複数のトピックに含まれていたり**、  
明らかに**関係のない単語が多く含まれてしまっている**ことが分かります。  
やはり前処理やツイートの取得方法に問題があると思われます。

次に分類されたトピック、単語をワードクラウドで可視化していきます。

```wordcloud3.py
#wordcloudの作成

#プロットするエリアを設定
fig, axs = plt.subplots(ncols=num_topics, nrows=1, figsize=(15,7))
#axs = axs.flatten()  #複数行にplotする場合は実行

#wordcloudの形を円形にするためのマスク
mask = np.array(Image.open("./image/ball.png"))
#フォントを指定するためのパス
fpath = "./data/Corporate-Logo-Bold-ver2.otf"

for i, t in enumerate(range(lda.num_topics)):
    #[t]番目のtopicに含まれる出現頻度が上位30個の単語と、その出現頻度を格納
    #中身は「('単語', 出現頻度), ('---', 0.0___),....」
    x = dict(lda.show_topic(t, 30))
    
    #wordcloudのインスタンスを作成
    im = WordCloud(
        font_path=fpath,               #フォントを指定
        background_color="white",      #背景を設定
        mask=mask,                     #画像の形を指定
        random_state=0
    ).generate_from_frequencies(x)     #頻度を元に文字サイズを決定
    
    axs[i].imshow(im)                  #wordcloudを出力
    axs[i].axis("off")                 #axisを外す
    axs[i].set_title("Topic "+str(t))  #タイトルを設定

plt.tight_layout()                     #wordcloudの出力位置を自動調整
plt.show()
```

![word_cloud_topic3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/eb823385-bc53-84f1-80b2-8341b950438a.png "word_cloud_topic3")
**【重要そうなワードと影響していそうなツイートの性質】**
**topic 0**:「配信」「動画」「フェス」　動画配信者のツイートが強く反映  
**topic 1**:「募集」「質問」「一緒」　　フレンドを募集するツイート　  
**topic 2**:「ギア」「キル」「アサリ」　ゲームの内容に関するツイート


次にトピック数最適化のために2つを指標を計算してプロットしていきます。
計算した2つの指標はそれぞれ、 <font color="red">perplexityは低いほど</font>、
<font color="red">coherenceは高いほど</font>、より適切にトピックを分類できていると考えられます。

```perplexity.py
#ldaのトピック数最適化のために"coherence", "perplexity"を計算する
coherence_vals = []
perplexity_vals = []

#調べるトピック数を2-20個に設定
t_num = np.arange(2, 21)

for n_topic in t_num:
    #ldaモデルを作成してperplexityの計算結果を格納
    lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                                id2word=dictionary,
                                                num_topics=n_topic,
                                                random_state=0)
    perplexity_vals.append(np.exp2(-lda_model.log_perplexity(corpus)))
    
    #coherence計算用のモデルを作成して結果を格納
    coherence_model_lda = gensim.models.CoherenceModel(model=lda_model,
                                                       texts=noun_list,
                                                       dictionary=dictionary,
                                                       coherence='u_mass')  #"c_v", "c_uci", "c_npmi"もある
    coherence_vals.append(coherence_model_lda.get_coherence())
```
```plot.py
x = np.arange(2,21)

#プロットするエリアを指定
fig, ax1 = plt.subplots(figsize=(12,5))

#coherenceの設定
color_c = 'darkturquoise'  #カラー指定
ax1.plot(x, coherence_vals, 'o-', color=color_c)
ax1.set_xlabel('Num Topics')
ax1.set_ylabel('Coherence', color=color_c); ax1.tick_params('y', colors=color_c)  #ax1のy軸のカラーを指定

#perplexityの設定
color_p = 'slategray'
ax2 = ax1.twinx()  #ax1に対となる軸を作る
ax2.plot(x, perplexity_vals, 'o-', color=color_p)
ax2.set_ylabel('Perplexity', color=color_p); ax2.tick_params('y', colors=color_p)  #ax2のy軸のカラーを指定

ax1.set_xticks(x)   #x軸の目盛り設定
fig.tight_layout()  #グラフ配置の自動調整
plt.show()
```

![u_mass_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/0589b2e8-b4ae-26e5-477e-aa444aeff11e.png "u_mass_graph")
**"Num Topics"が小さいほど"coherence"が高く、"perplexity"は低い**ことが分かります。  
このグラフからは、"Num Topics"=[2]で設定するのが最適だと推測されます。


```coherence='c_v'.py
#coherence='c_v'で設定した場合

coherence_vals = []
perplexity_vals = []

t_num = np.arange(2, 21)

for n_topic in t_num:
    lda_model = gensim.models.ldamodel.LdaModel(corpus=corpus,
                                                id2word=dictionary,
                                                num_topics=n_topic,
                                                random_state=0)
    perplexity_vals.append(np.exp2(-lda_model.log_perplexity(corpus)))
    
    coherence_model_lda = gensim.models.CoherenceModel(model=lda_model,
                                                       texts=noun_list,
                                                       dictionary=dictionary,
                                                       coherence='c_v')
    coherence_vals.append(coherence_model_lda.get_coherence())
```
```plot.py
x = np.arange(2,21)

#プロットするエリアを指定
fig, ax1 = plt.subplots(figsize=(12,5))

color_c = 'darkturquoise'
ax1.plot(x, coherence_vals, 'o-', color=color_c)
ax1.set_xlabel('Num Topics')
ax1.set_ylabel('Coherence', color=color_c); ax1.tick_params('y', colors=color_c)

color_p = 'slategray'
ax2 = ax1.twinx()
ax2.plot(x, perplexity_vals, 'o-', color=color_p)
ax2.set_ylabel('Perplexity', color=color_p); ax2.tick_params('y', colors=color_p)

ax1.set_xticks(x)
fig.tight_layout()
plt.show()
```

![c_v_graph.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/9c5cf020-05a2-8462-4c54-6c1174005b2c.png "c_v_graph")
**"Num Topics"=3 で "coherence"が一時的に上がっています。**
また"Num Topics"=6 以上からは"coherence"の上昇率は落ち着き、
**"Perplexity"は"Num Topics"=11-12あたりから増加**していることが分かります。

以上より **<font color="red">"Num Topics"=[2, 3, 10]</font>** の3パターンでワードクラウドを出力することとしました。

```n_topic10.py
#再度ldaを学習(n_topic=10)
dictionary = corpora.Dictionary(noun_list)
corpus = [dictionary.doc2bow(noun) for noun in noun_list]
num_topics = 10
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=num_topics, id2word=dictionary)
```
```wordcloud10.py
#wordcloudの作成
fig, axs = plt.subplots(ncols=5, nrows=2, figsize=(15,7))
axs = axs.flatten()

mask = np.array(Image.open("./image/ball.png"))
fpath = "./data/Corporate-Logo-Bold-ver2.otf"

for i, t in enumerate(range(lda.num_topics)):
    x = dict(lda.show_topic(t, 30))
    
    im = WordCloud(
        font_path=fpath,
        background_color="white",
        mask=mask,
        random_state=0
    ).generate_from_frequencies(x)
    
    axs[i].imshow(im)
    axs[i].axis("off")
    axs[i].set_title("Topic "+str(t))

plt.tight_layout()
plt.show()
```

![word_cloud_topic10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/49d21d41-24ad-cc80-ee23-65f2b072e9b2.png "word_cloud_topic10")
**【重要そうなワードと影響していそうなツイートの性質】**
topic 0:「友達」「募集」　　　　　　　フレンド募集のツイート  
topic 1:「バンカラマッチ」「ギア」　　バンカラマッチの募集や質問(？)ツイート  
topic 2:「ウデマエ」「ランク」　　　　プレイによって変動するシステムに関するツイート  
topic 3:「通信」「エラー」　　　　　　システムの問題に関するツイート  
topic 4:「味方」「試合」「武器」　　　ゲームプレイの振り返りツイート  
topic 5:「明日」「台風」　　　　　　　台風に関するツイート、回線に関するツイート  
topic 6:「ヒーローモード」「クリア」　ストーリーモードに関するツイート  
topic 7:「配信」「今日」「仕事」　　　日時に関する情報を含んだツイート  
topic 8:「フェス」「道具」「楽しみ」　フェスに関するツイート  
topic 9:「わかば」「ガロン」「安定」　ブキに関するツイート

```n_topic2.py
#再度ldaを学習(n_topic=2)
dictionary = corpora.Dictionary(noun_list)
corpus = [dictionary.doc2bow(noun) for noun in noun_list]
num_topics = 2
lda = gensim.models.ldamodel.LdaModel(corpus=corpus, num_topics=num_topics, id2word=dictionary)
```
```wordcloud2.py
#wordcloudの作成
fig, axs = plt.subplots(ncols=2, nrows=1, figsize=(15,7))
#axs = axs.flatten()

mask = np.array(Image.open("./image/ball.png"))
fpath = "./data/Corporate-Logo-Bold-ver2.otf"

for i, t in enumerate(range(lda.num_topics)):
    x = dict(lda.show_topic(t, 30))
    
    im = WordCloud(
        font_path=fpath,
        background_color="white",
        mask=mask,
        random_state=0
    ).generate_from_frequencies(x)
    
    axs[i].imshow(im)
    axs[i].axis("off")
    axs[i].set_title("Topic "+str(t))

plt.tight_layout()
plt.show()
```

![word_cloud_topic2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2512226/b2d9e449-891d-96db-41aa-91b42dc93c96.png "word_cloud_topic2")
複数のトピックが入り混じっているように見えます。  
トピックのテーマを推定するのは難しいと感じました。

# 3. 分析の結論と所感
最後に最終的な分析の結論と反省点の整理、
そして分析を実施した所感を自身の整理として残したいと思います。

## 3-1. 結論
最初に立てた分析目的に対しての結論としては以下のようにしました。
**分析目的**：<font color="red">**「Splatoon3」のアクティブユーザーの中で注目されているワードを可視化する」**</font>
**結論**：分析内での**最適なトピック分類は出力結果の目視から、`n_topic=10`の時**であり、
　　　その際の注目ワードは以下のようなものが抽出されていることが確認できた。
- **ブキの性能**　　　　「わかば」、「(52,96)ガロン」
- **フェスイベント**　　「フェス」、「道具(最新のフェスのテーマ)」
- **ストーリーモード**　「ヒーローモード」、「クリア」
- **マッチ内容**　　　　「試合」、「味方」
- **システム**　　　　　「通信」、「エラー」

## 3-2. 考えられる問題点
最終的なワードクラウドの出力から **"関係のないコンテンツのツイート"** 、
**"日常的なツイート"** 、**"twitter特有のユーザー交流リプライ"** の存在が多く確認されました。  
またtopic数が2の場合ではうまくトピックを分類できていないように見えました。
- 対象としたユーザーにアクティブでないユーザーも含まれる  
- 取得したツイートに、コンテンツには関係のない日常のツイートに含まれる  
- テキストクレンジングの精度が不十分
("こと","うち","とこ"などのワード、"サーモンラン" -> "サーモン","ラン"などの分解)  
- そもそものデータ数が不十分であった可能性

## 3-3. 検討する対応
上記の問題点を解決する方法としては、以下のようなものが考えられます。  
やはり**元のデータの取得方法が最も影響力が高い**と思われます。
- LDAの前にクラスタリングを行う  
- 特定のキーワードを自分で複数設定し、それらを含むツイートのみを対象データとする  
- tweetに含まれる一般頻出語句をstopwordとしてリスト化する  
- 公式アカウントへのリプライのみを対象データとする    
- ツイートを自動取得するシステムを作り、データを追加していく

## 3-4. 今後の分析の方向性
今後は上記の対応を含め、別の方向性での分析も候補として考えています。  
- クラスタリングの実施  
- 公式アカウントへのリプライのみを対象としたワードクラウド/感情分析  
- AWSを利用した開発環境演習  
- 定期実行システムを作成した自動データ分析/自動出力用bot作成

## 3-5. 全体を通しての所感
データの取得から出力までを通した分析は今回初めて行いました。  
複雑なモデルを作成して精度を求めるような分析ではありませんでしたが、
全体を通して様々な点で躓きがありました。  

**3-5-1．twitterAPIの利用方法**  
申請は難なく通りましたが、バージョンによる挙動の違いにとても戸惑いました。  
v1で利用できるapiと、v2で利用できるclientでは使えるメソッドにも違いがあり、  
そもそもAPIの利用が初めてだったので、ひたすらドキュメントを確認して進めたような感じです。  

しかし今回の分析でtwitterAPIの利用方法はだいぶ身についたので、とても自信になりました。  

**3-5-2．テキストデータのクレンジング**  
SNSから取得した生のデータは絵文字や顔文字を含むものも多く、  
またネットスラングや特有の文調があり、何度も処理の方法を考え直しました。  

最終的な出力をワードクラウドにしたため、処理の方法はシンプルなものにしましたが、  
のちに感情分析を行う際には再度クレンジングの方法を考える必要がありそうです。  

**3-5-3．教師なし学習/トピックモデルにおける解釈**  
今回はLDAによるトピック抽出をメインに行いましたが、  
正解率や誤差などがない教師なし学習では、出力された結果から自分で解釈をしなければならず、  
どの程度適切な分析ができているのかの判断が難しいと感じました。  

今後このような分析を行う際は、様々な方法での可視化を試したり、  
他の評価方法を調べてみようと思います。

**3-5-4. 所感まとめ**  
データの取得から出力まで、すべて通しての分析はとてもいい経験になりました。  
実際にかけた時間のほとんどはデータの取得から前処理部分の繰り返しで、  
モデル構築以外の重要度、そして難しさを経験できたと思います。

稚拙な文章と分析でしたが、最後まで閲覧頂いた方は本当にありがとうございます。
よろしければ分析に対する提案/アドバイス等をコメント頂けると幸いです。

## 次回の記事

https://qiita.com/kawanago_py/items/68999167adb87001764c#%E6%AC%A1%E5%9B%9E%E3%81%AE%E8%A8%98%E4%BA%8B

## 【参考リンク】
[【python】Twitterで検索する方法](https://web-lh.fromation.co.jp/archives/10000051001)
[形態素解析前の日本語文書の前処理(python) - け日記](https://ohke.hateblo.jp/entry/2019/02/09/141500
)
[LDAによるトピック解析 with Gensim - Qiita](https://qiita.com/Spooky_Maskman/items/0d03ea499b88abf56819
)