LoginSignup
10
13

More than 3 years have passed since last update.

scattertextで文章の単語出現頻度を可視化

Last updated at Posted at 2021-04-04

scattertextとは

単語出現頻度の散布図を生成できるライブラリです。
各ポイントに重ならないようなラベル付けや、カテゴリと単語の関連度を示す色付けを自動で行ってくれるため、簡単にきれいな散布図を作成することできます。また、作成後は検索窓に単語を検索したりといった動的な操作も可能です。
この記事ではそんなscattertextの実装例と、散布図がどのように作成されるかについて解説します。

実装例

Google Colaboratoryにて、livedoorニュースコーパスの「Sports Watch」と「MOVIE ENTER」の出現単語をscattertextで可視化していきます。
image.png

流れ

  • 事前準備
  • 散布図を作成するデータセットを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を準備します。
※ここは本筋ではないので解説は割愛します。
image.png

作成用コード
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)

(再掲)
image.png

プロットされた各ポイントの位置/色の決定方法

ここからは、散布図がどのように作成されるかについて見ていきます。
上記コードで作成された散布図について、各ポイントの位置/色は共にカテゴリ毎の出現回数の順位が決定要因となります(出現回数ではなく、出現回数の順位)。それぞれ具体的に見ていきます。

ポイントの位置

ポイントの位置はカテゴリ毎の順位に対応します。イメージは下記の通りで、あくまで順位に対応するので、仮に下記の例でAの「りんご」の出現回数が500回だったとしても、順位は変わらず1位であるため、プロットされる位置は同じになります。

image.png

ポイントの色

ポイントの色付けは、カテゴリ間の順位差で算出したスコアによって行われます。スコアは下図の要領で算出され、絶対値が大きいほど青/赤が濃くなります。順位差が大きい→一方のカテゴリにのみ使用されている→そのカテゴリの特徴を表す単語、という考え方のようです。なお、マウスを点に置いたときに表示されるツールチップに表示されるscoreにもこのスコアが記載されています。
image.png
コードで確認したい方はこちらを参考にしてください。

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でやってみた話

10
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
13