数据挖掘

评分卡类模型的概率对齐和分数映射,代码实现

2020-12-22  本文已影响0人  xiaogp

摘要:评分卡机器学习

为什么要做概率对齐和分数映射

一般的机器学习二分类问题输出为概率在0-1之间的小数值,概率对齐指将模型输出的概率和真实的事件概率进行比对和对齐,尤其对一些极端模型概率进行修正,分数映射指的对模型的概率输出再做一层变换使结果更加契合业务理解和分析,一般是线性映射不影响排序不影响分布


需要对模型概率值做二次加工的原因:

概率对齐

概率对齐参考度小满的一篇分享


概率标准化.png

思路是直接对模型的预测坏概率和真实坏比率在做一次拟合,几个细节:


代码实现

模型使用逻辑回归,并且弃用框架的predict方法,将模型的权重和偏置拿出来写入配置,预测直接wx+b得到log(odds)也就是预测概率的logit,先写一个函数实现wx+b输出log(odds),其中params和feature分别是字段为key,权重和特征值为value的字典

def get_odds(feature):
    res = 0
    for i in feature_columns:
        res += params[i] * feature[i]
    res += params["intercept"]
    return res

取测试集,测试集的正负样本比例应该等于自然比例,输出模型预测的logit和实际的label,这份测试集的质量非常重要,因为需要基于他做事实的拟合

test = pd.read_csv("data/tmp_test.txt")
res6 = []
for i in range(len(test)):
    score = [get_odds(test.loc[i].to_dict()), test.loc[i]["标签"]]
    res6.append(score)

下一步通过模型预测logit和实际label的结果计算每个测试点的实际坏率,可以采用等频分箱

# 等频分箱
bin_num = int(len(res6) / 100)  # 分为100箱
bin_data = []
pair_data = []
# 根据模型预测输出排序
for line in sorted(res6, key=lambda x: x[0], reverse=True):
    bin_data.append(line)
    if len(bin_data) == bin_num:
        # 计算平均预测值
        mean_predict = np.mean([x[0] for x in bin_data])
        # 真实坏率
        bad = len([x for x in bin_data if x[1] == 1])
        good = len([x for x in bin_data if x[1] == 0])
        actual_odds = np.log(bad / good)
        pair_data.append([mean_predict, actual_odds])
        bin_data.clear()

也可以采用等距分箱

# 等距分箱
max_predict = max([x[0] for x in res6])
min_predict = min([x[0] for x in res6])
bin = (max_predict - min_predict) / 30  # 等距离分为30箱
pair_data = []
for i in range(30):
    bin_start = min_predict + i * bin
    bin_end = bin_start + bin
    bin_data = [x for x in res6 if bin_start <= x[0] < bin_end]
    # 平均预测值
    mean_predict = np.mean([x[0] for x in bin_data])
    # 真实坏率
    bad = len([x for x in bin_data if x[1] == 1 ])
    good = len([x for x in bin_data if x[1] == 0])
    actual_odds = np.log(bad / good)
    pair_data.append([mean_predict, actual_odds])

以上两种分箱方法都是为了平滑,需要注意某分箱可能存在分子或者分母为0的情况,需要剔除或者调整分箱或者其他处理,本例中采用另一种即不做分箱不做平滑处理,每个点都生成一对模型结果和实际坏率进行拟合,但是对每个点阈值以上数据量有数据要求,如果数据量太小不作为拟合样本,代码如下

# 每个点都拟合
res7 = []
count = 0
for score, label in res6:
    count += 1
    threshold_data = [x for x in res6 if x[0] >= score]
    # 限制数据量
    if len(threshold_data) >= 50:
        bad = len([x for x in threshold_data if x[1] == 1])
        good = len([x for x in threshold_data if x[1] == 0])
        # 限制都非0
        if bad and good:
            odds = np.log(bad / good)
        res7.append([score, odds])

以模型的输出logit作为x,实际坏率的logit(odds)作为y,做散点图,观察两者的关系

x = []
y = []
for line in res7:
    if -6.5 <= line[0] <= 6.5:
        x.append(line[0])
        y.append(line[1])
plt.scatter(x, y, s=2)
plt.show()
模型概率和实际坏率的关系.png

进行拟合,尝试多次后使用三次拟合

# 拟合三次多项式
np.plotfit(x, y, 2)  # 系数 array([-0.00282885, 0.00503271, 0.50497722, -2.06355245])
p = np.plot1d(np.plotfit(x, y, 2))

查看拟合曲线和之前的散点图,可以看到拟合了线性关系,将概率修正到真实坏率,并且在头尾处有修正

plt.scatter(x, y, s=2)
plt.scatter(x, [p(i) for i in x], s=2)
plt.show()
拟合图.png

获得系数后编写一个函数获得修正后的log(odds)

plot_params = [-0.00282885, 0.00503271, 0.50497722, -2.06355245]
def get_plotfit_odds(x):
    # 把原输出修正到[-9, 9],防止三次函数作怪
    if x < -9:
        x = -9
    if x > 9:
        x = 9
    return polt_params[0] * x ** 3 + poly_params[1] * x ** 2 + poly_params[2] * x + poly_params[3]

测试修正之后的logit(odds)

get_plotfit_odds(5)  # 0.2335451499
get_plotfit_odds(-1)  # -2.56066811
get_plotfit_odds(3)  # -0.57970535

分数映射

完成了第一步分数对齐,下一步分数映射就比较固定了,此步骤是一个线性映射,即确定w和b,度小满给出的公式中:


编写分数映射函数,此处期望分数在[0, 100]之间,并且分数越大风险越高,由于在上一步已知修正后的logit(odds)在[-4.5, 1]之间,所以计算可得要把分数放到100的区间,w=100 / (4.5 + 1) = 18,此时b=80差不多可以把100分填满,分数上升18分风险变为原来的2倍

def get_score(feature, w=18, b=80):
    odds = get_plotfit_odds(get_od ds(feature))
    score = int(round(w * odds + b, 1))
    if score > 100:
        score = 100
    if score < 0:
        score = 0
    return score

这个函数将模型预测,概率对齐,分数映射,异常修正全部包装在一起,现在应用到test数据集中查看分数结果

res4 = []
for i in range(len(test)):
    score1 = get_odds(test.loc[i].to_dict())  # 模型输出
    score2 = get_plotfit_odds(score1)  # 概率对齐
    score3 = get_score(test.loc[i].to_dict())  # 分数映射
    res4.append([score1, score2, score3])

查看原始概率的分布

plt.hist([x[0] for x in res4])
plt.show()
原始模型输出.png

查看概率对齐+分数映射后的最终得分

plt.hist([x[-1] for x in res4])
plt.show()
分数二次加工后的最终得分.png
上一篇下一篇

猜你喜欢

热点阅读