kaggle支付反欺诈:IEEE-CIS Fraud Detection第一名方案复现过程(有代码)

写在前面:

IEEE-CIS Fraud Detection比赛参加人数有6000多人,有很多国内和国外的竞赛大神都参与其中,算是含金量比较高的比赛了。楼主前期基本上徘徊在100名左右,但到后期由于工作上的事情,后来基本上也没时间搞了(主要也是能力有限,哈哈)。因为自己也是做反欺诈这块的工作,所以希望能够通过这次比赛能够学到一些能在工作中使用的小trick。

本文主要是针对第一名的方案进行复现(后面也会写前排其他大佬的方案,待更新。。。),包括在复现过程中的一些问题和思考,希望能给大家带来一些启发。当然,本人能力有限,有说的不对的地方,希望大家能够评论指证。

相关链接,参考:

(1)第1名方案:

part1: 1st Place Solution - Part 1

part2: 1st Place Solution - Part 2

Very short summary | Kaggle

比赛数据:链接:pan.baidu.com/s/1e3KceZ 密码:yigf

复现代码:github.com/DJofOUC/IEEE

赛题简析:

该赛题的主要目标是识别出每笔交易是否是欺诈的。其中训练集样本约59万(欺诈占3.5%),测试集样本约50万。数据主要分为2类,交易数据transaction和identity数据,数据字段的有关说明见:Data Description (Details and Discussion)

具体的样本量和特征维度如下图:

样本量和特征维度

从上图可以看出,总共的特征维度有393 + 41 = 434维,很明显,我们要做数据降维。

如何定位支付欺诈?支付欺诈背后真正的业务意义是什么?

(我的理解)打标签的逻辑:将该次交易之后用户提出了退款的交易定义为欺诈(一般是受骗之后,用户发现了,然后向银行提出取消交易或退款),当然正常的退款除外,如退货之类的。并且若其他的交易与欺诈交易相关的用户的账户,email和账单地址有直接联系,也定义为欺诈(isFraud=1)。若该次交易发生120天后还没有被定义为欺诈,那么该次交易就是正常交易(isFraud=0)。

理解了打标签的逻辑,在分析欺诈的特征的时候,就会发现,有73838位用户是有2次或2次以上的交易,其中71575位 (96.9%)是没有欺诈标签的(isFraud=0),2134位 (2.9%)是全部都是欺诈标签(isFraud=1),只有129 位(0.2%)是既有欺诈标签也有非欺诈标签的。从中,我们可以获得在业务中欺诈的逻辑,一个用户有过欺诈经历,那么他下次欺诈的概率还是非常高的,我们需要关注到这一点。

如何找到用户的唯一标识?

在原始数据中是没有uid这类的字段的,那么如何找到用户的唯一标识呢?这怎么能难到大神们,经过分析(也不知道怎么分析的),card1(银行卡的前多少位),addr1D1 这三个字段合并起来可以唯一识别一位用户。D1表示截止到目前为止的用户开卡天数。实际上就是通过银行卡的前多少位,用户地址(类似区号代码之类的)和该用户银行卡的使用天数,我们就可以绝大概率对应着某位用户。如下所示:

card1+addr1+D1n可以唯一标识一位用户

D1n = TransactionDT/ (60*60*24) - D1,表示开卡日期。

用户唯一标识如何使用?

确定了用户的唯一标识之后,我们并不能直接把它当作一个特征直接加入到模型中去,因为通过分析发现,测试集中有68.2%的用户是新用户,并不在训练集中。我们需要间接的使用uid,用uid构造一些聚合特征。

Main magic:

  • client identification (uid) using card/D/C/V columns (we found almost all 600 000 unique cards and respective clients)用户的唯一标识(十分重要)
  • uid (unique client ID) generalization by agg to remove train set overfitting for known clients and cards使用uid构建聚合特征
  • categorical features / supportive features for models类别特征的编码(主要是用频率编码和label encode)
  • horizontal blend by model / vertical blend by client post-process(水平方向:模型融合;垂直方向:针对用户的后处理)

特征筛选策略:

  • Train 2 month / skip 2 / predict 2
  • Train 4 / skip 1 / predict 1

构建一些特征之后,加入到原有特征中去,使用以上方式来验证该特征是否有效。

特征编码:

主要使用以下五种特征编码方式

# encoding function
# frequency encode频率编码
def encode_FE(df1, df2, cols):
    for col in cols:
        df = pd.concat([df1[col], df2[col]])
        vc = df.value_counts(dropna=True, normalize=True).to_dict()
        vc[-1] = -1
        nm = col + "FE"
        df1[nm] = df1[col].map(vc)
        df1[nm] = df1[nm].astype("float32")
        df2[nm] = df2[col].map(vc)
        df2[nm] = df2[nm].astype("float32")
        print(col)

# label encode
def encode_LE(col, train=X_train, test=X_test, verbose=True):
    df_comb = pd.concat([train[col], test[col]], axis=0)
    df_comb, _ = pd.factorize(df_comb[col])
    nm = col
    if df_comb.max() > 32000:
        train[nm] = df_comb[0: len(train)].astype("float32")
        test[nm] = df_comb[len(train):].astype("float32")
    else:
        train[nm] = df_comb[0: len(train)].astype("float16")
        test[nm] = df_comb[len(train):].astype("float16")
    del df_comb
    gc.collect()
    if verbose:
        print(col)

def encode_AG(main_columns, uids, aggregations=["mean"], df_train=X_train, df_test=X_test, fillna=True, usena=False):
    for main_column in main_columns:
        for col in uids:
            for agg_type in aggregations:
                new_column = main_column + "_" + col + "_" + agg_type
                temp_df = pd.concat([df_train[[col, main_column]], df_test[[col, main_column]]])
                if usena:
                    temp_df.loc[temp_df[main_column] == -1, main_column] = np.nan
                #求每个uid下,该col的均值或标准差
                temp_df = temp_df.groupby([col])[main_column].agg([agg_type]).reset_index().rename(
                    columns={agg_type: new_column})
                #将uid设成index
                temp_df.index = list(temp_df[col])
                temp_df = temp_df[new_column].to_dict()
                #temp_df是一个映射字典
                df_train[new_column] = df_train[col].map(temp_df).astype("float32")
                df_test[new_column] = df_test[col].map(temp_df).astype("float32")
                if fillna:
                    df_train[new_column].fillna(-1, inplace=True)
                    df_test[new_column].fillna(-1, inplace=True)
                print(new_column)

# COMBINE FEATURES交叉特征
def encode_CB(col1, col2, df1=X_train, df2=X_test):
    nm = col1 + '_' + col2
    df1[nm] = df1[col1].astype(str) + '_' + df1[col2].astype(str)
    df2[nm] = df2[col1].astype(str) + '_' + df2[col2].astype(str)
    encode_LE(nm, verbose=False)
    print(nm, ', ', end='')

# GROUP AGGREGATION NUNIQUE
def encode_AG2(main_columns, uids, train_df=X_train, test_df=X_test):
    for main_column in main_columns:
        for col in uids:
            comb = pd.concat([train_df[[col] + [main_column]], test_df[[col] + [main_column]]], axis=0)
            mp = comb.groupby(col)[main_column].agg(['nunique'])['nunique'].to_dict()
            train_df[col + '_' + main_column + '_ct'] = train_df[col].map(mp).astype('float32')
            test_df[col + '_' + main_column + '_ct'] = test_df[col].map(mp).astype('float32')
            print(col + '_' + main_column + '_ct, ', end='')

聚合特征的构建:见以上代码

模型融合:

三个经典的GBDT模型

  • Catboost (0.963915 public / 0.940826 private)
  • LGBM (0.961748 / 0.938359)
  • XGB (0.960205 / 0.932369)

目前复现的结果还只是单XGB的结果,提交了late submission得分如下:

编辑于 2019-10-10 17:46