Skip to the content.

← 目次← 前: 28次: 30 →

29. テキスト処理と正規表現

テーマ: 文書集合から TF-IDF を自前実装し、特徴語抽出と文書類似度を求める

学習点: re モジュール, collections.Counter, 集合演算, コサイン類似度, 辞書内包表記

依存: 標準ライブラリのみ / 難易度: 上級

実行方法

uv run 29_text_tfidf.py

スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは uv が自動的に仮想環境へ導入します。事前の pip install は不要です。

解説

何をするプログラムか

検索エンジンの順位付け、ニュース記事の自動分類、類似文書の推薦——これらの土台にあるのが「文書を語の重みベクトルで表す」という考え方です。本スクリプトは、金融政策・財政政策・企業経営・情報システム・インフレという経済・経営分野の 5 つの短い文書を材料に、TF-IDF(語の重要度)を標準ライブラリだけで自前実装し、各文書の特徴語抽出と文書間のコサイン類似度計算を行います。後半では、メールアドレス・電話番号・金額・日付などをテキストから抜き出す正規表現の実用例も扱います。

コードの読みどころ

理論的背景

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%

← 目次← 前: 28次: 30 →