python数据分析:商品数据化运营(下)——基于投票组合模型的异常检测

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/85082284

本案例用到的主要技术包括:

  • 基本预处理:使用DictVectorizer将字符串分类变量转换为数值型变量、使用SMOTE对不均衡样本做过抽样处理。
  • 数据建模:基于cross_val_score的交叉检验、基于LogisticRegression、RandomForest、Bagging概率投票组合模型做分类。

案例数据

以下是本数据集的13个特征变量的详细说明:

  • order_id:订单ID,数字组合而成,例如4283851335。
  • order_date:订单日期,格式为YYYY-MM-DD,例如2013-10-17。
  • order_time:订单日期,格式为HH:MM:SS,例如12:54:44。
  • cat:商品一级类别,字符串型,包含中文、英文。
  • attribution:商品所属的渠道来源,字符串型,包含中文、英文。
  • pro_id:商品ID,数字组合而成。
  • pro_brand:商品品牌,字符串型,包含中文、英文。
  • total_money:商品销售金额,浮点型。
  • total_quantity:商品销售数量,整数型。
  • order_source:订单来源,从哪个渠道形成的销售,字符串型,包含中文、英文。
  • pay_type:支付类型,字符串型,包含中文、英文。
  • use_id:用户ID,由数字和字母等组成的字符串。
  • city:用户订单时的城市,字符串型,中文。

目标变量:abnormal_label,代表该订单记录是否是异常订单。

import numpy as np  # numpy库
import pandas as pd  # pandas库
from sklearn.feature_extraction import DictVectorizer  # 数值分类转整数分类库
from imblearn.over_sampling import SMOTE  # 过抽样处理库SMOTE
from sklearn.model_selection import StratifiedKFold, cross_val_score  # 导入交叉检验算法
from sklearn.linear_model import LogisticRegression  # 导入逻辑回归库
from sklearn.preprocessing import LabelEncoder
from sklearn.ensemble import VotingClassifier, RandomForestClassifier, BaggingClassifier  # 三种集成分类库和投票方法库
import warnings
warnings.filterwarnings('ignore')

# 定义特殊字段数据格式
dtypes = {'order_id': np.object,
          'pro_id': np.object,
          'use_id': np.object}
# 载入数据,有点大,需要等待
df = pd.read_csv('https://raw.githubusercontent.com/ffzs/dataset/master/abnormal_orders.txt', dtype=dtypes)

# 数据概观
df.head().T

在这里插入图片描述

# 数据类型
df.dtypes

在这里插入图片描述

# 查看缺失值
df.isna().sum()

在这里插入图片描述

# 有缺失值的总行数
df.isna().any(axis=1).sum()
# 1429

# 查看样本类分布情况
df.abnormal_label.value_counts()

'''
0    105733
1     28457
Name: abnormal_label, dtype: int64
'''

类样本分布审查,执行label_samples_summary(raw_data),从结果中发现数据存在一定程度的不均衡,异常值记录(label为1)跟非异常值的比例为1:3.7左右。该结果可以处理也可以不处理,这里我们选择处理。

# 数据描述
df.describe()

在这里插入图片描述

通过描述性统计,发现total_money和total_quantity中存在了极大值。但是我们选择不做任何处理,因为本节的主题就是针对异常值的分类检测。

def str2int(set, convert_object, unique_object, training=True):
    '''
    用于将分类变量中的字符串转换为数值索引分类
    :param set: 数据集
    :param convert_object:  DictVectorizer转换对象,当training为True时为空;当training为False时则使用从训练阶段得到的对象
    :param unique_object: 唯一值列表,当training为True时为空;当training为False时则使用从训练阶段得到的唯一值列表
    :param training: 是否为训练阶段
    :return: 训练阶段返回model_dvtransform,unique_list,traing_part_data;预测应用阶段返回predict_part_data
    '''
    convert_cols = ['cat', 'attribution', 'pro_id', 'pro_brand', 'order_source', 'pay_type', 'use_id',
                    'city']  # 定义要转换的列
    final_convert_matrix = set[convert_cols]  # 获得要转换的数据集合
    lines = set.shape[0]  # 获得总记录数
    dict_list = []  # 总空列表,用于存放字符串与对应索引组成的字典
    if training == True:  # 如果是训练阶段
        unique_list = []  # 总唯一值列表,用于存储每个列的唯一值列表
        for col_name in convert_cols:  # 循环读取每个列名
            cols_unqiue_value = set[col_name].unique().tolist()  # 获取列的唯一值列表
            unique_list.append(cols_unqiue_value)  # 将唯一值列表追加到总列表
        for line_index in range(lines):  # 读取每行索引
            each_record = final_convert_matrix.iloc[line_index]  # 获得每行数据,是一个Series
            for each_index, each_data in enumerate(each_record):  # 读取Series每行对应的索引值
                list_value = unique_list[each_index]  # 读取该行索引对应到总唯一值列表列索引下的数据(其实是相当于原来的列做了转置成了行,目的是查找唯一值在列表中的位置)
                each_record[each_index] = list_value.index(each_data)  # 获得每个值对应到总唯一值列表中的索引
            each_dict = dict(zip(convert_cols, each_record))  # 将每个值和对应的索引组合字典
            dict_list.append(each_dict)  # 将字典追加到总列表
        model_dvtransform = DictVectorizer(sparse=False, dtype=np.int64)  # 建立转换模型对象
        model_dvtransform.fit(dict_list)  # 应用分类转换训练
        traing_part_data = model_dvtransform.transform(dict_list)  # 转换训练集
        return model_dvtransform, unique_list, traing_part_data
    else:  # 如果是预测阶段
        for line_index in range(lines):  # 读取每行索引
            each_record = final_convert_matrix.iloc[line_index]  # 获得每行数据,是一个Series
            for each_index, each_data in enumerate(each_record):  # 读取Series每行对应的索引值
                list_value = unique_object[each_index]  # 读取该行索引对应到总唯一值列表列索引下的数据(其实是相当于原来的列做了转置成了行,目的是查找唯一值在列表中的位置)
                each_record[each_index] = list_value.index(each_data)  # 获得每个值对应到总唯一值列表中的索引
            each_dict = dict(zip(convert_cols, each_record))  # 将每个值和对应的索引组合字典
            dict_list.append(each_dict)  # 将字典追加到总列表
        predict_part_data = convert_object.transform(dict_list)  # 转换预测集
        return predict_part_data
    
def datetime2int(df):
    '''
    将日期和时间数据拓展出其他属性,例如星期几、周几、小时、分钟等。
    :param set: 数据集
    :return: 拓展后的属性矩阵
    '''
    # 获取datetime
    datetime_data = pd.to_datetime(df.order_date + " " + df.order_time)
    # 获取周数
    weekday_data = datetime_data.dt.weekday.tolist()
    # 获取天数
    daysinmonth_data = datetime_data.dt.day.tolist()
    # 获取月份
    month_data = datetime_data.dt.month.tolist()
    # 获取秒
    second_data = datetime_data.dt.second.tolist()
    # 获取分钟
    minute_data = datetime_data.dt.minute.tolist()
    # 获取小时
    hour_data = datetime_data.dt.hour.tolist()
    final_set = []  # 列表,用于将上述拓展属性组合起来
    final_set.extend((weekday_data, daysinmonth_data, month_data, second_data, minute_data, hour_data))  # 将属性列表批量组合
    final_matrix = np.array(final_set).T  # 转换为矩阵并转置
    return final_matrix

def sample_balance(X, y):
    '''
    使用SMOTE方法对不均衡样本做过抽样处理
    :param X: 输入特征变量X
    :param y: 目标变量y
    :return: 均衡后的X和y
    '''
    model_smote = SMOTE()  # 建立SMOTE模型对象
    x_smote_resampled, y_smote_resampled = model_smote.fit_sample(X, y)  # 输入数据并作过抽样处理
    return x_smote_resampled, y_smote_resampled

drop_na_set = df.dropna()  # 丢弃带有NA值的数据行
X_raw = drop_na_set.iloc[:, 1:-1]  # 分割输入变量X,并丢弃订单ID列和最后一列目标变量
y_raw = drop_na_set.iloc[:, -1]  # 分割目标变量y

model_dvtransform, unique_object, str2int_data = str2int(X_raw, None, None, training=True)  # 字符串分类转整数型分类

datetime2int_data = datetime2int(X_raw)  # 拓展日期时间属性

combine_set = np.hstack((str2int_data, datetime2int_data))  # 合并转换后的分类和拓展后的日期数据集

constant_set = X_raw[['total_money', 'total_quantity']]  # 原始连续数据变量
X_combine = np.hstack((combine_set, constant_set))  # 再次合并数据集
X, y = sample_balance(X_combine, y_raw)  # 样本均衡处理

丢弃NA值:由于样本量足够大,因此我们处理中会选择丢弃缺失值,这是一种“大数据”量下的缺失值问题。使用drop方法丢弃,形成不含有NA值的drop_na_set。

分割输入变量X:在drop_na_set基础上,使用ix方法获取从第二列开始到倒数第二列,形成输入变量集合X_raw。第一列为订单ID,该列用于区别每个订单,因此该唯一区别值不具有规律特征;最后一列是目标变量y。

分割目标变量y:在drop_na_set基础上,使用iloc方法获取最后一列数据,形成目标数据集y_raw。

字符串分类转整数型分类:直接调用str2int方法对X做转换,返回model_dvtransform、unique_object、str2int_data分别是训练后的DictVectorizer对象、唯一值总列表和转换后的数值型分类。

拓展日期时间属性:直接调用datetime2int方法对X做转换,形成结果集datetime2int_data。

合并转换后的分类和拓展后的日期数据集:使用numpy的hastck方法将分类和拓展后的日期数据集沿列合并,形成合并数据集combine_set。

将原始数据集中的total_money和total_quantity列数据集提取出来,然后再次使用numpy的hstack方法与combine_set做合并,至此形成了完整的输入变量集X_combine。

最后调用sample_balance函数对X_combine和y_raw做过抽样处理,形成最终结果集X和y。

model_rf = RandomForestClassifier(n_estimators=20, random_state=0)  # 随机森林分类模型对象
model_lr = LogisticRegression(random_state=0)  # 逻辑回归分类模型对象
model_BagC = BaggingClassifier(n_estimators=20, random_state=0)  # Bagging分类模型对象
estimators = [('randomforest', model_rf), ('Logistic', model_lr), ('bagging', model_BagC)]  # 建立组合评估器列表
model_vot = VotingClassifier(estimators=estimators, voting='soft', weights=[0.9, 1.2, 1.1], n_jobs=-1)  # 建立组合评估模型
cv = StratifiedKFold(8)  # 设置交叉检验方法
cv_score = cross_val_score(model_vot, X, y, cv=cv)  # 交叉检验
print ('{:*^60}'.format('Cross val socres:'))
print (cv_score)  # 打印每次交叉检验得分
print ('Mean scores is: %.2f' % cv_score.mean())  # 打印平均交叉检验得分
model_vot.fit(X, y)  # 模型训练

结果如下:

   *********************Cross val socres:**********************
    [0.76707504 0.92312404 0.97251149 0.97182236 0.92419602 0.90868367
     0.90967915 0.9167241 ]
    Mean scores is: 0.91

从交叉检验结果看出,8次交叉检验除了第一次结果略差以外,其他7次都比较稳定,整体交叉检验得分(准确率)达到91%,说明了其准确率和鲁棒性相对不错。

建立多个分类模型对象。通过RandomForestClassifier方法建立随机森林分类模型对象model_rf,设置分类器数量为20,目的是希望通过更多的分类器达到更好的分类精度;设置随机状态为0,目的是控制每次随机的结果相同。然后按照类似的步骤分别建立逻辑回归分类模型对象model_lr、Bagging分类模型对象model_BagC。

提示:RandomForest、Bagging以及之前我们用到的AdaBoost、Gradient Boosting都是常用的集成方法。除了这些外,sklearn.ensemble中还提供了extra-trees、Isolation Forest等多种集成方法。这些集成方法大多数都既有分类器又有回归器,意味着可以用于分类和回归。

建立一个由模型对象名称和模型对象组合的元组的列表estimators。其中:对象名称为了区分和识别使用,任意字符串都可以;模型对象是上面建立的三个分类器对象。该列表用于组合投票模型器的参数设置。

使用VotingClassifier方法建立一个基于投票方法的组合分类模型器,具体参数如下:

  • estimators:模型组合为上面建立的estimators列表。
  • voting:投放方法设置为soft,意味着使用每个分类器的概率做投票统计,最终按投票概率选出;还可以设置为hard,意味着通过每个分类器的label按得票最多的label做预测输出。
  • weights:设置三个分类器对应的投票权重,这样可以将分类概率和权重做加权求和。
  • n_jobs:设置为-1意味着计算时使用所有的CPU。

ps:在设置voting参数时,如果设置为soft,要求每个模型器必须都支持predict_proba方法,否则只能使用hard方法。例如使用SVC(SVM的分类器)时,就只能设置为hard。

使用StratifiedKFold(8)设置一个8折交叉检验方法,这是一个按照目标变量的样本比例进行随机抽样的方法,尤其适合分类算法的交叉检验。

X_raw_data = pd.read_csv('https://raw.githubusercontent.com/ffzs/dataset/master/new_abnormal_orders.csv', dtype=dtypes)  # 读取要预测的数据集
X_raw_new = X_raw_data.iloc[:, 1:]  # 分割输入变量X,并丢弃订单ID列和最后一列目标变量
str2int_data_new = str2int(X_raw_new, model_dvtransform, unique_object, training=False)  # 字符串分类转整数型分类
datetime2int_data_new = datetime2int(X_raw_new)  # 日期时间转换
combine_set_new = np.hstack((str2int_data_new, datetime2int_data_new))  # 合并转换后的分类和拓展后的日期数据集
constant_set_new = X_raw_new[['total_money', 'total_quantity']]  # 原始连续数据变量
X_combine_new = np.hstack((combine_set_new, constant_set_new))  # 再次合并数据集
y_predict = model_vot.predict(X_combine_new)  # 预测结果
print ('{:*^60}'.format('Predicted Labesls:'))
print (y_predict)  # 打印预测值。

结果:

*********************Predicted Labesls:********************* 
 [1 0 0 0 0 0 0]

结论

在该案例中,91%的准确率是一个比较高的结果,该结果无论是预测的准确率或者多数据集的鲁棒性都表现突出。这首先得益于各个分类评估器本身的性能比较稳定,尤其是集成方法的随机森林和Bagging方法;其次是基于预测概率的投票方法配合经验上的权重分配,会使得经验与数据完美结合,也会产生相互叠加效应,在正确配置的前提下,会进一步增强组合模型的分类准确率。

注意点

关于耗时

以笔者的工作环境,完整运行一次大约需要需要15分钟时间,这里面有两个主要耗时的环节:

训练阶段的字符串分类转整数型分类str2int,该过程需要对矩阵中的每个值做映射。
训练阶段的交叉检验,该过程由于是8折交叉检验并且里面有2个集成方法,再加上使用组合投票的分类器的应用,导致整个交叉检验耗时较长。
因此,如果读者更侧重于效率的话,那么这种基于组合投票以及集成分类方法的实现思路将不是优先选择。

关于输入特征变量

本案例中应用了两个特殊字段pro_id、use_id,这两类ID一般作为关联主键或者数据去重唯一ID,而很少用于模型训练本身。这里使用的原因是希望能从中找到是否异常订单也会集中在某些品类或某些客户上。笔者经过测试,如果把这两个维度去掉,整个模型的准确率会下降到70%以下。

关于样本均衡

由于本案例中的两类数据差异并没有特别大(例如1:10甚至更大),因此均衡处理不是必须的。本案例中由于运营对于异常的定义比较宽松,因此才会形成大量异常名单范围。但实际上异常检测在很多情况下的记录是比较少的,因此样本均衡操作通常必不可少。

字符串分类转整数型分类

字符串分类转整数型分类以及后续的二值化标志问题,应用的前提是训练集中被转换的唯一值域必须是固定的,否则在预测集转换时遇到新数据值时就会报错,这点在之前提到过。在这里再次提出希望读者注意。

有关数据集中的NA值

在数据处理的一开始,我们就已经将NA值排除了。但读者是否想过,如果预测应用时,再次出现NA值该如何处理?

我们先分析下输入变量在订单信息生成时,是否允许出现缺失值。attribution、cat、pro_id、pro_brand这几个字段都是根据数据库中商品信息自动匹配的,因此不应该出现缺失值;order_id、order_date、order_time、total_money、total_quantity、order_source、use_id、city是订单时生成的必填信息,也不应该有缺失;而关于pay_type这个要看具体业务部门如何定义:

  • 如果基于已经支付的订单做异常分类检测,那么该字段不应为空;
  • 如果基于全部订单做异常分类检测,那么该字段会经常出现为空的情况;

基于上述分析,我们会有如下对应策略:

  • 针对attribution、cat、pro_id、pro_brand字段,只要有pro_id(商品ID),就可以从商品库中匹配出这些信息来。
  • 针对order_id、order_date、order_time、total_money、total_quantity、order_source、use_id、city字段,只要有order_id,就可以从订单库中匹配出这些信息来。

那如果商品库和订单库没有两个ID,或者即使匹配回来的数据仍然有NA值如何处理?由于这些数据理论上不应该为空,建议将其筛选出来单独存储(当然不作预测),然后跟IT部门沟通,分析到底为什么会出现缺失值并制定补足策略,该策略会应用到缺失值处理过程中。

如果缺失值无法预测、也无法避免,那么可以通过条件判断如果数据记录中有缺失值则不作检测,毕竟缺失值只占1%不到;如果读者认为有必要,则可以将缺失值作为一种特殊值的分布形态,以具体值(例如0)做填充,用于后续数据处理和建模使用,这也是一种行之有效的方法。

提示 通常很多数据处理环节对NA是“无法容忍”的,例如OneHotEncoder就无法将NA值转换为二值化矩阵,因为NA不是整数型数据。此时将NA值以特定值填充转换是一种变通思路。**

参考:

《python数据分析与数据化运营》 宋天龙

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/85082284
今日推荐