返回介绍

6.8 案例:基于 LogisticRegression、RandomForest、Bagging 概率投票组合模型的异常检测

发布于 2024-01-27 22:54:28 字数 22582 浏览 0 评论 0 收藏 0

6.8.1 案例背景

异常检测在之前介绍过,可以通过监督式和非监督式两类算法实现。本案例是使用监督式算法中的分类算法实现的异常检测应用。

本节案例的输入源数据new_abnormal_orders.csv、abnormal_orders.txt和源代码chapter6_code2.py位于“附件-chapter6”中,默认工作目录为“附件-chapter6”(如果不是,请cd切换到该目录下,否则会报"IOError:File new_abnormal_orders.csv does not exist"错误)。程序的输出预测数据直接打印输出,没有写入文件。

6.8.2 案例主要应用技术

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

基本预处理:使用DictVectorizer将字符串分类变量转换为数值型变量、使用SMOTE对不均衡样本做过抽样处理。

数据建模:基于cross_val_score的交叉检验,基于LogisticRegression、RandomForest、Bagging概率投票组合模型做分类。

主要用到的库包括:Numpy、Pandas、Sklearn、Imblearn,其中Sklearn是数据建模的核心库。

本案例的技术应用重点有两部分:

一部分是将原始数据集中的字符串分类转换为数值分类,便于参与建模运算;

一部分是通过概率投票方法,基于LogisticRegression、RandomForest、Bagging三个分类器建立一个组合分类器,用于实现组合投票和分类预测。

6.8.3 案例数据

案例数据是某企业的部分订单,以下是数据概况:

特征变量数:13。

数据记录数:134190。

是否有NA值:有。

是否有异常值:有。

以下是本数据集的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,代表该订单记录是否是异常订单。

6.8.4 案例过程

步骤1 导入库。

import sys
reload(sys)
sys.setdefaultencoding('utf-8')
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.ensemble import VotingClassifier, RandomForestClassifier, Bagging-Classifier  # 三种集成分类库和投票方法库

本案例主要用到了以下库:

sys:导入sys并设置字符编码为utf-8,目的是显示中文不乱码。

Numpy:基本数据处理。

Pandas:数据读取和审查,异常值处理,缺失值审查和处理等。

DictVectorizer:用于将定义好的字符串值和数值键值对做转换。

SMOTE:用于处理样本不均衡的过抽样处理库。

StratifiedKFold:适用于有标签数据集的交叉检验数据集划分方法。

cross_val_score:通过交叉检验方法做模型效果评估。

LogisticRegression、RandomForestClassifier、BaggingClassifier:逻辑回归、随机森林、Bagging方法的分类库。

VotingClassifier:用于分类的投票组合模型方法库。

步骤2 数据审查预处理函数。

基本状态查看,查看数据集后2条数据、数据类型、描述性统计。

def set_summary(df):
    '''
查看数据集后2条数据、数据类型、描述性统计
    :param df: 数据框
    :return: 无
    '''
    print ('{:*^60}'.format('Data overview:'))
    print (df.tail(2))  # 打印原始数据后2条
    print ('{:*^60}'.format('Data dtypes:'))
    print (df.dtypes)  # 打印数据类型
    print ('{:*^60}'.format('Data DESC:'))
    print (df.describe().round(2).T)  # 打印原始数据基本描述性信息

本函数主要功能定义如下:

使用tail(2)方法预览数据集最后2条数据,与head(2)方法相对应。

使用dtypes打印输出所有列数据类型

通过describe().round(2).T做描述性统计,并保留2位小数,最后做数据转置(目的是便于查看)。

缺失值审查,查看数据集的缺失数据列、行记录数

def na_summary(df):
    '''
查看数据集的缺失数据列、行记录数
    :param df: 数据框
    :return: 无
    '''
    na_cols = df.isnull().any(axis=0)  # 查看每一列是否具有缺失值
    print ('{:*^60}'.format('NA Cols:'))
    print (na_cols)  # 查看具有缺失值的列
    na_lines = df.isnull().any(axis=1)  # 查看每一行是否具有缺失值
    print ('Total number of NA lines is: {0}'.format(na_lines.sum()))  # 查看具有缺失值的行总记录数

本函数主要功能定义如下:

isnull().any()查看指定轴是否具有缺失值,axis=0指定沿列查看,axis=1指定按行查看。

sum()对数据中所有为True的数据求和。

类样本均衡审查,查看每个类的样本量分布

def label_samples_summary(df):
    '''
查看每个类的样本量分布
    :param df: 数据框
    :return: 无
    '''
    print ('{:*^60}'.format('Labesl samples count:'))
    print (df.ix[:, 1].groupby(df.ix[:, -1]).count())

本函数主要功能定义如下:

df.ix[:,1].groupby(df.ix[:,-1]).count()使用.ix方法指定以最后一列为维度,对第一列做计数统计。

字符串分类转整数分类,用于将分类变量中的字符串转换为数值索引分类,这是本书新提到的用于预处理的函数。在很多情况下,数据库中存储的是字符串型的数据(例如性别是M/F),为了能够进行矩阵计算,需要把这些字符串型的分类变量转换为数值型分类变量(例如将M/F转换为0/1)。

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

本函数的功能定义略显复杂,主要包括以下过程:

首先定义要转换的数据,通过convert_cols定义转换的列名,并在此基础上新建转换数据集final_convert_matrix。

接下来获取总记录数,便于按行做循环。建立一个空列表用于存储字符串与对应索引的字典。由于我们要使用sklearn的DictVectorizer方法做转换,它要求转换对象是一个由字典组成的列表,字典的键值对是字符串及其对应的数字映射,我们的目的是从唯一字符集中取出其value和对应的index作为该键值对的组合。

通过training做条件判断,然后分别应用不同计算逻辑。训练阶段比预测集的应用阶段主要多了两部分内容:唯一值的列表,训练好的DictVectorizer对象。

当处于训练阶段时(training==True),先建立unique_list,用于存储每个列的唯一值列表。

再使用for循环读取每个列名,单独获取该列数据并使用unique方法获取唯一值,由于该唯一值的结果是一个数组,因此需要使用tolist方法转换为列表,便于后期应用。最后将每个列的唯一值列表追加到总的唯一值列表中。

获得所有列的唯一值组合后,下面开始将每条记录的具体值与其在唯一值列表中的索引做映射。例如唯一值结果是['1','2','3','4','5'],如果该列中有一个值为'4',那么需要将该字符串转换为其对应的索引值3(索引从0开始)。

由于转换是按行实现的,因此使用for循环读取每行索引,然后使用iloc方法获取对应行数据,该数据是一个Series格式的列表;下面要做的是将每个列表中的value映射到唯一值列表中的index。

使用一个for循环结合enumerate读取列表的每个值及对应索引,索引结合unique_list[each_index]用于从唯一值总列表中找到原始所处的列的唯一列表,值用于从unique_list[each_index]中匹配出值对应的索引,使用的是列表的index(value)方法,得到的索引再替换掉原始字符串数据。该子循环结束后,每条记录已经是转换为数值型分类的列表,使用dict结合zip方法将其与列名转换为字典。

上述循环完成后,我们已经在dict_list中存储了所有的数据记录,每天的数据记录以一个字典的形式存储,整个数据集是由字典组成的列表。

下面开始做转换。先建立DictVectorizer转换模型对象,这里设置了2个参数:sparse=False指定转换后的数据集是一个数组,否则默认为压缩后的稀疏矩阵,这样设置的原因是后续很多步骤和模型都不支持直接基于压缩后的稀疏矩阵做转换和建模;dtype=np.int64用于设置转换后的数据类型是整数型,否则默认是浮点型。使用fit方法做训练,使用tranform方法做转换应用。最后返回转换模型对象,唯一值总列表和转换后的数据。

相关知识点:使用DictVectorizer将字符串映射为指定数值变量

在之前的章节我们提到过使用OneHotEncoder方法将数值型变量转换为二值化的标志变量,DictVectorizer可以看作是OneHotEncoder方法的上游步骤,它可以基于用户指定的字符串和任意数值的映射列表(通过字典的形式做映射),对字符串做转换。

假如现在有一个数组,如表6-12所示。

表6-12 原始字符串数据

我们可以将每条记录转换为字典的形式(参照本节的方法),上述数据可以表示为:[{'sex':1,'edu':1},{'sex':1,'edu':10},{'sex':2,'edu':5}]

上述每个变量的唯一值所对应的数值可以任意指定,例如education中的University可以设置为1、1000、1312等任意数字。这也是这种指定方法的灵活性所在,在本案例中我们设置为值所在列表的索引。

使用DictVectorizer的转换具体过程为:

from sklearn.feature_extraction import DictVectorizer
model_dv = DictVectorizer(sparse=False)
data =[{'sex':1,'edu':1},{'sex':1,'edu':10},{'sex':2,'edu':5}]
data_new = model_dv.fit_transform(data)
print (model_dv.feature_names_)
print (data_new)

上述输出结果为:

['edu', 'sex']
[[  1.   1.]
 [ 10.   1.]
 [  5.   2.]]

当处于预测阶段时(training==False),由于在训练阶段已经获取了唯一值总列表以及转换模型对象,这里只需要做转换即可。整个过程中,相比于训练阶段主要少了两个步骤:

无需再次计算唯一值总列表。

无需对DictVectorizer模型对象应用fit方法,在tranform时,直接使用训练阶段的对象。

时间属性拓展,用于将日期和时间数据拓展出其他属性,例如星期几、周几、小时、分钟等。由于纯时间数据无法作为有效数据集参与分类模型计算,因此这里将其转换出多种属性。

def datetime2int(set):
    '''
将日期和时间数据拓展出其他属性,例如星期几、周几、小时、分钟等。
    :param set: 数据集
    :return: 拓展后的属性矩阵
    '''
    date_set = map(lambda dates: pd.datetime.strptime(dates, '%Y-%m-%d'),
                  set['order_date'])  # 将set中的order_date列转换为特定日期格式
    weekday_data = map(lambda data: data.weekday(), date_set)  # 周几
    daysinmonth_data = map(lambda data: data.day, date_set)  # 当月几号
    month_data = map(lambda data: data.month, date_set)  # 月份

    time_set = map(lambda times: pd.datetime.strptime(times, '%H:%M:%S'),
                set['order_time'])  # 将set中的order_time列转换为特定时间格式
    second_data = map(lambda data: data.second, time_set)  # 秒
    minute_data = map(lambda data: data.minute, time_set)  # 分钟
    hour_data = map(lambda data: data.hour, time_set)  # 小时

    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

本函数的功能是使用map配合lambda为指定的数据集应用特定功能,具体包括以下过程:

先通过date_set=map(lambda dates:pd.datetime.strptime(dates,'%Y-%m-%d'),set['order_date'])将数据集中的order_date字符串转换为指定日期格式的数据。有关日期格式的转换,我们之前已经分别介绍了在导入数据时先定义一个匿名函数date_parse=lambda dates:pd.datetime.strptime(dates,'%m-%d-%Y'),再配合导入参数date_parser=date_parse做解析的方法以及直接使用pd.to_datetime()为指定列做转换的方法。这里介绍的方法应用格式类似,只是用了不同的实现过程。接下来分别通过weekday()、day、month获取转换后日期数据的周几、当月几号和月份。

再使用相同的方法对数据集中的order_time类做转换,然后分别应用second、minute、hour获得其秒、分钟、小时数据。

上述两个转换过程完成后,通过列表的extend方法批量添加到一个空列表中,并转换为数组后再转置,最后返回该数组。

相关知识点:使用map配合lambda实现自定义功能应用

lambda匿名函数在4.6节中已经介绍过,在此重点介绍map函数。

map是Python内置的标准函数,它的作用是为一个序列对象的每个元素应用指定的函数功能,然后返回一个列表。语法:map(function,sequence[,sequence,...])

其中:

function可以是任意函数,一般情况下是针对单个元素实现的具体映射功能。

sequence是要应用的列表。

map()函数的特点是小巧灵活,再配合lambda可以应用到很多小型的迭代过程中,例如日期格式转换、大小写转换、对每个元素做切分、数值计算等以个体为单位的功能中。

样本均衡,使用SMOTE方法对不均衡样本做过抽样处理。

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

本函数的定义过程比较简单:先建立SMOTE()模型对象,然后使用其fit_sample方法做训练后过抽样处理,最后返回处理后的数据集。

步骤3 读取数据,从这里开始进入到正式的数据应用过程。

# 定义特殊字段数据格式
dtypes = {'order_id': np.object,
          'pro_id': np.object,
          'use_id': np.object}
raw_data = pd.read_table('abnormal_orders.txt', delimiter=',', dtype=dtypes)  # 读取数据集

定义的dtypes用来为特定列指定数据类型为字符串型,该列表在训练数据和预测数据读取时都会用到。使用Pandas的read_table方法读取训练集并指定分隔符和指定列的数据类型。

步骤4 数据审查。

基本状态查看,执行set_summary(raw_data),从返回的结果中分析:

通过基本状态输出的概览,未发现数据有录入异常。

***********************Data overview:***********************
          order_id  order_date order_time     cat attribution      pro_id  \
134188  4285770012  2013-09-19   23:55:06     家居日用              GO  1000335947
134189  4285770056  2013-05-20   23:58:59     生活电器和厨卫电器      GO  1000009280
       pro_brand  total_money  total_quantity order_source pay_type  \
134188       炊大师         79.0               1           抢购合并支付
134189        海尔         799.0               1           抢购合并支付
            use_id city  abnormal_label  
134188      shukun  东莞市               0  
134189  544975322_  海口市               0  

通过数据类型输出,发现我们指定的几个列以及其他包含有字符串的列都被识别为字符串型,其他类型识别正常。

************************Data dtypes:************************
order_id           object
order_date         object
order_time         object
cat                object
attribution        object
pro_id             object
pro_brand          object
total_money       float64
total_quantity      int64
order_source       object
pay_type           object
use_id             object
city               object
abnormal_label      int64
dtype: object

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

*************************Data DESC:*************************
                   count    mean      std  min   25%   50%    75%       max
total_money     134189.0  660.11  2901.21  0.5  29.0  98.4  372.0  766000.0
total_quantity  134190.0    1.20     3.23  1.0   1.0   1.0    1.0    1000.0
abnormal_label  134190.0    0.21     0.41  0.0   0.0   0.0    0.0       1.0 

在使用describe做描述性统计时,默认情况下,只针对数值型数据做统计,如果要对全部数据做统计,那么需要使用describe(include='all')。

缺失值审查,执行na_summary(raw_data),从返回的结果中分析有三列值存在缺失,总的缺失记录为1429,占总体样本量的1%(1429/134189)左右。

**************************NA Cols:**************************
order_id          False
order_date        False
order_time        False
cat                True
attribution       False
pro_id            False
pro_brand          True
total_money        True
total_quantity    False
order_source      False
pay_type          False
use_id            False
city               True
abnormal_label    False
dtype: bool
Total number of NA lines is: 1429

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

*******************Labesl samples count:********************
abnormal_label
0    105733
1     28457
Name: order_date, dtype: int64

步骤5 数据预处理,经过上面的基本分析,我们对特定问题基本有初步的判断,该步骤着手进行处理。

drop_na_set = raw_data.dropna()  # 丢弃带有NA值的数据行
X_raw = drop_na_set.ix[:, 1:-1]  # 分割输入变量X,并丢弃订单ID列和最后一列目标变量
y_raw = drop_na_set.ix[:, -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基础上,使用ix方法获取最后一列数据,形成目标数据集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。

步骤6 组合分类模型交叉检验,这是本节内容的核心。

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)  # 模型训练

建立多个分类模型对象。通过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。

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

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

通过cross_val_score方法做交叉检验模型,设置的参数分别为上述的组合分类模型器和交叉检验方法。打印输出每次交叉检验的结果以及均值如下:

*********************Cross val socres:**********************
[ 0.77178407  0.91971669  0.97473201  0.9724732   0.92316233  0.90960257
  0.91006203  0.91538403]
Mean scores is: 0.91

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

在检验之后,我们直接对组合分类模型器model_vot应用fit方法做模型训练,形成可用于预测的模型对象。

步骤7 对新数据集做预测,该部分跟训练集时的思路相同,仅少了分割目标变量y和样本均衡两个环节,在此不做赘述。

X_raw_data = pd.read_csv('new_abnormal_orders.csv', dtype=dtypes)  # 读取要预测的数据集
X_raw_new = X_raw_data.ix[:, 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)  # 打印预测值。

最后使用model_vot的predict方法做预测并打印结果值如下:

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

6.8.5 案例数据结论

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

6.8.6 案例应用和部署

对于该类型的案例,通常情况下的应用分类有两种情况:

(1)异常订单的分析

包括异常订单的主要特征、品类集中度、重点客户等,尤其是可以将异常订单联系人加入黑名单,以降低其对公司正常运营的干扰。

(2)订单的实时检测

订单实时检测后会将结果为异常的订单记录发送到审核部门,然后由审核部门做进一步审查。这时需要额外做两部分工作:

一部分是将训练过程与预测过程分离,目的是训练阶段与预测阶段形成两个并行进程。实时检测的前端属于预测阶段的实时调用,仅应用本案例的步骤7,这个过程中还有一步非常重要的内容是将训练过程中用到的模型对象(包括DictVectorizer对象和VotingClassifier对象等)持久化保存到服务器硬盘,这样做的好处是在预测应用调用不同的模型对象时需一次引用,多次调用,将功能分离的同时又会大大提高运行效率。

一部分是在训练阶段做增量更新,而不必每次都跑所有数据,这样也能提高实时运算效率。

限于篇幅,本书暂时不过多介绍订单的实时检测中提到的两部分内容。

6.8.7 案例注意点

在本案例中有几个关键点需要读者注意:

(1)关于耗时

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

训练阶段的字符串分类转整数型分类str2int,该过程需要对矩阵中的每个值做映射。

训练阶段的交叉检验,该过程由于是8折交叉检验并且里面有2个集成方法,再加上使用组合投票的分类器的应用,导致整个交叉检验耗时较长。

因此,如果读者更侧重于效率的话,那么这种基于组合投票以及集成分类方法的实现思路将不是优先选择。

(2)关于输入特征变量

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

(3)关于样本均衡

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

6.8.8 案例引申思考

(1)更高的模型准确率或更低的模型误差

本案例的核心建模思路是通过组合多个单一或集成模型器的方法,实现更高层次的手动集成分类模型。我们在6.7节中提到了自动参数优化方法,该方法可以与本案例的组合模型器相结合,这样会进一步提高模型效果。另外,我们在5.8节也介绍过通过交叉检验得到不同参数下的模型效果值,并手动做最优参数配置的方法。这三种方法,尤其是前两种在追求高准确率或低误差的应用场景中非常有效。

(2)有关二值标志转换的问题

虽然本案例中没有用到二值标志转换操作,但由于用到了两个具有特殊意义的字段pro_id和、use_id,在此讨论下如果使用了这类唯一性特别强或分类特别多的变量,会遇到哪些问题。这类字段通常会产生海量的唯一值,如果对其做二值化标志转换,那么会出现两个主要问题:

第一,海量的商品ID和用户ID会使得数据稀疏特征非常明显,数据将主要受到这两个特征所转换的海量特征的影响,而使得整个数据集的类别划分效果变得非常差。

第二,如果通过现有的二值化转换方法OneHotEncoder来得到的数组结果,在面临海量唯一值时会报错"ValueError:array is too big;`arr.size*arr.dtype.itemsize`is larger than the maximum possible size.",原因是:

在32位Python版本下由于系统限制,即使内存再多,Python进程最多只能占用2G内存(如果使用IMAGE_FILE_LARGE_ADDRESS_AWARE方法将二值化结果做压缩,最多也只能占用4G内存,实际上大多数32位Python都没有做该设置)。

即使在64位Python版本下,Numpy(sklearn基于numpy)的最大数据类型是complex128,其最大可占用的内存空间大约有5G左右,而ID对象的唯一值可能有无限大,例如将销售周期拉长,会发现销售SKU能达到几千万甚至上亿。这种有限的对象长度将无法承载无限增长的ID。

当然,可能有的读者会想到通过二值化转换形成压缩稀疏矩阵,默认的OneHot-Encoder方法也是提供基于csc(一种稀疏矩阵压缩方式)压缩稀疏矩阵,scipy.sparse也提供了相关方法可以基于压缩稀疏矩阵做数据合并等处理。这种想法没错,但是大多数的数据处理的输入都要求是数组或矩阵,转换为数组几乎是建模的前置条件,压缩矩阵目前则完全行不通。

(3)字符串分类转整数型分类

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

(4)有关数据集中的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值以特定值填充转换是一种变通思路。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文