!date
!python --version
Thu Apr 17 08:23:30 AM UTC 2025
Python 3.11.12
更新ログ
2025年4月17日: 描画方法をiframe方式に変更。
25. 特徴的な単語の抽出#
ある文書における特徴的な単語とは何だろうか。様々な指標が提案されているが、基本的には (1) 何か特徴を設定し、(2) その重要度を求め、(3) ランキングすることで求める事が多い。最もシンプルなアプローチは (1) 単語毎に、(2) 出現頻度を求め、 (3) 頻出上位を特徴的な単語と捉える方法だ。ここではワードクラウド形式で眺める例と、2文書間の出現頻度分布を眺める例を観察してみよう。
25.1. required#
spacy, sklearn
wordcloud:
pip install wordcloud
scattertext:
pip install scattertext
# spacy, ginzaインストール
!pip install -U ginza ja_ginza scattertext pandas
#!pip install scattertext pandas
Requirement already satisfied: ginza in /usr/local/lib/python3.11/dist-packages (5.2.0)
Requirement already satisfied: ja_ginza in /usr/local/lib/python3.11/dist-packages (5.2.0)
Requirement already satisfied: scattertext in /usr/local/lib/python3.11/dist-packages (0.2.2)
Requirement already satisfied: pandas in /usr/local/lib/python3.11/dist-packages (2.2.3)
Requirement already satisfied: spacy<4.0.0,>=3.4.4 in /usr/local/lib/python3.11/dist-packages (from ginza) (3.8.5)
Requirement already satisfied: plac>=1.3.3 in /usr/local/lib/python3.11/dist-packages (from ginza) (1.4.5)
Requirement already satisfied: SudachiPy<0.7.0,>=0.6.2 in /usr/local/lib/python3.11/dist-packages (from ginza) (0.6.10)
Requirement already satisfied: SudachiDict-core>=20210802 in /usr/local/lib/python3.11/dist-packages (from ginza) (20250129)
Requirement already satisfied: numpy>=1.2.6 in /usr/local/lib/python3.11/dist-packages (from scattertext) (1.26.4)
Requirement already satisfied: scipy<1.14.0,>=1.7.0 in /usr/local/lib/python3.11/dist-packages (from scattertext) (1.13.1)
Requirement already satisfied: scikit-learn>=1.4 in /usr/local/lib/python3.11/dist-packages (from scattertext) (1.6.1)
Requirement already satisfied: statsmodels>=0.14.1 in /usr/local/lib/python3.11/dist-packages (from scattertext) (0.14.4)
Requirement already satisfied: flashtext>=2.7 in /usr/local/lib/python3.11/dist-packages (from scattertext) (2.7)
Requirement already satisfied: gensim>=4.0.0 in /usr/local/lib/python3.11/dist-packages (from scattertext) (4.3.3)
Requirement already satisfied: tqdm>=4.0 in /usr/local/lib/python3.11/dist-packages (from scattertext) (4.67.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.11/dist-packages (from pandas) (2.8.2)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.11/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.11/dist-packages (from pandas) (2025.2)
Requirement already satisfied: smart-open>=1.8.1 in /usr/local/lib/python3.11/dist-packages (from gensim>=4.0.0->scattertext) (7.1.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.11/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.4->scattertext) (1.4.2)
Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.11/dist-packages (from scikit-learn>=1.4->scattertext) (3.6.0)
Requirement already satisfied: spacy-legacy<3.1.0,>=3.0.11 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (3.0.12)
Requirement already satisfied: spacy-loggers<2.0.0,>=1.0.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (1.0.5)
Requirement already satisfied: murmurhash<1.1.0,>=0.28.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (1.0.12)
Requirement already satisfied: cymem<2.1.0,>=2.0.2 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (2.0.11)
Requirement already satisfied: preshed<3.1.0,>=3.0.2 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (3.0.9)
Requirement already satisfied: thinc<8.4.0,>=8.3.4 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (8.3.4)
Requirement already satisfied: wasabi<1.2.0,>=0.9.1 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (1.1.3)
Requirement already satisfied: srsly<3.0.0,>=2.4.3 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (2.5.1)
Requirement already satisfied: catalogue<2.1.0,>=2.0.6 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (2.0.10)
Requirement already satisfied: weasel<0.5.0,>=0.1.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (0.4.1)
Requirement already satisfied: typer<1.0.0,>=0.3.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (0.15.2)
Requirement already satisfied: requests<3.0.0,>=2.13.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (2.32.3)
Requirement already satisfied: pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (2.11.3)
Requirement already satisfied: jinja2 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (3.1.6)
Requirement already satisfied: setuptools in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (75.2.0)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (24.2)
Requirement already satisfied: langcodes<4.0.0,>=3.2.0 in /usr/local/lib/python3.11/dist-packages (from spacy<4.0.0,>=3.4.4->ginza) (3.5.0)
Requirement already satisfied: patsy>=0.5.6 in /usr/local/lib/python3.11/dist-packages (from statsmodels>=0.14.1->scattertext) (1.0.1)
Requirement already satisfied: language-data>=1.2 in /usr/local/lib/python3.11/dist-packages (from langcodes<4.0.0,>=3.2.0->spacy<4.0.0,>=3.4.4->ginza) (1.3.0)
Requirement already satisfied: annotated-types>=0.6.0 in /usr/local/lib/python3.11/dist-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy<4.0.0,>=3.4.4->ginza) (0.7.0)
Requirement already satisfied: pydantic-core==2.33.1 in /usr/local/lib/python3.11/dist-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy<4.0.0,>=3.4.4->ginza) (2.33.1)
Requirement already satisfied: typing-extensions>=4.12.2 in /usr/local/lib/python3.11/dist-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy<4.0.0,>=3.4.4->ginza) (4.13.1)
Requirement already satisfied: typing-inspection>=0.4.0 in /usr/local/lib/python3.11/dist-packages (from pydantic!=1.8,!=1.8.1,<3.0.0,>=1.7.4->spacy<4.0.0,>=3.4.4->ginza) (0.4.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.11/dist-packages (from requests<3.0.0,>=2.13.0->spacy<4.0.0,>=3.4.4->ginza) (3.4.1)
Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.11/dist-packages (from requests<3.0.0,>=2.13.0->spacy<4.0.0,>=3.4.4->ginza) (3.10)
Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.11/dist-packages (from requests<3.0.0,>=2.13.0->spacy<4.0.0,>=3.4.4->ginza) (2.3.0)
Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.11/dist-packages (from requests<3.0.0,>=2.13.0->spacy<4.0.0,>=3.4.4->ginza) (2025.1.31)
Requirement already satisfied: wrapt in /usr/local/lib/python3.11/dist-packages (from smart-open>=1.8.1->gensim>=4.0.0->scattertext) (1.17.2)
Requirement already satisfied: blis<1.3.0,>=1.2.0 in /usr/local/lib/python3.11/dist-packages (from thinc<8.4.0,>=8.3.4->spacy<4.0.0,>=3.4.4->ginza) (1.2.1)
Requirement already satisfied: confection<1.0.0,>=0.0.1 in /usr/local/lib/python3.11/dist-packages (from thinc<8.4.0,>=8.3.4->spacy<4.0.0,>=3.4.4->ginza) (0.1.5)
Requirement already satisfied: click>=8.0.0 in /usr/local/lib/python3.11/dist-packages (from typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (8.1.8)
Requirement already satisfied: shellingham>=1.3.0 in /usr/local/lib/python3.11/dist-packages (from typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (1.5.4)
Requirement already satisfied: rich>=10.11.0 in /usr/local/lib/python3.11/dist-packages (from typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (13.9.4)
Requirement already satisfied: cloudpathlib<1.0.0,>=0.7.0 in /usr/local/lib/python3.11/dist-packages (from weasel<0.5.0,>=0.1.0->spacy<4.0.0,>=3.4.4->ginza) (0.21.0)
Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.11/dist-packages (from jinja2->spacy<4.0.0,>=3.4.4->ginza) (3.0.2)
Requirement already satisfied: marisa-trie>=1.1.0 in /usr/local/lib/python3.11/dist-packages (from language-data>=1.2->langcodes<4.0.0,>=3.2.0->spacy<4.0.0,>=3.4.4->ginza) (1.2.1)
Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.11/dist-packages (from rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (3.0.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.11/dist-packages (from rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (2.18.0)
Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.11/dist-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer<1.0.0,>=0.3.0->spacy<4.0.0,>=3.4.4->ginza) (0.1.2)
25.2. 利用ライブラリの用意、データセット準備#
事前に、load_r_assesment.ipynb でデータセットを作成し、pkl形式でファイル保存(r_assesment.pkl)しておく。今回は作成済みファイルをダウンロードして利用することにする。
r_assesment.pklは授業評価アンケートの自由記述欄をpd.DataFrame形式で保存したもので、授業名(title)、学年(grade)、必修か否か(required)、質問番号(q_id)、コメント(comment)で構成される。
!curl -O https://ie.u-ryukyu.ac.jp/~tnal/2022/dm/static/r_assesment.pkl
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 34834 100 34834 0 0 8497 0 0:00:04 0:00:04 --:--:-- 8498
import collections
import numpy as np
import pandas as pd
import spacy
nlp = spacy.load("ja_ginza")
assesment_df = pd.read_pickle('r_assesment.pkl')
assesment_df.head()
title | grade | required | q_id | comment | |
---|---|---|---|---|---|
0 | 工業数学Ⅰ | 1 | True | Q21 (1) | 特になし |
1 | 工業数学Ⅰ | 1 | True | Q21 (2) | 正直わかりずらい。むだに間があるし。 |
2 | 工業数学Ⅰ | 1 | True | Q21 (2) | 例題を取り入れて理解しやすくしてほしい。 |
3 | 工業数学Ⅰ | 1 | True | Q21 (2) | 特になし |
4 | 工業数学Ⅰ | 1 | True | Q21 (2) | スライドに書く文字をもう少しわかりやすくして欲しいです。 |
25.3. (何故かみんな大好き) ワードクラウド#
分かち書きした文章を用意し、最大フォントサイズや画像サイズを指定するぐらいで作成可能。
wordcloudで日本語を扱う場合、フォント指定が必要。OS毎にフォントの場所が異なるので「Windows wordcolud 日本語」のようにググってみよう。
# 分かち書き
assesment_df['wakati'] = ''
for index, comment in enumerate(assesment_df['comment']):
doc = nlp(comment)
wakati_words = []
for token in doc:
wakati_words.append(token.lemma_)
wakati_text = ' '.join(wakati_words)
assesment_df.at[index, 'wakati'] = wakati_text
assesment_df
title | grade | required | q_id | comment | wakati | |
---|---|---|---|---|---|---|
0 | 工業数学Ⅰ | 1 | True | Q21 (1) | 特になし | 特に なし |
1 | 工業数学Ⅰ | 1 | True | Q21 (2) | 正直わかりずらい。むだに間があるし。 | 正直 わかる ずらい 。 むだ だ 間 が ある し 。 |
2 | 工業数学Ⅰ | 1 | True | Q21 (2) | 例題を取り入れて理解しやすくしてほしい。 | 例題 を 取り入れる て 理解 する やすい する て ほしい 。 |
3 | 工業数学Ⅰ | 1 | True | Q21 (2) | 特になし | 特に なし |
4 | 工業数学Ⅰ | 1 | True | Q21 (2) | スライドに書く文字をもう少しわかりやすくして欲しいです。 | スライド に 書く 文字 を もう 少し わかる やすい する て 欲しい です 。 |
... | ... | ... | ... | ... | ... | ... |
165 | データマイニング | 3 | False | Q22 | 課題が難しいものが多く、時間を多くとってもらえたのは非常に良かったですがかなりきつかったです... | 課題 が 難しい もの が 多い 、 時間 を 多い とる て もらえる た の は 非常 ... |
166 | ICT実践英語Ⅰ | 3 | False | Q22 | オンラインなどで顔を合わせてやりたかったです。 | オンライン など で 顔 を 合わせる て やる たい た です 。 |
167 | 知能情報実験Ⅲ | 3 | True | Q21 (2) | unityの操作方法の説明などを最初に行ってもらえたらもう少しスムーズにできたのではないかと思う。 | unity の 操作方法 の 説明 など を 最初 に 行く て もらえる た もう 少し ... |
168 | 知能情報実験Ⅲ | 3 | True | Q22 | それぞれに任せるといった形で進められたものだったのでそれなりに進めやすかったですが、オンライ... | それぞれ に 任せる と いう た 形 で 進める られる た もの だ た の だ それ ... |
169 | 知能情報実験Ⅲ | 3 | True | Q22 | モバイルアプリ班\r\nHTML/CSS,JavaScriptなどを用いてアプリケーションを... | モバイルアプリ 班 \r\n HTML / CSS , javascript など を 用い... |
170 rows × 6 columns
# フォントのインストールと設定
!apt-get -y install fonts-ipafont-gothic
font_path = '/usr/share/fonts/truetype/fonts-japanese-mincho.ttf'
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
fonts-ipafont-gothic is already the newest version (00303-21ubuntu1).
0 upgraded, 0 newly installed, 0 to remove and 30 not upgraded.
from wordcloud import WordCloud
#font_path = '/Library/Fonts/Arial Unicode' # macOSでデフォルトであると思われるフォント
#wc = WordCloud(background_color='white', font_path=font_path, max_font_size=100, width=1000, height=500).generate(' '.join(assesment_df['wakati']))
wc = WordCloud(background_color='white', font_path=font_path, max_font_size=100, width=1000, height=500).generate(' '.join(assesment_df['wakati']))
wc.to_image()

25.4. scattertextによる2文書の傾向比較#
scattertextは、2つの文書(もしくは2つの文書集合)の違いを単語出現分布から観察するのに適した可視化ツールだ。対比させるという点が重要であり、そうではないタスク、例えばある文書を要約する(重要語を抽出する)というタスクには向いていない。対比する文書は1文書単位でも良いし、複数文書でも構わない。
なお、3種類以上を同時に比較することはできない。もしそのような場合に用いたいのであれば、例えば「文書1とそれ以外」「文書2とそれ以外」のように one-vs-rest を複数回実行すると良いだろう。
以下では、授業毎のコメント数上位2科目を比較対象とし、以下の手順で描画する。
上位2科目の dataframe を用意する。
コメントの spacy.nlp 解析結果(Doc形式)を用意する。
コメント文そのものや分かち書き結果ではなく、Doc型を用意する必要がある。
dataframe と列を指定して scattertext に処理してもらう。
25.4.1. 前処理なし#
# 授業毎のコメント数上位を確認
assesment_df['title'].value_counts()
count | |
---|---|
title | |
コンピュータシステム | 32 |
プログラミングⅠ | 19 |
技術者の倫理 | 18 |
工業数学Ⅰ | 16 |
アルゴリズムとデータ構造 | 15 |
データサイエンス基礎 | 15 |
プログラミング演習Ⅰ | 13 |
工学基礎演習 | 12 |
プロジェクトデザイン | 9 |
情報ネットワークⅠ | 7 |
情報処理技術概論 | 7 |
知能情報実験Ⅲ | 3 |
ディジタル回路 | 1 |
キャリアデザイン | 1 |
データマイニング | 1 |
ICT実践英語Ⅰ | 1 |
# 上位2科目のみの dataframe を用意。
# (1) 比較対象をカテゴリ名として保存している列(以下では new_df['title'])と、
# (2) 処理対象となる文書(以下では new_df['comment'])を保存すること。
title1 = 'コンピュータシステム'
title2 = 'プログラミングⅠ'
condition1 = assesment_df['title'] == title1
condition2 = assesment_df['title'] == title2
new_df = assesment_df[condition1 | condition2].loc[:,['title', 'comment']]
# コメント文の nlp 解析結果を用意し、new_df に新しい列として保存する。
# new_df['doc'] の中は丸括弧付きで分かち書きされているように出力されるが、中身はDoc形式である点に注意。
docs = []
for comment in new_df['comment']:
doc = nlp(comment)
docs.append(doc)
new_df['doc'] = docs
new_df.head()
title | comment | doc | |
---|---|---|---|
46 | プログラミングⅠ | 特になし | (特に, なし) |
47 | プログラミングⅠ | たまに説明がないコードがあったりしたので少し戸惑った。いずれはやっていくものではあるが、、、 | (たまに, 説明, が, ない, コード, が, あっ, たり, し, た, の, で, 少... |
48 | プログラミングⅠ | できれば、対面を増やして欲しい | (できれ, ば, 、, 対面, を, 増やし, て, 欲しい) |
49 | プログラミングⅠ | 特になし | (特に, なし) |
50 | プログラミングⅠ | 他人の課題を変更できてしまうのが怖い。 | (他人, の, 課題, を, 変更, でき, て, しまう, の, が, 怖い, 。) |
import scattertext as st
# 用意したdataframeと、比較対象カテゴリを保存している列(title)、Docを保存している列(doc)を指定。
corpus = st.CorpusFromParsedDocuments(new_df,
category_col='title',
parsed_col='doc').build()
# 上記で用意した corpusと、比較対象したいカテゴリ名(title1, title2)を指定。
html = st.produce_scattertext_explorer(corpus,
category=title1,
category_name=title1,
not_category_name=title2)
# 生成されたHTMLを描画。
from IPython.display import display, HTML
# 単純にdisplay使うだけでは描画できないための工夫
import html as htmllib # 文字列エスケープ用
iframe = f'''
<iframe
srcdoc="{htmllib.escape(html)}"
style="width:100%;height:700px;border:none;"
sandbox="allow-scripts allow-same-origin">
</iframe>
'''
HTML(iframe)
25.4.2. 前処理あり#
コメント文をそのまま処理してしまうと観察したくない単語(助詞など)が多々現れているため、傾向を掴みづらい結果となってしまった。これまでにも見てきたように品詞を指定して観察するとしよう。このためには、(1) new_df[‘comment’] に含まれるコメントを事前に分かち書きし、そのタイミングで品詞判定をして不要語を削除する方法と、(2) scattertext側でオプション指定する方法がある。ここでは(2)の方法を眺めてみよう。
scattertext側で品詞指定するには、(a) コメント文そのもの、(b) 解析器(spacy.nlp)、(c) 解析クラスを用意する必要がある。(a) は new_df[‘comment’] をそのまま用いれば良い。(b)は既に用意している nlp を用いれば良い。(c)については st.FeatsFromSpacyDoc を継承した子クラスを作成し、その中で解析方法を書く必要がある。
NOTE
先程は処理済みDoc型を利用するため st.CorpusFromParsedDocuments() を利用した。今回はテキストと解析機を渡して scattertext 内部で処理するため、st.CorpusFromPandas() を利用している。
class SelectPOS(st.FeatsFromSpacyDoc):
'''小クラス。
get_feats() で解析方法を指定する。
'''
poses = ['PRPON', 'NOUN', 'VERB', 'ADJ', 'ADV']
def __init__(self, use_pos=poses):
super().__init__()
self._use_pos = use_pos
def get_feats(self, doc):
return collections.Counter([c.lemma_ for c in doc if c.pos_ in self._use_pos])
corpus = st.CorpusFromPandas(new_df,
category_col='title',
text_col='comment',
nlp=nlp,
feats_from_spacy_doc=SelectPOS()).build()
html2 = st.produce_scattertext_explorer(corpus,
category=title1,
category_name=title1,
not_category_name=title2)
iframe = f'''
<iframe
srcdoc="{htmllib.escape(html2)}"
style="width:100%;height:700px;border:none;"
sandbox="allow-scripts allow-same-origin">
</iframe>
'''
HTML(iframe)
# 描画が被って見づらい場合には、ファイル出力したものをダウンロードし、
# 別途ブラウザで閲覧する方法と見やすいことも。
with open('scattertext_result.html', 'w') as f:
f.write(html2)