業務テキストから数値を捉える:Pythonによる量情報抽出と正規化テクニック
はじめに
業務で扱うテキストデータには、顧客の声、技術文書、ログファイル、市場レポートなど、様々な情報が含まれています。これらのテキストの中から、定量的な情報、すなわち数値や単位付きの「量情報」を正確に抽出することは、データに基づいた意思決定や自動化において非常に重要です。例えば、
- 顧客レビューから製品のスペック(例:「バッテリーが10時間持つ」)や価格(例:「価格は5万円以下」)を収集したい。
- 技術文書から部品の寸法(例:「直径5mm」)や性能値(例:「消費電力は50W」)を抽出したい。
- サーバーログから特定の時間内に発生したエラー回数(例:「エラーが100件発生」)や処理時間(例:「処理に1.5秒かかった」)を監視したい。
- 契約書から金額(例:「1,000,000円」)や期限(例:「3ヶ月以内」)を自動確認したい。
このように、テキストの中から数値や量情報を抽出し、さらにそれを比較可能な形式に「正規化」できれば、多くの業務プロセスを効率化できます。しかし、テキスト中の数値や量表現は多様であり、単純なキーワード検索だけでは対応しきれません。本記事では、Pythonを用いて、このような数値・量情報の抽出と正規化を行うための具体的なテクニックを、正規表現と自然言語処理(NLP)ライブラリの両面から解説します。
テキスト中の数値・量情報表現の多様性
テキスト中に現れる数値や量情報は、以下のような様々な形式をとります。
- 単純な数値: 10, 1000, 3.14, -5
- 桁区切り: 1,000, 10,000,000
- 単位付き: 10km, 5kg, 3時間, 50W, 10万円, 20%, 100件
- 全角表記: 10km, 5万円
- 漢字表記: 十キログラム, 五万円
- 範囲表現: 10km〜20km, 1000円から2000円まで, 5年以上
- 比較表現: 100件以上, 5万円以下
- 省略表現: 数百万, 数十億
- 文脈依存: 単に「100」という数値だけでは意味不明だが、「100件のエラー」や「価格が100円」のように文脈で意味が決まる。
これらの多様な表現から必要な情報を正確に抽出し、さらに例えば「10km」と「10000m」を同じ距離として扱えるように標準化するには、工夫が必要です。
正規表現による基本的な数値・量情報抽出
最も基本的かつ強力な手法の一つは、正規表現(Regular Expression)を用いることです。Pythonの re
モジュールを使えば、特定のパターンに一致する文字列を効率的に検索・抽出できます。
単純な数値の抽出
整数や小数は比較的容易に抽出できます。
import re
text = "在庫数は123個です。価格は98.5ドルでした。損失は-5%です。"
# 整数を抽出
integers = re.findall(r'\d+', text)
print(f"抽出された整数: {integers}") # ['123', '98', '5'] - 小数の小数点以下も含まれる
# 小数点を含む数値を抽出
numbers = re.findall(r'\d+\.?\d*|\d*\.?\d+', text) # または r'-?\d+\.?\d*' で負数も考慮
print(f"抽出された数値(小数含む): {numbers}") # ['123', '98.5', '5']
# 負数、カンマ区切り、小数点を含むより一般的な数値パターン
# 例: -1,234.56
generic_number_pattern = r'-?\d{1,3}(,\d{3})*(\.\d+)?'
text_with_complex_numbers = "売上は1,234,567円、利益は-123.45ドルです。"
complex_numbers = re.findall(generic_number_pattern, text_with_complex_numbers)
# 正規表現のグループ化によりタプルで返される場合があるため、処理が必要
extracted_numbers = [m[0] + "".join(m[1:]) + (m[2] if len(m) > 2 else "") for m in re.findall(r'(-?\d{1,3}(?:,\d{3})*)(\.\d+)?', text_with_complex_numbers)]
print(f"抽出された複雑な数値: {extracted_numbers}") # ['1,234,567', '-123.45']
単位付き量情報の抽出
特定の単位が後続する数値パターンを定義します。
import re
text = "速度は100km/h、距離は500m、重さは5.2kg、時間は3時間、価格は1500円です。"
# 距離・重さ・時間の単位を含む量情報
# 単純な例として、数値の後に単位が続くパターン
quantity_pattern = r'\d+\.?\d*\s*(km|m|kg|g|時間|分|秒|円|ドル|%)'
quantities = re.findall(quantity_pattern, text)
# re.findallはグループがあるとタプルのリストを返す
# このパターンでは数値部分と単位部分が別々のグループになる
extracted_quantities = re.findall(r'(\d+\.?\d*)\s*(km|m|kg|g|時間|分|秒|円|ドル|%)', text)
print("抽出された量情報 (数値, 単位):")
for value, unit in extracted_quantities:
print(f" 数値: {value}, 単位: {unit}")
# 注意: この単純なパターンでは "100km/h" は "100km" として抽出されます。
# より複雑な単位や表現には、より洗練された正規表現が必要になります。
正規表現の限界
正規表現は強力ですが、以下のような限界があります。
- 複雑な文脈依存性: 「100」が何に関する数値か(価格か、数量かなど)は、周辺の単語や文の構造を見ないと判断できません。正規表現だけでは難しいです。
- 表記ゆれ: 「キログラム」「kg」「kgs」など、同じ意味でも異なる表記がある場合、全てのパターンを正規表現で網羅するのは困難かつ保守が大変です。
- 非定型表現: 「数年」「数十万」のような曖昧な表現や、「A社よりB社の方が2割高い」のような比較による表現は、正規表現だけでは捉えにくいです。
これらの課題に対処するため、NLPライブラリを活用します。
NLPライブラリ(SpaCy)を活用した量情報抽出
SpaCyのようなNLPライブラリは、テキストの構造(単語、品詞、固有表現、依存関係など)を解析する機能を提供します。これを利用することで、単なるパターン一致以上の高度な情報抽出が可能になります。
固有表現抽出(NER)の活用
多くのNLPライブラリは、数値や量に関する固有表現を認識する機能を持っています。SpaCyでは CARDINAL
(基数), QUANTITY
(量), MONEY
(金額), DATE
(日付), TIME
(時間), PERCENT
(パーセント) などのエンティティタイプでこれらの情報を捉えることができます。
import spacy
# 事前にモデルをダウンロードしてください (例: python -m spacy download ja_core_news_sm)
try:
nlp = spacy.load("ja_core_news_sm")
except OSError:
print("SpaCyモデルが見つかりません。'python -m spacy download ja_core_news_sm' を実行してください。")
exit()
text = "価格は1,500円で、重さは約5 kg、サイズは10インチです。消費電力は50 Wで、在庫は100個あります。プロジェクトは3ヶ月で完了予定です。"
doc = nlp(text)
print("SpaCyによる固有表現抽出:")
for ent in doc.ents:
# 数値や量に関するエンティティタイプに注目
if ent.label_ in ["CARDINAL", "QUANTITY", "MONEY", "DATE", "TIME", "PERCENT", "ORDINAL"]:
print(f" テキスト: {ent.text}, ラベル: {ent.label_}")
# この例では:
# テキスト: 1,500円, ラベル: MONEY
# テキスト: 5 kg, ラベル: QUANTITY
# テキスト: 10インチ, ラベル: QUANTITY
# テキスト: 50 W, ラベル: QUANTITY
# テキスト: 100個, ラベル: QUANTITY
# テキスト: 3ヶ月, ラベル: DATE (しばしば量や期間として扱われる)
# のように抽出される可能性があります。(モデルやバージョンによります)
NERは、正規表現よりも柔軟に単位や周辺語句(「約」など)を考慮して量情報を捉えることができます。しかし、すべてのドメイン固有の量表現(例:「コア数」「スレッド数」)をデフォルトで認識するわけではありません。その場合は、カスタムエンティティを追加する必要があります。
依存構造解析の活用
数値や量情報が、テキスト中のどの対象(名詞句など)にかかっているかを知ることは、情報の意味を理解する上で重要です。「価格は5万円」であれば、「価格」が5万円であるとわかります。SpaCyの依存構造解析を利用すれば、このような主語-述語や修飾関係を調べることができます。
import spacy
try:
nlp = spacy.load("ja_core_news_sm")
except OSError:
print("SpaCyモデルが見つかりません。'python -m spacy download ja_core_news_sm' を実行してください。")
exit()
text = "この製品の価格は5万円です。バッテリーの持続時間は10時間、メモリは8 GBです。"
doc = nlp(text)
print("依存構造解析による数値と対象の関連付け:")
for ent in doc.ents:
# CARDINAL や QUANTITY などの数値/量エンティティを対象
if ent.label_ in ["CARDINAL", "QUANTITY", "MONEY"]:
print(f"\n対象エンティティ: {ent.text} ({ent.label_})")
for token in ent:
# エンティティ内の各トークンについて依存関係を調べる
# headは依存元の単語、dep_は依存関係のラベル
print(f" トークン: {token.text}, 依存関係: {token.dep_}, 依存元: {token.head.text}, 依存元の品詞: {token.head.pos_}")
# 簡単なヒューリスティックとして、数値エンティティのheadを探る
# 例えば、数値が名詞にかかっている場合などを検出する
if ent.root: # エンティティのルートトークン
head_token = ent.root.head
print(f" エンティティのルートトークン: {ent.root.text}")
print(f" 依存元の単語: {head_token.text}, 品詞: {head_token.pos_}, 依存関係: {ent.root.dep_}")
# 実行例 (モデルによって異なる場合があります):
# 対象エンティティ: 5万円 (MONEY)
# トークン: 5, 依存関係: nummod, 依存元: 万円, 依存元の品詞: NOUN
# トークン: 万, 依存関係: compound, 依存元: 円, 依存元の品詞: NOUN
# トークン: 円, 依存関係: suffix, 依存元: 5万円, 依存元の品詞: NOUN
# エンティティのルートトークン: 5万円 (SpaCy 3.xではエンティティ全体を一つのスパンとして扱うことが多い)
# 依存元の単語: は, 品詞: ADP, 依存関係: nsubj
# 対象エンティティ: 10時間 (QUANTITY)
# トークン: 10, 依存関係: nummod, 依存元: 時間, 依存元の品詞: NOUN
# トークン: 時間, 依存関係: suffix, 依存元: 10時間, 依存元の品詞: NOUN
# エンティティのルートトークン: 10時間
# 依存元の単語: は, 品詞: ADP, 依存関係: nsubj
# 対象エンティティ: 8 GB (QUANTITY)
# トークン: 8, 依存関係: nummod, 依存元: GB, 依存元の品詞: NOUN
# トークン: GB, 依存関係: suffix, 依存元: 8 GB, 依存元の品詞: NOUN
# エンティティのルートトークン: 8 GB
# 依存元の単語: は, 品詞: ADP, 依存関係: nsubj
# この情報から、「価格」が「5万円」、「バッテリーの持続時間」が「10時間」、「メモリ」が「8 GB」といった関連を推測できます。
# より堅牢な関連付けには、依存パスのパターンマッチングなど高度なテクニックが必要です。
依存構造解析を組み合わせることで、「どのような」量情報が抽出されたのかをより正確に把握することが可能になります。
Matcher/PhraseMatcherによる柔軟なパターンマッチング
SpaCyの Matcher
や PhraseMatcher
を利用すれば、単語の並びや品詞、依存関係などの属性に基づいたカスタムパターンを定義し、テキスト中から該当箇所を抽出できます。これは正規表現とNERの中間に位置するような手法で、特定の形式を持つ量情報(例:「〜以上」「〜以下」「〜から〜まで」)を柔軟に捉えるのに役立ちます。
import spacy
from spacy.matcher import Matcher
try:
nlp = spacy.load("ja_core_news_sm")
except OSError:
print("SpaCyモデルが見つかりません。'python -m spacy download ja_core_news_sm' を実行してください。")
exit()
matcher = Matcher(nlp.vocab)
# パターンの定義例: 「[数値] 以上」
pattern_more_than = [{"LIKE_NUM": True}, {"LOWER": "以上"}]
# パターンの定義例: 「[数値] 以下」
pattern_less_than = [{"LIKE_NUM": True}, {"LOWER": "以下"}]
# パターンの定義例: 「[数値] から [数値] まで」
pattern_range = [{"LIKE_NUM": True}, {"LOWER": "から"}, {"LIKE_NUM": True}, {"LOWER": "まで"}]
# LIKE_NUM: 数値っぽいトークンに一致
matcher.add("QUANTITY_MORE_THAN", [pattern_more_than])
matcher.add("QUANTITY_LESS_THAN", [pattern_less_than])
matcher.add("QUANTITY_RANGE", [pattern_range])
text = "在庫は100個以上あります。価格は5万円以下に抑えたい。サイズは10インチから15インチまで対応しています。"
doc = nlp(text)
matches = matcher(doc)
print("\nSpaCy Matcherによるカスタムパターン抽出:")
for match_id, start, end in matches:
string_id = nlp.vocab.strings[match_id] # パターン名を取得
span = doc[start:end] # マッチしたスパン (単語の並び)
print(f" パターン名: {string_id}, マッチしたテキスト: {span.text}")
# この例では:
# パターン名: QUANTITY_MORE_THAN, マッチしたテキスト: 100個以上
# パターン名: QUANTITY_LESS_THAN, マッチしたテキスト: 5万円以下
# パターン名: QUANTITY_RANGE, マッチしたテキスト: 10インチから15インチまで
# のように抽出される可能性があります。
Matcher
を活用することで、正規表現だけでは難しい、単語の意味や種類(ここでは LIKE_NUM
で数値らしさ)を考慮した柔軟なパターン定義が可能になります。
抽出した量情報の正規化
テキストから量情報を抽出するだけでは不十分な場合があります。例えば、「10km」と抽出した情報と、「10000m」と抽出した情報を比較するには、単位を統一する必要があります。このプロセスを「正規化」と呼びます。
正規化の一般的なステップは以下の通りです。
- 数値部分のクリーンアップ: カンマ除去、全角数字の半角化などを行います。
- 単位の標準化: 抽出された単位を標準的な形式に変換します(例: "km" -> "km", "キログラム" -> "kg")。
- 単位換算: 必要に応じて、異なる単位間で値を換算します(例: "10km" -> "10000m")。
- 範囲/比較表現の構造化: 「100件以上」を
{'value': 100, 'operator': '>=', 'unit': '件'}
のような辞書形式に変換します。
import re
import unicodedata # 全角半角変換用
# 簡単な数値クリーンアップ関数
def clean_number(value_str):
# 全角数字を半角に変換
value_str = unicodedata.normalize('NFKC', value_str)
# カンマを除去
value_str = value_str.replace(',', '')
return value_str
# 簡単な単位標準化・換算関数 (限定的な例)
# 実際にはより網羅的な辞書やライブラリが必要
unit_map = {
'km': {'unit': 'm', 'factor': 1000},
'm': {'unit': 'm', 'factor': 1},
'kg': {'unit': 'g', 'factor': 1000},
'g': {'unit': 'g', 'factor': 1},
'時間': {'unit': '分', 'factor': 60},
'分': {'unit': '分', 'factor': 1},
'円': {'unit': '円', 'factor': 1},
'ドル': {'unit': 'ドル', 'factor': 1} # 通貨換算は為替レートが必要で複雑
}
def normalize_quantity(value_str, unit_str):
cleaned_value_str = clean_number(value_str)
try:
value = float(cleaned_value_str)
except ValueError:
return None # 数値に変換できない場合はスキップ
standardized_unit_info = unit_map.get(unit_str)
if standardized_unit_info:
standardized_value = value * standardized_unit_info['factor']
standardized_unit = standardized_unit_info['unit']
return {'value': standardized_value, 'unit': standardized_unit, 'original_unit': unit_str}
else:
# 未知の単位の場合はそのまま返すか、Noneを返すなど
return {'value': value, 'unit': unit_str, 'original_unit': unit_str}
# 正規表現で抽出したデータ例
extracted_data = [
('100', 'km'),
('5000', 'm'),
('5', 'kg'),
('1,500', '円'),
('3', '時間')
]
print("\n抽出データの正規化:")
normalized_data = []
for value_str, unit_str in extracted_data:
normalized_info = normalize_quantity(value_str, unit_str)
if normalized_info:
normalized_data.append(normalized_info)
print(f" 元: {value_str}{unit_str} -> 正規化: {normalized_info['value']}{normalized_info['unit']}")
# 結果例:
# 元: 100km -> 正規化: 100000.0m
# 元: 5000m -> 正規化: 5000.0m
# 元: 5kg -> 正規化: 5000.0g
# 元: 1,500円 -> 正規化: 1500.0円
# 元: 3時間 -> 正規化: 180.0分
# 範囲/比較表現の正規化例 (Matcherで抽出したテキストから)
def normalize_range_comparison(text):
doc = nlp(text) # SpaCyで解析
matches = matcher(doc) # 事前に定義したMatcherを使用
results = []
for match_id, start, end in matches:
span = doc[start:end]
string_id = nlp.vocab.strings[match_id]
values = [token.text for token in span if token.like_num] # 数値部分を抽出
if string_id == "QUANTITY_MORE_THAN":
if values:
results.append({'operator': '>=', 'value': clean_number(values[0]), 'text': span.text})
elif string_id == "QUANTITY_LESS_THAN":
if values:
results.append({'operator': '<=', 'value': clean_number(values[0]), 'text': span.text})
elif string_id == "QUANTITY_RANGE":
if len(values) == 2:
results.append({'operator': 'range', 'min': clean_number(values[0]), 'max': clean_number(values[1]), 'text': span.text})
return results
text_ranges = "在庫は100個以上あります。価格は5万円以下に抑えたい。サイズは10インチから15インチまで対応しています。"
normalized_ranges = normalize_range_comparison(text_ranges)
print("\n範囲/比較表現の正規化:")
for res in normalized_ranges:
print(f" 正規化結果: {res}")
# 結果例:
# 正規化結果: {'operator': '>=', 'value': '100', 'text': '100個以上'}
# 正規化結果: {'operator': '<=', 'value': '5', 'text': '5万円以下'}
# 正規化結果: {'operator': 'range', 'min': '10', 'max': '15', 'text': '10インチから15インチまで'}
単位の換算など、より高度な正規化には pint
のような専用ライブラリの利用も検討できますが、小規模なタスクであれば自前の関数で対応することも可能です。重要なのは、抽出した情報が後続の処理(データベース格納、比較、分析など)で扱いやすい構造化された形式になるように変換することです。
実務への応用と考慮事項
実務での応用例
- データ入力自動化: 請求書や注文書、技術仕様書などのPDFや画像から数値データを抽出し、基幹システムやデータベースに自動入力する。
- 競合分析: 競合製品のスペック情報(価格、メモリ容量、バッテリー時間など)をWebサイトから抽出し、比較可能なデータセットを作成する。
- 顧客フィードバック分析: 製品レビューやアンケート回答から、定量的な評価(例:「起動時間が30秒かかった」「サポートの応答が1時間以内だった」)を抽出し、改善点や顧客満足度を測る指標として活用する。
- ログ分析: サーバーログやアプリケーションログから、特定イベントの発生回数、処理時間、リソース使用量などの数値を抽出し、異常検知やパフォーマンス分析に役立てる。
パフォーマンスとシステム設計
- 処理速度: 大量のテキストデータを扱う場合、処理速度は重要な課題となります。正規表現は一般的に高速ですが、非常に複雑なパターンや大量のパターンを使う場合は遅くなることもあります。NLPライブラリは解析に時間がかかる傾向がありますが、精度が高いメリットがあります。どちらの手法を選択するか、あるいは組み合わせるかは、要件とデータの性質によります。
- システム連携: 抽出・正規化したデータは、データベースへの格納、API経由での提供、他の分析ツールへの連携など、様々なシステムと連携することを想定します。データ形式(JSON, CSVなど)やスキーマ設計を事前に考慮しておくことが重要です。
- クラウドサービス: AWS ComprehendやGCP Natural Language AIのようなクラウドベースのNLPサービスも、数値・量情報を含む固有表現抽出機能を提供しています。自前でライブラリを管理する手間を省ける一方、コストやデータプライバシーに関する考慮が必要です。
- 精度と頑健性: テキストの表記ゆれや非定型表現にどこまで対応できるか、誤抽出や見落としをどこまで許容できるかは、タスクの性質に依存します。高い精度が求められる場合は、複数の手法を組み合わせたり、抽出後のルールベースでのフィルタリングや補正処理を加えたりする必要があります。また、ドメイン固有の表現への対応には、カスタム辞書やカスタムモデルの学習が必要になる場合があります。
注意点
- 曖昧な表現: 「多数」「少量」「以前」のような曖昧な量や時期に関する表現は、定量的な抽出・正規化が困難です。これらの情報を扱うには、別の手法(例:感情分析、テキスト分類)が必要になることがあります。
- 文脈の曖昧さ: 同じ数値でも、文脈によって意味が大きく変わります。「価格が100」が「100円」なのか「100ドル」なのか、あるいは「100万円」なのかは、周辺情報から判断する必要があります。依存構造解析やより高度なセマンティック解析が必要になる場合があります。
- メンテナンス: 正規表現やカスタムパターンは、新しいテキスト形式や表現が出現するたびに更新が必要になる場合があります。メンテナンスコストも考慮して設計する必要があります。
まとめ
本記事では、業務テキストから数値や単位付きの量情報を抽出・正規化するためのPythonによる実践的なテクニックを紹介しました。正規表現はシンプルで高速なパターンマッチングに適しており、NLPライブラリ(SpaCyなど)は固有表現認識や文脈解析によるより高精度な抽出に役立ちます。これらの手法を適切に組み合わせることで、多様な形式で表現される量情報を効果的に捉えることが可能になります。
抽出した情報は、分析やシステム連携のために正規化することが重要です。単位の統一や数値形式のクリーンアップなどを行うことで、後続処理での扱いやすさが格段に向上します。
実務でこれらのテクニックを活用する際には、抽出対象となるテキストの性質をよく理解し、正規表現、NLPライブラリ、カスタムルールなどを組み合わせて最適なアプローチを選択することが肝要です。パフォーマンス、システムへの組み込みやすさ、そして求められる精度レベルを考慮しながら、段階的に実装を進めることをお勧めします。