> 技术文档 > Python数据分析案例61——信贷风控评分卡模型(A卡)(scorecardpy 全面解析)

Python数据分析案例61——信贷风控评分卡模型(A卡)(scorecardpy 全面解析)


案例背景

虽然在效果上,传统的逻辑回归模型通常不如现代的机器学习模型,但在风控领域,解释性至关重要。逻辑回归的解释性是这些“黑箱”模型所无法比拟的,因此,研究传统的评分卡模型依然是有意义的。 传统的评分卡模型与机器学习模型的主要差异在于特征工程的处理。机器学习模型通常不需要对数据进行大幅度的调整,而传统评分卡模型则需要对数据进行分箱、编码、计算WOE(Weight of Evidence)等处理。

并且由于评分卡模型是线性模型,还需要进行一定的变量筛选。因此,评分卡模型的特征工程相对更加复杂,但解释性更强。 至于哪种模型效果更好,取决于具体的数据鄂使用场景(老板非要看解释性那就得评分卡了,客户非要效果那就得LGBM)。在这个案例中,我们使用一个之前测试过的信贷评分数据集来构建评分卡模型,并与主流机器学习模型进行对比,以评估其效果。这个案例也展示了构建全面的A卡(授信行为)和信贷风控评分模型的整体流程,同时解析了ScoreCardPy包的用法,方便未来快速应用于相关项目。

scorecardpy 的这些用法都会演示,全面解析 数据集划分 (split_df)

变量筛选(iv, var_filter)

变量分箱(woebin, woebin_plot, woebin_adj, woebin_ply)

分数转换(scorecard, scorecard_ply)

效果评估(perf_eva, perf_psi)


数据介绍

大部分的教学,一提到ScoreCardPy包应用,马上就要掏出了他内置的德国信用卡数据集。这个其实对新手小白不是很友好的,他们也不知道这个数据集,咋样读取的,怎么做清洗。我下面会从数据读取开始一步步的进行.

本次数据是这样的:

 

最后一列一变量就代表这个人是否会违约,也就是是不是要给他贷款的和响应变量y。所以这是一个a卡模型。

当然需要本次演示的所有代码文件和数据的同学还是可以参考:信贷评分卡


代码实现

首先导入数据科学常用的包

import numpy as npimport pandas as pdimport matplotlib.pyplot as plt import seaborn as snsimport scorecardpy as scimport shap plt.rcParams [\'font.sans-serif\'] =\'SimHei\'  #显示中文plt.rcParams [\'axes.unicode_minus\']=False  #显示负号

然后读取数据

读取数据

train=pd.read_csv(\'train.csv\').set_index(\'Id\')test=pd.read_csv(\'test.csv\').set_index(\'Id\')print(train.shape,test.shape)train.head()

可以看到训练集7500条,测试集2500条。变量维度为十六个x,一个y。

查看数据基本信息

train.info()

我们可以看到数据的缺失率参差不齐,并且有一些object的对象这些不能放入模型直接训练,得进行一个转化。

变量的含义的简介:

查看一下标签 分布

train[\'Credit Default\'].value_counts()

可以看到大概的比例是好样本为五,坏样本为二,比例还算较为均衡,没有那么极端。

非数值型变量查看其描述性统计。

train.select_dtypes(exclude=[\'number\']).describe()

可以看到这几个类别变量的类别都不是很多,我们可以直接进行因子化,label encoder。

我们自定义一个函数,不仅训练集要进行同样的转化,测试集也要同样进行类别变量的转化。

def ensure_category_consistency(train, test, columns): for col in columns: # 找到所有可能的类别 categories = pd.Categorical(train[col]).categories #.append(test[col]) # 定义一致的类别顺序 train[col] = pd.Categorical(train[col], categories=categories) test[col] = pd.Categorical(test[col], categories=categories) # 转换为category数据类型 train[col] = train[col].astype(\'category\') test[col] = test[col].astype(\'category\') return train, test### 类别变量转化columns_to_convert = train.select_dtypes(exclude=[\'number\']).columns.to_list()train, test = ensure_category_consistency(train, test, columns_to_convert)

查看数据信息

train.info()

现在可以看到所有的数据基本上都是数值型和类别型变量,可以直接进行模型计算。


变量筛选

做机器学习其实大体上是不用进行变量筛选的,变量越多越好,但是做传统的评分卡模型,由于它是线性模型,所以它不能用那么多变量,它得进行一定的筛选,得把一些解释性不好的变量,效果不好的变量给它进行去掉。

其主要用法解析:

默认的参数配置为:iv_limit=0.02, missing_limit=0.95, identical_limit=0.95,即当某个变量的 IV 值小于0.02,或缺失率大于95%,或同值率(除空值外)大于95%,则剔除掉该变量。

此外,该方法还内置了除上述以外的其他参数:

def var_filter(dt, y, x=None, iv_limit=0.02, missing_limit=0.95,  
               identical_limit=0.95, var_rm=None, var_kp=None, 
               return_rm_reason=False, positive=\'bad|1\')
其中各参数含义如下:

varrm可设置强制保留的变量,默认为空;
varkp可设置强制剔除的变量,默认为空;
return_rm_reason可设置是否返回剔除原因,默认为不返回(False);
positive可设置坏样本对应的值,默认为“bad|1”。

下面进行变量筛选,我们使用iv最小为0.02的这个条件。(不懂iv是啥的可以去搜一下)

dt_s = sc.var_filter(train, y=\"Credit Default\",iv_limit=0.02, missing_limit=0.95, identical_limit=0.95,return_rm_reason=True)

我们这里打开了返回原因的这个参数,因为所以它后面会返回一个字典,我们来查看一下这个字典是什么。

dt_s[\'rm\'] ## return_rm_reason=True 返回一个字典

可以看到它移除了5个变量,并且它这个方法返回的rm的这个键对应的值是一个data frame,他清楚地告诉你哪些变量是v小于0.02才导致他们被移除的。

data_train=dt_s[\'dt\']data_train.info()

现在只剩下十二个变量了,x只有十一个了

我们对测试集也进行同样的变量过滤

### 测试集也进行同样的变量过滤,但是没ytest=test[data_train.columns[:-1]]

这里附赠一个,我自己写的如何能全面的看iv的一个方法吧。

### 变量筛选咋定义看IV的方法def calculate_pred_proba_bin(true_labels,predictions, bins=10): # 创建分箱区间 bin_edges = np.linspace(0, 1, bins + 1) # 分箱 bin_labels = [f\"{bin_edges[i]:.2f}-{bin_edges[i+1]:.2f}\" for i in range(len(bin_edges)-1)] bin_indices = np.digitize(predictions, bin_edges, right=False) - 1 # 创建数据框 df = pd.DataFrame({ \'bin\': [bin_labels[i] for i in bin_indices], \'label\': true_labels }) # 统计各个分箱的总数、类别为0和1的样本数 result = df.groupby(\'bin\')[\'label\'].agg(total=\'count\', count_0=lambda x: (x == 0).sum(), count_1=lambda x: (x == 1).sum()).reset_index() # 计算坏样本率和坏样本在所有坏样本中的比例 total_bad_samples = result[\'count_1\'].sum() result[\'bad_rate\'] = result[\'count_1\'] / result[\'total\'] result[\'bad_percent\'] = result[\'count_1\'] / total_bad_samples #result=result.sort_values(\'bin\',ascending=False) result[\'lift\']=result[\'bad_rate\']/(total_bad_samples/result[\'total\'].sum()) result[\'cumulative_bad_percent\'] = result[\'bad_percent\'][::-1].cumsum()[::-1] return result.style.bar(color=\'skyblue\').format(subset=[\'bad_rate\',\'bad_percent\',\'lift\',\'cumulative_bad_percent\'], precision=4) def scorecardpy_display_bin(bins_info): df_list = [] for col, bin_data in bins_info.items(): df = pd.DataFrame(bin_data) df_list.append(df) result_df = pd.concat(df_list, ignore_index=True) # 增加 lift 列 total_bad = result_df[\'bad\'].sum() ; total_count = result_df[\'count\'].sum() overall_bad_rate = total_bad / total_count result_df[\'lift\'] = result_df[\'badprob\'] / overall_bad_rate result_df=result_df.sort_values([\'total_iv\',\'variable\'],ascending=False).set_index([\'variable\',\'total_iv\',\'bin\'])[[\'count_distr\', \'count\',\'good\',\'bad\',\'badprob\',\'lift\',\'bin_iv\',\'woe\']] return result_df.style.format(subset=[\'count\',\'good\',\'bad\'], precision=0).format(subset=[\'count_distr\', \'bad\',\'lift\',  \'badprob\',\'woe\',\'bin_iv\'], precision=4).bar(subset=[\'badprob\',\'bin_iv\',\'lift\'], color=[\'#d65f5f\', \'#5fba7d\'])train_miss=train.copy()train_miss[\'Years in current job\']=train_miss[\'Years in current job\'].astype(\'str\').fillna(\'missing\').astype(\'category\')bins_adj = sc.woebin(train_miss, y=\"Credit Default\")scorecardpy_display_bin(bins_adj)

这个是经过排序的,可以清楚看到哪些变量,他们对应的IV是多少,他们的分箱情况是怎么样的,并且每一箱的换样本浓度还有提升度以及他们对iv的贡献都在上面,可能一目了然。


数据集划分

虽然sklearn库有它自带的划分训练集的方式,但是我们的sc包它也有。划分训练集和验证集的方法,我们就用他的好了。

train, val = sc.split_df(data_train, \'Credit Default\', ratio=0.7, seed=186).values()print(train.shape, val.shape )

可以看到我们的训练集大概有5250条数据,12个变量,其中11个是x,一个是y,我们的验证集是2250条。也是12个变量。


变量分箱

可通过woebin()函数对全部变量进行自动分箱,并基于woe_bin的结果,使用woebin_plot对各变量分箱的count distribution和bad probability进行可视化,可观察是否存在单调性:

woebin()函数包括如下参数:

def woebin(dt, y, x=None, 
           var_skip=None, breaks_list=None, special_values=None, 
           stop_limit=0.1, count_distr_limit=0.05, bin_num_limit=8, 
           # min_perc_fine_bin=0.02, min_perc_coarse_bin=0.05, max_num_bin=8, 
           positive=\"bad|1\", no_cores=None, print_step=0, method=\"tree\",
           ignore_const_cols=True, ignore_datetime_cols=True, 
           check_cate_num=True, replace_blank=True, 
           save_breaks_list=None, **kwargs)
woebin()可针对数值型和类别型变量生成最优分箱结果,方法可选择决策树分箱、卡方分箱或自定义分箱。其他各参数的含义如下:

var_skip: 设置需要跳过分箱操作的变量;
breaks_list: 切分点列表,默认为空。如果非空,则按设置的切分点进行分箱处理;
special_values: 设置需要单独分箱的值,默认为空;
count_distr_limit: 设置分箱占比的最小值,一般可接受范围为0.01-0.2,默认值为0.05;
method: 设置分箱方法,可设置\"tree\"(决策树)或\"chimerge\"(卡方),默认值为\"tree\";

stop_limit: 当IV值的增长率小于所设置的stop_limit,或卡方值小于qchisq(1-stoplimit, 1)时,停止分箱。一般可接受范围为0-0.5,默认值为0.1;
bin_num_limit: 该参数为整数,代表最大分箱数。
positive: 指定样本中正样本对应的标签,默认为\"bad|1\";
no_cores: 设置用于并行计算的 CPU 数目;
print_step: 该参数为非负数,默认值为1。若print_step>0,每次迭代会输出变量名。若iteration=0或no_cores>1,不会输出任何信息;

ignore_const_cols: 是否忽略常数列,默认值为True,即忽略常数列;
ignore_datetime_cols: 是否忽略日期列,默认值为True,即忽略日期列;
check_cate_num: 检查类别变量中枚举值数目是否大于50,默认值为True,即自动进行检查。若枚举值过多,会影响分箱过程的速度;
replace_blank: 设置是否将空值填为None,默认为True。
若对自动分箱结果不满意,还可手动自定义分箱:

breaks_adj = {\'age.in.years\': [26, 35, 40],
    \'other.debtors.or.guarantors\': [\"none\", \"co-applicant%,%guarantor\"] }
bins_adj = sc.woebin(dt_s, y=\"creditability\", breaks_list=breaks_adj)

其实使用很简单,就下面一行代码,然后再画图看一看就行。

bins = sc.woebin(data_train, y=\"Credit Default\" )sc.woebin_plot(bins)

所有变量都有一张图,但是我这里篇幅限制就不放那么多了,只放这一个变量。

从这个图中我们可以清楚的看到它的分箱的情况,每一箱里面的好坏的数量,比例以及坏样本的浓度是怎么样变化的。

## 可以看到 Credit Score 这个字段有点问题,按理来说分数越高黑样本浓度越低 ,750分以上黑样本这么高明显有问题

我们画拉出明细来看一下

bins[\'Credit Score\']#.plot.hist()

这个明细其实就是上面的图,但是他并没有展示750以上的这个数据的分布长什么样,我们直接筛选来看一下。

##查看到750以上的,部分是比750大一点,很多是5000以上的分数,明显有问题,这个切割点需要重新划分,data_train[data_train[\"Credit Score\"]>800][\"Credit Score\"].plot.hist()

800以上都是直接跳到了6000这种最高分了,下面重新划分这个变量的分箱阈值

breaks_adj = {\'Credit Score\': [678,696,728,740,800] }bins_adj = sc.woebin(data_train, y=\"Credit Default\", breaks_list=breaks_adj)sc.woebin_plot(bins_adj)

这里就使用到了break list,就是可以自己手工定义分享的节点阈值的。参数我觉得还是挺好用的,可以调整这个分箱,然后重新再看我们的这个变量的情况。

由于分箱多了一箱,所以可能有点密集重叠,但是我们可以明显的看到。信用分数这一项,随着分数变高,它的换样本浓度是明显的下降的只有在800分以上。6000分以上的这种才全是黑样本,这应该是数据的异常情况。

可以看到 iv已经升到了1.6119级,信用卡评分在800分以上的基本全部是黑样本。这应该是数据的一些错误之类的,因为信用卡评分也不可能到五六千分以上。

重新划分后,800分以上的,黑样本100浓度,IV超高。分箱完成后,使用woebin_ply()函数对变量进行woe变换,之后需要把所得到的woe值作为模型的输入。


WOE转化

分箱分好之后,我们就要进行woe的转化编码,在这个包里面使用很简单。

train_woe = sc.woebin_ply(train, bins_adj)val_woe = sc.woebin_ply(val, bins_adj)test_woe = sc.woebin_ply(test, bins_adj)

数据现在就准备好了我们可以进行训练了。


模型训练

我们取出x跟y训练集和验证集,还有测试集。

y_train = train_woe.loc[:,\'Credit Default\']X_train = train_woe.loc[:,train_woe.columns != \'Credit Default\']y_val = val_woe.loc[:,\'Credit Default\']X_val = val_woe.loc[:,train_woe.columns != \'Credit Default\']X_test = test_woe.loc[:,train_woe.columns[1:] != \'Credit Default\']print(X_train.shape,y_train.shape,X_val.shape,y_val.shape, X_test.shape)

使用逻辑回归进行学习器

# 逻辑回归 ------from sklearn.linear_model import LogisticRegressionlr = LogisticRegression(penalty=\'l1\', C=0.9, solver=\'saga\', n_jobs=-1)lr.fit(X_train, y_train)lr.coef_,lr.intercept_

可以看到回归的系数项和截距项。

然后我们可以直接进行预测概率。

# 预测train_pred = lr.predict_proba(X_train)[:,1]val_pred = lr.predict_proba(X_val)[:,1]test_pred = lr.predict_proba(X_test)[:,1]

效果评估

可使用perf_eva()函数对模型效果进行计算及可视化,基于预测的概率值和label值,提供KS(kolmogorov-smirnow), ROC, lift以及precision-recall四种评估指标:

该函数还包括以下参数:

def perf_eva(label, pred, title=None, groupnum=None, plot_type=[\"ks\", \"roc\"], 
             show_plot=True, positive=\"bad|1\", seed=186)
参数plot_type可设置为:\"ks\", \"lift\", \"roc\", \"pr\",默认为[\"ks\", \"roc\"]。
 

他的评估看ks跟auc特别简单,一行代码就可以了。

train_perf = sc.perf_eva(y_train, train_pred, title = \"train\")val_perf = sc.perf_eva(y_val, val_pred, title = \"val\")

训练集KS=0.38,AUC=0.77,验证集KS=0.3716,AUC=0.765,没有过拟合,很好的效果。

到这里可能就有小聪明要问了,为什么不用分类问题常用的评价指标,准确率,精准度,召回率f1值呢。

呃,因为在信贷模型里面大部分的时候都是样本不平衡的,这4个指标在样本不平衡的情况下效果非常差,信贷模型主要还是看ks跟auc。


分数转换

然后基于’woebin\'的结果和sklearn.linear_model的LogisticRegression,创建scorecard()函数,用于构建评分卡,只需一行代码:

scorecard()包括以下参数:

def scorecard(bins, model, xcolumns, points0=600, odds0=1/19, 
              pdo=50, basepoints_eq0=False, digits=0)
各参数含义如下:

bins:由`woebin`得到的分箱信息;
model:LogisticRegression模型对象;
points0:基准分数,默认值为600;
odds0: 基准 Odds(好坏比),与真实违约概率对应,可换算得到违约概率,Odds = p/(1-p)。默认值为 1/19;
pdo: Points toDouble theOdds,即Odds变成2倍时,所增加的信用分。默认值为50;
basepoints_eq0:设置是否要把basepoints均分给每个变量的得分,默认为False,即不进行均分。但大多数评分卡倾向于所有分数均为正数,所以可手动改为True。

使用很简单

card = sc.scorecard(bins_adj, lr, X_train.columns)

然后基于scorecard的结果,用scorecard_ply()函数计算train和test数据集的信用分数:

train_score = sc.scorecard_ply(train, card, print_step=0)val_score = sc.scorecard_ply(val, card, print_step=0)test_score = sc.scorecard_ply(test, card, print_step=0)

最后用perf_psi()得到该评分卡在测试数据集上的表现。

但是这个方法好像有时候版本问题容易报错,所以我们就自己自定义了一个怎么算psi的方法。

train.shape,val.shape,train_score.shape,val_score.shape , test_score.shapedef calculate_psi(expected, actual, bins=10, epsilon=1e-10): # 分箱 breakpoints = np.linspace(0, 1, bins + 1) * (max(expected.max(), actual.max()) - min(expected.min(), actual.min())) + min(expected.min(), actual.min()) expected_percents = np.histogram(expected, bins=breakpoints)[0] / len(expected) actual_percents = np.histogram(actual, bins=breakpoints)[0] / len(actual) # 增加一个小的正数,避免零值 expected_percents = np.where(expected_percents == 0, epsilon, expected_percents) actual_percents = np.where(actual_percents == 0, epsilon, actual_percents) # 计算PSI psi_value = np.sum((expected_percents - actual_percents) * np.log(expected_percents / actual_percents)) return psi_valueprint(f\'训练集和验证集的PSI{calculate_psi(train_score.to_numpy().reshape(-1,), val_score.to_numpy().reshape(-1,))}\') print(f\'训练集测试集的PSI{calculate_psi(train_score.to_numpy().reshape(-1,), test_score.to_numpy().reshape(-1,)) }\')

可以看到训练集和验证集,还有训练集和测试集的预测的分数PSI变化都不大,因此预测出来的分布都较为稳定,模型性能应该都挺良好的。

plt.figure(figsize=(7, 3),dpi=128)sns.kdeplot(train_score, bw_adjust=1.5,label=\'Train Score\', fill=True, palette=\"Set1\", color=\'gold\', alpha=0.3)#sns.kdeplot(val_score,bw_adjust=1.5,label=\'Validation Score\',fill=True, palette=\"Set2\", alpha=0.3)#sns.kdeplot(test_score, label=\'Test Score\', fill=True,palette=\"Set3\", alpha=0.3) #plt.title(\'Kernel Density Plot of Scores\')plt.xlabel(\'Score\')plt.ylabel(\'Density\')plt.legend()plt.show()

高度重叠,效果很好,测试集也没偏移

test_score是信用卡评分,是概率转化后的分数,test_pred是概率,现在重新预测为变成枚举值后 可以储存,然后提交

test_pred = lr.predict(X_test)#[:,1]df_pred=pd.DataFrame(test_pred,index=[test.index],columns=[\'Credit Default\'])df_pred.head()

df_pred.to_csv(\'评分卡模型预测结果.csv\')

进行储存,我们测试集上的这些申请人,就知道要不要给他贷款了。


和机器学习模型对比

上面的评分卡模型和机器学习模型到底谁效果好呢?我们测试一下

(之前有全面用这个数据集做过一篇关于机器学习及其可解释性可视化分析的一个案例链接在这儿)

这里的机器学习就是简单的算一个评价指标ks,做一下对比。

重新读取数据

train=pd.read_csv(\'train.csv\').set_index(\'Id\')test=pd.read_csv(\'test.csv\').set_index(\'Id\')columns_to_convert = train.select_dtypes(exclude=[\'number\']).columns.to_list()train, test = ensure_category_consistency(train, test, columns_to_convert)

划分训练集和验证集

X=train.iloc[:,:-1] ; y=train.iloc[:,-1]from sklearn.model_selection import train_test_splitX_train,X_val,y_train,y_val=train_test_split(X,y,stratify=y,test_size=0.3,random_state=186)

定义分类问题常用的四个评价指标

from sklearn.metrics import confusion_matrixfrom sklearn.metrics import classification_reportfrom sklearn.metrics import cohen_kappa_score def evaluation(y_test, y_predict): accuracy=classification_report(y_test, y_predict,output_dict=True)[\'accuracy\'] s=classification_report(y_test, y_predict,output_dict=True)[\'weighted avg\'] precision=s[\'precision\'] recall=s[\'recall\'] f1_score=s[\'f1-score\'] #kappa=cohen_kappa_score(y_test, y_predict) return accuracy,precision,recall,f1_score #, kappa

构建Lgbml模型

from lightgbm import LGBMClassifiermodel=LGBMClassifier(objective=\'binary\',random_state=1,verbose=-1,max_depth=6,n_estimators=100,eta=0.05)model.fit(X_train, y_train)y_pred=model.predict(X_val)evaluation(y_val,y_pred)

可以看到这4个指标的数据的情况,但是信贷模型一般不关注他们,所以就不是很重要了。

画出pr曲线和roc图。

from sklearn.metrics import roc_curve, auc, precision_recall_curvey_pred_proba = model.predict_proba(X_val)[:, 1]# 计算ROC曲线和AUC值fpr, tpr, _ = roc_curve(y_val, y_pred_proba)roc_auc = auc(fpr, tpr)# 计算PR曲线precision, recall, _ = precision_recall_curve(y_val, y_pred_proba)# 创建1*2的子图plt.figure(figsize=(10, 4),dpi=128)# 绘制ROC曲线plt.subplot(1, 2, 1)plt.plot(fpr, tpr, color=\'tomato\', lw=2, label=\'AUC = %0.2f\' % roc_auc)plt.plot([0, 1], [0, 1], color=\'k\', lw=1, linestyle=\'--\')plt.xlim([0.0, 1.0]) ; plt.ylim([0.0, 1.05])plt.xlabel(\'False Positive Rate\') ; plt.ylabel(\'True Positive Rate\')plt.title(\'Receiver Operating Characteristic (ROC) Curve\')plt.legend(loc=\"lower right\")# 绘制PR曲线plt.subplot(1, 2, 2)plt.plot(recall, precision, color=\'skyblue\', lw=2)plt.xlim([0.0, 1.0]) ; plt.ylim([0.0, 1.05])plt.xlabel(\'Recall\') ; plt.ylabel(\'Precision\')plt.title(\'Precision-Recall (PR) Curve\')# 显示图像plt.tight_layout()plt.show()

画出ks的图

import scikitplot as skpltskplt.metrics.plot_ks_statistic(y_val,model.predict_proba(X_val))plt.show()

Lgbm模型验证集上的KS=0.369,AUC=0.77 ,比上面的评分卡模型验证集KS=0.3716,AUC=0.765差不多

Lgbm模型处理简单,但是没太多解释性,评分卡逻辑回归要做很多woe处理,解释性强,效果都差不多,各有优缺点吧

预测结果储存

test_pred=model.predict(test)df_pred=pd.DataFrame(test_pred,index=[test.index],columns=[\'Credit Default\'])df_pred.head()df_pred.to_csv(\'LGBM模型预测结果.csv\')

那么到底谁的结果好呢?我们把测试集上用两种不同模型得到的结果分别提交到kaggle上去进行一个打分。

 

测试集上传到kaggle上计算F1值的对比,高下立见!

看来还是现代的机器学习效果更好啊!


下面是PS,补充学习内容

评分卡转化的影响

我们模型输出的是概率要转化为评分卡的这个分数,要经过一定的几率转化,这个转化有一些参数,我们可以研究一下它对分数的分布的影响。

import numpy as npimport matplotlib.pyplot as pltimport seaborn as sns# 生成1000个正态分布的随机概率,假设均值在0.5,标准差较小以确保大部分集中在0到1之间np.random.seed(42) # 固定随机种子以保证结果可重复random_probs = np.clip(np.random.normal(loc=0.5, scale=0.1, size=1000), 0, 1)import numpy as npdef cal_score(pred, pdo=0.2, base_score=0.5, base_odds=1.0): factor = pdo / np.log(2) # 调整这部分 offset = base_score - factor * np.log(base_odds) odds = (1 - pred) / pred score = offset + factor * np.log(odds) # 限制分数在0到1之间 #score = np.clip(score, score_min, score_max) return score# 应用分数转换scores = cal_score(random_probs)

调整 pdo 参数:pdo(每翻一倍的分数差)决定了分数随赔率变化的敏感度。通过降低 pdo 值,你可以增加分数对概率变化的敏感度,从而增加区分度。

调整 base_odds 参数:base_odds 是基准的赔率。调整这个值可以影响分数的整体偏移。例如,如果你将基准赔率稍微增大或减小,可能会得到偏离中心的分数。

调整 base_score 参数:base_score 是赔率为基准赔率时的分数。通过改变这个值,你可以整体上平移分数分布。

scores2 = cal_score(random_probs,pdo=0.2, base_score=0.5, base_odds=1,)scores3 = cal_score(random_probs,pdo=0.5, base_score=0.5, base_odds=1,)scores4 = cal_score(random_probs,pdo=0.2, base_score=0.05, base_odds=1,)scores5 = cal_score(random_probs,pdo=0.2, base_score=0.5, base_odds=2,)

可视化对比

# 画核密度图plt.figure(figsize=(7, 4),dpi=128)sns.kdeplot(random_probs, fill=True, bw_adjust=1.5,label=\'real\')sns.kdeplot(scores2, fill=True, bw_adjust=1.5,label=\'pdo=0.2, base_score=0.5, base_odds=1\')sns.kdeplot(scores3, fill=True, bw_adjust=1.5,label=\'pdo=0.5, base_score=0.5, base_odds=1\')sns.kdeplot(scores4, fill=True, bw_adjust=1.5,label=\'pdo=0.2, base_score=0.05, base_odds=1\')sns.kdeplot(scores5, fill=True, bw_adjust=1.5,label=\'pdo=0.2, base_score=0.5, base_odds=2\')plt.title(\'KDE of Transformed Probabilities\')plt.xlabel(\'Probability\')plt.ylabel(\'Density\')plt.legend(fontsize=6)plt.show()


创作不易,看官觉得写得还不错的话点个关注和赞吧,本人会持续更新python数据分析领域的代码文章~(需要定制类似的代码可私信)

以往的文章可以在这里查看:数据分析案例合集


大学生论坛