共起ネットワークのコミュニティ抽出でテキストのバラエティを把握  〜 デジタルとアナログの合わせ技でつぶやきを要約してみる 〜

**2022/10/01** 国語研長単位がVer2.9→3.2にVer.upし、コード記述変更しないとインストールできなくなったのでpip変更／なぜかnlplotの共起ネットワークが以前のコードでは描画されなくなったので、コード変更
***

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/729511be-da72-517d-3cdb-c267e702283e.png)
## はじめに

**テキスト分析は、分析を通じてテキストの訴えをまとめること**です。

テキスト分析のプロセスは
>まず
>　**①対象テキストの質のバラエティを把握**し、
>つぎに
>　**②質の中身を把握**し、
>最後に
>　**③まとめる**

という流れと思います。

今回は、**共起ネットワークで「コミュニティ抽出」した結果がテキストのバラエティ把握に有効か？** を確認したいと思います。

 
## コミュニティって？
コミュニティは、「密に結びついた部分ネットワーク」のことです。
もうすこし詳しくは、「ネットワークにおいて内部の頂点間の結合が密で、外部の頂点との結合が疎である部分ネットワーク」のことです。

分析したいテキストをネットワークに表現した時、これをいくつかのコミュニティに区分します。これを「テキストをバラエティ区分」として利用しようというものになります。

ありがたいことに、[**NLPLOT**](https://pypi.org/project/nlplot/) では コミュニティ抽出が反映された共起ネットワークを描くことができますので、これを利用しました。

## nlplotによる共起ネットワーク
[**NLPLOT**](https://pypi.org/project/nlplot/)の共起ネットワークは、コミュニティの違いがノード色で区分されて描かれます。
テキストのバラエティの違いをノード色で表現してくれていることになります。

また、どの単語がどのコミュニティに区分されたかはデータフレームとして取り出すこともできます。

 
## 実行したこと
>- Google Colabで実行しています。
>- 国語研長単位モデルを利用しています。長単位モデルは言語的特徴の解明を目的としたモデルです。
>- テキストデータは、Excel表形式にまとめられたアンケート等を想定し、csvデータとしています。適用データは [**以前の記事**](https://qiita.com/hima2b4/items/7125b15ad76b2f5c38e3) 同様、tweetデータ（csv）です。（※「再エネ」でtweet検索したつぶやきを [**ついすぽ**](https://chrome.google.com/webstore/detail/%E3%81%A4%E3%81%84%E3%81%99%E3%81%BD-tweet-export/imjojcgefiinokaclmifakpphkfeiddg?hl=ja) でcsvにエクスポートしたデータです。）

https://chrome.google.com/webstore/detail/ついすぽ-tweet-export/imjojcgefiinokaclmifakpphkfeiddg?hl=ja


## コミュニティを見てみる
nlplot の共起ネットワークは、min_edge_frequencyでノードの数を制限することができます。

コミュニティをテキストのバラエティを把握することに利用するなら、コミュニティの数は20前後までがありがたい。

先の通り、min_edge_frequencyの値を大きくする＝ノードの数を制限→ 共起ネットワークの密度は下がり、コミュニティ抽出数も下がりますので、これを調整しました。

以下は、min_edge_frequency=3 とした時の共起ネットワークです。
![8873C824-207D-4782-96C2-FA5D854E4B9A.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/1329a40f-afb4-4943-8d2f-6a216048c5f6.jpeg)
以下はコミュニティリストです。0-9の10通りとなりました。
![B532BEF5-62B6-4F1D-A551-F5E663EFA657.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/97cc2149-1396-69bd-a386-d0356666d900.jpeg)
コミュニティを認識しやすくするため、共起ネットワークとコミュニティリストを眺めながら、コミュニティ毎にタグ的タイトルを付けてみました。

>- 0：再エネは混沌のなかにあり
>- 1：日本の電力課題
>- 2：風力発電は悪？！
>- 3：新電力ピンチ
>- 4：太陽光も火力も必要？！
>- 5：太陽光と賦課金
>- 6：再エネ開発事情
>- 7〜9：省略

コミュニティの形は、さまざまです。
大きく、①あるノードから放射状にパスが伸び独立した他のノードに繋がっているコミュニティ、②周辺ノードがパスで繋がっているコミュニティがあり、②のほうが可読性が高く感じました。

例えるなら、①は「あるテーマは、あれにもこれにもそれにも関係している」、②は「あるテーマは、あれとこれとそれによって」という感じです。（わかりにくくてすいません）

①の場合、次の原文検索では、いくつかの視点（あれでもこれでもそれでもというOR検索）で見たほうがよいですし、②の場合は、あれとこれのAND検索でよいと思います。


## 原文検索でコミュニティの状況を探る
ざっくりではありますが、テキストの全体感を掴みましたので、この全体感をベースにコミュニティ毎に原文検索を行いました。
![B8798B93-E4E5-4346-B368-2C7664C6A090.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/4c3f82cf-cc58-b63f-9f58-aa1a361982b7.jpeg)

以下がコミュニティ毎にピックアップしたつぶやきです。（簡易要約しています。）

- コミュニティ0
>- 1 「再エネなら何をしてもいいのか？（三村知事）」
>- 2 再エネは気候に左右されるエネルギーだ。
>- 3 エネルギー政策は、他国を倣うのではなく、日本固有の事情を踏まえる必要がある。
>- 4 経済的に安定的な供給には、石炭や原子力が必要だ。
>- 5 カーボンニュートラル達成には、再エネも原発も蓄電池もEVも送電網の強化も水素も炭素回収も。切り札はない。

- コミュニティ1
>- 6 佐賀のマイクロ発電、火力でCO2を90％回収するCCUSなど、日本には素晴らしい技術もある。
>- 7 台風の影響を受ける洋上風力は日本に不向きだ。利権優先ではないか？
>- 8 メガソーラーは自然破壊、安全保障上も問題。

- コミュニティ2
>- 9 宮城・北海道は環境を破壊する風力発電計画を断念。政府は再エネ拡大策を見直すべき。

- コミュニティ3
>- 10 再エネ新電力が倒産のピンチ。

- コミュニティ4
>- 11 再エネ推進による太陽光導入コストは国民負担。火力は稼働率下がり、電力不足が常態化。

- コミュニティ5
>- 12 再エネ賦課金＝太陽光は国民負担増えるばかり。

- コミュニティ6
>- 13 東北山間部の再エネ開発（太陽光）は東京ドーム1200個分。

## 取り上げたつぶやきを要約してみる
ここからはアナログです。機械は、つぶやきの心持ちまでは探ってくれませんので、コミュニティ毎に抽出した全13ツイート（つぶやき）を親和性の面から要約してみました。
>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/12b1b17c-cee1-594c-0d26-d17f15e99bfd.png)
※各ラベルに記載した数値（1-13）はコミュニティ毎に抽出した全13ツイート（つぶやき）の番号です。親和性の高いつぶやきを集めてラベルに要約（①）し、さらに親和性の高いラベル（①）を集めて要約（②）、空間配置し、関係性を矢印でつなぐ等して図解化しています。


　
上記のように図解化まで完了しましたので、最後に文章化しました。

> **カーボンニュートラル達成に向けての政策は、特定の切り札に固執したものではなく、生活を脅かさぬ 多面的な日本独自の状況をベースとした政策の実行が望まれている。再エネ推進は 負担増・不足・破壊 にもつながっている面があること、再エネに対する偏った政策の影響もあり、イメージが悪化傾向にあるためである。**

　

コミュニティ抽出（の活用）はいいですね。
テキストのバラエティ区分を補佐してくれますので、全体理解までを効率的に進めることができました。
実務では、なんだかんだと時間が取れず、共起ネットワークを描いて終わり。。。ということもありましたが、時間が取れなかったのではなく、全体理解までをどう進めるかで詰まっていたように思います。

このアプローチならば、声として多いものだけではなく、少数意見も反映させることができます。
ただ、コミュニティ数を絞りすぎないようにしないといけないなと思います。


とはいえ、再エネの世界観は深いですね。
ある一面だけを取り上げて、賛成か、反対か、ということではなく、より広い世界観を眺めないといけないなとあらためて感じました。いまは過渡期、混乱の最中にありますが、よりよい未来が日本の技術でつながることを期待したいですね。


## 最後に
これまでに、いくつかの方法でテキストのバリエーション把握に取り組みましたが、個人的には、今回の[**NLPLOT**](https://pypi.org/project/nlplot/)のコミュニティ抽出を活用する方法が一番しっくりきました。

テキストのバラエティ把握と原文の確認を通して、**テーマの全体感を把握**し、把握した全体感から見た時に「これは欠かせないな」と思えるコメントをバラエティ毎にピックアップ、ピックアップしたコメントで狭義KJ法を進める・・・ということがストレスなく進められたからです。

**テーマの全体感** の把握はとても重要と思います。
全体感を把握しないと、分析において逃しちゃいけないコメントがつかめないということになりますし、つかめないと最終的なまとめも不十分な内容となりえるからです。

機械的なテキスト分析でえられる結果は、あくまで手がかり。ただ、すべてマニュアルで実行するのは大変です。
**デジタルとアナログの融合** をうまく図って、よりよい分析結果を導けるようになりたい。。。
結局、最終的なまとめは分析者の力量にかかっている・・・あらためて、テキスト分析は深いなと思います。


## 実行コード

### GoogleColabで実行するためのライブラリのインストール
以下のライブラリや辞書をインストールしました。

```Python:sudachipy, GINZAのインストール
!pip install sudachipy sudachidict_core
!pip install "https://github.com/megagonlabs/ginza/releases/download/latest/ginza-latest.tar.gz"
```
```Python:「国語研長単位」モデルのインストール
#!pip install ja_gsdluw deplacy -f https://github.com/megagonlabs/UD_Japanese-GSD/releases/tag/r2.9-NE
!pip install https://github.com/megagonlabs/UD_Japanese-GSD/releases/download/r2.9-NE/ja_gsdluw-3.2.0-py3-none-any.whl
```
```Python:GoogleColabへのGINZAインストール時のランライム再起動を不要にするおまじない
import pkg_resources, imp
imp.reload(pkg_resources)
```
このおまじないがないと、[**GiNZA**](https://pypi.org/project/ginza/)インストール後にランタイムの再起動が求められますが、このおまじないの実行により、インストールするだけ（＝ランタイムの再起動は不要）になります。

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

### データ読込み
まず、データの読込みです。
データの読込みにおいては、GoogleColabのフォーム機能を利用し、データセットを選択できるようにしています。
選択肢は2つ。tweetサンプルデータ（tweet_data_sampleを選択）と、任意のcsvファイル（Uploadを選択）です。
※tweet_data_sampleは、Githubにアップしたこの記事の適用データです。

```Python:データセット選択
#@title Select_Dataset { run: "auto" }
#@markdown  **<font color= "Crimson">注意</font>：かならず 実行する前に 設定してください。**</font>

dataset = 'tweet_data_sample' #@param ['tweet_data_sample','Upload']
```

以下を実行すると、datasetの選択に沿ったデータが読み込まれ、読み込んだデータをデータフレームに反映し、表示します。

```Python:データ読込み
#@title データ読込み
import pandas as pd
from google.colab import files

# データの読み込み
if dataset =='Upload':
  uploaded = files.upload()#Upload
  target = list(uploaded.keys())[0]
  df = pd.read_csv(target,encoding='utf-8')

elif dataset == 'tweet_data_sample':
  file_url ='https://raw.githubusercontent.com/hima2b4/Natural-language-processing/main/TwExport_20220806_152219.csv'
  df = pd.read_csv(file_url,encoding='utf-8')

display(df)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/8ccfe902-a38c-c48d-6aea-e2e4001c8e57.png)

次に、テキスト処理するカラムを指定します。
これもGoogleColabのフォーム機能を利用し、カラム名を入力することで指定できるようにしています。
今回のデータの場合、「テキスト」カラムが対象となりますので、テキストと入力しています。
```Python:意見カラム名の入力
#@title 意見カラム名の入力 { run: "auto" }

column_name = 'テキスト' #@param {type:"raw"}
```


### テキスト処理
つぎは、テキスト処理です。
テキストから不要語を取りのぞく前処理を行った後、テキスト処理を実行して単語に分解、指定した品詞の単語を取り出し、stop_wordsに指定した単語があれば除外します。
その後、行毎に取り出した単語をデータフレームに格納しています。
品詞とstop_wordsの指定は、GoogleColabのフォーム機能を利用しています。
フォーム機能を利用するとコード操作が不要となりますので、とても便利です。

```Python:品詞とストップワード指定
#@title 使用する品詞とストップワードの指定
#@markdown  ※include_pos：使用する品詞，stopwords：表示させない単語 ← それぞれ任意に追加と削除が可能

include_pos = ('NOUN', 'PROPN', 'VERB', 'ADJ')#@param {type:"raw"}
stop_words = ('する', 'ある', 'ない', 'いう', 'もの', 'こと', 'よう', 'なる', 'ほう', 'いる', 'くる', 'お', 'つ', 'とき','ところ', '為', '他', '物', '時', '中', '方', '目', '回', '事', '点', 'ため') #@param {type:"raw"}
```

```Python:テキストの前処置と分かち書き
#@title テキスト処理実行
import spacy
import pandas as pd
import re

# 意見に空行がある場合は削除
df[column_name] = df[column_name].replace('\n+', '\n', regex=True)

# テキストデータの前処理
def text_preprocessing(text):
   # 改行コード、タブ、スペース削除
   text = ''.join(text.split())
   # URLの削除
   text = re.sub(r'https?://[\w/:%#\$&\?\(\)~\.=\+\-]+', '', text)
   # メンション除去 
   text = re.sub(r'@([A-Za-z0-9_]+)', '', text) 
   # 記号の削除
   text = re.sub(r'[!"#$%&\'\\\\()*+,-./:;<=>?@[\\]^_`{|}~「」〔〕“”〈〉『』【】＆＊・（）＄＃＠。、？！｀＋￥]', '', text)

   return text

df[column_name] = df[column_name].map(text_preprocessing)

#モデルをja_gsdluw（国語研長単位）に
nlp = spacy.load("ja_gsdluw")

# 行毎に出現する単語をリストに追加
words_list=[]  #行毎の単語リスト
for doc in nlp.pipe(df[column_name]):
    sep_word=[token.lemma_ for token in doc if token.pos_ in include_pos and token.lemma_ not in stop_words]
    words_list.append(sep_word)
    #print(word_list)

#行毎の意見 → 単語に分解し、カラムに格納
df['separate_words']= [s for s in words_list]

# 参考:対象カラムに形態素解析実行、すべての形態素結果をseparateカラムに格納
#df['separate'] = [list(nlp(s)) for s in df[column_name]

```


### ③ 共起ネットワーク
```Python:
#@title 共起ネットワーク
#@markdown  **<font color= "Crimson">ガイド</font>：ネットワーク表示の濃淡は min_edge_frequency で変更できます。表示にみにくさを感じた場合は値を大きく、より細かく表示したい場合は値を小さくしてください。**

min_edge_frequency = 1 #@param {type:"slider", min:1, max:20, step:1}

import nlplot
import plotly
from plotly.subplots import make_subplots
from plotly.offline import iplot

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

npt.build_graph(
    #stopwords=stopwords,
    min_edge_frequency=min_edge_frequency)
# The number of nodes and edges to which this output is plotted.
# If this number is too large, plotting will take a long time, so adjust the [min_edge_frequency] well.
# >> node_size:70, edge_size:166
fig_co_network = npt.co_network(
    title='Co-occurrence network',
    sizing=100,
    node_size='adjacency_frequency',
    color_palette='hls',
    width=1100,
    height=700,
    save=False
)
iplot(fig_co_network)

display_community_list = True #@param {type:"boolean"}

if display_community_list == True:
  df_npt = npt.node_df
  df_npt.drop(columns=['id_code','betweeness_centrality','clustering_coefficient'],inplace=True)
  df_npt = df_npt.sort_values(['community','adjacency_frequency'],ascending=[True, False])
  df_=pd.DataFrame(df_npt.groupby('community')['id'].apply(list))
  pd.set_option('display.max_colwidth', None)
  display(df_)
```
[**NLPLOT**](https://pypi.org/project/nlplot/)は、ノードが色分けされたネットワーク図が描かれます。（※min_edge_frequency = 2のときの共起ネットワーク）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/daf255e4-03a4-292e-692b-368ffaa2538b.png)
ノード色の違い＝コミュニティの違い（**community**）です。どの単語がどの**community**に区分されたかは、データフレームとして取り出すことができますので、取り出してみました。
![2A43F3AD-8091-4441-AD0B-0DEF638D7F7B.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1275001/6ed2c38f-570c-f3fb-a65d-690ad6ae63e7.jpeg)
communityの単語一覧を共起ネットワークと照らし合わせてみるとより理解が進みました。

また、edge（共起関係の強い単語の組合せ）もデータフレームとして取り出すことができますので、こちらも取り出せるようにしています。

```Python:
#@title edge 表示（共起関係の強い単語の組合せリスト）

display_edge_frequency_list = True #@param {type:"boolean"}

if display_edge_frequency_list == True:
  display(npt.edge_df)
```

　
**nlplotが表示するデータフレームカラムに関するメモ**
- adjacency_frequency　　：エッジ数
- betweenness_centrality　：媒介中心性 （さまざまなネットワークにおいて、他の頂点どうしの間にあって、それらをつなぐ働きをする頂点を見出そうとする中心性座標。つまり、他の頂点同士をつなぐ最短距離上に位置する頂点は、頂点間の仲介や情報のコントロールが可能な点で有力であり、より多くの頂点間の最短距離上にあるほど影響力が大きいと考える。）
- community　　　　　　：コミュニティ
- edge_frequency　　　　：共起の強さ
- clustering_coefficient　　：あるノードからみて、それと隣接するノードからなるサブグラフを考え、その密度（つまり、ありうるエッジの数に占める実際のエッジの数の割合）。

※ まずは、影響力の大きさを示す媒介中心性betweenness_centralityの値が大きい単語を検索するのがよさそうだ。この時、エッジの数を示すadjacency_frequencyの値が大きいほど、他の単語と関連があるということになるが、併せてクラスタ係数clustering_coefficientも見ておいた方がいい。この値が1のものは完全グラフなので、いわばネットワークが閉じているのであれこれ見なくても文脈が掴みやすいといえる。逆にこの値が小さいものはネットワークは閉じていないので、あれこれ見ないと文脈は掴みにくいといえる。

※ また、kamada_kawai という 頂点を力学モデルにより配置したネットワーク可視化モデル が利用されている感じです。

## 参考

https://qiita.com/hima2b4/items/54f734489ff8f4a13d07

https://qiita.com/hima2b4/items/23a1dfad16321c7f23ae

https://www.amazon.co.jp/Pythonで学ぶネットワーク分析-ColaboratoryとNetworkXを使った実践入門-村田-剛志/dp/4274224252

https://stars.repo.nii.ac.jp/index.php?action=pages_view_main&active_action=repository_action_common_download&item_id=8915&item_no=1&attribute_id=22&file_no=1&page_id=13&block_id=67

https://ang.tokyo/get_tweets/

https://www.anlp.jp/proceedings/annual_meeting/2008/pdf_dir/PC3-1.pdf