金融風控相關業務介紹
- 早期信貸風控怎麼做?
- 人審靠業務經驗,效率低
- 不适用于移動網際網路時代的金融場景
- 模組化的概念
- 模組化就是構造一個數學公式,将我們手上有的資料輸入進去,通過計算得到預測結果
- 風控模型最原始的思路就是輸入使用者的資訊,得到這個人 “會還錢” 還是 “不會還錢”。這就是個二分類問題。
- 而評分卡模型就是希望能将一系列的個人資訊輸入模型,然後得到一個使用者的還款機率
- 機率越大,評分越高,越容易還錢
- 機率越小,評分越低,越容易跑路
- 典型例子就是芝麻信用分。
- 為什麼一定要應射成某種分數呢
- 有分數刻度的好處
- 我們可以随時根據業務需求調整通過率
- 更容易向使用者解釋他的信用評級
- 更容易向上司解釋一個使用者被拒絕的原因
- 更容易監控一個模型的效果
-
風控流程
風控的角度來看,基本上可以歸結為以下幾個部分:
資料采集
反欺詐
政策
模型
催收
-
資料采集
資料采集會涉及到埋點和爬蟲技術,基本上業内的資料都大同小異。免費的營運商資料、和安卓可爬的手機内部資訊(app名稱,手機裝置資訊,部分app内容資訊)、以及收費的征信資料、各種資訊校驗、外部黑名單之類的。還有一些特定場景的現金貸和消費金融會有自有的資料可供使用,比如阿裡京東自己的電商資料、滴滴的司機資料、順豐中通的快遞資料等等。由于不涉及爬蟲,這一塊主要讓大家了解一下都可以做些什麼變量。
-
反欺詐引擎
反欺詐引擎主要包括兩個部分,反欺詐規則和反欺詐模型。這裡其實很少使用傳統監督模型。涉及到的算法以無監督算法、社交網絡算法、深度學習居多。大部分的公司都使用的是反欺詐規則,這也是主要提倡的。【一個原因是欺詐标簽不好得到,很難做監督學習的訓練。還有一個原因是傳統的機器學習對欺詐的檢測效果很差。因為所謂欺詐,就是一些黑産或者個人将自己包裝成信用良好的使用者,進行借款後失聯或者拒不還錢。既然都僞裝成了好客戶,基于風控人員主觀思考建立的統計模型,又怎麼可能有好的效果。但是經過一段時間的實驗,這一塊其實用深度學習反而有意想不到的效果,基本思想可以了解為,簡單評分卡解釋性強,帶來的壞處就是可以被逆向破解,而複雜模型的黑箱操作雖然解釋性差,卻有一定的安全性,尤其是搭配了線上學習等動态手段之後。反向破解的成本極高。此外還有很多算法諸如異常檢測和知識圖譜都在這一塊有所應用。
-
規則引擎
規則引擎其實就是我們常說的政策,主要通過資料分析、挖掘手段以及一些監督、無監督算法,得到不同字段、各個區間的壞賬率(badrate),找到最佳分段區間,然後得到篩選後信用較好的一批特定人群進行放款。這一塊主要有單變量分析和一些關鍵名額的計算和監控,比如Rollrate、PSI、KS、AUC,等等。通正常則和模型是組合使用的,尤其在反欺詐場景中。
-
風控模型
風控模型是機器學習在風控領域的主要展現。當然前面提到的反欺詐模型也是重點之一。主要是通過監督算法建構違約機率預測模型。但是因為實際業務中,是資料的品質并不是永遠那麼完美,這裡通常我們會使用到深度學習、無監督、弱監督等等方法去輔助 傳統監督學習算法。
風控模型其中包含了A/B/C卡。模型算法之間可以沒有顯著差別,而是根據其發生的時間點不同而進行劃分的
- 貸前 申請評分卡 Application score card A卡可以用客戶曆史逾期天數最大的天數
- 貸中 行為評分卡 Behavior score card B卡則可以多期借款中逾期最大的一次
- 貸後 催收評分卡 Collection score card C卡 是否被内催催回來定義y
- 也就是y産生的方式不一樣。通常信貸領域都是用逾期天數來定義y
- C卡因為用途不同有不同的建立方法。比如你們公司有内催,有外催。外催肯定是回款率低,單價貴的。那麼就可以根據是否被内催催回來定義y。
-
催收
催收是風控的最終手段。這個環節可以産生很多對模型有幫助的資料。比如催收記錄的文字描述、觸達率、欺詐标簽等等。并且壞賬的客戶會被列入黑名單。其實隻要是能被催回來的,都不是壞賬。但是很多公司為了保險起見,逾期超過一定時間的客戶,即使被催回來,也會被拉入黑名單。這裡主要的算法就是催收模型相關的,可能是監督、無監督算法。也有基于社交網絡算法構造的失聯模型等等。
-
幾個概念
Badrate: 壞人占比
MOB (month on book):開卡時長
Vintage分析法是一種動态分析法,用來分析不同時期資産的表現情況,它以貸款的賬齡為基礎,觀察每批放款貸後1,2,3…N個月時的逾期情況。
Roll-Rate分析追溯貸款狀态之間每月的遷移情況,展示了每批貸款進入下一個逾期狀态的機率。
金融反欺詐 常用特征處理方法
使用者基本屬性
phone_nember
- 手機号字首,歸屬地是否相同
- 是否是虛拟營運商
- 流量卡還是通話卡
電話本備注 nickname
- 昵稱符合固定的規律(中文+數字)
- 備注是否符合某種親密的稱呼
birthday
- 年紀 星座 生肖
sex
- 性别是否失衡
password
- 是否都相同
身份證号碼
- 年齡 核對
- 性别 核對
- 城市
郵箱
- 是否是一次性郵箱
- username 滿足規律
- 是否同一郵箱服務商
- 郵箱裡面的資料(賬單)
學曆
- 相似性
住房
- 租房情況是否雷同
積分
- 是不是超過某個門檻值
簽到
- 相似性
ip
- 是否是同一個号段
- 每次登入ip位址是否相同
- 是不是臨時ip 和 gps
- ip 和 gps 是否能對的上
gps
- 經緯度相似性分析
- 國家 省份 城市 相似性
- ip 和 gps 是否能對的上
wifi
- ssid
- wifi list
- 貸款前的幾分鐘有沒有切換過wifi
application time
- 時間切片
- 注冊用了多長時間(太快太慢都有問題)
- 一共申請了幾次
login time
- 時間切片
- 登陸了幾次、頻率
- 最後一次登入時間距貸款時間的間隔
- 同一時間登入做一個校驗(同一時間多人登入)
ua(user agent)
- 每次打開是否是同一個ua
管道
- app/H5/微信
- 管道ID屬于違規管道
app version
- 每次app的版本号是否相同
- app版本會不會太老了(老版本的app有bug,可能會被黑中介用來攻擊我們)
推薦人/聯系人
- 名字比對
- 手機号比對
裝置指紋
imei
- 否都相同
- 每次登入imei号是否都相同
device id
- 否都相同
- 每次登入device id号是否都相同
分辨率
- 手機型号和螢幕分辨率是否一緻
mobile type
- 手機品牌
- 手機型号
os(operating system)
- 每次打開作業系統是否都相同
- 來申請的人是否os都相同
- os的版本是否太舊
中文錯别字可以考慮轉換成拼音做相似度比對
address
- 位址要标準化
- 模糊比對
- 相似度計算(cos距離,詞向量)
company
- 正則
- 位元組拆分
- 關鍵字提取
- 相似度計算
- 錯别字/同音字識别
第三方資料
人行征信
- 公司資訊是否一緻
- 學曆是否一緻
- 居住位址是否一緻
- 手機号碼是否一緻
- 逾期資料
營運商
- 是否有相同的聯系人
- 是否有黑名單客戶在通訊錄中
- 通話最頻繁的幾個人(所在地是否和他相同)
社保公積金
- 工資
- 社保
- 公積金
特征工程
特征選擇 (feature_selection)
Filter
- 移除低方差的特征 (Removing features with low variance)
- 單變量特征選擇 (Univariate feature selection)
Wrapper
- 遞歸特征消除 (Recursive Feature Elimination)
Embedded
- 使用SelectFromModel選擇特征 (Feature selection using SelectFromModel)
- 将特征選擇過程融入pipeline (Feature selection as part of a pipeline)
當資料預處理完成後,我們需要選擇有意義的特征輸入機器學習的算法和模型進行訓練。
通常來說,從兩個方面考慮來選擇特征:
特征是否發散
如果一個特征不發散,例如方差接近于0,也就是說樣本在這個特征上基本上沒有差異,這個特征對于樣本的區分并沒有什麼用。
特征與目标的相關性
這點比較顯見,與目标相關性高的特征,應當優選選擇。除移除低方差法外,本文介紹的其他方法均從相關性考慮。
根據特征選擇的形式又可以将特征選擇方法分為3種:
- Filter:過濾法,按照發散性或者相關性對各個特征進行評分,設定門檻值或者待選擇門檻值的個數,選擇特征。
- Wrapper:包裝法,根據目标函數(通常是預測效果評分),每次選擇若幹特征,或者排除若幹特征。
- Embedded:嵌入法,先使用某些機器學習的算法和模型進行訓練,得到各個特征的權值系數,根據系數從大到小選擇特征。類似于Filter方法,但是是通過訓練來确定特征的優劣。
特征選擇主要有兩個目的:
- 減少特征數量、降維,使模型泛化能力更強,減少過拟合;
- 增強對特征和特征值之間的了解。
拿到資料集,一個特征選擇方法,往往很難同時完成這兩個目的。通常情況下,選擇一種自己最熟悉或者最友善的特征選擇方法(往往目的是降維,而忽略了對特征和資料了解的目的)。接下來将結合 Scikit-learn提供的例子 介紹幾種常用的特征選擇方法,它們各自的優缺點和問題。
Filter
1)移除低方差的特征 (Removing features with low variance)
假設某特征的特征值隻有0和1,并且在所有輸入樣本中,95%的執行個體的該特征取值都是1,那就可以認為這個特征作用不大。如果100%都是1,那這個特征就沒意義了。當特征值都是離散型變量的時候這種方法才能用,如果是連續型變量,就需要将連續變量離散化之後才能用。而且實際當中,一般不太會有95%以上都取某個值的特征存在,是以這種方法雖然簡單但是不太好用。可以把它作為特征選擇的預處理,先去掉那些取值變化小的特征,然後再從接下來提到的的特征選擇方法中選擇合适的進行進一步的特征選擇。
In [3]:
from sklearn.feature_selection import VarianceThreshold
X = [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 1], [0, 1, 0], [0, 1, 1]]
sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
sel.fit_transform(X)
Out[3]:
array([[0, 1],
[1, 0],
[0, 0],
[1, 1],
[1, 0],
[1, 1]])
果然, VarianceThreshold 移除了第一列特征,第一列中特征值為0的機率達到了5/6.
2)單變量特征選擇 (Univariate feature selection)
單變量特征選擇的原理是分别單獨的計算每個變量的某個統計名額,根據該名額來判斷哪些變量重要,剔除那些不重要的變量。
對于分類問題(y離散),可采用:
- 卡方檢驗
對于回歸問題(y連續),可采用:
- 皮爾森相關系數
- f_regression,
- mutual_info_regression
- 最大資訊系數
這種方法比較簡單,易于運作,易于了解,通常對于了解資料有較好的效果(但對特征優化、提高泛化能力來說不一定有效)。
- SelectKBest 移除得分前 k 名以外的所有特征(取top k)
- SelectPercentile 移除得分在使用者指定百分比以後的特征(取top k%)
- 對每個特征使用通用的單變量統計檢驗: 假正率(false positive rate) SelectFpr, 僞發現率(false discovery rate) SelectFdr, 或族系誤差率 SelectFwe.
- GenericUnivariateSelect 可以設定不同的政策來進行單變量特征選擇。同時不同的選擇政策也能夠使用超參數尋優,進而讓我們找到最佳的單變量特征選擇政策。
卡方(Chi2)檢驗
經典的卡方檢驗是檢驗定性自變量對定性因變量的相關性。比如,我們可以對樣本進行一次chi2 測試來選擇最佳的兩項特征:
In [4]:
from sklearn.datasets import load_iris
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
iris = load_iris()
X, y = iris.data, iris.target
X.shape
Out[4]:
(150, 4)
In [5]:
X_new = SelectKBest(chi2, k=2).fit_transform(X, y)
X_new.shape
Out[5]:
(150, 2)
Pearson相關系數 (Pearson Correlation)
皮爾森相關系數是一種最簡單的,能幫助了解特征和響應變量之間關系的方法,該方法衡量的是變量之間的線性相關性,結果的取值區間為[-1,1],-1表示完全的負相關,+1表示完全的正相關,0表示沒有線性相關。
In [8]:
import numpy as np
from scipy.stats import pearsonr
np.random.seed(0)
size = 300
x = np.random.normal(0, 1, size)
# pearsonr(x, y)的輸入為特征矩陣和目标向量,能夠同時計算 相關系數 和p-value.
print("Lower noise", pearsonr(x, x + np.random.normal(0, 1, size)))
print("Higher noise", pearsonr(x, x + np.random.normal(0, 10, size)))
Lower noise (0.7182483686213841, 7.32401731299835e-49)
Higher noise (0.057964292079338155, 0.3170099388532475)
這個例子中,我們比較了變量在加入噪音之前和之後的差異。當噪音比較小的時候,相關性很強,p-value很低。
我們使用Pearson相關系數主要是為了看特征之間的相關性,而不是和因變量之間的。
Wrapper
遞歸特征消除 (Recursive Feature Elimination)
遞歸消除特征法使用一個基模型來進行多輪訓練,每輪訓練後,移除若幹權值系數的特征,再基于新的特征集進行下一輪訓練。
對特征含有權重的預測模型(例如,線性模型對應參數coefficients),RFE通過遞歸減少考察的特征集規模來選擇特征。首先,預測模型在原始特征上訓練,每個特征指定一個權重。之後,那些擁有最小絕對值權重的特征被踢出特征集。如此往複遞歸,直至剩餘的特征數量達到所需的特征數量。
RFECV 通過交叉驗證的方式執行RFE,以此來選擇最佳數量的特征:對于一個數量為d的feature的集合,他的所有的子集的個數是2的d次方減1(包含空集)。指定一個外部的學習算法,比如SVM之類的。通過該算法計算所有子集的validation error。選擇error最小的那個子集作為所挑選的特征。
In [29]:
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
rf = RandomForestClassifier()
iris=load_iris()
X,y=iris.data,iris.target
rfe = RFE(estimator=rf, n_features_to_select=3)
X_rfe = rfe.fit_transform(X,y)
X_rfe.shape
Out[29]:
(150, 3)
In [30]:
X_rfe[:5,:]
Out[30]:
array([[5.1, 1.4, 0.2],
[4.9, 1.4, 0.2],
[4.7, 1.3, 0.2],
[4.6, 1.5, 0.2],
[5. , 1.4, 0.2]])
In [ ]:
#### Embedded
使用SelectFromModel選擇特征 (Feature selection using SelectFromModel)
基于L1的特征選擇 (L1-based feature selection)
使用L1範數作為懲罰項的線性模型(Linear models)會得到稀疏解:大部分特征對應的系數為0。當你希望減少特征的次元以用于其它分類器時,可以通過 feature_selection.SelectFromModel 來選擇不為0的系數。
特别指出,常用于此目的的稀疏預測模型有 linear_model.Lasso(回歸), linear_model.LogisticRegression 和 svm.LinearSVC(分類)
In [31]:
from sklearn.feature_selection import SelectFromModel
from sklearn.svm import LinearSVC
lsvc = LinearSVC(C=0.01, penalty="l1", dual=False).fit(X,y)
model = SelectFromModel(lsvc, prefit=True)
X_embed = model.transform(X)
X_embed.shape
Out[31]:
In [32]:
X_embed[:5,:]
Out[32]:
array([[5.1, 3.5, 1.4],
[4.9, 3. , 1.4],
[4.7, 3.2, 1.3],
[4.6, 3.1, 1.5],
[5. , 3.6, 1.4]])
工作中的套路
模型上線後可能會遇到的問題:
- 模型效果不好
- 訓練集效果好,跨時間測試效果不好
- 跨時間測試效果也好,上線之後效果不好
- 上線之後效果還好,幾周之後分數分布開始下滑
- 一兩個月内都比較穩定,突然分數分布驟降
- 沒有明顯問題,但模型每個月逐漸失效
考慮一下業務所需要的變量是什麼。
- 變量必須對模型有貢獻,也就是說必須能對客群加以區分
- 邏輯回歸要求變量之間線性無關
- 邏輯回歸評分卡也希望變量呈現單調趨勢 (有一部分也是業務原因,但從模型角度來看,單調變量未必一定比有轉折的變量好)
- 客群在每個變量上的分布穩定,分布遷移無可避免,但不能波動太大
從上述方法中找到最貼合目前使用場景的幾種方法。
In [29]:
import pandas as pd
import numpy as np
df_train = pd.read_csv('train.csv')
df_train.head()
Out[29]:
PassengerId | label | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | A/5 21171 | 7.2500 | NaN | S | ||
1 | 2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th… | female | 38.0 | 1 | PC 17599 | 71.2833 | C85 | C |
2 | 3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | STON/O2. 3101282 | 7.9250 | NaN | S | |
3 | 4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 113803 | 53.1000 | C123 | S |
4 | 5 | 3 | Allen, Mr. William Henry | male | 35.0 | 373450 | 8.0500 | NaN | S |
1)變量重要性
- 卡方檢驗
- 模型篩選
2)共線性
- 相關系數 COR
在做很多基于空間劃分思想的模型的時候,我們必須關注變量之間的相關性。單獨看兩個變量的時候我們會使用皮爾遜相關系數。
In [6]:
df_train.corr()
Out[6]:
PassengerId | label | Pclass | Age | SibSp | Parch | Fare | |
---|---|---|---|---|---|---|---|
PassengerId | 1.000000 | -0.005007 | -0.035144 | 0.036847 | -0.057527 | -0.001652 | 0.012658 |
label | -0.005007 | 1.000000 | -0.338481 | -0.077221 | -0.035322 | 0.081629 | 0.257307 |
Pclass | -0.035144 | -0.338481 | 1.000000 | -0.369226 | 0.083081 | 0.018443 | -0.549500 |
Age | 0.036847 | -0.077221 | -0.369226 | 1.000000 | -0.308247 | -0.189119 | 0.096067 |
SibSp | -0.057527 | -0.035322 | 0.083081 | -0.308247 | 1.000000 | 0.414838 | 0.159651 |
Parch | -0.001652 | 0.081629 | 0.018443 | -0.189119 | 0.414838 | 1.000000 | 0.216225 |
Fare | 0.012658 | 0.257307 | -0.549500 | 0.096067 | 0.159651 | 0.216225 | 1.000000 |
3)單調性
- bivar圖
# 等頻切分
df_train.loc[:,'fare_qcut'] = pd.qcut(df_train['Fare'], 10)
df_train.head()
df_train = df_train.sort_values('Fare')
alist = list(set(df_train['fare_qcut']))
badrate = {}
for x in alist:
a = df_train[df_train.fare_qcut == x]
bad = a[a.label == 1]['label'].count()
good = a[a.label == 0]['label'].count()
badrate[x] = bad/(bad+good)
f = zip(badrate.keys(),badrate.values())
f = sorted(f,key = lambda x : x[1],reverse = True )
badrate = pd.DataFrame(f)
badrate.columns = pd.Series(['cut','badrate'])
badrate = badrate.sort_values('cut')
print(badrate)
badrate.plot('cut','badrate')
cut badrate
9 (-0.001, 7.55] 0.141304
6 (7.55, 7.854] 0.298851
8 (7.854, 8.05] 0.179245
7 (8.05, 10.5] 0.230769
3 (10.5, 14.454] 0.428571
4 (14.454, 21.679] 0.420455
2 (21.679, 27.0] 0.516854
5 (27.0, 39.688] 0.373626
1 (39.688, 77.958] 0.528090
0 (77.958, 512.329] 0.758621
Out:
<matplotlib.axes._subplots.AxesSubplot at 0x168263d8cf8>
4)穩定性
- PSI
- 跨時間交叉檢驗
跨時間交叉檢驗
就是将樣本按照月份切割,一次作為訓練集和測試集來訓練模型,取進入模型的變量之間的交集,但是要小心共線特征!
解決方法
- 不需要每次都進入模型,大部分都在即可
- 先去除共線性(這也是為什麼內建模型我們也會去除共線性)
群體穩定性名額(population stability index)
公式:
𝑃𝑆𝐼=∑(實際占比−預期占比)∗ln(實際占比預期占比)PSI=∑(實際占比−預期占比)∗ln(實際占比預期占比)
來自知乎的例子:
比如訓練一個logistic回歸模型,預測時候會有個機率輸出p。
測試集上的輸出設定為p1,将它從小到大排序後10等分,如0-0.1,0.1-0.2,…。
用這個模型去對新的樣本進行預測,預測結果叫p2,按p1的區間也劃分為10等分。
實際占:是p2上在各區間的使用者占比
預期占:p1上在各區間的使用者占比。
意義就是如果模型很穩定,那麼p1和p2上各區間的使用者應該是相近的,占比不會變動很大,也就是預測出來的機率不會差距很大。
一般認為psi小于0.1時候模型穩定性很高,0.1-0.25一般,大于0.25模型穩定性差,建議重做。
注意分箱的數量将會影響着變量的PSI值。
PSI并不隻可以對模型來求,對變量來求也一樣。隻需要對跨時間分箱的資料分别求PSI即可。
案例
import pandas as pd
import numpy as np
data = pd.read_excel('oil_data_for_tree.xlsx')
data.head()
uid | oil_actv_dt | create_dt | total_oil_cnt | pay_amount_total | class_new | bad_ind | oil_amount | discount_amount | sale_amount | amount | pay_amount | coupon_amount | payment_coupon_amount | channel_code | oil_code | scene | source_app | call_source |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
A8217710 | 2018-08-19 | 2018-08-17 | 275.0 | 48295495.4 | B | 3308.56 | 1760081.0 | 1796001.0 | 1731081.0 | 8655401.0 | 1.0 | 1.0 | 1 | 3 | 2 | 3 | ||
1 | A8217710 | 2018-08-19 | 2018-08-16 | 275.0 | 48295495.4 | B | 4674.68 | 2487045.0 | 2537801.0 | 2437845.0 | 12189221.0 | 1.0 | 1.0 | 1 | 3 | 2 | 3 | |
2 | A8217710 | 2018-08-19 | 2018-08-15 | 275.0 | 48295495.4 | B | 1873.06 | 977845.0 | 997801.0 | 961845.0 | 4809221.0 | 1.0 | 1.0 | 1 | 2 | 2 | 3 | |
3 | A8217710 | 2018-08-19 | 2018-08-14 | 275.0 | 48295495.4 | B | 4837.78 | 2526441.0 | 2578001.0 | 2484441.0 | 12422201.0 | 1.0 | 1.0 | 1 | 2 | 2 | 3 | |
4 | A8217710 | 2018-08-19 | 2018-08-13 | 275.0 | 48295495.4 | B | 2586.38 | 1350441.0 | 1378001.0 | 1328441.0 | 6642201.0 | 1.0 | 1.0 | 1 | 2 | 2 | 3 |
In [32]:
set(data.class_new)
Out[32]:
{'A', 'B', 'C', 'D', 'E', 'F'}
org_lst 不需要做特殊變換,直接去重
agg_lst 數值型變量做聚合
dstc_lst 文本型變量做cnt
In [33]:
org_lst = ['uid','create_dt','oil_actv_dt','class_new','bad_ind']
agg_lst = ['oil_amount','discount_amount','sale_amount','amount','pay_amount','coupon_amount','payment_coupon_amount']
dstc_lst = ['channel_code','oil_code','scene','source_app','call_source']
資料重組
In [34]:
df = data[org_lst].copy()
df[agg_lst] = data[agg_lst].copy()
df[dstc_lst] = data[dstc_lst].copy()
看一下缺失情況
In [35]:
Out[35]:
uid 0
create_dt 4944
oil_actv_dt 0
class_new 0
bad_ind 0
oil_amount 4944
discount_amount 4944
sale_amount 4944
amount 4944
pay_amount 4944
coupon_amount 4944
payment_coupon_amount 4946
channel_code 0
oil_code 0
scene 0
source_app 0
call_source 0
dtype: int64
看一下基礎變量的describe
In [7]:
df.describe()
Out[7]:
bad_ind | oil_amount | discount_amount | sale_amount | amount | pay_amount | coupon_amount | payment_coupon_amount | channel_code | oil_code | scene | source_app | call_source | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 50609.000000 | 45665.000000 | 4.566500e+04 | 4.566500e+04 | 4.566500e+04 | 4.566500e+04 | 45665.000000 | 45663.000000 | 50609.000000 | 50609.000000 | 50609.000000 | 50609.000000 | 50609.000000 |
mean | 0.017764 | 425.376107 | 1.832017e+05 | 1.881283e+05 | 1.808673e+05 | 9.043344e+05 | 0.576853 | 149.395397 | 1.476378 | 1.617894 | 1.906519 | 0.306072 | 2.900729 |
std | 0.132093 | 400.596244 | 2.007574e+05 | 2.048742e+05 | 1.977035e+05 | 9.885168e+05 | 0.494064 | 605.138823 | 1.511470 | 3.074166 | 0.367280 | 0.893682 | 0.726231 |
min | 0.000000 | 1.000000 | 0.000000e+00 | 0.000000e+00 | 1.000000e+00 | 5.000000e+00 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 0.000000 | 175.440000 | 6.039100e+04 | 6.200100e+04 | 5.976100e+04 | 2.988010e+05 | 0.000000 | 1.000000 | 1.000000 | 0.000000 | 2.000000 | 0.000000 | 3.000000 |
50% | 0.000000 | 336.160000 | 1.229310e+05 | 1.279240e+05 | 1.209610e+05 | 6.048010e+05 | 1.000000 | 1.000000 | 1.000000 | 0.000000 | 2.000000 | 0.000000 | 3.000000 |
75% | 0.000000 | 557.600000 | 2.399050e+05 | 2.454010e+05 | 2.360790e+05 | 1.180391e+06 | 1.000000 | 100.000000 | 1.000000 | 0.000000 | 2.000000 | 0.000000 | 3.000000 |
max | 1.000000 | 7952.820000 | 3.916081e+06 | 3.996001e+06 | 3.851081e+06 | 1.925540e+07 | 1.000000 | 50000.000000 | 6.000000 | 9.000000 | 2.000000 | 3.000000 | 4.000000 |
對creat_dt做補全,用oil_actv_dt來填補
截取6個月的資料。
構造變量的時候不能直接對曆史所有資料做累加。
否則随着時間推移,變量分布會有很大的變化。
In [37]:
def time_isna(x,y):
if str(x) == 'NaT':
x = y
else:
x = x
return x
df2 = df.sort_values(['uid','create_dt'],ascending = False)
df2['create_dt'] = df2.apply(lambda x: time_isna(x.create_dt,x.oil_actv_dt),axis = 1)
df2['dtn'] = (df2.oil_actv_dt - df2.create_dt).apply(lambda x :x.days)
df = df2[df2['dtn']<180]
df.head()
Out[37]:
uid | create_dt | oil_actv_dt | class_new | bad_ind | oil_amount | discount_amount | sale_amount | amount | pay_amount | coupon_amount | payment_coupon_amount | channel_code | oil_code | scene | source_app | call_source | dtn |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
50608 | B96436391985035703 | 2018-10-08 | 2018-10-08 | B | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6 | 9 | 2 | 3 | 4 | |
50607 | B96436391984693397 | 2018-10-11 | 2018-10-11 | E | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6 | 9 | 2 | 3 | 4 | |
50606 | B96436391977217468 | 2018-10-17 | 2018-10-17 | B | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6 | 9 | 2 | 3 | 4 | |
50605 | B96436391976480892 | 2018-09-28 | 2018-09-28 | B | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6 | 9 | 2 | 3 | 4 | |
50604 | B96436391972106043 | 2018-10-19 | 2018-10-19 | A | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 6 | 9 | 2 | 3 | 4 |
對org_list變量求曆史貸款天數的最大間隔,并且去重
In [38]:
base = df[org_lst]
base['dtn'] = df['dtn']
base = base.sort_values(['uid','create_dt'],ascending = False)
base = base.drop_duplicates(['uid'],keep = 'first')
base.shape
Out[38]:
(11099, 6)
In [39]:
gn = pd.DataFrame()
for i in agg_lst:
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:len(df[i])).reset_index())
tp.columns = ['uid',i + '_cnt']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.where(df[i]>0,1,0).sum()).reset_index())
tp.columns = ['uid',i + '_num']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nansum(df[i])).reset_index())
tp.columns = ['uid',i + '_tot']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmean(df[i])).reset_index())
tp.columns = ['uid',i + '_avg']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i])).reset_index())
tp.columns = ['uid',i + '_max']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmin(df[i])).reset_index())
tp.columns = ['uid',i + '_min']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanvar(df[i])).reset_index())
tp.columns = ['uid',i + '_var']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
tp = pd.DataFrame(df.groupby('uid').apply(lambda df:np.nanmax(df[i]) -np.nanmin(df[i]) ).reset_index())
tp.columns = ['uid',i + '_var']
if gn.empty == True:
gn = tp
else:
gn = pd.merge(gn,tp,on = 'uid',how = 'left')
In [40]:
gn.columns
Out[40]:
Index(['uid', 'oil_amount_cnt', 'oil_amount_num', 'oil_amount_tot',
'oil_amount_avg', 'oil_amount_max', 'oil_amount_min',
'oil_amount_var_x', 'oil_amount_var_y', 'discount_amount_cnt',
'discount_amount_num', 'discount_amount_tot', 'discount_amount_avg',
'discount_amount_max', 'discount_amount_min', 'discount_amount_var_x',
'discount_amount_var_y', 'sale_amount_cnt', 'sale_amount_num',
'sale_amount_tot', 'sale_amount_avg', 'sale_amount_max',
'sale_amount_min', 'sale_amount_var_x', 'sale_amount_var_y',
'amount_cnt', 'amount_num', 'amount_tot', 'amount_avg', 'amount_max',
'amount_min', 'amount_var_x', 'amount_var_y', 'pay_amount_cnt',
'pay_amount_num', 'pay_amount_tot', 'pay_amount_avg', 'pay_amount_max',
'pay_amount_min', 'pay_amount_var_x', 'pay_amount_var_y',
'coupon_amount_cnt', 'coupon_amount_num', 'coupon_amount_tot',
'coupon_amount_avg', 'coupon_amount_max', 'coupon_amount_min',
'coupon_amount_var_x', 'coupon_amount_var_y',
'payment_coupon_amount_cnt', 'payment_coupon_amount_num',
'payment_coupon_amount_tot', 'payment_coupon_amount_avg',
'payment_coupon_amount_max', 'payment_coupon_amount_min',
'payment_coupon_amount_var_x', 'payment_coupon_amount_var_y'],
dtype='object')
對dstc_lst變量求distinct個數
In [41]:
gc = pd.DataFrame()
for i in dstc_lst:
tp = pd.DataFrame(df.groupby('uid').apply(lambda df: len(set(df[i]))).reset_index())
tp.columns = ['uid',i + '_dstc']
if gc.empty == True:
gc = tp
else:
gc = pd.merge(gc,tp,on = 'uid',how = 'left')
将變量組合在一起
In [42]:
fn = pd.merge(base,gn,on= 'uid')
fn = pd.merge(fn,gc,on= 'uid')
fn.shape
Out[42]:
(11099, 67)
In [43]:
fn = fn.fillna(0)
In [14]:
fn.head(100)
Out[14]:
uid | create_dt | oil_actv_dt | class_new | bad_ind | dtn | oil_amount_cnt | oil_amount_num | oil_amount_tot | oil_amount_avg | … | payment_coupon_amount_max | payment_coupon_amount_min | payment_coupon_amount_var_x | payment_coupon_amount_var_y | payment_coupon_amount_var | channel_code_dstc | oil_code_dstc | scene_dstc | source_app_dstc | call_source_dstc |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
B96436391985035703 | 2018-10-08 | 2018-10-08 | B | 1 | 0.00 | 0.00 | … | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 1 | 1 | 1 | 1 | |||
1 | B96436391984693397 | 2018-10-11 | 2018-10-11 | E | 1 | 0.00 | 0.00 | … | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 1 | 1 | 1 | 1 | ||
2 | B96436391977217468 | 2018-10-17 | 2018-10-17 | B | 1 | 0.00 | 0.00 | … | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 1 | 1 | 1 | 1 | ||
3 | B96436391976480892 | 2018-09-28 | 2018-09-28 | B | 1 | 0.00 | 0.00 | … | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1 | 1 | 1 | 1 | 1 |
100 rows × 74 columns
訓練決策樹模型
In [44]:
x = fn.drop(['uid','oil_actv_dt','create_dt','bad_ind','class_new'],axis = 1)
y = fn.bad_ind.copy()
from sklearn import tree
dtree = tree.DecisionTreeRegressor(max_depth = 2,min_samples_leaf = 500,min_samples_split = 5000)
dtree = dtree.fit(x,y)
輸出決策樹圖像,并作出決策
In [45]:
import pydotplus
from IPython.display import Image
from sklearn.externals.six import StringIO
import os
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
with open("dt.dot", "w") as f:
tree.export_graphviz(dtree, out_file=f)
dot_data = StringIO()
tree.export_graphviz(dtree, out_file=dot_data,
feature_names=x.columns,
class_names=['bad_ind'],
filled=True, rounded=True,
special_characters=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(graph.create_png())
value = badrate
In [19]:
sum(fn.bad_ind)/len(fn.bad_ind)
Out[19]:
0.04658077304261645