在这个项目中,你将分析一个数据集的内在结构,这个数据集包含很多客户真对不同类型产品的年度采购额(用金额表示)。这个项目的任务之一是如何最好地描述一个批发商不同种类顾客之间的差异。这样做将能够使得批发商能够更好的组织他们的物流服务以满足每个客户的需求。
这个项目的数据集能够在UCI机器学习信息库中找到.因为这个项目的目的,分析将不会包括 ‘Channel’ 和 ‘Region’ 这两个特征——重点集中在6个记录的客户购买的产品类别上。
运行下面的的代码单元以载入整个客户数据集和一些这个项目需要的 Python 库。如果你的数据集载入成功,你将看到后面输出数据集的大小。
# 检查python版本
from sys import version_info
if version_info.major != 3:
raise Exception('请使用Python 3.x 来完成此项目')
# 引入项目所需要的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import visuals as vs
from IPython.display import display # 使得我们可以对DataFrame使用display()函数
# 设置以内联的形式显示matplotlib绘制的图片(在notebook中显示更美观)
%matplotlib inline
# 高分辨率显示
# %config InlineBackend.figure_format='retina'
# 载入整个客户数据集
try:
data = pd.read_csv("customers.csv")
data.drop(['Region', 'Channel'], axis=1, inplace=True)
print("Whole sale customers dataset has {} samples with {} features each.".format(*data.shape))
except:
print("Dataset could not be loaded. Is the dataset missing?")
Whole sale customers dataset has 440 samples with 6 features each.
display(data.head())
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
0 | 12669 | 9656 | 7561 | 214 | 2674 | 1338 |
1 | 7057 | 9810 | 9568 | 1762 | 3293 | 1776 |
2 | 6353 | 8808 | 7684 | 2405 | 3516 | 7844 |
3 | 13265 | 1196 | 4221 | 6404 | 507 | 1788 |
4 | 22615 | 5410 | 7198 | 3915 | 1777 | 5185 |
在这部分,你将开始分析数据,通过可视化和代码来理解每一个特征和其他特征的联系。你会看到关于数据集的统计描述,考虑每一个属性的相关性,然后从数据集中选择若干个样本数据点,你将在整个项目中一直跟踪研究这几个数据点。
运行下面的代码单元给出数据集的一个统计描述。注意这个数据集包含了6个重要的产品类型:‘Fresh’, ‘Milk’, ‘Grocery’, ‘Frozen’, **‘Detergents_Paper’**和 ‘Delicatessen’。想一下这里每一个类型代表你会购买什么样的产品。
# TODO: 描述数据集
display(data.describe())
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
count | 440.000000 | 440.000000 | 440.000000 | 440.000000 | 440.000000 | 440.000000 |
mean | 12000.297727 | 5796.265909 | 7951.277273 | 3071.931818 | 2881.493182 | 1524.870455 |
std | 12647.328865 | 7380.377175 | 9503.162829 | 4854.673333 | 4767.854448 | 2820.105937 |
min | 3.000000 | 55.000000 | 3.000000 | 25.000000 | 3.000000 | 3.000000 |
25% | 3127.750000 | 1533.000000 | 2153.000000 | 742.250000 | 256.750000 | 408.250000 |
50% | 8504.000000 | 3627.000000 | 4755.500000 | 1526.000000 | 816.500000 | 965.500000 |
75% | 16933.750000 | 7190.250000 | 10655.750000 | 3554.250000 | 3922.000000 | 1820.250000 |
max | 112151.000000 | 73498.000000 | 92780.000000 | 60869.000000 | 40827.000000 | 47943.000000 |
为了对客户有一个更好的了解,并且了解代表他们的数据将会在这个分析过程中如何变换。最好是选择几个样本数据点,并且更为详细地分析它们。在下面的代码单元中,选择三个索引加入到索引列表indices
中,这三个索引代表你要追踪的客户。我们建议你不断尝试,直到找到三个明显不同的客户。
# TODO: 从数据集中选择三个你希望抽样的数据点的索引
indices = [47, 86, 181]
# 为选择的样本创建一个DataFrame
samples = pd.DataFrame(data.loc[indices], columns = data.keys()).reset_index(drop=True)
print("Chosen samples of whole sale customers dataset:")
display(samples)
Chosen samples of whole sale customers dataset:
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
0 | 44466 | 54259 | 55571 | 7782 | 24171 | 6465 |
1 | 22925 | 73498 | 32114 | 987 | 20070 | 903 |
2 | 112151 | 29627 | 18148 | 16745 | 4948 | 8550 |
import seaborn as sns
%config InlineBackend.figure_format='retina'
samples_bar = samples.append(data.describe().loc[['50%', 'mean']])
samples_bar.plot(kind='bar', figsize=(14, 6))
在你看来你选择的这三个样本点分别代表什么类型的企业(客户)?对每一个你选择的样本客户,通过它在每一种产品类型上的花费与数据集的统计描述进行比较,给出你做上述判断的理由。
提示: 企业的类型包括超市、咖啡馆、零售商以及其他。注意不要使用具体企业的名字,比如说在描述一个餐饮业客户时,你不能使用麦当劳。你能使用各个平均值作为参考来比较样本,平均值如下
了解这一点后,你应该如何比较呢?这对推动你了解他们是什么类型的企业有帮助吗?
回答:
review:
和数据集的统计特征进行了很好的比较。同时,我们建议选择 percentile 而非 mean,因为在未知数据分布的情况下使用均值作为比较对象是比较危险的——因为不清楚概率分布,所以用 percentile、median 这样的统计特征会相对好一点。更多你可以参考描述统计学、或者数据的统计量特征。
一个有趣的想法是,考虑这六个类别中的一个(或者多个)产品类别,是否对于理解客户的购买行为具有实际的相关性。也就是说,当用户购买了一定数量的某一类产品,我们是否能够确定他们必然会成比例地购买另一种类的产品。有一个简单的方法可以检测相关性:我们用移除了某一个特征之后的数据集来构建一个监督学习(回归)模型,然后用这个模型去预测那个被移除的特征,再对这个预测结果进行评分,看看预测结果如何。
在下面的代码单元中,你需要实现以下的功能:
使用 DataFrame.drop
函数移除数据集中你选择的不需要的特征,并将移除后的结果赋值给 new_data
。
使用 sklearn.model_selection.train_test_split
将数据集分割成训练集和测试集。
test_size
为 0.25
并设置一个 random_state
。导入一个 DecisionTreeRegressor (决策树回归器),设置一个 random_state
,然后用训练集训练它。
使用回归器的 score
函数输出模型在测试集上的预测得分。
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
features = data.columns.tolist()
for feature in features:
# TODO:为DataFrame创建一个副本,用'drop'函数丢弃一个特征# TODO:
new_data = data.drop(feature, axis=1)
target = data[feature]
# TODO:使用给定的特征作为目标,将数据分割成训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(new_data, target, test_size=0.25, random_state=10)
# TODO:创建一个DecisionTreeRegressor(决策树回归器)并在训练集上训练它
regressor = DecisionTreeRegressor(random_state=10)
regressor.fit(X_train, y_train)
# TODO:输出在测试集上的预测得分
score = regressor.score(X_test, y_test)
print("%s在预测集上的预测得分:%.4f" % (feature, score))
Fresh在预测集上的预测得分:-0.3792
Milk在预测集上的预测得分:-0.4421
Grocery在预测集上的预测得分:0.7238
Frozen在预测集上的预测得分:0.0548
Detergents_Paper在预测集上的预测得分:0.4944
Delicatessen在预测集上的预测得分:-10.5627
提示: 决定系数(coefficient of determination), R 2 R^2 R2 结果在0到1之间,1表示完美拟合,一个负的 R 2 R^2 R2 表示模型不能够拟合数据。如果你对某个特征得到了低分,这使我们相信这一特征点是难以预测其余特征点的。当考虑相关性时,这使其成为一个重要特征。
回答:
预测特征及其得分:
特征Fresh、Milk和Delicatessen均在预测上取得了负的R2分数,说明将这个三个特征与其他特征具有较低的相关性,因此是比较重要的特征,对于区分用户的消费习惯很有必要;考虑到Grocery这一特征得分较高,说明该特征可以通过其他特征进行预测,这个特征也许不是重要的;另外两个特征Frozen和Detergents_Paper的R2分数虽然大于0,但是不高,因此不应该忽视。
review:
为了能够对这个数据集有一个更好的理解,我们可以对数据集中的每一个产品特征构建一个散布矩阵(scatter matrix)。如果你发现你在上面尝试预测的特征对于区分一个特定的用户来说是必须的,那么这个特征和其它的特征可能不会在下面的散射矩阵中显示任何关系。相反的,如果你认为这个特征对于识别一个特定的客户是没有作用的,那么通过散布矩阵可以看出在这个数据特征和其它特征中有关联性。运行下面的代码以创建一个散布矩阵。
scatter = pd.plotting.scatter_matrix(data, alpha=0.5, figsize=(14, 8), diagonal='kde')
提示: 数据是正态分布(normally distributed)吗? 大多数数据点在哪里? 您可以使用corr()来获取特征相关性,然后使用热图(heatmap)将其可视化(输入到热图中的数据应该是有相关性的值,例如:data.corr())以获得进一步的理解。
import seaborn as sns
corr = data.corr()
display(corr)
ax = sns.heatmap(corr)
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
Fresh | 1.000000 | 0.100510 | -0.011854 | 0.345881 | -0.101953 | 0.244690 |
Milk | 0.100510 | 1.000000 | 0.728335 | 0.123994 | 0.661816 | 0.406368 |
Grocery | -0.011854 | 0.728335 | 1.000000 | -0.040193 | 0.924641 | 0.205497 |
Frozen | 0.345881 | 0.123994 | -0.040193 | 1.000000 | -0.131525 | 0.390947 |
Detergents_Paper | -0.101953 | 0.661816 | 0.924641 | -0.131525 | 1.000000 | 0.069291 |
Delicatessen | 0.244690 | 0.406368 | 0.205497 | 0.390947 | 0.069291 | 1.000000 |
回答:
review:
在这个部分,你将通过在数据上做一个合适的缩放,并检测异常点(你可以选择性移除)将数据预处理成一个更好的代表客户的形式。预处理数据是保证你在分析中能够得到显著且有意义的结果的重要环节。
如果数据不是正态分布的,尤其是数据的平均数和中位数相差很大的时候(表示数据非常歪斜)。这时候通常用一个非线性的缩放是很合适的,(英文原文) — 尤其是对于金融数据。一种实现这个缩放的方法是使用 Box-Cox 变换,这个方法能够计算出能够最佳减小数据倾斜的指数变换方法。一个比较简单的并且在大多数情况下都适用的方法是使用自然对数。
在下面的代码单元中,你将需要实现以下功能:
np.log
函数在数据 data
上做一个对数缩放,然后将它的副本(不改变原始data的值)赋值给 log_data
。np.log
函数在样本数据 samples
上做一个对数缩放,然后将它的副本赋值给 log_samples
。# TODO:使用自然对数缩放数据
log_data = np.log(data)
# TODO:使用自然对数缩放样本数据
log_samples = np.log(samples)
# 为每一对新产生的特征制作一个散射矩阵
pd.plotting.scatter_matrix(log_data, alpha = 0.5, figsize = (14,8), diagonal = 'kde');
在使用了一个自然对数的缩放之后,数据的各个特征会显得更加的正态分布。对于任意的你以前发现有相关关系的特征对,观察他们的相关关系是否还是存在的(并且尝试观察,他们的相关关系相比原来是变强了还是变弱了)。
运行下面的代码以观察样本数据在进行了自然对数转换之后如何改变了。
display(log_data.corr())
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
Fresh | 1.000000 | -0.019834 | -0.132713 | 0.383996 | -0.155871 | 0.255186 |
Milk | -0.019834 | 1.000000 | 0.758851 | -0.055316 | 0.677942 | 0.337833 |
Grocery | -0.132713 | 0.758851 | 1.000000 | -0.164524 | 0.796398 | 0.235728 |
Frozen | 0.383996 | -0.055316 | -0.164524 | 1.000000 | -0.211576 | 0.254718 |
Detergents_Paper | -0.155871 | 0.677942 | 0.796398 | -0.211576 | 1.000000 | 0.166735 |
Delicatessen | 0.255186 | 0.337833 | 0.235728 | 0.254718 | 0.166735 | 1.000000 |
相关性变化:
features | before | after |
---|---|---|
Detergents_Paper和Grocery | 0.9246 | 0.7963 |
Milk和Grocery | 0.7283 | 0.7588 |
Milk和Detergents_Paper | 0.6618 | 0.6779 |
# 展示经过对数变换后的样本数据
display(log_samples)
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
0 | 10.702480 | 10.901524 | 10.925417 | 8.959569 | 10.092909 | 8.774158 |
1 | 10.039983 | 11.205013 | 10.377047 | 6.894670 | 9.906981 | 6.805723 |
2 | 11.627601 | 10.296441 | 9.806316 | 9.725855 | 8.506739 | 9.053687 |
你可以使用 seaborn库 中的代码,来实现对变化后每个特征 核密度估计(KDE)的可视化。参见:非参数估计:核密度估计KDE
import matplotlib.pyplot as plt
import seaborn as sns
# sns.set_palette('Reds_r')
# sns.set_style('ticks')
# plot densities of log-transformed data
plt.figure(figsize=(8,4))
for col in data.columns:
sns.kdeplot(log_data[col], shade=True)
plt.legend(loc=2)
对于任何的分析,在数据预处理的过程中检测数据中的异常值都是非常重要的一步。异常值的出现会使得把这些值考虑进去后结果出现倾斜。这里有很多关于怎样定义什么是数据集中的异常值的经验法则。这里我们将使用 Tukey 的定义异常值的方法:一个异常阶(outlier step)被定义成1.5倍的四分位距(interquartile range,IQR)。一个数据点如果某个特征包含在该特征的 IQR 之外的特征,那么该数据点被认定为异常点。
在下面的代码单元中,你需要完成下面的功能:
Q1
。使用 np.percentile
来完成这个功能。Q3
。同样的,使用 np.percentile
来完成这个功能。step
。outliers
列表中,以移除异常值。注意: 如果你选择移除异常值,请保证你选择的样本点不在这些移除的点当中!
一旦你完成了这些功能,数据集将存储在 good_data
中。
参考链接:Python数据分析基础: 异常值检测和处理
outlier_index = []
# 对于每一个特征,找到值异常高或者异常低的数据点
for feature in log_data.keys():
# TODO: 计算给定特征的Q1(数据的25th分位点)
Q1 = np.percentile(log_data[feature], 25)
# TODO: 计算给定特征的Q3(数据的75th分位点)
Q3 = np.percentile(log_data[feature], 75)
# TODO: 使用四分位范围计算异常阶(1.5倍的四分位距)
step = 1.5 * (Q3 - Q1)
# plot the outlier
print("Data points considered outliers for the feature ' {} ':".format(feature))
display(log_data[~((log_data[feature] <= Q3 + step) & (log_data[feature] >= Q1 - step))])
outlier_index += log_data[~((log_data[feature] <= Q3 + step) & (log_data[feature] >= Q1 - step))].index.tolist()
# TODO:列出有多个异常特征值的数据点
outlier = list(set([i for i in outlier_index if outlier_index.count(i) > 1]))
# TODO(可选): 选择你希望移除的数据点的索引
outliers = outlier
# 以下代码会移除outliers中索引的数据点, 并储存在good_data中
good_data = log_data.drop(log_data.index[outliers]).reset_index(drop=True)
Data points considered outliers for the feature ' Fresh ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
65 | 4.442651 | 9.950323 | 10.732651 | 3.583519 | 10.095388 | 7.260523 |
66 | 2.197225 | 7.335634 | 8.911530 | 5.164786 | 8.151333 | 3.295837 |
81 | 5.389072 | 9.163249 | 9.575192 | 5.645447 | 8.964184 | 5.049856 |
95 | 1.098612 | 7.979339 | 8.740657 | 6.086775 | 5.407172 | 6.563856 |
96 | 3.135494 | 7.869402 | 9.001839 | 4.976734 | 8.262043 | 5.379897 |
128 | 4.941642 | 9.087834 | 8.248791 | 4.955827 | 6.967909 | 1.098612 |
171 | 5.298317 | 10.160530 | 9.894245 | 6.478510 | 9.079434 | 8.740337 |
193 | 5.192957 | 8.156223 | 9.917982 | 6.865891 | 8.633731 | 6.501290 |
218 | 2.890372 | 8.923191 | 9.629380 | 7.158514 | 8.475746 | 8.759669 |
304 | 5.081404 | 8.917311 | 10.117510 | 6.424869 | 9.374413 | 7.787382 |
305 | 5.493061 | 9.468001 | 9.088399 | 6.683361 | 8.271037 | 5.351858 |
338 | 1.098612 | 5.808142 | 8.856661 | 9.655090 | 2.708050 | 6.309918 |
353 | 4.762174 | 8.742574 | 9.961898 | 5.429346 | 9.069007 | 7.013016 |
355 | 5.247024 | 6.588926 | 7.606885 | 5.501258 | 5.214936 | 4.844187 |
357 | 3.610918 | 7.150701 | 10.011086 | 4.919981 | 8.816853 | 4.700480 |
412 | 4.574711 | 8.190077 | 9.425452 | 4.584967 | 7.996317 | 4.127134 |
Data points considered outliers for the feature ' Milk ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
86 | 10.039983 | 11.205013 | 10.377047 | 6.894670 | 9.906981 | 6.805723 |
98 | 6.220590 | 4.718499 | 6.656727 | 6.796824 | 4.025352 | 4.882802 |
154 | 6.432940 | 4.007333 | 4.919981 | 4.317488 | 1.945910 | 2.079442 |
356 | 10.029503 | 4.897840 | 5.384495 | 8.057377 | 2.197225 | 6.306275 |
Data points considered outliers for the feature ' Grocery ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
75 | 9.923192 | 7.036148 | 1.098612 | 8.390949 | 1.098612 | 6.882437 |
154 | 6.432940 | 4.007333 | 4.919981 | 4.317488 | 1.945910 | 2.079442 |
Data points considered outliers for the feature ' Frozen ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
38 | 8.431853 | 9.663261 | 9.723703 | 3.496508 | 8.847360 | 6.070738 |
57 | 8.597297 | 9.203618 | 9.257892 | 3.637586 | 8.932213 | 7.156177 |
65 | 4.442651 | 9.950323 | 10.732651 | 3.583519 | 10.095388 | 7.260523 |
145 | 10.000569 | 9.034080 | 10.457143 | 3.737670 | 9.440738 | 8.396155 |
175 | 7.759187 | 8.967632 | 9.382106 | 3.951244 | 8.341887 | 7.436617 |
264 | 6.978214 | 9.177714 | 9.645041 | 4.110874 | 8.696176 | 7.142827 |
325 | 10.395650 | 9.728181 | 9.519735 | 11.016479 | 7.148346 | 8.632128 |
420 | 8.402007 | 8.569026 | 9.490015 | 3.218876 | 8.827321 | 7.239215 |
429 | 9.060331 | 7.467371 | 8.183118 | 3.850148 | 4.430817 | 7.824446 |
439 | 7.932721 | 7.437206 | 7.828038 | 4.174387 | 6.167516 | 3.951244 |
Data points considered outliers for the feature ' Detergents_Paper ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
75 | 9.923192 | 7.036148 | 1.098612 | 8.390949 | 1.098612 | 6.882437 |
161 | 9.428190 | 6.291569 | 5.645447 | 6.995766 | 1.098612 | 7.711101 |
Data points considered outliers for the feature ' Delicatessen ':
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
66 | 2.197225 | 7.335634 | 8.911530 | 5.164786 | 8.151333 | 3.295837 |
109 | 7.248504 | 9.724899 | 10.274568 | 6.511745 | 6.728629 | 1.098612 |
128 | 4.941642 | 9.087834 | 8.248791 | 4.955827 | 6.967909 | 1.098612 |
137 | 8.034955 | 8.997147 | 9.021840 | 6.493754 | 6.580639 | 3.583519 |
142 | 10.519646 | 8.875147 | 9.018332 | 8.004700 | 2.995732 | 1.098612 |
154 | 6.432940 | 4.007333 | 4.919981 | 4.317488 | 1.945910 | 2.079442 |
183 | 10.514529 | 10.690808 | 9.911952 | 10.505999 | 5.476464 | 10.777768 |
184 | 5.789960 | 6.822197 | 8.457443 | 4.304065 | 5.811141 | 2.397895 |
187 | 7.798933 | 8.987447 | 9.192075 | 8.743372 | 8.148735 | 1.098612 |
203 | 6.368187 | 6.529419 | 7.703459 | 6.150603 | 6.860664 | 2.890372 |
233 | 6.871091 | 8.513988 | 8.106515 | 6.842683 | 6.013715 | 1.945910 |
285 | 10.602965 | 6.461468 | 8.188689 | 6.948897 | 6.077642 | 2.890372 |
289 | 10.663966 | 5.655992 | 6.154858 | 7.235619 | 3.465736 | 3.091042 |
343 | 7.431892 | 8.848509 | 10.177932 | 7.283448 | 9.646593 | 3.610918 |
提示: 如果你发现一些数据点,有多个类别中的异常值,请考虑可能的原因以及是否需要删除。 还要注意k-means如何受到异常值的影响,以及这是否会影响您是否删除异常值。
display(log_data.iloc[outlier])
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
128 | 4.941642 | 9.087834 | 8.248791 | 4.955827 | 6.967909 | 1.098612 |
65 | 4.442651 | 9.950323 | 10.732651 | 3.583519 | 10.095388 | 7.260523 |
66 | 2.197225 | 7.335634 | 8.911530 | 5.164786 | 8.151333 | 3.295837 |
75 | 9.923192 | 7.036148 | 1.098612 | 8.390949 | 1.098612 | 6.882437 |
154 | 6.432940 | 4.007333 | 4.919981 | 4.317488 | 1.945910 | 2.079442 |
回答:
在这个部分中你将使用主成分分析(PCA)来分析批发商客户数据的内在结构。由于使用PCA在一个数据集上会计算出最大化方差的维度,我们将找出哪一个特征组合能够最好的描绘客户。
既然数据被缩放到一个更加正态分布的范围中并且我们也移除了需要移除的异常点,我们现在就能够在 good_data
上使用PCA算法以发现数据的哪一个维度能够最大化特征的方差。除了找到这些维度,PCA 也将报告每一个维度的解释方差比(explained variance ratio)–这个数据有多少方差能够用这个单独的维度来解释。注意 PCA 的一个组成部分(维度)能够被看做这个空间中的一个新的“特征”,但是它是原来数据中的特征构成的。
在下面的代码单元中,你将要实现下面的功能:
sklearn.decomposition.PCA
并且将 good_data
用 PCA 并且使用6个维度进行拟合后的结果保存到 pca
中。pca.transform
将 log_samples
进行转换,并将结果存储到 pca_samples
中。from sklearn.decomposition import PCA
pca = PCA(n_components=6, random_state=10)
pca.fit(good_data)
pca_samples = pca.transform(log_samples)
pca_result = vs.pca_results(good_data, pca)
def pca_results(good_data, pca):
'''
Create a DataFrame of the PCA results
Includes dimension feature weights and explained variance
Visualizes the PCA results
'''
# Dimension indexing
dimensions = ['Dimension {} '.format(i) for i in range(1, len(pca.components_) + 1)]
# PCA components
components = pd.DataFrame(np.round(pca.components_, 4), columns=list(good_data.keys()))
components.index = dimensions
# PCA explained variance
ratios = pca.explained_variance_ratio_.reshape(len(pca.components_), 1)
variance_ratios = pd.DataFrame(ratios, columns=['Explained Variance'])
variance_ratios.index = dimensions
# Create a bar plot visualization
fig, ax = plt.subplots(figsize=(14,8))
# Plot the feature weights as a function of the components
components.plot(ax=ax, kind='bar')
ax.set_ylabel('Feature Weight')
ax.set_xticklabels(dimensions, rotation=0)
# Display the explained variance ratios
for i, ev in enumerate(pca.explained_variance_ratio_):
ax.text(i-0.40, ax.get_ylim()[1] + 0.05, "Explained Variance\n %.4f" % ev)
pca_result = pd.concat([variance_ratios, components], axis=1)
return pca_result
display(pca_result.T)
Dimension 1 | Dimension 2 | Dimension 3 | Dimension 4 | Dimension 5 | Dimension 6 | |
---|---|---|---|---|---|---|
Explained Variance | 0.4430 | 0.2638 | 0.1231 | 0.1012 | 0.0485 | 0.0204 |
Fresh | 0.1675 | -0.6859 | -0.6774 | -0.2043 | -0.0026 | 0.0292 |
Milk | -0.4014 | -0.1672 | 0.0402 | 0.0128 | 0.7192 | -0.5402 |
Grocery | -0.4381 | -0.0707 | -0.0195 | 0.0557 | 0.3554 | 0.8205 |
Frozen | 0.1782 | -0.5005 | 0.3150 | 0.7854 | -0.0331 | 0.0205 |
Detergents_Paper | -0.7514 | -0.0424 | -0.2117 | 0.2096 | -0.5582 | -0.1824 |
Delicatessen | -0.1499 | -0.4941 | 0.6286 | -0.5423 | -0.2092 | 0.0197 |
为了解释主成分,我们必须计算主成分与每一个原始特征的相关系数,因此会生成一个(n_components, n_features)的矩阵,这也就是PCA.components_。
由于标准化的原因,所有主成分的均值为0,还给出了每个成分的标准偏差,这些是特征值的平方根。
对主成分的解释是基于找出哪些变量与每个成分最强相关,即,这些数中的哪一个在数量上是大的,在任一方向上距离零最远。我们认为哪些数字大或小是当然是一个主观决定。您需要确定相关性在多大程度上的重要性。在此,绝对值高于0.5的相关性被认为是重要的。
提示:
回答:
第一个和第二个主成分的可解释方差比率的和为0.7068, 也就是说这两个主成分解释了70.68%的数据变化;同理,前四个主成分解释了93.11%的数据变化;
对于第一个主成分来说,Detergents_Paper的特征贡献为-0.7514,同时,Milk和Grocery的贡献分别为-0.4014和-0.4381,这说明洗涤剂、牛奶和杂货的减小与第一个主成分的增加具有强相关,并且这三个特征呈正相关,这与咖啡馆或者杂货店的消费行为相似;
对于第二个主成分来说,按照相关程度依次是Fresh、Frozen和Delicateseen,并且这三个特征呈正相关,这与餐馆的消费行为类似;
对于第三个主成分来说,Delicateseen的增加和Fresh的减小具有相关性,一个买的多,一个买的少,并且这两个特征对主成分的贡献几乎相等,有点类似熟食店和生鲜店的消费行为;
对于第四个主成分来说,Frozen的增加和Delicateseen的减小具有相关性,Frozen对主成分的贡献较大,因而该主成分代表了冷冻店的消费行为;
由第一个和第二个主成分可见,六个特征根据彼此之间的关系可以被大致分成两组:(Detergents_Paper,Milk和Grocery)和(Fresh, Frozen和Delicateseen)
review:
所谓主成分分解,就是通过恰当的坐标变换,使得新坐标中的轴能够表达尽可能多的数据方差。最本质的,实际上某一 Dimension 就是原 Features 的线性组合,即
D = w1 * F1 + w2 * F2 + …
那么,你可以理解成,这代表了一种新的消费组合。如果某个 weight 绝对值很大,那么所对应的 feature 对该 Dimsension 影响很大。如果某两个 weight 同号,那么说明他们对这个 Dimension 的影响是相同的,反之则相反。
运行下面的代码,查看经过对数转换的样本数据在进行一个6个维度的主成分分析(PCA)之后会如何改变。观察样本数据的前四个维度的数值。考虑这和你初始对样本点的解释是否一致。
# 展示经过PCA转换的sample log-data
display(pd.DataFrame(np.round(pca_samples, 4), columns = pca_result.index.values))
Dimension 1 | Dimension 2 | Dimension 3 | Dimension 4 | Dimension 5 | Dimension 6 | |
---|---|---|---|---|---|---|
0 | -4.3646 | -3.9519 | -0.1229 | 0.6240 | 0.5379 | 0.0551 |
1 | -4.2903 | -1.4952 | -1.4997 | 0.1394 | 1.1469 | -0.6255 |
2 | -2.1899 | -4.8605 | 0.0008 | 0.4827 | 0.5041 | -0.1988 |
当使用主成分分析的时候,一个主要的目的是减少数据的维度,这实际上降低了问题的复杂度。当然降维也是需要一定代价的:更少的维度能够表示的数据中的总方差更少。因为这个,**累计解释方差比(cumulative explained variance ratio)**对于我们确定这个问题需要多少维度非常重要。另外,如果大部分的方差都能够通过两个或者是三个维度进行表示的话,降维之后的数据能够被可视化。
在下面的代码单元中,你将实现下面的功能:
good_data
用两个维度的PCA进行拟合,并将结果存储到 pca
中去。pca.transform
将 good_data
进行转换,并将结果存储在 reduced_data
中。pca.transform
将 log_samples
进行转换,并将结果存储在 pca_samples
中。# TODO:通过在good data上进行PCA,将其转换成两个维度
pca = PCA(n_components=2, random_state=10)
pca.fit(good_data)
# TODO:使用上面训练的PCA将good data进行转换
reduced_data = pca.transform(good_data)
# TODO:使用上面训练的PCA将log_samples进行转换
pca_samples = pca.transform(log_samples)
# 为降维后的数据创建一个DataFrame
reduced_data = pd.DataFrame(reduced_data, columns=['Dimension 1', 'Dimension 2'])
运行以下代码观察当仅仅使用两个维度进行 PCA 转换后,这个对数样本数据将怎样变化。观察这里的结果与一个使用六个维度的 PCA 转换相比较时,前两维的数值是保持不变的。
# 展示经过两个维度的PCA转换之后的样本log-data
display(pd.DataFrame(np.round(pca_samples, 4), columns=['Dimension 1', 'Dimension 2']))
Dimension 1 | Dimension 2 | |
---|---|---|
0 | -4.3646 | -3.9519 |
1 | -4.2903 | -1.4952 |
2 | -2.1899 | -4.8605 |
你可以考虑对变换后的数据进行可视化。使用seaborn库,你可以绘制分布的联合核密度图。
g = sns.JointGrid("Dimension 1", "Dimension 2", reduced_data, xlim=(-6,6), ylim=(-5,5))
g = g.plot_joint(sns.kdeplot, cmp="Blues", shade=True)
g = g.plot_marginals(sns.kdeplot, shade=True)
D:\Anaconda3\lib\site-packages\matplotlib\contour.py:1000: UserWarning: The following kwargs were not used by contour: 'cmp'
s)
双标图是一个散点图,每个数据点的位置由它所在主成分的分数确定。坐标系是主成分(这里是 Dimension 1
和 Dimension 2
)。此外,双标图还展示出初始特征在主成分上的投影。一个双标图可以帮助我们理解降维后的数据,发现主成分和初始特征之间的关系。
运行下面的代码来创建一个降维后数据的双标图。
# 可视化双标图
vs.biplot(good_data, reduced_data, pca)
def biplot(good_data, reduced_data, pca):
fig, ax = plt.subplots(figsize = (14, 8))
# scatterplot of the reduced data
ax.scatter(reduced_data.loc[:, 'Dimension 1'], reduced_data.loc[:, 'Dimension 2'],
facecolors='b', edgecolors='b', s=70, alpha=0.5)
ax.set_xlabel("Dimension 1", fontsize=14)
ax.set_ylabel("Dimension 2", fontsize=14)
ax.set_title("PC plane with original feature projections.", fontsize=16)
feature_vectors = pca.components_.T
# we use scaling factors to make the arrows easier to see
arrow_size, text_pos = 7.0, 8.0
# projections of the original features
for i, v in enumerate(feature_vectors):
ax.arrow(0, 0, arrow_size*v[0], arrow_size*v[1],
head_width=0.2, head_length=0.2, linewidth=2, color='red')
ax.text(v[0]*text_pos, v[1]*text_pos, good_data.columns[i], color = 'black',
ha='center', va='center', fontsize=18)
return ax
一旦我们有了原始特征的投影(红色箭头),就能更加容易的理解散点图每个数据点的相对位置。
在这个双标图中,哪些初始特征与第一个主成分有强关联?哪些初始特征与第二个主成分相关联?你观察到的是否与之前得到的 pca_results 图相符?
在这个部分,你将选择使用 K-Means 聚类算法或者是高斯混合模型聚类算法以发现数据中隐藏的客户分类。然后,你将从簇中恢复一些特定的关键数据点,通过将它们转换回原始的维度和规模,从而理解他们的含义。
使用 K-Means 聚类算法的优点是什么?
使用高斯混合模型聚类算法的优点是什么?
基于你现在对客户数据的观察结果,你选用了这两个算法中的哪一个,为什么?
提示: 想一想硬聚类(hard clustering)和软聚类(soft clustering)间的区别,以及哪一种更适用于我们的数据
回答:
参考链接:数据科学家必须了解的六大聚类算法:带你发现数据之美
针对不同情况,有些问题你需要的聚类数目可能是已知的。但是在聚类数目不作为一个先验知道的情况下,我们并不能够保证某个聚类的数目对这个数据是最优的,因为我们对于数据的结构(如果存在的话)是不清楚的。但是,我们可以通过计算每一个簇中点的轮廓系数来衡量聚类的质量。数据点的轮廓系数衡量了它与分配给他的簇的相似度,这个值范围在-1(不相似)到1(相似)。平均轮廓系数为我们提供了一种简单地度量聚类质量的方法。
在接下来的代码单元中,你将实现下列功能:
reduced_data
上使用一个聚类算法,并将结果赋值到 clusterer
,需要设置 random_state
使得结果可以复现。clusterer.predict
预测 reduced_data
中的每一个点的簇,并将结果赋值到 preds
。centers
。pca_samples
中的每一个样本点的类别并将结果赋值到 sample_preds
。sklearn.metrics.silhouette_score
包并计算 reduced_data
相对于 preds
的轮廓系数。
score
并输出结果。from sklearn.mixture import GaussianMixture
from sklearn.metrics import silhouette_score
from sklearn.cluster import KMeans
# TODO:在降维后的数据上使用你选择的聚类算法
for n_components in [2, 3, 4, 5]:
kmean = KMeans(n_clusters=n_components, random_state=10)
gmm = GaussianMixture(n_components=n_components, random_state=10)
clusters = (('kmean', kmean),('gmm', gmm))
for name, clusterer in clusters:
clusterer.fit(reduced_data)
# TODO:预测每一个点的簇
preds = clusterer.predict(reduced_data)
# TODO:找到聚类中心
if name == 'kmean':
centers = clusterer.cluster_centers_
else:
centers = clusterer.means_
# TODO:预测在每一个转换后的样本点的类
sample_preds = clusterer.predict(pca_samples)
# TODO:计算选择的类别的平均轮廓系数(mean silhouette coefficient)
score = silhouette_score(reduced_data, preds)
print("when the cluster is %s and the n_components is %d, the silhouette_score is %.4f." % (name, n_components, score))
when the cluster is kmean and the n_components is 2, the silhouette_score is 0.4263.
when the cluster is gmm and the n_components is 2, the silhouette_score is 0.4219.
when the cluster is kmean and the n_components is 3, the silhouette_score is 0.3903.
when the cluster is gmm and the n_components is 3, the silhouette_score is 0.3755.
when the cluster is kmean and the n_components is 4, the silhouette_score is 0.3329.
when the cluster is gmm and the n_components is 4, the silhouette_score is 0.2933.
when the cluster is kmean and the n_components is 5, the silhouette_score is 0.3522.
when the cluster is gmm and the n_components is 5, the silhouette_score is 0.3185.
回答:
cluster | n_clusters | silhouette_score |
---|---|---|
Kmean | 2 | 0.4263 |
Kmean | 3 | 0.3903 |
Kmean | 4 | 0.3329 |
Kmean | 5 | 0.3522 |
GMM | 2 | 0.4219 |
GMM | 3 | 0.3755 |
GMM | 4 | 0.2933 |
GMM | 5 | 0.3285 |
当聚类数目为2时,Kmean和高斯混合模型聚类均取得最大的轮廓系数,分别为0.4263和0.4219
关于轮廓系数(silhouette_score),你可以参考这个页面更细致地了解这个系数是怎么得到的,有什么意义。
一旦你选好了通过上面的评价函数得到的算法的最佳聚类数目,你就能够通过使用下面的代码块可视化来得到的结果。作为实验,你可以试着调整你的聚类算法的聚类的数量来看一下不同的可视化结果。但是你提供的最终的可视化图像必须和你选择的最优聚类数目一致。
n_components = 2
clusterer = GaussianMixture(n_components=n_components, covariance_type='full', random_state=10)
clusterer.fit(reduced_data)
preds = clusterer.predict(reduced_data)
centers = clusterer.means_
# 从已有的实现中展示聚类的结果
vs.cluster_results(reduced_data, preds, centers, pca_samples)
上面的可视化图像中提供的每一个聚类都有一个中心点。这些中心(或者叫平均点)并不是数据中真实存在的点,但是是所有预测在这个簇中的数据点的平均。对于创建客户分类的问题,一个簇的中心对应于那个分类的平均用户。因为这个数据现在进行了降维并缩放到一定的范围,我们可以通过施加一个反向的转换恢复这个点所代表的用户的花费。
在下面的代码单元中,你将实现下列的功能:
pca.inverse_transform
将 centers
反向转换,并将结果存储在 log_centers
中。np.log
的反函数 np.exp
反向转换 log_centers
并将结果存储到 true_centers
中。# TODO:反向转换中心点
log_centers = pca.inverse_transform(centers)
# TODO:对中心点做指数转换
true_centers = np.exp(log_centers)
# 显示真实的中心点
segments = ['Segment {}'.format(i) for i in range(0,len(centers))]
true_centers = pd.DataFrame(np.round(true_centers), columns = data.keys())
true_centers.index = segments
display(true_centers)
Fresh | Milk | Grocery | Frozen | Detergents_Paper | Delicatessen | |
---|---|---|---|---|---|---|
Segment 0 | 8953.0 | 2114.0 | 2765.0 | 2075.0 | 353.0 | 732.0 |
Segment 1 | 3552.0 | 7837.0 | 12219.0 | 870.0 | 4696.0 | 962.0 |
import seaborn as sns
true_centers = true_centers.append(data.describe().loc['mean'])
true_centers.plot(kind='bar', figsize=(15,6))
提示: 一个被分到'Cluster X'
的客户最好被用 'Segment X'
中的特征集来标识的企业类型表示。考虑每个细分所代表的选择特征点的值。 引用它们的各项平均值,以了解它们代表什么样的机构。
回答:
运行下面的代码单元以找到每一个样本点被预测到哪一个簇中去。
# 显示预测结果
for i, pred in enumerate(sample_preds):
print("Sample point", i, "predicted to be in Cluster", pred)
Sample point 0 predicted to be in Cluster 1
Sample point 1 predicted to be in Cluster 1
Sample point 2 predicted to be in Cluster 1
回答:
在之前的预测中:
现在的预测:
在最后一部分中,你要学习如何使用已经被分类的数据。首先,你要考虑不同组的客户客户分类,针对不同的派送策略受到的影响会有什么不同。其次,你要考虑到,每一个客户都被打上了标签(客户属于哪一个分类)可以给客户数据提供一个多一个特征。最后,你会把客户分类与一个数据中的隐藏变量做比较,看一下这个分类是否辨识了特定的关系。
在对他们的服务或者是产品做细微的改变的时候,公司经常会使用 A/B tests 以确定这些改变会对客户产生积极作用还是消极作用。这个批发商希望考虑将他的派送服务从每周5天变为每周3天,但是他只会对他客户当中对此有积极反馈的客户采用。
提示: 我们能假设这个改变对所有的客户影响都一致吗?我们怎样才能够确定它对于哪个类型的客户影响最大?
回答:
简单地了解了一下AB测试,当供应商要对现有的服务做出改进(每周5天发货变为每周3天发货)的时候,需要进行A/B测试,方法就是将客户分为2组,且这两组客户的数量相等,客户的特征相似(也就是每一种类型的客户在2组中的数量都差不多),因为我们之前已经通过GMM将客户分为2类,为了在影响尽可能少的客户的前提下得出结论,我们并不对所有客户都进行测试,我们挑选的测试的客户均来自于两个簇的中心,现在从这两个簇的中心各取相等数量的客户作为测试客户,然后从测试客户中每一类各取50%的客户放入第一组,剩余的客户放入第二组,这样分组就完成了。接下来分别对这两组的客户采取不同的发货策略,第一组采用之前的发货策略,第二组采用新的发货策略。如果在接下来的时间内,两组客户收到的投诉很接近甚至第二组的客户的投诉量少于第一组,那么说明发货周期的更改策略是可行的。如果第二组客户的投诉大于第一组,那么说明该发货策略对所有或者某一类客户是有影响的。接下来就分析第二组客户中投诉客户的类型,如果两种类型的客户的投诉量差不多,说明该策略的更改对所有客户都有影响。如果某种类型的客户的投诉量明显高于另一种类型,那么策略的更改对于这一类客户的影响更大。
通过聚类技术,我们能够将原有的没有标记的数据集中的附加结构分析出来。因为每一个客户都有一个最佳的划分(取决于你选择使用的聚类算法),我们可以把用户分类作为数据的一个工程特征。假设批发商最近迎来十位新顾客,并且他已经为每位顾客每个产品类别年度采购额进行了预估。
提示:在下面的代码单元中,我们提供了一个已经做好聚类的数据(聚类结果为数据中的cluster属性),我们将在这个数据集上做一个小实验。尝试运行下面的代码看看我们尝试预测‘Region’的时候,如果存在聚类特征’cluster’与不存在相比对最终的得分会有什么影响?这对你有什么启发?
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 读取包含聚类结果的数据
cluster_data = pd.read_csv('cluster.csv')
y = cluster_data['Region']
X = cluster_data.drop(['Region'], axis=1)
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=24)
clf = RandomForestClassifier(random_state=24)
clf.fit(X_train, y_train)
score_with_cluster = clf.score(X_test, y_test)
# 移除cluster特征
X_train = X_train.copy()
X_train.drop(['cluster'], axis=1, inplace=True)
X_test = X_test.copy()
X_test.drop(['cluster'], axis=1, inplace=True)
clf.fit(X_train, y_train)
score_no_cluster = clf.score(X_test, y_test)
print("不使用cluster特征的得分: %.4f"%score_no_cluster)
print("使用cluster特征的得分: %.4f"%score_with_cluster)
不使用cluster特征的得分: 0.6437
使用cluster特征的得分: 0.6667
D:\Anaconda3\lib\site-packages\sklearn\ensemble\forest.py:246: FutureWarning: The default value of n_estimators will change from 10 in version 0.20 to 100 in 0.22.
"10 in version 0.20 to 100 in 0.22.", FutureWarning)
回答:
结果表明,使用包含cluster特征的数据进行训练得到的模型取得了更好地分类效果。这就说明,预先使用聚类算法挖掘数据之间的潜在关系对于监督学习在于一定程度上是有帮助的,但是受限于数据本身的分布,并不一定总是有效。
review:
在这个项目的开始,我们讨论了从数据集中移除 'Channel'
和 'Region'
特征,这样在分析过程中我们就会着重分析用户产品类别。通过重新引入 Channel
这个特征到数据集中,并施加和原来数据集同样的 PCA 变换的时候我们将能够发现数据集产生一个有趣的结构。
运行下面的代码单元以查看哪一个数据点在降维的空间中被标记为 'HoReCa'
(旅馆/餐馆/咖啡厅)或者 'Retail'
。另外,你将发现样本点在图中被圈了出来,用以显示他们的标签。
# 根据‘Channel‘数据显示聚类的结果
vs.channel_results(reduced_data, outliers, pca_samples)
回答:
根据内在的分布,数据集可以分成两个簇,这与我选择的聚类点的数目一致。GMM算法将大部分数据都正确区分,但是实际上可以看到,绿色的类别中包含了不少的红色样本,但是由于GMM预测的是某一点属于某一类的概率,其认为属于绿色的概率更大,因此将其划分成了绿色点,因此我们应该关注GMM对于每一个样本点赋予的概率。
从上图也可以看出,两个类别的中间部分其实很难作出区分,也就是说既可以判定成零售商也可以判定成旅馆/饭店/咖啡馆,大体上来说,与之前对于用户分明类的定义一致。