實戰 SQL:銀行等金融機構可疑支付交易的監測

反洗錢

純屬原創,如有雷同,不是巧合,本文爲準!😎

大家好,我是隻談技術、不聊人生的 Tony 老師,在實戰 SQL 系列文章的上一篇中我們介紹瞭如何實現微信、微博等社交網絡中的友好、粉絲關係分析

今天,我們來談談另一個話題,如何利用 SQL 窗口函數發現可疑的銀行卡支付交易。2002 年,中國人民銀行爲了加強對人民幣支付交易的監督管理,規範人民幣支付交易報告行爲,防範利用銀行支付結算進行洗錢等違法犯罪活動,制定了《人民幣大額和可疑支付交易報告管理辦法》

該辦法定義了大額支付交易和可疑交易支付的各種場景和定義。其中大額交易判斷比較簡單,主要是通過單筆交易額進行監測;可疑交易的情況比較複雜,其中有一些是基於短期交易頻率、相同收付款人和交易額度等數據進行監測。針對這種類型的可疑交易,利用 SQL 窗口函數可以非常方便地進行分析。

本文示例經過驗證的數據庫包括 MySQL、Oracle、SQL Server、PostgreSQL 以及 SQLite,首先給出結論:

聚合窗口函數 MySQL Oracle SQL Server PostgreSQL SQLite
SUM() OVER() ✔️ ✔️ ✔️ ✔️ ✔️
COUNT() OVER() ✔️ ✔️ ✔️ ✔️ ✔️
AVG() OVER() ✔️ ✔️ ✔️ ✔️ ✔️
MAX() OVER() ✔️ ✔️ ✔️ ✔️ ✔️
MIN() OVER() ✔️ ✔️ ✔️ ✔️ ✔️

上面這些函數包含了OVER子句,都屬於窗口函數而不是聚合函數。

窗口函數簡介

窗口函數(Window Function)是專門用於數據分析的函數,它們針對查詢中的每一行數據,基於和當前行相關的一組數據計算出一個結果。我們可以通過與聚合函數比較來了解窗口函數的作用:

function
上圖中的 COUNT、SUM 以及 AVG 既可以用做聚合函數,也可以用作窗口函數;聚合函數針對所有的數據只返回一條結果,窗口函數爲每行數據都返回一個結果。

從定義上來講,窗口函數包含了一個OVER子句,用於指定數據分析的窗口:

window_function ( expression, ... ) OVER (
    PARTITION BY ...
    ORDER BY ...
    frame_clause
)

其中,window_function 是窗口函數的名稱;expression 是參數,有些函數不需要參數;OVER子句包含三個選項:分區(PARTITION BY)、排序(ORDER BY)以及窗口大小(frame_clause)。

PARTITION BY選項用於定義分區,作用類似於 GROUP BY 的分組。如果指定了分區選項,窗口函數將會分別針對每個分區單獨進行分析;如果省略分區選項,所有的數據作爲一個整體進行分析。

ORDER BY選項用於指定分區內的排序方式,通常用於數據的排名分析。

窗口選項 frame_clause 用於在當前分區內指定一個可移動的計算窗口;指定了窗口之後,分析函數不再基於分區進行計算,而是基於窗口內的數據進行計算。具體來說,窗口大小的常用選項如下:

{ ROWS | RANGE } frame_start
{ ROWS | RANGE } BETWEEN frame_start AND frame_end

其中,ROWS表示以行爲單位計算窗口的偏移量,RANGE表示以數值(例如 30 分鐘)爲單位計算窗口的偏移量,參考下圖:

frame
CURRENT ROW表示當前正在處理的行;其他的行可以使用相對當前行的位置表示;窗口的大小不會超出當前分區的範圍。

frame_start 用於定義窗口的起始位置,可以指定以下內容之一:

  • UNBOUNDED PRECEDING,窗口從分區的第一行開始,默認值;
  • N PRECEDING,窗口從當前行之前的第 N 行或者數值開始;
  • CURRENT ROW,窗口從當前行開始。

frame_end 用於定義窗口的結束位置,可以指定以下內容之一:

  • CURRENT ROW,窗口到當前行結束,默認值;
  • N FOLLOWING,窗口到當前行之後的第 N 行或者數值結束;
  • UNBOUNDED FOLLOWING,窗口到分區的最後一行結束。

常見的窗口函數可以分爲以下幾類:聚合窗口函數、排名窗口函數(實現產品的分類排名)以及取值窗口函數(實現銷量的同比/環比分析)。本文只涉及聚合窗口函數,其他函數下回分解。

接下來我們介紹兩個具體的案例,創建一個記錄銀行卡交易流水的表 transfer_log:

CREATE TABLE transfer_log
( log_id    INTEGER NOT NULL PRIMARY KEY,
  log_ts    TIMESTAMP NOT NULL,
  from_user VARCHAR(50) NOT NULL,
  to_user   VARCHAR(50),
  type      VARCHAR(10) NOT NULL,
  amount    NUMERIC(10) NOT NULL
);

INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (1,'2019-01-02 10:31:40','62221234567890',NULL,'存款',50000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (2,'2019-01-02 10:32:15','62221234567890',NULL,'存款',100000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (3,'2019-01-03 08:14:29','62221234567890','62226666666666','轉賬',200000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (4,'2019-01-05 13:55:38','62221234567890','62226666666666','轉賬',150000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (5,'2019-01-07 20:00:31','62221234567890','62227777777777','轉賬',300000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (6,'2019-01-09 17:28:07','62221234567890','62227777777777','轉賬',500000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (7,'2019-01-10 07:46:02','62221234567890','62227777777777','轉賬',100000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (8,'2019-01-11 09:36:53','62221234567890',NULL,'存款',40000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (9,'2019-01-12 07:10:01','62221234567890','62228888888881','轉賬',10000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (10,'2019-01-12 07:11:12','62221234567890','62228888888882','轉賬',8000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (11,'2019-01-12 07:12:36','62221234567890','62228888888883','轉賬',5000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (12,'2019-01-12 07:13:55','62221234567890','62228888888884','轉賬',6000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (13,'2019-01-12 07:14:24','62221234567890','62228888888885','轉賬',7000);
INSERT INTO transfer_log (log_id,log_ts,from_user,to_user,type,amount) VALUES (14,'2019-01-21 12:11:16','62221234567890','62228888888885','轉賬',70000);

還是需要說明一下:可疑支付交易並不一定就是有問題的交易;本文只是採用了一個簡化的計算模式作爲演示,主要目的是爲了說明窗口函數的作用。

短期累計轉賬超過一百萬元

當個人賬戶在短期(通常是 10 個營業日)內出現累計 100 萬元以上轉賬操作,我們認爲這是一個可疑的行爲,需要記錄並進一步進行分析。以下語句用於查詢 5 天之內累積轉賬超過 100 萬的賬號:

select *
from (
    select *, 
    sum(amount) over (partition by from_user order by log_ts range interval '5' day preceding) total_amount
    from transfer_log
    where type = '轉賬'
    ) t
where total_amount > 1000000;
log_id|log_ts             |from_user     |to_user       |type|amount|total_amount|
------|-------------------|--------------|--------------|----|------|------------|
     7|2019-01-10 07:46:02|62221234567890|62227777777777|轉賬  |100000|     1050000|

該查詢主要使用了窗口函數 sum,partition by 用於按照用戶進行分析,而不是將所有用戶交易混合在一起;order by 按照交易時間進行排序;range 將數據分析的窗口定義爲 5 天之內的交易流水。

查詢結果顯示賬號 62221234567890 在 5 天之內累計轉賬 105 萬。

相同收付款人短期頻繁轉賬

利用 COUNT 窗口函數,可以分析相同收付款人短期內的轉賬頻率,例如:

select *
from (
    select *, 
    count(1) over (partition by from_user,to_user order by log_ts range interval '5' day preceding) times
    from transfer_log
    where type = '轉賬'
    ) t
where times >= 3;
log_id|log_ts             |from_user     |to_user       |type|amount|times|
------|-------------------|--------------|--------------|----|------|-----|
     7|2019-01-10 07:46:02|62221234567890|62227777777777|轉賬  |100000|    3|

其中,count 函數用於統計次數;partition by 按照不同的發起方和接收方進行分組;其他參數和上一個示例相同。查詢表明賬號 62221234567890 在 5 天之內給賬號 62227777777777 轉賬了 3 次以上。

下面我們再來介紹一個 AVG 窗口函數的使用案例。

移動平均法預測產品的銷量

移動平均法是用一組最近的實際數據值來預測未來一期或幾期內公司產品的需求量、公司產能等的一種常用方法。移動平均法適用於近期預測,分爲簡單移動平均法、加權移動平均法、趨勢移動平均法等。

我們以簡單移動平均法爲例,也就是說未來一期的銷量等於前 N 期銷量的算術平均值。基於該銷售數據,我們預測一下未來的產品銷量:

select *, avg(amount) over (partition by product order by ym rows 4 preceding) next_amount
from sales_monthly
order by product,ym desc;
product  |ym    |amount  |next_amount |
---------|------|--------|------------|
桔子     |201906|11524.00|11351.400000|
桔子     |201905|11423.00|11266.400000|
桔子     |201904|11327.00|11179.400000|
桔子     |201903|11302.00|11102.400000|
桔子     |201902|11181.00|11009.600000|
桔子     |201901|11099.00|10931.000000|
桔子     |201812|10988.00|10847.200000|
桔子     |201811|10942.00|10765.200000|
桔子     |201810|10838.00|10677.800000|
桔子     |201809|10788.00|10603.200000|
桔子     |201808|10680.00|10510.600000|
桔子     |201807|10578.00|10423.600000|
...

avg 函數用於計算平均值;partition by 按照不同產品進行分析;order by 按照月份進行排序;rows 指定分析窗口爲前 4 個月和當前月(共 5 期數據進行平均)。

查詢結果顯示“桔子”最新一期(201907)的預期銷量爲 11351.4;利用已有的銷量數據和基於歷史的預測值,可以計算出預測的標準誤差(需要用到取值窗口函數 LAG),從而可以嘗試不同的 N 值並找出更誤差最小的值。

總結

SQL 窗口函數提供了強大的數據分析功能,我們介紹了一些聚合窗口函數的使用。SUM 函數常常用於計算曆史累計值,COUNT 函數可以用於計算數據累計出現的次數,AVG 函數可以用於計算移動平均值。

除了上面的幾種場景,你還遇到過或者知道哪些應用案例?歡迎關注❤️、評論📝、點贊👍!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章