0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SudachiPyハンズオン:求人票の「求める人物像」を単語分析

0
Posted at

背景

個人開発で、求人票から「求める人物像」を取り出して定量的にまとめ、職種ごとの対策づくりにつなげるアプリを構想しています。その最初の一歩として、人物像の文章を**単語レベルで集計する(頻出語・特徴語を見る)**ところから始めることにしました。

日本語の文章を単語に分けるには形態素解析ツールが必要です。調べると MeCab、Janome、SudachiPy など選択肢が複数あり、検索で出てくる記事には古い書き方と新しい書き方が混在していて、いま動く書き方がどれか分かりにくいと感じました。

そこで、ツールを選んだ理由と最小限の使い方を、実際に手を動かして確認しながらハンズオンにまとめました。

やりたいこと

この記事のゴールは次の3つです。

  • SudachiPyの基本(分割モード・品詞・正規化)を、出力を見ながら理解する
  • 求人票の人物像を想定した文章で、「その語が何件の求人に出たか」を数えるところまで進める
  • 動作確認済みのコードを1ファイルにまとめ、コピーしてそのまま動かせるようにする

やらないこと(別の機会に扱います)は、TF-IDFなどの特徴語、LLMによる要素抽出、クラスタリング、実データの読み込みです。

想定読者

  • Pythonの基本文法(関数、リスト内包表記、for ループ)が分かる方
  • 日本語テキストの形態素解析を初めて試す方、またはツール選びに迷っている方
  • 求人票やアンケートの自由記述など、文章データを単語で集計したい方

この記事で使う文章は、すべてハンズオンのために作ったダミーです。実際の求人票は使っていません。

動作確認環境

項目 バージョン
Python 3.13
sudachipy 0.7.0
sudachidict_core 20261015
OS Linux(Windows・macOSは未確認)
pip install sudachipy sudachidict_core

SudachiPyは辞書を別パッケージ(sudachidict_core)で入れます。辞書は small / core / full の3種類があり、この記事では標準の core を使います。

なぜSudachiPyを選んだか

主な候補を、公式ページや解説記事で確認した範囲で整理しました。

ツール 特徴 導入
MeCab C++製の定番。辞書を差し替えられる(IPA辞書、NEologd、UniDicなど)1 Pythonからはラッパー経由
fugashi MeCabのPythonラッパー。主要OS向けのwheelがあり、UniDicも入れやすい2 pip install fugashi[unidic-lite]
Janome Pure PythonでIPAdicを同梱。追加のバイナリ導入が不要3 pip install janome
SudachiPy 分割粒度を3モード(A/B/C)から選べる。表記ゆれを揃える正規化形が取れる4 pip install sudachipy sudachidict_core

導入の手軽さは、fugashiを使えばMeCab系でもpipだけで済むため、決め手になりませんでした。速度についても、fugashiの作者はJanomeを「導入は楽だが、速度は桁違いに遅いことがある」と述べています5。ただし今回は求人票100件規模を想定していて、速度は決め手ではありません。

最終的には、次の2点でSudachiPyを選びました。

  1. 表記ゆれを揃えられる: 「できる」と「出来る」のような違いを、集計の前にまとめたい
  2. 複合語の切れ方を選べる: 「データサイエンティスト」のような語を、1語で数えるか細かく分けるかを選びたい

選ぶ理由は、次のステップ2・4で実際の出力を見て確認します。

全体の流れ

ステップ6で組み立てる処理の流れです。

共通部品

最初に、辞書とtokenizerを1回だけ作ります。辞書の読み込みは時間がかかるため、使い回します。

sudachipy_handson.py(抜粋)
import sys
from collections import Counter

from sudachipy import Dictionary, SplitMode

# 辞書は「core」(標準)を使う。small / core / full の3種類があり、
# 後ろのほど語彙が多い。まずはデフォルトのcoreで十分。
# ※ 辞書の読み込みは時間がかかるので、1回だけ作って使い回す。
_dictionary = Dictionary(dict="core")
# ※ 古い記事にある .create() は非推奨になっている。現行は .tokenizer() を使う。
TOKENIZER = _dictionary.tokenizer()

# 分割モードの名前 → SplitMode の対応表(後のステップで使う)
MODES = {"A": SplitMode.A, "B": SplitMode.B, "C": SplitMode.C}

古い記事に注意: 検索で出てくる記事には、Dictionary().create() で tokenizer を作る書き方があります。sudachipy 0.7.0 では、これを実行すると DeprecationWarning(非推奨の警告)が出ました。現行の書き方は .tokenizer() です。また、sudachipy link コマンドは 0.5.2 以降では使えません6。

ステップ1: 文章を単語に分ける

tokenize(文章, 分割モード) で、単語(形態素)のリストが返ります。surface() は、文章に書かれていたままの表記です。

sudachipy_handson.py(抜粋)
text = "データサイエンティストとして主体的に動ける方を求めています。"

# tokenize(文章, 分割モード) で、単語(形態素)のリストが返る
morphemes = TOKENIZER.tokenize(text, SplitMode.C)

# surface() は「文章に書かれていたままの表記」
print([m.surface() for m in morphemes])
['データサイエンティスト', 'と', 'し', 'て', '主体的', 'に', '動ける', '方', 'を', '求め', 'て', 'い', 'ます', '。']

「データサイエンティスト」が1語のまま残っている点に注目してください。理由は次のステップで分かります。

ステップ2: 分割モード A / B / C の違い

Aは短い単位、Cは長い単位、Bはその中間です。長い語を1語で数えたいか、細かく分けたいかで選びます。

sudachipy_handson.py(抜粋)
for text in samples:
    print(f"\n{text}")
    for name, mode in MODES.items():
        parts = [m.surface() for m in TOKENIZER.tokenize(text, mode)]
        print(f"  {name}: {' / '.join(parts)}")
データサイエンティスト
  A: データ / サイエンティスト
  B: データ / サイエンティスト
  C: データサイエンティスト

主体的に動ける
  A: 主体 / 的 / に / 動ける
  B: 主体的 / に / 動ける
  C: 主体的 / に / 動ける

コミュニケーション能力
  A: コミュニケーション / 能力
  B: コミュニケーション / 能力
  C: コミュニケーション / 能力

ビジネス課題の解決
  A: ビジネス / 課題 / の / 解決
  B: ビジネス / 課題 / の / 解決
  C: ビジネス / 課題 / の / 解決

ここから分かることは2つです。

  • 「データサイエンティスト」は、Cなら1語、A・Bなら2語に分かれます。職種名のように1語で数えたい語には、Cが向いています。
  • 「コミュニケーション能力」は、Cでも2語に分かれます。辞書にない組み合わせはモードでは拾えないため、ステップ6の「2語の組(n-gram)」で拾います。

ステップ3: 1語ごとに取れる情報

表記だけでなく、原形、正規化形、読み、品詞も取れます。

sudachipy_handson.py(抜粋)
text = "チームで協働しながら、自走できる人を歓迎します。"
print(f"{'表記':<10}{'原形':<10}{'正規化形':<10}{'読み':<14}品詞")
for m in TOKENIZER.tokenize(text, SplitMode.C):
    pos = m.part_of_speech()  # 6要素のタプル。[0]が大分類、[1]が中分類
    print(
        f"{m.surface():<10}"
        f"{m.dictionary_form():<10}"   # 活用を戻した形(動ける → 動ける / 食べた → 食べる)
        f"{m.normalized_form():<10}"   # 表記ゆれを揃えた形(ステップ4で詳しく)
        f"{m.reading_form():<14}"      # カタカナの読み
        f"{pos[0]}-{pos[1]}"
    )
表記        原形        正規化形      読み            品詞
チーム       チーム       チーム       チーム           名詞-普通名詞
で         で         で         デ             助詞-格助詞
協働        協働        協働        キョウドウ         名詞-普通名詞
し         する        為る        シ             動詞-非自立可能
ながら       ながら       ながら       ナガラ           助詞-接続助詞
、         、         、         、             補助記号-読点
自走        自走        自走        ジソウ           名詞-普通名詞
できる       できる       出来る       デキル           動詞-非自立可能
人         人         人         ヒト            名詞-普通名詞
を         を         を         ヲ             助詞-格助詞
歓迎        歓迎        歓迎        カンゲイ          名詞-普通名詞
し         する        為る        シ             動詞-非自立可能
ます        ます        ます        マス            助動詞-*
。         。         。         。             補助記号-句点

品詞は「名詞」「動詞」「助詞」などで取れます。助詞(の、に、は)は集計しても意味が薄いため、ステップ5で除きます。

ステップ4: 表記ゆれ(原形と正規化形の違い)

sudachipy_handson.py(抜粋)
words = [
    "できる", "出来る",          # ひらがな/漢字の揺れ
    "シュミレーション",           # よくある誤記
    "PYTHON", "Python",     # 全角/大文字の揺れ
    "動ける",                     # 可能形
    "する",                       # ← 正規化形が「為る」になる点に注意
]
print(f"{'入力':<14}{'原形':<14}{'正規化形':<14}")
for w in words:
    m = TOKENIZER.tokenize(w, SplitMode.C)[0]
    print(f"{w:<14}{m.dictionary_form():<14}{m.normalized_form():<14}")
入力            原形            正規化形          
できる           できる           出来る           
出来る           出来る           出来る           
シュミレーション      シュミレーション      シミュレーション      
PYTHON        Python        Python        
Python        Python        Python        
動ける           動ける           動く            
する            する            為る            
入力 正規化形 分かること
できる / 出来る 出来る ひらがなと漢字の揺れがそろう
シュミレーション シミュレーション よくある誤記が直る
動ける 動く 可能形が基本形にそろう
する 為る 人が読みにくい形になる語もある

集計のキーには正規化形を使い、画面や記事に出すときは、よく出た元の表記を使う。この使い分けが安全だと考えています。

ステップ5: 品詞フィルタ(数える語を選ぶ)

助詞や記号を除き、数える価値のある語だけを残します。

sudachipy_handson.py(抜粋)
# 数えたい品詞。人物像は「動ける」「前向きな」のように動詞・形容詞にも特徴が出るので、
# 名詞だけにはしない。
KEEP_POS = {"名詞", "動詞", "形容詞", "形状詞"}  # 形状詞 = 「主体的」「柔軟」などの形容動詞の語幹

# 品詞では落とせない「意味の薄い語」。最初は最小限にして、結果を見ながら増やす。
# (最初から大量に入れると、必要な語まで消すおそれがある)
STOPWORDS = {"為る", "居る", "有る", "成る", "事", "物", "為", "等", "方", "人"}


def extract_keywords(text, mode=SplitMode.C):
    """文章から「数える価値のある語」のリストを返す(正規化形で返す)"""
    keywords = []
    for m in TOKENIZER.tokenize(text, mode):
        pos = m.part_of_speech()
        if pos[0] not in KEEP_POS:
            continue                          # 助詞・助動詞・記号などを除く
        if pos[1] in {"数詞", "代名詞"}:
            continue                          # 数字や「これ」「それ」を除く
        # 英単語は小文字にそろえる。
        # ※ 実際に試すと、同じ「Python」でも文脈によって正規化形が
        #    「python」になったり「Python」になったりした(Sudachiの挙動)。
        #    そろえないと、同じ語が別々に数えられてしまうので、自分で小文字化する。
        word = m.normalized_form().lower()
        if word in STOPWORDS:
            continue
        keywords.append(word)
    return keywords
元の文  : Pythonを使って、データから課題を発見し、主体的に提案できる方を歓迎します。
全ての語: ['Python', 'を', '使っ', 'て', '、', 'データ', 'から', '課題', 'を', '発見', 'し', '、', '主体的', 'に', '提案', 'できる', '方', 'を', '歓迎', 'し', 'ます', '。']
残った語: ['python', '使う', 'データ', '課題', '発見', '主体的', '提案', '出来る', '歓迎']

ポイントは3つです。

  • 名詞だけでなく動詞・形容詞も残す: 人物像は「動ける」「前向きな」のように、動詞や形容詞に特徴が出るためです。
  • ストップワードは最小限から始める: 最初から大量に入れると、必要な語まで消すおそれがあります。結果を見ながら増やします。
  • 英字は自分で小文字にそろえる: 同じ「Python」でも、文脈によって正規化形が python になったり Python になったりしました。そろえないと別の語として数えられます。

ステップ6: 「何件の求人に出たか」を数える

ここまでの部品を組み合わせて、ダミーの求人8件を集計します。

数え方の考え方

主な指標は**「出た求人数」(文書頻度)**にしました。1件の求人が同じ語を何度も繰り返しても、「求められやすさ」は上がらないと考えたためです。合計の出現回数は、補助として並べます。

sudachipy_handson.py(抜粋)
def document_frequency(docs, extractor):
    """各語が「何件の文書に出たか」を数える(同じ文書内の重複は1回)"""
    counter = Counter()
    for doc in docs:
        # set() にすることで、1件の中で何回出ても1回として数える。
        # → 「同じ求人が複数回書いていても1回」と数える考え方。
        counter.update(set(extractor(doc)))
    return counter

set() で重複を消してから数える点が、合計の出現回数との違いです。

sudachipy_handson.py(抜粋)
total = Counter(w for doc in DUMMY_JOBS for w in extract_keywords(doc))

この1行は、求人を1件ずつ取り出し、その中の語を1つずつ取り出して、すべてを数えています。重複を消さないので、合計の出現回数になります。たとえば「出来る」が1件の求人に2回書かれていれば、出た求人数は+1、合計の出現回数は+2です。

2語の組(n-gram)

ステップ2で見たとおり、「コミュニケーション能力」のような複合表現は、単語に分けると見えにくくなります。そこで、隣り合う2語を組にして数えます。

sudachipy_handson.py(抜粋)
def bigrams(text):
    """連続する2語の組を作る(ステップ2で見た「複合表現」を拾うため)"""
    # tokenize の結果はそのままではスライス([1:])できないので、list にする
    tokens = list(TOKENIZER.tokenize(text, SplitMode.C))
    result = []
    for left, right in zip(tokens, tokens[1:]):
        # 左右とも「数える品詞」で隣り合っている場合だけ組にする。
        # 間に助詞などが挟まる場合は、別の表現なので組にしない。
        if left.part_of_speech()[0] in KEEP_POS and right.part_of_speech()[0] in KEEP_POS:
            a, b = left.normalized_form().lower(), right.normalized_form().lower()
            if a not in STOPWORDS and b not in STOPWORDS:
                result.append(f"{a}+{b}")
    return result

TOKENIZER.tokenize() の戻り値は、そのままではスライス(tokens[1:])できず、TypeError になります。list() で包んでから使います。

実行結果

[単語] 上位10語(求人8件中)
語                出た求人数      割合     総出現回数
出来る                  5     62%         7
コミュニケーション            4     50%         4
能力                   3     38%         3
チーム                  3     38%         3
協働                   3     38%         3
課題                   3     38%         3
主体的                  2     25%         2
高い                   2     25%         2
歓迎                   2     25%         2
自走                   2     25%         2

[2語の組] 上位8組
  コミュニケーション+能力            3件
  自走+出来る                  2件
  ビジネス+課題                 2件
  論理的+思考力                 2件
  協働+出来る                  1件
  提案+出来る                  1件
  新しい+技術                  1件
  学習+意欲                   1件

読み取れることは次のとおりです。

  • 「コミュニケーション+能力」が3件で、2語の組の1位です。ステップ2で見た「Cモードでも2語に分かれる」複合表現を、組にして拾えています。
  • 「自走+出来る」が2件です。ダミーには「自走できる」と「自走出来る」が1件ずつありますが、正規化形でそろった状態で数えられています。
  • 「出来る」が上位に来るのは、「〜できる方」という決まり文句のためです。意味の薄い語をストップワードに足すかは、結果を見て判断します(「自走できる」の「できる」まで消してよいかは悩みどころです)。
  • 「出来る」だけ、出た求人数が5、合計の出現回数が7と差があります。1件の中で複数回使われた求人があるためです。

同じ件数の語の並び順は、実行のたびに変わることがあります(実際に、「能力」「チーム」「協働」「課題」の順が実行ごとに入れ替わりました)。set() の取り出し順が一定でないためです。件数が同じ語の順位に意味はないので、順位ではなく件数で見ます。

実データに進む前の注意

  • 件数が少ないと、順位の僅差に意味はありません。求人情報を商用ツールで分析した研究でも、単語や係り受けの頻度を統計的に論じるには数百件のデータが必要という知見が述べられています7。100件規模なら、順位の細かい差ではなく頻出語のグループとして見るのが安全だと考えています。
  • 求人が100件になると、1〜2件にしか出ない語が大量に出ます。「3件以上に出た語だけ見る」のように下限を決めて、偶然のブレを除きます。

まとめ

  • SudachiPyは、分割モードで複合語の切れ方を選べ、正規化形で表記ゆれをそろえられる
  • ただし「コミュニケーション能力」のような複合表現は、Cモードでも2語に分かれる。2語の組で拾う
  • 正規化形は便利だが癖がある(「する」→「為る」、英字の大文字小文字)。集計の前に自分でそろえる
  • 主な指標は「何件の求人に出たか」。1件の中の重複は消して数える
  • 古い記事の書き方(.create()、sudachipy link)は、現行では使えない・非推奨

次は、特徴語(TF-IDF)と、実データのCSV読み込みに進む予定です。

全コード

sudachipy_handson.py(全体・コピーして動かせます)
sudachipy_handson.py
"""
SudachiPy ハンズオン(求人票「求める人物像」の単語分析の準備)

目的:
    SudachiPyの基本操作を、小さなステップで手を動かして身につける。
    最後のステップ6で、単語分析(頻度の集計)の最小版を動かす。

準備(自分のPCのターミナルで):
    pip install sudachipy sudachidict_core
    python sudachipy_handson.py        # 全ステップを順に実行
    python sudachipy_handson.py 3      # ステップ3だけ実行

ここで使う文章は、すべてこのハンズオンのために作ったダミーです(実データではありません)。
動作確認済み: sudachipy 0.7.0 / sudachidict_core 20261015
"""

import sys
from collections import Counter

from sudachipy import Dictionary, SplitMode

# ---------------------------------------------------------------------------
# 共通部品
# ---------------------------------------------------------------------------

# 辞書は「core」(標準)を使う。small / core / full の3種類があり、
# 後ろのほど語彙が多い。まずはデフォルトのcoreで十分。
# ※ 辞書の読み込みは時間がかかるので、1回だけ作って使い回す。
_dictionary = Dictionary(dict="core")
# ※ 古い記事にある .create() は非推奨になっている。現行は .tokenizer() を使う。
TOKENIZER = _dictionary.tokenizer()

# 分割モードの名前 → SplitMode の対応表(後のステップで使う)
MODES = {"A": SplitMode.A, "B": SplitMode.B, "C": SplitMode.C}


def title(text):
    """ステップの見出しを表示する(出力を読みやすくするだけの関数)"""
    print()
    print("=" * 60)
    print(text)
    print("=" * 60)


# ---------------------------------------------------------------------------
# ステップ1: いちばん基本の使い方
# ---------------------------------------------------------------------------
def step1_basic():
    title("ステップ1: 文章を単語に分ける")

    text = "データサイエンティストとして主体的に動ける方を求めています。"

    # tokenize(文章, 分割モード) で、単語(形態素)のリストが返る
    morphemes = TOKENIZER.tokenize(text, SplitMode.C)

    # surface() は「文章に書かれていたままの表記」
    print([m.surface() for m in morphemes])

    # 【試してみよう】text を自分の好きな文に変えて、どう切れるか見てみる


# ---------------------------------------------------------------------------
# ステップ2: 分割モード A / B / C の違い
# ---------------------------------------------------------------------------
def step2_split_modes():
    title("ステップ2: 分割モード A / B / C の違い")

    # A = 短い単位 / B = 中間 / C = 長い単位(固有名詞など)
    # → 長い語をまとめて1語で数えたいか、細かく分けたいかで選ぶ。
    samples = [
        "データサイエンティスト",
        "主体的に動ける",
        "コミュニケーション能力",
        "ビジネス課題の解決",
    ]
    for text in samples:
        print(f"\n{text}")
        for name, mode in MODES.items():
            parts = [m.surface() for m in TOKENIZER.tokenize(text, mode)]
            print(f"  {name}: {' / '.join(parts)}")

    # 【気づくポイント】
    # ・「データサイエンティスト」は C だと1語、A だと分かれる
    #   → 職種名のように「1語で数えたい」語は C が向く。
    # ・「コミュニケーション能力」は C でも2語に分かれる(辞書にない組み合わせのため)
    #   → こういう複合表現は、モードでは拾えない。
    #      ステップ6の「n-gram(連続する語の組)」で拾う。これがn-gramを使う理由。


# ---------------------------------------------------------------------------
# ステップ3: 1語ごとに取れる情報
# ---------------------------------------------------------------------------
def step3_morpheme_info():
    title("ステップ3: 1語ごとの情報(表記・原形・読み・品詞)")

    text = "チームで協働しながら、自走できる人を歓迎します。"
    print(f"{'表記':<10}{'原形':<10}{'正規化形':<10}{'読み':<14}品詞")
    for m in TOKENIZER.tokenize(text, SplitMode.C):
        pos = m.part_of_speech()  # 6要素のタプル。[0]が大分類、[1]が中分類
        print(
            f"{m.surface():<10}"
            f"{m.dictionary_form():<10}"   # 活用を戻した形(動ける → 動ける / 食べた → 食べる)
            f"{m.normalized_form():<10}"   # 表記ゆれを揃えた形(ステップ4で詳しく)
            f"{m.reading_form():<14}"      # カタカナの読み
            f"{pos[0]}-{pos[1]}"
        )
    # 【気づくポイント】
    # ・品詞は「名詞」「動詞」「助詞」…と取れる。
    #   助詞(の、に、は)は集計しても意味が薄いので、ステップ5で除く。


# ---------------------------------------------------------------------------
# ステップ4: 表記ゆれ — 原形 と 正規化形 の違い
# ---------------------------------------------------------------------------
def step4_normalization():
    title("ステップ4: 表記ゆれ(原形 と 正規化形)")

    words = [
        "できる", "出来る",          # ひらがな/漢字の揺れ
        "シュミレーション",           # よくある誤記
        "PYTHON", "Python",     # 全角/大文字の揺れ
        "動ける",                     # 可能形
        "する",                       # ← 正規化形が「為る」になる点に注意
    ]
    print(f"{'入力':<14}{'原形':<14}{'正規化形':<14}")
    for w in words:
        m = TOKENIZER.tokenize(w, SplitMode.C)[0]
        print(f"{w:<14}{m.dictionary_form():<14}{m.normalized_form():<14}")

    # 【気づくポイント】
    # ・「できる」と「出来る」は、正規化形ではどちらも「出来る」にそろう。
    #   → 集計で「自走できる」「自走出来る」を同じものとして数えられる。
    # ・「シュミレーション」→「シミュレーション」のように誤記も直る。
    # ・「動ける」→「動く」のように、可能形も基本形にそろう。
    # ・ただし「する」→「為る」のように、人が読みにくい形になる語もある。
    #   → 集計のキーには正規化形を使い、表示にはよく出た表記を使う、という使い分けが安全。


# ---------------------------------------------------------------------------
# ステップ5: 品詞フィルタ(数える語を選ぶ)
# ---------------------------------------------------------------------------

# 数えたい品詞。人物像は「動ける」「前向きな」のように動詞・形容詞にも特徴が出るので、
# 名詞だけにはしない。
KEEP_POS = {"名詞", "動詞", "形容詞", "形状詞"}  # 形状詞 = 「主体的」「柔軟」などの形容動詞の語幹

# 品詞では落とせない「意味の薄い語」。最初は最小限にして、結果を見ながら増やす。
# (最初から大量に入れると、必要な語まで消すおそれがある)
STOPWORDS = {"為る", "居る", "有る", "成る", "事", "物", "為", "等", "方", "人"}


def extract_keywords(text, mode=SplitMode.C):
    """文章から「数える価値のある語」のリストを返す(正規化形で返す)"""
    keywords = []
    for m in TOKENIZER.tokenize(text, mode):
        pos = m.part_of_speech()
        if pos[0] not in KEEP_POS:
            continue                          # 助詞・助動詞・記号などを除く
        if pos[1] in {"数詞", "代名詞"}:
            continue                          # 数字や「これ」「それ」を除く
        # 英単語は小文字にそろえる。
        # ※ 実際に試すと、同じ「Python」でも文脈によって正規化形が
        #    「python」になったり「Python」になったりした(Sudachiの挙動)。
        #    そろえないと、同じ語が別々に数えられてしまうので、自分で小文字化する。
        word = m.normalized_form().lower()
        if word in STOPWORDS:
            continue
        keywords.append(word)
    return keywords


def step5_pos_filter():
    title("ステップ5: 品詞フィルタ")

    text = "Pythonを使って、データから課題を発見し、主体的に提案できる方を歓迎します。"
    print("元の文  :", text)
    print("全ての語:", [m.surface() for m in TOKENIZER.tokenize(text, SplitMode.C)])
    print("残った語:", extract_keywords(text))

    # 【試してみよう】
    # ・KEEP_POS から "動詞" を消すと、「発見」「提案」などの動作を表す語はどう変わるか?
    # ・STOPWORDS を空にすると、何が増えるか?


# ---------------------------------------------------------------------------
# ステップ6: ミニ版・単語分析(求人ごとの出現件数を数える)
# ---------------------------------------------------------------------------

# ダミーの求人8件(実データではありません)
DUMMY_JOBS = [
    "主体的に動ける方。コミュニケーション能力が高く、チームで協働できる方を歓迎します。",
    "自走できる方。ビジネス課題を整理し、データから提案できる方。",
    "コミュニケーション能力と論理的思考力のある方。主体性を持って業務を進められる方。",
    "学習意欲が高く、新しい技術を積極的に学べる方。チームでの協働を大切にできる方。",
    "自走出来る方歓迎。わからないことを自分で調べて解決できる方。",
    "ビジネス課題の理解力があり、関係者とのコミュニケーションを丁寧に取れる方。",
    "論理的思考力があり、主体的に課題を発見できる方。",
    "チームで協働し、周囲を巻き込みながら進められる方。コミュニケーション能力を重視します。",
]


def document_frequency(docs, extractor):
    """各語が「何件の文書に出たか」を数える(同じ文書内の重複は1回)"""
    counter = Counter()
    for doc in docs:
        # set() にすることで、1件の中で何回出ても1回として数える。
        # → 「同じ求人が複数回書いていても1回」と数える考え方。
        counter.update(set(extractor(doc)))
    return counter


def bigrams(text):
    """連続する2語の組を作る(ステップ2で見た「複合表現」を拾うため)"""
    # tokenize の結果はそのままではスライス([1:])できないので、list にする
    tokens = list(TOKENIZER.tokenize(text, SplitMode.C))
    result = []
    for left, right in zip(tokens, tokens[1:]):
        # 左右とも「数える品詞」で隣り合っている場合だけ組にする。
        # 間に助詞などが挟まる場合は、別の表現なので組にしない。
        if left.part_of_speech()[0] in KEEP_POS and right.part_of_speech()[0] in KEEP_POS:
            a, b = left.normalized_form().lower(), right.normalized_form().lower()
            if a not in STOPWORDS and b not in STOPWORDS:
                result.append(f"{a}+{b}")
    return result


def step6_mini_analysis():
    title("ステップ6: ミニ版・単語分析(ダミー求人8件)")

    n = len(DUMMY_JOBS)

    # --- (1) 出現した求人の件数(文書頻度)と、総出現回数を比べる ---
    df = document_frequency(DUMMY_JOBS, extract_keywords)
    # 【この1行がやっていること】各語が「全求人の合計で何回出たか」(総出現回数)を数える。
    #
    # 読み方(内側から外側へ):
    #   for doc in DUMMY_JOBS        → 求人を1件ずつ取り出す
    #   for w in extract_keywords(doc) → その求人から取り出した語を1つずつ取り出す
    #   w                            → 取り出した語そのものを材料として並べる
    #   Counter(...)                 → 並んだ語を数えて {語: 回数} の辞書のようにまとめる
    # 2つの for が1行に入っている書き方(ジェネレータ式)で、次のコードと同じ意味:
    #   words = []
    #   for doc in DUMMY_JOBS:
    #       for w in extract_keywords(doc):
    #           words.append(w)
    #   total = Counter(words)
    #
    # 上の df(document_frequency)との違い:
    #   df    … set() で重複を消してから数える → 「何件の求人に出たか」
    #   total … 重複を消さずにそのまま数える   → 「合計で何回出たか」
    # 例: 「出来る」が1件の求人に2回書かれていれば、df は +1、total は +2 になる。
    # 下の表で「出た求人数」と「総出現回数」を並べて比べるために作っている。
    # (この差が大きい語は、一部の求人で繰り返し使われている語、という手がかりになる)
    total = Counter(w for doc in DUMMY_JOBS for w in extract_keywords(doc))

    print(f"\n[単語] 上位10語(求人{n}件中)")
    print(f"{'語':<14}{'出た求人数':>8}{'割合':>8}{'総出現回数':>10}")
    for word, count in df.most_common(10):
        print(f"{word:<14}{count:>8}{count / n:>8.0%}{total[word]:>10}")
    # 【気づくポイント】
    # ・「出た求人数」を主指標にする理由: 1件の長い求人が同じ語を連呼しても、
    #   「求められやすさ」は上がらないため。総出現回数は補助として見る。
    # ・「出来る」が上位に来るのは、「〜できる方」という決まり文句のため。
    #   意味の薄い語は、STOPWORDS に足すかどうかを結果を見て判断する。
    #   (ただし「自走できる」の「できる」まで消してよいかは、自分で考えるポイント)

    # --- (2) 2語の組(n-gram)---
    df2 = document_frequency(DUMMY_JOBS, bigrams)
    print(f"\n[2語の組] 上位8組")
    for pair, count in df2.most_common(8):
        print(f"  {pair:<24}{count}件")
    # 【気づくポイント】
    # ・「コミュニケーション+能力」「論理的+思考力」のように、
    #   単語に分けると見えにくい表現が拾える。
    # ・「自走+出来る」のように、表記ゆれ(できる/出来る)が揃った状態で数えられている。
    # ・「+」は後の分析でつなぎ直して見る(コミュニケーション能力 など)。

    # --- (3) 実データでの注意 ---
    # 求人が100件になると、件数が1〜2件の語が大量に出る。
    # 「3件以上に出た語だけ見る」のように下限を決めて、偶然のブレを除く。


# ---------------------------------------------------------------------------
# 実行部
# ---------------------------------------------------------------------------
STEPS = {
    "1": step1_basic,
    "2": step2_split_modes,
    "3": step3_morpheme_info,
    "4": step4_normalization,
    "5": step5_pos_filter,
    "6": step6_mini_analysis,
}

if __name__ == "__main__":
    # 引数なし → 全ステップ。数字を渡す → そのステップだけ。
    targets = sys.argv[1:] or list(STEPS)
    for key in targets:
        STEPS[key]()

実行方法です。

python sudachipy_handson.py      # 全ステップを順に実行
python sudachipy_handson.py 3    # ステップ3だけ実行

参考

  1. MeCabの解説(crystal method) の「MeCabが多くの用途でデファクトスタンダードになっている理由」の節。 ↩

  2. fugashi(GitHub) の「Wheels are provided for Linux, OSX, and Win64, and UniDic is easy to install」および「Installing a Dictionary」の節。 ↩

  3. 形態素解析ツール比較(一創) のツール一覧の表(「Pure Python・IPAdic同梱でpipのみで導入」)と、crystal method の「追加のバイナリインストールが不要」という記述。 ↩

  4. SudachiPy(PyPI) の「Multi-granular tokenization」と「Normalization」の例。分割モードの説明は Sudachi(GitHub) の「The modes of splitting」。 ↩

  5. Fast Japanese Tokenization with a Single Pip Install の「their ease of use comes at the cost of speed, sometimes by orders of magnitude」。 ↩

  6. 情報処理学会 全国大会の資料 の「5.おわりに」。 ↩

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?