案例-基于自动K值的KMeans广告效果聚类分析

案例背景

某企业由于投放的广告渠道比较多,需要对其做广告效果分析以实现有针对性的广告效果测量和优化工作。跟以应用为目的的案例不同的是,由于本案例是一个分析型案例,该过程的输出其实是不固定的,因此需要跟业务运营方具体沟通需求。

以下是在开展研究之前的基本预设条件:

  • 广告渠道的范畴是什么?具体包括哪些渠道?——所有站外标记的广告类渠道(以ad_开头)。
  • 数据集时间选择哪个时间段?——最近90天的数据。
  • 数据集选择哪些维度和指标?——渠道代号、日均UV、平均注册率、平均搜索量、访问深度、平均停留时间、订单转化率、投放总时间、素材类型、广告类型、合作方式、广告尺寸、广告卖点。
  • 专题分析要解决什么问题?——将广告分类并找出其重点特征,为接下来的业务讨论和数据分析提供支持。

明确了上述具体需求后,下面开始案例的主要工作部分。本节案例的输入源数据ad_performance.txt和源代码chapter7_code2.py位于“附件-chapter7”中,默认工作目录为“附件-chapter7”(如果不是,请cd切换到该目录下,否则会报“IOError: File ad_performance.txt does not exist”)。程序的输出为不同聚类类别的详细信息数据以及雷达图。

7.11.2 案例主要应用技术

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

  • 数据预处理:数据标准化、字符串分类转整数型分类
  • 数据建模:KMeans聚类算法
  • 数据展示:使用matplotlib输出雷达图

主要用到的库包括:sys、numpy、pandas、sklearn、matplotlib,其中sklearn是展示数据的核心。

KMeans聚类在“4.1 聚类分析”已经介绍过,本案例的重点有两个:

一是如何基于最优数据尺度确定最佳K值,该技术实现的思路是:最优佳的聚类类别划分从数据特征上看是类内距离最小化的同时类间的距离最大化,可以使用平均轮廓系数作为指标评估,通过枚举每个K计算平均轮廓系数并得到最优值。

二是通过极坐标系的设置方式输出雷达图。

7.11.3 案例数据

案例数据来自某企业的营销部门数据,该数据基于营销数据、网站分析系统数据和运营系统数据总结而来。以下是数据概况:

  • 维度数:除了渠道唯一标记之外,共12个维度
  • 数据记录数:889
  • 是否有NA值:有
  • 是否有异常值:有

以下是本数据集的13个字段的详细说明:

  • 渠道代号:业务方统一命名规划的唯一渠道标志
  • 日均UV :每天的平均独立访客,从一个渠道中带来的一个访客即使一天中到达多次都统计为1次
  • 平均注册率:日均注册的用户数量/平均每天的访问量
  • 平均搜索量:平均每个访问的搜索次数
  • 访问深度:总页面浏览量/平均每天的访问量
  • 平均停留时间:总停留时间/平均每天的访问量
  • 订单转化率:总订单数量/平均每天的访问量
  • 投放总时间:每个广告媒介在站外投放的天数
  • 素材类型:广告素材类型,包括jpg、gif、swf、sp
  • 广告类型:广告投放类型,包括banner、tips、横幅、通栏、暂停以及不确定(不知道到底是何种形式)
  • 合作方式:广告合作方式,包括roi、cpc、cpm和cpd
  • 广告尺寸:每个广告投放的尺寸大小,包括140*40、308*388、450*300、600*90、480*360、960*126、900*120、390*270
  • 广告卖点:广告素材上主要的卖点诉求信息,包括打折、满减、满赠、秒杀、直降、满返

7.11.4 案例过程

步骤1 导入库

 

  1. import sys
  2. reload(sys)
  3. sys.setdefaultencoding('utf-8')
  4. import pandas as pd
  5. import numpy as np
  6. from sklearn.feature_extraction import DictVectorizer # 字符串分类转整数分类库
  7. from sklearn.preprocessing import MinMaxScaler # MinMaxScaler库
  8. from sklearn.cluster import KMeans # KMeans模块
  9. from sklearn import metrics # 导入sklearn效果评估模块
  10. import matplotlib.pyplot as plt # 图形库

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

  • sys:系统库,用来将默认编码设置为UTF-8,目的是处理中文处理
  • numpy:基本数据处理
  • pandas:数据读取、审查、异常值处理等
  • DictVectorizer:用来将字符串分类转换为整数型分类
  • MinMaxScaler:数据标准化库,用来将数据做标准化处理
  • KMeans:K均值聚类算法模块
  • metrics:sklearn效果评估模块
  • pyplot:用来做雷达图展示

步骤2 读取数据,使用pandas的read_table方法读取txt文件,分隔符为TAB。

  1. raw_data = pd.read_table('ad_performance.txt', delimiter='\t')

步骤3 数据审查和校验,该步骤包括多个环节:查看前2条数据、查看数据类型分布、查看缺失值、查看数据描述性统计信息、相关性分析等。

使用head(2)方法查看前2条数据

  1. print ('{:*^60}'.format('Data overview:'))
  2. print (raw_data.head(2)) # 打印输出前2条数据

从返回结果看,原始数据能正常识别且没有异常信息,前2条数据如下:

 

  1. ***********************Data overview:***********************
  2. 渠道代号 日均UV 平均注册率 平均搜索量 访问深度 平均停留时间 订单转化率 投放总时间 素材类型 广告类型 \
  3. 0 A203 3.69 0.0071 0.0214 2.3071 419.77 0.0258 20.0 jpg banner
  4. 1 A387 178.70 0.0040 0.0324 2.0489 157.94 0.0030 19.0 jpg banner
  5. 合作方式 广告尺寸 广告卖点
  6. 0 roi 140*40 打折
  7. 1 cpc 140*40 满减

使用dtypes方法查看数据类型分布,由于行比较多,因此这里使用pd.DataFrame方法将其结果转换为数据框,然后通过.T做转置。

 

  1. print ('{:*^60}'.format('Data dtypes:'))
  2. print (pd.DataFrame(raw_data.dtypes).T) # 打印数据类型分布

从数据类型结果来看,各个字段类型都能别正确识别,结果如下:

 

  1. ************************Data dtypes:************************
  2. 渠道代号 日均UV 平均注册率 平均搜索量 访问深度 平均停留时间 订单转化率 投放总时间 \
  3. 0 object float64 float64 float64 float64 float64 float64 float64
  4. 素材类型 广告类型 合作方式 广告尺寸 广告卖点
  5. 0 object object object object object

使用isnull().sum()查看所有字段中具有缺失值数据的统计量,同样使用pd.DataFrame将结果转换为数据框,然后通过.T做转置。

 

  1. print ('{:*^60}'.format(' NA counts:'))
  2. print (pd.DataFrame(raw_data.isnull().sum()).T) # 查看缺失值情况

从结果中发现,在“平均停留时间”字段中有2个缺失值。结果如下:

 

  1. ************************ NA counts:*************************
  2. 渠道代号 日均UV 平均注册率 平均搜索量 访问深度 平均停留时间 订单转化率 投放总时间 素材类型 广告类型 合作方式 \
  3. 0 0 0 0 0 0 2 0 0 0 0 0
  4. 广告尺寸 广告卖点
  5. 0 0 0

使用describe()查看数据描述性统计信息,并使用round(2)保留2位小数,最后使用.T将其转置。

 

  1. print ('{:*^60}'.format('Data DESC:'))
  2. print (raw_data.describe().round(2).T) # 打印原始数据基本描述性信息

如下的描述性统计结果中,反映了3个信息点:

  • 日均UV的数据波动非常大,说明了不同渠道间的特征差异非常明显。
  • 平均停留时间的有效数据(非空数据)只有887,比其他数据少2条,这也印证了上述缺失值统计结果。
  • 平均注册率、平均搜索量、订单转化率的多个统计量(例如最小值、25%分位数等)都为0,看似数据不太正常。
  1. *************************Data DESC:*************************
  2. count mean std min 25% 50% 75% max
  3. 日均UV 889.0 540.85 1634.41 0.06 6.18 114.18 466.87 25294.77
  4. 平均注册率 889.0 0.00 0.00 0.00 0.00 0.00 0.00 0.04
  5. 平均搜索量 889.0 0.03 0.11 0.00 0.00 0.00 0.01 1.04
  6. 访问深度 889.0 2.17 3.80 1.00 1.39 1.79 2.22 98.98
  7. 平均停留时间 887.0 262.67 224.36 1.64 126.02 236.55 357.98 4450.83
  8. 订单转化率 889.0 0.00 0.01 0.00 0.00 0.00 0.00 0.22
  9. 投放总时间 889.0 16.05 8.51 1.00 9.00 16.00 24.00 30.00

以上三类异常点,经过跟业务方的沟通以及再次数据验证,其结果如下:

  • 日均UV的差异性问题:由于广告的流量型特征明显,很多广告的流量爆发明显,因此渠道间确实带有非常大的差异性,这些差异性应该保留,不能作为异常值处理。
  • 平均停留时间的缺失值,该字段由于统计缺失导致数据丢失,可以使用均值法做填充。
  • 平均注册率、平均搜索量、订单转化率等的多个字段为0的问题,这是由于在打印输出过程中仅保留了2位小数,而这几个统计量的数据本身就非常小,将其通过round(3)保留3位小数后就能正常显示。

使用corr()方法做相关性分析

  1. print ('{:*^60}'.format('Correlation analysis:'))
  2. print (raw_data.corr().round(2).T) # 打印原始数据相关性信息

通过相关性结果分析,12个特征中平均停留时间和访问深度的相关系数为0.72,这两个指标具有较高的相关性,但特征也不是非常明显;其他特征之间的相关性关系都不突出。

步骤4 数据预处理,本步骤主要涉及到缺失值替换、字符串分类转换为整数分类、数据标准化、数据合并等操作。

使用fillna方法将“平均停留时间”中的缺失值替换为均值

 

  1. data_fillna = raw_data.fillna(raw_data['平均停留时间'].mean()) # 用后面的值替换缺失值

字符串分类转整数分类

该内容在“6.8 案例-基于LogisticRegression、RandomForest、Bagging概率投票组合模型的异常检测”介绍过,主要区别在于这里由于没有“预测”环节,因此无需区分训练阶段和预测阶段。主要实现方法如下:

part1 定义要转换的数据。

 

  1. conver_cols = ['素材类型', '广告类型', '合作方式', '广告尺寸', '广告卖点']
  2. convert_matrix = data_fillna[conver_cols] # 获得要转换的数组
  3. lines = data_fillna.shape[0] # 获得总记录数
  4. dict_list = [] # 总空列表,用于存放字符串与对应索引组成的字典
  5. unique_list = [] # 总唯一值列表,用于存储每个列的唯一值列表
  • 通过convert_cols定义转换的列名,并在此基础上新建转换数据集convert_matrix。
  • 通过shape[0]获取总记录数,便于按行做循环。
  • 建立一个空列表dict_list用于存储字符串与对应索引的字典。由于我们要使用sklearn的DictVectorizer方法做转换,它要求转换对象是一个由字典组成的列表,字典的键值对是字符串及其对应的数字映射,我们的目的是从唯一字符集中取出其value和对应的index作为该键值对的组合。
  • 总空列表dict_list,用于存放字符串与对应索引组成的字典。
  • 总唯一值列表unique_list,用于存储每个列的唯一值列表。

part2使用for获得所有列的唯一值列表,然后存到dict_list中。

 

  1. for col_name in conver_cols:  # 循环读取每个列名
  2.     cols_unqiue_value = data_fillna[col_name].unique().tolist()  # 获取列的唯一值列表
  3.     unique_list.append(cols_unqiue_value)  # 将唯一值列表追加到总列表

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

part3使用for循环将每条记录的具体值跟其在唯一值列表中的索引做映射。

例如,唯一值结果集是['1','2','3','4','5'],如果该列中有一个值为'4',那么需要将该字符串转换为其对应的索引值3(注意索引从0开始)。

  1. for line_index in range(lines):  # 读取每行索引
  2.     each_record = convert_matrix.iloc[line_index]  # 获得每行数据,是一个Series
  3.     for each_index, each_data in enumerate(each_record):  # 读取Series每行对应的索引值
  4.         list_value = unique_list[each_index]  # 读取该行索引对应到总唯一值列表列索引下的数据(其实是相当于原来的列做了转置成了行,目的是查找唯一值在列表中的位置)
  5.         each_record[each_index] = list_value.index(each_data)  # 获得每个值对应到总唯一值列表中的索引
  6.     each_dict = dict(zip(conver_cols, each_record))  # 将每个值和对应的索引组合字典
  7.     dict_list.append(each_dict)  # 将字典追加到总列表

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

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

上述循环完成后,我们已经在dict_list中存储了所有的数据记录,列表内的每个元素是一个以唯一值和对应索引值组成的字典。

part4使用DictVectorizer将字符串转换为整数。

  1. model_dvtransform = DictVectorizer(sparse=False, dtype=np.int64) # 建立转换模型对象
  2. data_dictvec = model_dvtransform.fit_transform(dict_list) # 应用分类转换训练

先建立DictVectorizer转换模型对象model_dvtransform,这里设置了2个参数,sparse=False指定转换后的数据集是一个数组,否则默认会是压缩后的稀疏矩阵,这样设置的原因是后续很多步骤和模型都不支持直接基于压缩后的稀疏矩阵做转换和建模;dtype=np.int64用于设置转换后的数据类型是整数型,否则默认是浮点型。

对model_dvtransform对象使用fit_tranform方法做转换应用。这里可以分开使用fit,然后再使用tranform方法。

数据标准化。由于不同字段间存在数值的量纲差异,例如日均UV有几万的量级,而转化率的范围却是0-1之间,因此需要做数据标准化,这里使用的是Min-Max标准化方法。

  1. sacle_matrix = data_fillna.ix[:, 1:8] # 获得要转换的矩阵
  2. minmax_scaler = MinMaxScaler() # 建立MinMaxScaler模型对象
  3. data_scaled = minmax_scaler.fit_transform(sacle_matrix) # MinMaxScaler标准化处理

标准化的实施是针对数值型字段进行的,因此这里通过data_fillna.ix[:,1:8] 选择需要标准化的数据。下面的应用过程比较简单,是熟悉的sklearn的应用方法:使用MinMaxScaler()建立标准化模型对象,然后对模型对象应用fit_transform方法做标准化转换。

合并所有输入维度。在上面的预处理阶段,我们分别针对数值型和字符串型两类输入维度做处理,这里需要将其处理后的结果合并起来。

 

  1. X = np.hstack((data_scaled, data_dictvec))

使用numpy的hstack方法,将data_scaled、data_dictvec合并,形成最终输入X。

注意 这里的X不包含渠道的唯一标志列,因为该列没有实际建模意义。

步骤5 通过平均轮廓系数检验得到最佳KMeans聚类模型。

K值的确定一直是KMeans算法的关键,而由于KMeans是一个非监督式学习,因此没有所谓的“最佳”K值。但是,从数据本身的特征来讲,最佳K值对应的类别下应该是类内距离最小化并且类间距离最大化。有多个指标可以用来评估这种特征,比如平均轮廓系数、类内距离/类间距离等都可以做此类评估。基于这种思路,我们可以通过枚举法计每个K下的平均轮廓系数值,然后选出平均轮廓系数最大下的K值。

注意 即使在数据上聚类特征最明显,也并不意味着聚类结果就是有效的,因为这里的聚类结果用来分析使用,不同类别间需要具有明显的差异性特征并且类别间的样本量需要大体分布均衡。而确定最佳K值时却没有考虑到这些“业务性”因素。

 

  1. score_list = list()  # 用来存储每个K下模型的平局轮廓系数
  2. silhouette_int = -1  # 初始化的平均轮廓系数阀值
  3. for n_clusters in range(2, 10):  # 遍历从2到10几个有限组
  4.     model_kmeans = KMeans(n_clusters=n_clusters, random_state=0)  # 建立聚类模型对象
  5.     cluster_labels_tmp = model_kmeans.fit_predict(X)  # 训练聚类模型
  6.     silhouette_tmp = metrics.silhouette_score(X, cluster_labels_tmp)  # 得到每个K下的平均轮廓系数
  7.     if silhouette_tmp > silhouette_int:  # 如果平均轮廓系数更高
  8.         best_k = n_clusters  # 将最好的K存储下来
  9.         silhouette_int = silhouette_tmp  # 将最好的平均轮廓得分存储下来
  10.         best_kmeans = model_kmeans  # 将最好的模型存储下来
  11.         cluster_labels_k = cluster_labels_tmp  # 将最好的聚类标签存储下来
  12.     score_list.append([n_clusters, silhouette_tmp])  # 将每次K及其得分追加到列表
  13. print ('{:*^60}'.format('K value and silhouette summary:'))
  14. print (np.array(score_list))  # 打印输出所有K下的详细得分
  15. print ('Best K is:{0} with average silhouette of {1}'.format(best_k, silhouette_int.round(4)))

该步骤的主要实现过程如下:

定义初始变量score_list和silhouette_int。score_list用来存储每个K下模型的平局轮廓系数,方便在最终打印输出详细计算结果;silhouette_int的初始值设置为-1,每个K下计算得到的平均轮廓系数如果比该值大,则将其值赋值给silhouette_int。

提示 对于平均轮廓系数而言,其值域分布式[-1,1]。因此silhouette_int的初始值可以设置为-1或比-1更小的值。

使用for循环遍历每个K值,这里的K的范围确定为从2-10.一般而言,用于聚类分析的K值的确定不会太大。如果值太大,那么聚类效果可能不明显,因为大量信息的都会被分散到各个小类之中,会导致数据的碎片化。

通过KMeans(n_clusters=n_clusters, random_state=0)建立KMeans模型对象model_kmeans,设置聚类数为循环中得到的K值,设置固定的初始状态。

对model_kmeans使用fit_predict得到其训练集的聚类标签。该步骤其实无需通过predict获得标签,可以先使用fit方法对模型做训练,然后使用模型对象model_kmeans的label_属性获得其训练集的标签分类。

使用metrics.silhouette_score方法对数据集做平均轮廓系数得分检验,将其得分赋值给silhouette_tmp,输入参数有两个:

  • X:为原始输入的数组或矩阵
  • cluster_labels:训练集对应的聚类标签

接下来做判断,如果计算后的得分大于初始化变量的得分,那么:

  • 将最佳K值存储下来,便于后续输出展示
  • 将最好的平均轮廓得分存储下来,便于跟其他后续得分做比较以及输出展示
  • 将最好的模型存储下来,这样省去了后续再做最优模型下fit(训练)的工作
  • 将最好的聚类标签存储下来,这样方便下面将原始训练集与最终标签合并

每次循环结束后,将当次循环的K值以及对应的评论轮廓得分使用append方法追加到列表。

最后打印输出每个K值下详细信息以及最后K值和最优评论轮廓得分,返回数据如下:

  1. **************K value and silhouette summary:***************
  2. [[ 2. 0.46692821]
  3. [ 3. 0.54904646]
  4. [ 4. 0.56968547]
  5. [ 5. 0.48186604]
  6. [ 6. 0.45477667]
  7. [ 7. 0.48204261]
  8. [ 8. 0.50447223]
  9. [ 9. 0.52697493]]
  10. Best K is:4 with average silhouette of 0.5697

上述结果显示了不同K下的平均轮廓得分。就经验看,如果平均轮廓得分值小于0,意味着聚类效果不佳;如果值大约0且小于0.5,那么说明聚类效果一般;如果值大于0.5,则说明聚类效果比较好。本案例在K=4时,得分为0.5697,说明效果较好。

步骤6针对聚类结果的特征分析。

part1将原始数据与聚类标签整合

 

  1. cluster_labels = pd.DataFrame(cluster_labels_k, columns=['clusters']) # 获得训练集下的标签信息
  2. merge_data = pd.concat((data_fillna, cluster_labels), axis=1) # 将原始处理过的数据跟聚类标签整合

该步骤中先将最佳K值下的cluster_labels_k转换为数据框,然后使用pandas.concat将原始数据与标签合并,axis=1设置为按列合并。

在合并数据集时,这里没有使用数据建模时的X与cluster_labels合并,而是使用了经过缺失值填充后的data_fillna数组做合并,目的是为了在分析时能够再现不同特征下原始值的特征。如果使用X则其标准化的数值则不符合业务的实际数据分布范围,会导致数据难以理解。

part2计算每个聚类类别下的样本量和样本占比

 

  1. clustering_count = pd.DataFrame(merge_data['渠道代号'].groupby(merge_data['clusters']).count()).T.rename(
  2. {'渠道代号': 'counts'}) # 计算每个聚类类别的样本量
  3. clustering_ratio = (clustering_count / len(merge_data)).round(2).rename({'counts': 'percentage'}) # 计算每个聚类类别的样本量占比

在计算各聚类类别下的样本量时,主要的函数用法如下:

  • 使用merge_data['渠道代号'].groupby(merge_data['clusters']).count()以clusters为维度对渠道代号做计数统计,然后使用DataFrame方法将其转换为数据框。
  • 使用.T将分类汇总的数据框做转置操作。
  • 使用rename({'渠道代号':'counts'}将分类结果数据框的名称改为由渠道代号改为counts,方便之后合并时的信息提示。

在计算各聚类类别样本量占比时,主要的函数用法如下:

  • 使用len(merge_data)获得样本总量
  • 使用clustering_count/len(merge_data)获得各个类别与样本总量的百分比
  • 使用round(2)将保留两位小数
  • 使用rename({'counts':'percentage'}将名称由counts改为'percentage'

part3计算各个聚类类别内部最显著特征值

 

  1. cluster_features = []  # 空列表,用于存储最终合并后的所有特征信息
  2. for line in range(best_k):  # 读取每个类索引
  3.     label_data = merge_data[merge_data['clusters'] == line]  # 获得特定类的数据
  4.     part1_data = label_data.ix[:, 1:8]  # 获得数值型数据特征
  5.     part1_desc = part1_data.describe().round(3)  # 得到数值型特征的描述性统计信息
  6.     merge_data1 = part1_desc.ix[2, :]  # 得到数值型特征的均值
  7.     part2_data = label_data.ix[:, 8:-1]  # 获得字符串型数据特征
  8.     part2_desc = part2_data.describe(include='all')  # 获得字符串型数据特征的描述性统计信息
  9.     merge_data2 = part2_desc.ix[2, :]  # 获得字符串型数据特征的最频繁值
  10.     merge_line = pd.concat((merge_data1, merge_data2), axis=0)  # 将数值型和字符串型典型特征沿行合并
  11.     cluster_features.append(merge_line)  # 将每个类别下的数据特征追加到列表

建立一个空列表cluster_features,用于存储最终合并后的所有特征信息。

使用for循环读出最佳K下的每个聚类索引值。

使用merge_data[merge_data['clusters'] == line] 获得特定类的数据。

获得数值型特征值信息。使用label_data.ix[:,1:8]获得数值型数据特征;使用part1_data.describe().round(3)得到数值型特征的描述性统计信息,并保留3位小数;使用part1_desc.ix[2,:]得到得到描述性统计结果中的均值。该均值将作为各类的数值型数据的典型特征。

按照同样的方法,获得字符串型数据特征。在应用时有以下不同点:

  • ix[:,8:-1]的意思是从第8列到最后一列,而无需指定最后一列的具体索引值。
  • describe(include='all')中通过include='all'指定对所有类型的数据做描述性统计,而非仅仅是数值型数据。

提示 describe可针对数值型和字符串型数据做描述性统计。针对数值型数据的描述性统计字段包括计数、均值、标准差、最小值、25%分位数、50%分位数、75%分位数和最大值,当然也可以定义其他分位数,通过percentiles=[0.1,0.5,0.8]参数即可指定,但需要注意的是区间为0-1之间;针对字符串型的描述性统计由于没有数据可直接做计算,可以提供计数、唯一值数量、频数最多的字符串值及其频数这几个字段。

获得两种类型的典型数据特征后,使用pd.concat方法将数据合并,这里需要注意的是使用axis=0指定沿行合并而非列。使用cluster_features.append(merge_line)将每个类别下的数据特征追加到列表。

part4输出完整的类别特征信息。

 

  1. cluster_pd = pd.DataFrame(cluster_features).T # 将列表转化为矩阵
  2. print ('{:*^60}'.format('Detailed features for all clusters:'))
  3. all_cluster_set = pd.concat((clustering_count, clustering_ratio, cluster_pd), axis=0) # 将每个聚类类别的所有信息合并
  4. print (all_cluster_set)

该过程主要实现如下:

  • 使用DataFrame(cluster_features).T先将列表cluster_features转换为数据框然后做转置。
  • 使用concat((clustering_count,clustering_ratio,cluster_pd),axis=0)将类别样本量统计、样本量占比以及各字段典型特征合并起来。

打印输出如下结果:

 

  1. ************Detailed features for all clusters:*************
  2. clusters 0 1 2 3
  3. counts 411 297 27 154
  4. percentage 0.46 0.33 0.03 0.17
  5. 日均UV 1369.81 1194.69 1263.03 2718.7
  6. 平均注册率 0.003 0.003 0.003 0.005
  7. 平均搜索量 0.082 0.144 0.151 0.051
  8. 访问深度 0.918 5.728 9.8 0.948
  9. 平均停留时间 165.094 285.992 374.689 104.14
  10. 订单转化率 0.009 0.016 0.017 0.007
  11. 投放总时间 8.462 8.57 7.996 8.569
  12. 素材类型 swf jpg swf jpg
  13. 广告类型 不确定 不确定 通栏 banner
  14. 合作方式 cpc cpc cpc cpc
  15. 广告尺寸 600*90 600*90 900*120 308*388
  16. 广告卖点 打折 直降 打折 满减

步骤7各类别显著数值特征对比。

从上面的各类别特征中可能很难直观发现不同类别的显著性特征,这里通过雷达图的形式对各个聚类类别的数值型特征做对比展示。

part1各类别数据预处理,由于不同特征的数据量级差异很大,因此需要先对其做标准化处理,这样才能使得不同数据间具有对比的可能性。

 

  1. num_sets = cluster_pd.ix[:6, :].T.astype(np.float64) # 获取要展示的数据
  2. num_sets_max_min = minmax_scaler.fit_transform(num_sets) # 获得标准化后的数据

先通过cluster_pd.ix[:6,:].T.astype(np.float64)获取要展示的数据,其中:

  • ix[:6,:]代表获取前6列数据
  • T表示对其做转置
  • astype(np.float64)表示将数据转换为数值型

使用在数据预处理阶段建立的min-max对象minmax_scaler的fit_transform方法将数据做0-1标准化处理。

part2画布基本设置

 

  1. fig = plt.figure() # 建立画布
  2. ax = fig.add_subplot(111, polar=True) # 增加子网格,注意polar参数
  3. labels = np.array(merge_data1.index[:-1]) # 设置要展示的数据标签
  4. cor_list = ['r', 'g', 'b', 'y'] # 定义不同类别的颜色
  5. angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False) # 计算各个区间的角度
  6. angles = np.concatenate((angles, [angles[0]])) # 建立相同首尾字段以便于闭合

先使用plt.figure()创建一个画布对象fig,该画布对象会在下面用于增加子网格对象。

使用fig.add_subplot(111, polar=True)为fig对象增加一个子网格,其中参数polar=True用来设置该子网格对象显示极坐标系。

np.array(merge_data1.index[:-1])设置要展示的数据标签,标签从merge_data1的索引中获取。

定义不同类别的颜色列表cor_list,分别代表红色、绿色、蓝色、黄色。

通过np.linspace(0, 2*np.pi, len(labels), endpoint=False) 计算各个区间的角度,由于要在雷达图中显示多个类别,这里需要按照类别将整个“圆”按照类别数平均划分。其中各个参数如下:

  • 0:创建间隔区间的起始
  • 2*np.pi:创建间隔区间的末尾
  • len(labels):间隔长度
  • endpoint:值设置False代表间隔的最后一个值不是间隔区间的末尾

最后使用angles = np.concatenate((angles, [angles[0]]))建立相同首尾字段以便于闭合。默认情况下,每个类别的多个特征在雷达图中首尾应该是相连的,这样才能形成闭合效果,因此这里将第一个只追加到列表末尾。

提示 np.concatenate是实现数组合并的常用方法,之前我们经常使用的数组合并方法是hstack和vstack分别用于按列合并、按含合并,也可以使用dstack按第三个轴合并。concatenate方法则相当于是这些合并功能的“合体”,可以通过axis来指定合并的轴。

相关知识点:极坐标系

极坐标系是指在平面内由极点、极轴和极径组成的坐标系。以图7-21为例,图中在平面上取定一点O,称为极点;从O出发引一条射线OX,称为极轴;对于该空间中的任意一点M,其中ρ为M到O长度,称为极径。OM与OX的夹角称为θ。空间中任意一个点,都可以通过(ρ,θ)的有序对的形式表示,而这样建立起来的坐标系叫做极坐标系。

python雷达图

极坐标系是一个二维坐标系,与我们日常应用的另外一个坐标系——平面直角坐标系(也称为笛卡尔坐标系)可以相互转换。对于图7-21中的M (ρ,θ))而言,对应到平面直接坐标系中的x和y分别为:

x=ρcosθ

y=ρsinθ

当X不等于0时,上述公式可以转换为

θ=arctan(y/x)

在x=0的情况下:如果y为整数,那么θ=90°;如果y为负数,那么θ=270°。

提示 极坐标轴在很多场景下都会用到,例如使用opencv做图像处理时,就会涉及到极坐标和平面直角坐标的转换问题。

part3画雷达图

 

  1. for i in range(len(num_sets)):  # 循环每个类别
  2.     data_tmp = num_sets_max_min[i, :]  # 获得对应类数据
  3.     data = np.concatenate((data_tmp, [data_tmp[0]]))  # 建立相同首尾字段以便于闭合
  4.     ax.plot(angles, data, 'o-', c=cor_list[i], label=i)  # 画线

该过程通过for循环遍历每个类别索引,然后使用num_sets_max_min[i,:]获得对应类数据,再通过np.concatenate((data_tmp, [data_tmp[0]]))建立相同首尾字段以便于闭合,最后通过ax.plot(angles, data, 'o-', c=cor_list[i],label=i)画出雷达图,其中的参数:

  • angles:“X”轴数据
  • data:“Y”轴数据
  • 'o-':线条样式,直线并在交点中显示圆
  • c=cor_list[i]:设置颜色
  • label=i:设置每个类别的标识索引

part4设置图像显示格式

 

  1. ax.set_thetagrids(angles * 180 / np.pi, labels, fontproperties="SimHei") # 设置极坐标轴
  2. ax.set_title("各聚类类别显著特征对比", fontproperties="SimHei") # 设置标题放置
  3. ax.set_rlim(-0.2, 1.2) # 设置坐标轴尺度范围
  4. plt.legend(loc=0) # 设置图例位置
  5. plt.show() # 展示图像

使用ax.set_thetagrids(angles * 180/np.pi, labels, fontproperties="SimHei")设置极坐标系,其中:

  • angles * 180/np.pi:代表角度
  • labels:极坐标轴的标签
  • fontproperties:设置字体为黑体(SimHei)

通过ax.set_title()设置图像标题

通过ax.set_rlim(-0.2,1.2)设置坐标轴尺度范围,方便显示在各类别极值下的显示效果

通过plt.legend(loc=0)设置图像位置为自动,最后显示图像如下:

python雷达图

7.11.5 案例数据结论

初步分析

从案例结果来看,所有的渠道被分为4个类别,每个类别的样本量分别为411、297、27、154,对应的占比分别为46%、33%、3%、17%。第三个类别样本量较少。

结合图7-22的雷达图,可以清晰的看到不同类别广告媒体的特征倾向:

聚类1(索引值为0):各方面的特征都不明显,换句话说就是效果比较平庸,没有明显的优势或短板。但这些“中庸”的广告媒体却构成了整个广告的主体。

聚类2(索引值为1):这类广告媒体在访问深度、平均停留时间、订单转化率以及平均搜索量等流量质量的特征上的表现较好,除了注册转化率较低外,该类渠道各方面比较均衡。更重要的是该类媒体的数量占据了33%的数量,因此是一类规模较大且综合效果较好的媒体。

聚类3(索引值为2):这类广告媒体跟聚类2非常类似,并且相对聚类2的典型特征表现更好,但综合其只有3%的媒体数量,属于少量的“精英”类渠道。

聚类4(索引值为3):这类渠道跟其他几类渠道有个明显的特征区隔,其日均UV和平均注册率非常突出,证明这是一类“引流”+“拉新”的渠道;而其他的流量质量方面的表现却比较差。

深入分析

综合初步分析的结果,再结合输出各渠道详细数据:

聚类1的广告渠道各方面表现均比较一般,因此需要业务部门重点考虑其投放的实际价值。

聚类2的广告渠道的短板是日均UV和平均注册率,因此该类媒体无法为企业带来大量的流量以及新用户。这类广告的特质适合用户转化,尤其是有关订单转化的提升。

聚类3的广告渠道跟聚类2的特质非常类似,也适合做订单转化的提升,并且由于其实际效果更好,因此效果会更佳明显。

聚类4的广告渠道更佳符合广告本身“广而告之”的基础诉求,因此适合在大规模的广告宣传和引流时使用,尤其对于新用户的注册转化上的效果非常明显,也适合“拉新”使用。

7.11.6 案例应用和部署

对于以上四类广告媒体,需要针对其不同的特征做针对性的运营应用:

对于聚类1的广告媒体,在资金不足或优化广告结构时,重点考虑其其取舍问题。

对于聚类2和聚类3的广告媒体,优化应用的方向有两个:

  • 一是增加对于注册转化效果的优化效应,重点从人群匹配、注册引导、注册激励等方面加强运营。在该过程中,打折、直降等优惠宣传点可以重点突出,广告素材本身以较大中等广告为主,例如900*120。
  • 二是对于整体广告效果的支撑价值。考虑到其流量规模的局限性,这类渠道更加适合在广告结构中作为一类具有流量质量支撑价值的角色,可以用来提升全局广告的订单质量效果,因此是一类非常关键的质量渠道。尤其是聚类3的广告媒体其质量指标表现非常优异,应该优先考虑投放组合。

对于聚类4的广告媒体,其可以作为在每次大型促销活动时做引流的骨干,因为这更符合广告本身在全部推给媒体中的角色定位。对于这类媒体的单位流量成本需要重点关注,实际投放过程过程中可以满减为促销点、通过较大尺寸(如308*338)的广告类型做引导流量点击。

提示 广告流量数量和质量是广告效果的“两条腿”,虽然大多数情况下广告更侧重于流量数量方面,但对于质量如果能够兼顾则可以锦上添花。因此对于广告结构中两类不同倾向的媒体,具体如何组织和结构调整,更多的要看营销中心自身的核心目标定位。对于广告效果不好的媒体(例如聚类1),也不一定就是效果不好就要切掉,这里面可能涉及到多方的利益关联问题。

我们知道,聚类作为数据探索的初步阶段,还可以为后期的深入分析提供方向,相关的深入分析方向包括:

  • 通过分类方法基于不同的类别划分,找到各自类别内的显著特征,而非描述性统计特征,这些特征会成为进一步落地动作确认的基础。
  • 结合特定的广告运营目标并将其作为目标变量,通过分类方法找到各个广告渠道对于目标转化与否的重要性影响程度。例如平均注册率、订单转化率等。
  • 结合不同类别的订单详细信息,将其购买的商品数据提取出来,分析其平均订单金额、购买频率、商品购买数量,以实现对不同类别渠道购买力、消费潜力的评估。
  • 将广告成本与广告效果评估指标结合起来,通过回归方法预测不同成本下能够达成的广告效果,可以作为广告媒体策划的基础。

7.11.7 案例注意点

本案例有以下内容需要读者注意:

  • 异常值对于聚类效果的影响是显著的,但是广告类媒体的一个典型特征就是流量规模差异非常大,因此对于这类的异常值则不能轻易去除,因此异常值属于“正常现象”。
  • 在做描述性统计时,一般情况下会将数值型和字符串型的字段分开做统计。如果放在一起统计,虽然数据是合并到一起的,但由于两种类型的数据结果字段差异非常大,会导致很多字段为空,实际效果并不直观。
  • 在做聚类之前的数据标准化方法选择上,由于数据中涉及到异常值的存在,因此使用的是可以兼顾异常值的处理方法,具体方法我们在“3.1 数据清洗:缺失值、异常值和重复值的处理”的代码实操里面详细介绍过。
  • matplolib中如果要显示中文,需要设置对应的中文字体名称,否则无法正常显示中文。

7.11.8 案例引申思考

本案例中通过平均轮廓系数的方法得到的最佳K值不一定在业务上具有明显的解读和应用价值。如果最佳K值的解读无效怎么办?有两种思路:

  • 扩大K值范围,例如将K的范围调整为[2,12],然后再次运算看更大范围内得到的K值是否更加有效并且能符合业务解读和应用需求。
  • 得到平均轮廓系数“次要好”(而不是最好)的K值,再对其结果做分析。

对于不同类别的典型特征的对比,除了使用雷达图直观的显示外,还可以使用多个柱形图的形式,将每个类别对应特征的值做柱形图统计,这样也是一个非常直观的对比方法。具体参考下图:

KMeans聚类指标评估


====================【好书推荐,我为自己代盐】====================

Python数据分析与数据化运营上市啦!

50+数据流工作知识点
14个数据分析与挖掘主题
8个综合性运营分析案例
涵盖会员、商品、流量、内容4大主题
360°把脉运营问题并贴合数据场景落地



发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>