2021 iFLYTEK-Прогнозирование дефолта по кредиту на транспортное средство Top1 - Схема обучения

сбор данных
2021 iFLYTEK-Прогнозирование дефолта по кредиту на транспортное средство Top1 - Схема обучения

2021 iFLYTEK-Прогнозирование дефолта по кредиту на транспортное средство Top1 - Схема обучения

Введение

Цель задачи прогнозирования дефолта по автокредиту состоит в том, чтобы создать модель идентификации риска для прогнозирования заемщиков, которые могут дефолтовать. Результатом прогнозирования является вероятность дефолта заемщика, что является проблемой бинарной классификации.

частичный数据挖掘игра, главное如何基于对数据的理解抽象归纳出有用的特征.

С точки зрения босса, постарайтесь узнать и обобщить, и встаньте на плечи гигантов, может быть, вы сможете видеть дальше.

Перейдите прямо к теме и начните учить упражнения, Вуху~

разработка функций

1. Общие библиотеки и импорт данных

import pandas as pd
import numpy as np
import lightgbm as lgb
import xgboost as xgb
from sklearn.metrics import roc_auc_score, auc, roc_curve, accuracy_score, f1_score
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import StandardScaler, QuantileTransformer, KBinsDiscretizer, LabelEncoder, MinMaxScaler, PowerTransformer

from tqdm import tqdm
import pickle
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
import os

Во второй половине используются некоторые инструменты:

  • tqdm: элегантный индикатор прогресса, который удобен для наблюдения за прогрессом и скоростью выполнения номера;
  • Pickle: храните объекты на диске в виде файлов. Почти все типы данных могут быть сериализованы с помощью pickle. Как правило, сначала создается дамп, а затем загрузка, что аналогично записи и импорту; эффект заключается в том, что один результат повторяется несколько раз. , чтобы избежать повторной работы, ххх, например, на обработку данных в столбце А уходит 2 часа. После каждой модификации нужно перезапускать данные в других столбцах, но не изменяя данные в столбце А, можно использовать pickle чтобы решить эту проблему и быстро получить предыдущие результаты. ;
  • ведение журнала: журнал вывода консоли, легко проверить рабочий статус;
logging.info('data loading...')
train = pd.read_csv('../xfdata/车辆贷款违约预测数据集/train.csv')
test = pd.read_csv('../xfdata/车辆贷款违约预测数据集/test.csv')

2. Разработка функций

2.1 Конструктивные особенности

Для обучающего набора и тестового набора:

  1. Рассчитывать новые функции на основе бизнес-понимания;
  2. некоторые особенности шкалы等宽分箱(вырезать), для некоторых числовых признаков等频分箱(qcut) и некоторые числовые функции для пользовательского бинирования для разделения диапазона бинов;
def gen_new_feats(train, test):
    '''生成新特征:如年利率/分箱等特征'''
    # Step 1: 合并训练集和测试集
    data = pd.concat([train, test])

    # Step 2: 具体特征工程
    # 计算二级账户的年利率
    data['sub_Rate'] = (data['sub_account_monthly_payment'] * data['sub_account_tenure'] - data[
        'sub_account_sanction_loan']) / data['sub_account_sanction_loan']

    # 计算主账户的年利率
    data['main_Rate'] = (data['main_account_monthly_payment'] * data['main_account_tenure'] - data[
        'main_account_sanction_loan']) / data['main_account_sanction_loan']

    # 对部分特征进行分箱操作
    # 等宽分箱
    loan_to_asset_ratio_labels = [i for i in range(10)]
    data['loan_to_asset_ratio_bin'] = pd.cut(data["loan_to_asset_ratio"], 10, labels=loan_to_asset_ratio_labels)
    # 等频分箱
    data['asset_cost_bin'] = pd.qcut(data['asset_cost'], 10, labels=loan_to_asset_ratio_labels)
    # 自定义分箱
    amount_cols = [
                   'total_monthly_payment',
                   'main_account_sanction_loan',
                   'main_account_disbursed_loan',
                   'sub_account_sanction_loan',
                   'sub_account_disbursed_loan',
                   'main_account_monthly_payment',
                   'sub_account_monthly_payment',
                   'total_sanction_loan'
                ]
    amount_labels = [i for i in range(10)]
    for col in amount_cols:
        total_monthly_payment_bin = [-1, 5000, 10000, 30000, 50000, 100000, 300000, 500000, 1000000, 3000000, data[col].max()]
        data[col + '_bin'] = pd.cut(data[col], total_monthly_payment_bin, labels=amount_labels).astype(int)

    # Step 3: 返回包含新特征的训练集 & 测试集
    return data[data['loan_default'].notnull()], data[data['loan_default'].isnull()]

2.2 Кодирование — целевое кодирование

Целевое кодирование — это способ кодирования функций в сочетании с целевыми значениями.

В бинарной классификации для признака i значение кодирования целевого кодирования, когда значение признака равно k, является ожидаемым целевым значением E(y|xi=xik), соответствующим категории k.

20211208003221

Всего в наборе выборки 10 записей, из которых значение признака «Тенденция в 3 записях» равно «Вверх», и мы сосредоточимся на этих 3 записях. Когда k=Up, математическое ожидание целевого значения составляет 2/3 ≈ 0,66, поэтому закодируйте Up как 0,66.

За старшим братом в основном стоит целевое кодирование функций id.

def gen_target_encoding_feats(train, test, encode_cols, target_col, n_fold=10):
    '''生成target encoding特征'''
    # for training set - cv
    tg_feats = np.zeros((train.shape[0], len(encode_cols)))
    kfold = StratifiedKFold(n_splits=n_fold, random_state=1024, shuffle=True)
    for _, (train_index, val_index) in enumerate(kfold.split(train[encode_cols], train[target_col])):
        df_train, df_val = train.iloc[train_index], train.iloc[val_index]
        for idx, col in enumerate(encode_cols):
            target_mean_dict = df_train.groupby(col)[target_col].mean()
            df_val[f'{col}_mean_target'] = df_val[col].map(target_mean_dict)
            tg_feats[val_index, idx] = df_val[f'{col}_mean_target'].values

    for idx, encode_col in enumerate(encode_cols):
        train[f'{encode_col}_mean_target'] = tg_feats[:, idx]

    # for testing set
    for col in encode_cols:
        target_mean_dict = train.groupby(col)[target_col].mean()
        test[f'{col}_mean_target'] = test[col].map(target_mean_dict)

    return train, test

Честно говоря, я еще не до конца понял этот код~ Я буду использовать небольшой блокнот, чтобы сначала запомнить его, и доставать сразу, когда я его использую, ххх

2.3 Ближайшие признаки мошенничества

Для учетных записей управления рисками рискованные учетные записи могут иметь большое количество регистраций в одном пакете, поэтому идентификаторы могут быть связаны.

Здесь большой парень построил функцию мошенничества соседей, то есть среднее значение лейблов 10 учетных записей до и после каждой учетной записи, что также представляет вероятность, то есть вероятность агломерации учетных записей, которые могут объявить дефолт, и в определенной степени представляет корреляцию возможного дефолта учетной записи.

def gen_neighbor_feats(train, test):
    '''产生近邻欺诈特征'''
    if not os.path.exists('../user_data/neighbor_default_probs.pkl'):
        # 该特征需要跑的时间较久,因此将其存成了pkl文件
        neighbor_default_probs = []
        for i in tqdm(range(train.customer_id.max())):
            if i >= 10 and i < 199706:
                customer_id_neighbors = list(range(i - 10, i)) + list(range(i + 1, i + 10))
            elif i < 199706:
                customer_id_neighbors = list(range(0, i)) + list(range(i + 1, i + 10))
            else:
                customer_id_neighbors = list(range(i - 10, i)) + list(range(i + 1, 199706))

            customer_id_neighbors = [customer_id_neighbor for customer_id_neighbor in customer_id_neighbors if
                                     customer_id_neighbor in train.customer_id.values.tolist()]
            neighbor_default_prob = train.set_index('customer_id').loc[customer_id_neighbors].loan_default.mean()
            neighbor_default_probs.append(neighbor_default_prob)

        df_neighbor_default_prob = pd.DataFrame({'customer_id': range(0, train.customer_id.max()),
                                                 'neighbor_default_prob': neighbor_default_probs})
        save_pkl(df_neighbor_default_prob, '../user_data/neighbor_default_probs.pkl')
    else:
        df_neighbor_default_prob = load_pkl('../user_data/neighbor_default_probs.pkl')
    train = pd.merge(left=train, right=df_neighbor_default_prob, on='customer_id', how='left')
    test = pd.merge(left=test, right=df_neighbor_default_prob, on='customer_id', how='left')

    return train, test

2.4 Вывод результатов разработки признаков

TARGET_ENCODING_FETAS = [
                            'employment_type',
                             'branch_id',
                             'supplier_id',
                             'manufacturer_id',
                             'area_id',
                             'employee_code_id',
                             'asset_cost_bin'
                         ]


# 特征工程
logging.info('feature generating...')
train, test = gen_new_feats(train, test)
train, test = gen_target_encoding_feats(train, test, TARGET_ENCODING_FETAS, target_col='loan_default', n_fold=10)
train, test = gen_neighbor_feats(train, test)

Последующая обработка признаков, такая как преобразование типа данных некоторых преобразованных признаков и упрощение некоторых признаков с оценкой скорости, облегчает последующее обучение модели и повышает надежность модели.

# 保存的最终特征名称列表
SAVE_FEATS = [
                 'customer_id',
                 'neighbor_default_prob',
                 'disbursed_amount',
                 'asset_cost',
                 'branch_id',
                 'supplier_id',
                 'manufacturer_id',
                 'area_id',
                 'employee_code_id',
                 'credit_score',
                 'loan_to_asset_ratio',
                 'year_of_birth',
                 'age',
                 'sub_Rate',
                 'main_Rate',
                 'loan_to_asset_ratio_bin',
                 'asset_cost_bin',
                 'employment_type_mean_target',
                 'branch_id_mean_target',
                 'supplier_id_mean_target',
                 'manufacturer_id_mean_target',
                 'area_id_mean_target',
                 'employee_code_id_mean_target',
                 'asset_cost_bin_mean_target',
                 'credit_history',
                 'average_age',
                 'total_disbursed_loan',
                 'main_account_disbursed_loan',
                 'total_sanction_loan',
                 'main_account_sanction_loan',
                 'active_to_inactive_act_ratio',
                 'total_outstanding_loan',
                 'main_account_outstanding_loan',
                 'Credit_level',
                 'outstanding_disburse_ratio',
                 'total_account_loan_no',
                 'main_account_tenure',
                 'main_account_loan_no',
                 'main_account_monthly_payment',
                 'total_monthly_payment',
                 'main_account_active_loan_no',
                 'main_account_inactive_loan_no',
                 'sub_account_inactive_loan_no',
                 'enquirie_no',
                 'main_account_overdue_no',
                 'total_overdue_no',
                 'last_six_month_defaulted_no'
            ]


# 特征工程 后处理
# 简化特征
for col in ['sub_Rate', 'main_Rate', 'outstanding_disburse_ratio']:
     train[col] = train[col].apply(lambda x: 1 if x > 1 else x)
     test[col] = test[col].apply(lambda x: 1 if x > 1 else x)

# 数据类型转换
train['asset_cost_bin'] = train['asset_cost_bin'].astype(int)
test['asset_cost_bin'] = test['asset_cost_bin'].astype(int)
train['loan_to_asset_ratio_bin'] = train['loan_to_asset_ratio_bin'].astype(int)
test['loan_to_asset_ratio_bin'] = test['loan_to_asset_ratio_bin'].astype(int)

# 存储包含新特征的数据集
logging.info('new data saving...')
cols = SAVE_FEATS + ['loan_default', ]
train[cols].to_csv('./train_final.csv', index=False)
test[cols].to_csv('./test_final.csv', index=False)

Построение модели

1. Обучение модели — перекрестная проверка

Используя lightgbm и xgboost две модели деревьев с градиентным усилением, здесь не так много объяснений, следующие коды стали «стандартными», DDDD ~

def train_lgb_kfold(X_train, y_train, X_test, n_fold=5):
    '''train lightgbm with k-fold split'''
    gbms = []
    kfold = StratifiedKFold(n_splits=n_fold, random_state=1024, shuffle=True)
    oof_preds = np.zeros((X_train.shape[0],))
    test_preds = np.zeros((X_test.shape[0],))

    for fold, (train_index, val_index) in enumerate(kfold.split(X_train, y_train)):
        logging.info(f'############ fold {fold} ###########')
        X_tr, X_val, y_tr, y_val = X_train.iloc[train_index], X_train.iloc[val_index], y_train[train_index], y_train[val_index]
        dtrain = lgb.Dataset(X_tr, y_tr)
        dvalid = lgb.Dataset(X_val, y_val, reference=dtrain)

        params = {
            'objective': 'binary',
            'metric': 'auc',
            'num_leaves': 64,
            'learning_rate': 0.02,
            'min_data_in_leaf': 150,
            'feature_fraction': 0.8,
            'bagging_fraction': 0.7,
            'n_jobs': -1,
            'seed': 1024
        }

        gbm = lgb.train(params,
                        dtrain,
                        num_boost_round=1000,
                        valid_sets=[dtrain, dvalid],
                        verbose_eval=50,
                        early_stopping_rounds=20)

        oof_preds[val_index] = gbm.predict(X_val, num_iteration=gbm.best_iteration)
        test_preds += gbm.predict(X_test, num_iteration=gbm.best_iteration) / kfold.n_splits
        gbms.append(gbm)

    return gbms, oof_preds, test_preds



def train_xgb_kfold(X_train, y_train, X_test, n_fold=5):
    '''train xgboost with k-fold split'''
    gbms = []
    kfold = StratifiedKFold(n_splits=10, random_state=1024, shuffle=True)
    oof_preds = np.zeros((X_train.shape[0],))
    test_preds = np.zeros((X_test.shape[0],))

    for fold, (train_index, val_index) in enumerate(kfold.split(X_train, y_train)):
        logging.info(f'############ fold {fold} ###########')
        X_tr, X_val, y_tr, y_val = X_train.iloc[train_index], X_train.iloc[val_index], y_train[train_index], y_train[val_index]
        dtrain = xgb.DMatrix(X_tr, y_tr)
        dvalid = xgb.DMatrix(X_val, y_val)
        dtest = xgb.DMatrix(X_test)

        params={
            'booster':'gbtree',
            'objective': 'binary:logistic',
            'eval_metric': ['logloss', 'auc'],
            'max_depth': 8,
            'subsample':0.9,
            'min_child_weight': 10,
            'colsample_bytree':0.85,
            'lambda': 10,
            'eta': 0.02,
            'seed': 1024
        }

        watchlist = [(dtrain, 'train'), (dvalid, 'test')]

        gbm = xgb.train(params,
                        dtrain,
                        num_boost_round=1000,
                        evals=watchlist,
                        verbose_eval=50,
                        early_stopping_rounds=20)

        oof_preds[val_index] = gbm.predict(dvalid, iteration_range=(0, gbm.best_iteration))
        test_preds += gbm.predict(dtest, iteration_range=(0, gbm.best_iteration)) / kfold.n_splits
        gbms.append(gbm)

    return gbms, oof_preds, test_preds
def train_xgb(train, test, feat_cols, label_col, n_fold=10):
    '''训练xgboost'''
    for col in ['sub_Rate', 'main_Rate', 'outstanding_disburse_ratio']:
        train[col] = train[col].apply(lambda x: 1 if x > 1 else x)
        test[col] = test[col].apply(lambda x: 1 if x > 1 else x)

    X_train = train[feat_cols]
    y_train = train[label_col]
    X_test = test[feat_cols]
    gbms_xgb, oof_preds_xgb, test_preds_xgb = train_xgb_kfold(X_train, y_train, X_test, n_fold=n_fold)

    if not os.path.exists('../user_data/gbms_xgb.pkl'):
        save_pkl(gbms_xgb, '../user_data/gbms_xgb.pkl')

    return gbms_xgb, oof_preds_xgb, test_preds_xgb


def train_lgb(train, test, feat_cols, label_col, n_fold=10):
    '''训练lightgbm'''
    X_train = train[feat_cols]
    y_train = train[label_col]
    X_test = test[feat_cols]
    gbms_lgb, oof_preds_lgb, test_preds_lgb = train_lgb_kfold(X_train, y_train, X_test, n_fold=n_fold)

    if not os.path.exists('../user_data/gbms_lgb.pkl'):
        save_pkl(gbms_lgb, '../user_data/gbms_lgb.pkl')

    return gbms_lgb, oof_preds_lgb, test_preds_lgb

Результаты обучения модели вывода:

# 读取原始数据集
logging.info('data loading...')
train = pd.read_csv('../xfdata/车辆贷款违约预测数据集/train.csv')
test = pd.read_csv('../xfdata/车辆贷款违约预测数据集/test.csv')

# 特征工程
logging.info('feature generating...')
train, test = gen_new_feats(train, test)
train, test = gen_target_encoding_feats(train, test, TARGET_ENCODING_FETAS, target_col='loan_default', n_fold=10)
train, test = gen_neighbor_feats(train, test)

train['asset_cost_bin'] = train['asset_cost_bin'].astype(int)
test['asset_cost_bin'] = test['asset_cost_bin'].astype(int)
train['loan_to_asset_ratio_bin'] = train['loan_to_asset_ratio_bin'].astype(int)
test['loan_to_asset_ratio_bin'] = test['loan_to_asset_ratio_bin'].astype(int)
train['asset_cost_bin_mean_target'] = train['asset_cost_bin_mean_target'].astype(float)
test['asset_cost_bin_mean_target'] = test['asset_cost_bin_mean_target'].astype(float)

# 模型训练:linux和mac的xgboost结果会有些许不同,以模型文件结果为主
gbms_xgb, oof_preds_xgb, test_preds_xgb = train_xgb(train.copy(), test.copy(),
                                                    feat_cols=SAVE_FEATS,
                                                    label_col='loan_default')
gbms_lgb, oof_preds_lgb, test_preds_lgb = train_lgb(train, test,
                                                    feat_cols=SAVE_FEATS,
                                                    label_col='loan_default')

2. Порог разделения

потому что0-1二分类, то среднее итоговой классификации можно приблизительно понимать как вероятность взятия кредита_по умолчанию=1. Затем, путем сортировки результатов прогнозирования cv, вероятность, соответствующая квантилю (1-P(loan_default=1)) принимается в качестве критической точки прогнозирования разделения положительных и отрицательных выборок.

Чтобы сделать результаты более точными, предпринимаются небольшие шаги для обхода точек вблизи критической точки, чтобы найти локально оптимальный порог вероятности.

def gen_thres_new(df_train, oof_preds):
    df_train['oof_preds'] = oof_preds
    # 可看作训练集取到loan_default=1的概率
    quantile_point = df_train['loan_default'].mean() 
    thres = df_train['oof_preds'].quantile(1 - quantile_point) 
    # 比如 0,1,1,1 mean=0.75 1-mean=0.25,也就是25%分位数取值为0

    _thresh = []
     #  按照理论阈值的上下0.2范围,0.01步长,找到最佳阈值,f1分数最高对应的阈值即为最佳阈值
    for thres_item in np.arange(thres - 0.2, thres + 0.2, 0.01):
        _thresh.append(
            [thres_item, f1_score(df_train['loan_default'], np.where(oof_preds > thres_item, 1, 0), average='macro')])

    _thresh = np.array(_thresh)
    best_id = _thresh[:, 1].argmax() # 找到f1最高对应的行
    best_thresh = _thresh[best_id][0] # 取出最佳阈值

    print("阈值: {}\n训练集的f1: {}".format(best_thresh, _thresh[best_id][1]))
    return best_thresh

3. Слияние моделей

Выполните квантили результатов модели cv xgb и lgb加权求和, а затем найти порог вероятности объединенной модели 0-1.

xgb_thres = gen_thres_new(train, oof_preds_xgb)
lgb_thres =  gen_thres_new(train, oof_preds_lgb)


# 结果聚合
df_oof_res = pd.DataFrame({'customer_id': train['customer_id'],
                            'loan_default':train['loan_default'],
                            'oof_preds_xgb': oof_preds_xgb,
                            'oof_preds_lgb': oof_preds_lgb})

# 模型融合
df_oof_res['xgb_rank'] = df_oof_res['oof_preds_xgb'].rank(pct=True) # percentile rank,返回的是排序后的分位数
df_oof_res['lgb_rank'] = df_oof_res['oof_preds_lgb'].rank(pct=True)

df_oof_res['preds'] = 0.31 * df_oof_res['xgb_rank'] + 0.69 * df_oof_res['lgb_rank']

# 融合后的模型,概率阈值
thres = gen_thres_new(df_oof_res, df_oof_res['preds'])

предсказывать

В соответствии с порогом вероятности обучающего набора после слияния моделей результаты прогнозирования тестового набора делятся на 0-1, и выводится окончательный результат представления прогноза.

def gen_submit_file(df_test, test_preds, thres, save_path):
    # 按最终模型融合后的阈值进行划分
    df_test['test_preds_binary'] = np.where(test_preds > thres, 1, 0)  
    df_test_submit = df_test[['customer_id', 'test_preds_binary']]
    df_test_submit.columns = ['customer_id', 'loan_default']
    print(f'saving result to: {save_path}')
    df_test_submit.to_csv(save_path, index=False)
    print('done!')
    return df_test_submit



df_test_res = pd.DataFrame({'customer_id': test['customer_id'],
                                'test_preds_xgb': test_preds_xgb,
                                'test_preds_lgb': test_preds_lgb})

df_test_res['xgb_rank'] = df_test_res['test_preds_xgb'].rank(pct=True)
df_test_res['lgb_rank'] = df_test_res['test_preds_lgb'].rank(pct=True)
df_test_res['preds'] = 0.31 * df_test_res['xgb_rank'] + 0.69 * df_test_res['lgb_rank']

# 结果产出
df_submit = gen_submit_file(df_test_res, df_test_res['preds'], thres,
                            save_path='../prediction_result/result.csv')

Суммировать

Стиль кода большого парня ясен и лаконичен. Код очень гладкий, а мышление очень ясное. Вы можете хорошо изучить эти инженерные коды, которые легко масштабируются и легко отлаживают.

С точки зрения вопроса о конкуренции, после размышлений о бизнесе, из концентрации идентификаторов создается «функция соседского мошенничества»; в операции слияния моделей она взвешивается в соответствии с квантилем ранжирования значения прогнозируемого значения. . Эти советы можно использовать повторно~ (также главный пункт, упомянутый большим парнем)

Следующие два вопроса, по оценкам, у многих студентов, как и у меня, будут некоторые сомнения, поэтому я сделаю снимок экрана прямо из b:

20211208161333

Исходный код:GitHub.com/WangLiLin/Литтл…

Кроме того, я также организовал ipynb для облегчения обучения, и студенты, которым это нужно, ответят на «1208» в фоновом режиме публичного аккаунта, чтобы получить его.


Ссылаться на:

  1. модуль регистрации
  2. модуль рассола
  3. модуль tqdm
  4. Формула целевого кодирования
  5. Target Encoding
  6. zhuanlan.zhihu.com/p/412337232

Приглашаем обратить внимание на личный публичный номер:Distinct数说