テキストデータ抽出テクニック

業務テキストから「〜以上」「〜未満」を捉える:Pythonによる範囲・比較量情報抽出

Tags: Python, NLP, SpaCy, 情報抽出, テキスト分析, 量情報

はじめに:業務テキストにおける範囲・比較量情報の重要性と抽出の課題

日々の業務で扱うテキストデータには、数値だけでなく、その数値が示す範囲や比較に関する情報が多く含まれています。例えば、

これらのテキストから、単なる数値を抽出するだけでなく、「以下」「以上」「未満」「超える」「から〜まで」「最大」「最小」「約」「〜倍」といった範囲や比較を示す表現と、それに付随する数値を正確に捉え、構造化された情報として利用できることは、システム開発、データ分析、ビジネスインテリゼンスなど、多くの場面で価値があります。

しかし、これらの情報を抽出する際には、単純な数値パターンだけでなく、その前後にある比較を示すキーワードや、数値が修飾する対象などを同時に考慮する必要があります。正規表現だけでは、多様な表現パターン全てに対応したり、数値と比較表現の正確な関係性を捉えたりすることが難しくなることがあります。

本記事では、Pythonを用いた自然言語処理(NLP)の手法を活用し、業務テキストから範囲や比較を伴う量情報を効率的かつ精度高く抽出するための具体的なアプローチを解説します。NLPライブラリであるSpaCyを主に使用し、その機能とPythonコードを組み合わせた実践的な手法を紹介します。

範囲・比較量情報の対象パターン

業務テキストによく出現する範囲や比較を伴う量情報のパターンには、以下のようなものが挙げられます。

これらのパターンは、数値表現(漢数字、アラビア数字、単位)と、比較や範囲を示すキーワード、そしてそれらの配置によって形成されます。多様な表現が存在するため、網羅的に、かつ正確に抽出するためには、単語の表面的なパターンだけでなく、単語間の関係性を考慮する必要があります。

シンプルな正規表現の限界

数値と特定のキーワードの組み合わせであれば、ある程度は正規表現で対応できます。例えば、「\d+年以上」のようなパターンです。しかし、以下のようなケースでは正規表現だけでは限界があります。

  1. 多様な表現: 「〜以上」「〜より大きい」「最低〜」など、同じ意味でも異なる表現があります。これら全てを正規表現で網羅しようとすると、非常に複雑になり保守が難しくなります。
  2. 単語間の距離や順序: 数値とキーワードが離れていたり、間に他の単語が挟まったりする場合、正規表現のパターンが複雑化します。(例: 「100件という多数の問い合わせ、それ以上の対応が必要」)
  3. 曖昧さ: 「いくつか」「多数」のような数値が特定できない表現や、「〜と思われる」といった不確かさを伴う表現の区別が困難です。
  4. 構造の理解: 「AからBまで」のような範囲表現で、AとBがそれぞれ下限・上限であることを自動的に識別するには、単語の並び以上の文脈理解が必要です。

これらの課題に対応するため、NLPライブラリが提供する品詞タグ付け、固有表現抽出、依存構造解析といった機能を活用します。

SpaCyを活用した抽出手法

SpaCyは、高速かつ高精度なNLP処理を提供するPythonライブラリです。単語分割、品詞タグ付け、依存構造解析、固有表現抽出といった基本的な処理を効率的に実行できます。これらの機能を組み合わせて、範囲や比較を伴う量情報を抽出するアプローチを考えます。

基本的な流れは以下のようになります。

  1. テキスト前処理: SpaCyを使ってテキストを処理し、Token(単語)やその属性(品詞、依存関係、固有表現など)を取得します。
  2. 数値と単位の特定: 固有表現として認識される数値(CARDINALなど)や、辞書やパターンで単位(円、ドル、個、GBなど)を特定します。
  3. 比較・範囲キーワードの特定: 「以上」「以下」「未満」「超える」「から」「まで」「約」「倍」「最低」「最大」といったキーワードを特定します。
  4. 数値とキーワードの関係性抽出: 依存構造解析の結果や、Token間の距離、順序、品詞などの情報を利用して、特定した数値とキーワードがどのような関係にあるかを判断します。
  5. 構造化データへの変換: 抽出した数値、単位、比較/範囲の種類(以上、未満、範囲など)を構造化データ(例: 辞書)として出力します。

特に、SpaCyのMatcherDependencyMatcherは、Tokenの属性や依存関係に基づいた柔軟なパターンマッチングに強力です。正規表現が文字列のパターンにマッチさせるのに対し、MatcherはTokenオブジェクトのリストに対して、品詞やテキスト内容などの属性に基づいたパターンにマッチさせます。DependencyMatcherは、さらにToken間の依存関係ツリー構造に基づいたマッチングが可能です。

具体的な実装例(Python + SpaCy)

ここでは、SpaCyのMatcherと依存構造解析の結果を組み合わせた抽出例を示します。

まず、SpaCyモデルをロードし、日本語テキストを処理できる準備をします。

import spacy

# 日本語モデルをロード(適宜インストールしてください例: pip install spacy-udpipe)
# または ja_core_news_sm など既存の日本語モデルを使用
# udpipeモデルのダウンロード例: python -m spacy download ja_udpipe
try:
    nlp = spacy.load("ja_udpipe") # UDPIPEモデルの方が依存構造解析の精度が高い場合が多い
except:
    try:
         nlp = spacy.load("ja_core_news_sm") # 標準の日本語モデル
         print("ja_core_news_sm モデルを使用します。")
    except:
         print("SpaCyの日本語モデルが見つかりません。ja_udpipeまたはja_core_news_smをインストールしてください。")
         nlp = None # モデルロード失敗

次に、比較・範囲キーワードとそれに関連する数値のパターンを定義し、Matcherで検出します。ここでは、簡易的なパターンをいくつか示します。

if nlp:
    from spacy.matcher import Matcher
    matcher = Matcher(nlp.vocab)

    # パターン定義例
    # 「数値 + 単位 + 以上/以下/未満/超」のようなパターン
    pattern_quantity_comparison = [
        {"POS": "NUM"}, # 数値
        {"POS": "NOUN", "OP": "*"}, # 単位など(名詞、0回以上)
        {"LEMMA": {"IN": ["以上", "以下", "未満", "超", "超える", "上回る", "下回る"]}} # 比較キーワード
    ]

    # 「最低/最大/約 + 数値 + 単位」のようなパターン
    pattern_limit_approx_quantity = [
        {"LEMMA": {"IN": ["最低", "最小", "最大", "せいぜい", "約", "およそ"]}}, # 限定キーワード
        {"POS": "NUM"}, # 数値
        {"POS": "NOUN", "OP": "*"} # 単位など(名詞、0回以上)
    ]

    # 「数値 + 単位 + 程度」のような概数パターン
    pattern_quantity_teido = [
         {"POS": "NUM"}, # 数値
         {"POS": "NOUN", "OP": "*"}, # 単位など(名詞、0回以上)
         {"LEMMA": "程度"} # 程度キーワード
    ]

    # 「AからBまで」のような範囲パターン (簡易版: NUM + "から" + NUM + "まで")
    # 単位や間の単語を考慮する場合はより複雑なパターンが必要
    pattern_range_from_to = [
        {"POS": "NUM"}, # 数値A
        {"LEMMA": "から"},
        {"POS": "NUM"}, # 数値B
        {"LEMMA": "まで"}
    ]

    matcher.add("QUANTITY_COMPARISON", [pattern_quantity_comparison])
    matcher.add("LIMIT_APPROX_QUANTITY", [pattern_limit_approx_quantity])
    matcher.add("QUANTITY_TEIDO", [pattern_quantity_teido])
    matcher.add("RANGE_FROM_TO", [pattern_range_from_to])

    # サンプルテキスト
    text = """
    価格は2万円以下にしてほしい。メモリ使用量は最大1.5GB、応答時間は500ミリ秒未満が目標です。
    この機能は最低3ヶ月は必要です。販売数量が月間10000個を超える場合、割引を適用します。
    約1000円の追加費用が発生する可能性があります。期間は1年から3年までを想定しています。
    対応件数は200件程度で収めたい。
    """

    doc = nlp(text)

    matches = matcher(doc)

    # 抽出結果の表示(簡易的)
    print("--- Matcherによる抽出結果 ---")
    for match_id, start, end in matches:
        span = doc[start:end]
        print(f"Match ID: {nlp.vocab.strings[match_id]}, Span: {span.text}")

    # より詳細な情報(数値、単位、比較タイプなど)を抽出するには、
    # 各Matcherルールの内部で、Span内のトークンを分析する必要があります。

    # 例:「QUANTITY_COMPARISON」パターンにマッチした場合の詳細分析
    extracted_info = []
    for match_id, start, end in matches:
        span = doc[start:end]
        rule_name = nlp.vocab.strings[match_id]

        if rule_name == "QUANTITY_COMPARISON":
            # 数値トークンを探す (最初の数値トークンを想定)
            num_token = None
            for token in span:
                if token.pos_ == "NUM":
                    num_token = token
                    break

            if num_token:
                quantity_value = num_token.text
                # 単位トークンを探す (数値トークンの直後にある名詞トークンなどを想定)
                unit_token = None
                # 簡単のため、数値トークンの次の名詞があれば単位と仮定
                if num_token.i + 1 < end and doc[num_token.i + 1].pos_ == "NOUN":
                    unit_token = doc[num_token.i + 1]
                    quantity_unit = unit_token.text
                else:
                    quantity_unit = ""

                # 比較キーワードを探す
                comparison_keyword = span[-1].lemma_ # パターン定義より最後のトークンがキーワードの場合

                extracted_info.append({
                    "type": "comparison",
                    "value": quantity_value,
                    "unit": quantity_unit,
                    "comparison": comparison_keyword,
                    "text": span.text
                })

        elif rule_name == "LIMIT_APPROX_QUANTITY":
             # 限定キーワード、数値トークン、単位トークンを探す
             limit_keyword = span[0].lemma_
             num_token = None
             for token in span:
                 if token.pos_ == "NUM":
                     num_token = token
                     break

             if num_token:
                 quantity_value = num_token.text
                 quantity_unit = ""
                 # 簡単のため、数値トークンの次の名詞があれば単位と仮定
                 if num_token.i + 1 < end and doc[num_token.i + 1].pos_ == "NOUN":
                    quantity_unit = doc[num_token.i + 1].text

                 extracted_info.append({
                    "type": "limit_approx",
                    "value": quantity_value,
                    "unit": quantity_unit,
                    "limit_approx": limit_keyword,
                    "text": span.text
                 })

        elif rule_name == "RANGE_FROM_TO":
            # 2つの数値トークンを探す
            nums = [token for token in span if token.pos_ == "NUM"]
            if len(nums) >= 2:
                value_from = nums[0].text
                value_to = nums[1].text
                # 単位トークンは別途判定が必要になる(例: 「100円から200円まで」なら両方に単位)
                # 簡単のためここでは省略

                extracted_info.append({
                    "type": "range",
                    "value_from": value_from,
                    "value_to": value_to,
                    # "unit": unit, # 単位抽出は別途実装
                    "text": span.text
                })

    print("\n--- 構造化された抽出結果(簡易)---")
    import json
    print(json.dumps(extracted_info, indent=2, ensure_ascii=False))

else:
    print("SpaCyモデルがロードできなかったため、コード例は実行できません。")

上記のコード例は、Matcherによる基本的なパターンマッチングを示しています。実際の業務テキストはさらに複雑な表現を含むため、パターンの網羅性を高めたり、依存構造解析を用いてより正確な関係性を捉えたりする必要があります。

例えば、「〜より大きい」という表現では、「大きい」という形容詞が主語となる名詞と数値表現に依存関係を持つことがあります。DependencyMatcherを使うと、これらの依存関係ツリー構造に基づいたパターンを定義できます。

依存構造解析を活用したより高度な抽出

依存構造解析は、文中の単語間の修飾・被修飾関係や主語・目的語などの文法的な関係性をツリー構造で示します。この情報を用いることで、どの数値がどの比較表現によって修飾されているか、あるいはどの単語がどの範囲の対象となっているかをより正確に判断できます。

例:「価格が1000円を超える」の場合、「超える」という動詞が「価格」を主語として持ち、「1000円」を目的語あるいは数量修飾として持つといった関係性が解析できます。この関係性をDependencyMatcherでパターン化することで、「価格」という文脈で「1000円を超える」という情報が述べられていることを抽出できます。

DependencyMatcherの使い方はMatcherに似ていますが、Tokenだけでなく、その依存関係(dep_属性)や親トークン(head属性)の情報を使ってパターンを定義します。

if nlp:
    from spacy.matcher import DependencyMatcher
    dep_matcher = DependencyMatcher(nlp.vocab)

    # 例:「[数値] + [単位] + [比較動詞]」のパターンで、比較動詞が数値/単位に依存するケース
    # 例: 「10000個を超える」 -> 超える (動詞) <--- obj ---> 10000個 (数値+単位)
    # Pattern:
    # Token 0: 比較動詞 (例: lemma="超える", pos="VERB")
    # Token 1: 数値/単位 (例: pos="NUM" or pos="NOUN", text="10000個")
    # Relation: Token 0がToken 1に'obj'あるいは類似の依存関係を持つ

    # より汎用的な依存構造パターンは複雑になるため、ここでは概念説明に留めます。
    # 実装では、対象とする表現(〜以上、〜未満、〜から〜まで)ごとに、
    # 数値、単位、対象名詞、比較/範囲キーワードがどのような依存関係になりやすいかを分析し、
    # それに基づいたDependencyMatcherパターンを構築する必要があります。

    # 例:簡易的なDependencyMatcherパターン例(「数値+単位」が「〜以上」のようなキーワードに修飾されるイメージ)
    # 実際は日本語UDPIPEモデルの依存関係ラベルを確認し、より正確なパターンを記述する必要があります。
    # 以下は概念的なコードです。
    # pattern_dep = [
    #     {"RIGHT_ID": "comparison_keyword", "RIGHT_ATTRS": {"LEMMA": {"IN": ["以上", "以下", "未満", "超える"]}}},
    #     {"LEFT_ID": "comparison_keyword", "REL_OP": ">", "RIGHT_ID": "quantity", "RIGHT_ATTRS": {"POS": {"IN": ["NUM", "NOUN"]}}}
    # ]
    # dep_matcher.add("QUANTITY_COMPARISON_DEP", [pattern_dep])

    # doc = nlp(text)
    # dep_matches = dep_matcher(doc)
    # print("\n--- DependencyMatcherによる抽出結果(概念)---")
    # for match_id, token_ids in dep_matches:
    #     span = doc[min(token_ids):max(token_ids)+1]
    #     print(f"Match ID: {nlp.vocab.strings[match_id]}, Span: {span.text}")

    print("\nDependencyMatcherによる抽出例は、モデルの依存関係ラベルに強く依存するため、ここでは概念的な説明に留めます。")
    print("実際の活用には、使用するSpaCyモデル(例: ja_udpipe)の依存関係ラベルを確認し、対象となるパターンでの出現頻度が高い依存構造を分析してパターンを構築する必要があります。")

else:
     print("SpaCyモデルがロードできなかったため、DependencyMatcherのコード例は実行できません。")

DependencyMatcherはより強力ですが、使用するSpaCyモデルの依存構造解析の精度や、対象言語における一般的な依存関係パターンに関する理解が必要です。多くの場合、Matcherと依存構造解析の結果を組み合わせることで、十分な精度と柔軟性を実現できます。例えば、Matcherで大まかなパターン(数値と近くにある比較キーワード)を抽出し、そのマッチ結果のToken間の依存関係を後からチェックして、正しいペアであるか検証するといったアプローチです。

抽出結果の構造化と正規化

抽出した情報は、後続の処理で利用しやすいように構造化することが重要です。例えば、以下のような辞書形式で整理します。

{
    "text": "価格は2万円以下",
    "type": "comparison", # comparison, range, limit, approx など
    "value": 20000, # 数値として正規化
    "unit": "円",
    "comparison": "以下", # 以上, 未満, 超過 など
    "lower_bound": None, # 範囲/比較による下限値(数値正規化済み)
    "upper_bound": 20000 # 範囲/比較による上限値(数値正規化済み)
}

数値の正規化(例:「2万円」→ 20000、「1.5GB」→ 1.5 * 10^9 バイトなど)や、単位の正規化も同時に行うと、後続処理での扱いが容易になります。

「〜以上」「〜以下」といった比較表現は、下限値と上限値を持つ範囲として解釈できます。「100以上」は下限100、上限なし。「500未満」は下限なし、上限500未満(または499.99...)。「1万円から3万円」は下限10000円、上限30000円となります。このように範囲の下限/上限として表現することで、一貫性のある形式で情報を管理できます。

応用事例と実務上の考慮事項

抽出した範囲・比較量情報は、様々な業務で応用できます。

実務システムへ組み込む際には、以下の点を考慮する必要があります。

まとめ

本記事では、業務テキストから範囲や比較を伴う量情報を抽出するために、PythonとNLPライブラリSpaCyを活用する手法を紹介しました。単語の表面的なパターンだけでなく、品詞、固有表現、そして依存構造解析といった言語的な情報を組み合わせることで、多様な表現に対応し、数値と修飾表現の正確な関係性を捉えることが可能になります。

SpaCyのMatcherを用いることで、Tokenの属性に基づいた柔軟なパターンを定義し、効率的に情報を抽出できます。さらに高度なパターンにはDependencyMatcherが有効ですが、対象とする言語モデルの依存関係ラベルに関する理解が必要です。

抽出した情報を構造化・正規化することで、後続のデータ分析やシステム連携が容易になります。パターンの網羅性、曖昧さへの対応、単位の処理、そしてパフォーマンスやメンテナンス性といった実務上の考慮事項を踏まえ、本記事で紹介した手法が、皆様のテキストデータからの情報抽出課題の解決の一助となれば幸いです。