PolarDBを使用したグラフデータベース：銀行及金融セクターにおけるグラフ分析の実践

> 本記事は[こちらのブログ](https://www.alibabacloud.com/blog/graph-database-with-polardb-practice-of-graph-analysis-in-the-banking-and-financial-sector_601865?spm=a2c65.11461433.0.0.4ee353559N4sIW)を参考にしています。
> 翻訳にはアリババクラウドの[ModelStudio](https://www.alibabacloud.com/en/product/modelstudio)([Qwen](https://www.alibabacloud.com/en/solutions/generative-ai/qwen))を使用しております。

## 概要
この記事では、<a href=https://www.alibabacloud.com/product/polardb-for-postgresql target=_blank>アリババクラウドのPolarDB for PostgreSQL</a> 及びそのAGE拡張を使用してグラフデータ分析を行う方法を紹介します。特に、金融取引における詐欺検出に焦点を当てています。PolarDB for PostgreSQL版とAGE拡張の組み合わせにより、グラフデータの効率的な処理や問い合わせ（Cypherクエリ言語の使用を含む）がサポートされています。記事は、データ準備からグラフ構造の作成、特定の問い合わせ例までの過程を詳細に説明し、グラフ問い合わせを通じて詐欺取引の関係を発見し、トランザクション間のJaccard類似度を計算し、詐欺警告を提供する方法を示しています。

## はじめに
グラフ解析は、データサイエンスにおいて重要な分野であり、データをグラフ構造を通じて表現し、様々な計算や分析タスクを行うことを目的としています。グラフ構造はノード（または頂点）とエッジから成り立っており、ノードは通常エンティティを表し、エッジはエンティティ間の関係を表します。グラフ計算は、ソーシャルネットワーク分析、推薦システム、知識グラフ、経路最適化など様々な分野で広く利用されています。<a href=https://www.alibabacloud.com/product/polardb-for-postgresql target=_blank>PolarDB for PostgreSQL</a>（以下、PolarDBと略称）は、アリババクラウドが独立開発したクラウドネイティブなリレーショナルデータベース製品であり、PostgreSQLと完全互換性があり、Oracle構文にも高い互換性を持っています（パブリッククラウド版ではOracle構文に対応）。共有ストレージに基づいたストレージ・コンピューティング分離アーキテクチャを採用し、極限までに弾性的なスケールアウト、ミリ秒単位の遅延、HTAP機能、高い信頼性、可用性、そして弾性的なスケーラビリティを特徴とする企業向けのデータベース特性を持っています。また、PolarDBは大規模な並列計算能力を持ち、混合型のOLTPおよびOLAPワークロードに対応できます。本記事で説明するグラフ分析能力は、アリババクラウドのネイティブリレーショナルデータベースPolarDB for PostgreSQLの出力に基づいています。

## ビジネスシーン
### 背景情報
現代の詐欺や様々な金融犯罪では、詐欺者がリスク管理ルールを回避するために身元を変えることで目的を達成しています。グラフデータベースを通じてグラフ構造を構築し、ユーザー行動を追跡することで、詐欺行為に関する離散データのリアルタイム分析を行い、詐欺の輪を迅速に特定し、詐欺行為の予防と解決に寄与することができます。

### データとスキーマ
<a href=https://www.kaggle.com/c/ieee-fraud-detection/data target=_blank>IEEE-CIS Fraud Detection データセット</a>は、金融取引分野の公開データセットです。データには、電子商取引プラットフォームVestaが提供する取引記録が含まれており、デバイス、住所、メールなどの関連情報を含んでいます。このデータセットは、詐欺取引の識別とリスクの予測に使用できます。元データはCSV形式で、取引情報（取引番号、住所、メールなど）と取引識別情報（デバイス情報、デバイスタイプなど）を含んでいます。

! [1](https://yqintl.alicdn.com/336003e55bfdd216243ab0114cd33209933818fa.png)
! [2](https://yqintl.alicdn.com/2ba508a395b5dcb391594cc7aae3eac549b36636.png)

このデータセットには大量の情報が含まれており、本記事では、以下の図のようなデータモデルを抽象化しています。実際のビジネスシーンでは、状況に応じて調整することが可能です。

! [3](https://yqintl.alicdn.com/08316357c0a666c1506084a9d6de26d77bf55ed1.png)

その中で：
- ノード：
  - transaction（取引）
  - product（取引商品）
  - addr1（取引先住所1）
  - addr2（取引先住所2）
  - emaildomain（取引で使用されたメールドメイン）
  - deviceinfo（取引デバイス情報）
  - devicetype（取引デバイスタイプ）

- エッジ：
  - transaction_product（取引と商品の関係）
  - transaction_addr1（取引と住所1の関係）
  - transaction_addr2（取引と住所2の関係）
  - transaction_emaildomain（取引とメールドメインの関係）
  - transaction_deviceinfo（取引とデバイス情報の関係）
  - transaction_devicetype（取引とデバイスタイプの関係）

上記のモデルは、取引を中心に展開され、取引ID（transactionid）を通じて関連付けられています。

### 最善実践
#### 技術実装
##### PolarDB
PolarDBのグラフデータベースエンジンAGE（A Graph Extension）は、PostgreSQLシリーズのデータベース向けに設計された拡張機能であり、グラフデータの処理能力を強化することを目指しています。AGEは、リレーショナルデータベースとグラフデータベースの長所を組み合わせることを目的としており、高性能で柔軟性があり、簡単にスケーラブルなソリューションを提供します。AGEの主な特徴は以下の通りです：

- PostgreSQLとの完全互換性
AGEはPolarDBのPostgreSQL版の拡張であり、既存のPolarDBデータベース内で使用できるため、データベースの再構築は必要ありません。AGEはPolarDBのすべての強力な機能を引き継いでいます。トランザクション、并发制御、様々なインデックスや最適化技術を含みます。

- リレーショナルおよびグラフクエリの一元化
AGEは、リレーショナルデータとグラフデータを同時に処理できるため、同じクエリ内でSQLとグラフクエリ言語を混在させることができます。これにより、複雑なデータモデルをより簡単にかつ効率的に扱うことができます。

- Cypherクエリ言語のサポート
AGEはCypherクエリ言語の使用をサポートしており、これはグラフデータベース向けに特別にデザインされたクエリ言語であり、シンプルで柔軟な構文を持っています。ユーザーにグラフデータの照会や操作のための直感的な方法を提供します。

- 高性能
PolarDBの最適化技術とグラフデータ向けに設計されたインデックスを組み合わせることで、AGEは大規模なグラフデータや複雑なグラフクエリを効率的に処理することができます。AGEの強力な機能により、PolarDBは様々なグラフクエリをシンプルかつ効率的に処理できるようになります。

! [4](https://yqintl.alicdn.com/b14a824b52a1c3ef1af27b07a3eeb1dbbee1b6bd.png)

##### Age-viewer
<a href=https://github.com/apache/age-viewer target=_blank>age-viewer</a>はAGEプロジェクトの組み込みグラフデータ可視化ツールであり、問い合わせ結果を可視化することができます。PolarDBと互換性があるage-viewerのミラーサイトはここからダウンロード可能です。

##### 推奨設定
良質なPOC体験を実現するため、以下の設定を推奨します。

| 項目       | 推奨設定       |
|----------|-------------|
| PolarDBバージョン | PostgreSQL 14互換の標準版 |
| CPU      | >16コア       |
| メモリ   | >64GB        |
| ディスク  | >100GB (AUTOPL)
変換されたデータは以下の通りです：

**・頂点データの一部**sql
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:transaction {transactionid : 2990783, isfraud : 0 } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:product {productid : 158945 } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:addr1 {addr1 : 299.0 } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:addr2 {addr2 : 87.0 } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:emaildomain {emaildomain : 'gmail.com' } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:deviceinfo {deviceinfo : 'SM-G920V Build/NRD90M' } ) RETURN v $$ ) AS (n agtype);
SELECT * FROM cypher(fraud_graph, $$ MERGE (v:devicetype {devicetype : 'mobile' } ) RETURN v $$ ) AS (n agtype);
...

**・エッジデータの一部**sql
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:product) WHERE a.transactionid = 2990783 AND b.productid = 158945 MERGE (a)-[e:transaction_product]->(b) RETURN e$$) AS (e agtype);
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:addr1) WHERE a.transactionid = 2990783 AND b.addr1 = 299.0 MERGE (a)-[e:transaction_addr1]->(b) RETURN e$$) AS (e agtype);
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:addr2) WHERE a.transactionid = 2990783 AND b.addr2 = 87.0 MERGE (a)-[e:transaction_addr2]->(b) RETURN e$$) AS (e agtype);
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:emaildomain) WHERE a.transactionid = 2990783 AND b.emaildomain = 'gmail.com' MERGE (a)-[e:transaction_emaildomain_p]->(b) RETURN e$$) AS (e agtype);
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:deviceinfo) WHERE a.transactionid = 2999403 AND b.deviceinfo = 'SM-G920V Build/NRD90M' MERGE (a)-[e:transaction_deviceinfo]->(b) RETURN e$$) AS (e agtype);
SELECT * FROM cypher(fraud_graph, $$ MATCH (a:transaction), (b:devicetype) WHERE a.transactionid = 2999404 AND b.devicetype = 'mobile' MERGE (a)-[e:transaction_devicetype]->(b) RETURN e$$) AS (e agtype);
...

変換結果をSQLファイルとして保存し、`psql`などのクライアントツールを使用してデータのインポートを完了します。使用例には、単純なクエリやデータ統計などが含まれます:

**例**

**・単純なクエリ**

**・データ統計**
- **頂点数**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n)
    RETURN count(*)
$$) AS (number_of_vertex agtype);
結果:
 number_of_vertex 
-----------------
 1076004

- **トランザクション頂点数**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)
    RETURN count(*)
$$) AS (number_of_transaction agtype);
結果:
number_of_transaction 
---------------------
 545591

- **不正と識別されたトランザクション数**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)
    WHERE n.isfraud = 1
    RETURN count(*)
$$) AS (number_of_fraud_transaction agtype);
結果:
number_of_fraud_transaction 
--------------------------
 18919

- **エッジ数**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH ()-[r]->()
    RETURN count(*)
$$) AS (number_of_edge agtype);
結果:
number 
-------
 2131254

**・フィルタクエリ、ソートクエリ**
- **IDが2988706のトランザクション情報を照会し、関連情報をすべて表示**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[r]->(v)
    WHERE n.transactionid = 2988706
    RETURN v
$$) AS (r agtype);
結果は省略されていますが、各頂点の情報が返されます。

結果をプレビューするには、`age-viewer`で以下のSQLを使用:sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[r]->(v)
    WHERE n.transactionid = 2988706
    RETURN [n,r,v]::path
$$) AS (r agtype);
![5](https://yqintl.alicdn.com/ff86fe1cb5c18ba66a477e6ec29031067989306f.png)

**・共通シナリオ**
- **K近傍法 (K Nearest Neighbors)**
K近傍法(KNN)は、データポイント間の類似性を使って潜在的な不正行為を特定するために使われます。例えば、あるトランザクションが金額、場所、時間に関してほとんどの近傍と大きな差異を示す場合、それは疑わしいものとマークされるかもしれません。

- **IDが2988706のトランザクションと同様の住所を持つ他のトランザクション記録を照会し、これらの近隣からの関連情報をもとにこのトランザクションの疑わしさを判断**sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[:transaction_addr1]->(:addr1)<-[:transaction_addr1]-(t:transaction)
    WHERE n.transactionid = 2988706
    RETURN t
$$) AS (r agtype);
結果は省略されていますが、関連するトランザクション頂点の情報が返されます。

結果をプレビューするには、`age-viewer`で以下のSQLを使用（注意：最大50件のレコードを返します）:sql
SELECT * FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[r:transaction_addr1]->(a:addr1)<-[r2:transaction_addr1]-(t:transaction)
    WHERE n.transactionid = 2988706
    RETURN [n,r,a,r2,t]::path
    LIMIT 50
$$) AS (r agtype);
![6](https://yqintl.alicdn.com/5c2b4bd82e9522a35a46b89e93889b6b775039cd.png)

**・パス検索**
不正トランザクションの識別において、パスの概念はグラフ理論とネットワーク分析法を使用して潜在的な不正行動を特定することに関連します。金融取引やネットワークにおいて、トランザクションはグラフとして考えられる場合があり、ノードはアカウントや顧客を、エッジはトランザクション活動を表します。このグラフの構造を解析することで、異常なパターンや不正行為を発見することができます。トランザクション間のパスを計算することで、疑わしいアカウントから他のアカウントへのトランザクションチェーンを迅速に特定し、表面的に無関係なアカウント間の隠れたつながりを明らかにすることができます。

- **トランザクション記録2987000と2987172を取得し、不正トランザクションに基づいて関連パスを特定し、2つの表面的に無関sql
SELECT *
FROM cypher(不正グラフ, $$
    MATCH (n:transaction)-[r]->(v)<-[r1]-(t:transaction)-[r2]->(v2)<-[r3]-(k:transaction)
    WHERE n.transactionid = 2987000 
        AND k.transactionid=2987172
        AND t.isfraud = 1
    RETURN t 
$$) AS (e agtype);

----
{id: 844424930618281, label: transaction, properties: {isfraud: 1, transactionid: 3473312}}::頂点
{id: 844424930626886, label: transaction, properties: {isfraud: 1, transactionid: 3481917}}::頂点
{id: 844424930649640, label: transaction, properties: {isfraud: 1, transactionid: 3504671}}::頂点
{id: 844424930631805, label: transaction, properties: {isfraud: 1, transactionid: 3486836}}::頂点
{id: 844424930641980, label: transaction, properties: {isfraud: 1, transactionid: 3497011}}::頂点
{id: 844424930644942, label: transaction, properties: {isfraud: 1, transactionid: 3499973}}::頂点
age-viewerで次のSQLを使用して結果をプレビュー:
SELECT *
FROM cypher(不正グラフ, $$
    MATCH (n:transaction)-[r]->(v)<-[r1]-(t:transaction)-[r2]->(v2)<-[r3]-(k:transaction)
    WHERE n.transactionid = 2987000 
        AND k.transactionid=2987172
        AND t.isfraud = 1
    RETURN [n,r,v,r1,t,r2,v2,r3,k]::path
    LIMIT 50
$$) AS (e agtype);
![](https://yqintl.alicdn.com/74672d0db4d18bfd34e6df1b780b51ac76b5e86b.png)

共通隣人
不正取引の識別において、共通隣人判定は、社会的ネットワークやトランザクションネットワーク分析に基づく手法であり、可能性のある不正行為を特定するために使用される。この方法は主にグラフ理論に基づき、トランザクション参加者の関係ネットワークを分析し、疑わしい取引パターンを特定する。もし2つの取引が複数の共通隣を持ち、これらの隣人が取引上で異常な行動を示す場合（例えば、高頻度の取引、異常な金額など）、それらの2つの取引者が間のリスクが存在する可能性がある。● 取引記録2987000と取引記録2987172の共通隣人を探し、似た属性（住所、デバイスなど）を持つ取引記録を見つける。
SELECT *
FROM cypher(不正グラフ, $$
    MATCH (n:transaction)-[]->(v)<-[]-(t:transaction)
    WHERE n.transactionid = 2987000 AND t.transactionid=2987172
    RETURN v
$$) AS (r agtype);

----
{id: 2251799813685249, label: addr2, properties: {addr2: 87.0}}::頂点
{id: 1970324836974594, label: addr1, properties: {addr1: 315.0}}::頂点
age-viewerで次のSQLを使用して結果をプレビュー:
SELECT *
FROM cypher(不正グラフ, $$
    MATCH (n:transaction)-[r]->(v)<-[r1]-(t:transaction)
    WHERE n.transactionid = 2987000 AND t.transactionid=2987172
    RETURN [n,r,v,r1,t]::path
$$) AS (r agtype);
![](https://yqintl.alicdn.com/4c63dc09e7a27b3824c77eedf4b660a51dcc0e9a.png)

ジャカード類似度
ジャカード類似度は、2つの集合の類似度を測定するために使用される。式は次の通り:
![](https://yqintl.alicdn.com/a2dabf2302f6d6efbec39f3b431c95cad30a4adf.png)

ここで:
● J(A,B)は集合AとBのジャカード類似度である。
● |A∩B|は集合AとBの交集である。
● |A∪B|は集合AとBの和集合である。

不正検出において、ジャカード類似度は次のように活用できる:
・パターン認識: 不正行為には共通の特徴がよく見られる。ユーザーの取引や行動のジャカード類似度を計算することで、似た取引パターンを特定し、潜在的な不正活動を検出できる。
・顧客グループ分析: 顧客行動を分析する際、ジャカード類似度を使って異なる顧客の類似度を比較できる。より高い類似度を持つ顧客は、同様のリスク特性を持っている可能性がある。

この例では、管理された住所、メール、住所などの情報に基づいて取引のジャカード類似度を計算できる。関連情報の重なり合いが大きい場合は、取引の類似度が高いことを示す。

特定の取引に関連するすべてのノードを取得するための関数を作成し、すべてのポイントのIDの配列を返す。
CREATE OR REPLACE FUNCTION find_ids(transactionid integer)
RETURNS bigint[]
LANGUAGE plpgsql
AS $function$
DECLARE 
    sql VARCHAR;
    ids bigint[];
BEGIN
  sql := 'SELECT ARRAY_AGG(CAST(r AS BIGINT)) FROM (
SELECT *
FROM cypher(不正グラフ, $$
MATCH (n:transaction)-[]->(v)
    WHERE n.transactionid = ' || text($1) || '
RETURN id(v) 
$$) AS (r agtype)) AS t;';
    EXECUTE sql INTO ids;
    RETURN ids;
END
$function$;

配列に対するUnionとIntersection操作を行うサポート関数を作成。
create or replace function array_union(anyarray, anyarray)
returns anyarray language sql immutable as $$
select array_agg(a ORDER BY a)
from (
    select distinct unnest($1 || $2) as a
) s;
$$;

create or replace function array_intersection(anyarray, anyarray)
returns anyarray language sql immutable as $$
select array_agg(e)
from (
    select unnest($1)
    intersect
    select unnest($2)
) as dt(e)
$$;

最後に、2つの取引のジャカード類似度を計算する関数を作成。
CREATE OR REPLACE FUNCTION jaccardSimilarity(tid1 integer, tid2 integer)
RETURNS float8
LANGUAGE plpgsql
AS $function$
DECLARE 
    sql VARCHAR;
    ids1 bigint[];
    ids2 bigint[];
    union_list bigint[];
    intersection_list bigint[];
BEGIN
  ids1 = find_ids($1);
  ids2 = find_ids($2);
  union_list = array_union(ids1, ids2); -- 和集合
  intersection_list = array_intersection(ids1, ids2); -- 交集

  RETURN 
    CASE 
        WHEN array_length(union_list,1) = 0 THEN 0
        ELSE array_length(intersection_list,1) * 1.0 / array_length(union_list,1) 
    END AS jaccardSimilarity;
END
$function$;

使用する際に、2つの取引のIDを指定できる。
SELECT jaccardSimilarity(2987000, 2987172);

jaccardsimilarity 
----
 0.4

すべての取引の類似度を比較する必要がある場合、PolarDBのストアドプロシージャ機能をフルに活用し、より複雑な類似度計算タスクを完成させることができる。これにより、不正取引検出のニーズを満たすことができる。
例えば、以下のSQLを使用して、同じ住所1と住所2を持ち、並且つトランザクション2987002と同じメールドメインを持つすべてのトランザクションを見つけることができます。その後、ジャカード類似度で並べ替え、上位50件の類似トランザクションを見つけることができます。sql
WITH tmp AS (
  SELECT cast(r as INTEGER) AS transactionid
  FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[:transaction_addr2]->(:addr2)<-[:transaction_addr2]-(t:transaction)
    WHERE n.transactionid = 2987002
    MATCH (n:transaction)-[:transaction_addr1]->(:addr1)<-[:transaction_addr1]-(t:transaction)
    WHERE n.transactionid = 2987002
    MATCH (n:transaction)-[:transaction_emaildomain_p]->(:emaildomain)<-[:transaction_emaildomain_p]-(t:transaction)
    WHERE n.transactionid = 2987002
    RETURN t.transactionid
  $$) AS (r agtype)
)
SELECT transactionid, jaccard_similarity(2987002, transactionid) AS jaccard_similarity
FROM tmp
ORDER BY jaccard_similarity DESC
LIMIT 50;
 transactionid | jaccardsimilarity 
---------------+-------------------
       3323911 |               0.6
       3328911 |               0.6
       3009043 |               0.6
       3039416 |               0.6
       3039425 |               0.6
       2993652 |               0.6
       3045027 |               0.6
       3037644 |               0.6
       3045041 |               0.6
       ... 

結果をプレビューするには、age-viewer内で以下のSQLを使用してください（結果は事前に計算されており、上位10件のみを取り出しています。必要に応じてリストに含めることができます）：sql
SELECT *
FROM cypher(fraud_graph, $$
    MATCH (n:transaction)-[r]->(v)<-[r2]-(t:transaction)
    WHERE n.transactionid = 2987002
      AND t.transactionid IN [3323911, 3328911,3009043,3039416,3039425,2993652,3045027,3037644,3045041,3045049,3045279]
    RETURN [n,r,v,r2,t]::path
$$) AS (r agtype);

![9](https://yqintl.alicdn.com/47c932b8ae23521ae327449a78d3086be1341f39.png)

### 概要
この記事では、Alibaba CloudのクラウドネイティブなリレーショナルデータベースPolarDB for PostgreSQLエディションのグラフ分析機能を使用する方法を紹介します。PolarDBはAGE拡張機能と組み合わせることで、グラフデータの計算と分析能力を提供し、Cypherクエリ言語を使用して効率的にグラフデータの問い合わせ処理を行います。金融トランザクション分野のパブリックデータセットを例に、グラフクエリを実行して不正なトランザクションを検出する方法を示します。例えば、不正なトランザクションに関連する住所やデバイス情報の照会、不正トランザクションの関係特定、およびトランザクション間の類似度計算による不正行為検知などが含まれます。リレーショナルデータベース上で構築されたPolarDBは、企業統合データ管理と分析に強力なサポートを提供します。

### 付録
データ変換スクリプトが必要な場合は、<a href=https://www.alibabacloud.com/product/polardb-for-postgresql target=_blank>PolarDB for PostgreSQL</a>グラフィカルデータベースエンジンのサポートチームにお問い合わせくださいか、チケットを提出して取得してください。