生命保険におけるスマートクレームに対する一検討

以前、こちらの記事を翻訳しました。

https://qiita.com/taka_yayoi/items/748397712410955104c1

こちらでは損害保険の主訴(クレーム)処理において、不正な申請がないかを効率的に特定するための**スマートクレーム**が紹介されていました。自動車事故の損害が不正に報告されていないかどうかを、車両の写真やテレマティクスデータを用いて分析すると言うアプローチをとっていました。

こちらを動かしている中で、「生命保険の場合はどうなるのだろう？」と思い立ちました。そこで、上記のソリューションをベースに生命保険におけるソリューションを検討してみます。

:::note info
本記事は、私の思考実験です。保険の専門家ではないので、実業務に適用する際には適切な型のコンサルテーションを受けることをお勧めします。
:::

# スマートクレームとは

日本ですと**クレーム**という単語は、批判や文句のように捉えられますが、保険の文脈においては支払い請求を指します。このクレーム処理を効率化するための取り組み、仕組みがスマートクレームです。[元記事](https://qiita.com/taka_yayoi/items/748397712410955104c1
)にもあるように、申請業務における人間の調査官の負荷やリスクを見逃す可能性を低減することが重要となります。

> クレーム対応プロセスの自動化と最適化は、時間短縮と人間資本への依存の削減を通じて、コストを劇的に削減できる領域の一つです。さらに、データや高度な分析からの洞察を効果的に活用することで、リスクにさらされる可能性を劇的に削減することができます。
>
> この'スマートクレーム'のソリューションアクセラレータのモチベーションはシンプルです - レイクハウスを用いて、迅速な決着、処理コストの削減、潜在的な不正クレームに関する洞察のクイックな提供を可能にすることでクレーム対応を改善することです。

損害保険のケースでは、以下のようなデータを入力として意思決定を支援していました。

- 事故車両の写真から機械学習を用いて深刻度を判定し、申告されている内容と一致しているかを判定
- 報告日が保険適用期間に含まれているかを判定
- テレマティクスデータによって報告される事故の場所がクレームで報告される場所と一致していること
- etc.

![](https://github.com/databricks-industry-solutions/smart-claims/raw/main/resource/images/rule_engine.png)

まず気づくのは、生命保険の場合には自動車のコンディションは基本的に関係がないということです。こちらにありますように、生命保険の保険金の請求は診断書による書類ベースとなります。

https://www.jili.or.jp/seikyutebiki/2flow.html

そこで、今回は診断書とともに請求が行われたというシナリオを想定して、ソリューションを検討します。

# 生命保険におけるスマートクレームのプロトタイピング

損害保険のソリューションからもいくつか考え方を流用させてもらい、以下のようなソリューションをプロトタイピングします。
![Screenshot 2024-07-22 at 16.04.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/512207f1-f1e0-b305-16db-e8edfc027fbd.png)


データソースはポリシーとクレームの2種類:

- **ポリシー**: 被保険者の氏名、保険適用期間などを保持
- **クレーム**: 保険金を請求する根拠となる診療明細書(PDF)

PDFをパーシングし、生成AIを介してさまざまな情報を抽出します。これによって、後段のルールベースの判定に活用するとともに、人間の調査官の意思決定を支援する情報を提示します。

# Databricksにおける実装

## サンプルデータの準備

まずは、サンプルのPDFを準備します。こちらのサンプルを参考にさせてもらいながらExcelでいくつかサンプルを作成し、PDFで出力します。
![medical_statement_001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/04877e52-3d22-80de-4021-e19a087ee4ab.jpeg)

これらのPDFをDatabricksのボリュームにアップロードします。
![Screenshot 2024-07-22 at 16.04.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/da2623db-0854-1f8e-b016-e9c9c0e365f5.png)

## PDFのパースおよび情報の抽出

ここでは、パーシングに[LlamaParse](https://cloud.llamaindex.ai/parse)、情報抽出にはOpenAIの[gpt-4o mini](https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/)を使わせていただきます。こちらのセクションの処理は`parse_medical_statement`ノートブックで実行しているものとなります。

```py:parse_medical_statement
%pip install llama-index
%pip install llama-parse
%pip install PyMuPDF
dbutils.library.restartPython()
```

### パラメータの受け取り

あとでこの処理を複数回呼び出すので、ファイル名はパラメータ化します。

```py
dbutils.widgets.text("target_file", "", "Target file name")
target_file = dbutils.widgets.get("target_file")
print(target_file)
```

### LLAMA_CLOUD_API_KEYの設定

事前にAPIを取得してシークレットに保存しておきます。

```py
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にコピーしているのは、あとでダッシュボードに明細書の画像を表示させるためです。

```py
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を使用

```py
parser = LlamaParse(
    api_key=api_key,  # LLAMA_CLOUD_API_KEY　環境変数に設定することもできます
    result_type="markdown",
)

documents = parser.load_data(filename)
```

```py
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
```

```py
from llama_index.core import VectorStoreIndex

index = VectorStoreIndex.from_documents(documents)

query_engine = index.as_query_engine()
```

色々問い合わせて情報を取り出します。

```py
response = query_engine.query("受診内容を要約してください")
summary = str(response)
print(summary)
```
```
患者の弥生隆明様は2024年6月15日に受診され、以下の内容が行われました。

- 薬剤管理指導や点滴注射が実施され、無菌製剤処理も行われました。
- 救命処置として気管内挿管、カウンターショック、人工呼吸、非開胸的心マッサージが行われました。
- 微生物学的検査やHCV核酸定量などの検査も実施されました。
- 心大血管疾患に対するリハビリテーションが行われ、合計12回のリハビリが記録されています。
- 入院は急性期一般入院料が適用され、救命救急入院も含まれています。
```

```py
response = query_engine.query("どのような処置がされていますか")
treatment = str(response)
print(treatment)
```
```
以下の処置が行われています：

1. 救命のための気管内挿管
2. カウンターショック（その他）
3. 人工呼吸（5時間超）
4. 非開胸的心マッサージ（60分）
```

```py
response = query_engine.query("この処置は重症患者に行われたものですか")
is_severe_condition = str(response)
print(is_severe_condition)
```

```
はい、この処置は重症患者に行われたものです。
```

```py
response = query_engine.query("受診日はいつですか。日付のみを返してください")
consultation_date = str(response)
print(consultation_date)
```

```
2024/6/15
```

```py
response = query_engine.query("受診者は誰ですか。氏名のみを返してください")
patient_name = str(response)
print(patient_name)
```
```
弥生 隆明
```

```py
response = query_engine.query("この診療内容から推定される病状はなんですか。症状名のみを教えてください")
estimated_symptom = str(response)
print(estimated_symptom)
```

```py
急性心筋梗塞
```

### 抽出結果の保存

画像データと上記抽出データをまとめてDeltaテーブルに保存します。

```py
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)
```
![Screenshot 2024-07-22 at 16.14.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/a58adb2a-f90e-f754-aa2d-a66b4bc90416.png)

:::note info
この例では、PDFのファイル名に顧客IDを含めており、これをキーとして用いています。
:::

```py
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してテーブルに書き込みます。

```py
df_claim = df.join(image_df, "customer_id")
df_claim.write.mode("append").saveAsTable("takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims")
```

これらの処理をバッチで実行するようにします。別のノートブックを作成し、ボリュームにあるPDFファイルに対して処理をかけます。

```py: バッチ処理用ノートブック
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})
```

今回はバッチにしていますが、[ファイル到着トリガー](https://docs.databricks.com/ja/workflows/jobs/file-arrival-triggers.html)と組み合わせても良いかと思います。


## ポリシーデータの準備

こちらはシンプルに氏名と保険期間のみのデータとしています。実際のケースではこれ以外にも保険の種類や適用条件などを含める形になるかと思います。

```py
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")
```

**ポリシーデータ**
![Screenshot 2024-07-22 at 16.18.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/aefddde7-163d-1167-dfb8-5c38a5cbfd1b.png)

**クレームデータ**
![Screenshot 2024-07-22 at 16.19.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/cb2fd0ff-5a4b-5c07-88fa-43a94455d5f7.png)

## ダッシュボードの作成

### 概要ダッシュボード

ルールベースでの突き合わせを行い、その結果を表示するダッシュボードを作成します。

```sql:overview
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
```

氏名不一致の件数の取得。

```sql:invalid_name
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
```

保険期間不一致の件数の取得。

```sql:invalid_period
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
```

これらを組み合わせて以下のようなダッシュボードを作成しました。
![Screenshot 2024-07-22 at 16.23.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/51b380f7-a0c1-2120-4cda-5517c7a5e4cf.png)

申請一覧の顧客IDはリンクになっており、詳細画面に遷移できるようになっています。こちらは、もともとHTML文字列を出力しており、以下のような設定でハイパーリンクにしています。
![Screenshot 2024-07-22 at 16.24.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/c643a715-4421-0ed7-5fd6-83b49e50380c.png)


### 詳細ダッシュボード

こちらのダッシュボードでは、各申請の詳細を確認できるようにしています。
![Screenshot 2024-07-22 at 16.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/906c8ab6-9aa8-d937-bb5b-7c5588dbe50f.png)


```sql:healthcare_claims
SELECT * FROM takaakiyayoi_catalog.fsi_smart_claims.healthcare_claims
```
ここでのハイライトは右下の診療明細書の画像表示です。[PDFファイルの前処理](#pdfファイルの前処理)で、FileStore配下に画像をコピーしているので、そちらのファイルを参照させるように設定しています。
![Screenshot 2024-07-22 at 16.30.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/ee13202d-f048-5747-17c4-167902327a99.png)

**URLテンプレート**には以下を記載しています。パスは適宜変更する必要があります。

```
https://<Databricksホスト名>/files/shared_uploads/takaaki.yayoi@databricks.com/medical_statement_{{ @ }}.jpg
```

あとは、概要ダッシュボードでも行っているルールベースの判定結果です。

```sql:master_matching
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
```

# まとめ

今回は思考実験の一環で、ごくシンプルなプロトタイプを作成してみました。当然、実際のケースではソースデータも異なりますし、適用すべきルールもかず多く存在するかと思います。ただ、今回のプロトタイピングを通じて、パーシングの技術の進化や大規模言語モデル適用の可能性を感じることができました。


### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)