29. テキスト処理と正規表現
テーマ: 文書集合から TF-IDF を自前実装し、特徴語抽出と文書類似度を求める
学習点: re モジュール, collections.Counter, 集合演算, コサイン類似度, 辞書内包表記
依存: 標準ライブラリのみ / 難易度: 上級
実行方法
uv run 29_text_tfidf.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
検索エンジンの順位付け、ニュース記事の自動分類、類似文書の推薦——これらの土台にあるのが「文書を語の重みベクトルで表す」という考え方です。本スクリプトは、金融政策・財政政策・企業経営・情報システム・インフレという経済・経営分野の 5 つの短い文書を材料に、TF-IDF(語の重要度)を標準ライブラリだけで自前実装し、各文書の特徴語抽出と文書間のコサイン類似度計算を行います。後半では、メールアドレス・電話番号・金額・日付などをテキストから抜き出す正規表現の実用例も扱います。
コードの読みどころ
- 正規表現
TOKEN = re.compile(r"[一-龥ァ-ヴー]{2,}|[A-Za-z]{3,}")は「漢字・カタカナが 2 文字以上連続する部分」を語とみなす簡易トークナイザです。形態素解析器を使わずに日本語の内容語を粗く拾う工夫で、STOP集合で「ある」「こと」などの機能語を除外します。 tfidf()の中のdf.update(set(ts))が要点です。トークン列をいったんsetにすることで、同じ語が 1 文書に何回出ても文書頻度 df は 1 回だけ数えられます。ここが語頻度 tf と文書頻度 df の違いです。- TF-IDF ベクトルは辞書内包表記
{t: (n / total) * idf[t] for t, n in c.items()}の 1 行で構築されます。 cosine()はa.keys() & b.keys()という辞書キーの集合積で「両方の文書に現れる語」だけを取り出して内積を計算します。疎なベクトル同士の内積を効率よく求める定石です。- 正規表現の実用例では、
re.findallによる抽出に加えて、re.subの置換関数にlambda m: f"{m[1]}年{int(m[2])}月{int(m[3])}日"を渡して2026-08-09と2026/8/9の混在表記を統一する例、(?P<year>\d{4})の名前付きグループで財務データを辞書としてパースする例を示しています。
理論的背景
| tf-idf(t, d) = tf(t, d) × log(N / df(t)) で、tf は文書 d 内での語 t の出現割合、df は語 t を含む文書数、N は総文書数です。多くの文書に現れる語ほど log(N/df) が小さくなり重みが下がる、という逆文書頻度の発想は Sparck Jones(1972)に遡ります。文書間の類似度にはコサイン類似度 (a·b)/( | a | b | ) を使います。ベクトルをその長さで割るため、文書の長短の影響を受けずに「語の使われ方の向き」だけを比較できます。 |
実行結果の見方
特徴語の表では、D2 財政政策の「政府支出」「拡大」が 0.2012 と突出しています。これらは D2 に何度も出て他の文書には出ないため、tf と idf の両方が大きくなるからです。逆に「企業」「総需要」は 5 文書中 3 文書に現れるため idf = 0.5108 と小さく、特徴語になりません。類似度行列では D1 金融政策と D5 インフレの 0.169 が最大で、「政策金利」「中央銀行」「物価」といった共通語彙を持つという直観と一致します。後半の正規表現では、2 つの日付表記がともに「2026年8月9日」に正規化されていることを確認してください。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""29: テキスト処理と正規表現 ---------------------------------------------
テーマ: 文書集合から TF-IDF を自前実装し、特徴語抽出と文書類似度を求める
学習点: re モジュール, collections.Counter, 集合演算, コサイン類似度,
辞書内包表記
根拠: tf-idf(t,d) = tf(t,d) × log(N / df(t))。df は語 t を含む文書数。
稀な語ほど重みが大きくなる(Sparck Jones 1972 の逆文書頻度)。
コサイン類似度 = (a·b)/(|a||b|) は文書長の影響を除いた類似度尺度。
"""
import math
import re
from collections import Counter
DOCS = {
"D1 金融政策": """
中央銀行は物価の安定を目的として政策金利を操作する。政策金利の引き上げは
企業の資金調達コストを高め、投資を抑制することで総需要を減少させ、
インフレ率を低下させる。ただし金融政策の効果には長く可変のラグがある。
""",
"D2 財政政策": """
政府支出の拡大は総需要を直接押し上げる。乗数効果により、追加的な政府支出は
国民所得をそれ以上に増加させうる。ただし財政赤字の拡大は金利を押し上げ、
民間投資を締め出すクラウディングアウトを招く可能性がある。
""",
"D3 企業経営": """
企業は限られた経営資源を配分して競争優位を構築する。差別化戦略とコスト
リーダーシップ戦略はトレードオフの関係にあるとされてきたが、近年は
デジタル技術によって両立する事例も報告されている。
""",
"D4 情報システム": """
情報システムの導入は業務プロセスの標準化を伴う。企業は既存業務に合わせて
システムを改造するか、システムに合わせて業務を変えるかの選択に直面する。
この意思決定は導入コストと組織の競争優位の双方に影響する。
""",
"D5 インフレ": """
インフレ率の上昇は実質賃金を低下させ、家計の購買力を削ぐ。中央銀行は
インフレ期待の安定を重視し、政策金利の経路を市場に伝達する。
物価と賃金の相互作用は総需要の動向に依存する。
""",
}
# 日本語の内容語らしきものを粗く抽出する(形態素解析器なしの簡易版)
TOKEN = re.compile(r"[一-龥ァ-ヴー]{2,}|[A-Za-z]{3,}")
STOP = {"ある", "する", "こと", "もの", "これ", "それ", "ため", "よう",
"など", "この", "その", "から", "まで", "ただし", "近年", "双方"}
def tokenize(text: str) -> list[str]:
return [t for t in TOKEN.findall(text) if t not in STOP]
def tfidf(docs: dict[str, str]):
tokens = {k: tokenize(v) for k, v in docs.items()}
N = len(docs)
df = Counter()
for ts in tokens.values():
df.update(set(ts))
idf = {t: math.log(N / d) for t, d in df.items()}
out = {}
for k, ts in tokens.items():
c = Counter(ts)
total = sum(c.values())
out[k] = {t: (n / total) * idf[t] for t, n in c.items()}
return out, idf, df, tokens
def cosine(a: dict, b: dict) -> float:
common = a.keys() & b.keys()
num = sum(a[t] * b[t] for t in common)
da = math.sqrt(sum(v * v for v in a.values()))
db = math.sqrt(sum(v * v for v in b.values()))
return num / (da * db) if da and db else 0.0
def main() -> None:
vecs, idf, df, tokens = tfidf(DOCS)
print("[各文書の特徴語 上位5(TF-IDF)]")
for k, v in vecs.items():
top = sorted(v.items(), key=lambda kv: -kv[1])[:5]
print(f" {k:<12} " + " / ".join(f"{t}({s:.4f})" for t, s in top))
print("\n[逆文書頻度が小さい=どこにでも出る語(特徴語になりにくい)]")
for t, d in df.most_common(6):
print(f" {t:<10} df={d}/{len(DOCS)} idf={idf[t]:.4f}")
print("\n[文書間コサイン類似度]")
keys = list(DOCS)
print(" " + "".join(f"{k[:2]:>8}" for k in keys))
for a in keys:
print(f" {a[:2]:<5}" + "".join(f"{cosine(vecs[a], vecs[b]):>8.3f}"
for b in keys))
pairs = [(cosine(vecs[a], vecs[b]), a, b)
for i, a in enumerate(keys) for b in keys[i + 1:]]
s, a, b = max(pairs)
print(f"\n 最も似ている組: {a} と {b} (類似度 {s:.3f})")
print(" ※ 金融政策とインフレは共通語彙が多いため高くなる。")
print("\n[正規表現の実用例]")
text = ("問い合わせは info@example.co.jp または support@univ.ac.jp まで。"
"電話 03-1234-5678 / 090-8765-4321。売上は 1,234,567円 で "
"前年比 +12.3% 、URLは https://example.com/report?id=42 です。"
"日付は 2026-08-09 と 2026/8/9 の表記が混在。")
patterns = {
"メール": r"[\w.+-]+@[\w-]+\.[\w.-]+",
"電話番号": r"0\d{1,3}-\d{2,4}-\d{4}",
"金額": r"[\d,]+円",
"パーセント": r"[+-]?\d+(?:\.\d+)?%",
"URL": r"https?://[^\s、。]+",
"日付": r"\d{4}[-/]\d{1,2}[-/]\d{1,2}",
}
for name, pat in patterns.items():
print(f" {name:<8}{re.findall(pat, text)}")
print("\n[置換とグループ参照] 日付表記を統一する")
normalized = re.sub(r"(\d{4})[-/](\d{1,2})[-/](\d{1,2})",
lambda m: f"{m[1]}年{int(m[2])}月{int(m[3])}日", text)
print(" ", re.findall(r"\d{4}年\d{1,2}月\d{1,2}日", normalized))
print("\n[名前付きグループ] 財務データのパース")
line = "2025Q4 revenue=1543.2 profit=210.8 margin=13.66%"
m = re.match(r"(?P<year>\d{4})Q(?P<q>\d) revenue=(?P<rev>[\d.]+) "
r"profit=(?P<prof>[\d.]+)", line)
if m:
print(f" {m.groupdict()}")
print(f" 利益率の再計算 = "
f"{float(m['prof'])/float(m['rev']):.2%}")
if __name__ == "__main__":
main()
実行結果
[各文書の特徴語 上位5(TF-IDF)]
D1 金融政策 政策金利(0.0965) / 目的(0.0847) / 操作(0.0847) / 資金調達コスト(0.0847) / 投資(0.0847)
D2 財政政策 政府支出(0.2012) / 拡大(0.2012) / 直接押(0.1006) / 乗数効果(0.1006) / 追加的(0.1006)
D3 企業経営 経営資源(0.1150) / 配分(0.1150) / 構築(0.1150) / 差別化戦略(0.1150) / コスト(0.1150)
D4 情報システム システム(0.1893) / 情報システム(0.0947) / 導入(0.0947) / 業務プロセス(0.0947) / 標準化(0.0947)
D5 インフレ 上昇(0.0805) / 実質賃金(0.0805) / 家計(0.0805) / 購買力(0.0805) / インフレ期待(0.0805)
[逆文書頻度が小さい=どこにでも出る語(特徴語になりにくい)]
企業 df=3/5 idf=0.5108
総需要 df=3/5 idf=0.5108
インフレ率 df=2/5 idf=0.9163
中央銀行 df=2/5 idf=0.9163
安定 df=2/5 idf=0.9163
物価 df=2/5 idf=0.9163
[文書間コサイン類似度]
D1 D2 D3 D4 D5
D1 1.000 0.006 0.008 0.007 0.169
D2 0.006 1.000 0.000 0.000 0.006
D3 0.008 0.000 1.000 0.029 0.000
D4 0.007 0.000 0.029 1.000 0.000
D5 0.169 0.006 0.000 0.000 1.000
最も似ている組: D1 金融政策 と D5 インフレ (類似度 0.169)
※ 金融政策とインフレは共通語彙が多いため高くなる。
[正規表現の実用例]
メール ['info@example.co.jp', 'support@univ.ac.jp']
電話番号 ['03-1234-5678', '090-8765-4321']
金額 ['1,234,567円']
パーセント ['+12.3%']
URL ['https://example.com/report?id=42']
日付 ['2026-08-09', '2026/8/9']
[置換とグループ参照] 日付表記を統一する
['2026年8月9日', '2026年8月9日']
[名前付きグループ] 財務データのパース
{'year': '2025', 'q': '4', 'rev': '1543.2', 'prof': '210.8'}
利益率の再計算 = 13.66%