欢迎来到机器学习工程师纳米学位的第三个项目!在这个notebook文件中,有些模板代码已经提供给你,但你还需要实现更多的功能来完成这个项目。除非有明确要求,你无须修改任何已给出的代码。以'练习'开始的标题表示接下来的代码部分中有你必须要实现的功能。每一部分都会有详细的指导,需要实现的部分也会在注释中以'TODO'标出。请仔细阅读所有的提示!
除了实现代码外,你还必须回答一些与项目和你的实现有关的问题。每一个需要你回答的问题都会以'问题 X'为标题。请仔细阅读每个问题,并且在问题后的'回答'文字框中写出完整的答案。我们将根据你对问题的回答和撰写代码所实现的功能来对你提交的项目进行评分。
提示:Code 和 Markdown 区域可通过 Shift + Enter 快捷键运行。此外,Markdown可以通过双击进入编辑模式。
在这个项目中,你将分析一个数据集的内在结构,这个数据集包含很多客户真对不同类型产品的年度采购额(用金额表示)。这个项目的任务之一是如何最好地描述一个批发商不同种类顾客之间的差异。这样做将能够使得批发商能够更好的组织他们的物流服务以满足每个客户的需求。
这个项目的数据集能够在UCI机器学习信息库中找到.因为这个项目的目的,分析将不会包括'Channel'和'Region'这两个特征——重点集中在6个记录的客户购买的产品类别上。
运行下面的的代码单元以载入整个客户数据集和一些这个项目需要的Python库。如果你的数据集载入成功,你将看到后面输出数据集的大小。
In [33]:
%%time
# 引入这个项目需要的库
import numpy as np
import pandas as pd
import visuals as vs
from IPython.display import display # 使得我们可以对DataFrame使用display()函数
# 设置以内联的形式显示matplotlib绘制的图片(在notebook中显示更美观)
%matplotlib inline
# 载入整个客户数据集
try:
data = pd.read_csv("customers.csv")
data.drop(['Region', 'Channel'], axis = 1, inplace = True)
print "Wholesale customers dataset has {} samples with {} features each.".format(*data.shape)
except:
print "Dataset could not be loaded. Is the dataset missing?"
In [34]:
# 显示数据集的一个描述
display(data.describe())
In [35]:
# TODO:从数据集中选择三个你希望抽样的数据点的索引
indices = [90, 200, 265]
# 为选择的样本建立一个DataFrame
samples = pd.DataFrame(data.loc[indices], columns = data.keys()).reset_index(drop = True)
print "Chosen samples of wholesale customers dataset:"
display(samples)
回答:
一个有趣的想法是,考虑这六个类别中的一个(或者多个)产品类别,是否对于理解客户的购买行为具有实际的相关性。也就是说,当用户购买了一定数量的某一类产品,我们是否能够确定他们必然会成比例地购买另一种类的产品。有一个简单的方法可以检测相关性:我们用移除了某一个特征之后的数据集来构建一个监督学习(回归)模型,然后用这个模型去预测那个被移除的特征,再对这个预测结果进行评分,来判断由余下5个特征构建的模型对移除掉的那一个特征预测能力的好坏。
在下面的代码单元中,你需要实现以下的功能:
DataFrame.drop
函数移除数据集中你选择的不需要的特征,并将移除后的结果赋值给new_data
。sklearn.model_selection.train_test_split
将数据集分割成训练集和测试集。test_size
为0.25
并设置一个random_state
。random_state
,然后用训练集训练它。score
函数输出模型在测试集上的预测得分。
In [36]:
%%time
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeRegressor
# TODO:为DataFrame创建一个副本,用'drop'函数丢弃一些指定的特征
grocery = data['Grocery']
data_without_grocery = data.drop(['Grocery'],1)
# display(data.head(1))
# display(fresh.head())
# display(new_data.head())
# TODO:使用给定的特征作为目标,将数据分割成训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(data_without_grocery, grocery, test_size=0.25, random_state=20)
# display(X_train.head(1))
# display(X_test.head(1))
# display(y_train.head(1))
# display(y_test.head(1))
# # TODO:创建一个DecisionTreeRegressor(决策树回归器)并在训练集上训练它
regressor = DecisionTreeRegressor(max_depth=15, random_state=20)
regressor.fit(X_train, y_train)
# # TODO:输出在测试集上的预测得分
score = regressor.score(X_test, y_test)
print score
回答: 预测Grocery,预测得分约为0.728,这个特征对于区分用户是没有必要的。因为这个特征可以被其他特征很好的预测,说明与其他特征的相关性好,可以被其他的特征替代,所以没有存在的必要性。
In [37]:
# 对于数据中的每一对特征构造一个散布矩阵
pd.plotting.scatter_matrix(data, alpha = 0.3, figsize = (14,8), diagonal = 'kde');
In [38]:
%pdb
# 数据关联度表格呈现
import seaborn
seaborn.heatmap(data.corr(), annot=True)
# 添加这些代码以后,导致所有曲线图背景颜色显示异常(???????????)
Out[38]:
回答:
如果数据不是正态分布的,尤其是数据的平均数和中位数相差很大的时候(表示数据非常歪斜)。这时候通常用一个非线性的缩放是很合适的,(英文原文) — 尤其是对于金融数据。一种实现这个缩放的方法是使用Box-Cox 变换,这个方法能够计算出能够最佳减小数据倾斜的指数变换方法。一个比较简单的并且在大多数情况下都适用的方法是使用自然对数。
在下面的代码单元中,你将需要实现以下功能:
np.log
函数在数据 data
上做一个对数缩放,然后将它的副本(不改变原始data的值)赋值给log_data
。np.log
函数在样本数据 samples
上做一个对数缩放,然后将它的副本赋值给log_samples
。
In [39]:
%%time
from scipy import stats
display(data.head())
# TODO:使用自然对数缩放数据
# log_data = data.apply(lambda x: np.log(x))
log_data = np.log(data)
display(log_data.head())
# TODO:使用自然对数缩放样本数据
log_samples = np.log(samples)
display(log_samples.head())
# 为每一对新产生的特征制作一个散射矩阵
pd.plotting.scatter_matrix(log_data, alpha = 0.3, figsize = (14,8), diagonal = 'kde');
In [40]:
# 展示经过对数变换后的样本数据
display(log_samples)
对于任何的分析,在数据预处理的过程中检测数据中的异常值都是非常重要的一步。异常值的出现会使得把这些值考虑进去后结果出现倾斜。这里有很多关于怎样定义什么是数据集中的异常值的经验法则。这里我们将使用Tukey的定义异常值的方法:一个异常阶(outlier step)被定义成1.5倍的四分位距(interquartile range,IQR)。一个数据点如果某个特征包含在该特征的IQR之外的特征,那么该数据点被认定为异常点。
在下面的代码单元中,你需要完成下面的功能:
Q1
。使用np.percentile
来完成这个功能。Q3
。同样的,使用np.percentile
来完成这个功能。step
.outliers
列表中,以移除异常值。注意: 如果你选择移除异常值,请保证你选择的样本点不在这些移除的点当中!
一旦你完成了这些功能,数据集将存储在good_data
中。
In [41]:
import numpy as np
# 用于记录所有的异常值的索引
allOutliers = []
# 对于每一个特征,找到值异常高或者是异常低的数据点
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)
# 显示异常点
print "Data points considered outliers for the feature '{}':".format(feature)
display(log_data[~((log_data[feature] >= Q1 - step) & (log_data[feature] <= Q3 + step))])
outlier = log_data[~((log_data[feature] >= Q1 - step) & (log_data[feature] <= Q3 + step))]
allOutliers.extend(outlier.index)
# 找出重复的索引值
sortedAllOutliers = np.sort(allOutliers)
duplicatedOutliers = []
preElement = -1
for element in sortedAllOutliers.flat:
if element == preElement and element not in duplicatedOutliers:
duplicatedOutliers.append(element);
preElement = element
print "sortedAllOutliers:{0}".format(sortedAllOutliers)
print "duplicatedOutliers: {0}".format(duplicatedOutliers)
# 可选:选择你希望移除的数据点的索引
outliers = np.unique(sortedAllOutliers)
print "outliers: {}".format(outliers)
# 如果选择了的话,移除异常点
good_data = log_data.drop(log_data.index[outliers]).reset_index(drop = True)
回答:多于一个特征下被看作是异常的数据点为:65, 66, 75, 128, 154。 虽然这些数据点有多个特性,说明信息比较丰富,不是单一的数据异常。但是,这些数据做增加的信息所带来的积极的影响,远小于其使类聚中心偏移所带来的消极影响。所以,这些数据点应该被移除。
既然数据被缩放到一个更加正态分布的范围中并且我们也移除了需要移除的异常点,我们现在就能够在good_data
上使用PCA算法以发现数据的哪一个维度能够最大化特征的方差。除了找到这些维度,PCA也将报告每一个维度的解释方差比(explained variance ratio)--这个数据有多少方差能够用这个单独的维度来解释。注意PCA的一个组成部分(维度)能够被看做这个空间中的一个新的“特征”,但是它是原来数据中的特征构成的。
在下面的代码单元中,你将要实现下面的功能:
sklearn.decomposition.PCA
并且将good_data
用PCA并且使用6个维度进行拟合后的结果保存到pca
中。pca.transform
将log_samples
进行转换,并将结果存储到pca_samples
中。
In [42]:
%%time
from sklearn.decomposition import PCA
# TODO:通过在good data上使用PCA,将其转换成和当前特征数一样多的维度
pca = PCA(n_components=good_data.shape[1], random_state=20)
pca.fit(good_data)
# TODO:使用上面的PCA拟合将变换施加在log_samples上
pca_samples = pca.transform(log_samples)
# 生成PCA的结果图
pca_results = vs.pca_results(good_data, pca)
数据的第一个和第二个主成分 总共 表示了多少的方差? 前四个主成分呢?使用上面提供的可视化图像,讨论从用户花费的角度来看前四个主要成分的消费行为最能代表哪种类型的客户并给出你做出判断的理由。
提示: 某一特定维度上的正向增长对应正权特征的增长和负权特征的减少。增长和减少的速率和每个特征的权重相关。参考资料(英文)。
回答:前两个主成分表示了0.7252,即%72.52的方差。前四个主成分表示了0.9279,即%92.79的方差。
正的权值越大,说明购买相关性越大,所以,主成分分析的结果,才会放到一起,合并到同一个维度里面来。
【CodeReview20170723】 在这边,我们使用了主成分分析法,将原来的6个特征通过数学变换,变换为了另外6个特征。对方差的计算,是为了让我们能够选择方差较大的特征以保留它们。每个新特征,实际上都是由原来的特征通过某种带权重的组合得到的,权重就是图中柱状图柱高度。考虑权重的绝对值,权重绝对值越大,说明权重对应的原特征对这个新特征带来的影响越大,反之亦反。权重若为正,则说明他们有正相关性;负值则说明它们是负相关性。A和B有正相关性可以理解为,买更多的A意味着有很大可能买更多的B;负相关性意味着买更多的A意味着有很大可能买更少的B。
In [43]:
# 展示经过PCA转换的sample log-data
display(samples)
display(pd.DataFrame(np.round(pca_samples, 4), columns = pca_results.index.values))
当使用主成分分析的时候,一个主要的目的是减少数据的维度,这实际上降低了问题的复杂度。当然降维也是需要一定代价的:更少的维度能够表示的数据中的总方差更少。因为这个,累计解释方差比(cumulative explained variance ratio)对于我们确定这个问题需要多少维度非常重要。另外,如果大部分的方差都能够通过两个或者是三个维度进行表示的话,降维之后的数据能够被可视化。
在下面的代码单元中,你将实现下面的功能:
good_data
用两个维度的PCA进行拟合,并将结果存储到pca
中去。pca.transform
将good_data
进行转换,并将结果存储在reduced_data
中。pca.transform
将log_samples
进行转换,并将结果存储在pca_samples
中。
In [44]:
%%time
from sklearn.decomposition import PCA
# TODO:通过在good data上进行PCA,将其转换成两个维度
pca = PCA(n_components = 2, random_state=20)
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'])
In [45]:
# 展示经过两个维度的PCA转换之后的样本log-data
display(pd.DataFrame(np.round(pca_samples, 4), columns = ['Dimension 1', 'Dimension 2']))
In [46]:
# Create a biplot
vs.biplot(good_data, reduced_data, pca)
Out[46]:
回答:与第一个主成分有强关联的有:Milk,Grocery,Detergents_Paper。与第二个主成分有关联的有:Fresh,Frozen。这个与之前得到的pca_results 图相符。
回答: K-Means类聚算法的优点是:
高斯混合模型聚类算法的优点是:
根据数据分布比较区域比较大,分类的界限并没有那么的严格和清晰,GMM这种分类结果是概率的方法,更加的精确严谨。我打算选用高斯混合模型聚类算法。
针对不同情况,有些问题你需要的聚类数目可能是已知的。但是在聚类数目不作为一个先验知道的情况下,我们并不能够保证某个聚类的数目对这个数据是最优的,因为我们对于数据的结构(如果存在的话)是不清楚的。但是,我们可以通过计算每一个簇中点的轮廓系数来衡量聚类的质量。数据点的轮廓系数衡量了它与分配给他的簇的相似度,这个值范围在-1(不相似)到1(相似)。平均轮廓系数为我们提供了一种简单地度量聚类质量的方法。
在接下来的代码单元中,你将实现下列功能:
reduced_data
上使用一个聚类算法,并将结果赋值到clusterer
,需要设置 random_state
使得结果可以复现。clusterer.predict
预测reduced_data
中的每一个点的簇,并将结果赋值到preds
。centers
。pca_samples
中的每一个样本点的类别并将结果赋值到sample_preds
。reduced_data
相对于preds
的轮廓系数。score
并输出结果。
In [47]:
%%time
from sklearn.mixture import GaussianMixture
from sklearn.metrics import silhouette_score
# TODO:在降维后的数据上使用你选择的聚类算法
clusterer = GaussianMixture(n_components=2, random_state=20).fit(reduced_data)
# TODO:预测每一个点的簇
preds = clusterer.predict(reduced_data)
# TODO:找到聚类中心
centers = clusterer.means_
print "centers: \n{0}".format(centers)
# TODO:预测在每一个转换后的样本点的类
sample_preds = clusterer.predict(pca_samples)
# TODO:计算选择的类别的平均轮廓系数(mean silhouette coefficient)
score = silhouette_score(reduced_data, preds)
print "silhouette_score: {0}".format(score)
回答:
当类聚数目为2时,能够得到最佳的轮廓系数。
In [48]:
print pca_samples
In [49]:
# 从已有的实现中展示聚类的结果
vs.cluster_results(reduced_data, preds, centers, pca_samples)
In [50]:
log_centers = pca.inverse_transform(centers)
print log_centers
In [51]:
# 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)
In [52]:
import seaborn as sns
import matplotlib.pyplot as plt
# add the true centers as rows to our original data
newdata = data.append(true_centers)
# show the percentiles of the centers
ctr_pcts = 100. * newdata.rank(axis=0, pct=True).loc[['Segment 0', 'Segment 1']].round(decimals=3)
print ctr_pcts
# visualize percentiles with heatmap
sns.heatmap(ctr_pcts, annot=True, cmap='Greens', fmt='.1f', linewidth=.1, square=True, cbar=False)
plt.xticks(rotation=45, ha='center')
plt.yticks(rotation=0)
plt.title('Percentile ranks of\nsegment centers');
回答:
In [53]:
# 显示预测结果
display(samples.head())
for i, pred in enumerate(sample_preds):
print "Sample point", i, "predicted to be in Cluster", pred
回答:
与之前的预测基本相符。
在对他们的服务或者是产品做细微的改变的时候,公司经常会使用A/B tests以确定这些改变会对客户产生积极作用还是消极作用。这个批发商希望考虑将他的派送服务从每周5天变为每周3天,但是他只会对他客户当中对此有积极反馈的客户采用。这个批发商应该如何利用客户分类来知道哪些客户对它的这个派送策略的改变有积极的反馈,如果有的话?你需要给出在这个情形下A/B 测试具体的实现方法,以及最终得出结论的依据是什么?
提示: 我们能假设这个改变对所有的客户影响都一致吗?我们怎样才能够确定它对于哪个类型的客户影响最大?
回答:
通过聚类技术,我们能够将原有的没有标记的数据集中的附加结构分析出来。因为每一个客户都有一个最佳的划分(取决于你选择使用的聚类算法),我们可以把用户分类作为数据的一个工程特征。假设批发商最近迎来十位新顾客,并且他已经为每位顾客每个产品类别年度采购额进行了预估。进行了这些估算之后,批发商该如何运用它的预估和非监督学习的结果来对这十个新的客户进行更好的预测?
提示:在下面的代码单元中,我们提供了一个已经做好聚类的数据(聚类结果为数据中的cluster属性),我们将在这个数据集上做一个小实验。尝试运行下面的代码看看我们尝试预测‘Region’的时候,如果存在聚类特征'cluster'与不存在相比对最终的得分会有什么影响?这对你有什么启发?
In [54]:
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, train_size=0.8, random_state=24)
clf = RandomForestClassifier(random_state=24)
clf.fit(X_train, y_train)
print "使用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)
print "不使用cluster特征的得分", clf.score(X_test, y_test)
回答: 得分基本相同,但是不使用cluster特征的得分略微低一些。 在数据有Label的情况下,即可以用于监督学习的情况。可以先用非监督学习来类聚数据,然后对比非监督学习与监督学习的结果进行比较,相互印证。增加对于模型,以及分析结果的信心。 另外,将使用非监督学习的预测出来的10个新用户的label,组成新的样本数据(feature,label),来训练出新的监督学习模型。(这样做的目的何在???)
【CodeReview20170725】 根据上一次review中给出的note,这里我们已经通过历史数据得到了非监督学习的结果,那么现在我们迎来了10个新的用户,实际上我们可以将这10个samples的类别预测出来,这样我们就得到了新用户的label,所以这里说,在这个情景中,我们可以使用非监督学习的成果,即得到的label,来实现监督学习的功能:利用得到的(feature,label)来学到一个监督学习模型。
In [55]:
# 根据‘Channel‘数据显示聚类的结果
vs.channel_results(reduced_data, outliers, pca_samples)
回答: 我类聚结果对于数据点的分割是类似的,都是左右两块区域对应两个分类。但是,两个分类的标签是不一样的。尤其是,零售商这个分类,刚好与我的分类是反的。对于左右两块区域的标签分别是:超市、零售商(便利店),而这里原题给出的标签是:零售商、旅馆/餐馆/咖啡店。原题对于两个分类给出的标签的依据是什么,不是很清楚。而我的依据是,总的采购量的大小。
注意: 当你写完了所有的代码,并且回答了所有的问题。你就可以把你的 iPython Notebook 导出成 HTML 文件。你可以在菜单栏,这样导出File -> Download as -> HTML (.html)把这个 HTML 和这个 iPython notebook 一起做为你的作业提交。