「TF-IDF」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。
🍰 まずはやさしく
単語の重要さを測る物差しです。
文章の特徴を数字で表すために使います。
スマホの検索などでキーワードを探します。
まずは結論を短くまとめます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
🍰 まずはやさしく
文章を数字の列に変える方法です。
記事の内容を自動で分けるために使います。
ニュースのジャンル分けに役立ちます。
どんな場面で使うかを説明します。
「ニュース記事を分類したい」 「論文の特徴的キーワードを取り出したい」 — テキストを数値ベクトルに変える最も古典的かつ強力な方法が TF-IDF。 scikit-learn の TfidfVectorizer 一発で動きます。
このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。
🍰 まずはやさしく
レアな単語を大切にする考え方です。
その文章らしさを判断するために使います。
料理のレシピで珍しい材料を探す感じです。
直感的にわかる例で解説します。
10 件のレシピがあり、 そのうち 1 件のレシピに「ターメリック」が 5 回出てくるとします。
一方「塩」は全 10 件に出てくるので IDF ≈ log(10/10) = 0。 TF×IDF も 0 → 全体共通の単語は自動的に重み 0 に。
TF-IDF の理解には、 (1) 単語頻度の分布形 (Zipf 則の確認)、 (2) 文書間の類似度パターン (散布図)、 (3) 用語-文書行列のヒートマップ表現 の 3 点を同時に把握する必要があります。 ここでは SSDSE-B-2026 から作った 2 種類の小さなコーパス (指標名 109 個、 47 都道府県のプロフィール文書) で、 この 3 点を実際に描いてみます。
読み取るポイント:
読み取るポイント:
読み取るポイント:
この 3 枚を順に追うと、 「単語分布の確認 → 文書構造の可視化 → 共起パターンの抽出」 という TF-IDF 解析の標準フローが体系化される。 関連用語 コサイン類似度 / 情報エントロピー / エンベディング へ進むと、 各図の理論背景を深掘りできる。
TF-IDF の核を本当に掴めたかを 6 問で確認する。 すべて 1 分以内で答えられる粒度。 「自分で」答えを口に出して言える状態を目指したい。
これらの問題に詰まった項目があれば、 「TF と IDF の定義」「単語分布の可視化」セクションへ戻ろう。 TF-IDF は古典的だが、 現代の NLP パイプラインでも前処理や検索エンジン (Elasticsearch) で現役の道具である。
🍰 まずはやさしく
計算式で重要度を出す仕組みです。
正確なスコア(点数)を出すために使います。
部活の報告書から重要な言葉を選びます。
具体的な計算方法を学びます。
「TF-IDF」は 文書中の単語の重要度を測る古典的指標。頻出だがどの文書にも出る単語にはペナルティ。 です。 ここでは定義式の各記号、 直感的意味、 SSDSE-B-2026 への当てはめを段階的に解きほぐします。
| 定義 | 式 | 意味 |
|---|---|---|
| 素朴 (raw count) | $f_{t,d}$ | 単純出現回数 |
| 対数正規化 | $1 + \log f_{t,d}$ | 頻度の歪み緩和 |
| 頻度正規化 | $f_{t,d} / \sum_{t'} f_{t',d}$ | 文書長正規化 |
| 最大頻度正規化 | $0.5 + 0.5 \cdot f_{t,d} / \max_{t'} f_{t',d}$ | 長文偏り抑制 |
| 定義 | 式 |
|---|---|
| 標準 | $\log(N / \text{df}(t))$ |
| +1 平滑化 (sklearn) | $\log((1+N) / (1+\text{df}(t))) + 1$ |
| 確率的 IDF | $\log((N - \text{df}(t)) / \text{df}(t))$ |
TF-IDF の現代版が BM25 (Best Match 25)。 単語頻度の飽和 (saturation) と文書長正規化を導入。 検索エンジン (Elasticsearch・Lucene) のデフォルト。 $\text{BM25}(t, d) = \text{IDF}(t) \cdot \frac{f_{t,d}(k_1 + 1)}{f_{t,d} + k_1(1 - b + b \cdot |d|/\text{avgdl})}$ で $k_1 \approx 1.2, b \approx 0.75$。
SSDSE-B-2026 で「都道府県プロフィール文書」を TF-IDF 化。 入力は 47 都道府県を文書、 各都道府県の上位指標名(人口・出生・経済等)を単語に、 出力は scikit-learn TfidfVectorizer で重要指標を抽出。 47 都道府県 × 複数年の実データで具体計算を実施します。
| 業界 | 事例 | 役割 | SSDSE-B-2026 との対比 |
|---|---|---|---|
| Google 検索 | 初期ランキングで核となった指標 | クエリ-文書のマッチング | 都道府県名×指標のレポート検索 |
| Elasticsearch | BM25 (TF-IDF 改良版) を標準採用 | 全文検索エンジン | SSDSE 文書検索 |
| Lucene | オープンソース検索ライブラリ | Solr/ES の中核 | テキストデータベース |
| ニュース推薦 | Yahoo・Smartnews の特徴量 | TF-IDF + 行列分解 | 都道府県別ニュース類似度 |
| 剽窃検出 | Turnitin・iThenticate | TF-IDF ベクトル類似 | 学術文書比較 |
| カスタマーサポート FAQ マッチ | Zendesk・Salesforce | 問い合わせと FAQ | 47 都道府県別問い合わせ |
| 手法 | 定義 | 特徴 | 用途 |
|---|---|---|---|
| TF-IDF | 古典統計ベース | 解釈容易・軽量 | 中小規模検索 |
| BM25 | TF-IDF の確率版 | 長文ペナルティ追加 | 現代検索エンジン標準 |
| Word2Vec | 分布意味論・ベクトル | 類似語捕捉 | 推薦・分類 |
| BERT埋め込み | 文脈依存ベクトル | 文意理解可 | QA・分類・要約 |
| LSA/LSI | TF-IDF + SVD | 潜在意味抽出 | トピック分析 |
| LDA | 確率的トピックモデル | 文書の混合分布 | テーマ抽出 |
| Sentence-BERT | 文埋め込み専用 BERT | 高速類似度 | 意味検索 |
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023].copy() # 「都道府県プロフィール」を作成 docs = [] for _, row in d.iterrows(): doc = f"{row['Prefecture']} 人口{row['A1101']//10000}万 出生{row['A4101']//100}百" docs.append(doc) v = TfidfVectorizer() M = v.fit_transform(docs) print(M.shape) |
💬 47 県それぞれに「県名・人口◯万・出生◯百」の 3 語の文書を作ったので、語の延べ数は 141 だが、語彙は 134 語になった。重複したのは「人口129万」「出生69百」など 6 語だけで、ほとんどの語が 1 つの県にしか出てこない。数値をそのまま語にすると値が 1 つ違うだけで別の語になるため、TF-IDF の「共通する語で文書を比べる」仕組みがほとんど働かない行列になっていることを、この 134 という数が示している。
1 2 3 4 5 | import numpy as np idx = list(d['Prefecture']).index('東京都') row = M[idx].toarray().flatten() top = row.argsort()[-3:][::-1] print([v.get_feature_names_out()[i] for i in top]) |
💬 東京都の上位 3 語は「東京都」「出生863百」「人口1408万」で、東京都の文書の 3 語すべてが並んだ。3 語ともほかの県に出てこないので IDF はどれも ln(48/2)+1≈4.18 と同じになり、正規化後の重みはそろって 1/√3≈0.577 になる。表示順は同点のなかでの argsort の並びにすぎず、「県名が最も特徴的」という意味は無い。
1 2 3 4 5 6 7 8 9 10 | from sklearn.metrics.pairwise import cosine_similarity idx = list(d['Prefecture']).index('東京都') sims = cosine_similarity(M[idx], M).flatten() sims[idx] = -1 # 自分自身を除く order = sims.argsort()[::-1] # 類似度 0 の県を「似ている順」に並べても意味が無いので、語を共有する県だけを出す shared = [i for i in order if sims[i] > 0] print('東京都と語を 1 つでも共有する県:', len(shared), '県') for i in shared[:4]: print(d.iloc[i]['Prefecture'], round(sims[i], 3)) |
💬 東京都の 3 語は他のどの県の文書にも出てこないため、コサイン類似度は 46 県すべてで 0 になり、語を共有する県は 0 県だった。類似度で並べ替えて上位 4 県を表示すると、類似度 0.0 の沖縄県・千葉県などが「似ている県」のように出てしまうが、それは同点の中の並び順にすぎないので、ここでは類似度が 0 より大きい県だけを数えている。数値を語として扱うなら「人口100万台」のように幅でまとめ、語を共有させてから類似度を測る必要がある。
実測では東京都との類似度は全て 0.000 になります。 既定 tokenizer が「人口1408万」等を丸ごと 1 トークン化し、 東京都は他県と共有語を持たないためです(各県が独自語彙)。 意味ある TOP 3 を得るには数値を分割・ビン化してトークンを共有させる前処理が必要。
min_df, max_df, ngram_range の意味を述べよ。
3 文書のミニコーパス:
「猫」の TF-IDF を計算:
「は」は全 3 文書に出るので IDF = log(3/3) = 0、 TF-IDF も 0。 「魚」は d1 にしか出ないので IDF = log(3/1) ≈ 1.10 と大きく、 d1 で重要な単語と判定されます。
合成 3 文書で単語 "data" の TF-IDF を計算する。
1 2 3 4 5 6 7 | import numpy as np tf = np.array([0.05, 0.02, 0]) N = 3; df = 2 idf = np.log(N/df) tfidf = tf * idf print(f"IDF: {idf:.3f}") print(f"TF-IDF: {tfidf.round(4)}") |
💬 手計算 (Step 2) と Python 出力が完全一致。
最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:
1 2 3 4 5 6 | from sklearn.feature_extraction.text import TfidfVectorizer docs = ['猫 は 魚 が 好き', '犬 は 肉 が 好き', '猫 と 犬 は 動物'] vec = TfidfVectorizer(token_pattern=r'\S+') X = vec.fit_transform(docs) print(vec.get_feature_names_out()) print(X.toarray().round(3)) |
💬 3 文すべてに出てくる「は」は IDF が ln(4/4)+1=1 と最小で、1 文目の重みは 0.336 にとどまる。1 文目にしか出ない「魚」は IDF が ln(4/2)+1≈1.69 と最大で、重みも 0.569 と文中で最も高い。2 文に出る「が」「好き」「猫」は 0.433 で、その間に入る。各行は長さ 1 に正規化されているので、1 文にしか出ない語(「と」「動物」、各 0.534)を 2 つ含む 3 文目では、「犬」「猫」の重みが 0.406 と 1・2 文目の 0.433 より小さくなる点も読み取れる。
補足:ライブラリのバージョンや前処理状態によって出力は変わります。 自分の環境で動かすときは pip list でバージョンを確認し、 入力 CSV のパス・列名を実態に合わせてください。
🎯 このコードでやること:47 都道府県の主要指標を文字列化して文書集合とし、 TF-IDF ベクトル化する。
📥 入力データ:SSDSE-B-2026 から 2023 年データ。 都道府県名 + 主要指標カテゴリで文書を作成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) d = df[df['年度']==2023].copy() d['高齢化率'] = (d['65歳以上人口']/d['総人口']*100).astype(int) # 都道府県プロフィール文書を作成 def make_doc(row): return (f"{row['都道府県']} " f"人口{row['総人口']//10000}万 " f"出生{row['出生数']//100}百 " f"死亡{row['死亡数']//100}百 " f"高齢化{row['高齢化率']}") docs = [make_doc(r) for _, r in d.iterrows()] v = TfidfVectorizer() M = v.fit_transform(docs) print(f"文書数 : {M.shape[0]}") print(f"語彙数 : {M.shape[1]}") print(f"非ゼロ : {M.nnz}") print(f"\nサンプル文書:") print(f" {docs[12]}") # 東京都 |
📤 実行結果:
💬 結果の読み方:47 都道府県 × 191 語彙のスパース TF-IDF 行列(非ゼロ 235=各文書ちょうど 5 トークン × 47 文書)。 各都道府県の特徴的な単語(東京なら「人口1408万」「出生863百」など)が高 TF-IDF 値を持つ。 47×191 でも疎で、 巨大コーパスでも省メモリ。 なお既定の TfidfVectorizer は空白・記号区切りのため「人口1408万」がそのまま 1 トークン化し、 数値ごと語彙が分かれる点に注意(後述の類似度に影響)。
🎯 このコードでやること:東京都の TF-IDF ベクトルと他 46 都道府県のベクトルとのコサイン類似度を計算し TOP 3 を表示。
📥 入力データ:前のコードで M (47×191) を取得済み。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.metrics.pairwise import cosine_similarity idx = list(d['都道府県']).index('東京都') sims = cosine_similarity(M[idx], M).flatten() # 自分自身を除外 sims[idx] = -1 top = sims.argsort()[-5:][::-1] print("東京都に類似する都道府県 TOP 5:") for i in top: print(f" {d.iloc[i]['都道府県']:<6s} : {sims[i]:.4f}") |
📤 実行結果:
💬 結果の読み方(重要な落とし穴):実際に走らせると東京都との類似度は全て 0.0000。 これは失敗ではなく、 既定 TfidfVectorizer の tokenizer の性質そのものを可視化した結果です。 「人口1408万」「出生863百」のように数値がまるごと 1 トークンになるため、 東京都の 5 語(県名+4 指標値)は他県と 1 語も共有せず、 コサイン類似度が 0 になります。 実際、 東京都は高齢化率 22(全国最小・固有値)を含むため共有語がゼロでした。 一方で高齢化率などの階級値が一致する地方県同士だけは僅かに類似(47 県 1081 ペア中 95 ペアのみ非ゼロ、 最大は奈良県×愛媛県の 0.3808)。 教訓:数値をそのままトークン化すると各県が「独自語彙の孤島」になり、 意味ある類似度が測れない。 実運用では数値をビン化・単位語に分割するか、 tokenizer を差し替える必要があります(→ FAQ「日本語 TF-IDF」)。
🎯 このコードでやること:各都道府県の TF-IDF 値が最大の単語 (= 最も特徴的な単語) を抽出する。
📥 入力データ:前述 v, M, d を使用。
1 2 3 4 5 6 7 8 9 | import numpy as np features = v.get_feature_names_out() print("各都道府県の最特徴語 (TF-IDF 最大):") for i in range(0, 47, 5): # 5 件刻みで表示 row = M[i].toarray().flatten() top_idx = row.argsort()[-3:][::-1] top_words = [features[j] for j in top_idx] print(f" {d.iloc[i]['都道府県']:<6s}: {', '.join(top_words)}") |
📤 実行結果:
💬 結果の読み方:各都道府県の数値情報がそのまま「最特徴語」として浮上。 IDF 大 (他に出ない数値)、 TF 大 (自身の文書に出る) で TF-IDF 最大値。 TF-IDF は「数値の指紋」も検出できる。 実際のテキスト分析でも、 ユニークな専門用語が高 TF-IDF で表面化する。
🎯 このコードでやること:TF-IDF ベクトルで 47 都道府県を 4 クラスタに分け、 「特徴の似た都道府県群」を発見。
📥 入力データ:M (47, 191) を KMeans に入力。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.cluster import KMeans from collections import defaultdict km = KMeans(n_clusters=4, random_state=0, n_init=10).fit(M) groups = defaultdict(list) for i, c in enumerate(km.labels_): groups[c].append(d.iloc[i]['都道府県']) for c, prefs in sorted(groups.items()): print(f"\nクラスタ {c} ({len(prefs)}県):") print(" " + ", ".join(prefs[:10]) + ("..." if len(prefs)>10 else "")) |
📤 実行結果:
💬 結果の読み方(退化に注意):seed 固定(random_state=0, n_init=10)で実行すると 7/28/5/7 県という大きく偏ったクラスタになります。 前のコードで見たとおり数値がトークン化され県間の共有語がほぼ無いため、 ベクトルはほぼ直交し、 クラスタ分けは主に高齢化率の階級値が一致する少数ペアに引きずられます(28 県の巨大クラスタは「他とほぼ無関係=寄せ集め」に近い)。 「大都市圏/小規模県」のようなきれいな分離にはなりません。 教訓:特徴設計(数値のビン化・正規化)を怠ると、 KMeans は意味あるクラスタを作れない。 数値テキストをそのまま使う限界を体験できる好例です。
tokenizer 引数に渡す。 ストップワード辞書も必要。rank_bm25 パッケージ。 Elasticsearch でクエリすると標準で BM25 採用。IDF $= -\log P(t)$ = 単語の自己情報量。 TF と掛け合わせ「情報量加重頻度」。
BM25 は Probability Ranking Principle から導出。 「関連文書である確率」を推定。
コサイン類似度 = 単位球面上の角度。 高次元疎では距離より角度が安定。
1972 年に IDF を提案、 「珍しさが識別力に直結」を示した。 IR の基礎。
1975 年に文書をベクトル化する枠組を確立。 現代 IR の礎。
SSDSE-B-2026 を使った段階的ハンズオン。 初学者→中級→上級と順に深めるシナリオ構成です。
SSDSE-B-2026 から 2023 年 47 都道府県の指標を文字列化。
TfidfVectorizer(min_df=1)。 47 文書なので min_df 緩めに。
M = v.fit_transform(docs)。 形状 (47, ~200)。
v.get_feature_names_out()[:20] で最初の 20 語。
v.idf_。 大 IDF = レア語 = 識別力大。
cosine_similarity(M[0], M) で 1 都道府県 vs 全 46 県。
argsort() で上位 5 件を抽出。
ラベル列 (例:人口減少率) を作り LogisticRegression で予測。
KMeans(n_clusters=4).fit(M) で類似都道府県群を発見。
BM25Okapi でランキングを TF-IDF と比較。
実務で頻用するコード・概念・公式を 1 ページにまとめた早見表。 印刷して机に貼っておくと便利です。
| 領域 | 項目 | 説明 |
|---|---|---|
| 作成 | TfidfVectorizer() | デフォルト設定 |
| 学習 | v.fit_transform(docs) | コーパスから TF-IDF 行列 |
| 変換 | v.transform(['新文書']) | 学習済みベクタイザで変換 |
| 語彙 | v.get_feature_names_out() | 語彙リスト |
| IDF | v.idf_ | 各単語の IDF |
| min_df | min_df=2 | 2 文書未満の語除去 |
| max_df | max_df=0.95 | 95% 超出現語除去 |
| ngram | ngram_range=(1,2) | uni + bigram |
| stop_words | stop_words='english' | 英語ストップワード |
| max_features | max_features=10000 | 上位 10k 語のみ |
| L2 正規化 | norm='l2' | コサイン類似度準備 |
| 対数 TF | sublinear_tf=True | 1+log(tf) |
| IDF 平滑化 | smooth_idf=True | +1 平滑化 |
| トークナイザ | tokenizer=lambda s: ... | カスタム分割 |
| コサイン類似 | cosine_similarity(M[0], M[1]) | ベクトル間類似度 |
| BM25 | from rank_bm25 import BM25Okapi | 改良版 |
| Hashing | HashingVectorizer() | オンライン用 (語彙無し) |
| Count | CountVectorizer() | TF のみ (IDF なし) |
| Pipeline | Pipeline([('tfidf', TfidfVectorizer()), ('clf', LogReg())]) | 前処理 + 分類 |
| MeCab | MeCab.Tagger('-Owakati') | 日本語分かち書き |
本編のコードに加え、 さらに 4 種の発展的コード例を 4 要素 (🎯/📥/📤/💬) 付きで提示。 段階的に「読む→動かす→改造する」を体験できます。
🎯 このコードでやること:同じクエリで BM25 と TF-IDF のランキングを比較。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from rank_bm25 import BM25Okapi docs = [r['都道府県']+' '+str(r['総人口']//10000)+'万' for _, r in d.iterrows()] tokenized = [doc.split() for doc in docs] bm25 = BM25Okapi(tokenized) query = '1000 万人'.split() bm25_scores = bm25.get_scores(query) tfidf_q = v.transform([' '.join(query)]) tfidf_scores = (M @ tfidf_q.T).toarray().flatten() import pandas as pd result = pd.DataFrame({ '都道府県': d['都道府県'].values, 'BM25': bm25_scores, 'TF-IDF': tfidf_scores }).sort_values('BM25', ascending=False).head(5) print(result) |
📤 実行結果:
💬 結果の読み方(クエリ側も同じ落とし穴):クエリ '1000 万人'.split() は ['1000', '万人'] になりますが、 文書側のトークンは「924万」「509万」のように数値と単位が一体で、 「1000」も「万人」も語彙に存在しません。 結果、 BM25・TF-IDF とも全県スコア 0(並び順は元の県順のまま)。 これは前述の tokenizer 退化がクエリ照合でも同様に起きることを示します。 意味あるランキングを得るには、 文書とクエリでトークン化の粒度を揃える(例:数値を「1000」「万」に分割)必要があります。 BM25 の理論的利点(頻度飽和・文書長正規化)を体感するには、 まず語彙が噛み合う前処理が前提です。
🎯 このコードでやること:都道府県プロフィール文書を「人口 100 万以上か否か」で 2 値分類。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 | from sklearn.svm import LinearSVC from sklearn.metrics import classification_report labels = (d['総人口'] > 1_000_000).astype(int).values clf = LinearSVC().fit(M, labels) pred = clf.predict(M) print(classification_report(labels, pred)) |
📤 実行結果:
💬 結果の読み方(=リーク気味の満点):ラベルは「総人口 > 100 万」(該当 37 県/非該当 10 県)で定義。 実行すると学習データ上で Accuracy 1.00(support は 10/37)になります。 これは分類器が優秀というより、 各文書に「人口○○万」という人口そのものを表すトークンが入っているため、 高次元疎な線形 SVM がほぼ暗記してしまうから。 訓練データで評価している点も含め過学習・情報リークの典型で、 汎化性能の指標にはなりません。 正しく評価するなら人口関連トークンを除く、 または train/test 分割が必須です。
🎯 このコードでやること:TF-IDF 行列を SVD で 5 次元に圧縮し、 潜在意味次元を抽出。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 | from sklearn.decomposition import TruncatedSVD svd = TruncatedSVD(n_components=5, random_state=0).fit(M) print(f"説明分散比: {svd.explained_variance_ratio_}") print(f"累積: {svd.explained_variance_ratio_.cumsum()}") # 5 次元埋め込み emb = svd.transform(M) print(f"\n5 次元埋め込み shape: {emb.shape}") print(f"東京都の埋め込み: {emb[12]}") |
📤 実行結果:
💬 結果の読み方(低い分散=ここでも退化):191 次元 → 5 次元に圧縮しても累積寄与率はわずか 15.4%(各成分 2〜4%)。 これも tokenizer 退化の帰結で、 ベクトルがほぼ直交しているため少数の潜在次元では説明しきれません(意味ある LSA では上位数次元で数十%に達するのが普通)。 東京都の埋め込みも全成分が 0 近傍で、 5 次元では都道府県を十分表現できていません。 LSA の威力を出すには、 まず語彙が共有される前処理が必要という点まで含めて教材化できます。
🎯 このコードでやること:47 都道府県全てについて TF-IDF 上位 5 語を表示。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 | import numpy as np features = v.get_feature_names_out() print("都道府県別 TOP 5 特徴語:\n" + "="*60) for i in range(47): row = M[i].toarray().flatten() top_idx = row.argsort()[-5:][::-1] words = [features[j] for j in top_idx] print(f"{d.iloc[i]['都道府県']:<6s}: {', '.join(words)}") |
📤 実行結果:
💬 結果の読み方:47 県すべてに固有の特徴語が抽出される。 数値の桁が各県のサイズを反映、 県名自体も 1 文書にしか出ないため高 TF-IDF。
ここまでのコードは数値を文字列にした「プロフィール文書」でしたが、SSDSE-B-2026 には本物の日本語テキストも入っています。2 行目の列名(「65歳以上人口」「一般病院数」など、年度・地域コード・都道府県を除く 109 個)です。1 つの指標名を 1 文書とみなし、形態素解析(janome)で名詞に分けてから TF-IDF にかけ、IDF の大小と、検索語に近い指標名を調べます。
🎯 このコードでやること:SSDSE-B-2026 の指標名 109 個を janome で名詞に分けて TF-IDF 行列を作り、文書頻度 df と IDF の上位語を表にし、「65歳以上人口(女)」に近い指標名をコサイン類似度で 5 件探す。
📥 入力データ:SSDSE-B-2026.csv の 2 行目(日本語の列名)のうち 4 列目以降の 109 個。例:「65歳以上人口」→「65 歳 以上 人口」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import numpy as np import pandas as pd from janome.tokenizer import Tokenizer from sklearn.feature_extraction.text import TfidfVectorizer names = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=2, header=None).iloc[1, 3:].tolist() tok = Tokenizer() def words(s): # 名詞(数字を含む)だけを残し、助詞・記号は捨てる return [t.surface for t in tok.tokenize(s) if t.part_of_speech.startswith('名詞')] docs = [' '.join(words(s)) for s in names] print(len(docs), '個の指標名。例:', names[12], '→', docs[12]) vec = TfidfVectorizer(tokenizer=str.split, token_pattern=None, lowercase=False) X = vec.fit_transform(docs) idf = pd.Series(vec.idf_, index=vec.get_feature_names_out()) df_count = pd.Series(np.asarray((X > 0).sum(0)).ravel(), index=idf.index) tab = pd.DataFrame({'df': df_count, 'IDF': idf.round(3)}).sort_values('df', ascending=False) print(tab.head(6).to_string()) print('IDF 最大(df=1)の語の例:', ', '.join(tab[tab.df == 1].index[:6])) q = vec.transform([' '.join(words('65歳以上人口(女)'))]) sim = (X @ q.T).toarray().ravel() top = np.argsort(-sim)[:5] print('クエリ「65歳以上人口(女)」に近い指標名:') for i in top: print(f' {sim[i]:.3f} {names[i]}') |
📤 実行結果:
💬 結果の読み方:最も多くの指標名に出る語は「数」(出生数・死亡数・病院数など 61 個)で、IDF は ln((1+109)/(1+61)) + 1 = 1.573 と最小です。逆に「食料」「病院」のように 1 つの指標名にしか出ない語は IDF が最大になります。検索では「65歳以上人口(女)」自身が 1.000、「65歳以上人口」が 0.897、男性版が 0.805 と、共有する語が多い順に並び、「総人口(女)」(0.583)は「人口」と「女」だけを共有するので下がります。TF-IDF が「どの語を共有しているか」と「その語がどれだけ珍しいか」だけで近さを決めていることが、実際の列名で確かめられます。
IDF の効き目を確かめるため、「病院数」で検索したときの結果を、出現回数だけのベクトル(TF)と TF-IDF で比べます。どちらも長さ 1 に正規化したうえで余弦類似度を計算します。
🎯 このコードでやること:指標名 109 個について、出現回数だけのベクトルと TF-IDF ベクトルで「病院数」を検索し、類似度が 0 より大きい指標名の数と上位 4 件を比べる。
📥 入力データ:上と同じ 109 個の指標名(janome で名詞に分割)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import numpy as np import pandas as pd from janome.tokenizer import Tokenizer from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer from sklearn.preprocessing import normalize names = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=2, header=None).iloc[1, 3:].tolist() tok = Tokenizer() def words(s): return [t.surface for t in tok.tokenize(s) if t.part_of_speech.startswith('名詞')] docs = [' '.join(words(s)) for s in names] opt = dict(tokenizer=str.split, token_pattern=None, lowercase=False) query = ' '.join(words('病院数')) for name, v in [('出現回数のみ (TF)', CountVectorizer(**opt)), ('TF-IDF', TfidfVectorizer(**opt))]: X = normalize(v.fit_transform(docs).astype(float)) # どちらも長さ 1 に揃えて余弦類似度 q = normalize(v.transform([query]).astype(float)) sim = (X @ q.T).toarray().ravel() order = np.argsort(-sim, kind='stable') print(f'{name}: 類似度 > 0 の指標 {int((sim > 0).sum())} 個 / 上位 4 位') for i in order[:4]: print(f' {sim[i]:.3f} {names[i]}') |
📤 実行結果:
💬 結果の読み方:どちらも 1 位は「病院」を含む唯一の指標名「一般病院数」ですが、2 位以下が違います。出現回数だけでは「数」を共有するだけの出生数・死亡数・幼稚園数などが類似度 0.500 で同点に並び、検索語と関係の薄い指標名が上位を埋めます。TF-IDF では「数」の重みが IDF 1.573 に抑えられるので、2 位以下は 0.112 以下に下がり、1 位との差がはっきりします。類似度が 0 より大きい指標名は 61 個で変わらないので、IDF は「どれがヒットするか」ではなく「ヒットした文書の並び方」を変えていることが分かります。
TF-IDF を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。
log_e、 教科書は log_2 や log_10 が多い。 ライブラリ間で値が違うのは底の違い。min_df で足切りを。※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。
初学者を超えた中級者がハマる「2 周目の失敗例」を集めました。 本編の 5 件と合わせて 15 件のチェックリストとして活用してください。
token_pattern で英数字+ハイフンを許可しないとちぎれる。fit_transform をテストにも適用するとリーク。 train で fit、 test は transform のみ。本編の 10 語に加え、 さらに専門用語 20 個を整理。 文献を読むときの「分からない単語チェッカー」として使えます。
k1=1.2 (頻度飽和)、 b=0.75 (文書長正規化)。 経験値。joblib.dump(v, 'v.pkl') で永続化。 推論時に joblib.load。HashingVectorizer + TfidfTransformer。 ただし IDF は再計算。match クエリで BM25 自動。 大規模全文検索の定番。47 都道府県 × 12 年分のデータを使い、 TF-IDF を多角的に体験する総合演習。 単発の操作ではなく、 一連の分析フローを通して理解を深めます。
🎯 このコードでやること:SSDSE-B-2026 を TF-IDF で類似県発見
📥 入力:SSDSE-B-2026.csv の 47 都道府県データ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) d = df[df['年度']==2023].reset_index(drop=True) d['高齢化率'] = (d['65歳以上人口']/d['総人口']*100).astype(int) docs = [] for _, r in d.iterrows(): docs.append(f"{r['都道府県']} 人口{r['総人口']//10000}万 出生{r['出生数']//100}百 高齢{r['高齢化率']}") v = TfidfVectorizer() M = v.fit_transform(docs) sim = cosine_similarity(M) # 各県の TOP3 類似県 for i in range(47): s = sim[i].copy() s[i] = -1 top3 = s.argsort()[-3:][::-1] cs = ', '.join([d.iloc[j]['都道府県'] for j in top3]) if i < 5: print(f"{d.iloc[i]['都道府県']:<6s} ~ {cs}") |
📤 実行結果:
💬 結果の読み方(退化に注意):類似度はほぼ全ペアで 0 か極小で、 上位に出るのは「高齢◯◯」の階級値がたまたま一致した県だけ(例:北海道と宮崎・鹿児島は cos≈0.13、 秋田は非ゼロの相手が無く同率 0 の並びに意味なし)。 「東北は東北と似る」のような地理的近縁性は再現されない点に注意。 本編コード 2 で見た tokenizer 退化(数値が丸ごと 1 トークン化され県間の共有語がほぼ無い)がここでも起きている。 意味のある類似県探索には数値のビン化・トークン分割などの特徴設計が必須。
TF-IDF は語の一致しか見ないので、同じ意味でも違う言葉で書かれた文書は見つけられません。上の指標名コーパスで、言い換えた検索語と語順を入れ替えた検索語を試します。
🎯 このコードでやること:指標名 109 個の TF-IDF 行列に対して「高齢者の人口」「65歳以上人口」「お年寄り」「女 人口」「人口 女」を検索し、語彙に含まれた語と上位 3 件を表示する。
📥 入力データ:上と同じ 109 個の指標名(janome で名詞に分割)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import numpy as np import pandas as pd from janome.tokenizer import Tokenizer from sklearn.feature_extraction.text import TfidfVectorizer names = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', nrows=2, header=None).iloc[1, 3:].tolist() tok = Tokenizer() def words(s): return [t.surface for t in tok.tokenize(s) if t.part_of_speech.startswith('名詞')] docs = [' '.join(words(s)) for s in names] vec = TfidfVectorizer(tokenizer=str.split, token_pattern=None, lowercase=False) X = vec.fit_transform(docs) def search(query, k=3): q = vec.transform([' '.join(words(query))]) sim = (X @ q.T).toarray().ravel() known = [w for w in words(query) if w in vec.vocabulary_] hits = [f'{names[i]}({sim[i]:.2f})' for i in np.argsort(-sim)[:k] if sim[i] > 0] print(f'「{query}」 語彙にある語 {known} → {hits if hits else "一致なし"}') search('高齢者の人口') # 「高齢者」は指標名に無い言い方 search('65歳以上人口') search('お年寄り') search('女 人口') # 語順を入れ替えても search('人口 女') # 同じベクトルになる |
📤 実行結果:
💬 結果の読み方:「高齢者の人口」は、指標名に「高齢」という語が無いため、一致するのは「者」と「人口」だけで、1 位は「総人口」(0.73)になり、目的の「65歳以上人口」は上位に来ません。指標名と同じ言い方の「65歳以上人口」なら 1.00 で一致します。「お年寄り」は語彙に 1 語も無いので一致なしです。また「女 人口」と「人口 女」は結果が完全に同じで、語順の情報は使われていません。言い換えに対応するには、同義語辞書でクエリを展開するか、単語や文の意味をベクトルにする埋め込み(Word2Vec・BERT 系)を使います。
Q1. 109 個の指標名のうち「数」を含むのは 61 個。sklearn の既定(smooth_idf=True)で「数」の IDF を手で計算せよ。
→ 解答:ln((1+109)/(1+61)) + 1 = ln(1.774) + 1 ≈ 0.573 + 1 = 1.573。コードの出力と一致する。
Q2. 「病院数」で検索すると、TF でも TF-IDF でも類似度が 0 より大きい指標名は 61 個だった。なぜ同じ数になるのか。
→ 解答:類似度が 0 より大きくなる条件は「クエリと共有する語が 1 つでもある」ことで、IDF は語の重みを変えるだけで 0 にはしない(smooth_idf では最小でも 1)。したがってヒットする文書の集合は同じで、並び順だけが変わる。
Q3. 「高齢者の人口」で「65歳以上人口」が見つからなかった。TF-IDF のままで見つけられるようにするには、どこを変えればよいか。
→ 解答:クエリ側で「高齢者」を「65 歳 以上」に展開する同義語辞書を入れる。文書とクエリで同じ語が使われて初めて TF-IDF の類似度が生じるので、語彙をそろえる前処理が必要になる。
TF-IDF を中心に、 単語頻度 (TF) × 逆文書頻度 (IDF) という掛け算が、 BoW・コサイン類似度・文書分類・検索ランキングへどう派生するかを整理。
中心の「TF-IDF」から、 上に BoW (基礎)、 右上に コサイン類似度 (組合せ)、 右下に Word2Vec / BERT (発展)、 下に LSA / LDA (応用)、 左下に Sublinear TF (拡張)、 左上に「落とし穴 (語順無視・OOV)」が並ぶ。 SSDSE-B-2026 の都道府県統計を「文書」、 47 県を「コーパス」と見立てて TF-IDF を計算すると、 各県の特徴語 (北海道なら「畑」「酪農」、 沖縄なら「観光」) を抽出できる。
TF-IDF は「テキスト → 数値ベクトル」の橋渡し役で、 前処理・後段の手法と直結する。
SSDSE-B-2026 の県別記述文を corpus とした場合、 TF-IDF 行列 (47 県 × 語彙数) を SVD で 50 次元に圧縮 → コサイン類似度 で「東京 ≈ 大阪 ≈ 愛知」を確認、 という pipeline が典型。
テキスト解析で TF-IDF を使うか、 別の表現を選ぶかは 4 つの分岐で決まる。
SSDSE-B-2026 の県別統計コメントのように 定型語彙が多い (n=47, 語彙 ~500) ケースでは TF-IDF が最適。 BERT は overkill で計算コストが過大。
下のスライダーで 3 つの文書 (A / B / C) における各単語の出現回数を変えると、 TF(語頻度)・ IDF(レア度 ln(N/df))・ TF-IDF = TF × IDF が右の表とバーにリアルタイムで反映されます。 「人口」を 3 文書すべてに入れると IDF が 0 に潰れ、 特定文書に偏る「経済」「高齢」ほどバーが伸びる —— この重み付けを指で体感してください。
① 各文書での単語頻度をスライダーで調整(タップ&ドラッグ対応)
注目する文書:
② 注目文書に現れる語の TF・IDF・TF-IDF(TF-IDF 降順)
③ TF-IDF バー(大きい語=「その文書らしい」語)— バーに触れると詳細表示
バーに触れると詳細を表示
💡 直感: 「珍しくて(=多くの文書には出ない)、 その文書には何度も出る」語ほど重要。 全文書に出る語(the・です・人口)は IDF = ln(N/df) が 0 に近づき TF-IDF が小さくなる。 単語の単位を変えたいときは n-gram を参照。
⚠️ よくある落とし穴: (1) 正規化 — scikit-learn は既定で L2 正規化するため、 素朴な TF×IDF と絶対値は異なる(大小関係の直感は概ね保たれる)。 (2) ストップワード — 「は・が・the」は IDF で自然に弱まるが、 事前除去でノイズが減る。 日本語では 形態素解析・分かち書き で語に分ける前処理が前提。 (3) 対数の底 — 本デモは自然対数 ln(底 e)。 教科書の log2・log10 やライブラリの平滑化で数値は変わるが、 順位は概ね不変。
🚀 発展: BM25(文書長で TF を飽和させる確率的ランキング。 検索で TF-IDF より高精度・専用ページは未整備)/ 単語の意味を捉える 埋め込み・Transformer(BERT)/ ベクトル化後は コサイン類似度 で テキスト類似度 を測る。 Bag-of-Words(語順を捨てた頻度ベクトル)は TF-IDF の土台となる表現。
log_e、 教科書は log_2 や log_10 が多い。 ライブラリ間で値が違うのは底の違い。min_df で足切りを。token_pattern で英数字+ハイフンを許可しないとちぎれる。fit_transform をテストにも適用するとリーク。 train で fit、 test は transform のみ。TF-IDF は「NLP」分野の中で発展してきた概念・手法です。 学術的には継続的な研究で精緻化され、 実務的にはツール・ライブラリの普及で誰でも使えるようになってきました。 用語の使い方・意味は時代と分野で少しずつ変わるため、 文脈に応じた解釈が大切です。 入門書だけでなく、 標準的な教科書(例:データサイエンス・統計学の定本)や信頼できるオンライン教材も併用すると、 ぶれない理解に近づけます。
本編では都道府県データを「プロフィール文書」に変換して TF-IDF を適用しました。 ここでは逆向きの角度から掘り下げます。 文章を一切使わずに、 SSDSE-B-2026 の数値そのものから「トークン」を作り、 IDF という考え方の正体(=珍しさの対数)と、 文章では見えにくい弱点を露出させます。
IDF(t) = log(N / df(t)) は、 「N 文書から 1 つ引いたとき、 語 t を含む文書を引き当てる確率 df/N」の逆数の対数、 つまり情報理論でいう 自己情報量(サプライザル)−log(df/N) そのものです。 「みんなが持っている語は驚きゼロ、 少数派だけが持つ語は驚き大」。 この見方に立つと、 トークンは単語である必要がない ことに気づきます。 「ある性質を持つ/持たない」という 0/1 のメンバーシップさえ定義できれば、 どんなデータにも IDF は計算できます。
実験:SSDSE-B-2026 の 2023 年・47 都道府県 × 数値 109 列で、 「変数 X が全国平均を上回る」ことを 1 トークンとみなします(47 文書 × 109 語彙の 0/1 行列)。 以下は Python で実際に計算した 実測値 です(IDF は自然対数)。
| トークン(平均超えの変数) | df(該当県数) | IDF = ln(47/df) |
|---|---|---|
| 外国人延べ宿泊者数(G7102) | 8 | 1.77(最レア級) |
| 着工新設分譲住宅数(H1803) | 8 | 1.77 |
| 総人口(A1101) | 12 | 1.37 |
| 年平均気温(B4101) | 26 | 0.59 |
| 消費支出・二人以上世帯(L3221) | 28 | 0.52(最コモン) |
「外国人延べ宿泊者数が平均超え」なのは北海道・千葉・東京・神奈川・京都・大阪・福岡・沖縄の 8 道都府県だけ。 このトークンを持つことは県の性格を強く語る(IDF 高)のに対し、 「消費支出が平均超え」は 28 県が持つため、 ほとんど個性を語りません(IDF 低)。 ストップワードが自動的に軽くなる仕組みと完全に同型です。
① 小さいコーパスでは IDF のダイナミックレンジが潰れる。 N=47 のとき IDF の理論上の最大値は ln(47/1) = 3.85、 上の実測でも 0.52〜1.77 の幅しかありません。 数千〜数百万文書を前提に設計された IDF は、 文書数が少ないと「レア語ボーナス」がわずかしか付かず、 重み付けの効果が薄れます。 小規模データで TF-IDF の効きが悪いのは実装ミスではなく数学的必然です。
② トークン化(離散化)の設計ひとつで IDF は崩壊する。 上の実験でしきい値を「平均」から「中央値」に変えると、 df は必ず半数近く(実測で 21〜23 県)になり、 IDF は 0.715〜0.806 の狭い帯に 全 109 変数が押し込まれて しまいます。 珍しさの情報が消え、 全語が同じ重みに。 テキストでも同じで、 形態素解析の粒度・ストップワード辞書・n-gram の切り方という「トークン化の設計」が、 数式より先に結果を決めます。
③ 「長い文書」問題は構造化データにも現れる。 平均超えトークンの保有数を数えると、 東京都は 109 変数中 102 個、 福岡県 101 個、 神奈川県・愛知県 99 個。 一方で愛媛県はわずか 3 個(合計特殊出生率・年平均気温・最低気温)、 青森県 4 個、 秋田県 5 個でした。 SSDSE-B の多くが人口規模に比例する「総量」変数のため、 大きな県が何でも持つ「長文書」になるのです。 長い文書ほどあらゆる語を含んでしまい類似度計算を支配する——TF の文書長正規化や BM25 のパラメータ b が解決しようとしているのは、 まさにこの規模効果です。 都道府県分析なら「人口あたり」に直してから比較するのが定石です。
※ 本節の数値はすべて SSDSE-B-2026(2023 年・47 都道府県・数値 109 列)からの実測値です(IDF は自然対数)。 合成データ・仮想例は使用していません。