scattertextとは
単語出現頻度の散布図を生成できるライブラリです。
各ポイントに重ならないようなラベル付けや、カテゴリと単語の関連度を示す色付けを自動で行ってくれるため、簡単にきれいな散布図を作成することできます。また、作成後は検索窓に単語を検索したりといった動的な操作も可能です。
この記事ではそんなscattertextの実装例と、散布図がどのように作成されるかについて解説します。
実装例
Google Colaboratoryにて、livedoorニュースコーパスの「Sports Watch」と「MOVIE ENTER」の出現単語をscattertextで可視化していきます。
流れ
- 事前準備
- 散布図を作成するデータセットをPandas DataFrameに格納
- spacy、scattertextを使用したDataFrame → Corpusへの変換
- htmlの作成、可視化
事前準備
%%capture
!pip install scattertext
!pip install ginza
!pip install neologdn # データセット作成時に使用
# GiNZAを使用するためのおまじない
import pkg_resources, imp
imp.reload(pkg_resources)
散布図を作成するデータセットをPandas DataFrameに格納
下記のようなtext列(文章)とcategory列から成るDataFrameを準備します。
※ここは本筋ではないので解説は割愛します。
作成用コード
import glob
import os
import pathlib
import tarfile
import neologdn
import pandas as pd
# Livedoorニュースのファイルをダウンロード
! wget "https://www.rondhuit.com/download/ldcc-20140209.tar.gz"
# rawデータ解凍
tar = tarfile.open("./ldcc-20140209.tar.gz", "r:gz")
tar.extractall(".")
tar.close()
# Dataframeの作成
df = pd.DataFrame(columns=["text", "category"])
for file_path in pathlib.Path("./text").glob("**/*.txt"):
f_path = pathlib.Path(file_path)
file_name = f_path.name
category_name = f_path.parent.name
if category_name in ['movie-enter', 'sports-watch']:
# 特殊ファイルはスキップ
if file_name in ["CHANGES.txt", "README.txt", "LICENSE.txt"]:
continue
with open(file_path, "r") as f:
text_all = f.read()
text_lines = text_all.split("\n")
url, time, title, *text = text_lines
# text前処理
text = "".join(text)
text = text.strip()
text = neologdn.normalize(text)
text = text.replace(' ', '')
text = text.replace(' ', '')
df.loc[file_name] = [text,category_name]
# インデックスに使用していたファイル名を削除
df.reset_index(inplace=True,drop=True)
spacy、scattertextを使用したDataFrame → Corpusへの変換
DataFrameができたら、CorpusFromPandas
を使用して、DataFrameをCorpusに変換していきます。
- 引数
nlp
で指定したspacyモデルでDataFrameのtext_col
列の各文章をscapyのDoc型に変換 -
feats_from_spacy_doc
で指定した方法でcategory_col
毎に単語の出現頻度をカウント
というのが挙動イメージです。
今回は品詞の絞り込みを行い、unigramで集計するクラスをデフォルトのst.FeatsFromSpacyDoc
を継承して作成しました。
import scattertext as st
import spacy
from collections import Counter
from itertools import chain
from IPython.display import HTML
# 品詞を絞りこみつつ、unigramの出現回数を集計
class UnigramSelectedPos(st.FeatsFromSpacyDoc):
"""
品詞の絞り込みを行い、unigramをカウント
デフォルトの絞り込み品詞は[固有名詞、名詞、動詞、形容詞、副詞]
"""
def __init__(self,use_pos=['PROPN', 'NOUN', 'VERB', 'ADJ', 'ADV']):
super().__init__()
self._use_pos = use_pos
def get_feats(self, doc):
return Counter([c.lemma_ for c in doc if c.pos_ in self._use_pos])
# Corpusの作成
corpus = (st.CorpusFromPandas(df,
category_col='category',
text_col='text',
nlp = spacy.load("ja_ginza"),
feats_from_spacy_doc=UnigramSelectedPos()
)
.build())
品詞指定+unigram+bigramにしたい場合はこちらのクラスを使用してください。
品詞指定+bigramのクラス
class BigramSelectedPos(st.FeatsFromSpacyDoc):
"""
品詞の絞り込みを行い、unigramとbigramをカウント
デフォルトの絞り込み品詞は[固有名詞、名詞、動詞、形容詞、副詞]
"""
def __init__(self, use_pos=['PROPN', 'NOUN', 'VERB', 'ADJ', 'ADV'], use_lemmas=True):
super().__init__()
self._use_pos = use_pos
self._use_lemmas = use_lemmas
def _get_bigram_feats(self, unigrams):
"""
list型で返すように書き換え。
"""
if len(unigrams) > 1:
return list(map(' '.join, zip(unigrams[:-1], unigrams[1:])))
else:
return list()
def _get_unigram_feats_use(self, sent):
"""
品詞の絞り込みを行う形に書き換え。
"""
unigrams = []
for tok in sent:
if tok.pos_ in self._use_pos:
if tok.ent_type_ in self._entity_types_to_censor:
unigrams.append('_' + tok.ent_type_)
elif tok.tag_ in self._tag_types_to_censor:
unigrams.append(tok.tag_)
elif self._use_lemmas and tok.lemma_.strip():
unigrams.append(self._post_process_term(
tok.lemma_.strip().lower()))
elif tok.lower_.strip():
unigrams.append(
self._post_process_term(tok.lower_.strip()))
return unigrams
def get_feats(self, doc):
"""
品詞の絞り込みにより、実際は存在しないbigramがカウントされることを防ぐため書き換え。
例えば、「今日は晴れ」という文は品詞の絞り込みにより「は」が削除されるが、書き換え
を行わないと「今日 晴れ」という実際は存在しないbigramが集計されてしまう。
"""
ngram_counter = Counter()
for sent in doc.sents:
unigrams_all = self._get_unigram_feats(sent)
unigrams_use = self._get_unigram_feats_use(sent)
bigrams_all = list(self._get_bigram_feats(unigrams_all))
bigrams_use = list(self._get_bigram_feats(unigrams_use))
for element in bigrams_use:
if element not in bigrams_all:
bigrams_use.remove(element)
ngram_counter += Counter(chain(unigrams_use, bigrams_use))
return ngram_counter
htmlの作成、可視化
Corpusができれば、あとはproduce_scattertext_explorer
でhtmlを作成し、表示すれば完成です。各引数の内容についてはコメントを参照ください。
# htmlの作成
html = st.produce_scattertext_explorer(
corpus,
category='sports-watch', # y軸カテゴリ
not_categories = ['movie-enter'], # x軸カテゴリ(複数選択可)
category_name='Sports Watch', # y軸ラベル
not_category_name='MOVIE ENTER', # x軸ラベル
asian_mode=True, # 日本語モード
minimum_term_frequency=50, # 指定より出現回数の多い単語のみをプロット
max_terms=4000 # プロットする最大数
)
# 散布図の表示
HTML(html)
プロットされた各ポイントの位置/色の決定方法
ここからは、散布図がどのように作成されるかについて見ていきます。
上記コードで作成された散布図について、各ポイントの位置/色は共にカテゴリ毎の出現回数の順位が決定要因となります(出現回数ではなく、出現回数の順位)。それぞれ具体的に見ていきます。
ポイントの位置
ポイントの位置はカテゴリ毎の順位に対応します。イメージは下記の通りで、あくまで順位に対応するので、仮に下記の例でAの「りんご」の出現回数が500回だったとしても、順位は変わらず1位であるため、プロットされる位置は同じになります。
ポイントの色
ポイントの色付けは、カテゴリ間の順位差で算出したスコアによって行われます。スコアは下図の要領で算出され、絶対値が大きいほど青/赤が濃くなります。順位差が大きい→一方のカテゴリにのみ使用されている→そのカテゴリの特徴を表す単語、という考え方のようです。なお、マウスを点に置いたときに表示されるツールチップに表示されるscore
にもこのスコアが記載されています。
コードで確認したい方はこちらを参考にしてください。
from scattertext.termscoring.RankDifference import RankDifference
freq_df = corpus.get_term_freq_df()
freq_df["score"] = RankDifference().get_scores(freq_df["sports-watch freq"], freq_df["movie-enter freq"])
freq_df.sort_values("score",ascending=False)
※「Scaled F-Scoreの値による色付けが行われている」旨の記載があるサイトもあったのですが、ソースコードを見ると当該部分がコメントアウトされており、現在は上記の通りscoreの算出、色付けが行われているものと思われます。間違っている場合はご指摘頂けますと幸いです。
感想
簡単に散布図が作成できるので、分析を始める際の一手目として非常に有用だと思いました。また、本記事で紹介できませんでしたが、Corpus
には様々な指標を簡単に計算できるメソッドが用意されている様子なので、こちらも今後活用していきたいなと思いました。
参考
https://github.com/JasonKessler/scattertext
はじめての自然言語処理 第6回 OSS によるテキストマイニング
修正
(2021/4/6)
GiNZAを使用する形に変更しました。事前準備部分と、モデルロード部分spacy.load('ja_ginza')
に変更を加えています。
参考:何もない所から一瞬で、自然言語処理と係り受け解析をライブコーディングする手品を、LTでやってみた話