IEEE-CIS Fraud Detection 覆盤——嘗試對原始資料負取樣後進行細緻的特徵工程
X=pd。concat([normal,fraud])
X=X。sort_values(by=‘TransactionDT’)
X。TransactionDT。plot()
ok, 然後還是一樣,我們先針對取樣後組合的資料先建一個baseline起來。
可以看到,下采樣之後,樣本數量大大下降,訓練的速度快了非常多,對於反反覆覆的特徵工程的過程來說非常的方便。
那麼就開始吧:
可以看到transactionDT的作用最大,我們對其進行衍生。
在discuss裡,transactionDT已經有很多人提出了處理的方案了,這裡可以直接使用:
train=pd。DataFrame()
train[‘time’] = pd。to_datetime(X[‘TransactionDT’]+1546272000, unit=‘s’)#2019。01。01 0:0:0偏移
train[‘year’] = train[“time”]。dt。year
train[‘month’] = train[“time”]。dt。month
train[‘day’] = train[“time”]。dt。day
train[‘hour’] = train[“time”]。dt。hour
train[‘minute’] = train[“time”]。dt。minute
train[‘weekday’] = train[“time”]。dt。dayofweek
train。pop(‘time’)
train。pop(‘year’)
train。pop(‘month’)
不用擔心月份是否不對,重要的是日期的週期性。
不過這裡有個問題就是year和month都沒法用,因為統計得到的year 和 month的值很少,而且訓練集和測試集的這兩個特徵的取值完全不一樣,屬於兩個範圍,所以這裡year 和 month就直接去除了。然後我們需要注意的一個問題就是,
這裡明顯是一個過擬合的情況,經過調參發現提升不是很大,那麼問題應該就是出在特徵偏移的問題上,很明顯,transactiondt的重要性最高,但是他整體的分佈是非常詭異的
顯然,這樣的特徵在切分的時候妥妥的特徵偏移了。而且很難透過特徵工程的方式去處理,因為完全就是趨勢性的特徵,這裡我們可以看看每次切分的時候這個特徵的偏移程度是怎麼樣的,我本來想使用對抗性訓練的方法來做,但是發現其實用psi來衡量特徵的穩定性足夠了,而且psi又有明確的評價標準,相當nice。
可以看到,TransactionDT的Psi非常的高,按照PSI一般的經驗範圍來說:
衍生出特徵之後,帶入原始的資料中,刪掉transactionDT這個偏移太大的特徵,用它的偏移不太大的特徵來代替。
提升了2個點!
感動的一批,此時我們實際上進行了“一階衍生”,也就是在原始資料的層面上衍生出了新的特徵,如果在新的特徵上再衍生特徵則是“二階衍生”。。。。以此類推,為了避免混論,這裡我們先統一進行“一階衍生”。
根據上面的特徵重要性圖,我們再對TransactionAmt進行特徵工程的處理。
這裡我們直接使用大佬開發的toad包,這個包是專門用於開發評分卡的神庫,安利一下,非常好用,並且做了很多cython方面的效能最佳化,太難得了,常見的分箱、ks、psi的計算等等都集成了,非常的方便!!!
cats是類別特徵
from
sklearn。model_selection
import
StratifiedKFold
import
lightgbm
as
lgb
NFOLDS
=
5
folds
=
StratifiedKFold
(
n_splits
=
NFOLDS
)
columns
=
X
。
drop
(
cats
,
axis
=
1
)
。
columns
splits
=
folds
。
split
(
X
,
y
)
PSI
=
pd
。
DataFrame
()
PSI
[
‘feature’
]
=
columns
for
fold_n
,
(
train_index
,
valid_index
)
in
enumerate
(
splits
):
X_train
,
X_valid
=
X
。
iloc
[
train_index
],
X
。
iloc
[
valid_index
]
y_train
,
y_valid
=
y
。
iloc
[
train_index
],
y
。
iloc
[
valid_index
]
PSI
[
f
‘fold_{fold_n + 1}’
]
=
toad
。
metrics
。
PSI
(
X_train
。
drop
(
cats
,
axis
=
1
),
X_valid
。
drop
(
cats
,
axis
=
1
))
。
values
del
X_train
,
X_valid
,
y_train
,
y_valid
gc
。
collect
()
PSI
這裡我們直接用交叉驗證計算全部的psi,可以看到TransactionAMT的psi
每一次交叉驗證都要超過0。25,可以看到這個特徵的分佈確實是非常的不規則,這就導致了每次交叉驗證,訓練集和測試集的TransactionAmt的分佈大不相同,這就導致了嚴重的過擬合,我們要想辦法讓特徵的分佈變得規則起來。
1、分箱
2、代數變換。
這裡我們先進行log變換。
這是原始的分佈。
這是進行log變換的分佈。
TransactionAmt=X。TransactionAmt
X。TransactionAmt=np。log1p(X。TransactionAmt)
然後我們再計算一次PSI。
可以看到log變換之後psi下降了一些,但是還是很高。。。這裡我們使用toad包來做一些常規的特徵分箱嘗試一下。
我們先計算一下原始特徵的iv和psi:
print(toad。quality(X[[‘TransactionAmt’,‘y’]],‘y’)。iv。values[0])
print(PSI_cal(5,X,y,cats))
我們先使用常規的cart tree的分箱來嘗試,分箱的數量做為超引數,然後使用IV值來衡量分箱後的結果,這裡我們把交叉的psi的計算也封裝一下:
def
PSI_cal
(
NFOLDS
,
X
,
y
,
cats
):
NFOLDS
=
NFOLDS
folds
=
StratifiedKFold
(
n_splits
=
NFOLDS
)
columns
=
X
。
drop
(
cats
,
axis
=
1
)
。
columns
splits
=
folds
。
split
(
X
,
y
)
PSI
=
pd
。
DataFrame
()
PSI
[
‘feature’
]
=
columns
for
fold_n
,
(
train_index
,
valid_index
)
in
enumerate
(
splits
):
X_train
,
X_valid
=
X
。
iloc
[
train_index
],
X
。
iloc
[
valid_index
]
y_train
,
y_valid
=
y
。
iloc
[
train_index
],
y
。
iloc
[
valid_index
]
#PSI[f‘fold_{fold_n + 1}’] = toad。metrics。PSI(X_train。drop(cats,axis=1),X_valid。drop(cats,axis=1))。values
PSI
[
f
‘fold_{fold_n + 1}’
]
=
toad
。
metrics
。
PSI
(
X_train
。
TransactionAmt
,
X_valid
。
TransactionAmt
)
del
X_train
,
X_valid
,
y_train
,
y_valid
gc
。
collect
()
return
PSI
。
drop
(
‘feature’
,
axis
=
1
)
。
iloc
[
0
]
。
mean
()
iv
=
[]
PSIs
=
[]
for
bins
in
[
5
,
10
,
15
,
20
,
25
,
30
,
35
,
40
,
45
,
50
]:
bins
=
toad
。
DTMerge
(
TransactionAmt
,
y
,
n_bins
=
bins
)
。
tolist
()
bins
。
insert
(
0
,
-
np
。
inf
)
bins
。
append
(
np
。
inf
)
X
。
TransactionAmt
=
np
。
digitize
(
TransactionAmt
,
bins
)
iv
。
append
(
toad
。
quality
(
X
[[
‘TransactionAmt’
,
‘y’
]],
‘y’
)
。
iv
。
values
[
0
])
PSIs
。
append
(
PSI_cal
(
5
,
X
,
y
,
cats
))
我們看到,當bins=10的時候,iv值最大,並且psi也相對較小,顯然我們這個時候應該取bins=10為最佳。
當然這只是一種近似的bins的求法,實際上最合適的還是bins分箱完之後放回原始資料中跑交叉驗證比較,但是那樣時間上消耗太多。
然後我們再比較一下其它的分箱方法:
chimerge
iv=[]
PSIs=[]
for bins in [5,10,15,20,25,30,35,40,45,50]:
bins=toad。ChiMerge(TransactionAmt,y,n_bins=bins)。tolist() # DTmerge,ChiMerge,KMeansMerge,QuantileMerge
bins。insert(0,-np。inf)
bins。append(np。inf)
X。TransactionAmt=np。digitize(TransactionAmt,bins)
iv。append(toad。quality(X[[‘TransactionAmt’,‘y’]],‘y’)。iv。values[0])
PSIs。append(PSI_cal(5,X,y,cats))
可以看到使用卡方分箱的最佳結果要比決策樹好一點,最佳分箱的數量也是10。
kmeans merge
iv=[]
PSIs=[]
for bins in [5,10,15,20,25,30,35,40,45,50]:
bins=toad。KMeansMerge(TransactionAmt,y,n_bins=bins)。tolist() # DTmerge,ChiMerge,KMeansMerge,QuantileMerge
bins。insert(0,-np。inf)
bins。append(np。inf)
X。TransactionAmt=np。digitize(TransactionAmt,bins)
iv。append(toad。quality(X[[‘TransactionAmt’,‘y’]],‘y’)。iv。values[0])
PSIs。append(PSI_cal(5,X,y,cats))
可以看到,聚類分箱的方法相對於有監督分箱的結果要差很多,但是穩定性確實很高,並且聚類的蔟越多,iv值越高,這個後續有空考慮放進去試試
quantile 等寬merge
iv=[]
PSIs=[]
for bins in [5,10,15,20,25,30,35,40,45,50]:
bins=toadQuantileMerge(TransactionAmt,y,n_bins=bins)。tolist() # DTmerge,ChiMerge,KMeansMerge,QuantileMerge
bins。insert(0,-np。inf)
bins。append(np。inf)
X。TransactionAmt=np。digitize(TransactionAmt,bins)
iv。append(toad。quality(X[[‘TransactionAmt’,‘y’]],‘y’)。iv。values[0])
PSIs。append(PSI_cal(5,X,y,cats))
psi和iv都不如有監督分箱。
所以這裡我們決定使用卡方分箱並且分箱數為10。
bins=toad。ChiMerge(TransactionAmt,y,n_bins=10)。tolist() # DTmerge,ChiMerge,KMeansMerge,QuantileMerge
bins。insert(0,-np。inf)
bins。append(np。inf)
X。TransactionAmt=np。digitize(TransactionAmt,bins)
sns。kdeplot(X。TransactionAmt)
分箱之後的結果。。也是有點怪異啊。
可以看到,iv值基本不變的情況下,psi下降了很多!然後我們看看效果如何。
很可惜,顯然分箱之後丟失了部分資訊,cv的結果整體下降。那麼我們考慮不刪除原始的特徵,而把分箱後的特徵放進去作為新得特徵,期望抵消一部分原始特徵得影響。嘗試看看結果如何。
X[‘original_TransactionAmt’]=TransactionAmt
可以看到,每一折扣得交叉驗證的得分的均值提高了萬3,但是總的auc降低了萬1左右,這就很尷尬,不知道是否真的提升了,暫且先留著。
後來discuss裡面開源了對於TransactionAmt的另一個神奇處理,透過交易金額的小數點的位數來確定不同的國家。。。簡直無敵。。。注意這個特徵代表了國家,因此要設定為category特徵
X
[
‘TransactionAmt_decimal’
]
=
((
X
[
‘TransactionAmt’
]
-
X
[
‘TransactionAmt’
]
。
astype
(
int
))
*
1000
)
。
astype
(
int
)
我們再訓練一下看下:
微微的提高了一點。
前面處理了TransactionAMT,衍生了兩個新的特徵,現在對card1下手,因為card1是原始特徵中未進行特徵工程的特徵中最強的。
可以看到card1沒有缺失值,並且是典型的高基數類別特徵。
熟悉lgb原理的應該知道,lgb在內部對於高基數類別特徵的處理是很粗糙的,它僅僅遍歷部分顯著的類別特徵中的類別,從而導致了模型的表達能力相對可能會有所欠缺。
對於高基數類別特徵的處理方式也有很多教科書式樣的方法了,大部分常用的編碼方法都放在category_encoders這個神庫裡,原始碼很簡單易懂,這裡我們採用二分類問題經典的woe編碼,替換原始的card1然後看看結果如何。
import category_encoders
woe=category_encoders。woe。WOEEncoder()
X。card1=woe。fit_transform(card1。astype(str),y)
感人,整整提升了一個多點,card1也變成了最重要的特徵,那麼我們把原始的card1併入看看效果如何。
X[‘original_card1’]=card1。values
X。original_card1=X。original_card1。astype(‘category’)
很可惜,加入之後的效果沒有之前好了。差了大概千一左右,不過這裡也有一個隱藏的問題,我們是否真的要直接刪除card1,因為後續我們可能還需要使用card1這類重要的特徵進行各種複雜的衍生操作。
問題不大,我們可以把這些特徵暫時儲存到另一個dataframe裡
一般來說,在比賽中,比較教學式的特徵衍生方法就是對特徵重要性靠前的特徵進行特徵交叉等操作:
連續特徵之間加減乘除等;
連續和類別之間groupby等統計資訊的計算;
類別和類別之間的特徵交叉。