以前、こちらの記事を翻訳しました。
こちらでは損害保険の主訴(クレーム)処理において、不正な申請がないかを効率的に特定するためのスマートクレームが紹介されていました。自動車事故の損害が不正に報告されていないかどうかを、車両の写真やテレマティクスデータを用いて分析すると言うアプローチをとっていました。
こちらを動かしている中で、「生命保険の場合はどうなるのだろう?」と思い立ちました。そこで、上記のソリューションをベースに生命保険におけるソリューションを検討してみます。
本記事は、私の思考実験です。保険の専門家ではないので、実業務に適用する際には適切な型のコンサルテーションを受けることをお勧めします。
スマートクレームとは
日本ですとクレームという単語は、批判や文句のように捉えられますが、保険の文脈においては支払い請求を指します。このクレーム処理を効率化するための取り組み、仕組みがスマートクレームです。元記事にもあるように、申請業務における人間の調査官の負荷やリスクを見逃す可能性を低減することが重要となります。
クレーム対応プロセスの自動化と最適化は、時間短縮と人間資本への依存の削減を通じて、コストを劇的に削減できる領域の一つです。さらに、データや高度な分析からの洞察を効果的に活用することで、リスクにさらされる可能性を劇的に削減することができます。
この'スマートクレーム'のソリューションアクセラレータのモチベーションはシンプルです - レイクハウスを用いて、迅速な決着、処理コストの削減、潜在的な不正クレームに関する洞察のクイックな提供を可能にすることでクレーム対応を改善することです。
損害保険のケースでは、以下のようなデータを入力として意思決定を支援していました。
- 事故車両の写真から機械学習を用いて深刻度を判定し、申告されている内容と一致しているかを判定
- 報告日が保険適用期間に含まれているかを判定
- テレマティクスデータによって報告される事故の場所がクレームで報告される場所と一致していること
- etc.
まず気づくのは、生命保険の場合には自動車のコンディションは基本的に関係がないということです。こちらにありますように、生命保険の保険金の請求は診断書による書類ベースとなります。
そこで、今回は診断書とともに請求が行われたというシナリオを想定して、ソリューションを検討します。
生命保険におけるスマートクレームのプロトタイピング
損害保険のソリューションからもいくつか考え方を流用させてもらい、以下のようなソリューションをプロトタイピングします。
データソースはポリシーとクレームの2種類:
- ポリシー: 被保険者の氏名、保険適用期間などを保持
- クレーム: 保険金を請求する根拠となる診療明細書(PDF)
PDFをパーシングし、生成AIを介してさまざまな情報を抽出します。これによって、後段のルールベースの判定に活用するとともに、人間の調査官の意思決定を支援する情報を提示します。
Databricksにおける実装
サンプルデータの準備
まずは、サンプルのPDFを準備します。こちらのサンプルを参考にさせてもらいながらExcelでいくつかサンプルを作成し、PDFで出力します。
これらのPDFをDatabricksのボリュームにアップロードします。
PDFのパースおよび情報の抽出
ここでは、パーシングにLlamaParse、情報抽出にはOpenAIのgpt-4o miniを使わせていただきます。こちらのセクションの処理はparse_medical_statement
ノートブックで実行しているものとなります。
%pip install llama-index
%pip install llama-parse
%pip install PyMuPDF
dbutils.library.restartPython()
パラメータの受け取り
あとでこの処理を複数回呼び出すので、ファイル名はパラメータ化します。
dbutils.widgets.text("target_file", "", "Target file name")
target_file = dbutils.widgets.get("target_file")
print(target_file)
LLAMA_CLOUD_API_KEYの設定
事前にAPIを取得してシークレットに保存しておきます。
import nest_asyncio
nest_asyncio.apply()
from llama_parse import LlamaParse
api_key = dbutils.secrets.get("demo-token-takaaki.yayoi", "llama_cloud_api_key") # cloud.llamaindex.ai でAPIキーを入手
PDFファイルの前処理
以下でFileStoreにコピーしているのは、あとでダッシュボードに明細書の画像を表示させるためです。
import fitz
import matplotlib.pyplot as plt
from PIL import Image
from io import BytesIO
from pathlib import Path
filename = f"/Volumes/takaakiyayoi_catalog/fsi_smart_claims/volume_claims/medical_statements/{target_file}"
doc = fitz.open(filename)
page = doc.load_page(0)
pixmap = page.get_pixmap(dpi=300)
img = pixmap.tobytes()
# BytesIOから画像の読み込み
im = Image.open(BytesIO(img))
# 画像の表示
dpi = 100
width, height = im.size
plt.figure(figsize = (width/dpi,height/dpi))
plt.imshow(im, aspect='auto')
# 画像の保存
filename_only = Path(filename).stem
# ファイル名から顧客IDを抽出
filename_components = filename_only.split("_")
customer_id = filename_components[2]
saved_image_path = f'/Volumes/takaakiyayoi_catalog/fsi_smart_claims/volume_claims/medical_statements/converted_image/{filename_only}.jpg'
im.save(saved_image_path, 'JPEG')
# FileStoreにコピー
dbutils.fs.cp(saved_image_path, f"dbfs:/FileStore/shared_uploads/takaaki.yayoi@databricks.com/")
PDFをパースするためにLlamaParseを使用
parser = LlamaParse(
api_key=api_key, # LLAMA_CLOUD_API_KEY 環境変数に設定することもできます
result_type="markdown",
)
documents = parser.load_data(filename)
documents
きちんとパースできています。
[Document(id_='327c3dfa-af07-4f17-ac39-2f662fbdff32', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='# 診療明細書\n\n|患者番号| |氏名|弥生 隆明 様|受診日|2024/6/15|\n|---|---|---|---|---|---|\n|受診科| | | | | |\n\n|部|項目名|点数|回数|\n|---|---|---|---|\n|医学管理|*薬剤管理指導料2(1の患者以外の患者)|325|1|\n|注射|*点滴注射|276|1|\n| |サークレス注0.1% 0.1%100mL1瓶| | |\n| |生理食塩液500mL 1瓶| | |\n| |*点滴注射料|97|1|\n| |*無菌製剤処理料2|40|1|\n|処置|*救命のための気管内挿管|500|1|\n| |*カウンターショック(その他)|3500|1|\n| |*人工呼吸(5時間超) 360分|819|1|\n| |*非開胸的心マッサージ 60分|290|1|\n|検査|*微生物学的検査判断料|150|1|\n| |*検体検査管理加算(2)|100|1|\n| |*HCV核酸定量|437|1|\n|リハビリ|*心大血管疾患リハビリテーション料(1)|280|12|\n| |早期リハビリテーション加算| | |\n| |初期加算| | |\n|入院料|*急性期一般入院料7|1782|7|\n| |一般病棟入院期間加算(14日以内)| | |\n| |*医師事務作業補助体制加算1(50対1)|270|1|\n| |*救命救急入院料1(3日以内)|9869|3|\n| |*救命救急入院料1(4日以上7日以内)|8929|2|', mimetype='text/plain', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n')]
LlamaIndexとgpt-4o miniによる情報抽出
LlamaIndexの枠組みの中でLLMとしてgpt-4o miniを使います。
import os
os.environ["OPENAI_API_KEY"] = dbutils.secrets.get("demo-token-takaaki.yayoi", "openai_api_key")
from llama_index.llms.openai import OpenAI
from llama_index.core import Settings
llm = OpenAI(model="gpt-4o-mini")
Settings.llm = llm
from llama_index.core import VectorStoreIndex
index = VectorStoreIndex.from_documents(documents)
query_engine = index.as_query_engine()
色々問い合わせて情報を取り出します。
response = query_engine.query("受診内容を要約してください")
summary = str(response)
print(summary)
患者の弥生隆明様は2024年6月15日に受診され、以下の内容が行われました。
- 薬剤管理指導や点滴注射が実施され、無菌製剤処理も行われました。
- 救命処置として気管内挿管、カウンターショック、人工呼吸、非開胸的心マッサージが行われました。
- 微生物学的検査やHCV核酸定量などの検査も実施されました。
- 心大血管疾患に対するリハビリテーションが行われ、合計12回のリハビリが記録されています。
- 入院は急性期一般入院料が適用され、救命救急入院も含まれています。
response = query_engine.query("どのような処置がされていますか")
treatment = str(response)
print(treatment)
以下の処置が行われています:
1. 救命のための気管内挿管
2. カウンターショック(その他)
3. 人工呼吸(5時間超)
4. 非開胸的心マッサージ(60分)
response = query_engine.query("この処置は重症患者に行われたものですか")
is_severe_condition = str(response)
print(is_severe_condition)
はい、この処置は重症患者に行われたものです。
response = query_engine.query("受診日はいつですか。日付のみを返してください")
consultation_date = str(response)
print(consultation_date)
2024/6/15
response = query_engine.query("受診者は誰ですか。氏名のみを返してください")
patient_name = str(response)
print(patient_name)
弥生 隆明
response = query_engine.query("この診療内容から推定される病状はなんですか。症状名のみを教えてください")
estimated_symptom = str(response)
print(estimated_symptom)
急性心筋梗塞
抽出結果の保存
画像データと上記抽出データをまとめてDeltaテーブルに保存します。
from pyspark.sql.functions import *
image_df = (
spark.read.format("binaryFile").option("mimeType", "image/*").load(saved_image_path)
)
image_df = image_df.withColumn("customer_id", lit(customer_id))
display(image_df)
この例では、PDFのファイル名に顧客IDを含めており、これをキーとして用いています。
from pyspark.sql.functions import col, to_date
# DataFrameを作成し、クレーム情報を含む行を追加
df = spark.createDataFrame(
[
Row(
customer_id=customer_id,
summary=summary,
treatment=treatment,
is_severe_condition=is_severe_condition,
consultation_date=consultation_date,
estimated_symptom=estimated_symptom,
patient_name=patient_name,
)
],
schema="customer_id string, summary string, treatment string, is_severe_condition string, consultation_date string, estimated_symptom string, patient_name string",
)
# consultation_date列を日付型に変換
df = df.withColumn("consultation_date", to_date(col("consultation_date"), "yyyy/M/dd"))
# DataFrameの内容を表示
display(df)
2つのデータフレームをjoinしてテーブルに書き込みます。
df_claim = df.join(image_df, "customer_id")
df_claim.write.mode("append").saveAsTable("takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims")
これらの処理をバッチで実行するようにします。別のノートブックを作成し、ボリュームにあるPDFファイルに対して処理をかけます。
import os
Direc = "/Volumes/takaakiyayoi_catalog/fsi_smart_claims/volume_claims/medical_statements/"
print(f"Files in the directory: {Direc}")
files = os.listdir(Direc)
files = [f for f in files if os.path.isfile(Direc+'/'+f)]
print(*files, sep="\n")
for file_name in files:
dbutils.notebook.run("parse_medical_statement", 360, {"target_file": file_name})
今回はバッチにしていますが、ファイル到着トリガーと組み合わせても良いかと思います。
ポリシーデータの準備
こちらはシンプルに氏名と保険期間のみのデータとしています。実際のケースではこれ以外にも保険の種類や適用条件などを含める形になるかと思います。
from pyspark.sql.functions import *
from datetime import datetime, date
from pyspark.sql import Row
df = spark.createDataFrame(
[
Row(
customer_id="001",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="弥生 隆明",
),
Row(
customer_id="002",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="弥生 隆明",
),
Row(
customer_id="003",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="山田 太郎",
),
Row(
customer_id="004",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="鈴木 一郎",
),
Row(
customer_id="005",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="山田 太郎",
),
Row(
customer_id="006",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="鈴木 一郎",
),
Row(
customer_id="007",
pol_effective_date=date(2024, 1, 1),
pol_expiry_date=date(2025, 6, 30),
name="弥生 隆明",
)
],
schema="customer_id string, pol_effective_date date, pol_expiry_date date, name string",
)
df.write.mode("overwrite").saveAsTable("takaakiyayoi_catalog.fsi_smart_claims.healthcare_customer")
ダッシュボードの作成
概要ダッシュボード
ルールベースでの突き合わせを行い、その結果を表示するダッシュボードを作成します。
SELECT
"<a href='/dashboardsv3/01ef47d10b64126a8ceb65d7a9774afc/published?f_01ef47d35635173b841bd7319f7aae1e=" || customer.customer_id || "'>" || customer.customer_id || "</a>" AS customer_id,
customer.name,
customer.pol_effective_date,
customer.pol_expiry_date,
claims.patient_name,
claims.consultation_date,
claims.estimated_symptom,
CASE
WHEN customer.name = claims.patient_name THEN "OK"
ELSE "NG"
END AS name_match,
CASE
WHEN claims.consultation_date BETWEEN customer.pol_effective_date
AND customer.pol_expiry_date THEN "OK"
ELSE "NG"
END AS date_match,
customer.pol_effective_date || "〜" || customer.pol_expiry_date AS covered_period
FROM
takaakiyayoi_catalog.fsi_smart_claims.healthcare_customer customer
JOIN takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims claims ON customer.customer_id = claims.customer_id
ORDER BY claims.customer_id ASC
氏名不一致の件数の取得。
SELECT COUNT(*) AS invalid_name_count FROM
takaakiyayoi_catalog.fsi_smart_claims.healthcare_customer customer
JOIN takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims claims ON customer.customer_id = claims.customer_id
WHERE customer.name != claims.patient_name
保険期間不一致の件数の取得。
SELECT
COUNT(*) AS invalid_period
FROM
takaakiyayoi_catalog.fsi_smart_claims.healthcare_customer customer
JOIN takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims claims ON customer.customer_id = claims.customer_id
WHERE
claims.consultation_date NOT BETWEEN customer.pol_effective_date
AND customer.pol_expiry_date
これらを組み合わせて以下のようなダッシュボードを作成しました。
申請一覧の顧客IDはリンクになっており、詳細画面に遷移できるようになっています。こちらは、もともとHTML文字列を出力しており、以下のような設定でハイパーリンクにしています。
詳細ダッシュボード
こちらのダッシュボードでは、各申請の詳細を確認できるようにしています。
SELECT * FROM takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims
ここでのハイライトは右下の診療明細書の画像表示です。PDFファイルの前処理で、FileStore配下に画像をコピーしているので、そちらのファイルを参照させるように設定しています。
URLテンプレートには以下を記載しています。パスは適宜変更する必要があります。
https://<Databricksホスト名>/files/shared_uploads/takaaki.yayoi@databricks.com/medical_statement_{{ @ }}.jpg
あとは、概要ダッシュボードでも行っているルールベースの判定結果です。
SELECT
customer.customer_id,
customer.name,
customer.pol_effective_date,
customer.pol_expiry_date,
claims.patient_name,
claims.consultation_date,
CASE
WHEN customer.name = claims.patient_name THEN "<h1><font color=blue>OK</font></h1>"
ELSE "<h1><font color=red>NG</font></h1>"
END AS name_match,
CASE
WHEN claims.consultation_date BETWEEN customer.pol_effective_date
AND customer.pol_expiry_date THEN "<h1><font color=blue>OK</font></h1>"
ELSE "<h1><font color=red>NG</font></h1>"
END AS date_match,
customer.pol_effective_date || "〜" || customer.pol_expiry_date AS covered_period
FROM
takaakiyayoi_catalog.fsi_smart_claims.healthcare_customer customer
JOIN takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims claims ON customer.customer_id = claims.customer_id
まとめ
今回は思考実験の一環で、ごくシンプルなプロトタイプを作成してみました。当然、実際のケースではソースデータも異なりますし、適用すべきルールもかず多く存在するかと思います。ただ、今回のプロトタイピングを通じて、パーシングの技術の進化や大規模言語モデル適用の可能性を感じることができました。