爲期2周的比賽,最後b榜線上AUC0.7765,排名130,不知道第一名是多少,但看羣聊,10名左右的人成績是0.7842,差一個百分點,就與大獎差之千里啊,我還是太菜了=-=。
不過,還是很開心的,以前學了一堆機器學習算法,現在這個比賽正好實戰了一下。以前知道特徵重要,現在真正體驗到特徵是多麼的重要。話不多說,直入主題了
目錄
一、賽題描述
主辦方提供了兩個數據集(訓練數據集和評分數據集),包含用戶標籤數據、過去60天的交易行爲數據、過去30天的APP行爲數據。希望參賽選手基於訓練數據集,通過有效的特徵提取,構建信用違約預測模型,並將模型應用在評分數據集上,輸出評分數據集中每個用戶的違約概率。評分指標爲AUC:
自己真是個鐵憨憨,還自己寫了個AUC的計算函數,想着反正數據量不大,就寫了個雙重循環來做,其實直接調用from sklearn.metrics import roc_auc_score就好了,算出來的結果一模一樣。
def auc(df):
# df:a dateframe,column 'preds' is predict probs,'flag' is true label
f_plus = df[df['flag']==1]['preds'] # 違約
f_minus = df[df['flag']==0]['preds']
s = 0
for x in f_plus:
for y in f_minus:
if x>y:
s += 1
elif x==y:
s += 0.5
else:
pass
s /= (len(f_plus)*len(f_minus))
return s
不過這個AUC的計算也恰恰說明其物理含義是:任意取一個正樣本和負樣本,正樣本得分大於負樣本的概率。
二、方案介紹
方案總覽:
其中建模和特徵工程存在循環,即根據模型表現在決定特徵。
2.1 EDA
1 label的分佈,在訓練集中是30970:8953,不平衡
2 數據字段類型 df.info()
3 數據字段分佈 df.describe()
3 異常值、缺失值。比如有個工作年限99年的,這絕對是異常了,這種類型的異常值不多,修正與否對模型結果影響並不大,大概是這種類型的異常比較少吧,不到10行。缺失值是有的字段存在"~","nan","\\N"這種,全被我替換爲np.nan了,在python3裏面,np.nan是float類型的,所以有缺失值的字段的數據類型就全變爲float了。
4 訓練集與測試集的分佈差異。我對比了訓練集和測試集在各個字段的分佈,發現有些字段相差甚大,所以對字段取log來儘可能的讓分佈差異不要那麼大,但並沒有帶來A榜效果的提升,線下五折效果倒是提升了。
5 訓練集和測試集label的分佈。訓練集約爲4:1,但模型預測出來的label分佈都差不多11:1了(閾值爲0.5的話),我嘗試着用過採樣或者改一下scale_pos_weight,來使得預測結果的label分佈約4:1,效果下降了=-=我斷定,評分數據集的label分佈與訓練數據集不一樣
2.2 特徵工程
三個表,每個表都構建了特徵,最後合在一起得到最終的特徵表,每一行對應於一個用戶的特徵,根據這些特徵預測這個用戶違約的概率。查閱了一些資料,畢竟風控類的比賽,貸款違約預測的比賽有些多啊=-=
前期主要就是些原始特徵,後期加了些時序特徵,其中有用的特徵大致如下,
tag表:用戶的基本信息表,字段deg_cd(學位)的缺失率達96.8%,缺失太多了,刪掉。其餘的特徵保留。
trd表:交易行爲表,主要特徵如下,其實主要就是些統計信息,主要是挖掘交易金額,交易金額與交易時間、交易類型的交互。
其中①②屬於對交易金額的挖掘,③④屬於金額與時間的交互,⑤屬於金額與交易類型的交互。
①總金額的Log值
②交易金額的小數部分,這個特徵挺有用的,也許是否是整數交易有特別的含義吧,像一個人如果買一些大額商品,比如家電等都是整數交易的,如果常去超時,那一般都是非整數交易。
data['trx_amt_decimal'] = ((data['cny_trx_amt'] - data['cny_trx_amt'].astype(int)) * 1000).astype(int)
③分別計算最近3天、7天、30天、60天的最大金額、最小金額、金額的平均值、金額的標準差、交易次數、總金額、總收入、收入次數、總支出、支出次數。其中金額的平均值和標準差的重要度挺大的。
④對交易時間做一些挖掘。分別按周幾、是否週末、小時進行分組,計算組內次數。
⑤對交易類型做一些挖掘。分別按交易方向、支付方式、收支一級分類代碼進行分組統計,統計組內交易總額和交易次數。
beh表:app行爲表,這部分數據的處理方式與trd表類似,但加上後效果沒有提升或者提高很少,所以最終沒有使用這部分數據。之所以不好,大概是由於這部分數據只涉及一小部分用戶,所以加上導致了很多數據缺失吧。
(ps. trd表特徵的加入提高了約8個百分點。)
也許有人會說,這些特徵很多都是強相關的啊,比如總額/交易次數=平均金額,沒錯,確實是這樣,所以如果採用像邏輯迴歸一類的模型的話,會因爲多重共線性而導致效果不好,需要進行精細的特徵選擇。而本文采取的是樹模型,特徵是否相關對模型的效果沒有任何影響,(雖然會影響特徵的重要度)。極端的來說,比如現在有個特徵age,我把age複製一下成爲一個新的特徵age_copy,那這兩列特徵就是完全相關的,但對樹模型而言,模型的性能絲毫不會降低或者升高,只不過在分裂的時候,有時候使用age,有時候使用age_copy而已。
2.3 構建模型
根據我對機器學習模型“精度+速度”的瞭解,以及類別特徵佔總特徵的比例,選用了lightgbm模型。emm用起來也很方便,官方文檔比那些二手博客好用且準確多了,直接上代碼,如下。
需要注意的是,
①LightGBM可以直接處理分類特徵,只要把參數categorical_feature設置下就好了,但要注意,這部分數據的數據類型必須是'category',即df[cat] = df[cat].astype('category')。
②驗證集的選取至關重要,要使得線下驗證集的評測結果和線上評測結果儘可能接近且趨勢一致,即兩者分佈類似。經過試驗,cv5比cv10的效果好,cv10會過擬合。
from sklearn.model_selection import StratifiedKFold
from sklearn.utils import shuffle
random_seed = 19950413
params = {
'num_leaves': 62,
'min_data_in_leaf': 174,
'min_child_weight': 0.06754699429052921,
'bagging_fraction': 0.6175808529241882,
'feature_fraction': 0.4149706165984789,
'learning_rate': 0.008252485997984926,
'max_depth': -1,
'reg_alpha': 0.12292704650786135,
'reg_lambda': 0.01601284792461355,
'objective': 'binary',
'seed': random_seed,
'feature_fraction_seed': random_seed,
'bagging_seed': random_seed,
'drop_seed': random_seed,
'data_random_seed': random_seed,
'boosting_type': 'gbdt',
'verbose': 1,
# 'scale_pos_weight':0.333,
# 'is_unbalance': True,
'boost_from_average': True,
'metric': 'auc'}
x = train_tag[x_col]
y = train_tag['flag']
train_model_pred = np.zeros(len(train_tag))
test_model_pred = np.zeros(len(test_tag))
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
print('*********************')
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
# cat:the list of columns name
clf = lgb.train(params, lgb_train, 2000, valid_sets = [lgb_train,lgb_eval], verbose_eval=100, early_stopping_rounds=100,
categorical_feature=cat)
clf.predict(valid[x_col],num_iteration=clf.best_iteration)
train_model_pred[test_index] = clf.predict(valid[x_col],num_iteration=clf.best_iteration)
test_model_pred += clf.predict(test_tag[x_col],num_iteration=clf.best_iteration)
train_tag['preds'] = train_model_pred
test_tag['preds'] = test_model_pred/n_cv
這裏StratifiedKFold的random_state=666的原因是,採用這個隨機種子,線下5折的模型表現和線上表現的變化趨勢是一致的。有些隨機種子,比如2022可以讓線下效果提升不少,線上反而下降。
有了模型以後,我們可以看下特徵的重要性,代碼如下
feature_importances = pd.DataFrame()
feature_importances['feature'] = x_col
feature_importances['import'] = clf.feature_importance()
plt.figure(figsize=(32, 32))
sns.barplot(data=feature_importances.sort_values(by='import', ascending=False), x='import', y='feature');
部分圖如下
可以結合這個圖,考慮刪去一些不重要的特徵,再看看模型效果。
2.4 調參
人工調參,隨機搜索,網格搜索,貝葉斯調參 都挺好用的,鑑於要調的參數有6個,每個參數的範圍又比較大,所以選用貝葉斯調參。通過調參,提高了約0.5個百分點。不過這有可能是因爲我的初始參數就很好(我拷貝了網上相似任務的lightgbm模型的參數,畢竟剛開始我不會貝葉斯調參),要是初始參數不好,那估計會提高更多百分點吧。
下面來說說貝葉斯調參的原理。
自變量是要調整的超參數X=(x1,x2,x3,……),因變量是y=F(X)是衡量這組參數好不好的評價指標。放在本文的問題裏,y就是五折交叉驗證的auc得分。
# 超參數
bounds_LGB = {
'num_leaves': (20, 500),
'min_data_in_leaf': (20, 200),
'bagging_fraction' : (0.1, 0.9),
'feature_fraction' : (0.1, 0.9),
'learning_rate': (0.001, 0.01),
'min_child_weight': (0.0001, 0.1),
'reg_alpha': (0, 2),
'reg_lambda': (0, 2),
}
# F(x)
def LGB_bayesian(
learning_rate,
num_leaves,
bagging_fraction,
feature_fraction,
min_child_weight,
min_data_in_leaf,
# max_depth,
reg_alpha,
reg_lambda,
# scale_pos_weight
):
# LightGBM expects next three parameters need to be integer.
num_leaves = int(num_leaves)
min_data_in_leaf = int(min_data_in_leaf)
# max_depth = int(max_depth)
assert type(num_leaves) == int
assert type(min_data_in_leaf) == int
# assert type(max_depth) == int
random_seed = 19950413
param = {
'num_leaves': num_leaves,
'min_data_in_leaf': min_data_in_leaf,
'min_child_weight': min_child_weight,
'bagging_fraction': bagging_fraction,
'feature_fraction': feature_fraction,
'learning_rate': learning_rate,
'max_depth': -1,
'reg_alpha': reg_alpha,
'reg_lambda': reg_lambda,
'objective': 'binary',
'seed': random_seed,
'feature_fraction_seed': random_seed,
'bagging_seed': random_seed,
'drop_seed': random_seed,
'data_random_seed': random_seed,
'boosting_type': 'gbdt',
'verbose': 1,
# 'is_unbalance': True,
# 'scale_pos_weight':scale_pos_weight,
'boost_from_average': True,
'metric': 'auc'}
x = train_tag[x_col]
y = train_tag['flag']
train_model_pred = np.zeros(len(train_tag))
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
# tmp = train[train['flag']==1]
# train = train.append(tmp,ignore_index=True).append(tmp,ignore_index=True)
# train = shuffle(train,random_state=666)
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
clf = lgb.train(param, lgb_train, 2000, valid_sets=[lgb_train, lgb_eval], verbose_eval=0,
early_stopping_rounds=100, categorical_feature=cat)
train_model_pred[test_index] = clf.predict(valid[x_col], num_iteration=clf.best_iteration)
score = roc_auc_score(train_tag['flag'], train_model_pred)
return score
貝葉斯調參基於高斯過程,它直接表示函數F的分佈,是非參數模型。(而像線性迴歸之類的模型,通過引入權重參數來避免直接對函數的分佈進行表示,屬於參數類模型),另外需要知曉的是高斯分佈的共軛分佈還是高斯分佈。下面是調參過程:
圖中所說的statistical model指的是P(y|x,Dn),這被假設爲高斯分佈。圖來自論文《Taking the Human Out of the Loop:A Review of Bayesian Optimization》
如果調包的話就很簡單了,如下
from bayes_opt import BayesianOptimization
LGB_BO = BayesianOptimization(LGB_bayesian, bounds_LGB, random_state=666)
#n_iter: How many steps of bayesian optimization you want to perform. The more #steps the more likely to find a good maximum you are.
#init_points: How many steps of random exploration you want to perform. Random #exploration can help by diversifying the exploration space.
init_points = 10
n_iter = 50
with warnings.catch_warnings():
warnings.filterwarnings('ignore')
LGB_BO.maximize(init_points=init_points, n_iter=n_iter, acq='ucb', xi=0.0, alpha=1e-6)
以下是部分訓練過程,紫色說明是目前最高的F(x)值,也就是本文的AUC。
2.5 進階
①單模之後,模型集成自然是少不了的,不過因爲我訓練的都是lightgbm,雖然超參或者特徵有些許差異,可能多樣性不夠吧,所以並沒有帶來線上的提升,線下倒是有提升emm
②嘗試了僞標籤,帶來了B榜少許的提升。就是說找了評測集中認爲預測的比較準的數據拿出來,放到每一折參與訓練,相當於在模型中引入評測集的分佈,雖然並不是真正的標籤。代碼如下
n_cv = 5
kf = StratifiedKFold(n_splits=n_cv, shuffle=True, random_state=666)
for train_index, test_index in kf.split(x, y):
print('*********************')
train, valid = train_tag.iloc[train_index], train_tag.iloc[test_index]
# psudo中存儲的是評測集的數據,其標籤是預測的,並不是真正的標籤
train = train.append(psudo[train.columns], ignore_index=True)
train[cat] = train[cat].astype('category')
lgb_train = lgb.Dataset(train[x_col], train['flag'])
lgb_eval = lgb.Dataset(valid[x_col], valid['flag'], reference=lgb_train)
clf = lgb.train(params, lgb_train, 2000, valid_sets = [lgb_train,lgb_eval], verbose_eval=100, early_stopping_rounds=100,
categorical_feature=cat)
clf.predict(valid[x_col],num_iteration=clf.best_iteration)
train_model_pred[test_index] = clf.predict(valid[x_col],num_iteration=clf.best_iteration)
test_model_pred += clf.predict(test_tag[x_col],num_iteration=clf.best_iteration)
train_tag['preds'] = train_model_pred
test_tag['preds'] = test_model_pred/n_cv
三、總結
3.1 個人參賽總結
①就比賽態度而言,我是滿意的,自從我開始做的那天起,先搭建好baseline,然後每天我都會分出一部分時間進行優化,比如學習貝葉斯調參,或者深挖特徵,或者嘗試模型集成,或者嘗試僞標籤。還有一個方法我尚未嘗試,就是用lstm來做時序特徵的embedding,然後和非時序特徵concat送入MLP進行分類;或者時序特徵經過lstm,其他特徵經過MLP,兩者得到的結果concat然後送入MLP進行分類。之所以沒有嘗試,一方面是時間問題,另一方面就是我覺得大概率不行,因爲數據量太少了。
②慶幸自己對每一個模型的結果做了詳細的記錄。其實切換B榜後,在A榜表現最好的模型並不是B榜表現最好的模型,而是之前在線下表現比較好的模型。
③就比賽結果而言,還是有點遺憾的。有些知識,比如RFM,我是清楚的,只是我業務敏感度不高,長久不接觸這些定義,我都忘記使用了,只是在單純的做一些統計特徵。沒有從業務角度出發是我的一大遺憾之處。
另外一個遺憾,就是我沒想到對A榜沒用的特徵可能會對B榜有用,這大概就是爲什麼公司有些特徵明明暫時沒發揮作用,卻依舊因爲其業務含義來保留這個特徵的原因吧。
3.2 賽後學習優秀開源代碼
招商銀行2020FinTech精英訓練營數據賽道(信用風險評分)方案分享(B榜0.78422)裏面用到了RFM的思想,嘿呀,我是知道RFM的,只是我沒有往這方面想,客戶價值、客戶的忠誠度這些本身就是與是否違約有聯繫的。所以特徵裏面可以加上最後一次購買相關的信息。另外還可以統計用戶交易行爲發生了幾天,而不是單純的統計交易次數,因爲也許這個用戶雖然交易次數很多,但都集中在某幾天啊,哦說起這個,還可以看看用戶的收支是不是存在週期性呀,如果存在週期性,且收支平衡的話是不會違約的。
2020招商銀行fintech數據賽,線上0.78026,最終53名~~菜雞分享,提出了強特增益,就lightgbm的特徵重要度來看,信用卡天數、借記卡天數、信用卡等級等一系列特徵是強特。對這些特徵進行相乘,目的使用戶之間的差距增大,強特增益。代碼如下
def tag(data):
#信用卡:持卡天數*等級
data['credit_level1']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])
data['credit_level2']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['perm_crd_lmt_cd'])
data['credit_level3']=data['cur_credit_min_opn_dt_cnt']*pd.to_numeric(data['hld_crd_card_grd_cd'])
data['level_level']=pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])*pd.to_numeric(data['perm_crd_lmt_cd']) #等級*等級
data['credit_amount']=data['cur_credit_min_opn_dt_cnt']*data['cur_credit_cnt'] #持卡天數*持卡數量
#信用卡:持卡數量*等級
data['amount_level1']=data['cur_credit_cnt']*pd.to_numeric(data['perm_crd_lmt_cd'])
data['amount_level2']=data['cur_credit_cnt']*pd.to_numeric(data['l1y_crd_card_csm_amt_dlm_cd'])
data['amount_level3']=data['cur_credit_cnt']*pd.to_numeric(data['hld_crd_card_grd_cd'])
return data
賽後學習總結:
學習到一波銀行類風控比賽的強特以及特徵工程的一些思路:
①強特,除去我自己構造的特徵外,下面這些特徵也很重要:"最後一次交易"有關的特徵;按天統計的特徵(交易了多少天,平均每天的交易金額等等);對於類別特徵,除了統計每類的次數,還可以統計佔比。
②特徵工程的思路:一方面考慮業務模型,從業務入手考慮特徵;另一方面對強特可以考慮強強聯合,對強特做一些進一步挖掘,比如說強特之間做點特徵交叉。