天天看點

連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)

文章目錄

  • 1.變量分箱對模型的好處
  • 2.分箱的局限
  • 3.變量分箱要注意的問題
  • 4.變量分箱的流程
  • 5.卡方分箱
  • 6.KS分箱
  • 7.混淆矩陣概念複習
  • 8.最優IV分箱
  • 9.基于樹的最優分箱方法
  • 10.分箱架構源碼(卡方、最優IV、資訊增益)

變量分箱主要是對連續變量離散化

對特征的一個優化過程

變量分箱(特征分箱)是一種特征工程方法,意在增強變量的可解釋性與預測能力。變量分箱方法主要用于連續變量,對于變量取值較稀疏的離散變量也應該進行分箱處理。

比如借款人的位址資訊往往非常稀疏,通常先對位址資訊處理到省或市,用每個省或市的壞樣本比率進行數值化,将數值化後的變量作為連續變量進行分箱.

1.變量分箱對模型的好處

  • 1.降低異常值的影響,增加模型的穩定性
    • 通過分箱來降低噪聲,使模型魯棒性更好
  • 2.缺失值作為特殊變量參與分箱,減少缺失值填補的不确定性(分箱還可以解決缺失值 )
    • 通常的做法是,離散特征将缺失值轉為字元串作為特殊字元即可,
    • 而連續特征将缺失值作為特殊值即可
    • 在後面的代碼中連續值填充-777,離散值填充NA
  • 3.增加變量的可解釋性
    • 分箱的方法往往要配合變量編碼使用,這就大大提高了變量的可解釋性
  • 4.增加變量的非線性
    • 提高了模型的拟合能力
  • 5.增加模型的預測效果
    • 通常假設訓練集和測試集滿足同分布,分箱使連續變量離散化,更容易滿足同分布的假設
    • 即減少模型在訓練集的表現和測試集的偏差

2.分箱的局限

  • 1.同一箱内的樣本具有同質性
    分箱的基本假設是分在一個箱内的樣本(借款人)具有相同
        的風險等級,比如按年齡分箱的結果為{[18,25],[25,40],
        [40,55],[55,100]},也就是将年齡在 18~25 的借款人統一按照
        同一個數值變量來代替。對于樹模型就減少了模型選擇最優切分
        點的可選擇範圍,會對模型的預測能力産生影響,損失了模型的
        分辨能力
               
  • 2.需要專家經驗支援

3.變量分箱要注意的問題

分箱分的不好的話有些值的預測能力會忽略 會影響模型的預測能力 削弱模型的預測能力
  • 1.分箱結果不宜過多
    • 分箱過多導緻特征過于稀疏,編碼後的特征次元快速增加,使特征更加稀疏,會降低模型的預測效果
    • 極端例子 一共100個樣本 分了100個箱子 嚴重失衡
  • 2.分箱結果不宜過少
    • 每個箱子預設是同質的即風險等級相同
    • 如果分箱過少則可能會造成模型辨識度過低
    • 例如,年齡分箱結果為{[18,50],[50,100]},認為18~50歲的借款人風險水準相同這是不符合業務解釋的。
  • 3.分箱後單調性的要求

4.變量分箱的流程

變量分箱的目的是增加變量的預測能力或減少變量的自身備援。

當預測能力不再提升或備援性不再降低時,則分箱完畢。

是以,分箱過程是一個優化過程,所有滿足上述要求的名額都可以用于變量分箱,這個名額也可叫作目标函數,可以終止或改變分箱的限制就是優化過程的限制條件。

連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)
連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)

5.卡方分箱

  • 基本思想:

自底向上的分箱方法,相鄰區間合并計算卡方值,卡方值越小說明兩個區間的類分布越相似,合并兩個區間

由設定的門檻值決定(自由度、置信度),小于門檻值就分箱

自底向上:由多至少逐層合并的過程
自頂向下是由少至多逐層切分的過程
數值特征離散化

特征之間強相關不好,但是某個特征和标簽相關是好的

強相關:一個特征可以用另一個特征線性表示
           
  • 解釋性強
  • 能解決多分類場景的分箱
  • 缺點是計算量大
    • 需要先對數值型變量離散化,然後疊代的計算卡方值

公式:

連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)
連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)

上述過程就是一個卡方檢驗的過程,是以,根據置信度和自由度可以計算出卡方檢驗的門檻值,當計算的卡方值小于門檻值,則認為相鄰區間的類分布情況相似,可進行合并。其中自由度為類别個數減 1,即本例中的自由度為 1;置信度可以使用 0.9、0.95和 0.99。

6.KS分箱

Best-KS 分箱方法是一種自頂向下的分箱方法。與卡方分箱相比,Best-KS分箱方法隻是目标函數采用了 KS 統計量,其餘分箱步驟沒有差别

注意KS隻能處理連續變量

可以用于模型對好壞樣本的區分能力

  • 基本思想:

根據KS曲線,取TPR和FPR之間的最大內插補點,就是KS統計率,也就是KS分箱最優切分點的位置

  • KS曲線
    • 橫軸就是認為設定的門檻值,就是區分好壞樣本的界限
    • 縱軸:一個是真正率TPR,一個是假正率FPR
    • 之間的內插補點一定程度反映模型對好壞樣本的區分能力
    • 我們希望真正率高一點,假正率低一點(好樣本多一點,壞樣本少一點)
    • 真正率:正樣本預測數 / 正樣本實際數
      • TP /(TP + FN)
    • 假正率:被預測為正的負樣本結果數 / 負樣本實際數
      • FP /(FP + TN)

KS分箱過程也就是遞歸的找最優切分點的過程

KS值越大 模型的區分能力越強
連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)

7.混淆矩陣概念複習

連續變量分箱1.變量分箱對模型的好處2.分箱的局限3.變量分箱要注意的問題4.變量分箱的流程5.卡方分箱6.KS分箱7.混淆矩陣概念複習8.最優IV分箱9.基于樹的最優分箱方法10.分箱架構源碼(卡方、最優IV、資訊增益)
  • 召回率,真正率(recall):TP/(TP+FN)
  • 準确率(accuracy):(TP+TN) / (TP+TN+FP+FN)
    • 預測正确的 / 總樣本數
  • 精确率(precision):TP / (TP+FP)
    • 預測為1且正确 / 所有預測為1的樣本數

8.最優IV分箱

最優 IV 分箱方法也是自頂向下的分箱方式,其目标函數為 IV 值

IV 值其本質是對稱化的 K-L 距離,即在切分點處分裂得到的兩部分資料中,選擇好壞樣本的分布差異最大點作為最優切分點。分箱結束後,計算每個箱内的 IV 值加和得到變量的 IV 值,可以用來刻畫變量對目标值的預測能力。即變量的 IV 值越大,則對目标變量的區分能力越強,是以,IV 值還可以用來做變量選擇。

9.基于樹的最優分箱方法

基于樹的分箱方法借鑒了決策樹在樹生成的過程中特征選擇(最優分裂點)的目标函數來完成變量分箱過程,可以了解為單變量的決策樹模型。決策樹采用自頂向下遞歸的方法進行樹的生成,每個節點的選擇目标是為了分類結果的純度更高,也就是樣本的分類效果更好。是以,不同的損失函數有不同的決策樹,ID3采用資訊增益方法,C4.5 采用資訊增益比,CART 采用基尼系數(Gini)名額

10.分箱架構源碼(卡方、最優IV、資訊增益)

# -*- coding: utf-8 -*-
import os
import pandas as pd
import numpy as np
import pickle
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import warnings

warnings.filterwarnings("ignore")  ##忽略警告


# 注意sklearn版本要在v.20.0以上,不同版本函數的位置會不同。
def data_read(data_path, file_name):
    df = pd.read_csv(os.path.join(data_path, file_name), delim_whitespace=True, header=None, engine='python')
    # 變量重命名
    columns = ['status_account', 'duration', 'credit_history', 'purpose', 'amount',
               'svaing_account', 'present_emp', 'income_rate', 'personal_status',
               'other_debtors', 'residence_info', 'property', 'age',
               'inst_plans', 'housing', 'num_credits',
               'job', 'dependents', 'telephone', 'foreign_worker', 'target']
    df.columns = columns

    # 将标簽變量由狀态1,2轉為0,1;0表示好使用者,1表示壞使用者
    df.target = df.target - 1

    # 資料分為data_train和 data_test兩部分,訓練集用于得到編碼函數,驗證集用已知的編碼規則對驗證集編碼
    # x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    # stratify: 依據标簽y,按原資料y中各類比例,配置設定給train和test,使得train和test中各類資料的比例與原資料集一樣
    data_train, data_test = train_test_split(df, test_size=0.2, random_state=0, stratify=df.target)
    return data_train, data_test


# one—hot編碼
# df: 資料框  data_path_1:編碼模型儲存的位置  flag:資料集
def onehot_encode(df, data_path_1, flag='train'):
    # reset_index:重置索引, drop=True:不想保留原來的index
    df = df.reset_index(drop=True)

    # 判斷資料集是否存在缺失值  如果是進行缺失值填補
    # df.isnull().any() 判斷哪些列存在缺失值
    if sum(df.isnull().any()) > 0:
        # 數值型和字元串型特征拿出來
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        var_numerics = df.select_dtypes(include=numerics).columns
        var_str = [i for i in df.columns if i not in var_numerics]

        # 資料類型的缺失值用-77777填補
        if len(var_numerics) > 0:
            df.loc[:, var_numerics] = df[var_numerics].fillna(-7777)

        # 字元串類型的缺失值用NA填補
        if len(var_str) > 0:
            df.loc[:, var_str] = df[var_str].fillna('NA')

    # pickle.dump(obj, file, [,protocol])  序列化對象,将對象obj儲存到檔案file中去
    # 參數protocol是序列化模式,預設是0(ASCII協定,表示以文本的形式進行序列化)
    if flag == 'train':
        enc = OneHotEncoder(dtype='int').fit(df)
        # 儲存編碼模型
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'wb') as save_model:
            pickle.dump(enc, save_model, 0)

        df_return = pd.DataFrame(enc.transform(df).toarray())
        df_return.columns = enc.get_feature_names(df.columns)

    elif flag == 'test':
        # 測試資料編碼
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') as read_model:
            onehot_model = pickle.load(read_model)

        # 如果訓練集無缺失值,測試集有缺失值則将該樣本删除
        var_range = onehot_model.categories_  # 訓練集one-hot編碼後的類别種類
        var_name = df.columns
        del_index = []
        for i in range(len(var_range)):
            if 'NA' not in var_range[i] and 'NA' in df[var_name[i]].unique():
                index = np.where(df[var_name[i]] == 'NA')
                del_index.append(index)
            elif -7777 not in var_range[i] and -7777 in df[var_name[i]].unique():
                index = np.where(df[var_name[i]] == -7777)
                del_index.append(index)

        # 删除樣本
        if len(del_index) > 0:
            del_index = np.unique(del_index)
            df = df.drop(del_index)
            print('訓練集無缺失值,但測試集有缺失值,第{0}條樣本被删除'.format(del_index))
        df_return = pd.DataFrame(onehot_model.transform(df).toarray())
        df_return.columns = onehot_model.get_feature_names(df.columns)

    elif flag == 'transform':
        # 編碼資料值轉化為原始變量
        with open(os.path.join(data_path_1, 'onehot.pkl'), 'rb') as read_model:
            onehot_model = pickle.load(read_model)

        # 逆變換
        df_return = pd.DataFrame(onehot_model.inverse_transform(df))
        df_return.columns = np.unique(['_'.join(i.rsplit('_')[:-1]) for i in df.columns])

    return df_return


# 标簽編碼
def label_encode(df, data_path_1, flag='train'):
    if flag == 'train':
        enc = LabelEncoder().fit(df)
        # 儲存編碼模型
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'wb') as save_model:
            pickle.dump(enc, save_model, 0)

        df_return = pd.DataFrame(enc.transform(df))
        df_return.name = df.name

    elif flag == 'test':
        # 測試資料編碼
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') as read_model:
            label_model = pickle.load(read_model)

        df_return = pd.DataFrame(label_model.transform(df))
        df_return.name = df.name

    elif flag == 'transform':
        # 編碼資料值轉化為原始變量
        with open(os.path.join(data_path_1, 'labelcode.pkl'), 'rb') as read_model:
            label_model = pickle.load(read_model)

        # 逆變換
        df_return = pd.DataFrame(label_model.inverse_transform(df))
    return df_return


# 自定義映射
def dict_encode(df, data_path_1):
    # 自定義映射
    embarked_mapping = {}
    embarked_mapping['status_account'] = {'NA': 1, 'A14': 2, 'A11': 3, 'A12': 4, 'A13': 5}
    embarked_mapping['svaing_account'] = {'NA': 1, 'A65': 1, 'A61': 3, 'A62': 5, 'A63': 6, 'A64': 8}
    embarked_mapping['present_emp'] = {'NA': 1, 'A71': 2, 'A72': 5, 'A73': 6, 'A74': 8, 'A75': 10}
    embarked_mapping['property'] = {'NA': 1, 'A124': 1, 'A123': 4, 'A122': 6, 'A121': 9}

    df = df.reset_index(drop=True)

    # 判斷資料集是否存在缺失值
    if sum(df.isnull().any()) > 0:
        df = df.fillna('NA')

    # 字典映射
    var_dictEncode = []
    for i in df.columns:
        col = i + '_dictEncode'
        df[col] = df[i].map(embarked_mapping[i])
        var_dictEncode.append(col)
    return df[var_dictEncode]


# WOE編碼
# 傳回某個特征的woe映射後的df、woe字典、iv值
# x:特征   y:類别   target:正樣本為1
def woe_cal_trans(x, y, target=1):
    # 計算總體的正負樣本數
    p_total = sum(y == target)  # 正樣本數
    n_total = len(x) - p_total  # 負樣本數
    value_num = list(x.unique())  # 去重後的總數
    woe_map = {}
    iv_value = 0
    for i in value_num:  # 這個特征每種取值的woe值
        # 計算該變量取值箱内的正負樣本總數
        y1 = y[np.where(x == i)[0]]
        p_num_1 = sum(y1 == target)
        n_num_1 = len(y1) - p_num_1
        # 計算占比
        # bad_1 = p_num_1 / p_total  # 壞樣本分布率
        # good_1 = n_num_1 / n_total  # 好樣本分布率
        good_1 = p_num_1 / p_total  # 壞樣本分布率
        bad_1 = n_num_1 / n_total  # 好樣本分布率
        if bad_1 == 0:
            bad_1 = 1e-5
        elif good_1 == 0:
            good_1 = 1e-5
        woe_map[i] = np.log(bad_1 / good_1)  # woe值
        iv_value += (bad_1 - good_1) * woe_map[i]  # iv值
    x_woe_trans = x.map(woe_map)
    x_woe_trans.name = x.name + "_woe"

    return x_woe_trans, woe_map, iv_value


# WOE編碼映射
def woe_encode(df, data_path_1, varnames, y, filename, flag='train'):
    """
    Param:
    df: 待編碼資料
    data_path_1 :存取檔案路徑
    varnames: 變量清單
    y:  目标變量
    filename:編碼存取的檔案名
    flag: 選擇訓練還是測試
    ---------------------------------------
    Return:
    df: 編碼後的資料,包含了原始資料
    woe_maps: 編碼字典
    iv_values: 每個變量的IV值
    var_woe_name: 每個特征拼接woe的列名
    """
    df = df.reset_index(drop=True)  # 重置索引,不保留原來的索引

    # 判斷資料集是否存在缺失值
    if sum(df.isnull().any()) > 0:
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        var_numerics = df.select_dtypes(include=numerics).columns  # 數值型特征
        var_str = [i for i in df.columns if i not in var_numerics]  # 字元串型特征
        # 資料類型的缺失值用-77777填補
        if len(var_numerics) > 0:
            df.loc[:, var_numerics] = df[var_numerics].fillna(-7777)
        # 字元串類型的缺失值用NA填補
        if len(var_str) > 0:
            df.loc[:, var_str] = df[var_str].fillna('NA')

    if flag == 'train':
        iv_values = {}  # 儲存每個特征的iv值
        woe_maps = {}  # 儲存每個特征的woe值
        var_woe_name = []
        for var in varnames:  # 周遊每一個特征
            x = df[var]
            # 變量映射
            x_woe_trans, woe_map, info_value = woe_cal_trans(x, y)
            var_woe_name.append(x_woe_trans.name)
            df = pd.concat([df, x_woe_trans], axis=1)  # 按行拼接woe值
            woe_maps[var] = woe_map
            iv_values[var] = info_value

        # 儲存woe映射字典
        with open(os.path.join(data_path_1, filename + '.pkl'), 'wb') as save_woe_dict:
            pickle.dump(woe_maps, save_woe_dict, 0)

        return df, woe_maps, iv_values, var_woe_name

    elif flag == 'test':
        # 測試資料編碼
        with open(os.path.join(data_path_1, filename + '.pkl'), 'rb') as read_woe_dict:
            woe_dict = pickle.load(read_woe_dict)

        # 如果訓練集無缺失值,測試集有缺失值則将該樣本删除
        woe_dict.keys()
        del_index = []
        for key, value in woe_dict.items():
            if 'NA' not in value.keys() and 'NA' in df[key].unique():
                index = np.where(df[key] == 'NA')
                del_index.append(index)
            elif -7777 not in value.keys() and -7777 in df[key].unique():
                index = np.where(df[key] == -7777)
                del_index.append(index)
        # 删除樣本
        if len(del_index) > 0:
            del_index = np.unique(del_index)
            df = df.drop(del_index)
            print('訓練集無缺失值,但測試集有缺失值,該樣本{0}删除'.format(del_index))

        # WOE編碼映射
        var_woe_name = []
        for key, value in woe_dict.items():
            val_name = key + "_woe"
            df[val_name] = df[key].map(value)
            var_woe_name.append(val_name)

        return df, var_woe_name


if __name__ == '__main__':
    path = r'G:\A_實訓前置1\python_workspace\finance_code\chapter5\\'
    data_path = os.path.join(path, 'data')
    file_name = 'german.csv'
    # 讀取資料
    data_train, data_test = data_read(data_path, file_name)
    # 不可排序變量
    var_no_order = ['credit_history', 'purpose', 'personal_status', 'other_debtors',
                    'inst_plans', 'housing', 'job', 'telephone', 'foreign_worker']

    # x_woe_trans, woe_map, iv_value = woe_cal_trans(data_train['job'], data_test['target'])
    # print(x_woe_trans)
    # print(woe_map)
    # print(iv_value)

    # one-hot編碼
    # 訓練資料編碼
    data_train.credit_history[882] = np.nan
    data_train_encode = onehot_encode(data_train[var_no_order], data_path, flag='train')

    # 測試集資料編碼
    data_test.credit_history[529] = np.nan
    data_test.purpose[355] = np.nan
    data_test_encode = onehot_encode(data_test[var_no_order], data_path, flag='test')

    # 檢視編碼逆變化後的原始變量名
    df_encoded = data_test_encode.loc[0:4]
    data_inverse = onehot_encode(df_encoded, data_path, flag='transform')
    print(data_inverse)

    # 啞變量編碼
    data_train_dummies = pd.get_dummies(data_train[var_no_order])
    data_test_dummies = pd.get_dummies(data_test[var_no_order])
    print(data_train_dummies.columns)

    # 可排序變量
    # 注意,如果分類變量的标簽為字元串,這是需要将字元串數值化才可以進行模型訓練,标簽編碼其本質是為
    # 标簽變量數值化而提出的方法,是以,其值支援單列資料的轉化操作,并且轉化後的結果是無序的。
    # 是以有序變量統一用字典映射的方式完成。
    var_order = ['status_account', 'svaing_account', 'present_emp', 'property']

    # 标簽編碼
    # 訓練資料編碼
    data_train_encode = label_encode(data_train[var_order[1]], data_path, flag='train')

    # 驗證集資料編碼
    data_test_encode = label_encode(data_test[var_order[1]], data_path, flag='test')

    # 檢視編碼你變化後的原始變量名
    # 後面再改一下
    df_encoded = data_test_encode
    data_inverse = label_encode(df_encoded, data_path, flag='transform')

    # 自定義映射
    # 訓練資料編碼
    data_train.credit_history[882] = np.nan
    data_train_encode = dict_encode(data_train[var_order], data_path)

    # 測試集資料編碼
    data_test.status_account[529] = np.nan
    data_test_encode = dict_encode(data_test[var_order], data_path)
    print(data_test_encode)

    # WOE編碼
    # 訓練集WOE編碼
    df_train_woe, dict_woe_map, dict_iv_values, var_woe_name = woe_encode(data_train, data_path, var_no_order,
                                                                          data_train.target, 'dict_woe_map',
                                                                          flag='train')
    print(df_train_woe, '\n')
    print(dict_woe_map, '\n')
    print(dict_iv_values, '\n')
    print(var_woe_name, '\n')

    # 測試集WOE編碼
    df_test_woe, var_woe_name = woe_encode(data_test, data_path, var_no_order, data_train.target, 'dict_woe_map',
                                           flag='test')

    print(df_train_woe)

           

繼續閱讀