19-数据分析实战45讲

开篇词 | 你为什么需要数据分析能力？

你好，我是陈旸，清华大学计算机系博士毕业。清华有一门课，叫数据挖掘，正是通过这门课，我学会了如何从海量的数据中找到关联关系，以及如何进行价值挖掘。那时候感觉自己掌握了一门利器，就特别想找到一个钉子，来试试自己手里的这把锤子。

当时恰好赶上 2009 年微博的热潮。我用 3 个月的时间就积累了 4 万粉丝，一年的时间积累了上百万粉丝。这是怎么做到的呢？

通过数据采集，我收集了每天的微博热点，然后对热点进行抓取、去广告，再让机器定时自动进行发布。同时我让账号每天都去关注明星的粉丝列表，这样可以获得 15% 的回粉概率。久而久之，就会有源源不断的粉丝。

你看，其实就是数据分析帮我做到了微博的自动化运营。这还只是一个小例子，数据分析的影响已经渗透到了我们工作生活的方方面面。

通过数据分析，我们可以更好地了解用户画像，为企业做留存率、流失率等指标分析，进而精细化产品运营。

如果你关注比特币，数据分析可以帮助你预测比特币的走势。

面对生活中遇到的种种麻烦，数据分析也可以提供解决方案，比如信用卡反欺诈，自动屏蔽垃圾邮件等。

可以说，我们生活在数据驱动一切的时代，数据挖掘和数据分析就是这个时代的“淘金”，从国家、企业、组织到个人，都一定会关注各种数据，从这些数据中得到价值。

也正是这个原因，数据分析人才成了香饽饽，不管是数据分析师，数据分析工程师，还是数据产品经理，有数据思维的运营人员，都变得越来越抢手。你是不是也已经摩拳擦掌，做好了了解这一领域的准备呢？

我想在接下来的 15 周时间里，把自己在清华学习数据挖掘的体会和工作实践中对数据分析的理解，重新梳理整合呈现给你，和你一起在数据分析这个领域来一场急行军。

说了这么多数据分析的重要性，你是不是有这样的疑问：我也知道数据分析能力很重要，但是数据分析是不是很难？到底该怎么学呢？

其实这里有一些误区，数据分析并非遥不可及，它不难，掌握高效的学习方法很重要；但是它也不简单，需要你耐下性子，跟我一起来慢慢掌握数据分析的核心知识点和工具操作。

我招聘过一个实习生，很普通的本科学校。最开始他只会简单的 PHP 语法，实习期间薪水也就只有 3000 元，但到后来他不仅可以做爬虫抓取，还可以做数据分析，薪水就涨到了税后 1.3 万，这个进步用了不到一年的时间。

他的成长速度非常快，这是怎么做到的呢？

总结一下，就是他找到了高效的学习方法，我把它称为MAS 方法。

Multi-Dimension：想要掌握一个事物，就要从多个角度去认识它。

Ask：不懂就问，程序员大多都很羞涩，突破这一点，不懂就问最重要。

Sharing：最好的学习就是分享。用自己的语言讲出来，是对知识的进一步梳理。

所以学习这个专栏我们也用 MAS 方法，我来负责你和数据分析建立起多维度连接，你来负责提问和分享。

怎么和数据分析建立多维度连接呢？我特意把内容分成了三个大类。

第一类是基础概念。这是我们学习的基础，一定不能落下。

第二类是工具。这个部分可以很好地锻炼你的实操能力。

第三类是题库。题库的作用是帮你查漏补缺，在这个过程中，你会情不自禁地进行思考。

这个连接的过程，也是我们从“思维”到“工具”再到“实践”的一个突破过程。如果说重要性，一定是“思维”最重要，因为思维是底层逻辑和框架，可以让我们一通百通，举一反三，但是思维修炼也是最难的。所以，我强调把学习重心放在工具和实践上，即学即用，不断积累成就感，思维也就慢慢养成了。

说到底，学习数据分析的核心就是培养数据思维，掌握挖掘工具，熟练实践并积累经验。为了能带给你更好的学习效果，我在专栏里设计了五大模块。

1. 预习篇

我会给你介绍数据分析的全景图，和你进一步探讨最佳的学习路径。我还专门准备了 3 篇 Python 入门内容，如果你还没有 Python 基础，希望能帮你快速上手，如果你已掌握了 Python，可以当作一个复习。这么安排是因为 Python 是数据科学领域当之无愧的王牌语言，很多数据分析利器也是基于 Python 的（再或者，你也可以购买极客时间上的“零基础学 Python”视频课程）。

2. 基础篇

我会带你修炼数据思维，从数据分析的基础概念，到数据采集、数据处理以及数据可视化。我们一起从数据准备的整个流程上了解数据的方方面面。

3. 算法篇

算法是数据挖掘的精华所在，也是我们专栏的重点内容。我精选了 10 大算法，包括分类、聚类和预测三大类型。每个算法我们都从原理和案例两个维度来理解，达到即学即用的目的。

4. 实战篇

项目实战是我们学习的一个重要关卡。我准备了 5 个项目带你真实体验。比如在金融行业中，如何使用数据分析算法对信用卡违约率进行分析？现在的互联网产品都进入到千人千面的人工智能阶段，如何针对一个视频网站搭建视频推荐算法？

5. 工作篇

我选择了几个大家最关心的职场问题，比如面试时注意什么，职位晋升路径是怎样的等等，助你一臂之力。

我希望，通过这个专栏，你将有如下收获。

1. 数据和算法思维

这不仅是在技术上的思维模式，更是我们平时看待问题解决问题的思维方式。如果你将数据视为财富，将数据分析视为获得财富的工具，那么在大数据时代，你将获得更宽广的视野。

2. 工具

用好工具，你将拥有收集数据、处理数据、得到结果的能力，它会让你在工作中游刃有余。

3. 更好的工作机会和价值

无论是当前火爆的人工智能，还是数据算法工程师的市场，都很看重数据分析和数据处理的能力。从“思维”到“工具”再到“实践”，沿着这个路径拓展自己的能力边界，拥有更强的竞争力。

在你面前，即将开始一场数据科学之旅。我们一起用 15 周的时间，从算法原理、分析工具和实战案例三个维度体会数据科学之美。

在专栏学习的过程中，如果你遇到问题，不论是概念不懂，还是工具使用遇到 error，你都可以来找我。也希望你可以把自己的学习笔记分享出来，它不仅是最好的自我学习方法，也是最好的交流语言。

我愿意跟你一起，将这些看似“高大上”的内容琢磨得通俗易懂。当你完成这段旅程，你将会发现这个世界从来不缺少“石油”，而它们，正在等着你的勘探。

正式启程之前，我想邀请你聊聊自己对课程的期待，你如何看待数据挖掘和数据分析？你的工作和生活中有什么事情用到过数据思维吗？

01丨数据分析全景图及修炼指南

今天我们的学习正式开始，我想先给你一张数据分析的全景图，让你对后面的学习做到心中有数。

现在，你已经知道了数据分析在现代社会中的重要地位。掌握数据，就是掌握规律。当你了解了市场数据，对它进行分析，就可以得到市场规律。当你掌握了产品自身的数据，对它进行分析，就可以了解产品的用户来源、用户画像等等。所以说数据是个全新的视角。数据分析如此重要，它不仅是新时代的“数据结构 + 算法”，也更是企业争夺人才的高地。

当我们谈论数据分析的时候，都在讲些什么呢？

这里我可以把数据分析分成三个重要的组成部分。

数据采集。它是我们的原材料，也是最“接地气”的部分，因为任何分析都要有数据源。

数据挖掘。它可以说是最“高大上”的部分，也是整个商业价值所在。之所以要进行数据分析，就是要找到其中的规律，来指导我们的业务。因此数据挖掘的核心是挖掘数据的商业价值，也就是我们所谈的商业智能 BI。

数据可视化。它可以说是数据领域中万金油的技能，可以让我们直观地了解到数据分析的结果。

下面我来一一为你讲解一下这三个重要的部分。

数据采集

在数据采集部分中，你通常会和数据源打交道，然后使用工具进行采集。

在专栏里，我会告诉你都有哪些常用的数据源，以及如何获取它们。另外在工具使用中，你也将掌握“八爪鱼”这个自动抓取的神器，它可以帮你抓取 99% 的页面源。当然我也会教你如何编写 Python 爬虫。掌握 Python 爬虫的乐趣是无穷的。它不仅能让你获取微博上的热点评论，自动下载例如“王祖贤”的海报，还能自动给微博加粉丝，让你掌握自动化的快感。

数据挖掘

第二个部分是数据挖掘，它可以说是知识型的工程，相当于整个专栏中的“算法”部分。首先你要知道它的基本流程、十大算法、以及背后的数学基础。

这一部分我们会接触到一些概念，比如关联分析，Adaboost 算法等等，你可能对这些概念还是一知半解，没有关系，我会详细为你介绍这些“朋友”。

每讲完一个算法原理，我都会带你做一个项目的实战，我精选了一些典型的、有趣的项目，比如对泰坦尼克号乘客进行生存预测、对文档进行自动分类、以及导演是如何选择演员的等等。

掌握了数据挖掘，就好比手握水晶球一样，它会通过历史数据，告诉你未来会发生什么。当然它也会告诉你这件事发生的置信度是怎样的，置信度这个词你先记住就可以了，后面我们来学习它具体代表什么。

数据可视化

第三个就是数据可视化，这是一个非常重要的步骤，也是我们特别感兴趣的一个步骤。数据往往是隐性的，尤其是当数据量大的时候很难感知，可视化可以帮我们很好地理解这些数据的结构，以及分析结果的呈现。

如何进行数据可视化呢？有两种方法。

第一种就是使用 Python。在 Python 对数据进行清洗、挖掘的过程中，我们可以使用 Matplotlib、Seaborn 等第三方库进行呈现。

第二种就是使用第三方工具。如果你已经生成了 csv 格式文件，想要采用所见即所得的方式进行呈现，可以采用微图、DataV、Data GIF Maker 等第三方工具，它们可以很方便地对数据进行处理，还可以帮你制作呈现的效果。

数据采集和数据可视化的原理简单，容易理解。这两个部分注重的是工具的掌握，所以我会把重点放在讲解工具以及应用实战上。

虽然这些理论我会给你一一讲解，但纸上得来终觉浅，绝知此事要躬行。手拿地图，我们知道要去哪里，但是怎么去呢？我认为学习数据分析最好的方法是：在工具中灵活运用，在项目中加深理解。

修炼指南

刚才我们讲了数据分析全景图，包括数据采集、数据挖掘、数据可视化这三个部分。你可能觉得东西很多，无从下手，或者感觉数据挖掘涉及好多算法，有点“高深莫测”，掌握起来是不是会吃力。其实这些都是不必要的烦恼。

开篇词里我给你介绍了 MAS 学习法，有了这个方法，学习数据分析就是从“思维”到“工具”再到“实践”的一个过程。今天我会从更多的角度来和你分享我的学习经验，我们可以把今天的内容叫作“修炼指南”。

借用傅盛的话来说，人与人最大的差别在于“认知”，所谓成长就是认知的升级。

很多人存在对“认知“的误解，认为认知不就是概念么？那么你有没有想过，针对同一个概念，为什么不同的人掌握的程度是不一样的呢？

我们只有把知识转化为自己的语言，它才真正变成了我们自己的东西。这个转换的过程，就是认知的过程。

那么如何提升自己的学习吸收能力呢？简单地说，就是要“知行合一”。

如果说认知是大脑，那么工具就好比我们的双手，数据工程师和算法科学家每天打交道最多的就是工具。

如果你开始做数据分析的项目，你脑海中已经思考好了数据挖掘的算法模型，请牢记下面这两点原则。

1. 不重复造轮子

举个数据采集的例子，我见过很多公司，都有数据采集的需求，他们认为某些工具不能满足他们个性化的需求，因此决定招人专门做这项工作。而结果怎样呢？做了 1 年多的实践，工资投入几十万，结果发现 Bug 一大堆，最后还是选择了第三方工具。耗时耗力，还没什么成效。

一个模型是否有相关的类库可以使用——这几乎是每个程序员入行被告知的第一条准则。我也会对新人反复灌输这个概念。大部分情况下你都能找到类库来完成你的想法。

2. 工具决定效率

“不要重复造轮子”意味着首先需要找到一个可以用的轮子，也就是工具。我们该如何选择呢？

这取决于你要做的工作，工具没有好坏之分，只有适合与否。除去研究型的工作，大部分情况下，工程师会选择使用者最多的工具。因为：Bug 少、文档全、案例多。

比如 Python 在处理数据挖掘上就有很多第三方库，这些库都有大量的用户和帮助文档可以帮助你来上手。

在后面的课程里，我会给你介绍最常用的工具，这些工具会让你的数据挖掘事半功倍。

选择好工具之后，你要做的就是积累 “资产”了。我们很难记住大段的知识点，也背不下来工具的指令，但是我们通常能记住故事、做过的项目、做过的题目。这些题目和项目是你最先行的“资产”。

如何快速积累这些“资产”呢？这里我送你三个字：熟练度。

把题目完成只是第一步，关键在于训练我们工具使用的“熟练度”。

高中的时候，有一次我做“八皇后”的问题，第一次解答花了一个小时的时间。当时老师明确告诉我必须在 20 分钟内完成，我不敢相信，从解题、思考、动手，最后完成，1 个小时不算慢。但是后来我调整了思考的结构。最后我 6 分钟就可以完成那道题。

当熟练度增加的时候，你的思考认知模型也在逐渐提升。所以专栏中，我给你做了一个 “专属题库”，在专属题库中你可以进行自我评测，当然我也会对这些练习题进行讲解。在工作篇中，我也会和你一起分享面试技巧、探讨职场上的晋升之路。

总结

认知三步曲，从认知到工具，再到实战，是我最想给你分享的学习建议。我看到过很多同学上课的模式，以及很多人工作中的思考模式，我特别认同“人与人最大的区别是在认知”这个观点。

他们很听老师的理论，但是这些理论最后又都还给了老师。所以我希望你在后面的 15 周学习里可以做到以下几点。

记录下你每天的认知。尤其是每次课程后，对知识点的自我理解。

这些认知对应工具的哪些操作。用工具来表达你对知识点的掌握，并用自己的语言记录下这些操作笔记。

做更多练习来巩固你的认知。我们学习的内容对于大部分外人来说，就像“开车”一样，很酷。我们学习的内容，对于要掌握的人来说，也像“开车”一样，其实并不难，而且很多人已经上路了。你需要的就是更多的练习。

不知道这一讲中你有没有不清楚的地方，如果有，一定要提问，而且问题越具体越好，我会在留言区和你讨论。如果你理解了我们讲的内容，也一定要分享给我，让我和你一起体验这种认知升级的喜悦。

最后我想问你个问题，就当做个小调查吧，你平时是怎么学习的呢？会做学习笔记吗？期待在留言区看到你的答案。也欢迎你把今天的内容分享给身边的朋友，和他一起学习。

02丨学习数据挖掘的最佳路径是什么？

上一节中，我给你分享了数据分析的全景图，其中最关键的部分就是数据挖掘，那什么是数据挖掘呢？

想象一下，茫茫的大海上，孤零零地屹立着钻井，想要从大海中开采出宝贵的石油。

对于普通人来说，大海是很难感知的，就更不用说找到宝藏了。但对于熟练的石油开采人员来说，大海是有坐标的。他们对地质做勘探，分析地质构造，从而发现哪些地方更可能有石油。然后用开采工具，进行深度挖掘，直到打到石油为止。

大海、地质信息、石油对开采人员来说就是数据源、地理位置、以及分析得到的结果。

而我们要做的数据挖掘工作，就好像这个钻井一样，通过分析这些数据，从庞大的数据中发现规律，找到宝藏。

数据挖掘，从知识清单开始

我们第一天学开车的时候一定不会直接上路，而是要你先学习基本的知识，然后再进行上车模拟。

只有对知识有全面的认知，才能确保在以后的工作中即使遇到了问题，也可以快速定位问题所在，然后找方法去对应和解决。

所以我列了一个数据挖掘的知识清单，分别是数据挖掘的基本流程、十大算法和数学原理，以此来开启我们的学习之旅。

数据挖掘的基本流程

在正式讲数据挖掘知识清单之前，我先和你聊聊数据挖掘的基本流程。

数据挖掘的过程可以分成以下 6 个步骤。

商业理解：数据挖掘不是我们的目的，我们的目的是更好地帮助业务，所以第一步我们要从商业的角度理解项目需求，在这个基础上，再对数据挖掘的目标进行定义。

数据理解：尝试收集部分数据，然后对数据进行探索，包括数据描述、数据质量验证等。这有助于你对收集的数据有个初步的认知。

数据准备：开始收集数据，并对数据进行清洗、数据集成等操作，完成数据挖掘前的准备工作。

模型建立：选择和应用各种数据挖掘模型，并进行优化，以便得到更好的分类结果。

模型评估：对模型进行评价，并检查构建模型的每个步骤，确认模型是否实现了预定的商业目标。

上线发布：模型的作用是从数据中找到金矿，也就是我们所说的“知识”，获得的知识需要转化成用户可以使用的方式，呈现的形式可以是一份报告，也可以是实现一个比较复杂的、可重复的数据挖掘过程。数据挖掘结果如果是日常运营的一部分，那么后续的监控和维护就会变得重要。

数据挖掘的十大算法

为了进行数据挖掘任务，数据科学家们提出了各种模型，在众多的数据挖掘模型中，国际权威的学术组织 ICDM （the IEEE International Conference on Data Mining）评选出了十大经典的算法。

按照不同的目的，我可以将这些算法分成四类，以便你更好的理解。

l 分类算法：C4.5，朴素贝叶斯（Naive Bayes），SVM，KNN，Adaboost，CART

l 聚类算法：K-Means，EM

l 关联分析：Apriori

l 连接分析：PageRank

1. C4.5

C4.5 算法是得票最高的算法，可以说是十大算法之首。C4.5 是决策树的算法，它创造性地在决策树构造过程中就进行了剪枝，并且可以处理连续的属性，也能对不完整的数据进行处理。它可以说是决策树分类中，具有里程碑式意义的算法。

2. 朴素贝叶斯（Naive Bayes）

朴素贝叶斯模型是基于概率论的原理，它的思想是这样的：对于给出的未知物体想要进行分类，就需要求解在这个未知物体出现的条件下各个类别出现的概率，哪个最大，就认为这个未知物体属于哪个分类。

3. SVM

SVM 的中文叫支持向量机，英文是 Support Vector Machine，简称 SVM。SVM 在训练中建立了一个超平面的分类模型。如果你对超平面不理解，没有关系，我在后面的算法篇会给你进行介绍。

4. KNN

KNN 也叫 K 最近邻算法，英文是 K-Nearest Neighbor。所谓 K 近邻，就是每个样本都可以用它最接近的 K 个邻居来代表。如果一个样本，它的 K 个最接近的邻居都属于分类 A，那么这个样本也属于分类 A。

5. AdaBoost

Adaboost 在训练中建立了一个联合的分类模型。boost 在英文中代表提升的意思，所以 Adaboost 是个构建分类器的提升算法。它可以让我们多个弱的分类器组成一个强的分类器，所以 Adaboost 也是一个常用的分类算法。

6. CART

CART 代表分类和回归树，英文是 Classification and Regression Trees。像英文一样，它构建了两棵树：一棵是分类树，另一个是回归树。和 C4.5 一样，它是一个决策树学习方法。

7. Apriori

Apriori 是一种挖掘关联规则（association rules）的算法，它通过挖掘频繁项集（frequent item sets）来揭示物品之间的关联关系，被广泛应用到商业挖掘和网络安全等领域中。频繁项集是指经常出现在一起的物品的集合，关联规则暗示着两种物品之间可能存在很强的关系。

8. K-Means

K-Means 算法是一个聚类算法。你可以这么理解，最终我想把物体划分成 K 类。假设每个类别里面，都有个“中心点”，即意见领袖，它是这个类别的核心。现在我有一个新点要归类，这时候就只要计算这个新点与 K 个中心点的距离，距离哪个中心点近，就变成了哪个类别。

9. EM

EM 算法也叫最大期望算法，是求参数的最大似然估计的一种方法。原理是这样的：假设我们想要评估参数 A 和参数 B，在开始状态下二者都是未知的，并且知道了 A 的信息就可以得到 B 的信息，反过来知道了 B 也就得到了 A。可以考虑首先赋予 A 某个初值，以此得到 B 的估值，然后从 B 的估值出发，重新估计 A 的取值，这个过程一直持续到收敛为止。

EM 算法经常用于聚类和机器学习领域中。

10. PageRank

PageRank 起源于论文影响力的计算方式，如果一篇文论被引入的次数越多，就代表这篇论文的影响力越强。同样 PageRank 被 Google 创造性地应用到了网页权重的计算中：当一个页面链出的页面越多，说明这个页面的“参考文献”越多，当这个页面被链入的频率越高，说明这个页面被引用的次数越高。基于这个原理，我们可以得到网站的权重划分。

算法可以说是数据挖掘的灵魂，也是最精华的部分。这 10 个经典算法在整个数据挖掘领域中的得票最高的，后面的一些其他算法也基本上都是在这个基础上进行改进和创新。今天你先对十大算法有一个初步的了解，你只需要做到心中有数就可以了，具体内容不理解没有关系，后面我会详细给你进行讲解。

数据挖掘的数学原理

我说了这么多数据挖掘中的经典算法，但是如果你不了解概率论和数理统计，还是很难掌握算法的本质；如果你不懂线性代数，就很难理解矩阵和向量运作在数据挖掘中的价值；如果你没有最优化方法的概念，就对迭代收敛理解不深。所以说，想要更深刻地理解数据挖掘的方法，就非常有必要了解它后背的数学原理。

1. 概率论与数理统计

概率论在我们上大学的时候，基本上都学过，不过大学里老师教的内容，偏概率的多一些，统计部分讲得比较少。在数据挖掘里使用到概率论的地方就比较多了。比如条件概率、独立性的概念，以及随机变量、多维随机变量的概念。

很多算法的本质都与概率论相关，所以说概率论与数理统计是数据挖掘的重要数学基础。

2. 线性代数

向量和矩阵是线性代数中的重要知识点，它被广泛应用到数据挖掘中，比如我们经常会把对象抽象为矩阵的表示，一幅图像就可以抽象出来是一个矩阵，我们也经常计算特征值和特征向量，用特征向量来近似代表物体的特征。这个是大数据降维的基本思路。

基于矩阵的各种运算，以及基于矩阵的理论成熟，可以帮我们解决很多实际问题，比如 PCA 方法、SVD 方法，以及 MF、NMF 方法等在数据挖掘中都有广泛的应用。

3. 图论

社交网络的兴起，让图论的应用也越来越广。人与人的关系，可以用图论上的两个节点来进行连接，节点的度可以理解为一个人的朋友数。我们都听说过人脉的六度理论，在 Facebook 上被证明平均一个人与另一个人的连接，只需要 3.57 个人。当然图论对于网络结构的分析非常有效，同时图论也在关系挖掘和图像分割中有重要的作用。

4. 最优化方法

最优化方法相当于机器学习中自我学习的过程，当机器知道了目标，训练后与结果存在偏差就需要迭代调整，那么最优化就是这个调整的过程。一般来说，这个学习和迭代的过程是漫长、随机的。最优化方法的提出就是用更短的时间得到收敛，取得更好的效果。

总结

今天我列了下学习数据挖掘你要掌握的知识清单，只有你对数据挖掘的流程、算法、原理有更深的理解，你才能在实际工作中更好地运用，我将在后面的章节中对它们进行一一介绍。

最后给你留道思考题吧。

今天我给你讲了如何学习数据挖掘，你从中有什么样的体会呢？如果某电商网站想挖掘商品之间的关联关系，从而提升销售额，你觉得可以采用上面的哪个算法？为什么？

欢迎在留言区和我讨论，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起来交流，一起来进步。

03丨Python基础语法：开始你的Python之旅

上一节课我跟你分享了数据挖掘的最佳学习路径，相信你对接下来的学习已经心中有数了。今天我们继续预习课，我会用三篇文章，分别对 Python 的基础语法、NumPy 和 Pandas 进行讲解，带你快速入门 Python 语言。如果你已经有 Python 基础了，那先恭喜你已经掌握了这门简洁而高效的语言，这几节课你可以跳过，或者也可以当作复习，自己查漏补缺，你还可以在留言区分享自己的 Python 学习和使用心得。

好了，你现在心中是不是有个问题，要学好数据分析，一定要掌握 Python 吗？

我的答案是，想学好数据分析，你最好掌握 Python 语言。为什么这么说呢？

首先，在一份关于开发语言的调查中，使用过 Python 的开发者，80% 都会把 Python 作为自己的主要语言。Python 已经成为发展最快的主流编程语言，从众多开发语言中脱颖而出，深受开发者喜爱。其次，在数据分析领域中，使用 Python 的开发者是最多的，远超其他语言之和。最后，Python 语言简洁，有大量的第三方库，功能强大，能解决数据分析的大部分问题，这一点我下面具体来说。

Python 语言最大的优点是简洁，它虽然是 C 语言写的，但是摒弃了 C 语言的指针，这就让代码非常简洁明了。同样的一行 Python 代码，甚至相当于 5 行 Java 代码。我们读 Python 代码就像是读英文一样直观，这就能让程序员更好地专注在问题解决上，而不是在语言本身。

当然除了 Python 自身的特点，Python 还有强大的开发者工具。在数据科学领域，Python 有许多非常著名的工具库：比如科学计算工具 NumPy 和 Pandas 库，深度学习工具 Keras 和 TensorFlow，以及机器学习工具 Scikit-learn，使用率都非常高。

总之，如果你想在数据分析、机器学习等数据科学领域有所作为，那么掌握一项语言，尤其是 Python 语言的使用是非常有必要的，尤其是我们刚提到的这些工具，熟练掌握它们会让你事半功倍。

安装及 IDE 环境

了解了为什么要学 Python，接下来就带你快速开始你的第一个 Python 程序，所以我们先来了解下如何安装和搭建 IDE 环境。

Python 的版本选择

Python 主要有两个版本： 2.7.x 和 3.x。两个版本之间存在一些差异，但并不大，它们语法不一样的地方不到 10%。

另一个事实就是：大部分 Python 库都同时支持 Python 2.7.x 和 3.x 版本。虽然官方称 Python2.7 只维护到 2020 年，但是我想告诉你的是：千万不要忽视 Python2.7，它的寿命远不止到 2020 年，而且这两年 Python2.7 还是占据着 Python 版本的统治地位。一份调查显示：在 2017 年的商业项目中 2.7 版本依然是主流，占到了 63.7%，即使这两年 Python3.x 版本使用的增速较快，但实际上 Python3.x 在 2008 年就已经有了。

那么你可能会问：这两个版本该如何选择呢？

版本选择的标准就是看你的项目是否会依赖于 Python2.7 的包，如果有依赖的就只能使用 Python2.7，否则你可以用 Python 3.x 开始全新的项目。

Python IDE 推荐

确定了版本问题后，怎么选择 Python IDE 呢？有众多优秀的选择，这里推荐几款。

1. PyCharm

这是一个跨平台的 Python 开发工具，可以帮助用户在使用 Python 时提升效率，比如：调试、语法高亮、代码跳转、自动完成、智能提示等。

2. Sublime Text

SublimeText 是个著名的编辑器，Sublime Text3 基本上可以 1 秒即启动，反应速度很快。同时它对 Python 的支持也很到位，具有代码高亮、语法提示、自动完成等功能。

3. Vim

Vim 是一个简洁、高效的工具，速度很快，可以做任何事，从来不崩溃。不过 Vim 相比于 Sublime Text 上手有一定难度，配置起来有些麻烦。

4. Eclipse+PyDev

习惯使用 Java 的人一定对 Eclipse 这个 IDE 不陌生，那么使用 Eclipse+PyDev 插件会是一个很好的选择，这样熟悉 Eclipse 的开发者可以轻易上手。

如果上面这些 IDE 你之前都没有怎么用过，那么推荐你使用 Sublime Text，上手简单，反应速度快。

Python 基础语法

环境配置好后，我们就来快速学习几个 Python 必会的基础语法。我假设你是 Python 零基础，但已经有一些其他编程语言的基础。下面我们一一来看。

输入与输出

name = raw_input("What's your name?")

sum = 100+100

print ('hello,%s' %name)

print ('sum = %d' %sum)

raw_input 是 Python2.7 的输入函数，在 python3.x 里可以直接使用 input，赋值给变量 name，print 是输出函数，%name 代表变量的数值，因为是字符串类型，所以在前面用的 %s 作为代替。

这是运行结果：

What's your name?cy

hello,cy

sum = 200

判断语句：if … else …

if score>= 90:

       print 'Excellent'

else:

       if score < 60:

           print 'Fail'

       else:

           print 'Good Job'

if … else … 是经典的判断语句，需要注意的是在 if expression 后面有个冒号，同样在 else 后面也存在冒号。

另外需要注意的是，Python 不像其他语言一样使用{}或者 begin…end 来分隔代码块，而是采用代码缩进和冒号的方式来区分代码之间的层次关系。所以代码缩进在 Python 中是一种语法，如果代码缩进不统一，比如有的是 tab 有的是空格，会怎样呢？会产生错误或者异常。相同层次的代码一定要采用相同层次的缩进。

循环语句：for … in

sum = 0

for number in range(11):

    sum = sum + number

print sum

运行结果：

55

for 循环是一种迭代循环机制，迭代即重复相同的逻辑操作。如果规定循环的次数，我们可以使用 range 函数，它在 for 循环中比较常用。range(11) 代表从 0 到 10，不包括 11，也相当于 range(0,11)，range 里面还可以增加步长，比如 range(1,11,2) 代表的是 [1,3,5,7,9]。

循环语句: while

sum = 0

number = 1

while number < 11:

       sum = sum + number

       number = number + 1

print sum

运行结果：

55

1 到 10 的求和也可以用 while 循环来写，这里 while 控制了循环的次数。while 循环是条件循环，在 while 循环中对于变量的计算方式更加灵活。因此 while 循环适合循环次数不确定的循环，而 for 循环的条件相对确定，适合固定次数的循环。

数据类型：列表、元组、字典、集合

列表：[]

lists = ['a','b','c']

lists.append('d')

print lists

print len(lists)

lists.insert(0,'mm')

lists.pop()

print lists

运行结果：

['a', 'b', 'c', 'd']

4

['mm', 'a', 'b', 'c']

列表是 Python 中常用的数据结构，相当于数组，具有增删改查的功能，我们可以使用 len() 函数获得 lists 中元素的个数；使用 append() 在尾部添加元素，使用 insert() 在列表中插入元素，使用 pop() 删除尾部的元素。

元组 (tuple)

tuples = ('tupleA','tupleB')

print tuples[0]

运行结果：

tupleA

元组 tuple 和 list 非常类似，但是 tuple 一旦初始化就不能修改。因为不能修改所以没有 append(), insert() 这样的方法，可以像访问数组一样进行访问，比如 tuples[0]，但不能赋值。

字典 {dictionary}

# -*- coding: utf-8 -*

# 定义一个 dictionary

score = {'guanyu':95,'zhangfei':96}

# 添加一个元素

score['zhaoyun'] = 98

print score

# 删除一个元素

score.pop('zhangfei')

# 查看 key 是否存在

print 'guanyu' in score

# 查看一个 key 对应的值

print score.get('guanyu')

print score.get('yase',99)

运行结果：

{'guanyu': 95, 'zhaoyun': 98, 'zhangfei': 96}

True

95

99

字典其实就是{key, value}，多次对同一个 key 放入 value，后面的值会把前面的值冲掉，同样字典也有增删改查。增加字典的元素相当于赋值，比如 score[‘zhaoyun’] = 98，删除一个元素使用 pop，查询使用 get，如果查询的值不存在，我们也可以给一个默认值，比如 score.get(‘yase’,99)。

集合：set

s = set(['a', 'b', 'c'])

s.add('d')

s.remove('b')

print s

print 'c' in s

运行结果：

set(['a', 'c', 'd'])

True

集合 set 和字典 dictory 类似，不过它只是 key 的集合，不存储 value。同样可以增删查，增加使用 add，删除使用 remove，查询看某个元素是否在这个集合里，使用 in。

注释：#

注释在 python 中使用 #，如果注释中有中文，一般会在代码前添加 # -- coding: utf-8 -。

如果是多行注释，使用三个单引号，或者三个双引号，比如：

# -*- coding: utf-8 -*

'''

这是多行注释，用三个单引号

这是多行注释，用三个单引号

这是多行注释，用三个单引号

'''

引用模块 / 包：import

# 导入一个模块

import model_name

# 导入多个模块

import module_name1,module_name2

# 导入包中指定模块

from package_name import moudule_name

# 导入包中所有模块

from package_name import *

Python 语言中 import 的使用很简单，直接使用 import module_name 语句导入即可。这里 import 的本质是什么呢？import 的本质是路径搜索。import 引用可以是模块 module，或者包 package。

针对 module，实际上是引用一个.py 文件。而针对 package，可以采用 from … import …的方式，这里实际上是从一个目录中引用模块，这时目录结构中必须带有一个 __init__.py 文件。

函数：def

def addone(score):

   return score + 1

print addone(99)

运行结果：

100

函数代码块以 def 关键词开头，后接函数标识符名称和圆括号，在圆括号里是传进来的参数，然后通过 return 进行函数结果得反馈。

A+B Problem

上面的讲的这些基础语法，我们可以用 sumlime text 编辑器运行 Python 代码。另外，告诉你一个相当高效的方法，你可以充分利用一个刷题进阶的网址： http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1 ，这是浙江大学 ACM 的 OnlineJudge。

什么是 OnlineJudge 呢？它实际上是一个在线答题系统，做题后你可以在后台提交代码，然后 OnlineJudge 会告诉你运行的结果，如果结果正确就反馈：Accepted，如果错误就反馈：Wrong Answer。

不要小看这样的题目，也会存在编译错误、内存溢出、运行超时等等情况。所以题目对编码的质量要求还是挺高的。下面我就给你讲讲这道 A+B 的题目，你可以自己做练习，然后在后台提交答案。

题目：A+B

输入格式：有一系列的整数对 A 和 B，以空格分开。

输出格式：对于每个整数对 A 和 B，需要给出 A 和 B 的和。

输入输出样例：

INPUT

1 5

OUTPUT

6

针对这道题，我给出了下面的答案：

while True:

       try:

              line = raw_input()

              a = line.split()

              print int(a[0]) + int(a[1])

       except:

              break

当然每个人可以有不同的解法，官方也有 Python 的答案，这里给你介绍这个 OnlineJudge 是因为：

可以在线得到反馈，提交代码后，系统会告诉你对错。而且你能看到每道题的正确率，和大家提交后反馈的状态；

有社区论坛可以进行交流学习；

对算法和数据结构的提升大有好处，当然对数据挖掘算法的灵活运用和整个编程基础的提升都会有很大的帮助。

总结

现在我们知道，Python 毫无疑问是数据分析中最主流的语言。今天我们学习了这么多 Python 的基础语法，你是不是体会到了它的简洁。如果你有其他编程语言基础，相信你会非常容易地转换成 Python 语法的。那到此，Python 我们也就算入门了。有没有什么方法可以在此基础上快速提升 Python 编程水平呢？给你分享下我的想法。

在日常工作中，我们解决的问题都不属于高难度的问题，大部分人做的都是开发工作而非科研项目。所以我们要提升的主要是熟练度，而通往熟练度的唯一路径就是练习、练习、再练习！

如果你是第一次使用 Python，不用担心，最好的方式就是直接做题。把我上面的例子都跑一遍，自己在做题中体会。

如果你想提升自己的编程基础，尤其是算法和数据结构相关的能力，因为这个在后面的开发中都会用到。那么 ACM Online Judge 是非常好的选择，勇敢地打开这扇大门，把它当作你进阶的好工具。

你可以从 Accepted 比率高的题目入手，你做对的题目数越多，你的排名也会越来越往前，这意味着你的编程能力，包括算法和数据结构的能力都有了提升。另外这种在社区中跟大家一起学习，还能排名，就像游戏一样，让学习更有趣味，从此不再孤独。

我在文章中多次强调练习的作用，这样可以增加你对数据分析相关内容的熟练度。所以我给你出了两道练习题，你可以思考下如何来做，欢迎把答案放到评论下面，我也会和你一起在评论区进行讨论。

如果我想在 Python 中引用 scikit-learn 库该如何引用？

求 1+3+5+7+…+99 的求和，用 Python 该如何写？

欢迎你把今天的内容分享给身边的朋友，和他一起掌握 Python 这门功能强大的语言。

04丨Python科学计算：用NumPy快速处理数据

上一节我讲了 Python 的基本语法，今天我来给你讲下 Python 中一个非常重要的第三方库 NumPy。

它不仅是 Python 中使用最多的第三方库，而且还是 SciPy、Pandas 等数据科学的基础库。它所提供的数据结构比 Python 自身的“更高级、更高效”，可以这么说，NumPy 所提供的数据结构是 Python 数据分析的基础。

我上次讲到了 Python 数组结构中的列表 list，它实际上相当于一个数组的结构。而 NumPy 中一个关键数据类型就是关于数组的，那为什么还存在这样一个第三方的数组结构呢？

实际上，标准的 Python 中，用列表 list 保存数组的数值。由于列表中的元素可以是任意的对象，所以列表中 list 保存的是对象的指针。虽然在 Python 编程中隐去了指针的概念，但是数组有指针，Python 的列表 list 其实就是数组。这样如果我要保存一个简单的数组 [0,1,2]，就需要有 3 个指针和 3 个整数的对象，这样对于 Python 来说是非常不经济的，浪费了内存和计算时间。

使用 NumPy 让你的 Python 科学计算更高效

为什么要用 NumPy 数组结构而不是 Python 本身的列表 list？这是因为列表 list 的元素在系统内存中是分散存储的，而 NumPy 数组存储在一个均匀连续的内存块中。这样数组计算遍历所有的元素，不像列表 list 还需要对内存地址进行查找，从而节省了计算资源。

另外在内存访问模式中，缓存会直接把字节块从 RAM 加载到 CPU 寄存器中。因为数据连续的存储在内存中，NumPy 直接利用现代 CPU 的矢量化指令计算，加载寄存器中的多个连续浮点数。另外 NumPy 中的矩阵计算可以采用多线程的方式，充分利用多核 CPU 计算资源，大大提升了计算效率。

当然除了使用 NumPy 外，你还需要一些技巧来提升内存和提高计算资源的利用率。一个重要的规则就是：避免采用隐式拷贝，而是采用就地操作的方式。举个例子，如果我想让一个数值 x 是原来的两倍，可以直接写成 x*=2，而不要写成 y=x*2。

这样速度能快到 2 倍甚至更多。

既然 NumPy 这么厉害，你该从哪儿入手学习呢？在 NumPy 里有两个重要的对象：ndarray（N-dimensional array object）解决了多维数组问题，而 ufunc（universal function object）则是解决对数组进行处理的函数。下面，我就带你一一来看。

ndarray 对象

ndarray 实际上是多维数组的含义。在 NumPy 数组中，维数称为秩（rank），一维数组的秩为 1，二维数组的秩为 2，以此类推。在 NumPy 中，每一个线性的数组称为一个轴（axes），其实秩就是描述轴的数量。

下面，你来看 ndarray 对象是如何创建数组的，又是如何处理结构数组的呢？

创建数组

import numpy as np

a = np.array([1, 2, 3])

b = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

b[1,1]=10

print a.shape

print b.shape

print a.dtype

print b

运行结果：

(3L,)

(3L, 3L)

int32

[[ 1  2  3]

 [ 4 10  6]

 [ 7  8  9]]

创建数组前，你需要引用 NumPy 库，可以直接通过 array 函数创建数组，如果是多重数组，比如示例里的 b，那么该怎么做呢？你可以先把一个数组作为一个元素，然后嵌套起来，比如示例 b 中的 [1,2,3] 就是一个元素，然后 [4,5,6][7,8,9] 也是作为元素，然后把三个元素再放到 [] 数组里，赋值给变量 b。

当然数组也是有属性的，比如你可以通过函数 shape 属性获得数组的大小，通过 dtype 获得元素的属性。如果你想对数组里的数值进行修改的话，直接赋值即可，注意下标是从 0 开始计的，所以如果你想对 b 数组，九宫格里的中间元素进行修改的话，下标应该是 [1,1]。

结构数组

如果你想统计一个班级里面学生的姓名、年龄，以及语文、英语、数学成绩该怎么办？当然你可以用数组的下标来代表不同的字段，比如下标为 0 的是姓名、下标为 1 的是年龄等，但是这样不显性。

实际上在 C 语言里，可以定义结构数组，也就是通过 struct 定义结构类型，结构中的字段占据连续的内存空间，每个结构体占用的内存大小都相同，那在 NumPy 中是怎样操作的呢？

import numpy as np

persontype = np.dtype({

    'names':['name', 'age', 'chinese', 'math', 'english'],

    'formats':['S32','i', 'i', 'i', 'f']})

peoples = np.array([("ZhangFei",32,75,100, 90),("GuanYu",24,85,96,88.5),

       ("ZhaoYun",28,85,92,96.5),("HuangZhong",29,65,85,100)],

    dtype=persontype)

ages = peoples[:]['age']

chineses = peoples[:]['chinese']

maths = peoples[:]['math']

englishs = peoples[:]['english']

print np.mean(ages)

print np.mean(chineses)

print np.mean(maths)

print np.mean(englishs)

运行结果：

28.25

77.5

93.25

93.75

你看下这个例子，首先在 NumPy 中是用 dtype 定义的结构类型，然后在定义数组的时候，用 array 中指定了结构数组的类型 dtype=persontype，这样你就可以自由地使用自定义的 persontype 了。比如想知道每个人的语文成绩，就可以用 chineses = peoples[:][‘chinese’]，当然 NumPy 中还有一些自带的数学运算，比如计算平均值使用 np.mean。

ufunc 运算

ufunc 是 universal function 的缩写，是不是听起来就感觉功能非常强大？确如其名，它能对数组中每个元素进行函数操作。NumPy 中很多 ufunc 函数计算速度非常快，因为都是采用 C 语言实现的。

连续数组的创建

NumPy 可以很方便地创建连续数组，比如我使用 arange 或 linspace 函数进行创建：

x1 = np.arange(1,11,2)

x2 = np.linspace(1,9,5)

np.arange 和 np.linspace 起到的作用是一样的，都是创建等差数组。这两个数组的结果 x1,x2 都是 [1 3 5 7 9]。结果相同，但是你能看出来创建的方式是不同的。

arange() 类似内置函数 range()，通过指定初始值、终值、步长来创建等差数列的一维数组，默认是不包括终值的。

linspace 是 linear space 的缩写，代表线性等分向量的含义。linspace() 通过指定初始值、终值、元素个数来创建等差数列的一维数组，默认是包括终值的。

算数运算

通过 NumPy 可以自由地创建等差数组，同时也可以进行加、减、乘、除、求 n 次方和取余数。

x1 = np.arange(1,11,2)

x2 = np.linspace(1,9,5)

print np.add(x1, x2)

print np.subtract(x1, x2)

print np.multiply(x1, x2)

print np.divide(x1, x2)

print np.power(x1, x2)

print np.remainder(x1, x2)

运行结果：

[ 2.  6. 10. 14. 18.]

[0. 0. 0. 0. 0.]

[ 1.  9. 25. 49. 81.]

[1. 1. 1. 1. 1.]

[1.00000000e+00 2.70000000e+01 3.12500000e+03 8.23543000e+05

 3.87420489e+08]

[0. 0. 0. 0. 0.]

我还以 x1, x2 数组为例，求这两个数组之间的加、减、乘、除、求 n 次方和取余数。在 n 次方中，x2 数组中的元素实际上是次方的次数，x1 数组的元素为基数。

在取余函数里，你既可以用 np.remainder(x1, x2)，也可以用 np.mod(x1, x2)，结果是一样的。

统计函数

如果你想要对一堆数据有更清晰的认识，就需要对这些数据进行描述性的统计分析，比如了解这些数据中的最大值、最小值、平均值，是否符合正态分布，方差、标准差多少等等。它们可以让你更清楚地对这组数据有认知。

下面我来介绍下在 NumPy 中如何使用这些统计函数。

计数组 / 矩阵中的最大值函数 amax()，最小值函数 amin()

import numpy as np

a = np.array([[1,2,3], [4,5,6], [7,8,9]])

print np.amin(a)

print np.amin(a,0)

print np.amin(a,1)

print np.amax(a)

print np.amax(a,0)

print np.amax(a,1)

运行结果：

1

[1 2 3]

[1 4 7]

9

[7 8 9]

[3 6 9]

amin() 用于计算数组中的元素沿指定轴的最小值。对于一个二维数组 a，amin(a) 指的是数组中全部元素的最小值，amin(a,0) 是延着 axis=0 轴的最小值，axis=0 轴是把元素看成了 [1,4,7], [2,5,8], [3,6,9] 三个元素，所以最小值为 [1,2,3]，amin(a,1) 是延着 axis=1 轴的最小值，axis=1 轴是把元素看成了 [1,2,3], [4,5,6], [7,8,9] 三个元素，所以最小值为 [1,4,7]。同理 amax() 是计算数组中元素沿指定轴的最大值。

统计最大值与最小值之差 ptp()

a = np.array([[1,2,3], [4,5,6], [7,8,9]])

print np.ptp(a)

print np.ptp(a,0)

print np.ptp(a,1)

运行结果：

8

[6 6 6]

[2 2 2]

对于相同的数组 a，np.ptp(a) 可以统计数组中最大值与最小值的差，即 9-1=8。同样 ptp(a,0) 统计的是沿着 axis=0 轴的最大值与最小值之差，即 7-1=6（当然 8-2=6,9-3=6，第三行减去第一行的 ptp 差均为 6），ptp(a,1) 统计的是沿着 axis=1 轴的最大值与最小值之差，即 3-1=2（当然 6-4=2, 9-7=2，即第三列与第一列的 ptp 差均为 2）。

统计数组的百分位数 percentile()

a = np.array([[1,2,3], [4,5,6], [7,8,9]])

print np.percentile(a, 50)

print np.percentile(a, 50, axis=0)

print np.percentile(a, 50, axis=1)

运行结果：

5.0

[4. 5. 6.]

[2. 5. 8.]

同样，percentile() 代表着第 p 个百分位数，这里 p 的取值范围是 0-100，如果 p=0，那么就是求最小值，如果 p=50 就是求平均值，如果 p=100 就是求最大值。同样你也可以求得在 axis=0 和 axis=1 两个轴上的 p% 的百分位数。

统计数组中的中位数 median()、平均数 mean()

a = np.array([[1,2,3], [4,5,6], [7,8,9]])

# 求中位数

print np.median(a)

print np.median(a, axis=0)

print np.median(a, axis=1)

# 求平均数

print np.mean(a)

print np.mean(a, axis=0)

print np.mean(a, axis=1)

运行结果：

5.0

[4. 5. 6.]

[2. 5. 8.]

5.0

[4. 5. 6.]

[2. 5. 8.]

你可以用 median() 和 mean() 求数组的中位数、平均值，同样也可以求得在 axis=0 和 1 两个轴上的中位数、平均值。你可以自己练习下看看运行结果。

统计数组中的加权平均值 average()

a = np.array([1,2,3,4])

wts = np.array([1,2,3,4])

print np.average(a)

print np.average(a,weights=wts)

运行结果：

2.5

3.0

average() 函数可以求加权平均，加权平均的意思就是每个元素可以设置个权重，默认情况下每个元素的权重是相同的，所以 np.average(a)=(1+2+3+4)/4=2.5，你也可以指定权重数组 wts=[1,2,3,4]，这样加权平均 np.average(a,weights=wts)=(1*1+2*2+3*3+4*4)/(1+2+3+4)=3.0。

统计数组中的标准差 std()、方差 var()

a = np.array([1,2,3,4])

print np.std(a)

print np.var(a)

运行结果：

1.118033988749895

1.25

方差的计算是指每个数值与平均值之差的平方求和的平均值，即 mean((x - x.mean())** 2)。标准差是方差的算术平方根。在数学意义上，代表的是一组数据离平均值的分散程度。所以 np.var(a)=1.25, np.std(a)=1.118033988749895。

NumPy 排序

排序是算法中使用频率最高的一种，也是在数据分析工作中常用的方法，计算机专业的同学会在大学期间的算法课中学习。

那么这些排序算法在 NumPy 中实现起来其实非常简单，一条语句就可以搞定。这里你可以使用 sort 函数，sort(a, axis=-1, kind=‘quicksort’, order=None)，默认情况下使用的是快速排序；在 kind 里，可以指定 quicksort、mergesort、heapsort 分别表示快速排序、合并排序、堆排序。同样 axis 默认是 -1，即沿着数组的最后一个轴进行排序，也可以取不同的 axis 轴，或者 axis=None 代表采用扁平化的方式作为一个向量进行排序。另外 order 字段，对于结构化的数组可以指定按照某个字段进行排序。

a = np.array([[4,3,2],[2,4,1]])

print np.sort(a)

print np.sort(a, axis=None)

print np.sort(a, axis=0)

print np.sort(a, axis=1)

运行结果：

[[2 3 4]

 [1 2 4]]

[1 2 2 3 4 4]

[[2 3 1]

 [4 4 2]]

[[2 3 4]

 [1 2 4]]

你可以自己计算下这个运行结果，然后再跑一遍比对下。

总结

在 NumPy 学习中，你重点要掌握的就是对数组的使用，因为这是 NumPy 和标准 Python 最大的区别。在 NumPy 中重新对数组进行了定义，同时提供了算术和统计运算，你也可以使用 NumPy 自带的排序功能，一句话就搞定各种排序算法。

当然要理解 NumPy 提供的数据结构为什么比 Python 自身的“更高级、更高效”，要从对数据指针的引用角度进行理解。

我今天重点讲了 NumPy 的数据结构，你能用自己的话说明一下为什么要用 NumPy 而不是 Python 的列表 list 吗？除此之外，你还知道那些数据结构类型？

练习题：统计全班的成绩

假设一个团队里有 5 名学员，成绩如下表所示。你可以用 NumPy 统计下这些人在语文、英语、数学中的平均成绩、最小成绩、最大成绩、方差、标准差。然后把这些人的总成绩排序，得出名次进行成绩输出。

期待你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

05丨Python科学计算：Pandas

上一章中，我们讲了 Python 的一个重要的第三方库 NumPy，今天我来给你介绍 Python 的另一个工具 Pandas。

在数据分析工作中，Pandas 的使用频率是很高的，一方面是因为 Pandas 提供的基础数据结构 DataFrame 与 json 的契合度很高，转换起来就很方便。另一方面，如果我们日常的数据清理工作不是很复杂的话，你通常用几句 Pandas 代码就可以对数据进行规整。

Pandas 可以说是基于 NumPy 构建的含有更高级数据结构和分析能力的工具包。在 NumPy 中数据结构是围绕 ndarray 展开的，那么在 Pandas 中的核心数据结构是什么呢？

下面主要给你讲下Series 和 DataFrame 这两个核心数据结构，他们分别代表着一维的序列和二维的表结构。基于这两种数据结构，Pandas 可以对数据进行导入、清洗、处理、统计和输出。

数据结构：Series 和 DataFrame

Series 是个定长的字典序列。说是定长是因为在存储的时候，相当于两个 ndarray，这也是和字典结构最大的不同。因为在字典的结构里，元素的个数是不固定的。

Series有两个基本属性：index 和 values。在 Series 结构中，index 默认是 0,1,2,……递增的整数序列，当然我们也可以自己来指定索引，比如 index=[‘a’, ‘b’, ‘c’, ‘d’]。

import pandas as pd

from pandas import Series, DataFrame

x1 = Series([1,2,3,4])

x2 = Series(data=[1,2,3,4], index=['a', 'b', 'c', 'd'])

print x1

print x2

运行结果：

0    1

1    2

2    3

3    4

dtype: int64

a    1

b    2

c    3

d    4

dtype: int64

这个例子中，x1 中的 index 采用的是默认值，x2 中 index 进行了指定。我们也可以采用字典的方式来创建 Series，比如：

d = {'a':1, 'b':2, 'c':3, 'd':4}

x3 = Series(d)

print x3

运行结果：

a    1

b    2

c    3

d    4

dtype: int64

DataFrame 类型数据结构类似数据库表。

它包括了行索引和列索引，我们可以将 DataFrame 看成是由相同索引的 Series 组成的字典类型。

我们虚构一个王者荣耀考试的场景，想要输出几位英雄的考试成绩：

import pandas as pd

from pandas import Series, DataFrame

data = {'Chinese': [66, 95, 93, 90,80],'English': [65, 85, 92, 88, 90],'Math': [30, 98, 96, 77, 90]}

df1= DataFrame(data)

df2 = DataFrame(data, index=['ZhangFei', 'GuanYu', 'ZhaoYun', 'HuangZhong', 'DianWei'], columns=['English', 'Math', 'Chinese'])

print df1

print df2

在后面的案例中，我一般会用 df, df1, df2 这些作为 DataFrame 数据类型的变量名，我们以例子中的 df2 为例，列索引是 [‘English’, ‘Math’, ‘Chinese’]，行索引是 [‘ZhangFei’, ‘GuanYu’, ‘ZhaoYun’, ‘HuangZhong’, ‘DianWei’]，所以 df2 的输出是：

            English  Math  Chinese

ZhangFei         65    30       66

GuanYu           85    98       95

ZhaoYun          92    96       93

HuangZhong       88    77       90

DianWei          90    90       80

在了解了 Series 和 DataFrame 这两个数据结构后，我们就从数据处理的流程角度，来看下他们的使用方法。

数据导入和输出

Pandas 允许直接从 xlsx，csv 等文件中导入数据，也可以输出到 xlsx, csv 等文件，非常方便。

import pandas as pd

from pandas import Series, DataFrame

score = DataFrame(pd.read_excel('data.xlsx'))

score.to_excel('data1.xlsx')

print score

需要说明的是，在运行的过程可能会存在缺少 xlrd 和 openpyxl 包的情况，到时候如果缺少了，可以在命令行模式下使用“pip install”命令来进行安装。

数据清洗

数据清洗是数据准备过程中必不可少的环节，Pandas 也为我们提供了数据清洗的工具，在后面数据清洗的章节中会给你做详细的介绍，这里简单介绍下 Pandas 在数据清洗中的使用方法。

我还是以上面这个王者荣耀的数据为例。

data = {'Chinese': [66, 95, 93, 90,80],'English': [65, 85, 92, 88, 90],'Math': [30, 98, 96, 77, 90]}

df2 = DataFrame(data, index=['ZhangFei', 'GuanYu', 'ZhaoYun', 'HuangZhong', 'DianWei'], columns=['English', 'Math', 'Chinese'])

在数据清洗过程中，一般都会遇到以下这几种情况，下面我来简单介绍一下。

1. 删除 DataFrame 中的不必要的列或行

Pandas 提供了一个便捷的方法 drop() 函数来删除我们不想要的列或行。比如我们想把“语文”这列删掉。

df2 = df2.drop(columns=['Chinese'])

想把“张飞”这行删掉。

df2 = df2.drop(index=['ZhangFei'])

2. 重命名列名 columns，让列表名更容易识别

如果你想对 DataFrame 中的 columns 进行重命名，可以直接使用 rename(columns=new_names, inplace=True) 函数，比如我把列名 Chinese 改成 YuWen，English 改成 YingYu。

df2.rename(columns={'Chinese': 'YuWen', 'English': 'Yingyu'}, inplace = True)

3. 去重复的值

数据采集可能存在重复的行，这时只要使用 drop_duplicates() 就会自动把重复的行去掉。

df = df.drop_duplicates() # 去除重复行

4. 格式问题

更改数据格式

这是个比较常用的操作，因为很多时候数据格式不规范，我们可以使用 astype 函数来规范数据格式，比如我们把 Chinese 字段的值改成 str 类型，或者 int64 可以这么写：

df2['Chinese'].astype('str')

df2['Chinese'].astype(np.int64)

数据间的空格

有时候我们先把格式转成了 str 类型，是为了方便对数据进行操作，这时想要删除数据间的空格，我们就可以使用 strip 函数：

# 删除左右两边空格

df2['Chinese']=df2['Chinese'].map(str.strip)

# 删除左边空格

df2['Chinese']=df2['Chinese'].map(str.lstrip)

# 删除右边空格

df2['Chinese']=df2['Chinese'].map(str.rstrip)

如果数据里有某个特殊的符号，我们想要删除怎么办？同样可以使用 strip 函数，比如 Chinese 字段里有美元符号，我们想把这个删掉，可以这么写：

df2['Chinese']=df2['Chinese'].str.strip('$')

大小写转换

大小写是个比较常见的操作，比如人名、城市名等的统一都可能用到大小写的转换，在 Python 里直接使用 upper(), lower(), title() 函数，方法如下：

# 全部大写

df2.columns = df2.columns.str.upper()

# 全部小写

df2.columns = df2.columns.str.lower()

# 首字母大写

df2.columns = df2.columns.str.title()

查找空值

数据量大的情况下，有些字段存在空值 NaN 的可能，这时就需要使用 Pandas 中的 isnull 函数进行查找。比如，我们输入一个数据表如下：

如果我们想看下哪个地方存在空值 NaN，可以针对数据表 df 进行 df.isnull()，结果如下：

如果我想知道哪列存在空值，可以使用 df.isnull().any()，结果如下：

使用 apply 函数对数据进行清洗

apply 函数是 Pandas 中自由度非常高的函数，使用频率也非常高。

比如我们想对 name 列的数值都进行大写转化可以用：

df['name'] = df['name'].apply(str.upper)

我们也可以定义个函数，在 apply 中进行使用。比如定义 double_df 函数是将原来的数值 *2 进行返回。然后对 df1 中的“语文”列的数值进行 *2 处理，可以写成：

def double_df(x):

           return 2*x

df1[u'语文'] = df1[u'语文'].apply(double_df)

我们也可以定义更复杂的函数，比如对于 DataFrame，我们新增两列，其中’new1’列是“语文”和“英语”成绩之和的 m 倍，'new2’列是“语文”和“英语”成绩之和的 n 倍，我们可以这样写：

def plus(df,n,m):

    df['new1'] = (df[u'语文']+df[u'英语']) * m

    df['new2'] = (df[u'语文']+df[u'英语']) * n

    return df

df1 = df1.apply(plus,axis=1,args=(2,3,))

其中 axis=1 代表按照列为轴进行操作，axis=0 代表按照行为轴进行操作，args 是传递的两个参数，即 n=2, m=3，在 plus 函数中使用到了 n 和 m，从而生成新的 df。

数据统计

在数据清洗后，我们就要对数据进行统计了。

Pandas 和 NumPy 一样，都有常用的统计函数，如果遇到空值 NaN，会自动排除。

常用的统计函数包括：

表格中有一个 describe() 函数，统计函数千千万，describe() 函数最简便。它是个统计大礼包，可以快速让我们对数据有个全面的了解。下面我直接使用 df1.descirbe() 输出结果为：

df1 = DataFrame({'name':['ZhangFei', 'GuanYu', 'a', 'b', 'c'], 'data1':range(5)})

print df1.describe()

数据表合并

有时候我们需要将多个渠道源的多个数据表进行合并，一个 DataFrame 相当于一个数据库的数据表，那么多个 DataFrame 数据表的合并就相当于多个数据库的表合并。

比如我要创建两个 DataFrame：

df1 = DataFrame({'name':['ZhangFei', 'GuanYu', 'a', 'b', 'c'], 'data1':range(5)})

df2 = DataFrame({'name':['ZhangFei', 'GuanYu', 'A', 'B', 'C'], 'data2':range(5)})

两个 DataFrame 数据表的合并使用的是 merge() 函数，有下面 5 种形式：

1. 基于指定列进行连接

比如我们可以基于 name 这列进行连接。

df3 = pd.merge(df1, df2, on='name')

2. inner 内连接

inner 内链接是 merge 合并的默认情况，inner 内连接其实也就是键的交集，在这里 df1, df2 相同的键是 name，所以是基于 name 字段做的连接：

df3 = pd.merge(df1, df2, how='inner')

3. left 左连接

左连接是以第一个 DataFrame 为主进行的连接，第二个 DataFrame 作为补充。

df3 = pd.merge(df1, df2, how='left')

4. right 右连接

右连接是以第二个 DataFrame 为主进行的连接，第一个 DataFrame 作为补充。

df3 = pd.merge(df1, df2, how='right')

5. outer 外连接

外连接相当于求两个 DataFrame 的并集。

df3 = pd.merge(df1, df2, how='outer')

如何用 SQL 方式打开 Pandas

Pandas 的 DataFrame 数据类型可以让我们像处理数据表一样进行操作，比如数据表的增删改查，都可以用 Pandas 工具来完成。不过也会有很多人记不住这些 Pandas 的命令，相比之下还是用 SQL 语句更熟练，用 SQL 对数据表进行操作是最方便的，它的语句描述形式更接近我们的自然语言。

事实上，在 Python 里可以直接使用 SQL 语句来操作 Pandas。

这里给你介绍个工具：pandasql。

pandasql 中的主要函数是 sqldf，它接收两个参数：一个 SQL 查询语句，还有一组环境变量 globals() 或 locals()。这样我们就可以在 Python 里，直接用 SQL 语句中对 DataFrame 进行操作，举个例子：

import pandas as pd

from pandas import DataFrame

from pandasql import sqldf, load_meat, load_births

df1 = DataFrame({'name':['ZhangFei', 'GuanYu', 'a', 'b', 'c'], 'data1':range(5)})

pysqldf = lambda sql: sqldf(sql, globals())

sql = "select * from df1 where name ='ZhangFei'"

print pysqldf(sql)

运行结果：

   data1      name

0      0  ZhangFei

上面这个例子中，我们是对“name='ZhangFei”“的行进行了输出。当然你会看到我们用到了 lambda，lambda 在 python 中算是使用频率很高的，那 lambda 是用来做什么的呢？它实际上是用来定义一个匿名函数的，具体的使用形式为：

 lambda argument_list: expression

这里 argument_list 是参数列表，expression 是关于参数的表达式，会根据 expression 表达式计算结果进行输出返回。

在上面的代码中，我们定义了：

pysqldf = lambda sql: sqldf(sql, globals())

在这个例子里，输入的参数是 sql，返回的结果是 sqldf 对 sql 的运行结果，当然 sqldf 中也输入了 globals 全局参数，因为在 sql 中有对全局参数 df1 的使用。

总结

和 NumPy 一样，Pandas 有两个非常重要的数据结构：Series 和 DataFrame。使用 Pandas 可以直接从 csv 或 xlsx 等文件中导入数据，以及最终输出到 excel 表中。

我重点介绍了数据清洗中的操作，当然 Pandas 中同样提供了多种数据统计的函数。

最后我们介绍了如何将数据表进行合并，以及在 Pandas 中使用 SQL 对数据表更方便地进行操作。

Pandas 包与 NumPy 工具库配合使用可以发挥巨大的威力，正是有了 Pandas 工具，Python 做数据挖掘才具有优势。

我们来回顾一下今天的内容，在 Pandas 中，最主要的数据结构是什么？它都提供了哪些函数，可以帮我们做数据清洗？你可以自己描述一下吗？

练习题

对于下表的数据，请使用 Pandas 中的 DataFrame 进行创建，并对数据进行清洗。同时新增一列“总和”计算每个人的三科成绩之和。

欢迎在评论区与我分享你的答案。

如果你觉着这篇文章有价值，欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

06 | 学数据分析要掌握哪些基本概念？

美国明尼苏达州一家 Target 百货被客户投诉，这名客户指控 Target 将婴儿产品优惠券寄给他的女儿，而他女儿还是一名高中生。但没多久这名客户就来电道歉，因为女儿经他逼问后坦承自己真的怀孕了。

Target 百货寄送婴儿产品优惠券绝非偶然之举，他们发现妇女在怀孕的情况下，购买的物品会发生变化，比如护手霜会从有香味的改成无味的，此外还会购买大量维生素等保健品。通过类似的关联分析，Target 构建了一个“怀孕预测指数”，通过这个指数预测到了顾客已经怀孕的情况，并把优惠券寄送给她。

那么顾客怀孕与商品之间的关联关系是如何被发现的呢？

实际上他们都是用的Apriori 算法，该算法是由美国学者 Agrawal 在 1994 年提出的。他通过分析购物篮中的商品集合，找出商品之间的关联关系。利用这种隐性关联关系，商家就可以强化这类购买行为，从而提升销售额。

这就是数据分析的力量，人们总是从数据分析中得到有价值的信息，啤酒和尿布的故事也是个经典的案例。如今在超市中，我们还能看到不少组合的套装打包在一起卖，比如宝洁的产品：飘柔洗发水 + 玉兰油沐浴露、海飞丝洗发水 + 舒肤佳沐浴露等等。

商品的捆绑销售是个很有用的营销方式，背后都是数据分析在发挥作用。

商业智能 BI、数据仓库 DW、数据挖掘 DM 三者之间的关系

开头中的百货商店利用数据预测用户购物行为属于商业智能，他们积累的顾客的消费行为习惯会存储在数据仓库中，通过对个体进行消费行为分析总结出来的规律属于数据挖掘。

所以我们能在这个场景里看到三个重要的概念：商业智能、数据仓库和数据挖掘。

商业智能的英文是 Business Intelligence，缩写是 BI。相比于数据仓库、数据挖掘，它是一个更大的概念。商业智能可以说是基于数据仓库，经过了数据挖掘后，得到了商业价值的过程。所以说数据仓库是个金矿，数据挖掘是炼金术，而商业报告则是黄金。

数据仓库的英文是 Data Warehouse，缩写是 DW。它可以说是 BI 这个房子的地基，搭建好 DW 这个地基之后，才能进行分析使用，最后产生价值。

数据仓库可以说是数据库的升级概念。从逻辑上理解，数据库和数据仓库没有什么区别，都是通过数据库技术来存储数据的。不过从数量上来讲，数据仓库的量更庞大，适用于数据挖掘和数据分析。数据库可以理解是一项技术。

数据仓库将原有的多个数据来源中的数据进行汇总、整理而得。数据进入数据仓库前，必须消除数据中的不一致性，方便后续进行数据分析和挖掘。

数据挖掘的英文是 Data Mining，缩写是 DM。在商业智能 BI 中经常会使用到数据挖掘技术。数据挖掘的核心包括分类、聚类、预测、关联分析等任务，通过这些炼金术，我们可以从数据仓库中得到宝藏，比如商业报告。

很多时候，企业老板总是以结果为导向，他们认为商业报告才是他们想要的，但是这也是需要经过地基 DW、搬运工 ETL、科学家 DM 等共同的努力才得到的。

元数据 VS 数据元

我们前面提到了数据仓库，在数据仓库中，还有一类重要的数据是元数据，那么它和数据元有什么区别呢？

元数据（MetaData）：描述其它数据的数据，也称为“中介数据”。

数据元（Data Element）：就是最小数据单元。

在生活中，只要有一类事物，就可以定义一套元数据。举个例子，比如一本图书的信息包括了书名、作者、出版社、ISBN、出版时间、页数和定价等多个属性的信息，我们就可以把这些属性定义成一套图书的元数据。

在图书这个元数据中，书名、作者、出版社就是数据元。你可以理解是最小的数据单元。元数据最大的好处是使信息的描述和分类实现了结构化，让机器处理起来很方便。

元数据可以很方便地应用于数据仓库。比如数据仓库中有数据和数据之间的各种复杂关系，为了描述这些关系，元数据可以对数据仓库的数据进行定义，刻画数据的抽取和转换规则，存储与数据仓库主题有关的各种信息。而且整个数据仓库的运行都是基于元数据的，比如抽取调度数据、获取历史数据等。

通过元数据，可以很方便地帮助我们管理数据仓库。

数据挖掘的流程

聊完了数据仓库，我们再来谈谈数据挖掘。数据挖掘不是凭空产生的，它与数据库技术的发展分不开。数据挖掘的一个英文解释叫 Knowledge Discovery in Database，简称 KDD，也就是数据库中的知识发现。

在数据挖掘中，有几个非常重要的任务，就是分类、聚类、预测和关联分析。我来解释下这些概念。

1. 分类

就是通过训练集得到一个分类模型，然后用这个模型可以对其他数据进行分类。

这里需要说明下训练集和测试集的概念。一般来说数据可以划分为训练集和测试集。训练集是用来给机器做训练的，通常是人们整理好训练数据，以及这些数据对应的分类标识。通过训练，机器就产生了自我分类的模型，然后机器就可以拿着这个分类模型，对测试集中的数据进行分类预测。同样如果测试集中，人们已经给出了测试结果，我们就可以用测试结果来做验证，从而了解分类器在测试环境下的表现。

2. 聚类

人以群分，物以类聚。聚类就是将数据自动聚类成几个类别，聚到一起的相似度大，不在一起的差异性大。我们往往利用聚类来做数据划分。

3. 预测

顾名思义，就是通过当前和历史数据来预测未来趋势，它可以更好地帮助我们识别机遇和风险。

4. 关联分析

就是发现数据中的关联规则，它被广泛应用在购物篮分析，或事务数据分析中。比如我们开头提到的那个案例。

数据挖掘要怎么完成这些任务呢？它需要将数据库中的数据经过一系列的加工计算，最终得出有用的信息。

这个过程可以用以下步骤来描述。

首先，输入我们收集到的数据，然后对数据进行预处理。预处理通常是将数据转化成我们想要的格式，然后我们再对数据进行挖掘，最后通过后处理得到我们想要的信息。

那你可能想问，为什么不直接进行数据挖掘，还要进行数据预处理呢？

因为在这个过程中，输入的数据通常是从不同渠道采集而来的，所以数据的格式以及质量是参差不齐的，所以我们需要对数据进行预处理。

数据预处理中，我们会对数据进行几个处理步骤：数据清洗，数据集成，以及数据变换。

1. 数据清洗

主要是为了去除重复数据，去噪声（即干扰数据）以及填充缺失值。

2. 数据集成

是将多个数据源中的数据存放在一个统一的数据存储中。

3. 数据变换

就是将数据转换成适合数据挖掘的形式。比如，通过归一化将属性数据按照比例缩放，这样就可以将数值落入一个特定的区间内，比如 0~1 之间。

我会在后面的几节课给你讲解如何对数据进行预处理。

数据后处理是将模型预测的结果进一步处理后，再导出。比如在二分类问题中，一般能得到的是 0~1 之间的概率值，此时把数据以 0.5 为界限进行四舍五入就可以实现后处理。

白话数据概念

说了这么多概念，可能你还是觉得很抽象，我来打个比喻。比如你认识了两个漂亮的女孩。

商业智能会告诉你要追哪个？成功概率有多大？

数据仓库会说，我这里存储了这两个女孩的相关信息，你要吗？

其中每个女孩的数据都有单独的文件夹，里面有她们各自的姓名、生日、喜好和联系方式等，这些具体的信息就是数据元，加起来叫作元数据。

数据挖掘会帮助你确定追哪个女孩，并且整理好数据仓库，这里就可以使用到各种算法，帮你做决策了。

你可能会用到分类算法。御姐、萝莉、女王，她到底属于哪个分类？

如果认识的女孩太多了，多到你已经数不过来了，比如说 5 万人！你就可以使用聚类算法了，它帮你把这些女孩分成多个群组，比如 5 个组。然后再对每个群组的特性进行了解，进行决策。这样就把 5 万人的决策，转化成了 5 个组的决策。成功实现降维，大大提升了效率。如果你想知道这个女孩的闺蜜是谁，那么关联分析算法可以告诉你。

如果你的数据来源比较多，比如有很多朋友给你介绍女朋友，很多人都推荐了同一个，你就需要去重，这叫数据清洗；为了方便记忆，你把不同朋友推荐的女孩信息合成一个，这叫数据集成；有些数据渠道统计的体重的单位是公斤，有些是斤，你就需要将它们转换成同一个单位，这叫数据变换。

最后你可以进行数据可视化了，它会直观地把你想要的结果呈现出来。

上帝不会告诉我们规律，而是展示给我们数据

还记得高中物理课上我们计算自由落体运动加速度的实验吗？

我们将重物连上纸带，通过电火花打点计时器，在纸带上会出现多个打点。然后我们通过纸带上打点的个数，以及点之间的间距，来计算自由落体运动的加速度。通过多组实验，取平均值的方式将误差降到最小。

在我们的高中时代，许多定律都是通过实验得出的。参加工作以后，很多数据是业务数据，比如电商的客户购物数据等，这些数据依然有着某些规律。这就需要我们通过数据挖掘的力量来帮我们揭示规律，通过利用这些规律，可以帮我们创造更大的价值。

今天的内容到这里就结束了，我出几道题让你回顾下今天的内容吧。

1. 什么是 KDD？

A. 数据挖掘与知识发现
B. 领域知识发现
C. 文档知识发现
D. 动态知识发现

2. 某超市将火锅产品和骨汤进行并列陈列后，这两种商品相比以往提升了 50% 的销售额，这种属于数据挖掘的哪类问题？

A. 关联规则发现
B. 聚类
C. 分类
D. 自然语言处理

最后，说一下你是如何理解数据挖掘的价值的？可以谈谈你的体会或者项目经历。

如果你觉得今天的内容有所启迪，欢迎点击“请朋友读”，分享给你的朋友或者同事，一起来学习。

07 | 用户画像：标签化就是数据的抽象能力

王兴说过，我们已经进入到互联网的下半场。在上半场，也就是早期的互联网时代，你永远不知道在对面坐的是什么样的人。那个年代大部分人还是 QQ 的早期用户。在下半场，互联网公司已经不新鲜了，大部分公司已经互联网化。他们已经在用网络进行产品宣传，使用电商销售自己的商品。

这两年引领下半场发展的是那些在讲 “大数据”“赋能”的企业，他们有数据，有用户。

通过大数据告诉政府该如何智慧地管理交通，做城市规划。

通过消费数据分析，告诉企业该在什么时间生产什么产品，以最大化地满足用户的需求。

通过生活大数据告诉我们餐饮企业，甚至房地产企业该如何选址。

如果说互联网的上半场是粗狂运营，因为有流量红利不需要考虑细节。那么在下半场，精细化运营将是长久的主题。有数据，有数据分析能力才能让用户得到更好的体验。

所以，用户是根本，也是数据分析的出发点。

假如你进入到一家卖羊肉串的餐饮公司，老板说现在竞争越来越激烈，要想做得好就要明白顾客喜欢什么。于是上班第一天，老板问你：“你能不能分析下用户数据，给咱们公司的业务做个赋能啊？”

听到这，你会怎么想？

你说：“老板啊，咱们是卖羊肉串的，做数据挖掘没用啊。”估计老板听后，晚上就把你给开了。

那该怎么办呢？如果你感觉一头懵，没关系，我们今天就来讲讲怎么一步步分析用户数据。

用户画像的准则

首先就是将自己企业的用户画像做个白描，告诉他这些用户“都是谁”“从哪来”“要去哪”。

你可以这么和老板说：“老板啊，用户画像建模是个系统的工程，我们要解决三个问题。第一呢，就是用户从哪里来，这里我们需要统一标识用户 ID，方便我们对用户后续行为进行跟踪。我们要了解这些羊肉串的用户从哪里来，他们是为了聚餐，还是自己吃宵夜，这些场景我们都要做统计分析。第二呢，这些用户是谁？我们需要对这些用户进行标签化，方便我们对用户行为进行理解。第三呢，就是用户要到哪里去？我们要将这些用户画像与我们的业务相关联，提升我们的转化率，或者降低我们的流失率。”

听到这，老板给你竖起了大拇指，说：“不错，都需要什么资源，随时找我就行。”

刚才说的这三个步骤，下面我一一给你做个梳理。

首先，为什么要设计唯一标识？

用户唯一标识是整个用户画像的核心。我们以一个 App 为例，它把“从用户开始使用 APP 到下单到售后整个所有的用户行为”进行串联，这样就可以更好地去跟踪和分析一个用户的特征。

设计唯一标识可以从这些项中选择：用户名、注册手机号、联系人手机号、邮箱、设备号、CookieID 等。

其次，给用户打标签。

你可能会想，标签有很多，且不同的产品，标签的选择范围也不同，这么多的标签，怎样划分才能既方便记忆，又能保证用户画像的全面性呢？

这里我总结了八个字，叫“用户消费行为分析”。我们可以从这 4 个维度来进行标签划分。

用户标签：它包括了性别、年龄、地域、收入、学历、职业等。这些包括了用户的基础属性。

消费标签：消费习惯、购买意向、是否对促销敏感。这些统计分析用户的消费习惯。

行为标签：时间段、频次、时长、访问路径。这些是通过分析用户行为，来得到他们使用 App 的习惯。

内容分析：对用户平时浏览的内容，尤其是停留时间长、浏览次数多的内容进行分析，分析出用户对哪些内容感兴趣，比如，金融、娱乐、教育、体育、时尚、科技等。

可以说，用户画像是现实世界中的用户的数学建模，我们正是将海量数据进行标签化，来得到精准的用户画像，从而为企业更精准地解决问题。

最后，当你有了用户画像，可以为企业带来什么业务价值呢？

我们可以从用户生命周期的三个阶段来划分业务价值，包括：获客、粘客和留客。

获客：如何进行拉新，通过更精准的营销获取客户。

粘客：个性化推荐，搜索排序，场景运营等。

留客：流失率预测，分析关键节点降低流失率。

如果按照数据流处理的阶段来划分用户画像建模的过程，可以分为数据层、算法层和业务层。你会发现在不同的层，都需要打上不同的标签。

数据层指的是用户消费行为里的标签。我们可以打上“事实标签”，作为数据客观的记录。

算法层指的是透过这些行为算出的用户建模。我们可以打上“模型标签”，作为用户画像的分类标识。

业务层指的是获客、粘客、留客的手段。我们可以打上“预测标签”，作为业务关联的结果。

所以这个标签化的流程，就是通过数据层的“事实标签”，在算法层进行计算，打上“模型标签”的分类结果，最后指导业务层，得出“预测标签”。

美团外卖的用户画像该如何设计？

刚才讲的是用户画像的三个阶段，以及每个阶段的准则。下面，我们来使用这些准则做个练习。

如果你是美团外卖的数据分析师，你该如何制定用户标识 ID，制定用户画像，以及基于用户画像可以做哪些业务关联？

首先，我们先回顾下美团外卖的产品背景。美团已经和大众点评进行了合并，因此在大众点评和美团外卖上都可以进行外卖下单。另外美团外卖针对的是高频 O2O 的场景，美团外卖是美团的核心产品，基本上有一半的市值都是由外卖撑起来的。

基于用户画像实施的三个阶段，我们首先需要统一用户的唯一标识，那么究竟哪个字段可以作为用户标识呢？

我们先看下美团和大众点评都是通过哪些方式登录的。

我们看到，美团采用的是手机号、微信、微博、美团账号的登录方式。大众点评采用的是手机号、微信、QQ、微博的登录方式。这里面两个 APP 共同的登录方式都是手机号、微信和微博。

那么究竟哪个可以作为用户的唯一标识呢？当然主要是以用户的注册手机号为标准。这样美团和大众点评的账号体系就可以相通。

当然，大家知道在集团内部，各部门之间的协作，尤其是用户数据打通是非常困难的，所以这里建议，如果希望大数据对各个部门都能赋能，一定要在集团的战略高度上，尽早就在最开始的顶层架构上，将用户标识进行统一，这样在后续过程中才能实现用户数据的打通。

然后我们思考下，有了用户，用户画像都可以统计到哪些标签。我们按照“用户消费行为分析”的准则来进行设计。

用户标签：性别、年龄、家乡、居住地、收货地址、婚姻、宝宝信息、通过何种渠道进行的注册。

消费标签：餐饮口味、消费均价、团购等级、预定使用等级、排队使用等级、外卖等级。

行为标签：点外卖时间段、使用频次、平均点餐用时、访问路径。

内容分析：基于用户平时浏览的内容进行统计，包括餐饮口味、优惠敏感度等。

当你有了“用户消费行为分析”的标签之后，你就可以更好地理解业务了。

比如一个经常买沙拉的人，一般很少吃夜宵。同样，一个经常吃夜宵的人，吃小龙虾的概率可能远高于其他人。这些结果都是通过数据挖掘中的关联分析得出的。

有了这些数据，我们就可以预测用户的行为。

比如一个用户购买了“月子餐”后，更有可能购买婴儿水，同样婴儿相关的产品比如婴儿湿巾等的购买概率也会增大。

具体在业务层上，我们都可以基于标签产生哪些业务价值呢？

在获客上，我们可以找到优势的宣传渠道，如何通过个性化的宣传手段，吸引有潜在需求的用户，并刺激其转化。

在粘客上，如何提升用户的单价和消费频次，方法可以包括购买后的个性化推荐、针对优质用户进行优质高价商品的推荐、以及重复购买，比如通过红包、优惠等方式激励对优惠敏感的人群，提升购买频次。

在留客上，预测用户是否可能会从平台上流失。在营销领域，关于用户留存有一个观点——如果将顾客流失率降低 5%，公司利润将提升 25%~85%。可以看出留存率是多么的重要。用户流失可能会包括多种情况，比如用户体验、竞争对手、需求变化等，通过预测用户的流失率可以大幅降低用户留存的运营成本。

锻炼自己的抽象能力，将繁杂的事务简单化

上面我们讲到的“用户消费行为标签”都是基于一般情况考虑的，除此之外，用户的行为也会随着营销的节奏产生异常值，比如双十一的时候，如果商家都在促销就会产生突发的大量订单。因此在做用户画像的时候，还要考虑到异常值的处理。

总之，数据量是庞大的，会存在各种各样的使用情况。光是分析 EB 级别的大数据，我们就要花很长的时间。

但我们的最终目的不是处理这些数据，而是理解、使用这些数据挖掘的结果。对数据的标签化能让我们快速理解一个用户，一个商品，乃至一个视频内容的特征，从而方便我们去理解和使用数据。

对数据的标签化其实考验的是我们的抽象能力，在日常工作中，我们也要锻炼自己的抽象能力，它可以让我们很快地将一个繁杂的事物简单化，不仅方便理解，还有益后续的使用。

我们今天讲了用户画像的流程，其中很重要的一个步骤就是给用户打标签，那么你不妨想想，如果给羊肉串连锁店进行用户画像分析，都可以从哪些角度进行标签化？

最后，我们从现实生活中出发，打开你的手机，翻翻看你的微信通讯录，分析下你的朋友圈，都有哪些用户画像？如果你来给它设计标签，都有哪些种类需要统计呢。为了方便后续使用，你是如何将他们归类分组的？

欢迎在评论区与我分享你的答案，我会和你一起探讨。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，我们一起交流，一起进步。

08 | 数据采集：如何自动化采集数据？

上一节中我们讲了如何对用户画像建模，而建模之前我们都要进行数据采集。数据采集是数据挖掘的基础，没有数据，挖掘也没有意义。很多时候，我们拥有多少数据源，多少数据量，以及数据质量如何，将决定我们挖掘产出的成果会怎样。

举个例子，你做量化投资，基于大数据预测未来股票的波动，根据这个预测结果进行买卖。你当前能够拿到以往股票的所有历史数据，是否可以根据这些数据做出一个预测率高的数据分析系统呢？

实际上，如果你只有股票历史数据，你仍然无法理解股票为什么会产生大幅的波动。比如，当时可能是爆发了 SARS 疫情，或者某地区发生了战争等。这些重大的社会事件对股票的影响也是巨大的。

因此我们需要考虑到，一个数据的走势，是由多个维度影响的。我们需要通过多源的数据采集，收集到尽可能多的数据维度，同时保证数据的质量，这样才能得到高质量的数据挖掘结果。

那么，从数据采集角度来说，都有哪些数据源呢？我将数据源分成了以下的四类。

这四类数据源包括了：开放数据源、爬虫抓取、传感器和日志采集。它们各有特点。

开放数据源一般是针对行业的数据库。比如美国人口调查局开放了美国的人口信息、地区分布和教育情况数据。除了政府外，企业和高校也会开放相应的大数据，这方面北美相对来说做得好一些。国内，贵州做了不少大胆尝试，搭建了云平台，逐年开放了旅游、交通、商务等领域的数据量。

要知道很多研究都是基于开放数据源进行的，否则每年不会有那么多论文发表，大家需要相同的数据集才能对比出算法的好坏。

爬虫抓取，一般是针对特定的网站或 App。如果我们想要抓取指定的网站数据，比如购物网站上的购物评价等，就需要我们做特定的爬虫抓取。

第三类数据源是传感器，它基本上采集的是物理信息。比如图像、视频、或者某个物体的速度、热度、压强等。

最后是日志采集，这个是统计用户的操作。我们可以在前端进行埋点，在后端进行脚本收集、统计，来分析网站的访问情况，以及使用瓶颈等。

知道了有四类数据源，那如何采集到这些数据呢？

如何使用开放数据源

我们先来看下开放数据源，教你个方法，开放数据源可以从两个维度来考虑，一个是单位的维度，比如政府、企业、高校；一个就是行业维度，比如交通、金融、能源等领域。这方面，国外的开放数据源比国内做得好一些，当然近些年国内的政府和高校做开放数据源的也越来越多。一方面服务社会，另一方面自己的影响力也会越来越大。

比如，下面这张表格列举的就是单位维度的数据源。

所以如果你想找某个领域的数据源，比如金融领域，你基本上可以看下政府、高校、企业是否有开放的数据源。当然你也可以直接搜索金融开放数据源。

如何使用爬虫做抓取

爬虫抓取应该属于最常见的需求，比如你想要餐厅的评价数据。当然这里要注重版权问题，而且很多网站也是有反爬机制的。

最直接的方法就是使用 Python 编写爬虫代码，当然前提是你需要会 Python 的基本语法。除此之外，PHP 也可以做爬虫，只是功能不如 Python 完善，尤其是涉及到多线程的操作。

在 Python 爬虫中，基本上会经历三个过程。

使用 Requests 爬取内容。我们可以使用 Requests 库来抓取网页信息。Requests 库可以说是 Python 爬虫的利器，也就是 Python 的 HTTP 库，通过这个库爬取网页中的数据，非常方便，可以帮我们节约大量的时间。

使用 XPath 解析内容。XPath 是 XML Path 的缩写，也就是 XML 路径语言。它是一种用来确定 XML 文档中某部分位置的语言，在开发中经常用来当作小型查询语言。XPath 可以通过元素和属性进行位置索引。

使用 Pandas 保存数据。Pandas 是让数据分析工作变得更加简单的高级数据结构，我们可以用 Pandas 保存爬取的数据。最后通过 Pandas 再写入到 XLS 或者 MySQL 等数据库中。

Requests、XPath、Pandas 是 Python 的三个利器。当然做 Python 爬虫还有很多利器，比如 Selenium，PhantomJS，或者用 Puppteteer 这种无头模式。

另外我们也可以不编程就抓取到网页信息，这里介绍三款常用的抓取工具。

火车采集器

火车采集器已经有 13 年历史了，是老牌的采集工具。它不仅可以做抓取工具，也可以做数据清洗、数据分析、数据挖掘和可视化等工作。数据源适用于绝大部分的网页，网页中能看到的内容都可以通过采集规则进行抓取。

八爪鱼

八爪鱼也是知名的采集工具，它有两个版本，一个就是免费的采集模板，还有一个就是云采集（付费）。

免费的采集模板实际上就是内容采集规则，包括了电商类、生活服务类、社交媒体类和论坛类的网站都可以采集，用起来非常方便。当然你也可以自己来自定义任务。

那什么是云采集呢？就是当你配置好采集任务，就可以交给八爪鱼的云端进行采集。八爪鱼一共有 5000 台服务器，通过云端多节点并发采集，采集速度远远超过本地采集。此外还可以自动切换多个 IP，避免 IP 被封，影响采集。

做过工程项目的同学应该能体会到，云采集这个功能太方便了，很多时候自动切换 IP 以及云采集才是自动化采集的关键。

下一篇文章我会给你详细介绍八爪鱼的使用。

集搜客

这个工具的特点是完全可视化操作，无需编程。整个采集过程也是所见即所得，抓取结果信息、错误信息等都反应在软件中。相比于八爪鱼来说，集搜客没有流程的概念，用户只需要关注抓取什么数据，而流程细节完全交给集搜客来处理。

但是集搜客的缺点是没有云采集功能，所有爬虫都是在用户自己电脑上跑的。

如何使用日志采集工具

传感器采集基本上是基于特定的设备，将设备采集的信息进行收集即可，这里我们就不重点讲解了。

下面我们来看日志采集。

为什么要做日志采集呢？日志采集最大的作用，就是通过分析用户访问情况，提升系统的性能，从而提高系统承载量。及时发现系统承载瓶颈，也可以方便技术人员基于用户实际的访问情况进行优化。

日志采集也是运维人员的重要工作之一，那么日志都包括哪些呢，又该如何对日志进行采集呢？

日志就是日记的意思，它记录了用户访问网站的全过程：哪些人在什么时间，通过什么渠道（比如搜索引擎、网址输入）来过，都执行了哪些操作；系统是否产生了错误；甚至包括用户的 IP、HTTP 请求的时间，用户代理等。这些日志数据可以被写在一个日志文件中，也可以分成不同的日志文件，比如访问日志、错误日志等。

日志采集可以分两种形式。

通过 Web 服务器采集，例如 httpd、Nginx、Tomcat 都自带日志记录功能。同时很多互联网企业都有自己的海量数据采集工具，多用于系统日志采集，如 Hadoop 的 Chukwa、Cloudera 的 Flume、Facebook 的 Scribe 等，这些工具均采用分布式架构，能够满足每秒数百 MB 的日志数据采集和传输需求。

自定义采集用户行为，例如用 JavaScript 代码监听用户的行为、AJAX 异步请求后台记录日志等。

埋点是什么

埋点是日志采集的关键步骤，那什么是埋点呢？

埋点就是在有需要的位置采集相应的信息，进行上报。比如某页面的访问情况，包括用户信息、设备信息；或者用户在页面上的操作行为，包括时间长短等。这就是埋点，每一个埋点就像一台摄像头，采集用户行为数据，将数据进行多维度的交叉分析，可真实还原出用户使用场景，和用户使用需求。

那我们要如何进行埋点呢？

埋点就是在你需要统计数据的地方植入统计代码，当然植入代码可以自己写，也可以使用第三方统计工具。我之前讲到“不重复造轮子”的原则，一般来说需要自己写的代码，一般是主营核心业务，对于埋点这类监测性的工具，市场上已经比较成熟，这里推荐你使用第三方的工具，比如友盟、Google Analysis、Talkingdata 等。他们都是采用前端埋点的方式，然后在第三方工具里就可以看到用户的行为数据。但如果我们想要看到更深层的用户操作行为，就需要进行自定义埋点。

总结一下，日志采集有助于我们了解用户的操作数据，适用于运维监控、安全审计、业务数据分析等场景。一般 Web 服务器会自带日志功能，也可以使用 Flume 从不同的服务器集群中采集、汇总和传输大容量的日志数据。当然我们也可以使用第三方的统计工具或自定义埋点得到自己想要的统计内容。

总结

数据采集是数据分析的关键，很多时候我们会想到 Python 网络爬虫，实际上数据采集的方法、渠道很广，有些可以直接使用开放的数据源，比如想获取比特币历史的价格及交易数据，可以直接从 Kaggle 上下载，不需要自己爬取。

另一方面根据我们的需求，需要采集的数据也不同，比如交通行业，数据采集会和摄像头或者测速仪有关。对于运维人员，日志采集和分析则是关键。所以我们需要针对特定的业务场景，选择适合的采集工具。

今天我讲了数据采集的不同渠道以及相关的工具。给你留一个思考题，假如你想预测比特币的未来走势，都需要哪些维度的数据源呢？怎样收集到它们呢？欢迎在留言区与我分享你的想法，我也会和你一起探讨。

也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

09丨数据采集：如何用八爪鱼采集微博上的“D&G”评论

上一讲我给你讲了数据采集的来源，其中有一个很关键的工具叫做八爪鱼，今天我们就用八爪鱼实战模拟一下如何进行数据采集。

在文末你可以看到我操作的流程视频。

八爪鱼的基本操作

在开始操作前，我先来介绍下今天要讲的主角“八爪鱼”工具。相比使用 Python 进行爬虫，八爪鱼的使用更加简便，因为是所见即所得的方式，基本上不需要编写代码，除了在正则表达式匹配的时候会用到 XPath。

这里简单介绍下 XPath，XPath 的英文是 XML Path Language，也就是 XML 的路径语言，用来在 XML 文件中寻找我们想要的元素。所以八爪鱼可以使用 XPath 帮我们更灵活地定位我们想要找的元素。

自定义任务 VS 简易采集

如果你想要采集数据就需要新建一个任务，在建任务的时候，八爪鱼会给你一个提示，是使用八爪鱼自带的“简易采集”，还是自定义一个任务。

简易采集集成了一些热门的模板，也就是我们经常访问的一些网站。它可以帮助我们轻松地实现采集，只需要我们告诉工具两个信息即可，一个是需要采集的网址，另一个是登录网站的账号和密码。

虽然简易采集比较方便快捷，但通常还是推荐使用自定义任务的方式，这样可以更灵活地帮我们提取想要的信息，比如你只想采集关于“D&G”的微博评论。

流程步骤

八爪鱼的采集共分三步：

输入网页：每个采集需要输入你想要采集的网页。在新建任务的时候，这里是必填项。

设计流程：这个步骤最为关键，你需要告诉八爪鱼，你是如何操作页面的、想要提取页面上的哪些信息等。因为数据条数比较多，通常你还需要翻页，所以要进行循环翻页的设置。在设计流程中，你可以使用简易采集方式，也就是八爪鱼自带的模板，也可以采用自定义的方式。

启动采集：当你设计好采集流程后，就可以启动采集任务了，任务结束后，八爪鱼会提示你保存采集好的数据，通常是 xlsx 或 csv 格式。

如果你使用的是自定义采集，就需要自己来设计采集流程，也就是采集流程中的第二步。八爪鱼的流程步骤有两类，可以划分为基本步骤和高级步骤。

基本步骤就是最常用的步骤，每次采集都会用到，一共分为 4 步，分别是打开网页、点击元素、循环翻页、提取数据。

高级步骤是辅助步骤，可以帮我们更好地对数据进行提取，比如我们想要某个关键词的数据，就需要在网页输入框中输入对应的文字。有时候源网页的系统会提示需要输入验证码，我们就可以采用验证码识别的模块帮我们解决。有时候我们需要用下拉选项帮我们筛选想要的数据，或者某些判断条件下（比如存在某个关键词）才触发的采集等。这些操作可以更精细化地提取想要的数据。

下面我来介绍下基本步骤：

1. 打开网页

所有的采集默认第一项都是打开网页。所以在新建任务之后，系统会提示你输入网址。当你输入之后，八爪鱼就会自动建立一个“打开网页”的流程。

2. 点击元素

这里元素的定义比较广泛，它可以是某个按钮，或者某个链接，也或者是某个图片或文字。使用这个步骤是你在搜索或者提交某个请求。当你点击元素后，八爪鱼会提示你想要达到的目的：点击该按钮、采集该元素文本、还是鼠标移到该链接上。然后再选择“点击该按钮”进行确认即可。

如果我们点击某个元素的目的是循环翻页，或者提取数据，那么在点击之后，八爪鱼会确认你的目的，你只要点击相关的按钮即可。

3. 循环翻页

很多数据都存在翻页的情况，通常你需要找到翻页的位置，比如网页底部的“下一页”按钮，点击它，会提示你“循环点击下一页”、“采集该链接文本”还是“点击该链接”。你需要确认这里是进行的“循环点击下一页”。

4. 提取数据

在网页上选择你想要提取的页面范围，鼠标移动到页面上会呈现蓝色的阴影面积，它表明了你想提取的数据范围。然后点击鼠标后，在右侧选择“采集数据”即可。

这 4 个基本操作就像它们的名称一样简单直接，这里我给你一些使用的建议：

尽量使用用户操作视角进行模拟的方式进行操作，而不是在“流程视图”中手动创建相应的步骤。因为八爪鱼最大的特点就是所见即所得，所以一切就按照用户使用的流程进行操作即可。

使用“流程视图”方便管理和调整。右侧有“流程视图”的按钮，点击之后进入到流程视图，会把你之前的操作以流程图的方式展示出来。我会在文章下面详细介绍一下。

为什么要这么做呢？这样的话每个步骤流程清晰可见，而且你还可以调整每个步骤的参数，比如你之前的网址写错了想要修改，或者之前输入的文字需要调整等。

另外很多时候需要账号登录后才能采集数据，我们可以提前在八爪鱼工具里登录，这样再进行抓取的时候就是登录的状态，直接进行采集就可以了。

采集微博上的“Dolce&Gabbana”评论

在了解基本步骤之后，我们就可以自己动手采集内容了。比如说我想要采集微博上关于“D&G”的评论，那么我可以先在浏览器上，人工操作下整个流程，梳理出来以下的步骤。

这几个流程具体是怎么做的呢？我来给你一一梳理一下。

1. 输入网页

对应基本步骤“打开网页”，我们输入微博搜索的地址。

2. 输入关键词

对应“输入文本”，我把鼠标移动到输入框中，点击后会在右侧进行操作目的的确认，选择“输入文本”即可，然后输入我们想要搜索的内容“D&G”。

3. 点击搜索

对应“点击元素”，我们点击“搜索按钮”，然后确认操作目的是“点击元素”。

4. 设置翻页

因为我们想要采集全量数据，因此需要先设置翻页。这里特别注意下，翻页的操作要在数据提取之前，因为翻页是个循环的命令，就像我们平时写 for 语句一样，一定是先设置 for 循环，然后在循环中进行数据提取。

5. 提取数据

提取数据的时候，我们需要提取多个字段，比如，用户、微博内容、发表时间、该微博网址。而且一个页面上会有多个微博，都需要进行采集。所以你需要先选择单条内容的最大的目标区域，在确认目的时，会发现里面有子元素，这里目的选择为“选中子元素”。因为我们要对子元素内容进行采集，方便把内容按照字段进行划分。这时会提示页面中还有 20 个相同元素时，选择“选中全部”即可。

6. 启动采集

都选择好之后，系统会给出三个提示，分别是“启动本地采集”、“启动云采集”和“设置定时采集”。数据量不大的时候，我们选择“启动本地采集”即可。

你可以看出，这整个过程比较简便，但中间有一些细节你可能会出错，比如说你忘记了先翻页，再选取你想提取的元素。这样如果遇到了问题，有两个重要的工具一定要用好：流程视图和 XPath。

流程视图

流程视图我在上面提到过，这里详细介绍一下。流程视图应该是在可视化中应用最多的场景，我们可以使用流程视图查看创建流程，调整顺序，或者删掉不想要的步骤。

另外我们还能在视图中查看数据提取的字段。选中“提取数据”步骤，可以看到该步骤提取的字段都有哪些。一般都会出现很多冗余的字段，因为 HTML 代码段中有很多隐藏的内容也会被提取到，这里你可以删掉没用的字段，把保留的字段名称进行备注修改。

这里有张图，是我通过八爪鱼可视化操作采集微博评论时，自动生成的流程视图。

XPath

介绍完流程视图之后，我们再来说一下 XPath。在八爪鱼工具中内置了 XPath 引擎，所以在我们用可视化方式选择元素的时候，会自动生成相应的 XPath 路径。当然我们也可以查看这些元素的 XPath，方便对它们进行精细地控制。

为什么有了可视化操作，还需要自己来定义 XPath 呢？

这是因为有时候我们采集的网站页面是不规律的，比如你可以看到微博搜索结果页中，第一页和第二页的 HTML 排版是不同的，这样的话，可视化操作得到的 XPath 可能不具备通用性。这种情况下，如果你用搜索结果第一页提取数据得到的 XPath，就无法匹配搜索结果页第二页的数据。

在八爪鱼工具中，很多控件都有 XPath，最常用的还是循环和提取数据中的 XPath，下面我来一一简单介绍下。

循环中的 XPath

在微博采集这个例子中，我们用到了两种循环方式，一种是“循环翻页”，一种是“循环列表”。

在“循环翻页”中，你可以在“流程视图”中点击“循环翻页”的控件，看到右侧的“高级选项”中的 XPath。在微博采集这个例子中，循环翻页的 XPath 是 //A[@class=‘next’]。

在“循环列表”中，我在提取数据的时候，出现了页面提示“还有 20 个相同元素”，这时我选择“选中全部”。相当于出现了 20 个元素的循环列表。所以你在流程视图中，可以会看到提取数据外层嵌套了个循环列表。同样我们可以看到循环列表的 XPath 是 //DIV[@class=‘card-feed’]。

提取数据的 XPath

当我们点击流程中的“提取数据”，可以看到有很多字段名称，XPath 实际上定位到了这些要采集的字段。所以你需要选中一个字段，才能看到它的 XPath。

现在你知道了，八爪鱼的匹配是基于 XPath 的，那么你也可以自己来调整 XPath 参数。这样当匹配不到想要的数据的时候，可以检查下是不是 XPath 参数设置的问题，可以手动调整，从而抓取到正确的元素。

总结

今天我给你讲了八爪鱼的使用，然后带你实现了一个微博采集的例子。你可能会想问，我为什么要讲一个八爪鱼这样的第三方工具呢？

这里讲下我的一些心得体会。

我们的工作流程通常很长，所以更应该专注工作的核心，比如说数据分析这块，所有的辅助都可以采用第三方工具来做。如果老板让你统计微博上的评论，实际上老板最想知道的不是采集的过程，而是整体的概况，比如说影响了多少人，评论如何，是否有 KOL 关注等等。

如果你之前没有数据采集的经验，那么第三方工具，以及采用可视化的方式来进行采集应该是你的首选。可视化的方式可以让你迅速上手，了解到整个数据采集的过程。

我们应该从基础步骤开始，遇到特定需求的时候再学习了解高级步骤。这篇文章只介绍了基本的流程，但你可以上手操作了。在实际操作中，你可能会遇到各种问题，这个时候再对高级步骤进行学习，如果要进阶的话，还需要你掌握 XPath 的使用。

好了，我来总结一下今天的内容。今天我讲了八爪鱼的任务建立、流程设计，还有一个实操的案例。具体的内容概要可以看我整理的下面这张图。

虽然八爪鱼工具提供了各种简易采集的方式，我更建议你把它作为参考模板，可以看下别人是如何建立的，这样多做案例，你上手会更快。

下面是我操作八爪鱼的流程，你可以看一下这个视频，更直观一些。

最后给你留个练习题吧。如何通过八爪鱼抓取豆瓣网站中关于《海王》的电影评论呢？你不妨上手练习一下，这样会有更深的体会。

如果你觉得今天的内容对你有帮助，欢迎你把这篇文章分享给你的朋友或者同事，一起来学习进步。

10丨Python爬虫：如何自动化下载王祖贤海报？

上一讲中我给你讲了如何使用八爪鱼采集数据，对于数据采集刚刚入门的人来说，像八爪鱼这种可视化的采集是一种非常好的方式。它最大的优点就是上手速度快，当然也存在一些问题，比如运行速度慢、可控性差等。

相比之下，爬虫可以很好地避免这些问题，今天我来分享下如何通过编写爬虫抓取数据。

爬虫的流程

相信你对“爬虫”这个词已经非常熟悉了，爬虫实际上是用浏览器访问的方式模拟了访问网站的过程，整个过程包括三个阶段：打开网页、提取数据和保存数据。

在 Python 中，这三个阶段都有对应的工具可以使用。

在“打开网页”这一步骤中，可以使用 Requests 访问页面，得到服务器返回给我们的数据，这里包括 HTML 页面以及 JSON 数据。

在“提取数据”这一步骤中，主要用到了两个工具。针对 HTML 页面，可以使用 XPath 进行元素定位，提取数据；针对 JSON 数据，可以使用 JSON 进行解析。

在最后一步“保存数据”中，我们可以使用 Pandas 保存数据，最后导出 CSV 文件。

下面我来分别介绍下这些工具的使用。

Requests 访问页面

Requests 是 Python HTTP 的客户端库，编写爬虫的时候都会用到，编写起来也很简单。它有两种访问方式：Get 和 Post。这两者最直观的区别就是：Get 把参数包含在 url 中，而 Post 通过 request body 来传递参数。

假设我们想访问豆瓣，那么用 Get 访问的话，代码可以写成下面这样的：

r = requests.get('http://www.douban.com')

代码里的“r”就是 Get 请求后的访问结果，然后我们可以使用 r.text 或 r.content 来获取 HTML 的正文。

如果我们想要使用 Post 进行表单传递，代码就可以这样写：

r = requests.post('http://xxx.com', data = {'key':'value'})

这里 data 就是传递的表单参数，data 的数据类型是个字典的结构，采用 key 和 value 的方式进行存储。

XPath 定位

XPath 是 XML 的路径语言，实际上是通过元素和属性进行导航，帮我们定位位置。它有几种常用的路径表达方式。

我来给你简单举一些例子：

xpath(‘node’) 选取了 node 节点的所有子节点；

xpath(’/div’) 从根节点上选取 div 节点；

xpath(’//div’) 选取所有的 div 节点；

xpath(’./div’) 选取当前节点下的 div 节点；

xpath(’…’) 回到上一个节点；

xpath(’//@id’) 选取所有的 id 属性；

xpath(’//book[@id]’) 选取所有拥有名为 id 的属性的 book 元素；

xpath(’//book[@id=“abc”]’) 选取所有 book 元素，且这些 book 元素拥有 id= "abc"的属性；

xpath(’//book/title | //book/price’) 选取 book 元素的所有 title 和 price 元素。

上面我只是列举了 XPath 的部分应用，XPath 的选择功能非常强大，它可以提供超过 100 个内建函数，来做匹配。我们想要定位的节点，几乎都可以使用 XPath 来选择。

使用 XPath 定位，你会用到 Python 的一个解析库 lxml。这个库的解析效率非常高，使用起来也很简便，只需要调用 HTML 解析命令即可，然后再对 HTML 进行 XPath 函数的调用。

比如我们想要定位到 HTML 中的所有列表项目，可以采用下面这段代码。

from lxml import etree

html = etree.HTML(html)

result = html.xpath('//li')

JSON 对象

JSON 是一种轻量级的交互方式，在 Python 中有 JSON 库，可以让我们将 Python 对象和 JSON 对象进行转换。为什么要转换呢？原因也很简单。将 JSON 对象转换成为 Python 对象，我们对数据进行解析就更方便了。

这是一段将 JSON 格式转换成 Python 对象的代码，你可以自己运行下这个程序的结果。

import json

jsonData = '{"a":1,"b":2,"c":3,"d":4,"e":5}';

input = json.loads(jsonData)

print input

接下来，我们就要进行实战了，我会从两个角度给你讲解如何使用 Python 爬取海报，一个是通过 JSON 数据爬取，一个是通过 XPath 定位爬取。

如何使用 JSON 数据自动下载王祖贤的海报

我在上面讲了 Python 爬虫的基本原理和实现的工具，下面我们来实战一下。如果想要从豆瓣图片中下载王祖贤的海报，你应该先把我们日常的操作步骤整理下来：

打开网页；

输入关键词“王祖贤”；

在搜索结果页中选择“图片”；

下载图片页中的所有海报。

这里你需要注意的是，如果爬取的页面是动态页面，就需要关注 XHR 数据。因为动态页面的原理就是通过原生的 XHR 数据对象发出 HTTP 请求，得到服务器返回的数据后，再进行处理。XHR 会用于在后台与服务器交换数据。

你需要使用浏览器的插件查看 XHR 数据，比如在 Chrome 浏览器中使用开发者工具。

在豆瓣搜索中，我们对“王祖贤”进行了模拟，发现 XHR 数据中有一个请求是这样的：

https://www.douban.com/j/search_photo?q=%E7%8E%8B%E7%A5%96%E8%B4%A4&limit=20&start=0

url 中的乱码正是中文的 url 编码，打开后，我们看到了很清爽的 JSON 格式对象，展示的形式是这样的：

{"images":

       [{"src": …, "author": …, "url":…, "id": …, "title": …, "width":…, "height":…},

    …

	 {"src": …, "author": …, "url":…, "id": …, "title": …, "width":…, "height":…}],

 "total":22471,"limit":20,"more":true}

从这个 JSON 对象中，我们能看到，王祖贤的图片一共有 22471 张，其中一次只返回了 20 张，还有更多的数据可以请求。数据被放到了 images 对象里，它是个数组的结构，每个数组的元素是个字典的类型，分别告诉了 src、author、url、id、title、width 和 height 字段，这些字段代表的含义分别是原图片的地址、作者、发布地址、图片 ID、标题、图片宽度、图片高度等信息。

有了这个 JSON 信息，你很容易就可以把图片下载下来。当然你还需要寻找 XHR 请求的 url 规律。

如何查看呢，我们再来重新看下这个网址本身。

https://www.douban.com/j/search_photo?q= 王祖贤 &limit=20&start=0

你会发现，网址中有三个参数：q、limit 和 start。start 实际上是请求的起始 ID，这里我们注意到它对图片的顺序标识是从 0 开始计算的。所以如果你想要从第 21 个图片进行下载，你可以将 start 设置为 20。

王祖贤的图片一共有 22471 张，你可以写个 for 循环来跑完所有的请求，具体的代码如下：

# coding:utf-8

import requests

import json

query = '王祖贤'

''' 下载图片 '''

def download(src, id):

	dir = './' + str(id) + '.jpg'

	try:

		pic = requests.get(src, timeout=10)

		fp = open(dir, 'wb')

		fp.write(pic.content)

		fp.close()

	except requests.exceptions.ConnectionError:

		print('图片无法下载')

''' for 循环 请求全部的 url '''

for i in range(0, 22471, 20):

	url = 'https://www.douban.com/j/search_photo?q='+query+'&limit=20&start='+str(i)

	html = requests.get(url).text    # 得到返回结果

	response = json.loads(html,encoding='utf-8') # 将 JSON 格式转换成 Python 对象

	for image in response['images']:

		print(image['src']) # 查看当前下载的图片网址

		download(image['src'], image['id']) # 下载一张图片

如何使用 XPath 自动下载王祖贤的电影海报封面

如果你遇到 JSON 的数据格式，那么恭喜你，数据结构很清爽，通过 Python 的 JSON 库就可以解析。

但有时候，网页会用 JS 请求数据，那么只有 JS 都加载完之后，我们才能获取完整的 HTML 文件。XPath 可以不受加载的限制，帮我们定位想要的元素。

比如，我们想要从豆瓣电影上下载王祖贤的电影封面，需要先梳理下人工的操作流程：

打开网页 movie.douban.com；

输入关键词“王祖贤”；

下载图片页中的所有电影封面。

这里你需要用 XPath 定位图片的网址，以及电影的名称。

一个快速定位 XPath 的方法就是采用浏览器的 XPath Helper 插件，使用 Ctrl+Shift+X 快捷键的时候，用鼠标选中你想要定位的元素，就会得到类似下面的结果。

XPath Helper 插件中有两个参数，一个是 Query，另一个是 Results。Query 其实就是让你来输入 XPath 语法，然后在 Results 里看到匹配的元素的结果。

我们看到，这里选中的是一个元素，我们要匹配上所有的电影海报，就需要缩减 XPath 表达式。你可以在 Query 中进行 XPath 表达式的缩减，尝试去掉 XPath 表达式中的一些内容，在 Results 中会自动出现匹配的结果。

经过缩减之后，你可以得到电影海报的 XPath（假设为变量 src_xpath）：

//div[@class='item-root']/a[@class='cover-link']/img[@class='cover']/@src

以及电影名称的 XPath（假设为变量 title_xpath）：

//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']

但有时候当我们直接用 Requests 获取 HTML 的时候，发现想要的 XPath 并不存在。这是因为 HTML 还没有加载完，因此你需要一个工具，来进行网页加载的模拟，直到完成加载后再给你完整的 HTML。

在 Python 中，这个工具就是 Selenium 库，使用方法如下：

from selenium import webdriver

driver = webdriver.Chrome()

driver.get(request_url)

Selenium 是 Web 应用的测试工具，可以直接运行在浏览器中，它的原理是模拟用户在进行操作，支持当前多种主流的浏览器。

这里我们模拟 Chrome 浏览器的页面访问。

你需要先引用 Selenium 中的 WebDriver 库。WebDriver 实际上就是 Selenium 2，是一种用于 Web 应用程序的自动测试工具，提供了一套友好的 API，方便我们进行操作。

然后通过 WebDriver 创建一个 Chrome 浏览器的 drive，再通过 drive 获取访问页面的完整 HTML。

当你获取到完整的 HTML 时，就可以对 HTML 中的 XPath 进行提取，在这里我们需要找到图片地址 srcs 和电影名称 titles。这里通过 XPath 语法匹配到了多个元素，因为是多个元素，所以我们需要用 for 循环来对每个元素进行提取。

srcs = html.xpath(src_xpath)

titles = html.xpath(title_path)

for src, title in zip(srcs, titles):

	download(src, title.text)

然后使用上面我编写好的 download 函数进行图片下载。

总结

好了，这样就大功告成了，程序可以源源不断地采集你想要的内容。这节课，我想让你掌握的是：

Python 爬虫的流程；

了解 XPath 定位，JSON 对象解析；

如何使用 lxml 库，进行 XPath 的提取；

如何在 Python 中使用 Selenium 库来帮助你模拟浏览器，获取完整的 HTML。

其中，Python + Selenium + 第三方浏览器可以让我们处理多种复杂场景，包括网页动态加载、JS 响应、Post 表单等。因为 Selenium 模拟的就是一个真实的用户的操作行为，就不用担心 cookie 追踪和隐藏字段的干扰了。

当然，Python 还给我们提供了数据处理工具，比如 lxml 库和 JSON 库，这样就可以提取想要的内容了。

最后，你不妨来实践一下，你最喜欢哪个明星？如果想要自动下载这个明星的图片，该如何操作呢？欢迎和我在评论区进行探讨。

你也可以把这篇文章分享给你的朋友或者同事，一起动手练习一下。

11 | 数据科学家80%时间都花费在了这些清洗任务上？

我们在上一节中讲了数据采集，以及相关的工具使用，但做完数据采集就可以直接进行挖掘了吗？肯定不是的。

就拿做饭打个比方吧，对于很多人来说，热油下锅、掌勺翻炒一定是做饭中最过瘾的环节，但实际上炒菜这个过程只占做饭时间的 20%，剩下 80% 的时间都是在做准备，比如买菜、择菜、洗菜等等。

在数据挖掘中，数据清洗就是这样的前期准备工作。对于数据科学家来说，我们会遇到各种各样的数据，在分析前，要投入大量的时间和精力把数据“整理裁剪”成自己想要或需要的样子。

为什么呢？因为我们采集到的数据往往有很多问题。

我们先看一个例子，假设老板给你以下的数据，让你做数据分析，你看到这个数据后有什么感觉呢？

你刚看到这些数据可能会比较懵，因为这些数据缺少标注。

我们在收集整理数据的时候，一定要对数据做标注，数据表头很重要。比如这份数据表，就缺少列名的标注，这样一来我们就不知道每列数据所代表的含义，无法从业务中理解这些数值的作用，以及这些数值是否正确。但在实际工作中，也可能像这个案例一样，数据是缺少标注的。

我简单解释下这些数据代表的含义。

这是一家服装店统计的会员数据。最上面的一行是列坐标，最左侧一列是行坐标。

列坐标中，第 0 列代表的是序号，第 1 列代表的会员的姓名，第 2 列代表年龄，第 3 列代表体重，第 4~6 列代表男性会员的三围尺寸，第 7~9 列代表女性会员的三围尺寸。

了解含义以后，我们再看下中间部分具体的数据，你可能会想，这些数据怎么这么“脏乱差”啊，有很多值是空的（NaN），还有空行的情况。

是的，这还仅仅是一家商店的部分会员数据，我们一眼看过去就能发现一些问题。日常工作中的数据业务会复杂很多，通常我们要统计更多的数据维度，比如 100 个指标，数据量通常都是超过 TB、EB 级别的，所以整个数据分析的处理难度是呈指数级增加的。这个时候，仅仅通过肉眼就很难找到问题所在了。

我举了这样一个简单的例子，带你理解在数据分析之前为什么要有数据清洗这个重要的准备工作。有经验的数据分析师都知道，好的数据分析师必定是一名数据清洗高手，要知道在整个数据分析过程中，不论是在时间还是功夫上，数据清洗大概都占到了 80%。

数据质量的准则

在上面这个服装店会员数据的案例中，一看到这些数据，你肯定能发现几个问题。你是不是想知道，有没有一些准则来规范这些数据的质量呢？

准则肯定是有的。不过如果数据存在七八种甚至更多的问题，我们很难将这些规则都记住。有研究说一个人的短期记忆，最多可以记住 7 条内容或信息，超过 7 条就记不住了。而数据清洗要解决的问题，远不止 7 条，我们万一漏掉一项该怎么办呢？有没有一种方法，我们既可以很方便地记住，又能保证我们的数据得到很好的清洗，提升数据质量呢？

在这里，我将数据清洗规则总结为以下 4 个关键点，统一起来叫“完全合一”，下面我来解释下。

完整性：单条数据是否存在空值，统计的字段是否完善。

全面性：观察某一列的全部数值，比如在 Excel 表中，我们选中一列，可以看到该列的平均值、最大值、最小值。我们可以通过常识来判断该列是否有问题，比如：数据定义、单位标识、数值本身。

合法性：数据的类型、内容、大小的合法性。比如数据中存在非 ASCII 字符，性别存在了未知，年龄超过了 150 岁等。

唯一性：数据是否存在重复记录，因为数据通常来自不同渠道的汇总，重复的情况是常见的。行数据、列数据都需要是唯一的，比如一个人不能重复记录多次，且一个人的体重也不能在列指标中重复记录多次。

在很多数据挖掘的教学中，数据准则通常会列出来 7~8 项，在这里我们归类成了“完全合一”4 项准则，按照以上的原则，我们能解决数据清理中遇到的大部分问题，使得数据标准、干净、连续，为后续数据统计、数据挖掘做好准备。如果想要进一步优化数据质量，还需要在实际案例中灵活使用。

清洗数据，一一击破

了解了数据质量准则之后，我们针对上面服装店会员数据案例中的问题进行一一击破。

这里你就需要 Python 的 Pandas 工具了。这个工具我们之前介绍过。它是基于 NumPy 的工具，专门为解决数据分析任务而创建。Pandas 纳入了大量库，我们可以利用这些库高效地进行数据清理工作。

这里我补充说明一下，如果你对 Python 还不是很熟悉，但是很想从事数据挖掘、数据分析相关的工作，那么花一些时间和精力来学习一下 Python 是很有必要的。Python 拥有丰富的库，堪称数据挖掘利器。当然了，数据清洗的工具也还有很多，这里我们只是以 Pandas 为例，帮你应用数据清洗准则，带你更加直观地了解数据清洗到底是怎么回事儿。

下面，我们就依照“完全合一”的准则，使用 Pandas 来进行清洗。

1. 完整性

问题 1：缺失值

在数据中有些年龄、体重数值是缺失的，这往往是因为数据量较大，在过程中，有些数值没有采集到。通常我们可以采用以下三种方法：

删除：删除数据缺失的记录；

均值：使用当前列的均值；

高频：使用当前列出现频率最高的数据。

比如我们想对 df[‘Age’] 中缺失的数值用平均年龄进行填充，可以这样写：

df['Age'].fillna(df['Age'].mean(), inplace=True)

如果我们用最高频的数据进行填充，可以先通过 value_counts 获取 Age 字段最高频次 age_maxf，然后再对 Age 字段中缺失的数据用 age_maxf 进行填充：

age_maxf = train_features['Age'].value_counts().index[0]

train_features['Age'].fillna(age_maxf, inplace=True)

问题 2：空行

我们发现数据中有一个空行，除了 index 之外，全部的值都是 NaN。Pandas 的 read_csv() 并没有可选参数来忽略空行，这样，我们就需要在数据被读入之后再使用 dropna() 进行处理，删除空行。

# 删除全空的行

df.dropna(how='all',inplace=True)

2. 全面性

问题：列数据的单位不统一

观察 weight 列的数值，我们能发现 weight 列的单位不统一。有的单位是千克（kgs），有的单位是磅（lbs）。

这里我使用千克作为统一的度量单位，将磅（lbs）转化为千克（kgs）：

# 获取 weight 数据列中单位为 lbs 的数据

rows_with_lbs = df['weight'].str.contains('lbs').fillna(False)

print df[rows_with_lbs]

# 将 lbs 转换为 kgs, 2.2lbs=1kgs

for i,lbs_row in df[rows_with_lbs].iterrows():

	# 截取从头开始到倒数第三个字符之前，即去掉 lbs。

	weight = int(float(lbs_row['weight'][:-3])/2.2)

	df.at[i,'weight'] = '{}kgs'.format(weight)

3. 合理性

问题：非 ASCII 字符

我们可以看到在数据集中 Fristname 和 Lastname 有一些非 ASCII 的字符。我们可以采用删除或者替换的方式来解决非 ASCII 问题，这里我们使用删除方法：

# 删除非 ASCII 字符

df['first_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True)

df['last_name'].replace({r'[^\x00-\x7F]+':''}, regex=True, inplace=True)

4. 唯一性

问题 1：一列有多个参数

在数据中不难发现，姓名列（Name）包含了两个参数 Firtname 和 Lastname。为了达到数据整洁目的，我们将 Name 列拆分成 Firstname 和 Lastname 两个字段。我们使用 Python 的 split 方法，str.split(expand=True)，将列表拆成新的列，再将原来的 Name 列删除。

# 切分名字，删除源数据列

df[['first_name','last_name']] = df['name'].str.split(expand=True)

df.drop('name', axis=1, inplace=True)

问题 2：重复数据

我们校验一下数据中是否存在重复记录。如果存在重复记录，就使用 Pandas 提供的 drop_duplicates() 来删除重复数据。

# 删除重复数据行

df.drop_duplicates(['first_name','last_name'],inplace=True)

这样，我们就将上面案例中的会员数据进行了清理，来看看清理之后的数据结果。怎么样？是不是又干净又标准？

养成数据审核的习惯

现在，你是不是能感受到数据问题不是小事，上面这个简单的例子里都有 6 处错误。所以我们常说，现实世界的数据是“肮脏的”，需要清洗。

第三方的数据要清洗，自有产品的数据，也需要数据清洗。比如美团自身做数据挖掘的时候，也需要去除爬虫抓取，作弊数据等。可以说没有高质量的数据，就没有高质量的数据挖掘，而数据清洗是高质量数据的一道保障。

当你从事这方面工作的时候，你会发现养成数据审核的习惯非常重要。而且越是优秀的数据挖掘人员，越会有“数据审核”的“职业病”。这就好比编辑非常在意文章中的错别字、语法一样。

数据的规范性，就像是你的作品一样，通过清洗之后，会变得非常干净、标准。当然了，这也是一门需要不断修炼的功夫。终有一天，你会进入这样一种境界：看一眼数据，差不多 7 秒钟的时间，就能知道这个数据是否存在问题。为了这一眼的功力，我们要做很多练习。

刚开始接触数据科学工作的时候，一定会觉得数据挖掘是件很酷、很有价值的事。确实如此，不过今天我还要告诉你，再酷炫的事也离不开基础性的工作，就像我们今天讲的数据清洗工作。对于这些基础性的工作，我们需要耐下性子，一个坑一个坑地去解决。

好了，最后我们来总结下今天的内容，你都收获了什么？

学习完今天的内容后，给你留个小作业吧。下面是一个美食数据，如果你拿到下面的数据，按照我们今天讲的准则，你能找到几点问题？如果你来清洗这些数据，你打算怎样清洗呢？

欢迎在留言区写下你的思考，如果你对今天“数据清洗”的内容还有疑问，也欢迎留言和我讨论。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

12 | 数据集成：这些大号一共20亿粉丝？

我们采集的数据经常会有冗余重复的情况。举个简单的例子，假设你是一个网络综艺节目的制片人，一共有 12 期节目，你一共打算邀请 30 位明星作为节目的嘉宾。你知道这些明星影响力都很大，具体在微博上的粉丝数都有标记。于是你想统计下，这些明星一共能直接影响到微博上的多少粉丝，能产生多大的影响力。

然后你突然发现，这些明星的粉丝数总和超过了 20 亿。那么他们一共会影响到中国 20 亿人口么？显然不是的，我们都知道中国人口一共是 14 亿，这 30 位明星的影响力总和不会覆盖中国所有人口。

那么如何统计这 30 位明星真实的影响力总和呢？这里就需要用到数据集成的概念了。

数据集成就是将多个数据源合并存放在一个数据存储中（如数据仓库），从而方便后续的数据挖掘工作。

据统计，大数据项目中 80% 的工作都和数据集成有关，这里的数据集成有更广泛的意义，包括了数据清洗、数据抽取、数据集成和数据变换等操作。这是因为数据挖掘前，我们需要的数据往往分布在不同的数据源中，需要考虑字段表达是否一样，以及属性是否冗余。

数据集成的两种架构：ELT 和 ETL

数据集成是数据工程师要做的工作之一。一般来说，数据工程师的工作包括了数据的 ETL 和数据挖掘算法的实现。算法实现可以理解，就是通过数据挖掘算法，从数据仓库中找到“金子“。

什么是 ETL 呢？ETL 是英文 Extract、Transform 和 Load 的缩写，顾名思义它包括了数据抽取、转换、加载三个过程。ETL 可以说是进行数据挖掘这项工作前的“备菜”过程。

我来解释一下数据抽取、转换、加载这三个过程。

抽取是将数据从已有的数据源中提取出来。

转换是对原始数据进行处理，例如将表输入 1 和 表输入 2 进行连接形成一张新的表。

如果是三张表连接的话，可以怎么操作呢？先将表输入 1 和表输入 2 进行连接形成表输入 1-2，然后将表输入 1-2 和表输入 3 进行连接形成新的表。然后再将生成的新表写入目的地。

根据转换发生的顺序和位置，数据集成可以分为 ETL 和 ELT 两种架构。

ETL 的过程为提取 (Extract)——转换 (Transform)——加载 (Load)，在数据源抽取后首先进行转换，然后将转换的结果写入目的地。

ELT 的过程则是提取 (Extract)——加载 (Load)——变换 (Transform)，在抽取后将结果先写入目的地，然后利用数据库的聚合分析能力或者外部计算框架，如 Spark 来完成转换的步骤。

目前数据集成的主流架构是 ETL，但未来使用 ELT 作为数据集成架构的将越来越多。这样做会带来多种好处：

ELT 和 ETL 相比，最大的区别是“重抽取和加载，轻转换”，从而可以用更轻量的方案搭建起一个数据集成平台。使用 ELT 方法，在提取完成之后，数据加载会立即开始。一方面更省时，另一方面 ELT 允许 BI 分析人员无限制地访问整个原始数据，为分析师提供了更大的灵活性，使之能更好地支持业务。

在 ELT 架构中，数据变换这个过程根据后续使用的情况，需要在 SQL 中进行，而不是在加载阶段进行。这样做的好处是你可以从数据源中提取数据，经过少量预处理后进行加载。这样的架构更简单，使分析人员更好地了解原始数据的变换过程。

ETL 工具有哪些？

介绍完了这两种架构，你肯定想要知道 ETL 工具都有哪些？

典型的 ETL 工具有:

商业软件：Informatica PowerCenter、IBM InfoSphere DataStage、Oracle Data Integrator、Microsoft SQL Server Integration Services 等

开源软件：Kettle、Talend、Apatar、Scriptella、DataX、Sqoop 等

相对于传统的商业软件，Kettle 是一个易于使用的，低成本的解决方案。国内很多公司都在使用 Kettle 用来做数据集成。所以我重点给你讲解下 Kettle 工具的使用。

Kettle 工具的使用

Kettle 是一款国外开源的 ETL 工具，纯 Java 编写，可以在 Window 和 Linux 上运行，不需要安装就可以使用。Kettle 中文名称叫水壶，该项目的目标是将各种数据放到一个壶里，然后以一种指定的格式流出。

Kettle 在 2006 年并入了开源的商业智能公司 Pentaho, 正式命名为 Pentaho Data Integeration，简称“PDI”。因此 Kettle 现在是 Pentaho 的一个组件，下载地址：https://community.hitachivantara.com/docs/DOC-1009855

在使用 Kettle 之前还需要安装数据库软件和 Java 运行环境（JRE）。

Kettle 采用可视化的方式进行操作，来对数据库间的数据进行迁移。它包括了两种脚本：Transformation 转换和 Job 作业。

Transformation（转换）：相当于一个容器，对数据操作进行了定义。数据操作就是数据从输入到输出的一个过程。你可以把转换理解成为是比作业粒度更小的容器。在通常的工作中，我们会把任务分解成为不同的作业，然后再把作业分解成多个转换。

Job（作业）：相比于转换是个更大的容器，它负责将转换组织起来完成某项作业。

接下来，我分别讲下这两个脚本的创建过程。

如何创建 Transformation（转换）

Transformation 可以分成三个步骤，它包括了输入、中间转换以及输出。

在 Transformation 中包括两个主要概念：Step 和 Hop。Step 的意思就是步骤，Hop 就是跳跃线的意思。

Step（步骤）：Step 是转换的最小单元，每一个 Step 完成一个特定的功能。在上面这个转换中，就包括了表输入、值映射、去除重复记录、表输出这 4 个步骤；

Hop（跳跃线）：用来在转换中连接 Step。它代表了数据的流向。

如何创建 Job（作业）：

完整的任务，实际上是将创建好的转换和作业串联起来。在这里 Job 包括两个概念：Job Entry、Hop。

如何理解这两个概念呢？

Job Entry（工作实体）：Job Entry 是 Job 内部的执行单元，每一个 Job Entry 都是用来执行具体的任务，比如调用转换，发送邮件等。

Hop：指连接 Job Entry 的线。并且它可以指定是否有条件地执行。

在 Kettle 中，你可以使用 Spoon，它是一种一种图形化的方式，来让你设计 Job 和 Transformation，并且可以保存为文件或者保存在数据库中。下面我来带你做一个简单的例子。

案例 1：如何将文本文件的内容转化到 MySQL 数据库中

这里我给你准备了文本文件，这个文件我上传到了 GitHub 上，你可以自行下载：http://t.cn/E4SzvOf，数据描述如下：

下面我来教你，如何将文本文件的内容转化到 MySQL 数据库中。

Step1：创建转换，右键“转换→新建”；

Step2：在左侧“核心对象”栏目中选择“文本文件输入”控件，拖拽到右侧的工作区中；

Step3：从左侧选择“表输出”控件，拖拽到右侧工作区；

Step4：鼠标在“文本文件输入”控件上停留，在弹窗中选择图标，鼠标拖拽到“表输出”控件，将一条连线连接到两个控件上；

这时我们已经将转换的流程设计好了，现在是要对输入和输出两个控件进行设置。

Step5：双击“文本文件输入”控件，导入已经准备好的文本文件；

Step6：双击“表输出”控件，这里你需要配置下 MySQL 数据库的连接，同时数据库中需要有一个数据表，字段的设置与文本文件的字段设置一致（这里我设置了一个 wucai 数据库，以及 score 数据表。字段包括了 name、create_time、Chinese、English、Math，与文本文件的字段一致）。

具体操作可以看下面的演示：

Step7：创建数据库字段的对应关系，这个需要双击“表输出”，找到数据库字段，进行字段映射的编辑；

Step8：点击左上角的执行图标，如下图：

这样我们就完成了从文本文件到 MySQL 数据库的转换。

Kettle 的控件比较多，内容无法在一节课内容中完整呈现，我只给你做个入门了解。

另外给你推荐一个 Kettle 的开源社区：http://www.ukettle.org 。

在社区里，你可以和大家进行交流。因为 Kettle 相比其他工具上手简单，而且是开源工具，有问题可以在社群里咨询。因此我推荐你使用 Kettle 作为你的第一个 ETL 工具。

当然除了 Kettle 工具，实际工作中，你可能也会接触到其他的 ETL 工具，这里我给你简单介绍下阿里巴巴的开源工具 DataX 和 Apache 的 Sqoop。

阿里开源软件：DataX

在以往的数据库中，数据库都是两两之间进行的转换，没有统一的标准，转换形式是这样的：

但 DataX 可以实现跨平台、跨数据库、不同系统之间的数据同步及交互，它将自己作为标准，连接了不同的数据源，以完成它们之间的转换。

DataX 的模式是基于框架 + 插件完成的，DataX 的框架如下图：

在这个框架里，Job 作业被 Splitter 分割器分成了许多小作业 Sub-Job。在 DataX 里，通过两个线程缓冲池来完成读和写的操作，读和写都是通过 Storage 完成数据的交换。比如在“读”模块，切分后的小作业，将数据从源头装载到 DataXStorage，然后在“写”模块，数据从 DataXStorage 导入到目的地。

这样的好处就是，在整体的框架下，我们可以对 Reader 和 Writer 进行插件扩充，比如我想从 MySQL 导入到 Oracle，就可以使用 MySQLReader 和 OracleWriter 插件，装在框架上使用即可。

Apache 开源软件:Sqoop

Sqoop 是一款开源的工具，是由 Apache 基金会所开发的分布式系统基础架构。Sqoop 在 Hadoop 生态系统中是占据一席之地的，它主要用来在 Hadoop 和关系型数据库中传递数据。通过 Sqoop，我们可以方便地将数据从关系型数据库导入到 HDFS 中，或者将数据从 HDFS 导出到关系型数据库中。

Hadoop 实现了一个分布式文件系统，即 HDFS。Hadoop 的框架最核心的设计就是 HDFS 和 MapReduce。HDFS 为海量的数据提供了存储，而 MapReduce 则为海量的数据提供了计算。

总结

今天我介绍了数据集成的两种架构方式，以及 Kettle 工具的基本操作。不要小看了 ETL，虽然它不直接交付数据挖掘的结果，但是却是数据挖掘前重要的工作，它包括了抽取各种数据、完成转化和加载这三个步骤。

因此除了数据科学家外，还有个工作职位叫 ETL 工程师，这份工作正是我们今天介绍的从事 ETL 这种架构工作的人。如果你以后有机会从事这份工作，你不仅要对今天介绍的数据集成概念有所了解，还要掌握至少一种 ETL 开发工具，如 Kettle、DataX、 Sqoop 等；此外还需要熟悉主流数据库技术，比如 SQL Server、PostgeSQL、Oracle 等。

这是我操作 kettle 的流程视频，你可以看一下。

今天我给你讲了数据集成的两种架构，以及帮助我们实现 ETL 的工具 Kettle。纸上得来终觉浅，绝知此事要躬行。你不妨尝试下如何使用 Kettle 将 MySQL 数据库内容转化到文本文件？

另我想让你来谈谈，你对数据集成的理解，如果你之前做过 ETL 的工具，也请你来谈一谈你对 ETL 的工具选择和使用经历。

欢迎在评论区与我分享你的想法。如果你觉得这篇文章对你有帮助，欢迎点击“请朋友读”，分享给你的朋友和同事。

13 | 数据变换：考试成绩要求正态分布合理么？

上一讲中我给你讲了数据集成，今天我来讲下数据变换。

如果一个人在百分制的考试中得了 95 分，你肯定会认为他学习成绩很好，如果得了 65 分，就会觉得他成绩不好。如果得了 80 分呢？你会觉得他成绩中等，因为在班级里这属于大部分人的情况。

为什么会有这样的认知呢？这是因为我们从小到大的考试成绩基本上都会满足正态分布的情况。什么是正态分布呢？正态分布也叫作常态分布，就是正常的状态下，呈现的分布情况。

比如你可能会问班里的考试成绩是怎样的？这里其实指的是大部分同学的成绩如何。以下图为例，在正态分布中，大部分人的成绩会集中在中间的区域，少部分人处于两头的位置。正态分布的另一个好处就是，如果你知道了自己的成绩，和整体的正态分布情况，就可以知道自己的成绩在全班中的位置。

另一个典型的例子就是，美国 SAT 考试成绩也符合正态分布。而且美国本科的申请，需要中国高中生的 GPA 在 80 分以上（百分制的成绩），背后的理由也是默认考试成绩属于正态分布的情况。

为了让成绩符合正态分布，出题老师是怎么做的呢？他们通常可以把考题分成三类：

第一类：基础题，占总分 70%，基本上属于送分题；

第二类：灵活题，基础范围内 + 一定的灵活性，占 20%；

第三类：难题，涉及知识面较广的难题，占 10%；

那么，你想下，如果一个出题老师没有按照上面的标准来出题，而是将第三类难题比重占到了 70%，也就是我们说的“超纲”，结果会是怎样呢？

你会发现，大部分人成绩都“不及格”，最后在大家激烈的讨论声中，老师会将考试成绩做规范化处理，从而让成绩满足正态分布的情况。因为只有这样，成绩才更具有比较性。所以正态分布的成绩，不仅可以让你了解全班整体的情况，还能了解每个人的成绩在全班中的位置。

数据变换在数据分析中的角色

我们再来举个例子，假设 A 考了 80 分，B 也考了 80 分，但前者是百分制，后者 500 分是满分，如果我们把从这两个渠道收集上来的数据进行集成、挖掘，就算使用效率再高的算法，结果也不是正确的。因为这两个渠道的分数代表的含义完全不同。

所以说，有时候数据变换比算法选择更重要，数据错了，算法再正确也是错的。你现在可以理解为什么 80% 的工作时间会花在前期的数据准备上了吧。

那么如何让不同渠道的数据统一到一个目标数据库里呢？这样就用到了数据变换。

在数据变换前，我们需要先对字段进行筛选，然后对数据进行探索和相关性分析，接着是选择算法模型（这里暂时不需要进行模型计算），然后针对算法模型对数据的需求进行数据变换，从而完成数据挖掘前的准备工作。

所以你从整个流程中可以看出，数据变换是数据准备的重要环节，它通过数据平滑、数据聚集、数据概化和规范化等方式将数据转换成适用于数据挖掘的形式。

我来介绍下这些常见的变换方法：

数据平滑：去除数据中的噪声，将连续数据离散化。这里可以采用分箱、聚类和回归的方式进行数据平滑，我会在后面给你讲解聚类和回归这两个算法；

数据聚集：对数据进行汇总，在 SQL 中有一些聚集函数可以供我们操作，比如 Max() 反馈某个字段的数值最大值，Sum() 返回某个字段的数值总和；

数据概化：将数据由较低的概念抽象成为较高的概念，减少数据复杂度，即用更高的概念替代更低的概念。比如说上海、杭州、深圳、北京可以概化为中国。

数据规范化：使属性数据按比例缩放，这样就将原来的数值映射到一个新的特定区域中。常用的方法有最小—最大规范化、Z—score 规范化、按小数定标规范化等，我会在后面给你讲到这些方法的使用；

属性构造：构造出新的属性并添加到属性集中。这里会用到特征工程的知识，因为通过属性与属性的连接构造新的属性，其实就是特征工程。比如说，数据表中统计每个人的英语、语文和数学成绩，你可以构造一个“总和”这个属性，来作为新属性。这样“总和”这个属性就可以用到后续的数据挖掘计算中。

在这些变换方法中，最简单易用的就是对数据进行规范化处理。下面我来给你讲下如何对数据进行规范化处理。

数据规范化的几种方法

1. Min-max 规范化

Min-max 规范化方法是将原始数据变换到 [0,1] 的空间中。用公式表示就是：

新数值 =（原数值 - 极小值）/（极大值 - 极小值）。

2. Z-Score 规范化

假设 A 与 B 的考试成绩都为 80 分，A 的考卷满分是 100 分（及格 60 分），B 的考卷满分是 500 分（及格 300 分）。虽然两个人都考了 80 分，但是 A 的 80 分与 B 的 80 分代表完全不同的含义。

那么如何用相同的标准来比较 A 与 B 的成绩呢？Z-Score 就是用来可以解决这一问题的。

我们定义：新数值 =（原数值 - 均值）/ 标准差。

假设 A 所在的班级平均分为 80，标准差为 10。B 所在的班级平均分为 400，标准差为 100。那么 A 的新数值 =(80-80)/10=0，B 的新数值 =(80-400)/100=-3.2。

那么在 Z-Score 标准下，A 的成绩会比 B 的成绩好。

我们能看到 Z-Score 的优点是算法简单，不受数据量级影响，结果易于比较。不足在于，它需要数据整体的平均值和方差，而且结果没有实际意义，只是用于比较。

3. 小数定标规范化

小数定标规范化就是通过移动小数点的位置来进行规范化。小数点移动多少位取决于属性 A 的取值中的最大绝对值。

举个例子，比如属性 A 的取值范围是 -999 到 88，那么最大绝对值为 999，小数点就会移动 3 位，即新数值 = 原数值 /1000。那么 A 的取值范围就被规范化为 -0.999 到 0.088。

上面这三种是数值规范化中常用的几种方式。

Python 的 SciKit-Learn 库使用

SciKit-Learn 是 Python 的重要机器学习库，它帮我们封装了大量的机器学习算法，比如分类、聚类、回归、降维等。此外，它还包括了数据变换模块。

我现在来讲下如何使用 SciKit-Learn 进行数据规范化。

1. Min-max 规范化

我们可以让原始数据投射到指定的空间 [min, max]，在 SciKit-Learn 里有个函数 MinMaxScaler 是专门做这个的，它允许我们给定一个最大值与最小值，然后将原数据投射到 [min, max] 中。默认情况下 [min,max] 是 [0,1]，也就是把原始数据投放到 [0,1] 范围内。

我们来看下下面这个例子：

# coding:utf-8

from sklearn import preprocessing

import numpy as np

# 初始化数据，每一行表示一个样本，每一列表示一个特征

x = np.array([[ 0., -3.,  1.],

              [ 3.,  1.,  2.],

              [ 0.,  1., -1.]])

# 将数据进行 [0,1] 规范化

min_max_scaler = preprocessing.MinMaxScaler()

minmax_x = min_max_scaler.fit_transform(x)

print minmax_x

运行结果：

[[0.         0.         0.66666667]

 [1.         1.         1.        ]

 [0.         1.         0.        ]]

2. Z-Score 规范化

在 SciKit-Learn 库中使用 preprocessing.scale() 函数，可以直接将给定数据进行 Z-Score 规范化。

from sklearn import preprocessing

import numpy as np

# 初始化数据

x = np.array([[ 0., -3.,  1.],

              [ 3.,  1.,  2.],

              [ 0.,  1., -1.]])

# 将数据进行 Z-Score 规范化

scaled_x = preprocessing.scale(x)

print scaled_x

运行结果：

[[-0.70710678 -1.41421356  0.26726124]

 [ 1.41421356  0.70710678  1.06904497]

 [-0.70710678  0.70710678 -1.33630621]]

这个结果实际上就是将每行每列的值减去了平均值，再除以方差的结果。

我们看到 Z-Score 规范化将数据集进行了规范化，数值都符合均值为 0，方差为 1 的正态分布。

3. 小数定标规范化

我们需要用 NumPy 库来计算小数点的位数。NumPy 库我们之前提到过。

这里我们看下运行代码：

# coding:utf-8

from sklearn import preprocessing

import numpy as np

# 初始化数据

x = np.array([[ 0., -3.,  1.],

              [ 3.,  1.,  2.],

              [ 0.,  1., -1.]])

# 小数定标规范化

j = np.ceil(np.log10(np.max(abs(x))))

scaled_x = x/(10**j)

print scaled_x

运行结果：

[[ 0.  -0.3  0.1]

 [ 0.3  0.1  0.2]

 [ 0.   0.1 -0.1]]

数据挖掘中数据变换比算法选择更重要

在考试成绩中，我们都需要让数据满足一定的规律，达到规范性的要求，便于进行挖掘。这就是数据变换的作用。

如果不进行变换的话，要不就是维数过多，增加了计算的成本，要不就是数据过于集中，很难找到数据之间的特征。

在数据变换中，重点是如何将数值进行规范化，有三种常用的规范方法，分别是 Min-Max 规范化、Z-Score 规范化、小数定标规范化。其中 Z-Score 规范化可以直接将数据转化为正态分布的情况，当然不是所有自然界的数据都需要正态分布，我们也可以根据实际的情况进行设计，比如取对数 log，或者神经网络里采用的激励函数等。

在最后我给大家推荐了 Python 的 sklearn 库，它和 NumPy, Pandas 都是非常有名的 Python 库，在数据统计工作中起了很大的作用。SciKit-Learn 不仅可以用于数据变换，它还提供了分类、聚类、预测等数据挖掘算法的 API 封装。后面我会详细给你讲解这些算法，也会教你如何使用 SciKit-Learn 工具来完成数据挖掘算法的工作。

最后给你留道思考题吧，假设属性 income 的最小值和最大值分别是 5000 元和 58000 元。利用 Min-Max 规范化的方法将属性的值映射到 0 至 1 的范围内，那么属性 income 的 16000 元将被转化为多少？

另外数据规范化都有哪些方式，他们是如何进行规范化的？欢迎在评论区与我分享你的答案，也欢迎你把这篇文章分享给你的朋友或者同事，一起讨论一下。

14丨数据可视化：掌握数据领域的万金油技能

上一讲中，我给你讲了数据变换，今天讲一下数据可视化。如果你想做一名数据分析师，那么掌握可视化技能是必不可少的，因为在大部分情况下，老板更关心呈现的结果。

另外当这些可视化的结果呈现在你眼前时，你才能直观地体会到“数据之美”。图片在内容表达上，要远胜于文字，它不仅能体现数据真实性，还能给人很大的想象空间。

数据可视化的视图都有哪些？

在了解数据可视化的重要性之后，我们来看下，在数据可视化产品中，一般都包括哪些视图？

我们常用的可视化视图超过 20 种，分别包括：文本表、热力图、地图、符号地图、饼图、水平条、堆叠条、并排条、树状图、圆视图、并排圆、线、双线、面积图、双组合、散点图、直方图、盒须图、甘特图、靶心图、气泡图等。

你不用记住这些视图名称都是什么，因为在可视化工具中，操作面板上都会有这些图形可控选择。你看一眼就知道它是不是你想要的。

当然，你不仅要掌握这些视图的使用，更要了解使用它们背后的目的是什么，这里我整理了下，可以分为以下的 9 种情况：

在上面的这几种情况里，你也许想要看某个数据的分布情况，或者它随着时间的趋势，或者是局部与整体之间的关系等等。所以在设计之前，你需要思考的是，你的用户是谁，想给他们呈现什么，需要突出数据怎样的特点，以及采用哪种视图来进行呈现。

比如说，你想呈现某个变量的分布情况，就可以通过直方图的形式来呈现。如果你想要看两个变量之间的相关性及分布情况，可以采用散点图的形式呈现。

一个视图可能会有多种表达的目的，比如散点图既可以表明两个变量之间的关系，也可以体现它们的分布情况。同样，如果我想看变量的分布情况，既可以采用散点图的形式，也可以采用直方图的形式。

所以说，具体要采用哪种视图，取决于你想要数据可视化呈现什么样的目的。

数据可视化工具都有哪些？

那么如何把数据弄成上面说的视图呢？我们需要借助数据可视化的工具。

这些工具有很多，我从几个维度来给你介绍下。这几个维度分别是商业智能分析软件、可视化大屏、前端可视化组件和编程语言类。

商业智能分析

首先在商业智能分析软件中，最著名的当属 Tableau 和 PowerBI 了，另外中国帆软出品的 FineBI 也受到国内很多企业的青睐。

Tableau 是国外的商业软件，收费不低。它适合 BI 工程师、数据分析分析师。如果可以熟练掌握 Tableau，那么找到一份数据分析的工作是不难的。

PowerBI 是微软出品的，可以和 Excel 搭配使用，你可以通过 PowerBI 来呈现 Excel 的可视化内容。

在 BI 行业中，Tableau 和 PowerBI 远超其他 BI 产品，处于行业的领导者的位置。

另外 FineBI 是中国的帆软出品，针对国内使用更加友好，同时也倾向于企业级应用的 BI。

可视化大屏类

大屏作为一种视觉效果强、科技感强的技术，被企业老板所青睐，可以很好地展示公司的数据化能力。这里给你介绍两款可视化大屏的软件 DataV 和 FineReport。

DataV

DataV 是一款可视化的工具，天猫双十一大屏就是用它呈现的。你要做的就是选择相应的控件，配置控件的样式、数据传输和交互效果等。当然 DataV 本身有一些免费的模板，你可以直接通过模板来创建。不过一些特殊的控件和交互效果还是需要购买企业版才行。

FineReport

FineReport 是帆软出品的工具，你可以看出他家的产品基本上都是 Fine 开头的，包括刚才给你介绍的 FineBI。FineReport 可以做数据大屏，也可以做可视化报表，在很多行业都有解决方案，操作起来也很方便。可以实时连接业务数据，对数据进行展示。

前端可视化组件

如果你想要成为一名前端数据可视化工程师的话，至少熟练掌握一种前端可视化组件是必不可少的，不少公司招聘“高级前端工程师”的时候，都要求熟悉几个开源数据可视化组件。

可视化组件都是基于 Web 渲染的技术的。所以你需要了解一下几个典型的 Web 渲染技术：Canvas、SVG 和 WebGL。简单来说，Canvas 和 SVG 是 HTML5 中主要的 2D 图形技术，WebGL 是 3D 框架。

Canvas 适用于位图，也就是给了你一张白板，需要你自己来画点。Canvas 技术可以绘制比较复杂的动画。不过它是 HTML5 自带的，所以低版本浏览器不支持 Canvas。ECharts 这个可视化组件就是基于 Canvas 实现的。

SVG 的中文是可缩放矢量图形，它是使用 XML 格式来定义图形的。相当于用点和线来描绘了图形，相比于位图来说文件比较小，而且任意缩放都不会失真。SVG 经常用于图标和图表上。它最大的特点就是支持大部分浏览器，动态交互性实现起来也很方便，比如在 SVG 中插入动画元素等。

WebGL 是一种 3D 绘图协议，能在网页浏览器中呈现 3D 画面技术，并且可以和用户进行交互。你在网页上看到的很多酷炫的 3D 效果，基本上都是用 WebGL 来渲染的。下面介绍的 Three.js 就是基于 WebGL 框架的。

在了解这些 Web 渲染协议之后，我再来带你看下这些常用的可视化组件： Echarts、D3、Three.js 和 AntV。

ECharts 是基于 H5 canvas 的 Javascript 图表库，是百度的开源项目，一直都有更新，使用的人也比较多。它作为一个组件，可以和 DataV、Python 进行组合使用。

你可以在 DataV 企业版中接入 ECharts 图表组件。也可以使用 Python 的 Web 框架（比如 Django、Flask）+ECharts 的解决方案。这样可以让你的项目更加灵活地使用到 ECharts 的图表库，不论你是用 Python 语言，还是用 DataV 的工具，都可以享受到 ECharts 丰富的图表库样式。

D3 的全称是 Data-Driven Documents，简单来说，是一个 JavaScript 的函数库，因为文件的后缀名通常为“.js”，所以 D3 也常使用 D3.js 来称呼。

它提供了各种简单易用的函数，大大简化了 JavaScript 操作数据的难度。你只需要输入几个简单的数据，就能够转换为各种绚丽的图形。由于它本质上是 JavaScript，所以用 JavaScript 也是可以实现所有功能的。

Three.js，顾名思义，就是 Three+JS 的意思。“Three”表示 3D 的意思，“Three.js”就是使用 JavaScript 来实现 3D 效果。Three.js 是一款 WebGL 框架，封装了大量 WebGL 接口，因为直接用 WebGL API 写 3D 程序太麻烦了。

AntV 是蚂蚁金服出品的一套数据可视化组件，包括了 G2、G6、F2 和 L7 一共 4 个组件。其中 G2 应该是最知名的，它的意思是 The grammar Of Graphics，也就是一套图形语法。它集成了大量的统计工具，而且可以让用户通过简单的语法搭建出多种图表。G6 是一套流程图和关系分析的图表库。F2 适用于移动端的可视化方案。L7 提供了地理空间的数据可视化框架。

编程语言

使用数据分析工具，你一定离不开 Python 语言，当然也有人使用 R 语言。在用 Python 和 R 做数据分析的时候，一定少不了用到可视化的部分。

下面我简单介绍下，如何使用 Python 和 R 进行数据可视化。

在 Python 里包括了众多可视化库，比如 Matplotlib、Seaborn、Bokeh、Plotly、Pyecharts、Mapbox 和 Geoplotlib。其中使用频率最高，最需要掌握的就是 Matplotlib 和 Seaborn。

Matplotlib 是 Python 的可视化基础库，作图风格和 MATLAB 类似，所以称为 Matplotlib。一般学习 Python 数据可视化，都会从 Matplotlib 入手，然后再学习其他的 Python 可视化库。

下面这张图就是我拿 Matplotlib 做的蜘蛛图。

Seaborn 是一个基于 Matplotlib 的高级可视化效果库，针对 Matplotlib 做了更高级的封装，让作图变得更加容易。你可以用短小的代码绘制更多维度数据的可视化效果图，比如下面这个例子：

在 R 中也有很多可视化库可供选择。其中包括了 R 自带的绘图包 Graphics 以及工具包 ggplot2、ggmap、timevis 和 plotly 等。

其中 ggplot2 是 R 语言中重要的绘图包，这个工具包将数据与绘图操作进行了分离，所以使用起来清晰明了，画出的图也漂亮。其实在 Python 里后来也引入了 ggplot 库，这样在 Python 中也可以很方便地使用到 ggplot，而且和 R 语言中的 ggplot2 代码差别不大，稍作修改，就能直接在 Python 中运行了。

如何开始数据可视化的学习

其实很多企业都有在用商业分析软件，Tableau 算是使用率很高的。如果你想做相关的数据分析研究，掌握一门语言尤其是 Python 还是很有必要的。如果你想要全面的学习数据可视化，你可以有以下的 3 个路径：

1. 重点推荐 Tableau

Tableau 在可视化灵活分析上功能强大，主要目标用户更多是较专业的数据分析师。同时在工作场景中使用率高，因此掌握 Tableau 对于晋升和求职都很有帮助。不过 Tableau 是个商业软件，收费不低。而且上手起来有一些门槛，需要一定数据基础。

2. 使用微图、DataV

前面我给你讲过八爪鱼的使用，微图和八爪鱼是一家公司的产品，使用起来非常方便，而且免费。当你用八爪鱼采集数据之后，就直接可以用微图进行数据可视化。

DataV 是阿里推出的数字大屏技术，不过它是收费的产品。它最大的好处，就是可以分享链接，让别人可以在线浏览，不需要像 Tableau 一样安装客户端才能看到数据可视化的结果。另外 DataV 有一些模板，你直接可以使用。

你可以先使用微图和 DataV 作为你的数据可视化体验工具，因为成本低，上手起来快。这样你对数据可视化可以有个直观的了解。如果你想从事数据可视化这份工作，你可以花更多的精力去研究和学习 Tableau。

3. Python 可视化

Python 是数据分析的首选语言，如果你不进行编程，可以使用我在上文中提到的数据可视化的工具。如果你的目标是个数据挖掘工程师，或者算法工程师，那么最重要的就是要了解，并且熟练掌握 Python 的数据可视化。

总结

我今天给你介绍了数据可视化的视图，然后带你讲解了当前主流的数据可视化工具。Tableau 和 PowerBI 是 BI 商业智能行业的领导者，是很多大公司里商业数据分析工作必备的工具。如果你使用 Python 作为数据分析的语言，那么也需要对 Python 数据可视化有所掌握。

最后给你留两道思考题吧。你平时的工作是怎样的，是否需要用到数据可视化技术呢？在数据可视化技术中，你都用过哪些工具，你觉得哪个工具使用起来比较顺手？

欢迎和我在评论区进行讨论，也欢迎你把这篇文章分享给你的朋友或者同事。

15丨一次学会Python数据可视化的10种技能

今天我来给你讲讲 Python 的可视化技术。

如果你想要用 Python 进行数据分析，就需要在项目初期开始进行探索性的数据分析，这样方便你对数据有一定的了解。其中最直观的就是采用数据可视化技术，这样，数据不仅一目了然，而且更容易被解读。同样在数据分析得到结果之后，我们还需要用到可视化技术，把最终的结果呈现出来。

可视化视图都有哪些？

按照数据之间的关系，我们可以把可视化视图划分为 4 类，它们分别是比较、联系、构成和分布。我来简单介绍下这四种关系的特点：

比较：比较数据间各类别的关系，或者是它们随着时间的变化趋势，比如折线图；

联系：查看两个或两个以上变量之间的关系，比如散点图；

构成：每个部分占整体的百分比，或者是随着时间的百分比变化，比如饼图；

分布：关注单个变量，或者多个变量的分布情况，比如直方图。

同样，按照变量的个数，我们可以把可视化视图划分为单变量分析和多变量分析。

单变量分析指的是一次只关注一个变量。比如我们只关注“身高”这个变量，来看身高的取值分布，而暂时忽略其他变量。

多变量分析可以让你在一张图上可以查看两个以上变量的关系。比如“身高”和“年龄”，你可以理解是同一个人的两个参数，这样在同一张图中可以看到每个人的“身高”和“年龄”的取值，从而分析出来这两个变量之间是否存在某种联系。

可视化的视图可以说是分门别类，多种多样，今天我主要介绍常用的 10 种视图，这些视图包括了散点图、折线图、直方图、条形图、箱线图、饼图、热力图、蜘蛛图、二元变量分布和成对关系。

下面我给你一一进行介绍。

散点图

散点图的英文叫做 scatter plot，它将两个变量的值显示在二维坐标中，非常适合展示两个变量之间的关系。当然，除了二维的散点图，我们还有三维的散点图。

我在上一讲中给你简单介绍了下 Matplotlib 这个工具，在 Matplotlib 中，我们经常会用到 pyplot 这个工具包，它包括了很多绘图函数，类似 Matlab 的绘图框架。在使用前你需要进行引用：

import matplotlib.pyplot as plt

在工具包引用后，画散点图，需要使用 plt.scatter(x, y, marker=None) 函数。x、y 是坐标，marker 代表了标记的符号。比如“x”、“>”或者“o”。选择不同的 marker，呈现出来的符号样式也会不同，你可以自己试一下。

下面三张图分别对应“x”“>”和“o”。

除了 Matplotlib 外，你也可以使用 Seaborn 进行散点图的绘制。在使用 Seaborn 前，也需要进行包引用：

import seaborn as sns

在引用 seaborn 工具包之后，就可以使用 seaborn 工具包的函数了。如果想要做散点图，可以直接使用 sns.jointplot(x, y, data=None, kind=‘scatter’) 函数。其中 x、y 是 data 中的下标。data 就是我们要传入的数据，一般是 DataFrame 类型。kind 这类我们取 scatter，代表散点的意思。当然 kind 还可以取其他值，这个我在后面的视图中会讲到，不同的 kind 代表不同的视图绘制方式。

好了，让我们来模拟下，假设我们的数据是随机的 1000 个点。

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

N = 1000

x = np.random.randn(N)

y = np.random.randn(N)

# 用 Matplotlib 画散点图

plt.scatter(x, y,marker='x')

plt.show()

# 用 Seaborn 画散点图

df = pd.DataFrame({'x': x, 'y': y})

sns.jointplot(x="x", y="y", data=df, kind='scatter');

plt.show()

我们运行一下这个代码，就可以看到下面的视图（第一张图为 Matplotlib 绘制的，第二张图为 Seaborn 绘制的）。其实你能看到 Matplotlib 和 Seaborn 的视图呈现还是有差别的。Matplotlib 默认情况下呈现出来的是个长方形。而 Seaborn 呈现的是个正方形，而且不仅显示出了散点图，还给了这两个变量的分布情况。

Matplotlib 绘制：

Seaborn 绘制：

折线图

折线图可以用来表示数据随着时间变化的趋势。

在 Matplotlib 中，我们可以直接使用 plt.plot() 函数，当然需要提前把数据按照 x 轴的大小进行排序，要不画出来的折线图就无法按照 x 轴递增的顺序展示。

在 Seaborn 中，我们使用 sns.lineplot (x, y, data=None) 函数。其中 x、y 是 data 中的下标。data 就是我们要传入的数据，一般是 DataFrame 类型。

这里我们设置了 x、y 的数组。x 数组代表时间（年），y 数组我们随便设置几个取值。下面是详细的代码。

import pandas as pd

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

x = [2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019]

y = [5, 3, 6, 20, 17, 16, 19, 30, 32, 35]

# 使用 Matplotlib 画折线图

plt.plot(x, y)

plt.show()

# 使用 Seaborn 画折线图

df = pd.DataFrame({'x': x, 'y': y})

sns.lineplot(x="x", y="y", data=df)

plt.show()

然后我们分别用 Matplotlib 和 Seaborn 进行画图，可以得到下面的图示。你可以看出这两个图示的结果是完全一样的，只是在 seaborn 中标记了 x 和 y 轴的含义。

直方图

直方图是比较常见的视图，它是把横坐标等分成了一定数量的小区间，这个小区间也叫作“箱子”，然后在每个“箱子”内用矩形条（bars）展示该箱子的箱子数（也就是 y 值），这样就完成了对数据集的直方图分布的可视化。

在 Matplotlib 中，我们使用 plt.hist(x, bins=10) 函数，其中参数 x 是一维数组，bins 代表直方图中的箱子数量，默认是 10。

在 Seaborn 中，我们使用 sns.distplot(x, bins=10, kde=True) 函数。其中参数 x 是一维数组，bins 代表直方图中的箱子数量，kde 代表显示核密度估计，默认是 True，我们也可以把 kde 设置为 False，不进行显示。核密度估计是通过核函数帮我们来估计概率密度的方法。

这是一段绘制直方图的代码。

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

a = np.random.randn(100)

s = pd.Series(a)

# 用 Matplotlib 画直方图

plt.hist(s)

plt.show()

# 用 Seaborn 画直方图

sns.distplot(s, kde=False)

plt.show()

sns.distplot(s, kde=True)

plt.show()

我们创建一个随机的一维数组，然后分别用 Matplotlib 和 Seaborn 进行直方图的显示，结果如下，你可以看出，没有任何差别，其中最后一张图就是 kde 默认为 Ture 时的显示情况。

条形图

如果说通过直方图可以看到变量的数值分布，那么条形图可以帮我们查看类别的特征。在条形图中，长条形的长度表示类别的频数，宽度表示类别。

在 Matplotlib 中，我们使用 plt.bar(x, height) 函数，其中参数 x 代表 x 轴的位置序列，height 是 y 轴的数值序列，也就是柱子的高度。

在 Seaborn 中，我们使用 sns.barplot(x=None, y=None, data=None) 函数。其中参数 data 为 DataFrame 类型，x、y 是 data 中的变量。

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

x = ['Cat1', 'Cat2', 'Cat3', 'Cat4', 'Cat5']

y = [5, 4, 8, 12, 7]

# 用 Matplotlib 画条形图

plt.bar(x, y)

plt.show()

# 用 Seaborn 画条形图

sns.barplot(x, y)

plt.show()

我们创建了 x、y 两个数组，分别代表类别和类别的频数，然后用 Matplotlib 和 Seaborn 进行条形图的显示，结果如下：

箱线图

箱线图，又称盒式图，它是在 1977 年提出的，由五个数值点组成：最大值 (max)、最小值 (min)、中位数 (median) 和上下四分位数 (Q3, Q1)。它可以帮我们分析出数据的差异性、离散程度和异常值等。

在 Matplotlib 中，我们使用 plt.boxplot(x, labels=None) 函数，其中参数 x 代表要绘制箱线图的数据，labels 是缺省值，可以为箱线图添加标签。

在 Seaborn 中，我们使用 sns.boxplot(x=None, y=None, data=None) 函数。其中参数 data 为 DataFrame 类型，x、y 是 data 中的变量。

# 数据准备

# 生成 0-1 之间的 10*4 维度数据

data=np.random.normal(size=(10,4))

lables = ['A','B','C','D']

# 用 Matplotlib 画箱线图

plt.boxplot(data,labels=lables)

plt.show()

# 用 Seaborn 画箱线图

df = pd.DataFrame(data, columns=lables)

sns.boxplot(data=df)

plt.show()

这段代码中，我生成 0-1 之间的 10*4 维度数据，然后分别用 Matplotlib 和 Seaborn 进行箱线图的展示，结果如下。

Matplotlib 绘制：

Seaborn 绘制：

饼图

饼图是常用的统计学模块，可以显示每个部分大小与总和之间的比例。在 Python 数据可视化中，它用的不算多。我们主要采用 Matplotlib 的 pie 函数实现它。

在 Matplotlib 中，我们使用 plt.pie(x, labels=None) 函数，其中参数 x 代表要绘制饼图的数据，labels 是缺省值，可以为饼图添加标签。

这里我设置了 lables 数组，分别代表高中、本科、硕士、博士和其他几种学历的分类标签。nums 代表这些学历对应的人数。

import matplotlib.pyplot as plt

# 数据准备

nums = [25, 37, 33, 37, 6]

labels = ['High-school','Bachelor','Master','Ph.d', 'Others']

# 用 Matplotlib 画饼图

plt.pie(x = nums, labels=labels)

plt.show()

通过 Matplotlib 的 pie 函数，我们可以得出下面的饼图：

热力图

热力图，英文叫 heat map，是一种矩阵表示方法，其中矩阵中的元素值用颜色来代表，不同的颜色代表不同大小的值。通过颜色就能直观地知道某个位置上数值的大小。另外你也可以将这个位置上的颜色，与数据集中的其他位置颜色进行比较。

热力图是一种非常直观的多元变量分析方法。

我们一般使用 Seaborn 中的 sns.heatmap(data) 函数，其中 data 代表需要绘制的热力图数据。

这里我们使用 Seaborn 中自带的数据集 flights，该数据集记录了 1949 年到 1960 年期间，每个月的航班乘客的数量。

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

flights = sns.load_dataset("flights")

data=flights.pivot('year','month','passengers')

# 用 Seaborn 画热力图

sns.heatmap(data)

plt.show()

通过 seaborn 的 heatmap 函数，我们可以观察到不同年份，不同月份的乘客数量变化情况，其中颜色越浅的代表乘客数量越多，如下图所示：

蜘蛛图

蜘蛛图是一种显示一对多关系的方法。在蜘蛛图中，一个变量相对于另一个变量的显著性是清晰可见的。

假设我们想要给王者荣耀的玩家做一个战力图，指标一共包括推进、KDA、生存、团战、发育和输出。那该如何做呢？

这里我们需要使用 Matplotlib 来进行画图，首先设置两个数组：labels 和 stats。他们分别保存了这些属性的名称和属性值。

因为蜘蛛图是一个圆形，你需要计算每个坐标的角度，然后对这些数值进行设置。当画完最后一个点后，需要与第一个点进行连线。

因为需要计算角度，所以我们要准备 angles 数组；又因为需要设定统计结果的数值，所以我们要设定 stats 数组。并且需要在原有 angles 和 stats 数组上增加一位，也就是添加数组的第一个元素。

import numpy as np

import matplotlib.pyplot as plt

import seaborn as sns

from matplotlib.font_manager import FontProperties

# 数据准备

labels=np.array([u" 推进 ","KDA",u" 生存 ",u" 团战 ",u" 发育 ",u" 输出 "])

stats=[83, 61, 95, 67, 76, 88]

# 画图数据准备，角度、状态值

angles=np.linspace(0, 2*np.pi, len(labels), endpoint=False)

stats=np.concatenate((stats,[stats[0]]))

angles=np.concatenate((angles,[angles[0]]))

# 用 Matplotlib 画蜘蛛图

fig = plt.figure()

ax = fig.add_subplot(111, polar=True)

ax.plot(angles, stats, 'o-', linewidth=2)

ax.fill(angles, stats, alpha=0.25)

# 设置中文字体

font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf", size=14)

ax.set_thetagrids(angles * 180/np.pi, labels, FontProperties=font)

plt.show()

代码中 flt.figure 是创建一个空白的 figure 对象，这样做的目的相当于画画前先准备一个空白的画板。然后 add_subplot(111) 可以把画板划分成 1 行 1 列。再用 ax.plot 和 ax.fill 进行连线以及给图形上色。最后我们在相应的位置上显示出属性名。这里需要用到中文，Matplotlib 对中文的显示不是很友好，因此我设置了中文的字体 font，这个需要在调用前进行定义。最后我们可以得到下面的蜘蛛图，看起来是不是很酷？

二元变量分布

如果我们想要看两个变量之间的关系，就需要用到二元变量分布。当然二元变量分布有多种呈现方式，开头给你介绍的散点图就是一种二元变量分布。

在 Seaborn 里，使用二元变量分布是非常方便的，直接使用 sns.jointplot(x, y, data=None, kind) 函数即可。其中用 kind 表示不同的视图类型：“kind=‘scatter’”代表散点图，“kind=‘kde’”代表核密度图，“kind=‘hex’ ”代表 Hexbin 图，它代表的是直方图的二维模拟。

这里我们使用 Seaborn 中自带的数据集 tips，这个数据集记录了不同顾客在餐厅的消费账单及小费情况。代码中 total_bill 保存了客户的账单金额，tip 是该客户给出的小费金额。我们可以用 Seaborn 中的 jointplot 来探索这两个变量之间的关系。

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

tips = sns.load_dataset("tips")

print(tips.head(10))

# 用 Seaborn 画二元变量分布图（散点图，核密度图，Hexbin 图）

sns.jointplot(x="total_bill", y="tip", data=tips, kind='scatter')

sns.jointplot(x="total_bill", y="tip", data=tips, kind='kde')

sns.jointplot(x="total_bill", y="tip", data=tips, kind='hex')

plt.show()

代码中我用 kind 分别显示了他们的散点图、核密度图和 Hexbin 图，如下图所示。

散点图：

核密度图：

Hexbin 图：

成对关系

如果想要探索数据集中的多个成对双变量的分布，可以直接采用 sns.pairplot() 函数。它会同时展示出 DataFrame 中每对变量的关系，另外在对角线上，你能看到每个变量自身作为单变量的分布情况。它可以说是探索性分析中的常用函数，可以很快帮我们理解变量对之间的关系。

pairplot 函数的使用，就像在 DataFrame 中使用 describe() 函数一样方便，是数据探索中的常用函数。

这里我们使用 Seaborn 中自带的 iris 数据集，这个数据集也叫鸢尾花数据集。鸢尾花可以分成 Setosa、Versicolour 和 Virginica 三个品种，在这个数据集中，针对每一个品种，都有 50 个数据，每个数据中包括了 4 个属性，分别是花萼长度、花萼宽度、花瓣长度和花瓣宽度。通过这些数据，需要你来预测鸢尾花卉属于三个品种中的哪一种。

import matplotlib.pyplot as plt

import seaborn as sns

# 数据准备

iris = sns.load_dataset('iris')

# 用 Seaborn 画成对关系

sns.pairplot(iris)

plt.show()

这里我们用 Seaborn 中的 pairplot 函数来对数据集中的多个双变量的关系进行探索，如下图所示。从图上你能看出，一共有 sepal_length、sepal_width、petal_length 和 petal_width4 个变量，它们分别是花萼长度、花萼宽度、花瓣长度和花瓣宽度。

下面这张图相当于这 4 个变量两两之间的关系。比如矩阵中的第一张图代表的就是花萼长度自身的分布图，它右侧的这张图代表的是花萼长度与花萼宽度这两个变量之间的关系。

总结

我今天给你讲了 Python 可视化工具包 Matplotlib 和 Seaborn 工具包的使用。他们两者之间的关系就相当于 NumPy 和 Pandas 的关系。Seaborn 是基于 Matplotlib 更加高级的可视化库。

另外针对我讲到的这 10 种可视化视图，可以按照变量之间的关系对它们进行分类，这些关系分别是比较、联系、构成和分布。当然我们也可以按照随机变量的个数来进行划分，比如单变量分析和多变量分析。在数据探索中，成对关系 pairplot() 的使用，相好比 Pandas 中的 describe() 使用一样方便，常用于项目初期的数据可视化探索。

在 Matplotlib 和 Seaborn 的函数中，我只列了最基础的使用，也方便你快速上手。当然如果你也可以设置修改颜色、宽度等视图属性。你可以自己查看相关的函数帮助文档。这些留给你来进行探索。

关于本次 Python 可视化的学习，我希望你能掌握：

视图的分类，以及可以从哪些维度对它们进行分类；

十种常见视图的概念，以及如何在 Python 中进行使用，都需要用到哪些函数；

需要自己动手跑一遍案例中的代码，体验下 Python 数据可视化的过程。

最后，我给你留两道思考题吧，Seaborn 数据集中自带了 car_crashes 数据集，这是一个国外车祸的数据集，你要如何对这个数据集进行成对关系的探索呢？第二个问题就是，请你用 Seaborn 画二元变量分布图，如果想要画散点图，核密度图，Hexbin 图，函数该怎样写？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起来动手练习一下。

16丨数据分析基础篇答疑

截止到今天，我们已经将数据分析基础篇的内容都学习完了。在这个过程中，感谢大家积极踊跃地进行留言，既给其他同学提供了不少帮助，也让专栏增色了不少。在这些留言中，有很多同学对某个知识点有所疑惑，我总结了 NumPy、Pandas、爬虫以及数据变换中同学们遇到的问题，精选了几个具有代表性的来作为答疑。

NumPy 相关

答疑 1：如何理解 NumPy 中 axis 的使用？

这里我引用文稿中的一段代码：

a = np.array([[4,3,2],[2,4,1]])

print np.sort(a)

print np.sort(a, axis=None)

print np.sort(a, axis=0)

print np.sort(a, axis=1)

同学们最容易混淆的是 axis=0 和 axis=1 的顺序。你可以记住：axis=0 代表跨行（实际上就是按列），axis=1 代表跨列（实际上就是按行）。

如果排序的时候，没有指定 axis，默认 axis=-1，代表就是按照数组最后一个轴来排序。如果 axis=None，代表以扁平化的方式作为一个向量进行排序。

所以上面的运行结果为：

[[2 3 4]

 [1 2 4]]

[1 2 2 3 4 4]

[[2 3 1]

 [4 4 2]]

[[2 3 4]

 [1 2 4]]

我解释下 axis=0 的排序结果，axis=0 代表的是跨行（跨行就是按照列），所以实际上是对 [4, 2] [3, 4] [2, 1] 来进行排序，排序结果是 [2, 4] [3, 4] [1, 2]，对应的是每一列的排序结果。还原到矩阵中也就是 [[2 3 1], [4, 4, 2]]。

答疑 2：定义结构数组中的 S32 代表什么意思？

我文稿中定义了一个结构数组 persontype。

import numpy as np

persontype = np.dtype({

    'names':['name', 'age', 'chinese', 'math', 'english'],

    'formats':['S32','i', 'i', 'i', 'f']})

这里实际上用的是 numpy 中的字符编码来表示数据类型的定义，比如 i 代表整数，f 代表单精度浮点数，S 代表字符串，S32 代表的是 32 个字符的字符串。

如果数据中使用了中文，可以把类型设置为 U32，比如：

import numpy as np

persontype = np.dtype({

    'names':['name', 'age', 'chinese', 'math', 'english'],

    'formats':['U32','i', 'i', 'i', 'f']})

peoples = np.array([(" 张飞 ",32,75,100, 90),(" 关羽 ",24,85,96,88.5), (" 赵云 ",28,85,92,96.5),(" 黄忠 ",29,65,85,100)], dtype=persontype)

答疑 3：PyCharm 中无法 import numpy 的问题

有些同学已经安装好了 numpy，但在 PyCharm 中依然无法使用 numpy。遇到这个问题的主要原因是 PyCharm 会给每一个新建的项目都是一个全新的虚拟环境。

在这个环境下，默认的包只有 pip、setuptools 和 wheel 这三个工具，你可以在 File->Settings 里面找到这个界面。

这说明 numpy 并没有配置到你创建的这个 Project 下的环境中，需要手动点击右侧的 + 号，对 numpy 进行添加。

添加之后，你就可以正常运行程序，显示出结果了。

答疑 4：我不明白为什么打印出来的 name 会带一个 b？

这位同学的代码是这样的：

student = np.dtype([('name','S20'), ('age', 'i1'), ('marks', 'f4')])

 a = np.array([('abc', 21, 50),('xyz', 18, 75)], dtype = student)

 print(a)

 print(a['name'])

 结果：

 [(b'abc', 21, 50.) (b'xyz', 18, 75.)]

 [b'abc' b'xyz']

我来解释一下。Python3 默认 str 是 Unicode 类型，所以要转成 bytestring，会在原来的 str 前加上 b。如果你用 py2.7 就不会有这个问题，py3 的 b 只是告诉你这里它转化成了 bytestring 进行输出。

答疑 5：np.ceil 代表什么意思？

ceil 是 numpy 中的一个函数，代表向上取整。比如 np.ceil(2.4)=3。

数据分析思维培养及练习相关

答疑 1：Online Judge 的比赛题目，数学不好怎么办？

Vol1~Vol32 的难度是逐渐增加吗的，怎么可以选出有易到难的题目？

Online Judge 有一些简单的题目可以选择，选择使用人数多，且 accepted 比例高的题目。另外它面向的是一些参加比赛的人员，比如高中的 NOI 比赛，或者大学的 ACM 比赛。你也可以选择 leetcode 或者 pythontip 进行训练。

难度不一定是增加的，而是出题的先后顺序。难易程度，你可以看下提交的人数和 Accepted 的比例。提交人数和 Accepted 比例越高，说明越简单。

答疑 2：加餐中小区宽带使用多台手机等设备，不会被检测到吗？

小区宽带和手机飞行是两种解决方案。用手机飞行不需要用到小区宽带。 用小区宽带需要使用到交换机，这里可以自己来控制交换机，每次自动切换 IP。

答疑 3：加餐中提到的一万个手机号。。。那怎么更换呢？也要一万台设备吗？

1 万个手机号，主要用于账号注册，通常采用的是“卡池”这个设备。简单来说，卡池可以帮你做收发短信。一个卡池设备 512 张卡，并发 32 路。

有了卡池，还需要算法。你不能让这 512 张卡每次操作都是有规律可循的，比如都是同步执行某项操作，否则微信、Facebook 会直接把它们干掉。学过数据挖掘的人应该会知道，这 512 张卡如果是协同操作，可以直接被算法识别出来。在微信、Facebook 看来，这 512 张卡实际上是同一个人，也就是“机器人”。所以卡池可以帮你做短信验证码，以便账号登录用。MIFI+SIM 帮你做手机流量上网用。这是两套不同的设备。

答疑 4：听说企业里用 SQL 和 Excel 进行数据分析的很多，这块该如何选择？

SQL 和 Excel 做统计的工作多一些，涉及到编程的很少。如果你想在这个行业进一步提升，或者做一名算法工程师，那么你都要和 Python 打交道。专栏里数据挖掘算法的部分，是用 Python 交付的。Excel 和 SQL 很难做数据挖掘。

如果想对数据概况有个了解，做一些基础分析，用 Excel 和 SQL 是 OK 的。但是想进一步挖掘数据的价值，掌握 Python 还是挺有必要的。

另外，如果你做的是数据可视化工作，在企业里会用到 tableau 或者 powerBI 这些工具。数据采集你也可以选择第三方工具，或者自己用 Python 来编写。

答疑 5：学一些算法的时候比如 SVM，是不是掌握它们的理论内容即可。不需要自己去实现，用的时候调用库即可？

是的，这些算法都有封装，直接使用即可。在 python 的 sklearn 中就是一行语句的事。

答疑 6：老师，我现在等于从零开始学数据挖掘，所谓的数学基础指的是把高数学到哪种境界啊？是像考研那样不管极限导数积分每种题型都要会解，还是只需要了解这些必备的高数基础的概念？

不需要求解每一道数学题，只需要具备高数基础概念即可！概率论与数理统计、线性代数、最优化方法和图论这些，我在算法中涉及的地方都会讲到，你暂时不用提前学习这些数学知识。我觉得最好的方式就是在案例中灵活运用，这样可以加深你对这些数学知识的理解。

对于大部分从 0 开始学数据挖掘的人来说，可以淡化公式，重点理解使用场景和概念。

爬虫相关问题

答疑 1：关于 Python 爬虫工具的推荐

我除了在专栏里讲到了Requests、XPath 解析，以及 Selenium、PhantomJS。还有一些工具是值得推荐的。

Scrapy 是一个 Python 的爬虫框架，它依赖的工具比较多，所以在 pip install 的时候，会安装多个工具包。scrapy 本身包括了爬取、处理、存储等工具。在 scrapy 中，有一些组件是提供给你的，需要你针对具体任务进行编写。比如在 item.py 对抓取的内容进行定义，在 spider.py 中编写爬虫，在 pipeline.py 中对抓取的内容进行存储，可以保存为 csv 等格式。这里不具体讲解 scrapy 的使用。

另外，Puppeteer 是个很好的选择，可以控制 Headless Chrome，这样就不用 Selenium 和 PhantomJS。与 Selenium 相比，Puppeteer 直接调用 Chrome 的 API 接口，不需要打开浏览器，直接在 V8 引擎中处理，同时这个组件是由 Google 的 Chrome 团队维护的，所以兼容性会很好。

答疑 2：driver = webdriver.Chrome()，为什么输入这个代码就会报错了呢？

报错的原因是没有下载或正确配置 ChromeDriver 路径，正确的方法如下：

1. 下载 ChromeDriver，并放到 Chrome 浏览器目录中；

下载地址：http://npm.taobao.org/mirrors/chromedriver/72.0.3626.7/

2. 将 Chrome 浏览器目录添加到系统的环境变量 Path 中，然后再运行下试试.

另外你也可以在代码中设置 ChromeDriver 的路径，方法如下：

chrome_driver = "C:\Users\cheny\AppData\Local\Google\Chrome\Application\chromedriver.exe"

driver = webdriver.Chrome(executable_path=chrome_driver)

答疑 3：如果是需要用户登陆后才能爬取的数据该怎么用 python 来实现呢？

你可以使用 Python+Selenium 的方式完成账户的自动登录，因为 Selenium 是个自动化测试的框架，使用 Selenium 的 webdriver 就可以模拟浏览器的行为。找到输入用户名密码的地方，输入相应的值，然后模拟点击即可完成登录（没有验证码的情况下）。

另外你也可以使用 cookie 来登录网站，方法是你登录网站时，先保存网站的 cookie，然后在下次访问的时候，加载之前保存的 cookie，放到 request headers 中，这样就不需要再登录网站了。

答疑 4：为什么我在豆瓣网查询图片的网址与你不一样？https://www.douban.com/search?cat=1025&q=王祖贤 &source=suggest 。

咱们访问豆瓣查询图片的网址应该是一样的。只是我给出的是 json 的链接。

方法是这样的：用 Chrome 浏览器的开发者工具，可以监测出来网页中是否有 json 数据的传输，所以我给出的链接是 json 数据传输的链接： https://www.douban.com/j/search_photo?q=%E7%8E%8B%E7%A5%96%E8%B4%A4&limit=20&start=0

答疑 5：XHR 数据这个是如何查出来的，我使用 chrome 的开发者工具查看 XHR 数据，但是查不到这部分，麻烦老师帮忙解答。

你需要使用浏览器的插件查看 XHR 数据，比如在 Chrome 的开发者工具。

在豆瓣搜索中，我们对“王祖贤”进行了模拟，发现 XHR 数据中有一个请求是这样的：

https://www.douban.com/j/search_photo?q=王祖贤 &limit=20&start=0

你可以看一下操作流程。

数据变换相关

答疑 1：数据规范化、归一化、标准化是同一个概念么？

数据规范化是更大的概念，它指的是将不同渠道的数据，都按照同一种尺度来进行度量，这样做有两个好处，一是让数据之间具有可比较性；另一个好处就是方便后续运算，因为数据在同一个数量级上规整了，在机器学习迭代的时候，也会加快收敛效率。

数据归一化和数据标准化都是数据规范化的方式。不同点在于数据归一化会让数据在一个 [0,1] 或者 [-1,1] 的区间范围内。而数据标准化会让规范化的数据呈现正态分布的情况，所以你可以这么记：归一化的“一”，是让数据在 [0,1] 的范围内。而标准化，目标是让数据呈现标准的正态分布。

答疑 2：什么时候会用到数据规范化（Min-max、Z-Score 和小数定标）？

刚才提到了，进行数据规范化有两个作用：一是让数据之间具有可比较性，二是加快后续算法的迭代收敛速度。

实际上你能看到 Min-max、Z-Score 和小数定标规范化都是一种线性映射的关系，将原来的数值投射到新的空间中。这样变换的好处就是可以看到在特定空间内的数值分布情况，比如通过 Min-max 可以看到数据在 [0,1] 之间的分布情况，Z-Score 可以看到数值的正态分布情况等。

不论是采用哪种数据规范化方法，规范化后的数值都会在同一个数量的级别上，这样方便后续进行运算。

那么回过头来看，在数据挖掘算法中，是否都需要进行数据规范化呢？一般情况下是需要的，尤其是针对距离相关的运算，比如在 K-Means、KNN 以及聚类算法中，我们需要有对距离的定义，所以在做这些算法前，需要对数据进行规范化。

另外还有一些算法用到了梯度下降作为优化器，这是为了提高迭代收敛的效率，也就是提升找到目标函数最优解的效率。我们也需要进行数据规范化，比如逻辑回归、SVM 和神经网络算法。

在这些算法中都有目标函数，需要对目标函数进行求解。梯度下降的目标是寻找到目标函数的最优解，而梯度的方法则指明了最优解的方向，如下图所示。

当然不是所有的算法都需要进行数据规范化。在构造决策树的时候，可以不用提前做数据规范化，因为我们不需要关心特征值的大小维度，也没有使用到梯度下降来做优化，所以数据规范化对决策树的构造结果和构造效率影响不大。除此之外，还是建议你在做数据挖掘算法前进行数据规范化。

答疑 3：如何使用 Z-Score 规范化，将分数变成正态分布？

我在专栏文稿中举了一个 Z-Score 分数规范化的例子，假设 A 与 B 的考试成绩都为 80 分，A 的考卷满分是 100 分（及格 60 分），B 的考卷满分是 500 分（及格 300 分）。这里假设 A 和 B 的考试成绩都是成正态分布，可以直接采用 Z-Score 的线性化规范化方法。

在专栏的讨论区中，有个同学提出了“Z-Score”的非线性计算方式，大家可以一起了解下：

先按公式计算出百分等级。百分等级（年级）=100-(100x 年级名次 -50)/ 有效参加考试人数。这里百分等级是每个学生在该批学生中的相对位置，其中百分等级是按照正态分布图的所占面积比例求得的；

按照百分等级数去标准正态分布表中查询得出 Z-Score 值，这样最终得出的 Z 分便是标准的正态分布，能够将偏态转化成标准正态。

因为在很多情况下，数值如果不是正态分布，而是偏态分布，直接使用 Z-Score 的线性计算方式无法将分数转化成正态分布。采用以上的方法可以解决这一个问题，大家可以了解下。这里偏态分布指的是非对称分布的偏斜状态，包括了负偏态，也就是左偏态分布，以及正偏态，也就是右偏态分布。

我发现大家对工具的使用和场景比较感兴趣，所以最后留两道思考题。

第一道题：假设矩阵 a = np.array([[4,3,2],[2,4,1]])，请你编写代码将矩阵中的每一列按照从小到大的方式进行排序。

第二道题：你都用过哪些 Python 爬虫工具，抓取过哪些数据，觉得哪个工具好用？

欢迎你在评论分享你的想法，也欢迎你点击“请朋友读”，把它分享给你的朋友或者同事。

17 丨决策树（上）：要不要去打篮球？决策树来告诉你

想象一下一个女孩的妈妈给她介绍男朋友的场景：

女儿：长的帅不帅？

妈妈：挺帅的。

女儿：有没有房子？

妈妈：在老家有一个。

女儿：收入高不高？

妈妈：还不错，年薪百万。

女儿：做什么工作的？

妈妈：IT 男，互联网公司做数据挖掘的。

女儿：好，那我见见。

在现实生活中，我们会遇到各种选择，不论是选择男女朋友，还是挑选水果，都是基于以往的经验来做判断。如果把判断背后的逻辑整理成一个结构图，你会发现它实际上是一个树状图，这就是我们今天要讲的决策树。

决策树的工作原理

决策树基本上就是把我们以前的经验总结出来。我给你准备了一个打篮球的训练集。如果我们要出门打篮球，一般会根据“天气”、“温度”、“湿度”、“刮风”这几个条件来判断，最后得到结果：去打篮球？还是不去？

上面这个图就是一棵典型的决策树。我们在做决策树的时候，会经历两个阶段：构造和剪枝。

构造

什么是构造呢？构造就是生成一棵完整的决策树。简单来说，构造的过程就是选择什么属性作为节点的过程，那么在构造过程中，会存在三种节点：

根节点：就是树的最顶端，最开始的那个节点。在上图中，“天气”就是一个根节点；

内部节点：就是树中间的那些节点，比如说“温度”、“湿度”、“刮风”；

叶节点：就是树最底部的节点，也就是决策结果。

节点之间存在父子关系。比如根节点会有子节点，子节点会有子子节点，但是到了叶节点就停止了，叶节点不存在子节点。那么在构造过程中，你要解决三个重要的问题：

选择哪个属性作为根节点；

选择哪些属性作为子节点；

什么时候停止并得到目标状态，即叶节点。

剪枝

决策树构造出来之后是不是就万事大吉了呢？也不尽然，我们可能还需要对决策树进行剪枝。剪枝就是给决策树瘦身，这一步想实现的目标就是，不需要太多的判断，同样可以得到不错的结果。之所以这么做，是为了防止“过拟合”（Overfitting）现象的发生。

“过拟合”这个概念你一定要理解，它指的就是模型的训练结果“太好了”，以至于在实际应用的过程中，会存在“死板”的情况，导致分类错误。

欠拟合，和过拟合就好比是下面这张图中的第一个和第三个情况一样，训练的结果“太好“，反而在实际应用过程中会导致分类错误。

造成过拟合的原因之一就是因为训练集中样本量较小。如果决策树选择的属性过多，构造出来的决策树一定能够“完美”地把训练集中的样本分类，但是这样就会把训练集中一些数据的特点当成所有数据的特点，但这个特点不一定是全部数据的特点，这就使得这个决策树在真实的数据分类中出现错误，也就是模型的“泛化能力”差。

泛化能力指的分类器是通过训练集抽象出来的分类能力，你也可以理解是举一反三的能力。如果我们太依赖于训练集的数据，那么得到的决策树容错率就会比较低，泛化能力差。因为训练集只是全部数据的抽样，并不能体现全部数据的特点。

既然要对决策树进行剪枝，具体有哪些方法呢？一般来说，剪枝可以分为“预剪枝”（Pre-Pruning）和“后剪枝”（Post-Pruning）。

预剪枝是在决策树构造时就进行剪枝。方法是在构造的过程中对节点进行评估，如果对某个节点进行划分，在验证集中不能带来准确性的提升，那么对这个节点进行划分就没有意义，这时就会把当前节点作为叶节点，不对其进行划分。

后剪枝就是在生成决策树之后再进行剪枝，通常会从决策树的叶节点开始，逐层向上对每个节点进行评估。如果剪掉这个节点子树，与保留该节点子树在分类准确性上差别不大，或者剪掉该节点子树，能在验证集中带来准确性的提升，那么就可以把该节点子树进行剪枝。方法是：用这个节点子树的叶子节点来替代该节点，类标记为这个节点子树中最频繁的那个类。

如何判断要不要去打篮球？

我给你准备了打篮球的数据集，训练数据如下：

我们该如何构造一个判断是否去打篮球的决策树呢？再回顾一下决策树的构造原理，在决策过程中有三个重要的问题：将哪个属性作为根节点？选择哪些属性作为后继节点？什么时候停止并得到目标值？

显然将哪个属性（天气、温度、湿度、刮风）作为根节点是个关键问题，在这里我们先介绍两个指标：纯度和信息熵。

先来说一下纯度。你可以把决策树的构造过程理解成为寻找纯净划分的过程。数学上，我们可以用纯度来表示，纯度换一种方式来解释就是让目标变量的分歧最小。

我在这里举个例子，假设有 3 个集合：

集合 1：6 次都去打篮球；

集合 2：4 次去打篮球，2 次不去打篮球；

集合 3：3 次去打篮球，3 次不去打篮球。

按照纯度指标来说，集合 1> 集合 2> 集合 3。因为集合 1 的分歧最小，集合 3 的分歧最大。

然后我们再来介绍信息熵（entropy）的概念，它表示了信息的不确定度。

在信息论中，随机离散事件出现的概率存在着不确定性。为了衡量这种信息的不确定性，信息学之父香农引入了信息熵的概念，并给出了计算信息熵的数学公式：

p(i|t) 代表了节点 t 为分类 i 的概率，其中 log2 为取以 2 为底的对数。这里我们不是来介绍公式的，而是说存在一种度量，它能帮我们反映出来这个信息的不确定度。当不确定性越大时，它所包含的信息量也就越大，信息熵也就越高。

我举个简单的例子，假设有 2 个集合

集合 1：5 次去打篮球，1 次不去打篮球；

集合 2：3 次去打篮球，3 次不去打篮球。

在集合 1 中，有 6 次决策，其中打篮球是 5 次，不打篮球是 1 次。那么假设：类别 1 为“打篮球”，即次数为 5；类别 2 为“不打篮球”，即次数为 1。那么节点划分为类别 1 的概率是 5/6，为类别 2 的概率是 1/6，带入上述信息熵公式可以计算得出：

同样，集合 2 中，也是一共 6 次决策，其中类别 1 中“打篮球”的次数是 3，类别 2“不打篮球”的次数也是 3，那么信息熵为多少呢？我们可以计算得出：

从上面的计算结果中可以看出，信息熵越大，纯度越低。当集合中的所有样本均匀混合时，信息熵最大，纯度最低。

我们在构造决策树的时候，会基于纯度来构建。而经典的 “不纯度”的指标有三种，分别是信息增益（ID3 算法）、信息增益率（C4.5 算法）以及基尼指数（Cart 算法）。

我们先看下 ID3 算法。ID3 算法计算的是信息增益，信息增益指的就是划分可以带来纯度的提高，信息熵的下降。它的计算公式，是父亲节点的信息熵减去所有子节点的信息熵。在计算的过程中，我们会计算每个子节点的归一化信息熵，即按照每个子节点在父节点中出现的概率，来计算这些子节点的信息熵。所以信息增益的公式可以表示为：

公式中 D 是父亲节点，Di 是子节点，Gain(D,a) 中的 a 作为 D 节点的属性选择。

假设天气 = 晴的时候，会有 5 次去打篮球，5 次不打篮球。其中 D1 刮风 = 是，有 2 次打篮球，1 次不打篮球。D2 刮风 = 否，有 3 次打篮球，4 次不打篮球。那么 a 代表节点的属性，即天气 = 晴。

你可以在下面的图例中直观地了解这几个概念。

比如针对图上这个例子，D 作为节点的信息增益为：

也就是 D 节点的信息熵 -2 个子节点的归一化信息熵。2 个子节点归一化信息熵 =3/10 的 D1 信息熵 +7/10 的 D2 信息熵。

我们基于 ID3 的算法规则，完整地计算下我们的训练集，训练集中一共有 7 条数据，3 个打篮球，4 个不打篮球，所以根节点的信息熵是：

如果你将天气作为属性的划分，会有三个叶子节点 D1、D2 和 D3，分别对应的是晴天、阴天和小雨。我们用 + 代表去打篮球，- 代表不去打篮球。那么第一条记录，晴天不去打篮球，可以记为 1-，于是我们可以用下面的方式来记录 D1，D2，D3：

D1(天气 = 晴天)={1-,2-,6+}

D2(天气 = 阴天)={3+,7-}

D3(天气 = 小雨)={4+,5-}

我们先分别计算三个叶子节点的信息熵：

因为 D1 有 3 个记录，D2 有 2 个记录，D3 有 2 个记录，所以 D 中的记录一共是 3+2+2=7，即总数为 7。所以 D1 在 D（父节点）中的概率是 3/7，D2 在父节点的概率是 2/7，D3 在父节点的概率是 2/7。那么作为子节点的归一化信息熵 = 3/7*0.918+2/7*1.0+2/7*1.0=0.965。

因为我们用 ID3 中的信息增益来构造决策树，所以要计算每个节点的信息增益。

天气作为属性节点的信息增益为，Gain(D , 天气)=0.985-0.965=0.020。。

同理我们可以计算出其他属性作为根节点的信息增益，它们分别为 ：

Gain(D , 温度)=0.128
Gain(D , 湿度)=0.020
Gain(D , 刮风)=0.020

我们能看出来温度作为属性的信息增益最大。因为 ID3 就是要将信息增益最大的节点作为父节点，这样可以得到纯度高的决策树，所以我们将温度作为根节点。其决策树状图分裂为下图所示：

然后我们要将上图中第一个叶节点，也就是 D1={1-,2-,3+,4+}进一步进行分裂，往下划分，计算其不同属性（天气、湿度、刮风）作为节点的信息增益，可以得到：

Gain(D , 天气)=0

Gain(D , 湿度)=0

Gain(D , 刮风)=0.0615

我们能看到刮风为 D1 的节点都可以得到最大的信息增益，这里我们选取刮风作为节点。同理，我们可以按照上面的计算步骤得到完整的决策树，结果如下：

于是我们通过 ID3 算法得到了一棵决策树。ID3 的算法规则相对简单，可解释性强。同样也存在缺陷，比如我们会发现 ID3 算法倾向于选择取值比较多的属性。这样，如果我们把“编号”作为一个属性（一般情况下不会这么做，这里只是举个例子），那么“编号”将会被选为最优属性 。但实际上“编号”是无关属性的，它对“打篮球”的分类并没有太大作用。

所以 ID3 有一个缺陷就是，有些属性可能对分类任务没有太大作用，但是他们仍然可能会被选为最优属性。这种缺陷不是每次都会发生，只是存在一定的概率。在大部分情况下，ID3 都能生成不错的决策树分类。针对可能发生的缺陷，后人提出了新的算法进行改进。

在 ID3 算法上进行改进的 C4.5 算法

那么 C4.5 都在哪些方面改进了 ID3 呢？

1. 采用信息增益率

因为 ID3 在计算的时候，倾向于选择取值多的属性。为了避免这个问题，C4.5 采用信息增益率的方式来选择属性。信息增益率 = 信息增益 / 属性熵，具体的计算公式这里省略。

当属性有很多值的时候，相当于被划分成了许多份，虽然信息增益变大了，但是对于 C4.5 来说，属性熵也会变大，所以整体的信息增益率并不大。

2. 采用悲观剪枝

ID3 构造决策树的时候，容易产生过拟合的情况。在 C4.5 中，会在决策树构造之后采用悲观剪枝（PEP），这样可以提升决策树的泛化能力。

悲观剪枝是后剪枝技术中的一种，通过递归估算每个内部节点的分类错误率，比较剪枝前后这个节点的分类错误率来决定是否对其进行剪枝。这种剪枝方法不再需要一个单独的测试数据集。

3. 离散化处理连续属性

C4.5 可以处理连续属性的情况，对连续的属性进行离散化的处理。比如打篮球存在的“湿度”属性，不按照“高、中”划分，而是按照湿度值进行计算，那么湿度取什么值都有可能。该怎么选择这个阈值呢，C4.5 选择具有最高信息增益的划分所对应的阈值。

4. 处理缺失值

针对数据集不完整的情况，C4.5 也可以进行处理。

假如我们得到的是如下的数据，你会发现这个数据中存在两点问题。第一个问题是，数据集中存在数值缺失的情况，如何进行属性选择？第二个问题是，假设已经做了属性划分，但是样本在这个属性上有缺失值，该如何对样本进行划分？

我们不考虑缺失的数值，可以得到温度 D={2-,3+,4+,5-,6+,7-}。温度 = 高：D1={2-,3+,4+} ；温度 = 中：D2={6+,7-}；温度 = 低：D3={5-} 。这里 + 号代表打篮球，- 号代表不打篮球。比如 ID=2 时，决策是不打篮球，我们可以记录为 2-。

所以三个叶节点的信息熵可以结算为：

这三个节点的归一化信息熵为 3/6*0.918+2/6*1.0+1/6*0=0.792。

针对将属性选择为温度的信息增益率为：

Gain(D′, 温度)=Ent(D′)-0.792=1.0-0.792=0.208

D′的样本个数为 6，而 D 的样本个数为 7，所以所占权重比例为 6/7，所以 Gain(D′，温度) 所占权重比例为 6/7，所以：

Gain(D, 温度)=6/7*0.208=0.178

这样即使在温度属性的数值有缺失的情况下，我们依然可以计算信息增益，并对属性进行选择。

Cart 算法在这里不做介绍，我会在下一讲给你讲解这个算法。现在我们总结下 ID3 和 C4.5 算法。首先 ID3 算法的优点是方法简单，缺点是对噪声敏感。训练数据如果有少量错误，可能会产生决策树分类错误。C4.5 在 ID3 的基础上，用信息增益率代替了信息增益，解决了噪声敏感的问题，并且可以对构造树进行剪枝、处理连续数值以及数值缺失等情况，但是由于 C4.5 需要对数据集进行多次扫描，算法效率相对较低。

总结

前面我们讲了两种决策树分类算法 ID3 和 C4.5，了解了它们的数学原理。你可能会问，公式这么多，在实际使用中该怎么办呢？实际上，我们可以使用一些数据挖掘工具使用它们，比如 Python 的 sklearn，或者是 Weka（一个免费的数据挖掘工作平台），它们已经集成了这两种算法。只是我们在了解了这两种算法之后，才能更加清楚这两种算法的优缺点。

我们总结下，这次都讲到了哪些知识点呢？

首先我们采用决策树分类，需要了解它的原理，包括它的构造原理、剪枝原理。另外在信息度量上，我们需要了解信息度量中的纯度和信息熵的概念。在决策树的构造中，一个决策树包括根节点、子节点、叶子节点。在属性选择的标准上，度量方法包括了信息增益和信息增益率。在算法上，我讲解了两种算法：ID3 和 C4.5，其中 ID3 是基础的决策树算法，C4.5 在它的基础上进行了改进，也是目前决策树中应用广泛的算法。然后在了解这些概念和原理后，强烈推荐你使用工具，具体工具的使用我会在后面进行介绍。

最后我们留一道思考题吧。请你用下面的例子来模拟下决策树的流程，假设好苹果的数据如下，请用 ID3 算法来给出好苹果的决策树。

如果你觉得这篇文章有所价值，欢迎点击“请朋友读”，把它分享给你的朋友或者同事。

18丨决策树（中）：CART，一棵是回归树，另一棵是分类树

上节课我们讲了决策树，基于信息度量的不同方式，我们可以把决策树分为 ID3 算法、C4.5 算法和 CART 算法。今天我来带你学习 CART 算法。CART 算法，英文全称叫做 Classification And Regression Tree，中文叫做分类回归树。ID3 和 C4.5 算法可以生成二叉树或多叉树，而 CART 只支持二叉树。同时 CART 决策树比较特殊，既可以作分类树，又可以作回归树。

那么你首先需要了解的是，什么是分类树，什么是回归树呢？

我用下面的训练数据举个例子，你能看到不同职业的人，他们的年龄不同，学习时间也不同。如果我构造了一棵决策树，想要基于数据判断这个人的职业身份，这个就属于分类树，因为是从几个分类中来做选择。如果是给定了数据，想要预测这个人的年龄，那就属于回归树。

分类树可以处理离散数据，也就是数据种类有限的数据，它输出的是样本的类别，而回归树可以对连续型的数值进行预测，也就是数据在某个区间内都有取值的可能，它输出的是一个数值。

CART 分类树的工作流程

通过上一讲，我们知道决策树的核心就是寻找纯净的划分，因此引入了纯度的概念。在属性选择上，我们是通过统计“不纯度”来做判断的，ID3 是基于信息增益做判断，C4.5 在 ID3 的基础上做了改进，提出了信息增益率的概念。实际上 CART 分类树与 C4.5 算法类似，只是属性选择的指标采用的是基尼系数。

你可能在经济学中听过说基尼系数，它是用来衡量一个国家收入差距的常用指标。当基尼系数大于 0.4 的时候，说明财富差异悬殊。基尼系数在 0.2-0.4 之间说明分配合理，财富差距不大。

基尼系数本身反应了样本的不确定度。当基尼系数越小的时候，说明样本之间的差异性小，不确定程度低。分类的过程本身是一个不确定度降低的过程，即纯度的提升过程。所以 CART 算法在构造分类树的时候，会选择基尼系数最小的属性作为属性的划分。

我们接下来详解了解一下基尼系数。基尼系数不好懂，你最好跟着例子一起手动计算下。

假设 t 为节点，那么该节点的 GINI 系数的计算公式为：

这里 p(Ck|t) 表示节点 t 属于类别 Ck 的概率，节点 t 的基尼系数为 1 减去各类别 Ck 概率平方和。

通过下面这个例子，我们计算一下两个集合的基尼系数分别为多少：

集合 1：6 个都去打篮球；

集合 2：3 个去打篮球，3 个不去打篮球。

针对集合 1，所有人都去打篮球，所以 p(Ck|t)=1，因此 GINI(t)=1-1=0。

针对集合 2，有一半人去打篮球，而另一半不去打篮球，所以，p(C1|t)=0.5，p(C2|t)=0.5，GINI(t)=1-（0.5*0.5+0.5*0.5）=0.5。

通过两个基尼系数你可以看出，集合 1 的基尼系数最小，也证明样本最稳定，而集合 2 的样本不稳定性更大。

在 CART 算法中，基于基尼系数对特征属性进行二元分裂，假设属性 A 将节点 D 划分成了 D1 和 D2，如下图所示：

节点 D 的基尼系数等于子节点 D1 和 D2 的归一化基尼系数之和，用公式表示为：

归一化基尼系数代表的是每个子节点的基尼系数乘以该节点占整体父亲节点 D 中的比例。

上面我们已经计算了集合 D1 和集合 D2 的 GINI 系数，得到：

所以节点 D 的基尼系数为：

节点 D 被属性 A 划分后的基尼系数越大，样本集合的不确定性越大，也就是不纯度越高。

如何使用 CART 算法来创建分类树

通过上面的讲解你可以知道，CART 分类树实际上是基于基尼系数来做属性划分的。在 Python 的 sklearn 中，如果我们想要创建 CART 分类树，可以直接使用 DecisionTreeClassifier 这个类。创建这个类的时候，默认情况下 criterion 这个参数等于 gini，也就是按照基尼系数来选择属性划分，即默认采用的是 CART 分类树。

下面，我们来用 CART 分类树，给 iris 数据集构造一棵分类决策树。iris 这个数据集，我在 Python 可视化中讲到过，实际上在 sklearn 中也自带了这个数据集。基于 iris 数据集，构造 CART 分类树的代码如下：

# encoding=utf-8

from sklearn.model_selection import train_test_split

from sklearn.metrics import accuracy_score

from sklearn.tree import DecisionTreeClassifier

from sklearn.datasets import load_iris

# 准备数据集

iris=load_iris()

# 获取特征集和分类标识

features = iris.data

labels = iris.target

# 随机抽取 33% 的数据作为测试集，其余为训练集

train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.33, random_state=0)

# 创建 CART 分类树

clf = DecisionTreeClassifier(criterion='gini')

# 拟合构造 CART 分类树

clf = clf.fit(train_features, train_labels)

# 用 CART 分类树做预测

test_predict = clf.predict(test_features)

# 预测结果与测试集结果作比对

score = accuracy_score(test_labels, test_predict)

print("CART 分类树准确率 %.4lf" % score)

运行结果：

CART 分类树准确率 0.9600

如果我们把决策树画出来，可以得到下面的图示：

首先 train_test_split 可以帮助我们把数据集抽取一部分作为测试集，这样我们就可以得到训练集和测试集。

使用 clf = DecisionTreeClassifier(criterion=‘gini’) 初始化一棵 CART 分类树。这样你就可以对 CART 分类树进行训练。

使用 clf.fit(train_features, train_labels) 函数，将训练集的特征值和分类标识作为参数进行拟合，得到 CART 分类树。

使用 clf.predict(test_features) 函数进行预测，传入测试集的特征值，可以得到测试结果 test_predict。

最后使用 accuracy_score(test_labels, test_predict) 函数，传入测试集的预测结果与实际的结果作为参数，得到准确率 score。

我们能看到 sklearn 帮我们做了 CART 分类树的使用封装，使用起来还是很方便的。

CART 回归树的工作流程

CART 回归树划分数据集的过程和分类树的过程是一样的，只是回归树得到的预测结果是连续值，而且评判“不纯度”的指标不同。在 CART 分类树中采用的是基尼系数作为标准，那么在 CART 回归树中，如何评价“不纯度”呢？实际上我们要根据样本的混乱程度，也就是样本的离散程度来评价“不纯度”。

样本的离散程度具体的计算方式是，先计算所有样本的均值，然后计算每个样本值到均值的差值。我们假设 x 为样本的个体，均值为 u。为了统计样本的离散程度，我们可以取差值的绝对值，或者方差。

其中差值的绝对值为样本值减去样本均值的绝对值：

方差为每个样本值减去样本均值的平方和除以样本个数：

所以这两种节点划分的标准，分别对应着两种目标函数最优化的标准，即用最小绝对偏差（LAD），或者使用最小二乘偏差（LSD）。这两种方式都可以让我们找到节点划分的方法，通常使用最小二乘偏差的情况更常见一些。

我们可以通过一个例子来看下如何创建一棵 CART 回归树来做预测。

如何使用 CART 回归树做预测

这里我们使用到 sklearn 自带的波士顿房价数据集，该数据集给出了影响房价的一些指标，比如犯罪率，房产税等，最后给出了房价。

根据这些指标，我们使用 CART 回归树对波士顿房价进行预测，代码如下：

# encoding=utf-8

from sklearn.metrics import mean_squared_error

from sklearn.model_selection import train_test_split

from sklearn.datasets import load_boston

from sklearn.metrics import r2_score,mean_absolute_error,mean_squared_error

from sklearn.tree import DecisionTreeRegressor

# 准备数据集

boston=load_boston()

# 探索数据

print(boston.feature_names)

# 获取特征集和房价

features = boston.data

prices = boston.target

# 随机抽取 33% 的数据作为测试集，其余为训练集

train_features, test_features, train_price, test_price = train_test_split(features, prices, test_size=0.33)

# 创建 CART 回归树

dtr=DecisionTreeRegressor()

# 拟合构造 CART 回归树

dtr.fit(train_features, train_price)

# 预测测试集中的房价

predict_price = dtr.predict(test_features)

# 测试集的结果评价

print('回归树二乘偏差均值:', mean_squared_error(test_price, predict_price))

print('回归树绝对值偏差均值:', mean_absolute_error(test_price, predict_price))

运行结果（每次运行结果可能会有不同）：

['CRIM' 'ZN' 'INDUS' 'CHAS' 'NOX' 'RM' 'AGE' 'DIS' 'RAD' 'TAX' 'PTRATIO' 'B' 'LSTAT']

回归树二乘偏差均值: 23.80784431137724

回归树绝对值偏差均值: 3.040119760479042

如果把回归树画出来，可以得到下面的图示（波士顿房价数据集的指标有些多，所以树比较大）：

你可以在这里下载完整 PDF 文件。

我们来看下这个例子，首先加载了波士顿房价数据集，得到特征集和房价。然后通过 train_test_split 帮助我们把数据集抽取一部分作为测试集，其余作为训练集。

使用 dtr=DecisionTreeRegressor() 初始化一棵 CART 回归树。

使用 dtr.fit(train_features, train_price) 函数，将训练集的特征值和结果作为参数进行拟合，得到 CART 回归树。

使用 dtr.predict(test_features) 函数进行预测，传入测试集的特征值，可以得到预测结果 predict_price。

最后我们可以求得这棵回归树的二乘偏差均值，以及绝对值偏差均值。

我们能看到 CART 回归树的使用和分类树类似，只是最后求得的预测值是个连续值。

CART 决策树的剪枝

CART 决策树的剪枝主要采用的是 CCP 方法，它是一种后剪枝的方法，英文全称叫做 cost-complexity prune，中文叫做代价复杂度。这种剪枝方式用到一个指标叫做节点的表面误差率增益值，以此作为剪枝前后误差的定义。用公式表示则是：

其中 Tt 代表以 t 为根节点的子树，C(Tt) 表示节点 t 的子树没被裁剪时子树 Tt 的误差，C(t) 表示节点 t 的子树被剪枝后节点 t 的误差，|Tt|代子树 Tt 的叶子数，剪枝后，T 的叶子数减少了|Tt|-1。

所以节点的表面误差率增益值等于节点 t 的子树被剪枝后的误差变化除以剪掉的叶子数量。

因为我们希望剪枝前后误差最小，所以我们要寻找的就是最小α值对应的节点，把它剪掉。这时候生成了第一个子树。重复上面的过程，继续剪枝，直到最后只剩下根节点，即为最后一个子树。

得到了剪枝后的子树集合后，我们需要用验证集对所有子树的误差计算一遍。可以通过计算每个子树的基尼指数或者平方误差，取误差最小的那个树，得到我们想要的结果。

总结

今天我给你讲了 CART 决策树，它是一棵决策二叉树，既可以做分类树，也可以做回归树。你需要记住的是，作为分类树，CART 采用基尼系数作为节点划分的依据，得到的是离散的结果，也就是分类结果；作为回归树，CART 可以采用最小绝对偏差（LAD），或者最小二乘偏差（LSD）作为节点划分的依据，得到的是连续值，即回归预测结果。

最后我们来整理下三种决策树之间在属性选择标准上的差异：

ID3 算法，基于信息增益做判断；

C4.5 算法，基于信息增益率做判断；

CART 算法，分类树是基于基尼系数做判断。回归树是基于偏差做判断。

实际上这三个指标也是计算“不纯度”的三种计算方式。

在工具使用上，我们可以使用 sklearn 中的 DecisionTreeClassifier 创建 CART 分类树，通过 DecisionTreeRegressor 创建 CART 回归树。

你可以用代码自己跑一遍我在文稿中举到的例子。

最后给你留两道思考题吧，你能说下 ID3，C4.5，以及 CART 分类树在做节点划分时的区别吗？第二个问题是，sklearn 中有个手写数字数据集，调用的方法是 load_digits()，你能否创建一个 CART 分类树，对手写数字数据集做分类？另外选取一部分测试集，统计下分类树的准确率？

欢迎你在评论下面留言，与我分享你的答案。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起交流。

19丨决策树（下）：泰坦尼克乘客生存预测

在前面的两篇文章中，我给你讲了决策树算法。决策树算法是经常使用的数据挖掘算法，这是因为决策树就像一个人脑中的决策模型一样，呈现出来非常直观。基于决策树还诞生了很多数据挖掘算法，比如随机森林（Random forest）。

今天我来带你用决策树进行项目的实战。

决策树分类的应用场景非常广泛，在各行各业都有应用，比如在金融行业可以用决策树做贷款风险评估，医疗行业可以用决策树生成辅助诊断，电商行业可以用决策树对销售额进行预测等。

在了解决策树的原理后，今天我们用 sklearn 工具解决一个实际的问题：泰坦尼克号乘客的生存预测。

sklearn 中的决策树模型

首先，我们需要掌握 sklearn 中自带的决策树分类器 DecisionTreeClassifier，方法如下：

clf = DecisionTreeClassifier(criterion='entropy')

到目前为止，sklearn 中只实现了 ID3 与 CART 决策树，所以我们暂时只能使用这两种决策树，在构造 DecisionTreeClassifier 类时，其中有一个参数是 criterion，意为标准。它决定了构造的分类树是采用 ID3 分类树，还是 CART 分类树，对应的取值分别是 entropy 或者 gini：

entropy: 基于信息熵，也就是 ID3 算法，实际结果与 C4.5 相差不大；

gini：默认参数，基于基尼系数。CART 算法是基于基尼系数做属性划分的，所以 criterion=gini 时，实际上执行的是 CART 算法。

我们通过设置 criterion='entropy’可以创建一个 ID3 决策树分类器，然后打印下 clf，看下决策树在 sklearn 中是个什么东西？

DecisionTreeClassifier(class_weight=None, criterion='entropy', max_depth=None,

            max_features=None, max_leaf_nodes=None,

            min_impurity_decrease=0.0, min_impurity_split=None,

            min_samples_leaf=1, min_samples_split=2,

            min_weight_fraction_leaf=0.0, presort=False, random_state=None,

            splitter='best')

这里我们看到了很多参数，除了设置 criterion 采用不同的决策树算法外，一般建议使用默认的参数，默认参数不会限制决策树的最大深度，不限制叶子节点数，认为所有分类的权重都相等等。当然你也可以调整这些参数，来创建不同的决策树模型。

我整理了这些参数代表的含义：

在构造决策树分类器后，我们可以使用 fit 方法让分类器进行拟合，使用 predict 方法对新数据进行预测，得到预测的分类结果，也可以使用 score 方法得到分类器的准确率。

下面这个表格是 fit 方法、predict 方法和 score 方法的作用。

Titanic 乘客生存预测

问题描述

泰坦尼克海难是著名的十大灾难之一，究竟多少人遇难，各方统计的结果不一。现在我们可以得到部分的数据，具体数据你可以从 GitHub 上下载：https://github.com/cystanford/Titanic_Data

其中数据集格式为 csv，一共有两个文件：

train.csv 是训练数据集，包含特征信息和存活与否的标签；

test.csv: 测试数据集，只包含特征信息。

现在我们需要用决策树分类对训练集进行训练，针对测试集中的乘客进行生存预测，并告知分类器的准确率。

在训练集中，包括了以下字段，它们具体为：

生存预测的关键流程

我们要对训练集中乘客的生存进行预测，这个过程可以划分为两个重要的阶段：

准备阶段：我们首先需要对训练集、测试集的数据进行探索，分析数据质量，并对数据进行清洗，然后通过特征选择对数据进行降维，方便后续分类运算；

分类阶段：首先通过训练集的特征矩阵、分类结果得到决策树分类器，然后将分类器应用于测试集。然后我们对决策树分类器的准确性进行分析，并对决策树模型进行可视化。

下面，我分别对这些模块进行介绍。

模块 1：数据探索

数据探索这部分虽然对分类器没有实质作用，但是不可忽略。我们只有足够了解这些数据的特性，才能帮助我们做数据清洗、特征选择。

那么如何进行数据探索呢？这里有一些函数你需要了解：

使用 info() 了解数据表的基本情况：行数、列数、每列的数据类型、数据完整度；

使用 describe() 了解数据表的统计情况：总数、平均值、标准差、最小值、最大值等；

使用 describe(include=[‘O’]) 查看字符串类型（非数字）的整体情况；

使用 head 查看前几行数据（默认是前 5 行）；

使用 tail 查看后几行数据（默认是最后 5 行）。

我们可以使用 Pandas 便捷地处理这些问题：

import pandas as pd

# 数据加载

train_data = pd.read_csv('./Titanic_Data/train.csv')

test_data = pd.read_csv('./Titanic_Data/test.csv')

# 数据探索

print(train_data.info())

print('-'*30)

print(train_data.describe())

print('-'*30)

print(train_data.describe(include=['O']))

print('-'*30)

print(train_data.head())

print('-'*30)

print(train_data.tail())

运行结果：

<class 'pandas.core.frame.DataFrame'>

RangeIndex: 891 entries, 0 to 890

Data columns (total 12 columns):

PassengerId    891 non-null int64

Survived       891 non-null int64

Pclass         891 non-null int64

Name           891 non-null object

Sex            891 non-null object

Age            714 non-null float64

SibSp          891 non-null int64

Parch          891 non-null int64

Ticket         891 non-null object

Fare           891 non-null float64

Cabin          204 non-null object

Embarked       889 non-null object

dtypes: float64(2), int64(5), object(5)

memory usage: 83.6+ KB

None

------------------------------

       PassengerId    Survived     ...           Parch        Fare

count   891.000000  891.000000     ...      891.000000  891.000000

mean    446.000000    0.383838     ...        0.381594   32.204208

std     257.353842    0.486592     ...        0.806057   49.693429

min       1.000000    0.000000     ...        0.000000    0.000000

25%     223.500000    0.000000     ...        0.000000    7.910400

50%     446.000000    0.000000     ...        0.000000   14.454200

75%     668.500000    1.000000     ...        0.000000   31.000000

max     891.000000    1.000000     ...        6.000000  512.329200

[8 rows x 7 columns]

------------------------------

                                          Name   Sex   ...       Cabin Embarked

count                                      891   891   ...         204      889

unique                                     891     2   ...         147        3

top     Peter, Mrs. Catherine (Catherine Rizk)  male   ...     B96 B98        S

freq                                         1   577   ...           4      644

[4 rows x 5 columns]

------------------------------

   PassengerId  Survived  Pclass    ...        Fare Cabin  Embarked

0            1         0       3    ...      7.2500   NaN         S

1            2         1       1    ...     71.2833   C85         C

2            3         1       3    ...      7.9250   NaN         S

3            4         1       1    ...     53.1000  C123         S

4            5         0       3    ...      8.0500   NaN         S

[5 rows x 12 columns]

------------------------------

     PassengerId  Survived  Pclass    ...      Fare Cabin  Embarked

886          887         0       2    ...     13.00   NaN         S

887          888         1       1    ...     30.00   B42         S

888          889         0       3    ...     23.45   NaN         S

889          890         1       1    ...     30.00  C148         C

890          891         0       3    ...      7.75   NaN         Q

[5 rows x 12 columns]

模块 2：数据清洗

通过数据探索，我们发现 Age、Fare 和 Cabin 这三个字段的数据有所缺失。其中 Age 为年龄字段，是数值型，我们可以通过平均值进行补齐；Fare 为船票价格，是数值型，我们也可以通过其他人购买船票的平均值进行补齐。

具体实现的代码如下：

# 使用平均年龄来填充年龄中的 nan 值

train_data['Age'].fillna(train_data['Age'].mean(), inplace=True)

test_data['Age'].fillna(test_data['Age'].mean(),inplace=True)

# 使用票价的均值填充票价中的 nan 值

train_data['Fare'].fillna(train_data['Fare'].mean(), inplace=True)

test_data['Fare'].fillna(test_data['Fare'].mean(),inplace=True)

Cabin 为船舱，有大量的缺失值。在训练集和测试集中的缺失率分别为 77% 和 78%，无法补齐；Embarked 为登陆港口，有少量的缺失值，我们可以把缺失值补齐。

首先观察下 Embarked 字段的取值，方法如下：

print(train_data['Embarked'].value_counts())

结果如下：

S    644

C    168

Q     77

我们发现一共就 3 个登陆港口，其中 S 港口人数最多，占到了 72%，因此我们将其余缺失的 Embarked 数值均设置为 S：

# 使用登录最多的港口来填充登录港口的 nan 值

train_data['Embarked'].fillna('S', inplace=True)

test_data['Embarked'].fillna('S',inplace=True)

模块 3：特征选择

特征选择是分类器的关键。特征选择不同，得到的分类器也不同。那么我们该选择哪些特征做生存的预测呢？

通过数据探索我们发现，PassengerId 为乘客编号，对分类没有作用，可以放弃；Name 为乘客姓名，对分类没有作用，可以放弃；Cabin 字段缺失值太多，可以放弃；Ticket 字段为船票号码，杂乱无章且无规律，可以放弃。其余的字段包括：Pclass、Sex、Age、SibSp、Parch 和 Fare，这些属性分别表示了乘客的船票等级、性别、年龄、亲戚数量以及船票价格，可能会和乘客的生存预测分类有关系。具体是什么关系，我们可以交给分类器来处理。

因此我们先将 Pclass、Sex、Age 等这些其余的字段作特征，放到特征向量 features 里。

# 特征选择

features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']

train_features = train_data[features]

train_labels = train_data['Survived']

test_features = test_data[features]

特征值里有一些是字符串，这样不方便后续的运算，需要转成数值类型，比如 Sex 字段，有 male 和 female 两种取值。我们可以把它变成 Sex=male 和 Sex=female 两个字段，数值用 0 或 1 来表示。

同理 Embarked 有 S、C、Q 三种可能，我们也可以改成 Embarked=S、Embarked=C 和 Embarked=Q 三个字段，数值用 0 或 1 来表示。

那该如何操作呢，我们可以使用 sklearn 特征选择中的 DictVectorizer 类，用它将可以处理符号化的对象，将符号转成数字 0/1 进行表示。具体方法如下：

from sklearn.feature_extraction import DictVectorizer

dvec=DictVectorizer(sparse=False)

train_features=dvec.fit_transform(train_features.to_dict(orient='record'))

你会看到代码中使用了 fit_transform 这个函数，它可以将特征向量转化为特征值矩阵。然后我们看下 dvec 在转化后的特征属性是怎样的，即查看 dvec 的 feature_names_ 属性值，方法如下：

print(dvec.feature_names_)

运行结果：

['Age', 'Embarked=C', 'Embarked=Q', 'Embarked=S', 'Fare', 'Parch', 'Pclass', 'Sex=female', 'Sex=male', 'SibSp']

你可以看到原本是一列的 Embarked，变成了“Embarked=C”“Embarked=Q”“Embarked=S”三列。Sex 列变成了“Sex=female”“Sex=male”两列。

这样 train_features 特征矩阵就包括 10 个特征值（列），以及 891 个样本（行），即 891 行，10 列的特征矩阵。

模块 4：决策树模型

刚才我们已经讲了如何使用 sklearn 中的决策树模型。现在我们使用 ID3 算法，即在创建 DecisionTreeClassifier 时，设置 criterion=‘entropy’，然后使用 fit 进行训练，将特征值矩阵和分类标识结果作为参数传入，得到决策树分类器。

from sklearn.tree import DecisionTreeClassifier

# 构造 ID3 决策树

clf = DecisionTreeClassifier(criterion='entropy')

# 决策树训练

clf.fit(train_features, train_labels)

模块 5：模型预测 & 评估

在预测中，我们首先需要得到测试集的特征值矩阵，然后使用训练好的决策树 clf 进行预测，得到预测结果 pred_labels：

test_features=dvec.transform(test_features.to_dict(orient='record'))

# 决策树预测

pred_labels = clf.predict(test_features)

在模型评估中，决策树提供了 score 函数可以直接得到准确率，但是我们并不知道真实的预测结果，所以无法用预测值和真实的预测结果做比较。我们只能使用训练集中的数据进行模型评估，可以使用决策树自带的 score 函数计算下得到的结果：

# 得到决策树准确率

acc_decision_tree = round(clf.score(train_features, train_labels), 6)

print(u'score 准确率为 %.4lf' % acc_decision_tree)

运行结果：

score 准确率为 0.9820

你会发现你刚用训练集做训练，再用训练集自身做准确率评估自然会很高。但这样得出的准确率并不能代表决策树分类器的准确率。

这是为什么呢？

因为我们没有测试集的实际结果，因此无法用测试集的预测结果与实际结果做对比。如果我们使用 score 函数对训练集的准确率进行统计，正确率会接近于 100%（如上结果为 98.2%），无法对分类器的在实际环境下做准确率的评估。

那么有什么办法，来统计决策树分类器的准确率呢？

这里可以使用 K 折交叉验证的方式，交叉验证是一种常用的验证分类准确率的方法，原理是拿出大部分样本进行训练，少量的用于分类器的验证。K 折交叉验证，就是做 K 次交叉验证，每次选取 K 分之一的数据作为验证，其余作为训练。轮流 K 次，取平均值。

K 折交叉验证的原理是这样的：

将数据集平均分割成 K 个等份；

使用 1 份数据作为测试数据，其余作为训练数据；

计算测试准确率；

使用不同的测试集，重复 2、3 步骤。

在 sklearn 的 model_selection 模型选择中提供了 cross_val_score 函数。cross_val_score 函数中的参数 cv 代表对原始数据划分成多少份，也就是我们的 K 值，一般建议 K 值取 10，因此我们可以设置 CV=10，我们可以对比下 score 和 cross_val_score 两种函数的正确率的评估结果：

import numpy as np

from sklearn.model_selection import cross_val_score

# 使用 K 折交叉验证 统计决策树准确率

print(u'cross_val_score 准确率为 %.4lf' % np.mean(cross_val_score(clf, train_features, train_labels, cv=10)))

运行结果：

cross_val_score 准确率为 0.7835

你可以看到，score 函数的准确率为 0.9820，cross_val_score 准确率为 0.7835。

这里很明显，对于不知道测试集实际结果的，要使用 K 折交叉验证才能知道模型的准确率。

模块 6：决策树可视化

sklearn 的决策树模型对我们来说，还是比较抽象的。我们可以使用 Graphviz 可视化工具帮我们把决策树呈现出来。

安装 Graphviz 库需要下面的几步：

安装 graphviz 工具，这里是它的下载地址；http://www.graphviz.org/download/

将 Graphviz 添加到环境变量 PATH 中；

需要 Graphviz 库，如果没有可以使用 pip install graphviz 进行安装。

这样你就可以在程序里面使用 Graphviz 对决策树模型进行呈现，最后得到一个决策树可视化的 PDF 文件，可视化结果文件 Source.gv.pdf 你可以在 GitHub 上下载：https://github.com/cystanford/Titanic_Data

决策树模型使用技巧总结

今天我用泰坦尼克乘客生存预测案例把决策树模型的流程跑了一遍。在实战中，你需要注意一下几点：

特征选择是分类模型好坏的关键。选择什么样的特征，以及对应的特征值矩阵，决定了分类模型的好坏。通常情况下，特征值不都是数值类型，可以使用 DictVectorizer 类进行转化；

模型准确率需要考虑是否有测试集的实际结果可以做对比，当测试集没有真实结果可以对比时，需要使用 K 折交叉验证 cross_val_score；

Graphviz 可视化工具可以很方便地将决策模型呈现出来，帮助你更好理解决策树的构建。

我上面讲了泰坦尼克乘客生存预测的六个关键模块，请你用 sklearn 中的决策树模型独立完成这个项目，对测试集中的乘客是否生存进行预测，并给出模型准确率评估。数据从 GitHub 上下载即可。

最后给你留一个思考题吧，我在构造特征向量时使用了 DictVectorizer 类，使用 fit_transform 函数将特征向量转化为特征值矩阵。DictVectorizer 类同时也提供 transform 函数，那么这两个函数有什么区别？

欢迎你在评论区留言与我分享你的答案，也欢迎点击“请朋友读”把这篇文章分享给你的朋友或者同事，一起交流一下。

20丨朴素贝叶斯分类（上）：如何让机器判断男女？

很多人都听说过贝叶斯原理，在哪听说过？基本上是在学概率统计的时候知道的。有些人可能会说，我记不住这些概率论的公式，没关系，我尽量用通俗易懂的语言进行讲解。

贝叶斯原理是英国数学家托马斯·贝叶斯提出的。贝叶斯是个很神奇的人，他的经历类似梵高。生前没有得到重视，死后，他写的一篇关于归纳推理的论文被朋友翻了出来，并发表了。这一发表不要紧，结果这篇论文的思想直接影响了接下来两个多世纪的统计学，是科学史上著名的论文之一。

贝叶斯原理跟我们的生活联系非常紧密。举个例子，如果你看到一个人总是花钱，那么会推断这个人多半是个有钱人。当然这也不是绝对，也就是说，当你不能准确预知一个事物本质的时候，你可以依靠和事物本质相关的事件来进行判断，如果事情发生的频次多，则证明这个属性更有可能存在。

贝叶斯原理

贝叶斯原理是怎么来的呢？贝叶斯为了解决一个叫“逆向概率”问题写了一篇文章，尝试解答在没有太多可靠证据的情况下，怎样做出更符合数学逻辑的推测。

什么是“逆向概率”呢？

所谓“逆向概率”是相对“正向概率”而言。正向概率的问题很容易理解，比如我们已经知道袋子里面有 N 个球，不是黑球就是白球，其中 M 个是黑球，那么把手伸进去摸一个球，就能知道摸出黑球的概率是多少。但这种情况往往是上帝视角，即了解了事情的全貌再做判断。

在现实生活中，我们很难知道事情的全貌。贝叶斯则从实际场景出发，提了一个问题：如果我们事先不知道袋子里面黑球和白球的比例，而是通过我们摸出来的球的颜色，能判断出袋子里面黑白球的比例么？

正是这样的一个问题，影响了接下来近 200 年的统计学理论。这是因为，贝叶斯原理与其他统计学推断方法截然不同，它是建立在主观判断的基础上：在我们不了解所有客观事实的情况下，同样可以先估计一个值，然后根据实际结果不断进行修正。

我们用一个题目来体会下：假设有一种病叫做“贝叶死”，它的发病率是万分之一，即 10000 人中会有 1 个人得病。现有一种测试可以检验一个人是否得病的准确率是 99.9%，它的误报率是 0.1%，那么现在的问题是，如果一个人被查出来患有“叶贝死”，实际上患有的可能性有多大？

你可能会想说，既然查出患有“贝叶死”的准确率是 99.9%，那是不是实际上患“贝叶死”的概率也是 99.9% 呢？实际上不是的。你自己想想，在 10000 个人中，还存在 0.1% 的误查的情况，也就是 10 个人没有患病但是被诊断成阳性。当然 10000 个人中，也确实存在一个患有贝叶死的人，他有 99.9% 的概率被检查出来。所以你可以粗算下，患病的这个人实际上是这 11 个人里面的一员，即实际患病比例是 1/11≈9%。

上面这个例子中，实际上涉及到了贝叶斯原理中的几个概念：

先验概率：

通过经验来判断事情发生的概率，比如说“贝叶死”的发病率是万分之一，就是先验概率。再比如南方的梅雨季是 6-7 月，就是通过往年的气候总结出来的经验，这个时候下雨的概率就比其他时间高出很多。

后验概率：

后验概率就是发生结果之后，推测原因的概率。比如说某人查出来了患有“贝叶死”，那么患病的原因可能是 A、B 或 C。患有“贝叶死”是因为原因 A 的概率就是后验概率。它是属于条件概率的一种。

条件概率：

事件 A 在另外一个事件 B 已经发生条件下的发生概率，表示为 P(A|B)，读作“在 B 发生的条件下 A 发生的概率”。比如原因 A 的条件下，患有“贝叶死”的概率，就是条件概率。

似然函数（likelihood function）：

你可以把概率模型的训练过程理解为求参数估计的过程。举个例子，如果一个硬币在 10 次抛落中正面均朝上。那么你肯定在想，这个硬币是均匀的可能性是多少？这里硬币均匀就是个参数，似然函数就是用来衡量这个模型的参数。似然在这里就是可能性的意思，它是关于统计参数的函数。

介绍完贝叶斯原理中的这几个概念，我们再来看下贝叶斯原理，实际上贝叶斯原理就是求解后验概率，我们假设：A 表示事件 “测出为阳性”, 用 B1 表示“患有贝叶死”, B2 表示“没有患贝叶死”。根据上面那道题，我们可以得到下面的信息。

患有贝叶死的情况下，测出为阳性的概率为 P(A|B1)=99.9%，没有患贝叶死，但测出为阳性的概率为 P(A|B2)=0.1%。另外患有贝叶死的概率为 P(B1)=0.01%，没有患贝叶死的概率 P(B2)=99.99%。

那么我们检测出来为阳性，而且是贝叶死的概率 P(B1，A）=P(B1)*P(A|B1)=0.01%*99.9%=0.00999%。

这里 P(B1,A) 代表的是联合概率，同样我们可以求得 P(B2,A)=P(B2)*P(A|B2)=99.99%*0.1%=0.09999%。

然后我们想求得是检查为阳性的情况下，患有贝叶死的概率，也即是 P(B1|A)。

所以检查出阳性，且患有贝叶死的概率为：

检查出是阳性，但没有患有贝叶死的概率为：

这里我们能看出来 0.01%+0.1% 均出现在了 P(B1|A) 和 P(B2|A) 的计算中作为分母。我们把它称之为论据因子，也相当于一个权值因子。

其中 P(B1）、P(B2) 就是先验概率，我们现在知道了观测值，就是被检测出来是阳性，来求患贝叶死的概率，也就是求后验概率。求后验概率就是贝叶斯原理要求的，基于刚才求得的 P(B1|A)，P(B2|A)，我们可以总结出贝叶斯公式为：

由此，我们可以得出通用的贝叶斯公式：

朴素贝叶斯

讲完贝叶斯原理之后，我们再来看下今天重点要讲的算法，朴素贝叶斯。它是一种简单但极为强大的预测建模算法。之所以称为朴素贝叶斯，是因为它假设每个输入变量是独立的。这是一个强硬的假设，实际情况并不一定，但是这项技术对于绝大部分的复杂问题仍然非常有效。

朴素贝叶斯模型由两种类型的概率组成：

每个类别的概率P(Cj)；

每个属性的条件概率P(Ai|Cj)。

我来举个例子说明下什么是类别概率和条件概率。假设我有 7 个棋子，其中 3 个是白色的，4 个是黑色的。那么棋子是白色的概率就是 3/7，黑色的概率就是 4/7，这个就是类别概率。

假设我把这 7 个棋子放到了两个盒子里，其中盒子 A 里面有 2 个白棋，2 个黑棋；盒子 B 里面有 1 个白棋，2 个黑棋。那么在盒子 A 中抓到白棋的概率就是 1/2，抓到黑棋的概率也是 1/2，这个就是条件概率，也就是在某个条件（比如在盒子 A 中）下的概率。

在朴素贝叶斯中，我们要统计的是属性的条件概率，也就是假设取出来的是白色的棋子，那么它属于盒子 A 的概率是 2/3。

为了训练朴素贝叶斯模型，我们需要先给出训练数据，以及这些数据对应的分类。那么上面这两个概率，也就是类别概率和条件概率。他们都可以从给出的训练数据中计算出来。一旦计算出来，概率模型就可以使用贝叶斯原理对新数据进行预测。

另外我想告诉你的是，贝叶斯原理、贝叶斯分类和朴素贝叶斯这三者之间是有区别的。

贝叶斯原理是最大的概念，它解决了概率论中“逆向概率”的问题，在这个理论基础上，人们设计出了贝叶斯分类器，朴素贝叶斯分类是贝叶斯分类器中的一种，也是最简单，最常用的分类器。朴素贝叶斯之所以朴素是因为它假设属性是相互独立的，因此对实际情况有所约束，如果属性之间存在关联，分类准确率会降低。不过好在对于大部分情况下，朴素贝叶斯的分类效果都不错。

朴素贝叶斯分类工作原理

朴素贝叶斯分类是常用的贝叶斯分类方法。我们日常生活中看到一个陌生人，要做的第一件事情就是判断 TA 的性别，判断性别的过程就是一个分类的过程。根据以往的经验，我们通常会从身高、体重、鞋码、头发长短、服饰、声音等角度进行判断。这里的“经验”就是一个训练好的关于性别判断的模型，其训练数据是日常中遇到的各式各样的人，以及这些人实际的性别数据。

离散数据案例

我们遇到的数据可以分为两种，一种是离散数据，另一种是连续数据。那什么是离散数据呢？离散就是不连续的意思，有明确的边界，比如整数 1，2，3 就是离散数据，而 1 到 3 之间的任何数，就是连续数据，它可以取在这个区间里的任何数值。

我以下面的数据为例，这些是根据你之前的经验所获得的数据。然后给你一个新的数据：身高“高”、体重“中”，鞋码“中”，请问这个人是男还是女？

针对这个问题，我们先确定一共有 3 个属性，假设我们用 A 代表属性，用 A1, A2, A3 分别为身高 = 高、体重 = 中、鞋码 = 中。一共有两个类别，假设用 C 代表类别，那么 C1,C2 分别是：男、女，在未知的情况下我们用 Cj 表示。

那么我们想求在 A1、A2、A3 属性下，Cj 的概率，用条件概率表示就是 P(Cj|A1A2A3)。根据上面讲的贝叶斯的公式，我们可以得出：

因为一共有 2 个类别，所以我们只需要求得 P(C1|A1A2A3) 和 P(C2|A1A2A3) 的概率即可，然后比较下哪个分类的可能性大，就是哪个分类结果。

在这个公式里，因为 P(A1A2A3) 都是固定的，我们想要寻找使得 P(Cj|A1A2A3) 的最大值，就等价于求 P(A1A2A3|Cj)P(Cj) 最大值。

我们假定 Ai 之间是相互独立的，那么：

P(A1A2A3|Cj)=P(A1|Cj)P(A2|Cj)P(A3|Cj)

然后我们需要从 Ai 和 Cj 中计算出 P(Ai|Cj) 的概率，带入到上面的公式得出 P(A1A2A3|Cj)，最后找到使得 P(A1A2A3|Cj) 最大的类别 Cj。

我分别求下这些条件下的概率：

P(A1|C1)=1/2, P(A2|C1)=1/2, P(A3|C1)=1/4，P(A1|C2)=0, P(A2|C2)=1/2, P(A3|C2)=1/2，所以 P(A1A2A3|C1)=1/16, P(A1A2A3|C2)=0。

因为 P(A1A2A3|C1)P(C1)>P(A1A2A3|C2)P(C2)，所以应该是 C1 类别，即男性。

连续数据案例

我们做了一个离散的数据案例，实际生活中我们得到的是连续的数值，比如下面这组数据：

那么如果给你一个新的数据，身高 180、体重 120，鞋码 41，请问该人是男是女呢？

公式还是上面的公式，这里的困难在于，由于身高、体重、鞋码都是连续变量，不能采用离散变量的方法计算概率。而且由于样本太少，所以也无法分成区间计算。怎么办呢？

这时，可以假设男性和女性的身高、体重、鞋码都是正态分布，通过样本计算出均值和方差，也就是得到正态分布的密度函数。有了密度函数，就可以把值代入，算出某一点的密度函数的值。比如，男性的身高是均值 179.5、标准差为 3.697 的正态分布。所以男性的身高为 180 的概率为 0.1069。怎么计算得出的呢? 你可以使用 EXCEL 的 NORMDIST(x,mean,standard_dev,cumulative) 函数，一共有 4 个参数：

x：正态分布中，需要计算的数值；

Mean：正态分布的平均值；

Standard_dev：正态分布的标准差；

Cumulative：取值为逻辑值，即 False 或 True。它决定了函数的形式。当为 TRUE 时，函数结果为累积分布；为 False 时，函数结果为概率密度。

这里我们使用的是 NORMDIST(180,179.5,3.697,0)=0.1069。

同理我们可以计算得出男性体重为 120 的概率为 0.000382324，男性鞋码为 41 号的概率为 0.120304111。

所以我们可以计算得出：

P(A1A2A3|C1)=P(A1|C1)P(A2|C1)P(A3|C1)=0.10690.0003823240.120304111=4.9169e-6

同理我们也可以计算出来该人为女的可能性：

P(A1A2A3|C2)=P(A1|C2)P(A2|C2)P(A3|C2)=0.000001474890.0153541440.120306074=2.7244e-9

很明显这组数据分类为男的概率大于分类为女的概率。

当然在 Python 中，有第三方库可以直接帮我们进行上面的操作，这个我们会在下节课中介绍。这里主要是给你讲解下具体的运算原理。

朴素贝叶斯分类器工作流程

朴素贝叶斯分类常用于文本分类，尤其是对于英文等语言来说，分类效果很好。它常用于垃圾文本过滤、情感预测、推荐系统等。

流程可以用下图表示：

从图片你也可以看出来，朴素贝叶斯分类器需要三个流程，我来给你一一讲解下这几个流程。

第一阶段：准备阶段

在这个阶段我们需要确定特征属性，比如上面案例中的“身高”、“体重”、“鞋码”等，并对每个特征属性进行适当划分，然后由人工对一部分数据进行分类，形成训练样本。

这一阶段是整个朴素贝叶斯分类中唯一需要人工完成的阶段，其质量对整个过程将有重要影响，分类器的质量很大程度上由特征属性、特征属性划分及训练样本质量决定。

第二阶段：训练阶段

这个阶段就是生成分类器，主要工作是计算每个类别在训练样本中的出现频率及每个特征属性划分对每个类别的条件概率。

输入是特征属性和训练样本，输出是分类器。

第三阶段：应用阶段

这个阶段是使用分类器对新数据进行分类。输入是分类器和新数据，输出是新数据的分类结果。

好了，在这次课中你了解了概率论中的贝叶斯原理，朴素贝叶斯的工作原理和工作流程，也对朴素贝叶斯的强大和限制有了认识。下一节中，我将带你实战，亲自掌握 Python 中关于朴素贝叶斯分类器工具的使用。

最后给你留两道思考题吧，第一道题，离散型变量和连续变量在朴素贝叶斯模型中的处理有什么差别呢？

第二个问题是，如果你的女朋友，在你的手机里发现了和别的女人的暧昧短信，于是她开始思考了 3 个概率问题，你来判断下下面的 3 个概率分别属于哪种概率：

你在没有任何情况下，出轨的概率；

如果你出轨了，那么你的手机里有暧昧短信的概率；

在你的手机里发现了暧昧短信，认为你出轨的概率。

这三种概率分别属于先验概率、后验概率和条件概率的哪一种？

欢迎在评论区分享你的答案，我也会和你一起讨论。如果你觉得这篇文章对你有帮助，欢迎分享给你的朋友，一起来交流。

21丨朴素贝叶斯分类（下）：如何对文档进行分类？

我们上一节讲了朴素贝叶斯的工作原理，今天我们来讲下这些原理是如何指导实际业务的。

朴素贝叶斯分类最适合的场景就是文本分类、情感分析和垃圾邮件识别。其中情感分析和垃圾邮件识别都是通过文本来进行判断。从这里你能看出来，这三个场景本质上都是文本分类，这也是朴素贝叶斯最擅长的地方。所以朴素贝叶斯也常用于自然语言处理 NLP 的工具。

今天我带你一起使用朴素贝叶斯做下文档分类的项目，最重要的工具就是 sklearn 这个机器学习神器。

sklearn 机器学习包

sklearn 的全称叫 Scikit-learn，它给我们提供了 3 个朴素贝叶斯分类算法，分别是高斯朴素贝叶斯（GaussianNB）、多项式朴素贝叶斯（MultinomialNB）和伯努利朴素贝叶斯（BernoulliNB）。

这三种算法适合应用在不同的场景下，我们应该根据特征变量的不同选择不同的算法：

高斯朴素贝叶斯：特征变量是连续变量，符合高斯分布，比如说人的身高，物体的长度。

多项式朴素贝叶斯：特征变量是离散变量，符合多项分布，在文档分类中特征变量体现在一个单词出现的次数，或者是单词的 TF-IDF 值等。

伯努利朴素贝叶斯：特征变量是布尔变量，符合 0/1 分布，在文档分类中特征是单词是否出现。

伯努利朴素贝叶斯是以文件为粒度，如果该单词在某文件中出现了即为 1，否则为 0。而多项式朴素贝叶斯是以单词为粒度，会计算在某个文件中的具体次数。而高斯朴素贝叶斯适合处理特征变量是连续变量，且符合正态分布（高斯分布）的情况。比如身高、体重这种自然界的现象就比较适合用高斯朴素贝叶斯来处理。而文本分类是使用多项式朴素贝叶斯或者伯努利朴素贝叶斯。

什么是 TF-IDF 值呢？

我在多项式朴素贝叶斯中提到了“词的 TF-IDF 值”，如何理解这个概念呢？

TF-IDF 是一个统计方法，用来评估某个词语对于一个文件集或文档库中的其中一份文件的重要程度。

TF-IDF 实际上是两个词组 Term Frequency 和 Inverse Document Frequency 的总称，两者缩写为 TF 和 IDF，分别代表了词频和逆向文档频率。

词频 TF计算了一个单词在文档中出现的次数，它认为一个单词的重要性和它在文档中出现的次数呈正比。

逆向文档频率 IDF，是指一个单词在文档中的区分度。它认为一个单词出现在的文档数越少，就越能通过这个单词把该文档和其他文档区分开。IDF 越大就代表该单词的区分度越大。

所以 TF-IDF 实际上是词频 TF 和逆向文档频率 IDF 的乘积。这样我们倾向于找到 TF 和 IDF 取值都高的单词作为区分，即这个单词在一个文档中出现的次数多，同时又很少出现在其他文档中。这样的单词适合用于分类。

TF-IDF 如何计算

首先我们看下词频 TF 和逆向文档概率 IDF 的公式。

为什么 IDF 的分母中，单词出现的文档数要加 1 呢？因为有些单词可能不会存在文档中，为了避免分母为 0，统一给单词出现的文档数都加 1。

TF-IDF=TF*IDF。

你可以看到，TF-IDF 值就是 TF 与 IDF 的乘积, 这样可以更准确地对文档进行分类。比如“我”这样的高频单词，虽然 TF 词频高，但是 IDF 值很低，整体的 TF-IDF 也不高。

我在这里举个例子。假设一个文件夹里一共有 10 篇文档，其中一篇文档有 1000 个单词，“this”这个单词出现 20 次，“bayes”出现了 5 次。“this”在所有文档中均出现过，而“bayes”只在 2 篇文档中出现过。我们来计算一下这两个词语的 TF-IDF 值。

针对“this”，计算 TF-IDF 值：

所以 TF-IDF=0.02*(-0.0414)=-8.28e-4。

针对“bayes”，计算 TF-IDF 值：

TF-IDF=0.005*0.5229=2.61e-3。

很明显“bayes”的 TF-IDF 值要大于“this”的 TF-IDF 值。这就说明用“bayes”这个单词做区分比单词“this”要好。

如何求 TF-IDF

在 sklearn 中我们直接使用 TfidfVectorizer 类，它可以帮我们计算单词 TF-IDF 向量的值。在这个类中，取 sklearn 计算的对数 log 时，底数是 e，不是 10。

下面我来讲下如何创建 TfidfVectorizer 类。

TfidfVectorizer 类的创建：

创建 TfidfVectorizer 的方法是：

TfidfVectorizer(stop_words=stop_words, token_pattern=token_pattern)

我们在创建的时候，有两个构造参数，可以自定义停用词 stop_words 和规律规则 token_pattern。需要注意的是传递的数据结构，停用词 stop_words 是一个列表 List 类型，而过滤规则 token_pattern 是正则表达式。

什么是停用词？停用词就是在分类中没有用的词，这些词一般词频 TF 高，但是 IDF 很低，起不到分类的作用。为了节省空间和计算时间，我们把这些词作为停用词 stop words，告诉机器这些词不需要帮我计算。

当我们创建好 TF-IDF 向量类型时，可以用 fit_transform 帮我们计算，返回给我们文本矩阵，该矩阵表示了每个单词在每个文档中的 TF-IDF 值。

在我们进行 fit_transform 拟合模型后，我们可以得到更多的 TF-IDF 向量属性，比如，我们可以得到词汇的对应关系（字典类型）和向量的 IDF 值，当然也可以获取设置的停用词 stop_words。

举个例子，假设我们有 4 个文档：

文档 1：this is the bayes document；

文档 2：this is the second second document；

文档 3：and the third one；

文档 4：is this the document。

现在想要计算文档里都有哪些单词，这些单词在不同文档中的 TF-IDF 值是多少呢？

首先我们创建 TfidfVectorizer 类：

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vec = TfidfVectorizer()

然后我们创建 4 个文档的列表 documents，并让创建好的 tfidf_vec 对 documents 进行拟合，得到 TF-IDF 矩阵：

documents = [

    'this is the bayes document',

    'this is the second second document',

    'and the third one',

    'is this the document'

]

tfidf_matrix = tfidf_vec.fit_transform(documents)

输出文档中所有不重复的词：

print('不重复的词:', tfidf_vec.get_feature_names())

运行结果

不重复的词: ['and', 'bayes', 'document', 'is', 'one', 'second', 'the', 'third', 'this']

输出每个单词对应的 id 值：

print('每个单词的 ID:', tfidf_vec.vocabulary_)

运行结果

每个单词的 ID: {'this': 8, 'is': 3, 'the': 6, 'bayes': 1, 'document': 2, 'second': 5, 'and': 0, 'third': 7, 'one': 4}

输出每个单词在每个文档中的 TF-IDF 值，向量里的顺序是按照词语的 id 顺序来的：

print('每个单词的 tfidf 值:', tfidf_matrix.toarray())

运行结果：

每个单词的 tfidf 值: [[0.         0.63314609 0.40412895 0.40412895 0.         0.

  0.33040189 0.         0.40412895]

 [0.         0.         0.27230147 0.27230147 0.         0.85322574

  0.22262429 0.         0.27230147]

 [0.55280532 0.         0.         0.         0.55280532 0.

  0.28847675 0.55280532 0.        ]

 [0.         0.         0.52210862 0.52210862 0.         0.

  0.42685801 0.         0.52210862]]

如何对文档进行分类

如果我们要对文档进行分类，有两个重要的阶段：

基于分词的数据准备，包括分词、单词权重计算、去掉停用词；

应用朴素贝叶斯分类进行分类，首先通过训练集得到朴素贝叶斯分类器，然后将分类器应用于测试集，并与实际结果做对比，最终得到测试集的分类准确率。

下面，我分别对这些模块进行介绍。

模块 1：对文档进行分词

在准备阶段里，最重要的就是分词。那么如果给文档进行分词呢？英文文档和中文文档所使用的分词工具不同。

在英文文档中，最常用的是 NTLK 包。NTLK 包中包含了英文的停用词 stop words、分词和标注方法。

import nltk

word_list = nltk.word_tokenize(text) # 分词

nltk.pos_tag(word_list) # 标注单词的词性

在中文文档中，最常用的是 jieba 包。jieba 包中包含了中文的停用词 stop words 和分词方法。

import jieba

word_list = jieba.cut (text) # 中文分词

模块 2：加载停用词表

我们需要自己读取停用词表文件，从网上可以找到中文常用的停用词保存在 stop_words.txt，然后利用 Python 的文件读取函数读取文件，保存在 stop_words 数组中。

stop_words = [line.strip().decode('utf-8') for line in io.open('stop_words.txt').readlines()]

模块 3：计算单词的权重

这里我们用到 sklearn 里的 TfidfVectorizer 类，上面我们介绍过它使用的方法。

直接创建 TfidfVectorizer 类，然后使用 fit_transform 方法进行拟合，得到 TF-IDF 特征空间 features，你可以理解为选出来的分词就是特征。我们计算这些特征在文档上的特征向量，得到特征空间 features。

tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5)

features = tf.fit_transform(train_contents)

这里 max_df 参数用来描述单词在文档中的最高出现率。假设 max_df=0.5，代表一个单词在 50% 的文档中都出现过了，那么它只携带了非常少的信息，因此就不作为分词统计。

一般很少设置 min_df，因为 min_df 通常都会很小。

模块 4：生成朴素贝叶斯分类器

我们将特征训练集的特征空间 train_features，以及训练集对应的分类 train_labels 传递给贝叶斯分类器 clf，它会自动生成一个符合特征空间和对应分类的分类器。

这里我们采用的是多项式贝叶斯分类器，其中 alpha 为平滑参数。为什么要使用平滑呢？因为如果一个单词在训练样本中没有出现，这个单词的概率就会被计算为 0。但训练集样本只是整体的抽样情况，我们不能因为一个事件没有观察到，就认为整个事件的概率为 0。为了解决这个问题，我们需要做平滑处理。

当 alpha=1 时，使用的是 Laplace 平滑。Laplace 平滑就是采用加 1 的方式，来统计没有出现过的单词的概率。这样当训练样本很大的时候，加 1 得到的概率变化可以忽略不计，也同时避免了零概率的问题。

当 0<alpha<1 时，使用的是 Lidstone 平滑。对于 Lidstone 平滑来说，alpha 越小，迭代次数越多，精度越高。我们可以设置 alpha 为 0.001。

# 多项式贝叶斯分类器

from sklearn.naive_bayes import MultinomialNB

clf = MultinomialNB(alpha=0.001).fit(train_features, train_labels)

模块 5：使用生成的分类器做预测

首先我们需要得到测试集的特征矩阵。

方法是用训练集的分词创建一个 TfidfVectorizer 类，使用同样的 stop_words 和 max_df，然后用这个 TfidfVectorizer 类对测试集的内容进行 fit_transform 拟合，得到测试集的特征矩阵 test_features。

test_tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5, vocabulary=train_vocabulary)

test_features=test_tf.fit_transform(test_contents)

然后我们用训练好的分类器对新数据做预测。

方法是使用 predict 函数，传入测试集的特征矩阵 test_features，得到分类结果 predicted_labels。predict 函数做的工作就是求解所有后验概率并找出最大的那个。

predicted_labels=clf.predict(test_features)

模块 6：计算准确率

计算准确率实际上是对分类模型的评估。我们可以调用 sklearn 中的 metrics 包，在 metrics 中提供了 accuracy_score 函数，方便我们对实际结果和预测的结果做对比，给出模型的准确率。

使用方法如下：

from sklearn import metrics

print metrics.accuracy_score(test_labels, predicted_labels)

数据挖掘神器 sklearn

从数据挖掘的流程来看，一般包括了获取数据、数据清洗、模型训练、模型评估和模型部署这几个过程。

sklearn 中包含了大量的数据挖掘算法，比如三种朴素贝叶斯算法，我们只需要了解不同算法的适用条件，以及创建时所需的参数，就可以用模型帮我们进行训练。在模型评估中，sklearn 提供了 metrics 包，帮我们对预测结果与实际结果进行评估。

在文档分类的项目中，我们针对文档的特点，给出了基于分词的准备流程。一般来说 NTLK 包适用于英文文档，而 jieba 适用于中文文档。我们可以根据文档选择不同的包，对文档提取分词。这些分词就是贝叶斯分类中最重要的特征属性。基于这些分词，我们得到分词的权重，即特征矩阵。

通过特征矩阵与分类结果，我们就可以创建出朴素贝叶斯分类器，然后用分类器进行预测，最后预测结果与实际结果做对比即可以得到分类器在测试集上的准确率。

练习题

我已经讲了中文文档分类中的 6 个关键的模块，最后，我给你留一道对中文文档分类的练习题吧。

我将中文文档数据集上传到了 GitHub 上，点击这里下载。

数据说明：

文档共有 4 种类型：女性、体育、文学、校园；

训练集放到 train 文件夹里，测试集放到 test 文件夹里，停用词放到 stop 文件夹里。

请使用朴素贝叶斯分类对训练集进行训练，并对测试集进行验证，并给出测试集的准确率。

最后你不妨思考一下，假设我们要判断一个人的性别，是通过身高、体重、鞋码、外貌等属性进行判断的，如果我们用朴素贝叶斯做分类，适合使用哪种朴素贝叶斯分类器？停用词的作用又是什么？

欢迎你在评论区进行留言，与我分享你的答案。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

22丨SVM（上）：如何用一根棍子将蓝红两色球分开？

今天我来带你进行 SVM 的学习，SVM 的英文叫 Support Vector Machine，中文名为支持向量机。它是常见的一种分类方法，在机器学习中，SVM 是有监督的学习模型。

什么是有监督的学习模型呢？它指的是我们需要事先对数据打上分类标签，这样机器就知道这个数据属于哪个分类。同样无监督学习，就是数据没有被打上分类标签，这可能是因为我们不具备先验的知识，或者打标签的成本很高。所以我们需要机器代我们部分完成这个工作，比如将数据进行聚类，方便后续人工对每个类进行分析。SVM 作为有监督的学习模型，通常可以帮我们模式识别、分类以及回归分析。

听起来，是不是很高大上。我先带你做个小练习。

练习 1：桌子上我放了红色和蓝色两种球，请你用一根棍子将这两种颜色的球分开。

你可以很快想到解决方案，在红色和蓝色球之间画条直线就好了，如下图所示：

练习 2：这次难度升级，桌子上依然放着红色、蓝色两种球，但是它们的摆放不规律，如下图所示。如何用一根棍子把这两种颜色分开呢？

你可能想了想，认为一根棍子是分不开的。除非把棍子弯曲，像下面这样：

所以这里直线变成了曲线。如果在同一个平面上来看，红蓝两种颜色的球是很难分开的。那么有没有一种方式，可以让它们自然地分开呢？

这里你可能会灵机一动，猛拍一下桌子，这些小球瞬间腾空而起，如下图所示。在腾起的那一刹那，出现了一个水平切面，恰好把红、蓝两种颜色的球分开。

在这里，二维平面变成了三维空间。原来的曲线变成了一个平面。这个平面，我们就叫做超平面。

SVM 的工作原理

用 SVM 计算的过程就是帮我们找到那个超平面的过程，这个超平面就是我们的 SVM 分类器。

我们再过头来看最简单的练习 1，其实我们可以有多种直线的划分，比如下图所示的直线 A、直线 B 和直线 C，究竟哪种才是更好的划分呢？

很明显图中的直线 B 更靠近蓝色球，但是在真实环境下，球再多一些的话，蓝色球可能就被划分到了直线 B 的右侧，被认为是红色球。同样直线 A 更靠近红色球，在真实环境下，如果红色球再多一些，也可能会被误认为是蓝色球。所以相比于直线 A 和直线 B，直线 C 的划分更优，因为它的鲁棒性更强。

那怎样才能寻找到直线 C 这个更优的答案呢？这里，我们引入一个 SVM 特有的概念：分类间隔。

实际上，我们的分类环境不是在二维平面中的，而是在多维空间中，这样直线 C 就变成了决策面 C。

在保证决策面不变，且分类不产生错误的情况下，我们可以移动决策面 C，直到产生两个极限的位置：如图中的决策面 A 和决策面 B。极限的位置是指，如果越过了这个位置，就会产生分类错误。这样的话，两个极限位置 A 和 B 之间的分界线 C 就是最优决策面。极限位置到最优决策面 C 之间的距离，就是“分类间隔”，英文叫做 margin。

如果我们转动这个最优决策面，你会发现可能存在多个最优决策面，它们都能把数据集正确分开，这些最优决策面的分类间隔可能是不同的，而那个拥有“最大间隔”（max margin）的决策面就是 SVM 要找的最优解。

点到超平面的距离公式

在上面这个例子中，如果我们把红蓝两种颜色的球放到一个三维空间里，你发现决策面就变成了一个平面。这里我们可以用线性函数来表示，如果在一维空间里就表示一个点，在二维空间里表示一条直线，在三维空间中代表一个平面，当然空间维数还可以更多，这样我们给这个线性函数起个名称叫做“超平面”。超平面的数学表达可以写成：

在这个公式里，w、x 是 n 维空间里的向量，其中 x 是函数变量；w 是法向量。法向量这里指的是垂直于平面的直线所表示的向量，它决定了超平面的方向。

SVM 就是帮我们找到一个超平面，这个超平面能将不同的样本划分开，同时使得样本集中的点到这个分类超平面的最小距离（即分类间隔）最大化。

在这个过程中，支持向量就是离分类超平面最近的样本点，实际上如果确定了支持向量也就确定了这个超平面。所以支持向量决定了分类间隔到底是多少，而在最大间隔以外的样本点，其实对分类都没有意义。

所以说， SVM 就是求解最大分类间隔的过程，我们还需要对分类间隔的大小进行定义。

首先，我们定义某类样本集到超平面的距离是这个样本集合内的样本到超平面的最短距离。我们用 di 代表点 xi 到超平面 wxi+b=0 的欧氏距离。因此我们要求 di 的最小值，用它来代表这个样本到超平面的最短距离。di 可以用公式计算得出：

其中||w||为超平面的范数，di 的公式可以用解析几何知识进行推导，这里不做解释。

最大间隔的优化模型

我们的目标就是找出所有分类间隔中最大的那个值对应的超平面。在数学上，这是一个凸优化问题（凸优化就是关于求凸集中的凸函数最小化的问题，这里不具体展开）。通过凸优化问题，最后可以求出最优的 w 和 b，也就是我们想要找的最优超平面。中间求解的过程会用到拉格朗日乘子，和 KKT（Karush-Kuhn-Tucker）条件。数学公式比较多，这里不进行展开。

硬间隔、软间隔和非线性 SVM

假如数据是完全的线性可分的，那么学习到的模型可以称为硬间隔支持向量机。换个说法，硬间隔指的就是完全分类准确，不能存在分类错误的情况。软间隔，就是允许一定量的样本分类错误。

我们知道，实际工作中的数据没有那么“干净”，或多或少都会存在一些噪点。所以线性可分是个理想情况。这时，我们需要使用到软间隔 SVM（近似线性可分），比如下面这种情况：

另外还存在一种情况，就是非线性支持向量机。

比如下面的样本集就是个非线性的数据。图中的两类数据，分别分布为两个圆圈的形状。那么这种情况下，不论是多高级的分类器，只要映射函数是线性的，就没法处理，SVM 也处理不了。这时，我们需要引入一个新的概念：核函数。它可以将样本从原始空间映射到一个更高维的特质空间中，使得样本在新的空间中线性可分。这样我们就可以使用原来的推导来进行计算，只是所有的推导是在新的空间，而不是在原来的空间中进行。

所以在非线性 SVM 中，核函数的选择就是影响 SVM 最大的变量。最常用的核函数有线性核、多项式核、高斯核、拉普拉斯核、sigmoid 核，或者是这些核函数的组合。这些函数的区别在于映射方式的不同。通过这些核函数，我们就可以把样本空间投射到新的高维空间中。

当然软间隔和核函数的提出，都是为了方便我们对上面超平面公式中的 w* 和 b* 进行求解，从而得到最大分类间隔的超平面。

用 SVM 如何解决多分类问题

SVM 本身是一个二值分类器，最初是为二分类问题设计的，也就是回答 Yes 或者是 No。而实际上我们要解决的问题，可能是多分类的情况，比如对文本进行分类，或者对图像进行识别。

针对这种情况，我们可以将多个二分类器组合起来形成一个多分类器，常见的方法有“一对多法”和“一对一法”两种。

1. 一对多法

假设我们要把物体分成 A、B、C、D 四种分类，那么我们可以先把其中的一类作为分类 1，其他类统一归为分类 2。这样我们可以构造 4 种 SVM，分别为以下的情况：

（1）样本 A 作为正集，B，C，D 作为负集；

（2）样本 B 作为正集，A，C，D 作为负集；

（3）样本 C 作为正集，A，B，D 作为负集；

（4）样本 D 作为正集，A，B，C 作为负集。

这种方法，针对 K 个分类，需要训练 K 个分类器，分类速度较快，但训练速度较慢，因为每个分类器都需要对全部样本进行训练，而且负样本数量远大于正样本数量，会造成样本不对称的情况，而且当增加新的分类，比如第 K+1 类时，需要重新对分类器进行构造。

2. 一对一法

一对一法的初衷是想在训练的时候更加灵活。我们可以在任意两类样本之间构造一个 SVM，这样针对 K 类的样本，就会有 C(k,2) 类分类器。

比如我们想要划分 A、B、C 三个类，可以构造 3 个分类器：

（1）分类器 1：A、B；

（2）分类器 2：A、C；

（3）分类器 3：B、C。

当对一个未知样本进行分类时，每一个分类器都会有一个分类结果，即为 1 票，最终得票最多的类别就是整个未知样本的类别。

这样做的好处是，如果新增一类，不需要重新训练所有的 SVM，只需要训练和新增这一类样本的分类器。而且这种方式在训练单个 SVM 模型的时候，训练速度快。

但这种方法的不足在于，分类器的个数与 K 的平方成正比，所以当 K 较大时，训练和测试的时间会比较慢。

总结

今天我给你讲了 SVM 分类器，它在文本分类尤其是针对二分类任务性能卓越。同样，针对多分类的情况，我们可以采用一对多，或者一对一的方法，多个二值分类器组合成一个多分类器。

另外关于 SVM 分类器的概念，我希望你能掌握以下的三个程度：

完全线性可分情况下的线性分类器，也就是线性可分的情况，是最原始的 SVM，它最核心的思想就是找到最大的分类间隔；

大部分线性可分情况下的线性分类器，引入了软间隔的概念。软间隔，就是允许一定量的样本分类错误；

线性不可分情况下的非线性分类器，引入了核函数。它让原有的样本空间通过核函数投射到了一个高维的空间中，从而变得线性可分。

在 SVM 的推导过程中，有大量的数学公式，这里不进行推导演绎，因为除了写论文，你大部分时候不会用到这些公式推导。

所以最重要的还是理解我上面讲的这些概念，能在实际工作中使用 SVM 才是最重要的。下一节我会和你讲如何用 sklearn 工具包进行 SVM 分类，带你做一个实际的案例。

最后，你能说一下你对有监督学习和无监督学习的理解吗？以及，SVM 最主要的思想就是硬间隔、软间隔和核函数。你是如何理解它们的？

欢迎你在评论区进行留言，与我分享你的答案。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

23丨SVM（下）：如何进行乳腺癌检测？

讲完了 SVM 的原理之后，今天我来带你进行 SVM 的实战。

在此之前我们先来回顾一下 SVM 的相关知识点。SVM 是有监督的学习模型，我们需要事先对数据打上分类标签，通过求解最大分类间隔来求解二分类问题。如果要求解多分类问题，可以将多个二分类器组合起来形成一个多分类器。

上一节中讲到了硬间隔、软间隔、非线性 SVM，以及分类间隔的公式，你可能会觉得比较抽象。这节课，我们会在实际使用中，讲解对工具的使用，以及相关参数的含义。

如何在 sklearn 中使用 SVM

在 Python 的 sklearn 工具包中有 SVM 算法，首先需要引用工具包：

from sklearn import svm

SVM 既可以做回归，也可以做分类器。

当用 SVM 做回归的时候，我们可以使用 SVR 或 LinearSVR。SVR 的英文是 Support Vector Regression。这篇文章只讲分类，这里只是简单地提一下。

当做分类器的时候，我们使用的是 SVC 或者 LinearSVC。SVC 的英文是 Support Vector Classification。

我简单说一下这两者之前的差别。

从名字上你能看出 LinearSVC 是个线性分类器，用于处理线性可分的数据，只能使用线性核函数。上一节，我讲到 SVM 是通过核函数将样本从原始空间映射到一个更高维的特质空间中，这样就使得样本在新的空间中线性可分。

如果是针对非线性的数据，需要用到 SVC。在 SVC 中，我们既可以使用到线性核函数（进行线性划分），也能使用高维的核函数（进行非线性划分）。

如何创建一个 SVM 分类器呢？

我们首先使用 SVC 的构造函数：model = svm.SVC(kernel=‘rbf’, C=1.0, gamma=‘auto’)，这里有三个重要的参数 kernel、C 和 gamma。

kernel 代表核函数的选择，它有四种选择，只不过默认是 rbf，即高斯核函数。

linear：线性核函数

poly：多项式核函数

rbf：高斯核函数（默认）

sigmoid：sigmoid 核函数

这四种函数代表不同的映射方式，你可能会问，在实际工作中，如何选择这 4 种核函数呢？我来给你解释一下：

线性核函数，是在数据线性可分的情况下使用的，运算速度快，效果好。不足在于它不能处理线性不可分的数据。

多项式核函数可以将数据从低维空间映射到高维空间，但参数比较多，计算量大。

高斯核函数同样可以将样本映射到高维空间，但相比于多项式核函数来说所需的参数比较少，通常性能不错，所以是默认使用的核函数。

了解深度学习的同学应该知道 sigmoid 经常用在神经网络的映射中。因此当选用 sigmoid 核函数时，SVM 实现的是多层神经网络。

上面介绍的 4 种核函数，除了第一种线性核函数外，其余 3 种都可以处理线性不可分的数据。

参数 C 代表目标函数的惩罚系数，惩罚系数指的是分错样本时的惩罚程度，默认情况下为 1.0。当 C 越大的时候，分类器的准确性越高，但同样容错率会越低，泛化能力会变差。相反，C 越小，泛化能力越强，但是准确性会降低。

参数 gamma 代表核函数的系数，默认为样本特征数的倒数，即 gamma = 1 / n_features。

在创建 SVM 分类器之后，就可以输入训练集对它进行训练。我们使用 model.fit(train_X,train_y)，传入训练集中的特征值矩阵 train_X 和分类标识 train_y。特征值矩阵就是我们在特征选择后抽取的特征值矩阵（当然你也可以用全部数据作为特征值矩阵）；分类标识就是人工事先针对每个样本标识的分类结果。这样模型会自动进行分类器的训练。我们可以使用 prediction=model.predict(test_X) 来对结果进行预测，传入测试集中的样本特征矩阵 test_X，可以得到测试集的预测分类结果 prediction。

同样我们也可以创建线性 SVM 分类器，使用 model=svm.LinearSVC()。在 LinearSVC 中没有 kernel 这个参数，限制我们只能使用线性核函数。由于 LinearSVC 对线性分类做了优化，对于数据量大的线性可分问题，使用 LinearSVC 的效率要高于 SVC。

如果你不知道数据集是否为线性，可以直接使用 SVC 类创建 SVM 分类器。

在训练和预测中，LinearSVC 和 SVC 一样，都是使用 model.fit(train_X,train_y) 和 model.predict(test_X)。

如何用 SVM 进行乳腺癌检测

在了解了如何创建和使用 SVM 分类器后，我们来看一个实际的项目，数据集来自美国威斯康星州的乳腺癌诊断数据集，点击这里进行下载。

医疗人员采集了患者乳腺肿块经过细针穿刺 (FNA) 后的数字化图像，并且对这些数字图像进行了特征提取，这些特征可以描述图像中的细胞核呈现。肿瘤可以分成良性和恶性。部分数据截屏如下所示：

数据表一共包括了 32 个字段，代表的含义如下：

上面的表格中，mean 代表平均值，se 代表标准差，worst 代表最大值（3 个最大值的平均值）。每张图像都计算了相应的特征，得出了这 30 个特征值（不包括 ID 字段和分类标识结果字段 diagnosis），实际上是 10 个特征值（radius、texture、perimeter、area、smoothness、compactness、concavity、concave points、symmetry 和 fractal_dimension_mean）的 3 个维度，平均、标准差和最大值。这些特征值都保留了 4 位数字。字段中没有缺失的值。在 569 个患者中，一共有 357 个是良性，212 个是恶性。

好了，我们的目标是生成一个乳腺癌诊断的 SVM 分类器，并计算这个分类器的准确率。首先设定项目的执行流程：

首先我们需要加载数据源；

在准备阶段，需要对加载的数据源进行探索，查看样本特征和特征值，这个过程你也可以使用数据可视化，它可以方便我们对数据及数据之间的关系进一步加深了解。然后按照“完全合一”的准则来评估数据的质量，如果数据质量不高就需要做数据清洗。数据清洗之后，你可以做特征选择，方便后续的模型训练；

在分类阶段，选择核函数进行训练，如果不知道数据是否为线性，可以考虑使用 SVC(kernel=‘rbf’) ，也就是高斯核函数的 SVM 分类器。然后对训练好的模型用测试集进行评估。

按照上面的流程，我们来编写下代码，加载数据并对数据做部分的探索：

# 加载数据集，你需要把数据放到目录中

data = pd.read_csv("./data.csv")

# 数据探索

# 因为数据集中列比较多，我们需要把 dataframe 中的列全部显示出来

pd.set_option('display.max_columns', None)

print(data.columns)

print(data.head(5))

print(data.describe())

这是部分的运行结果，完整结果你可以自己跑一下。

Index(['id', 'diagnosis', 'radius_mean', 'texture_mean', 'perimeter_mean',

       'area_mean', 'smoothness_mean', 'compactness_mean', 'concavity_mean',

       'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean',

       'radius_se', 'texture_se', 'perimeter_se', 'area_se', 'smoothness_se',

       'compactness_se', 'concavity_se', 'concave points_se', 'symmetry_se',

       'fractal_dimension_se', 'radius_worst', 'texture_worst',

       'perimeter_worst', 'area_worst', 'smoothness_worst',

       'compactness_worst', 'concavity_worst', 'concave points_worst',

       'symmetry_worst', 'fractal_dimension_worst'],

      dtype='object')

         id diagnosis  radius_mean  texture_mean  perimeter_mean  area_mean  \

0    842302         M        17.99         10.38          122.80     1001.0

1    842517         M        20.57         17.77          132.90     1326.0

2  84300903         M        19.69         21.25          130.00     1203.0

3  84348301         M        11.42         20.38           77.58      386.1

4  84358402         M        20.29         14.34          135.10     1297.0

接下来，我们就要对数据进行清洗了。

运行结果中，你能看到 32 个字段里，id 是没有实际含义的，可以去掉。diagnosis 字段的取值为 B 或者 M，我们可以用 0 和 1 来替代。另外其余的 30 个字段，其实可以分成三组字段，下划线后面的 mean、se 和 worst 代表了每组字段不同的度量方式，分别是平均值、标准差和最大值。

# 将特征字段分成 3 组

features_mean= list(data.columns[2:12])

features_se= list(data.columns[12:22])

features_worst=list(data.columns[22:32])

# 数据清洗

# ID 列没有用，删除该列

data.drop("id",axis=1,inplace=True)

# 将 B 良性替换为 0，M 恶性替换为 1

data['diagnosis']=data['diagnosis'].map({'M':1,'B':0})

然后我们要做特征字段的筛选，首先需要观察下 features_mean 各变量之间的关系，这里我们可以用 DataFrame 的 corr() 函数，然后用热力图帮我们可视化呈现。同样，我们也会看整体良性、恶性肿瘤的诊断情况。

# 将肿瘤诊断结果可视化

sns.countplot(data['diagnosis'],label="Count")

plt.show()

# 用热力图呈现 features_mean 字段之间的相关性

corr = data[features_mean].corr()

plt.figure(figsize=(14,14))

# annot=True 显示每个方格的数据

sns.heatmap(corr, annot=True)

plt.show()

这是运行的结果：

热力图中对角线上的为单变量自身的相关系数是 1。颜色越浅代表相关性越大。所以你能看出来 radius_mean、perimeter_mean 和 area_mean 相关性非常大，compactness_mean、concavity_mean、concave_points_mean 这三个字段也是相关的，因此我们可以取其中的一个作为代表。

那么如何进行特征选择呢？

特征选择的目的是降维，用少量的特征代表数据的特性，这样也可以增强分类器的泛化能力，避免数据过拟合。

我们能看到 mean、se 和 worst 这三组特征是对同一组内容的不同度量方式，我们可以保留 mean 这组特征，在特征选择中忽略掉 se 和 worst。同时我们能看到 mean 这组特征中，radius_mean、perimeter_mean、area_mean 这三个属性相关性大，compactness_mean、daconcavity_mean、concave points_mean 这三个属性相关性大。我们分别从这 2 类中选择 1 个属性作为代表，比如 radius_mean 和 compactness_mean。

这样我们就可以把原来的 10 个属性缩减为 6 个属性，代码如下：

# 特征选择

features_remain = ['radius_mean','texture_mean', 'smoothness_mean','compactness_mean','symmetry_mean', 'fractal_dimension_mean']

对特征进行选择之后，我们就可以准备训练集和测试集：

# 抽取 30% 的数据作为测试集，其余作为训练集

train, test = train_test_split(data, test_size = 0.3)# in this our main data is splitted into train and test

# 抽取特征选择的数值作为训练和测试数据

train_X = train[features_remain]

train_y=train['diagnosis']

test_X= test[features_remain]

test_y =test['diagnosis']

在训练之前，我们需要对数据进行规范化，这样让数据同在同一个量级上，避免因为维度问题造成数据误差：

# 采用 Z-Score 规范化数据，保证每个特征维度的数据均值为 0，方差为 1

ss = StandardScaler()

train_X = ss.fit_transform(train_X)

test_X = ss.transform(test_X)

最后我们可以让 SVM 做训练和预测了：

# 创建 SVM 分类器

model = svm.SVC()

# 用训练集做训练

model.fit(train_X,train_y)

# 用测试集做预测

prediction=model.predict(test_X)

print('准确率: ', metrics.accuracy_score(prediction,test_y))

运行结果：

准确率:  0.9181286549707602

准确率大于 90%，说明训练结果还不错。完整的代码你可以从GitHub上下载。

总结

今天我带你一起做了乳腺癌诊断分类的 SVM 实战，从这个过程中你应该能体会出来整个执行的流程，包括数据加载、数据探索、数据清洗、特征选择、SVM 训练和结果评估等环节。

sklearn 已经为我们提供了很好的工具，对上节课中讲到的 SVM 的创建和训练都进行了封装，让我们无需关心中间的运算细节。但正因为这样，我们更需要对每个流程熟练掌握，通过实战项目训练数据化思维和对数据的敏感度。

最后给你留两道思考题吧。还是这个乳腺癌诊断的数据，请你用 LinearSVC，选取全部的特征（除了 ID 以外）作为训练数据，看下你的分类器能得到多少的准确度呢？另外你对 sklearn 中 SVM 使用又有什么样的体会呢？

欢迎在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起来交流，一起来进步。

24丨KNN（上）：如何根据打斗和接吻次数来划分电影类型？

今天我来带你进行 KNN 的学习，KNN 的英文叫 K-Nearest Neighbor，应该算是数据挖掘算法中最简单的一种。

我们先用一个例子体会下。

假设，我们想对电影的类型进行分类，统计了电影中打斗次数、接吻次数，当然还有其他的指标也可以被统计到，如下表所示。

我们很容易理解《战狼》《红海行动》《碟中谍 6》是动作片，《前任 3》《春娇救志明》《泰坦尼克号》是爱情片，但是有没有一种方法让机器也可以掌握这个分类的规则，当有一部新电影的时候，也可以对它的类型自动分类呢？

我们可以把打斗次数看成 X 轴，接吻次数看成 Y 轴，然后在二维的坐标轴上，对这几部电影进行标记，如下图所示。对于未知的电影 A，坐标为 (x,y)，我们需要看下离电影 A 最近的都有哪些电影，这些电影中的大多数属于哪个分类，那么电影 A 就属于哪个分类。实际操作中，我们还需要确定一个 K 值，也就是我们要观察离电影 A 最近的电影有多少个。

KNN 的工作原理

“近朱者赤，近墨者黑”可以说是 KNN 的工作原理。整个计算过程分为三步：

计算待分类物体与其他物体之间的距离；

统计距离最近的 K 个邻居；

对于 K 个最近的邻居，它们属于哪个分类最多，待分类物体就属于哪一类。

K 值如何选择

你能看出整个 KNN 的分类过程，K 值的选择还是很重要的。那么问题来了，K 值选择多少是适合的呢？

如果 K 值比较小，就相当于未分类物体与它的邻居非常接近才行。这样产生的一个问题就是，如果邻居点是个噪声点，那么未分类物体的分类也会产生误差，这样 KNN 分类就会产生过拟合。

如果 K 值比较大，相当于距离过远的点也会对未知物体的分类产生影响，虽然这种情况的好处是鲁棒性强，但是不足也很明显，会产生欠拟合情况，也就是没有把未分类物体真正分类出来。

所以 K 值应该是个实践出来的结果，并不是我们事先而定的。在工程上，我们一般采用交叉验证的方式选取 K 值。

交叉验证的思路就是，把样本集中的大部分样本作为训练集，剩余的小部分样本用于预测，来验证分类模型的准确性。所以在 KNN 算法中，我们一般会把 K 值选取在较小的范围内，同时在验证集上准确率最高的那一个最终确定作为 K 值。

距离如何计算

在 KNN 算法中，还有一个重要的计算就是关于距离的度量。两个样本点之间的距离代表了这两个样本之间的相似度。距离越大，差异性越大；距离越小，相似度越大。

关于距离的计算方式有下面五种方式：

欧氏距离；

曼哈顿距离；

闵可夫斯基距离；

切比雪夫距离；

余弦距离。

其中前三种距离是 KNN 中最常用的距离，我给你分别讲解下。

欧氏距离是我们最常用的距离公式，也叫做欧几里得距离。在二维空间中，两点的欧式距离就是：

同理，我们也可以求得两点在 n 维空间中的距离：

曼哈顿距离在几何空间中用的比较多。以下图为例，绿色的直线代表两点之间的欧式距离，而红色和黄色的线为两点的曼哈顿距离。所以曼哈顿距离等于两个点在坐标系上绝对轴距总和。用公式表示就是：

闵可夫斯基距离不是一个距离，而是一组距离的定义。对于 n 维空间中的两个点 x(x1,x2,…,xn) 和 y(y1,y2,…,yn) ， x 和 y 两点之间的闵可夫斯基距离为：

其中 p 代表空间的维数，当 p=1 时，就是曼哈顿距离；当 p=2 时，就是欧氏距离；当 p→∞时，就是切比雪夫距离。

那么切比雪夫距离怎么计算呢？二个点之间的切比雪夫距离就是这两个点坐标数值差的绝对值的最大值，用数学表示就是：max(|x1-y1|,|x2-y2|)。

余弦距离实际上计算的是两个向量的夹角，是在方向上计算两者之间的差异，对绝对数值不敏感。在兴趣相关性比较上，角度关系比距离的绝对值更重要，因此余弦距离可以用于衡量用户对内容兴趣的区分度。比如我们用搜索引擎搜索某个关键词，它还会给你推荐其他的相关搜索，这些推荐的关键词就是采用余弦距离计算得出的。

KD 树

其实从上文你也能看出来，KNN 的计算过程是大量计算样本点之间的距离。为了减少计算距离次数，提升 KNN 的搜索效率，人们提出了 KD 树（K-Dimensional 的缩写）。KD 树是对数据点在 K 维空间中划分的一种数据结构。在 KD 树的构造中，每个节点都是 k 维数值点的二叉树。既然是二叉树，就可以采用二叉树的增删改查操作，这样就大大提升了搜索效率。

在这里，我们不需要对 KD 树的数学原理了解太多，你只需要知道它是一个二叉树的数据结构，方便存储 K 维空间的数据就可以了。而且在 sklearn 中，我们直接可以调用 KD 树，很方便。

用 KNN 做回归

KNN 不仅可以做分类，还可以做回归。首先讲下什么是回归。在开头电影这个案例中，如果想要对未知电影进行类型划分，这是一个分类问题。首先看一下要分类的未知电影，离它最近的 K 部电影大多数属于哪个分类，这部电影就属于哪个分类。

如果是一部新电影，已知它是爱情片，想要知道它的打斗次数、接吻次数可能是多少，这就是一个回归问题。

那么 KNN 如何做回归呢？

对于一个新点，我们需要找出这个点的 K 个最近邻居，然后将这些邻居的属性的平均值赋给该点，就可以得到该点的属性。当然不同邻居的影响力权重可以设置成不同的。举个例子，比如一部电影 A，已知它是动作片，当 K=3 时，最近的 3 部电影是《战狼》，《红海行动》和《碟中谍 6》，那么它的打斗次数和接吻次数的预估值分别为 (100+95+105)/3=100 次、(5+3+31)/3=13 次。

总结

今天我给你讲了 KNN 的原理，以及 KNN 中的几个关键因素。比如针对 K 值的选择，我们一般采用交叉验证的方式得出。针对样本点之间的距离的定义，常用的有 5 种表达方式，你也可以自己来定义两个样本之间的距离公式。不同的定义，适用的场景不同。比如在搜索关键词推荐中，余弦距离是更为常用的。

另外你也可以用 KNN 进行回归，通过 K 个邻居对新的点的属性进行值的预测。

KNN 的理论简单直接，针对 KNN 中的搜索也有相应的 KD 树这个数据结构。KNN 的理论成熟，可以应用到线性和非线性的分类问题中，也可以用于回归分析。

不过 KNN 需要计算测试点与样本点之间的距离，当数据量大的时候，计算量是非常庞大的，需要大量的存储空间和计算时间。另外如果样本分类不均衡，比如有些分类的样本非常少，那么该类别的分类准确率就会低很多。

当然在实际工作中，我们需要考虑到各种可能存在的情况，比如针对某类样本少的情况，可以增加该类别的权重。

同样 KNN 也可以用于推荐算法，虽然现在很多推荐系统的算法会使用 TD-IDF、协同过滤、Apriori 算法，不过针对数据量不大的情况下，采用 KNN 作为推荐算法也是可行的。

最后我给你留几道思考题吧，KNN 的算法原理和工作流程是怎么样的？KNN 中的 K 值又是如何选择的？

上一篇文章思考题的代码

我在上篇文章里留了一道思考题，你可以在GitHub上看到我写的关于这道题的代码（完整代码和文章案例代码差别不大），供你借鉴。

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

25丨KNN（下）：如何对手写数字进行识别？

今天我来带你进行 KNN 的实战。上节课，我讲了 KNN 实际上是计算待分类物体与其他物体之间的距离，然后通过统计最近的 K 个邻居的分类情况，来决定这个物体的分类情况。

这节课，我们先看下如何在 sklearn 中使用 KNN 算法，然后通过 sklearn 中自带的手写数字数据集来进行实战。

之前我还讲过 SVM、朴素贝叶斯和决策树分类，我们还可以用这个数据集来做下训练，对比下这四个分类器的训练结果。

如何在 sklearn 中使用 KNN

在 Python 的 sklearn 工具包中有 KNN 算法。KNN 既可以做分类器，也可以做回归。如果是做分类，你需要引用：

from sklearn.neighbors import KNeighborsClassifier

如果是做回归，你需要引用：

from sklearn.neighbors import KNeighborsRegressor

从名字上你也能看出来 Classifier 对应的是分类，Regressor 对应的是回归。一般来说如果一个算法有 Classifier 类，都能找到相应的 Regressor 类。比如在决策树分类中，你可以使用 DecisionTreeClassifier，也可以使用决策树来做回归 DecisionTreeRegressor。

好了，我们看下如何在 sklearn 中创建 KNN 分类器。

这里，我们使用构造函数 KNeighborsClassifier(n_neighbors=5, weights=‘uniform’, algorithm=‘auto’, leaf_size=30)，这里有几个比较主要的参数，我分别来讲解下：

1.n_neighbors：即 KNN 中的 K 值，代表的是邻居的数量。K 值如果比较小，会造成过拟合。如果 K 值比较大，无法将未知物体分类出来。一般我们使用默认值 5。

2.weights：是用来确定邻居的权重，有三种方式：

weights=uniform，代表所有邻居的权重相同；

weights=distance，代表权重是距离的倒数，即与距离成反比；

自定义函数，你可以自定义不同距离所对应的权重。大部分情况下不需要自己定义函数。

3.algorithm：用来规定计算邻居的方法，它有四种方式：

algorithm=auto，根据数据的情况自动选择适合的算法，默认情况选择 auto；

algorithm=kd_tree，也叫作 KD 树，是多维空间的数据结构，方便对关键数据进行检索，不过 KD 树适用于维度少的情况，一般维数不超过 20，如果维数大于 20 之后，效率反而会下降；

algorithm=ball_tree，也叫作球树，它和 KD 树一样都是多维空间的数据结果，不同于 KD 树，球树更适用于维度大的情况；

algorithm=brute，也叫作暴力搜索，它和 KD 树不同的地方是在于采用的是线性扫描，而不是通过构造树结构进行快速检索。当训练集大的时候，效率很低。

4.leaf_size：代表构造 KD 树或球树时的叶子数，默认是 30，调整 leaf_size 会影响到树的构造和搜索速度。

创建完 KNN 分类器之后，我们就可以输入训练集对它进行训练，这里我们使用 fit() 函数，传入训练集中的样本特征矩阵和分类标识，会自动得到训练好的 KNN 分类器。然后可以使用 predict() 函数来对结果进行预测，这里传入测试集的特征矩阵，可以得到测试集的预测分类结果。

如何用 KNN 对手写数字进行识别分类

手写数字数据集是个非常有名的用于图像识别的数据集。数字识别的过程就是将这些图片与分类结果 0-9 一一对应起来。完整的手写数字数据集 MNIST 里面包括了 60000 个训练样本，以及 10000 个测试样本。如果你学习深度学习的话，MNIST 基本上是你接触的第一个数据集。

今天我们用 sklearn 自带的手写数字数据集做 KNN 分类，你可以把这个数据集理解成一个简版的 MNIST 数据集，它只包括了 1797 幅数字图像，每幅图像大小是 8*8 像素。

好了，我们先来规划下整个 KNN 分类的流程：

整个训练过程基本上都会包括三个阶段：

数据加载：我们可以直接从 sklearn 中加载自带的手写数字数据集；

准备阶段：在这个阶段中，我们需要对数据集有个初步的了解，比如样本的个数、图像长什么样、识别结果是怎样的。你可以通过可视化的方式来查看图像的呈现。通过数据规范化可以让数据都在同一个数量级的维度。另外，因为训练集是图像，每幅图像是个 8*8 的矩阵，我们不需要对它进行特征选择，将全部的图像数据作为特征值矩阵即可；

分类阶段：通过训练可以得到分类器，然后用测试集进行准确率的计算。

好了，按照上面的步骤，我们一起来实现下这个项目。

首先是加载数据和对数据的探索：

# 加载数据

digits = load_digits()

data = digits.data

# 数据探索

print(data.shape)

# 查看第一幅图像

print(digits.images[0])

# 第一幅图像代表的数字含义

print(digits.target[0])

# 将第一幅图像显示出来

plt.gray()

plt.imshow(digits.images[0])

plt.show()

运行结果：

(1797, 64)

[[ 0.  0.  5. 13.  9.  1.  0.  0.]

 [ 0.  0. 13. 15. 10. 15.  5.  0.]

 [ 0.  3. 15.  2.  0. 11.  8.  0.]

 [ 0.  4. 12.  0.  0.  8.  8.  0.]

 [ 0.  5.  8.  0.  0.  9.  8.  0.]

 [ 0.  4. 11.  0.  1. 12.  7.  0.]

 [ 0.  2. 14.  5. 10. 12.  0.  0.]

 [ 0.  0.  6. 13. 10.  0.  0.  0.]]

0

我们对原始数据集中的第一幅进行数据可视化，可以看到图像是个 8*8 的像素矩阵，上面这幅图像是一个“0”，从训练集的分类标注中我们也可以看到分类标注为“0”。

sklearn 自带的手写数字数据集一共包括了 1797 个样本，每幅图像都是 8*8 像素的矩阵。因为并没有专门的测试集，所以我们需要对数据集做划分，划分成训练集和测试集。因为 KNN 算法和距离定义相关，我们需要对数据进行规范化处理，采用 Z-Score 规范化，代码如下：

# 分割数据，将 25% 的数据作为测试集，其余作为训练集（你也可以指定其他比例的数据作为训练集）

train_x, test_x, train_y, test_y = train_test_split(data, digits.target, test_size=0.25, random_state=33)

# 采用 Z-Score 规范化

ss = preprocessing.StandardScaler()

train_ss_x = ss.fit_transform(train_x)

test_ss_x = ss.transform(test_x)

然后我们构造一个 KNN 分类器 knn，把训练集的数据传入构造好的 knn，并通过测试集进行结果预测，与测试集的结果进行对比，得到 KNN 分类器准确率，代码如下：

# 创建 KNN 分类器

knn = KNeighborsClassifier()

knn.fit(train_ss_x, train_y)

predict_y = knn.predict(test_ss_x)

print("KNN 准确率: %.4lf" % accuracy_score(predict_y, test_y))

运行结果：

KNN 准确率: 0.9756

好了，这样我们就构造好了一个 KNN 分类器。之前我们还讲过 SVM、朴素贝叶斯和决策树分类。我们用手写数字数据集一起来训练下这些分类器，然后对比下哪个分类器的效果更好。代码如下：

# 创建 SVM 分类器

svm = SVC()

svm.fit(train_ss_x, train_y)

predict_y=svm.predict(test_ss_x)

print('SVM 准确率: %0.4lf' % accuracy_score(predict_y, test_y))

# 采用 Min-Max 规范化

mm = preprocessing.MinMaxScaler()

train_mm_x = mm.fit_transform(train_x)

test_mm_x = mm.transform(test_x)

# 创建 Naive Bayes 分类器

mnb = MultinomialNB()

mnb.fit(train_mm_x, train_y)

predict_y = mnb.predict(test_mm_x)

print(" 多项式朴素贝叶斯准确率: %.4lf" % accuracy_score(predict_y, test_y))

# 创建 CART 决策树分类器

dtc = DecisionTreeClassifier()

dtc.fit(train_mm_x, train_y)

predict_y = dtc.predict(test_mm_x)

print("CART 决策树准确率: %.4lf" % accuracy_score(predict_y, test_y))

运行结果如下：

SVM 准确率: 0.9867

多项式朴素贝叶斯准确率: 0.8844

CART 决策树准确率: 0.8556

这里需要注意的是，我们在做多项式朴素贝叶斯分类的时候，传入的数据不能有负数。因为 Z-Score 会将数值规范化为一个标准的正态分布，即均值为 0，方差为 1，数值会包含负数。因此我们需要采用 Min-Max 规范化，将数据规范化到 [0,1] 范围内。

好了，我们整理下这 4 个分类器的结果。

你能看出来 KNN 的准确率还是不错的，和 SVM 不相上下。

你可以自己跑一遍整个代码，在运行前还需要 import 相关的工具包（下面的这些工具包你都会用到，所以都需要引用）：

from sklearn.model_selection import train_test_split

from sklearn import preprocessing

from sklearn.metrics import accuracy_score

from sklearn.datasets import load_digits

from sklearn.neighbors import KNeighborsClassifier

from sklearn.svm import SVC

from sklearn.naive_bayes import MultinomialNB

from sklearn.tree import DecisionTreeClassifier

import matplotlib.pyplot as plt

代码中，我使用了 train_test_split 做数据集的拆分，使用 matplotlib.pyplot 工具包显示图像，使用 accuracy_score 进行分类器准确率的计算，使用 preprocessing 中的 StandardScaler 和 MinMaxScaler 做数据的规范化。

完整的代码你可以从GitHub上下载。

总结

今天我带你一起做了手写数字分类识别的实战，分别用 KNN、SVM、朴素贝叶斯和决策树做分类器，并统计了四个分类器的准确率。在这个过程中你应该对数据探索、数据可视化、数据规范化、模型训练和结果评估的使用过程有了一定的体会。在数据量不大的情况下，使用 sklearn 还是方便的。

如果数据量很大，比如 MNIST 数据集中的 6 万个训练数据和 1 万个测试数据，那么采用深度学习 +GPU 运算的方式会更适合。因为深度学习的特点就是需要大量并行的重复计算，GPU 最擅长的就是做大量的并行计算。

最后留两道思考题吧，请你说说项目中 KNN 分类器的常用构造参数，功能函数都有哪些，以及你对 KNN 使用的理解？如果把 KNN 中的 K 值设置为 200，数据集还是 sklearn 中的手写数字数据集，再跑一遍程序，看看分类器的准确率是多少？

欢迎在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

26丨K-Means（上）：如何给20支亚洲球队做聚类？

今天我来带你进行 K-Means 的学习。K-Means 是一种非监督学习，解决的是聚类问题。K 代表的是 K 类，Means 代表的是中心，你可以理解这个算法的本质是确定 K 类的中心点，当你找到了这些中心点，也就完成了聚类。

那么请你和我思考以下三个问题：

如何确定 K 类的中心点？

如何将其他点划分到 K 类中？

如何区分 K-Means 与 KNN？

如果理解了上面这 3 个问题，那么对 K-Means 的原理掌握得也就差不多了。

先请你和我思考一个场景，假设我有 20 支亚洲足球队，想要将它们按照成绩划分成 3 个等级，可以怎样划分？

K-Means 的工作原理

对亚洲足球队的水平，你可能也有自己的判断。比如一流的亚洲球队有谁？你可能会说伊朗或韩国。二流的亚洲球队呢？你可能说是中国。三流的亚洲球队呢？你可能会说越南。

其实这些都是靠我们的经验来划分的，那么伊朗、中国、越南可以说是三个等级的典型代表，也就是我们每个类的中心点。

所以回过头来，如何确定 K 类的中心点？一开始我们是可以随机指派的，当你确认了中心点后，就可以按照距离将其他足球队划分到不同的类别中。

这也就是 K-Means 的中心思想，就是这么简单直接。你可能会问：如果一开始，选择一流球队是中国，二流球队是伊朗，三流球队是韩国，中心点选择错了怎么办？其实不用担心，K-Means 有自我纠正机制，在不断的迭代过程中，会纠正中心点。中心点在整个迭代过程中，并不是唯一的，只是你需要一个初始值，一般算法会随机设置初始的中心点。

好了，那我来把 K-Means 的工作原理给你总结下：

选取 K 个点作为初始的类中心点，这些点一般都是从数据集中随机抽取的；

将每个点分配到最近的类中心点，这样就形成了 K 个类，然后重新计算每个类的中心点；

重复第二步，直到类不发生变化，或者你也可以设置最大迭代次数，这样即使类中心点发生变化，但是只要达到最大迭代次数就会结束。

如何给亚洲球队做聚类

对于机器来说需要数据才能判断类中心点，所以我整理了 2015-2019 年亚洲球队的排名，如下表所示。

我来说明一下数据概况。

其中 2019 年国际足联的世界排名，2015 年亚洲杯排名均为实际排名。2018 年世界杯中，很多球队没有进入到决赛圈，所以只有进入到决赛圈的球队才有实际的排名。如果是亚洲区预选赛 12 强的球队，排名会设置为 40。如果没有进入亚洲区预选赛 12 强，球队排名会设置为 50。

针对上面的排名，我们首先需要做的是数据规范化。你可以把这些值划分到 [0,1] 或者按照均值为 0，方差为 1 的正态分布进行规范化。具体数据规范化的步骤可以看下 13 篇，也就是数据变换那一篇。

我先把数值都规范化到 [0,1] 的空间中，得到了以下的数值表：

如果我们随机选取中国、日本、韩国为三个类的中心点，我们就需要看下这些球队到中心点的距离。

距离有多种计算的方式，有关距离的计算我在 KNN 算法中也讲到过：

欧氏距离

曼哈顿距离

切比雪夫距离

余弦距离

欧氏距离是最常用的距离计算方式，这里我选择欧氏距离作为距离的标准，计算每个队伍分别到中国、日本、韩国的距离，然后根据距离远近来划分。我们看到大部分的队，会和中国队聚类到一起。这里我整理了距离的计算过程，比如中国和中国的欧氏距离为 0，中国和日本的欧式距离为 0.732003。如果按照中国、日本、韩国为 3 个分类的中心点，欧氏距离的计算结果如下表所示：

然后我们再重新计算这三个类的中心点，如何计算呢？最简单的方式就是取平均值，然后根据新的中心点按照距离远近重新分配球队的分类，再根据球队的分类更新中心点的位置。计算过程这里不展开，最后一直迭代（重复上述的计算过程：计算中心点和划分分类）到分类不再发生变化，可以得到以下的分类结果：

所以我们能看出来第一梯队有日本、韩国、伊朗、沙特、澳洲；第二梯队有中国、伊拉克、阿联酋、乌兹别克斯坦；第三梯队有卡塔尔、泰国、越南、阿曼、巴林、朝鲜、印尼、叙利亚、约旦、科威特和巴勒斯坦。

如何使用 sklearn 中的 K-Means 算法

sklearn 是 Python 的机器学习工具库，如果从功能上来划分，sklearn 可以实现分类、聚类、回归、降维、模型选择和预处理等功能。这里我们使用的是 sklearn 的聚类函数库，因此需要引用工具包，具体代码如下：

from sklearn.cluster import KMeans

当然 K-Means 只是 sklearn.cluster 中的一个聚类库，实际上包括 K-Means 在内，sklearn.cluster 一共提供了 9 种聚类方法，比如 Mean-shift，DBSCAN，Spectral clustering（谱聚类）等。这些聚类方法的原理和 K-Means 不同，这里不做介绍。

我们看下 K-Means 如何创建：

KMeans(n_clusters=8, init='k-means++', n_init=10, max_iter=300, tol=0.0001, precompute_distances='auto', verbose=0, random_state=None, copy_x=True, n_jobs=1, algorithm='auto')

我们能看到在 K-Means 类创建的过程中，有一些主要的参数：

n_clusters: 即 K 值，一般需要多试一些 K 值来保证更好的聚类效果。你可以随机设置一些 K 值，然后选择聚类效果最好的作为最终的 K 值；

max_iter： 最大迭代次数，如果聚类很难收敛的话，设置最大迭代次数可以让我们及时得到反馈结果，否则程序运行时间会非常长；

n_init：初始化中心点的运算次数，默认是 10。程序是否能快速收敛和中心点的选择关系非常大，所以在中心点选择上多花一些时间，来争取整体时间上的快速收敛还是非常值得的。由于每一次中心点都是随机生成的，这样得到的结果就有好有坏，非常不确定，所以要运行 n_init 次, 取其中最好的作为初始的中心点。如果 K 值比较大的时候，你可以适当增大 n_init 这个值；

init： 即初始值选择的方式，默认是采用优化过的 k-means++ 方式，你也可以自己指定中心点，或者采用 random 完全随机的方式。自己设置中心点一般是对于个性化的数据进行设置，很少采用。random 的方式则是完全随机的方式，一般推荐采用优化过的 k-means++ 方式；

algorithm：k-means 的实现算法，有“auto” “full”“elkan”三种。一般来说建议直接用默认的"auto"。简单说下这三个取值的区别，如果你选择"full"采用的是传统的 K-Means 算法，“auto”会根据数据的特点自动选择是选择“full”还是“elkan”。我们一般选择默认的取值，即“auto” 。

在创建好 K-Means 类之后，就可以使用它的方法，最常用的是 fit 和 predict 这个两个函数。你可以单独使用 fit 函数和 predict 函数，也可以合并使用 fit_predict 函数。其中 fit(data) 可以对 data 数据进行 k-Means 聚类。 predict(data) 可以针对 data 中的每个样本，计算最近的类。

现在我们要完整地跑一遍 20 支亚洲球队的聚类问题。我把数据上传到了GitHub上，你可以自行下载。

# coding: utf-8

from sklearn.cluster import KMeans

from sklearn import preprocessing

import pandas as pd

import numpy as np

# 输入数据

data = pd.read_csv('data.csv', encoding='gbk')

train_x = data[["2019 年国际排名 ","2018 世界杯 ","2015 亚洲杯 "]]

df = pd.DataFrame(train_x)

kmeans = KMeans(n_clusters=3)

# 规范化到 [0,1] 空间

min_max_scaler=preprocessing.MinMaxScaler()

train_x=min_max_scaler.fit_transform(train_x)

# kmeans 算法

kmeans.fit(train_x)

predict_y = kmeans.predict(train_x)

# 合并聚类结果，插入到原数据中

result = pd.concat((data,pd.DataFrame(predict_y)),axis=1)

result.rename({0:u'聚类'},axis=1,inplace=True)

print(result)

运行结果：

国家  2019 年国际排名  2018 世界杯  2015 亚洲杯  聚类

0       中国         73       40        7   2

1       日本         60       15        5   0

2       韩国         61       19        2   0

3       伊朗         34       18        6   0

4       沙特         67       26       10   0

5      伊拉克         91       40        4   2

6      卡塔尔        101       40       13   1

7      阿联酋         81       40        6   2

8   乌兹别克斯坦         88       40        8   2

9       泰国        122       40       17   1

10      越南        102       50       17   1

11      阿曼         87       50       12   1

12      巴林        116       50       11   1

13      朝鲜        110       50       14   1

14      印尼        164       50       17   1

15      澳洲         40       30        1   0

16     叙利亚         76       40       17   1

17      约旦        118       50        9   1

18     科威特        160       50       15   1

19    巴勒斯坦         96       50       16   1

总结

今天我给你讲了 K-Means 算法原理，我们再来看下开篇我给你提的三个问题。

如何确定 K 类的中心点？其中包括了初始的设置，以及中间迭代过程中中心点的计算。在初始设置中，会进行 n_init 次的选择，然后选择初始中心点效果最好的为初始值。在每次分类更新后，你都需要重新确认每一类的中心点，一般采用均值的方式进行确认。

如何将其他点划分到 K 类中？这里实际上是关于距离的定义，我们知道距离有多种定义的方式，在 K-Means 和 KNN 中，我们都可以采用欧氏距离、曼哈顿距离、切比雪夫距离、余弦距离等。对于点的划分，就看它离哪个类的中心点的距离最近，就属于哪一类。

如何区分 K-Means 和 KNN 这两种算法呢？刚学过 K-Means 和 KNN 算法的同学应该能知道两者的区别，但往往过了一段时间，就容易混淆。所以我们可以从三个维度来区分 K-Means 和 KNN 这两个算法：

首先，这两个算法解决数据挖掘的两类问题。K-Means 是聚类算法，KNN 是分类算法。

这两个算法分别是两种不同的学习方式。K-Means 是非监督学习，也就是不需要事先给出分类标签，而 KNN 是有监督学习，需要我们给出训练数据的分类标识。

最后，K 值的含义不同。K-Means 中的 K 值代表 K 类。KNN 中的 K 值代表 K 个最接近的邻居。

那么学完了今天的内容后，你能说一下 K-Means 的算法原理吗？如果我们把上面的 20 支亚洲球队用 K-Means 划分成 5 类，在规范化数据的时候采用标准化的方式（即均值为 0，方差为 1），该如何编写程序呢？运行的结果又是如何？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

27丨K-Means（下）：如何使用K-Means对图像进行分割？

上节课，我讲解了 K-Means 的原理，并且用 K-Means 对 20 支亚洲球队进行了聚类，分成 3 个梯队。今天我们继续用 K-Means 进行聚类的实战。聚类的一个常用场景就是对图像进行分割。

图像分割就是利用图像自身的信息，比如颜色、纹理、形状等特征进行划分，将图像分割成不同的区域，划分出来的每个区域就相当于是对图像中的像素进行了聚类。单个区域内的像素之间的相似度大，不同区域间的像素差异性大。这个特性正好符合聚类的特性，所以你可以把图像分割看成是将图像中的信息进行聚类。当然聚类只是分割图像的一种方式，除了聚类，我们还可以基于图像颜色的阈值进行分割，或者基于图像边缘的信息进行分割等。

将微信开屏封面进行分割

上节课，我讲了 sklearn 工具包中的 K-Means 算法使用，我们现在用 K-Means 算法对微信页面进行分割。微信开屏图如下所示：

我们先设定下聚类的流程，聚类的流程和分类差不多，如图所示：

在准备阶段里，我们需要对数据进行加载。因为处理的是图像信息，我们除了要获取图像数据以外，还需要获取图像的尺寸和通道数，然后基于图像中每个通道的数值进行数据规范化。这里我们需要定义个函数 load_data，来帮我们进行图像加载和数据规范化。代码如下：

# 加载图像，并对数据进行规范化

def load_data(filePath):

    # 读文件

    f = open(filePath,'rb')

    data = []

    # 得到图像的像素值

    img = image.open(f)

    # 得到图像尺寸

    width, height = img.size

    for x in range(width):

        for y in range(height):

            # 得到点 (x,y) 的三个通道值

            c1, c2, c3 = img.getpixel((x, y))

            data.append([c1, c2, c3])

    f.close()

    # 采用 Min-Max 规范化

    mm = preprocessing.MinMaxScaler()

    data = mm.fit_transform(data)

    return np.mat(data), width, height

因为 jpg 格式的图像是三个通道 (R,G,B)，也就是一个像素点具有 3 个特征值。这里我们用 c1、c2、c3 来获取平面坐标点 (x,y) 的三个特征值，特征值是在 0-255 之间。

为了加快聚类的收敛，我们需要采用 Min-Max 规范化对数据进行规范化。我们定义的 load_data 函数返回的结果包括了针对 (R,G,B) 三个通道规范化的数据，以及图像的尺寸信息。在定义好 load_data 函数后，我们直接调用就可以得到相关信息，代码如下：

# 加载图像，得到规范化的结果 img，以及图像尺寸

img, width, height = load_data('./weixin.jpg')

假设我们想要对图像分割成 2 部分，在聚类阶段，我们可以将聚类数设置为 2，这样图像就自动聚成 2 类。代码如下：

# 用 K-Means 对图像进行 2 聚类

kmeans =KMeans(n_clusters=2)

kmeans.fit(img)

label = kmeans.predict(img)

# 将图像聚类结果，转化成图像尺寸的矩阵

label = label.reshape([width, height])

# 创建个新图像 pic_mark，用来保存图像聚类的结果，并设置不同的灰度值

pic_mark = image.new("L", (width, height))

for x in range(width):

    for y in range(height):

        # 根据类别设置图像灰度, 类别 0 灰度值为 255， 类别 1 灰度值为 127

        pic_mark.putpixel((x, y), int(256/(label[x][y]+1))-1)

pic_mark.save("weixin_mark.jpg", "JPEG")

代码中有一些参数，我来给你讲解一下这些参数的作用和设置方法。

我们使用了 fit 和 predict 这两个函数来做数据的训练拟合和预测，因为传入的参数是一样的，我们可以同时进行 fit 和 predict 操作，这样我们可以直接使用 fit_predict(data) 得到聚类的结果。得到聚类的结果 label 后，实际上是一个一维的向量，我们需要把它转化成图像尺寸的矩阵。label 的聚类结果是从 0 开始统计的，当聚类数为 2 的时候，聚类的标识 label=0 或者 1。

如果你想对图像聚类的结果进行可视化，直接看 0 和 1 是看不出来的，还需要将 0 和 1 转化为灰度值。灰度值一般是在 0-255 的范围内，我们可以将 label=0 设定为灰度值 255，label=1 设定为灰度值 127。具体方法是用 int(256/(label[x][y]+1))-1。可视化的时候，主要是通过设置图像的灰度值进行显示。所以我们把聚类 label=0 的像素点都统一设置灰度值为 255，把聚类 label=1 的像素点都统一设置灰度值为 127。原来图像的灰度值是在 0-255 之间，现在就只有 2 种颜色（也就是灰度为 255，和灰度 127）。

有了这些灰度信息，我们就可以用 image.new 创建一个新的图像，用 putpixel 函数对新图像的点进行灰度值的设置，最后用 save 函数保存聚类的灰度图像。这样你就可以看到聚类的可视化结果了，如下图所示：

上面是分割成 2 个部分的分割可视化，完整代码见这里。

https://github.com/cystanford/kmeans/blob/master/kmeans1.py

如果我们想要分割成 16 个部分，该如何对不同分类设置不同的颜色值呢？这里需要用到 skimage 工具包，它是图像处理工具包。你需要使用 pip install scikit-image 来进行安装。

这段代码可以将聚类标识矩阵转化为不同颜色的矩阵：

from skimage import color

# 将聚类标识矩阵转化为不同颜色的矩阵

label_color = (color.label2rgb(label)*255).astype(np.uint8)

label_color = label_color.transpose(1,0,2)

images = image.fromarray(label_color)

images.save('weixin_mark_color.jpg')

代码中，我使用 skimage 中的 label2rgb 函数来将 label 分类标识转化为颜色数值，因为我们的颜色值范围是 [0,255]，所以还需要乘以 255 进行转化，最后再转化为 np.uint8 类型。unit8 类型代表无符号整数，范围是 0-255 之间。

得到颜色矩阵后，你可以把它输出出来，这时你发现输出的图像是颠倒的，原因可能是图像源拍摄的时候本身是倒置的。我们需要设置三维矩阵的转置，让第一维和第二维颠倒过来，也就是使用 transpose(1,0,2)，将原来的 (0,1,2）顺序转化为 (1,0,2) 顺序，即第一维和第二维互换。

最后我们使用 fromarray 函数，它可以通过矩阵来生成图片，并使用 save 进行保存。

最后得到的分类标识颜色化图像是这样的：

完整的代码见这里。

https://github.com/cystanford/kmeans/blob/master/kmeans2.py

刚才我们做的是聚类的可视化。如果我们想要看到对应的原图，可以将每个簇（即每个类别）的点的 RGB 值设置为该簇质心点的 RGB 值，也就是簇内的点的特征均为质心点的特征。

我给出了完整的代码，代码中，我可以把范围为 0-255 的数值投射到 1-256 数值之间，方法是对每个数值进行加 1，你可以自己来运行下：

# -*- coding: utf-8 -*-

# 使用 K-means 对图像进行聚类，并显示聚类压缩后的图像

import numpy as np

import PIL.Image as image

from sklearn.cluster import KMeans

from sklearn import preprocessing

import matplotlib.image as mpimg

# 加载图像，并对数据进行规范化

def load_data(filePath):

    # 读文件

    f = open(filePath,'rb')

    data = []

    # 得到图像的像素值

    img = image.open(f)

    # 得到图像尺寸

    width, height = img.size

    for x in range(width):

        for y in range(height):

            # 得到点 (x,y) 的三个通道值

            c1, c2, c3 = img.getpixel((x, y))

            data.append([(c1+1)/256.0, (c2+1)/256.0, (c3+1)/256.0])

    f.close()

    return np.mat(data), width, height

# 加载图像，得到规范化的结果 imgData，以及图像尺寸

img, width, height = load_data('./weixin.jpg')

# 用 K-Means 对图像进行 16 聚类

kmeans =KMeans(n_clusters=16)

label = kmeans.fit_predict(img)

# 将图像聚类结果，转化成图像尺寸的矩阵

label = label.reshape([width, height])

# 创建个新图像 img，用来保存图像聚类压缩后的结果

img=image.new('RGB', (width, height))

for x in range(width):

    for y in range(height):

        c1 = kmeans.cluster_centers_[label[x, y], 0]

        c2 = kmeans.cluster_centers_[label[x, y], 1]

        c3 = kmeans.cluster_centers_[label[x, y], 2]

        img.putpixel((x, y), (int(c1*256)-1, int(c2*256)-1, int(c3*256)-1))

img.save('weixin_new.jpg')

完整代码见这里。

https://github.com/cystanford/kmeans/blob/master/kmeans3.py

你可以看到我没有用到 sklearn 自带的 MinMaxScaler，而是自己写了 Min-Max 规范化的公式。这样做的原因是我们知道 RGB 每个通道的数值在 [0,255] 之间，所以我们可以用每个通道的数值 +1/256，这样数值就会在 [0,1] 之间。

对图像做了 Min-Max 空间变换之后，还可以对其进行反变换，还原出对应原图的通道值。

对于点 (x,y)，我们找到它们所属的簇 label[x,y]，然后得到这个簇的质心特征，用 c1,c2,c3 表示：

c1 = kmeans.cluster_centers_[label[x, y], 0]

c2 = kmeans.cluster_centers_[label[x, y], 1]

c3 = kmeans.cluster_centers_[label[x, y], 2]

因为 c1, c2, c3 对应的是数据规范化的数值，因此我们还需要进行反变换，即：

c1=int(c1*256)-1

c2=int(c2*256)-1

c3=int(c3*256)-1

然后用 img.putpixel 设置点 (x,y) 反变换后得到的特征值。最后用 img.save 保存图像。

总结

今天我们用 K-Means 做了图像的分割，其实不难发现 K-Means 聚类有个缺陷：聚类个数 K 值需要事先指定。如果你不知道该聚成几类，那么最好会给 K 值多设置几个，然后选择聚类结果最好的那个值。

通过今天的图像分割，你发现用 K-Means 计算的过程在 sklearn 中就是几行代码，大部分的工作还是在预处理和后处理上。预处理是将图像进行加载，数据规范化。后处理是对聚类后的结果进行反变换。

如果涉及到后处理，你可以自己来设定数据规范化的函数，这样反变换的函数比较容易编写。

另外我们还学习了如何在 Python 中如何对图像进行读写，具体的代码如下，上文中也有相应代码，你也可以自己对应下：

import PIL.Image as image

# 得到图像的像素值

img = image.open(f)

# 得到图像尺寸

width, height = img.size

这里会使用 PIL 这个工具包，它的英文全称叫 Python Imaging Library，顾名思义，它是 Python 图像处理标准库。同时我们也使用到了 skimage 工具包（scikit-image），它也是图像处理工具包。用过 Matlab 的同学知道，Matlab 处理起图像来非常方便。skimage 可以和它相媲美，集成了很多图像处理函数，其中对不同分类标识显示不同的颜色。在 Python 中图像处理工具包，我们用的是 skimage 工具包。

这节课没有太多的理论概念，主要讲了 K-Means 聚类工具，数据规范化工具，以及图像处理工具的使用，并在图像分割中进行运用。其中涉及到的工具包比较多，你需要在练习的时候多加体会。当然不同尺寸的图像，K-Means 运行的时间也是不同的。如果图像尺寸比较大，你可以事先进行压缩，长宽在 200 像素内运行速度会比较快，如果超过了 1000 像素，速度会很慢。

今天我讲了如何使用 K-Means 聚类做图像分割，谈谈你使用的体会吧。另外我在GitHub上上传了一张 baby.jpg 的图片，请你编写代码用 K-Means 聚类方法将它分割成 16 个部分。

链接：https://github.com/cystanford/kmeans/blob/master/baby.jpg

欢迎在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

28丨EM聚类（上）：如何将一份菜等分给两个人？

今天我来带你学习 EM 聚类。EM 的英文是 Expectation Maximization，所以 EM 算法也叫最大期望算法。

我们先看一个简单的场景：假设你炒了一份菜，想要把它平均分到两个碟子里，该怎么分？

很少有人用称对菜进行称重，再计算一半的分量进行平分。大部分人的方法是先分一部分到碟子 A 中，然后再把剩余的分到碟子 B 中，再来观察碟子 A 和 B 里的菜是否一样多，哪个多就匀一些到少的那个碟子里，然后再观察碟子 A 和 B 里的是否一样多……整个过程一直重复下去，直到份量不发生变化为止。

你能从这个例子中看到三个主要的步骤：初始化参数、观察预期、重新估计。首先是先给每个碟子初始化一些菜量，然后再观察预期，这两个步骤实际上就是期望步骤（Expectation）。如果结果存在偏差就需要重新估计参数，这个就是最大化步骤（Maximization）。这两个步骤加起来也就是 EM 算法的过程。

EM 算法的工作原理

说到 EM 算法，我们先来看一个概念“最大似然”，英文是 Maximum Likelihood，Likelihood 代表可能性，所以最大似然也就是最大可能性的意思。

什么是最大似然呢？举个例子，有一男一女两个同学，现在要对他俩进行身高的比较，谁会更高呢？根据我们的经验，相同年龄下男性的平均身高比女性的高一些，所以男同学高的可能性会很大。这里运用的就是最大似然的概念。

最大似然估计是什么呢？它指的就是一件事情已经发生了，然后反推更有可能是什么因素造成的。还是用一男一女比较身高为例，假设有一个人比另一个人高，反推他可能是男性。最大似然估计是一种通过已知结果，估计参数的方法。

那么 EM 算法是什么？它和最大似然估计又有什么关系呢？EM 算法是一种求解最大似然估计的方法，通过观测样本，来找出样本的模型参数。

再回过来看下开头我给你举的分菜的这个例子，实际上最终我们想要的是碟子 A 和碟子 B 中菜的份量，你可以把它们理解为想要求得的模型参数。然后我们通过 EM 算法中的 E 步来进行观察，然后通过 M 步来进行调整 A 和 B 的参数，最后让碟子 A 和碟子 B 的参数不再发生变化为止。

实际我们遇到的问题，比分菜复杂。我再给你举个一个投掷硬币的例子，假设我们有 A 和 B 两枚硬币，我们做了 5 组实验，每组实验投掷 10 次，然后统计出现正面的次数，实验结果如下：

投掷硬币这个过程中存在隐含的数据，即我们事先并不知道每次投掷的硬币是 A 还是 B。假设我们知道这个隐含的数据，并将它完善，可以得到下面的结果：

我们现在想要求得硬币 A 和 B 出现正面次数的概率，可以直接求得：

而实际情况是我不知道每次投掷的硬币是 A 还是 B，那么如何求得硬币 A 和硬币 B 出现正面的概率呢？

这里就需要采用 EM 算法的思想。

1. 初始化参数。我们假设硬币 A 和 B 的正面概率（随机指定）是θA=0.5 和θB=0.9。

2. 计算期望值。假设实验 1 投掷的是硬币 A，那么正面次数为 5 的概率为：

公式中的 C(10,5) 代表的是 10 个里面取 5 个的组合方式，也就是排列组合公式，0.5 的 5 次方乘以 0.5 的 5 次方代表的是其中一次为 5 次为正面，5 次为反面的概率，然后再乘以 C(10,5) 等于正面次数为 5 的概率。

假设实验 1 是投掷的硬币 B ，那么正面次数为 5 的概率为：

所以实验 1 更有可能投掷的是硬币 A。

然后我们对实验 2~5 重复上面的计算过程，可以推理出来硬币顺序应该是{A，A，B，B，A}。

这个过程实际上是通过假设的参数来估计未知参数，即“每次投掷是哪枚硬币”。

3. 通过猜测的结果{A, A, B, B, A}来完善初始化的参数θA 和θB。

然后一直重复第二步和第三步，直到参数不再发生变化。

简单总结下上面的步骤，你能看出 EM 算法中的 E 步骤就是通过旧的参数来计算隐藏变量。然后在 M 步骤中，通过得到的隐藏变量的结果来重新估计参数。直到参数不再发生变化，得到我们想要的结果。

EM 聚类的工作原理

上面你能看到 EM 算法最直接的应用就是求参数估计。如果我们把潜在类别当做隐藏变量，样本看做观察值，就可以把聚类问题转化为参数估计问题。这也就是 EM 聚类的原理。

相比于 K-Means 算法，EM 聚类更加灵活，比如下面这两种情况，K-Means 会得到下面的聚类结果。

因为 K-Means 是通过距离来区分样本之间的差别的，且每个样本在计算的时候只能属于一个分类，称之为是硬聚类算法。而 EM 聚类在求解的过程中，实际上每个样本都有一定的概率和每个聚类相关，叫做软聚类算法。

你可以把 EM 算法理解成为是一个框架，在这个框架中可以采用不同的模型来用 EM 进行求解。常用的 EM 聚类有 GMM 高斯混合模型和 HMM 隐马尔科夫模型。GMM（高斯混合模型）聚类就是 EM 聚类的一种。比如上面这两个图，可以采用 GMM 来进行聚类。

和 K-Means 一样，我们事先知道聚类的个数，但是不知道每个样本分别属于哪一类。通常，我们可以假设样本是符合高斯分布的（也就是正态分布）。每个高斯分布都属于这个模型的组成部分（component），要分成 K 类就相当于是 K 个组成部分。这样我们可以先初始化每个组成部分的高斯分布的参数，然后再看来每个样本是属于哪个组成部分。这也就是 E 步骤。

再通过得到的这些隐含变量结果，反过来求每个组成部分高斯分布的参数，即 M 步骤。反复 EM 步骤，直到每个组成部分的高斯分布参数不变为止。

这样也就相当于将样本按照 GMM 模型进行了 EM 聚类。

总结

EM 算法相当于一个框架，你可以采用不同的模型来进行聚类，比如 GMM（高斯混合模型），或者 HMM（隐马尔科夫模型）来进行聚类。GMM 是通过概率密度来进行聚类，聚成的类符合高斯分布（正态分布）。而 HMM 用到了马尔可夫过程，在这个过程中，我们通过状态转移矩阵来计算状态转移的概率。HMM 在自然语言处理和语音识别领域中有广泛的应用。

在 EM 这个框架中，E 步骤相当于是通过初始化的参数来估计隐含变量。M 步骤就是通过隐含变量反推来优化参数。最后通过 EM 步骤的迭代得到模型参数。

在这个过程里用到的一些数学公式这节课不进行展开。你需要重点理解 EM 算法的原理。通过上面举的炒菜的例子，你可以知道 EM 算法是一个不断观察和调整的过程。

通过求硬币正面概率的例子，你可以理解如何通过初始化参数来求隐含数据的过程，以及再通过求得的隐含数据来优化参数。

通过上面 GMM 图像聚类的例子，你可以知道很多 K-Means 解决不了的问题，EM 聚类是可以解决的。在 EM 框架中，我们将潜在类别当做隐藏变量，样本看做观察值，把聚类问题转化为参数估计问题，最终把样本进行聚类。

最后给你留两道思考题吧，你能用自己的话说一下 EM 算法的原理吗？EM 聚类和 K-Means 聚类的相同和不同之处又有哪些？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起来交流。

29丨EM聚类（下）：用EM算法对王者荣耀英雄进行划分

今天我来带你进行 EM 的实战。上节课，我讲了 EM 算法的原理，EM 算法相当于一个聚类框架，里面有不同的聚类模型，比如 GMM 高斯混合模型，或者 HMM 隐马尔科夫模型。其中你需要理解的是 EM 的两个步骤，E 步和 M 步：E 步相当于通过初始化的参数来估计隐含变量，M 步是通过隐含变量来反推优化参数。最后通过 EM 步骤的迭代得到最终的模型参数。

今天我们进行 EM 算法的实战，你需要思考的是：

如何使用 EM 算法工具完成聚类？

什么情况下使用聚类算法？我们用聚类算法的任务目标是什么？

面对王者荣耀的英雄数据，EM 算法能帮助我们分析出什么？

如何使用 EM 工具包

在 Python 中有第三方的 EM 算法工具包。由于 EM 算法是一个聚类框架，所以你需要明确你要用的具体算法，比如是采用 GMM 高斯混合模型，还是 HMM 隐马尔科夫模型。

这节课我们主要讲解 GMM 的使用，在使用前你需要引入工具包：

from sklearn.mixture import GaussianMixture

我们看下如何在 sklearn 中创建 GMM 聚类。

首先我们使用 gmm = GaussianMixture(n_components=1, covariance_type=‘full’, max_iter=100) 来创建 GMM 聚类，其中有几个比较主要的参数（GMM 类的构造参数比较多，我筛选了一些主要的进行讲解），我分别来讲解下：

1.n_components：即高斯混合模型的个数，也就是我们要聚类的个数，默认值为 1。如果你不指定 n_components，最终的聚类结果都会为同一个值。

2.covariance_type：代表协方差类型。一个高斯混合模型的分布是由均值向量和协方差矩阵决定的，所以协方差的类型也代表了不同的高斯混合模型的特征。协方差类型有 4 种取值：

covariance_type=full，代表完全协方差，也就是元素都不为 0；

covariance_type=tied，代表相同的完全协方差；

covariance_type=diag，代表对角协方差，也就是对角不为 0，其余为 0；

covariance_type=spherical，代表球面协方差，非对角为 0，对角完全相同，呈现球面的特性。

3.max_iter：代表最大迭代次数，EM 算法是由 E 步和 M 步迭代求得最终的模型参数，这里可以指定最大迭代次数，默认值为 100。

创建完 GMM 聚类器之后，我们就可以传入数据让它进行迭代拟合。

我们使用 fit 函数，传入样本特征矩阵，模型会自动生成聚类器，然后使用 prediction=gmm.predict(data) 来对数据进行聚类，传入你想进行聚类的数据，可以得到聚类结果 prediction。

你能看出来拟合训练和预测可以传入相同的特征矩阵，这是因为聚类是无监督学习，你不需要事先指定聚类的结果，也无法基于先验的结果经验来进行学习。只要在训练过程中传入特征值矩阵，机器就会按照特征值矩阵生成聚类器，然后就可以使用这个聚类器进行聚类了。

如何用 EM 算法对王者荣耀数据进行聚类

了解了 GMM 聚类工具之后，我们看下如何对王者荣耀的英雄数据进行聚类。

首先我们知道聚类的原理是“人以群分，物以类聚”。通过聚类算法把特征值相近的数据归为一类，不同类之间的差异较大，这样就可以对原始数据进行降维。通过分成几个组（簇），来研究每个组之间的特性。或者我们也可以把组（簇）的数量适当提升，这样就可以找到可以互相替换的英雄，比如你的对手选择了你擅长的英雄之后，你可以选择另一个英雄作为备选。

我们先看下数据长什么样子：

这里我们收集了 69 名英雄的 20 个特征属性，这些属性分别是最大生命、生命成长、初始生命、最大法力、法力成长、初始法力、最高物攻、物攻成长、初始物攻、最大物防、物防成长、初始物防、最大每 5 秒回血、每 5 秒回血成长、初始每 5 秒回血、最大每 5 秒回蓝、每 5 秒回蓝成长、初始每 5 秒回蓝、最大攻速和攻击范围等。

具体的数据集你可以在 GitHub 上下载：https://github.com/cystanford/EM_data。

现在我们需要对王者荣耀的英雄数据进行聚类，我们先设定项目的执行流程：

首先我们需要加载数据源；

在准备阶段，我们需要对数据进行探索，包括采用数据可视化技术，让我们对英雄属性以及这些属性之间的关系理解更加深刻，然后对数据质量进行评估，是否进行数据清洗，最后进行特征选择方便后续的聚类算法；

聚类阶段：选择适合的聚类模型，这里我们采用 GMM 高斯混合模型进行聚类，并输出聚类结果，对结果进行分析。

按照上面的步骤，我们来编写下代码。完整的代码如下：

# -*- coding: utf-8 -*-

import pandas as pd

import csv

import matplotlib.pyplot as plt

import seaborn as sns

from sklearn.mixture import GaussianMixture

from sklearn.preprocessing import StandardScaler

# 数据加载，避免中文乱码问题

data_ori = pd.read_csv('./heros7.csv', encoding = 'gb18030')

features = [u'最大生命',u'生命成长',u'初始生命',u'最大法力', u'法力成长',u'初始法力',u'最高物攻',u'物攻成长',u'初始物攻',u'最大物防',u'物防成长',u'初始物防', u'最大每 5 秒回血', u'每 5 秒回血成长', u'初始每 5 秒回血', u'最大每 5 秒回蓝', u'每 5 秒回蓝成长', u'初始每 5 秒回蓝', u'最大攻速', u'攻击范围']

data = data_ori[features]

# 对英雄属性之间的关系进行可视化分析

# 设置 plt 正确显示中文

plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签

plt.rcParams['axes.unicode_minus']=False # 用来正常显示负号

# 用热力图呈现 features_mean 字段之间的相关性

corr = data[features].corr()

plt.figure(figsize=(14,14))

# annot=True 显示每个方格的数据

sns.heatmap(corr, annot=True)

plt.show()

# 相关性大的属性保留一个，因此可以对属性进行降维

features_remain = [u'最大生命', u'初始生命', u'最大法力', u'最高物攻', u'初始物攻', u'最大物防', u'初始物防', u'最大每 5 秒回血', u'最大每 5 秒回蓝', u'初始每 5 秒回蓝', u'最大攻速', u'攻击范围']

data = data_ori[features_remain]

data[u'最大攻速'] = data[u'最大攻速'].apply(lambda x: float(x.strip('%'))/100)

data[u'攻击范围']=data[u'攻击范围'].map({'远程':1,'近战':0})

# 采用 Z-Score 规范化数据，保证每个特征维度的数据均值为 0，方差为 1

ss = StandardScaler()

data = ss.fit_transform(data)

# 构造 GMM 聚类

gmm = GaussianMixture(n_components=30, covariance_type='full')

gmm.fit(data)

# 训练数据

prediction = gmm.predict(data)

print(prediction)

# 将分组结果输出到 CSV 文件中

data_ori.insert(0, '分组', prediction)

data_ori.to_csv('./hero_out.csv', index=False, sep=',')

运行结果如下：

[28 14  8  9  5  5 15  8  3 14 18 14  9  7 16 18 13  3  5  4 19 12  4 12

 12 12  4 17 24  2  7  2  2 24  2  2 24  6 20 22 22 24 24  2  2 22 14 20

 14 24 26 29 27 25 25 28 11  1 23  5 11  0 10 28 21 29 29 29 17]

同时你也能看到输出的聚类结果文件 hero_out.csv（它保存在你本地运行的文件夹里，程序会自动输出这个文件，你可以自己看下）。

我来简单讲解下程序的几个模块。

关于引用包

首先我们会用 DataFrame 数据结构来保存读取的数据，最后的聚类结果会写入到 CSV 文件中，因此会用到 pandas 和 CSV 工具包。另外我们需要对数据进行可视化，采用热力图展现属性之间的相关性，这里会用到 matplotlib.pyplot 和 seaborn 工具包。在数据规范化中我们使用到了 Z-Score 规范化，用到了 StandardScaler 类，最后我们还会用到 sklearn 中的 GaussianMixture 类进行聚类。

数据可视化的探索

你能看到我们将 20 个英雄属性之间的关系用热力图呈现了出来，中间的数字代表两个属性之间的关系系数，最大值为 1，代表完全正相关，关系系数越大代表相关性越大。从图中你能看出来“最大生命”“生命成长”和“初始生命”这三个属性的相关性大，我们只需要保留一个属性即可。同理我们也可以对其他相关性大的属性进行筛选，保留一个。你在代码中可以看到，我用 features_remain 数组保留了特征选择的属性，这样就将原本的 20 个属性降维到了 13 个属性。

关于数据规范化

我们能看到“最大攻速”这个属性值是百分数，不适合做矩阵运算，因此我们需要将百分数转化为小数。我们也看到“攻击范围”这个字段的取值为远程或者近战，也不适合矩阵运算，我们将取值做个映射，用 1 代表远程，0 代表近战。然后采用 Z-Score 规范化，对特征矩阵进行规范化。

在聚类阶段

我们采用了 GMM 高斯混合模型，并将结果输出到 CSV 文件中。

这里我将输出的结果截取了一段（设置聚类个数为 30）：

第一列代表的是分组（簇），我们能看到张飞、程咬金分到了一组，牛魔、白起是一组，老夫子自己是一组，达摩、典韦是一组。聚类的特点是相同类别之间的属性值相近，不同类别的属性值差异大。因此如果你擅长用典韦这个英雄，不防试试达摩这个英雄。同样你也可以在张飞和程咬金中进行切换。这样就算你的英雄被别人选中了，你依然可以有备选的英雄可以使用。

总结

今天我带你一起做了 EM 聚类的实战，具体使用的是 GMM 高斯混合模型。从整个流程中可以看出，我们需要经过数据加载、数据探索、数据可视化、特征选择、GMM 聚类和结果分析等环节。

聚类和分类不一样，聚类是无监督的学习方式，也就是我们没有实际的结果可以进行比对，所以聚类的结果评估不像分类准确率一样直观，那么有没有聚类结果的评估方式呢？这里我们可以采用 Calinski-Harabaz 指标，代码如下：

from sklearn.metrics import calinski_harabaz_score

print(calinski_harabaz_score(data, prediction))

指标分数越高，代表聚类效果越好，也就是相同类中的差异性小，不同类之间的差异性大。当然具体聚类的结果含义，我们需要人工来分析，也就是当这些数据被分成不同的类别之后，具体每个类表代表的含义。

另外聚类算法也可以作为其他数据挖掘算法的预处理阶段，这样我们就可以将数据进行降维了。

最后依然是两道思考题。针对王者荣耀的英雄数据集，我进行了特征选择，实际上王者荣耀的英雄数量并不多，我们可以省略特征选择这个阶段，你不妨用全部的特征值矩阵进行聚类训练，来看下聚类得到的结果。第二个问题是，依然用王者荣耀英雄数据集，在聚类个数为 3 以及聚类个数为 30 的情况下，请你使用 GMM 高斯混合模型对数据集进行聚类，并得出 Calinski_Harabaz 分数。

欢迎在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

30丨关联规则挖掘（上）：如何用Apriori发现用户购物规则？

今天我来带你进行关联规则挖掘的学习，关联规则这个概念，最早是由 Agrawal 等人在 1993 年提出的。在 1994 年 Agrawal 等人又提出了基于关联规则的 Apriori 算法，至今 Apriori 仍是关联规则挖掘的重要算法。

关联规则挖掘可以让我们从数据集中发现项与项（item 与 item）之间的关系，它在我们的生活中有很多应用场景，“购物篮分析”就是一个常见的场景，这个场景可以从消费者交易记录中发掘商品与商品之间的关联关系，进而通过商品捆绑销售或者相关推荐的方式带来更多的销售量。所以说，关联规则挖掘是个非常有用的技术。

在今天的内容中，希望你能带着问题，和我一起来搞懂以下几个知识点：

搞懂关联规则中的几个重要概念：支持度、置信度、提升度；

Apriori 算法的工作原理；

在实际工作中，我们该如何进行关联规则挖掘。

搞懂关联规则中的几个概念

我举一个超市购物的例子，下面是几名客户购买的商品列表：

什么是支持度呢？

支持度是个百分比，它指的是某个商品组合出现的次数与总次数之间的比例。支持度越高，代表这个组合出现的频率越大。

在这个例子中，我们能看到“牛奶”出现了 4 次，那么这 5 笔订单中“牛奶”的支持度就是 4/5=0.8。

同样“牛奶 + 面包”出现了 3 次，那么这 5 笔订单中“牛奶 + 面包”的支持度就是 3/5=0.6。

什么是置信度呢？

它指的就是当你购买了商品 A，会有多大的概率购买商品 B，在上面这个例子中：

置信度（牛奶→啤酒）=2/4=0.5，代表如果你购买了牛奶，有多大的概率会购买啤酒？

置信度（啤酒→牛奶）=2/3=0.67，代表如果你购买了啤酒，有多大的概率会购买牛奶？

我们能看到，在 4 次购买了牛奶的情况下，有 2 次购买了啤酒，所以置信度 (牛奶→啤酒)=0.5，而在 3 次购买啤酒的情况下，有 2 次购买了牛奶，所以置信度（啤酒→牛奶）=0.67。

所以说置信度是个条件概念，就是说在 A 发生的情况下，B 发生的概率是多少。

什么是提升度呢？

我们在做商品推荐的时候，重点考虑的是提升度，因为提升度代表的是“商品 A 的出现，对商品 B 的出现概率提升的”程度。

还是看上面的例子，如果我们单纯看置信度 (可乐→尿布)=1，也就是说可乐出现的时候，用户都会购买尿布，那么当用户购买可乐的时候，我们就需要推荐尿布么？

实际上，就算用户不购买可乐，也会直接购买尿布的，所以用户是否购买可乐，对尿布的提升作用并不大。我们可以用下面的公式来计算商品 A 对商品 B 的提升度：

提升度 (A→B)= 置信度 (A→B)/ 支持度 (B)

这个公式是用来衡量 A 出现的情况下，是否会对 B 出现的概率有所提升。

所以提升度有三种可能：

提升度 (A→B)>1：代表有提升；

提升度 (A→B)=1：代表有没有提升，也没有下降；

提升度 (A→B)<1：代表有下降。

Apriori 的工作原理

明白了关联规则中支持度、置信度和提升度这几个重要概念，我们来看下 Apriori 算法是如何工作的。

首先我们把上面案例中的商品用 ID 来代表，牛奶、面包、尿布、可乐、啤酒、鸡蛋的商品 ID 分别设置为 1-6，上面的数据表可以变为：

Apriori 算法其实就是查找频繁项集 (frequent itemset) 的过程，所以首先我们需要定义什么是频繁项集。

频繁项集就是支持度大于等于最小支持度 (Min Support) 阈值的项集，所以小于最小值支持度的项目就是非频繁项集，而大于等于最小支持度的项集就是频繁项集。

项集这个概念，英文叫做 itemset，它可以是单个的商品，也可以是商品的组合。我们再来看下这个例子，假设我随机指定最小支持度是 50%，也就是 0.5。

我们来看下 Apriori 算法是如何运算的。

首先，我们先计算单个商品的支持度，也就是得到 K=1 项的支持度：

因为最小支持度是 0.5，所以你能看到商品 4、6 是不符合最小支持度的，不属于频繁项集，于是经过筛选商品的频繁项集就变成：

在这个基础上，我们将商品两两组合，得到 k=2 项的支持度：

我们再筛掉小于最小值支持度的商品组合，可以得到：

我们再将商品进行 K=3 项的商品组合，可以得到：

再筛掉小于最小值支持度的商品组合，可以得到：

通过上面这个过程，我们可以得到 K=3 项的频繁项集{1,2,3}，也就是{牛奶、面包、尿布}的组合。

到这里，你已经和我模拟了一遍整个 Apriori 算法的流程，下面我来给你总结下 Apriori 算法的递归流程：

K=1，计算 K 项集的支持度；

筛选掉小于最小支持度的项集；

如果项集为空，则对应 K-1 项集的结果为最终结果。

否则 K=K+1，重复 1-3 步。

Apriori 的改进算法：FP-Growth 算法

我们刚完成了 Apriori 算法的模拟，你能看到 Apriori 在计算的过程中有以下几个缺点：

可能产生大量的候选集。因为采用排列组合的方式，把可能的项集都组合出来了；

每次计算都需要重新扫描数据集，来计算每个项集的支持度。

所以 Apriori 算法会浪费很多计算空间和计算时间，为此人们提出了 FP-Growth 算法，它的特点是：

创建了一棵 FP 树来存储频繁项集。在创建前对不满足最小支持度的项进行删除，减少了存储空间。我稍后会讲解如何构造一棵 FP 树；

整个生成过程只遍历数据集 2 次，大大减少了计算量。

所以在实际工作中，我们常用 FP-Growth 来做频繁项集的挖掘，下面我给你简述下 FP-Growth 的原理。

1. 创建项头表（item header table）

创建项头表的作用是为 FP 构建及频繁项集挖掘提供索引。

这一步的流程是先扫描一遍数据集，对于满足最小支持度的单个项（K=1 项集）按照支持度从高到低进行排序，这个过程中删除了不满足最小支持度的项。

项头表包括了项目、支持度，以及该项在 FP 树中的链表。初始的时候链表为空。

2. 构造 FP 树

FP 树的根节点记为 NULL 节点。

整个流程是需要再次扫描数据集，对于每一条数据，按照支持度从高到低的顺序进行创建节点（也就是第一步中项头表中的排序结果），节点如果存在就将计数 count+1，如果不存在就进行创建。同时在创建的过程中，需要更新项头表的链表。

3. 通过 FP 树挖掘频繁项集

到这里，我们就得到了一个存储频繁项集的 FP 树，以及一个项头表。我们可以通过项头表来挖掘出每个频繁项集。

具体的操作会用到一个概念，叫“条件模式基”，它指的是以要挖掘的节点为叶子节点，自底向上求出 FP 子树，然后将 FP 子树的祖先节点设置为叶子节点之和。

我以“啤酒”的节点为例，从 FP 树中可以得到一棵 FP 子树，将祖先节点的支持度记为叶子节点之和，得到：

你能看出来，相比于原来的 FP 树，尿布和牛奶的频繁项集数减少了。这是因为我们求得的是以“啤酒”为节点的 FP 子树，也就是说，在频繁项集中一定要含有“啤酒”这个项。你可以再看下原始的数据，其中订单 1{牛奶、面包、尿布}和订单 5{牛奶、面包、尿布、可乐}并不存在“啤酒”这个项，所以针对订单 1，尿布→牛奶→面包这个项集就会从 FP 树中去掉，针对订单 5 也包括了尿布→牛奶→面包这个项集也会从 FP 树中去掉，所以你能看到以“啤酒”为节点的 FP 子树，尿布、牛奶、面包项集上的计数比原来少了 2。

条件模式基不包括“啤酒”节点，而且祖先节点如果小于最小支持度就会被剪枝，所以“啤酒”的条件模式基为空。

同理，我们可以求得“面包”的条件模式基为：

所以可以求得面包的频繁项集为{尿布，面包}，{尿布，牛奶，面包}。同样，我们还可以求得牛奶，尿布的频繁项集，这里就不再计算展示。

总结

今天我给你讲了 Apriori 算法，它是在“购物篮分析”中常用的关联规则挖掘算法，在 Apriori 算法中你最主要是需要明白支持度、置信度、提升度这几个概念，以及 Apriori 迭代计算频繁项集的工作流程。

Apriori 算法在实际工作中需要对数据集扫描多次，会消耗大量的计算时间，所以在 2000 年 FP-Growth 算法被提出来，它只需要扫描两次数据集即可以完成关联规则的挖掘。FP-Growth 算法最主要的贡献就是提出了 FP 树和项头表，通过 FP 树减少了频繁项集的存储以及计算时间。

当然 Apriori 的改进算法除了 FP-Growth 算法以外，还有 CBA 算法、GSP 算法，这里就不进行介绍。

你能发现一种新理论的提出，往往是先从最原始的概念出发，提出一种新的方法。原始概念最接近人们模拟的过程，但往往会存在空间和时间复杂度过高的情况。所以后面其他人会对这个方法做改进型的创新，重点是在空间和时间复杂度上进行降维，比如采用新型的数据结构。你能看出树在存储和检索中是一个非常好用的数据结构。

最后给你留两道思考题吧，你能说一说 Apriori 的工作原理吗？相比于 Apriori，FP-Growth 算法都有哪些改进？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事，一起来学习。

31丨关联规则挖掘（下）：导演如何选择演员？

上次我给你讲了关联规则挖掘的原理。关联规则挖掘在生活中有很多使用场景，不仅是商品的捆绑销售，甚至在挑选演员决策上，你也能通过关联规则挖掘看出来某个导演选择演员的倾向。

今天我来带你用 Apriori 算法做一个项目实战。你需要掌握的是以下几点：

熟悉上节课讲到的几个重要概念：支持度、置信度和提升度；

熟悉与掌握 Apriori 工具包的使用；

在实际问题中，灵活运用。包括数据集的准备等。

如何使用 Apriori 工具包

Apriori 虽然是十大算法之一，不过在 sklearn 工具包中并没有它，也没有 FP-Growth 算法。这里教你个方法，来选择 Python 中可以使用的工具包，你可以通过https://pypi.org/ 搜索工具包。

这个网站提供的工具包都是 Python 语言的，你能找到 8 个 Python 语言的 Apriori 工具包，具体选择哪个呢？建议你使用第二个工具包，即 efficient-apriori。后面我会讲到为什么推荐这个工具包。

首先你需要通过 pip install efficient-apriori 安装这个工具包。

然后看下如何使用它，核心的代码就是这一行：

itemsets, rules = apriori(data, min_support,  min_confidence)

其中 data 是我们要提供的数据集，它是一个 list 数组类型。min_support 参数为最小支持度，在 efficient-apriori 工具包中用 0 到 1 的数值代表百分比，比如 0.5 代表最小支持度为 50%。min_confidence 是最小置信度，数值也代表百分比，比如 1 代表 100%。

关于支持度、置信度和提升度，我们再来简单回忆下。

支持度指的是某个商品组合出现的次数与总次数之间的比例。支持度越高，代表这个组合出现的概率越大。

置信度是一个条件概念，就是在 A 发生的情况下，B 发生的概率是多少。

提升度代表的是“商品 A 的出现，对商品 B 的出现概率提升了多少”。

接下来我们用这个工具包，跑一下上节课中讲到的超市购物的例子。下面是客户购买的商品列表：

具体实现的代码如下：

from efficient_apriori import apriori

# 设置数据集

data = [('牛奶','面包','尿布'),

           ('可乐','面包', '尿布', '啤酒'),

           ('牛奶','尿布', '啤酒', '鸡蛋'),

           ('面包', '牛奶', '尿布', '啤酒'),

           ('面包', '牛奶', '尿布', '可乐')]

# 挖掘频繁项集和频繁规则

itemsets, rules = apriori(data, min_support=0.5,  min_confidence=1)

print(itemsets)

print(rules)

运行结果：

{1: {('啤酒',): 3, ('尿布',): 5, ('牛奶',): 4, ('面包',): 4}, 2: {('啤酒', '尿布'): 3, ('尿布', '牛奶'): 4, ('尿布', '面包'): 4, ('牛奶', '面包'): 3}, 3: {('尿布', '牛奶', '面包'): 3}}

[{啤酒} -> {尿布}, {牛奶} -> {尿布}, {面包} -> {尿布}, {牛奶, 面包} -> {尿布}]

你能从代码中看出来，data 是个 List 数组类型，其中每个值都可以是一个集合。实际上你也可以把 data 数组中的每个值设置为 List 数组类型，比如：

data = [['牛奶','面包','尿布'],

           ['可乐','面包', '尿布', '啤酒'],

           ['牛奶','尿布', '啤酒', '鸡蛋'],

           ['面包', '牛奶', '尿布', '啤酒'],

           ['面包', '牛奶', '尿布', '可乐']]

两者的运行结果是一样的，efficient-apriori 工具包把每一条数据集里的项式都放到了一个集合中进行运算，并没有考虑它们之间的先后顺序。因为实际情况下，同一个购物篮中的物品也不需要考虑购买的先后顺序。

而其他的 Apriori 算法可能会因为考虑了先后顺序，出现计算频繁项集结果不对的情况。所以这里采用的是 efficient-apriori 这个工具包。

挖掘导演是如何选择演员的

在实际工作中，数据集是需要自己来准备的，比如今天我们要挖掘导演是如何选择演员的数据情况，但是并没有公开的数据集可以直接使用。因此我们需要使用之前讲到的 Python 爬虫进行数据采集。

不同导演选择演员的规则是不同的，因此我们需要先指定导演。数据源我们选用豆瓣电影。

先来梳理下采集的工作流程。

首先我们先在https://movie.douban.com搜索框中输入导演姓名，比如“宁浩”。

页面会呈现出来导演之前的所有电影，然后对页面进行观察，你能观察到以下几个现象：

页面默认是 15 条数据反馈，第一页会返回 16 条。因为第一条数据实际上这个导演的概览，你可以理解为是一条广告的插入，下面才是真正的返回结果。

每条数据的最后一行是电影的演出人员的信息，第一个人员是导演，其余为演员姓名。姓名之间用“/”分割。

有了这些观察之后，我们就可以编写抓取程序了。在代码讲解中你能看出这两点观察的作用。抓取程序的目的是为了生成宁浩导演（你也可以抓取其他导演）的数据集，结果会保存在 csv 文件中。完整的抓取代码如下：

# -*- coding: utf-8 -*-

# 下载某个导演的电影数据集

from efficient_apriori import apriori

from lxml import etree

import time

from selenium import webdriver

import csv

driver = webdriver.Chrome()

# 设置想要下载的导演 数据集

director = u'宁浩'

# 写 CSV 文件

file_name = './' + director + '.csv'

base_url = 'https://movie.douban.com/subject_search?search_text='+director+'&cat=1002&start='

out = open(file_name,'w', newline='', encoding='utf-8-sig')

csv_write = csv.writer(out, dialect='excel')

flags=[]

# 下载指定页面的数据

def download(request_url):

	driver.get(request_url)

	time.sleep(1)

	html = driver.find_element_by_xpath("//*").get_attribute("outerHTML")

	html = etree.HTML(html)

	# 设置电影名称，导演演员 的 XPATH

	movie_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='title']/a[@class='title-text']")

	name_lists = html.xpath("/html/body/div[@id='wrapper']/div[@id='root']/div[1]//div[@class='item-root']/div[@class='detail']/div[@class='meta abstract_2']")

	# 获取返回的数据个数

	num = len(movie_lists)

	if num > 15: # 第一页会有 16 条数据

		# 默认第一个不是，所以需要去掉

		movie_lists = movie_lists[1:]

		name_lists = name_lists[1:]

	for (movie, name_list) in zip(movie_lists, name_lists):

		# 会存在数据为空的情况

		if name_list.text is None:

			continue

		# 显示下演员名称

		print(name_list.text)

		names = name_list.text.split('/')

		# 判断导演是否为指定的 director

		if names[0].strip() == director and movie.text not in flags:

			# 将第一个字段设置为电影名称

			names[0] = movie.text

			flags.append(movie.text)

			csv_write.writerow(names)

	print('OK') # 代表这页数据下载成功

	print(num)

	if num >= 14: # 有可能一页会有 14 个电影

		# 继续下一页

		return True

	else:

		# 没有下一页

		return False

# 开始的 ID 为 0，每页增加 15

start = 0

while start<10000: # 最多抽取 1 万部电影

	request_url = base_url + str(start)

	# 下载数据，并返回是否有下一页

	flag = download(request_url)

	if flag:

		start = start + 15

	else:

		break

out.close()

print('finished')

代码中涉及到了几个模块，我简单讲解下这几个模块。

在引用包这一段，我们使用 csv 工具包读写 CSV 文件，用 efficient_apriori 完成 Apriori 算法，用 lxml 进行 XPath 解析，time 工具包可以让我们在模拟后有个适当停留，代码中我设置为 1 秒钟，等 HTML 数据完全返回后再进行 HTML 内容的获取。使用 selenium 的 webdriver 来模拟浏览器的行为。

在读写文件这一块，我们需要事先告诉 python 的 open 函数，文件的编码是 utf-8-sig（对应代码：encoding=‘utf-8-sig’），这是因为我们会用到中文，为了避免编码混乱。

编写 download 函数，参数传入我们要采集的页面地址（request_url）。针对返回的 HTML，我们需要用到之前讲到的 Chrome 浏览器的 XPath Helper 工具，来获取电影名称以及演出人员的 XPath。我用页面返回的数据个数来判断当前所处的页面序号。如果数据个数 >15，也就是第一页，第一页的第一条数据是广告，我们需要忽略。如果数据个数 =15，代表是中间页，需要点击“下一页”，也就是翻页。如果数据个数 <15，代表最后一页，没有下一页。

在程序主体部分，我们设置 start 代表抓取的 ID，从 0 开始最多抓取 1 万部电影的数据（一个导演不会超过 1 万部电影），每次翻页 start 自动增加 15，直到 flag=False 为止，也就是不存在下一页的情况。

你可以模拟下抓取的流程，获得指定导演的数据，比如我上面抓取的宁浩的数据。这里需要注意的是，豆瓣的电影数据可能是不全的，但基本上够我们用。

有了数据之后，我们就可以用 Apriori 算法来挖掘频繁项集和关联规则，代码如下：

# -*- coding: utf-8 -*-

from efficient_apriori import apriori

import csv

director = u'宁浩'

file_name = './'+director+'.csv'

lists = csv.reader(open(file_name, 'r', encoding='utf-8-sig'))

# 数据加载

data = []

for names in lists:

     name_new = []

     for name in names:

           # 去掉演员数据中的空格

           name_new.append(name.strip())

     data.append(name_new[1:])

# 挖掘频繁项集和关联规则

itemsets, rules = apriori(data, min_support=0.5,  min_confidence=1)

print(itemsets)

print(rules)

代码中使用的 apriori 方法和开头中用 Apriori 获取购物篮规律的方法类似，比如代码中都设定了最小支持度和最小置信系数，这样我们可以找到支持度大于 50%，置信系数为 1 的频繁项集和关联规则。

这是最后的运行结果：

{1: {('徐峥',): 5, ('黄渤',): 6}, 2: {('徐峥', '黄渤'): 5}}

[{徐峥} -> {黄渤}]

你能看出来，宁浩导演喜欢用徐峥和黄渤，并且有徐峥的情况下，一般都会用黄渤。你也可以用上面的代码来挖掘下其他导演选择演员的规律。

总结

Apriori 算法的核心就是理解频繁项集和关联规则。在算法运算的过程中，还要重点掌握对支持度、置信度和提升度的理解。在工具使用上，你可以使用 efficient-apriori 这个工具包，它会把每一条数据中的项（item）放到一个集合（篮子）里来处理，不考虑项（item）之间的先后顺序。

在实际运用中你还需要灵活处理，比如导演如何选择演员这个案例，虽然工具的使用会很方便，但重要的还是数据挖掘前的准备过程，也就是获取某个导演的电影数据集。

最后给你留两道思考题吧。请你编写代码挖掘下张艺谋导演使用演员的频繁项集和关联规则，最小支持度可以设置为 0.1 或 0.05。另外你认为 Apriori 算法中的最小支持度和最小置信度，一般设置为多少比较合理？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

32丨PageRank（上）：搞懂Google的PageRank算法

互联网发展到现在，搜索引擎已经非常好用，基本上输入关键词，都能找到匹配的内容，质量还不错。但在 1998 年之前，搜索引擎的体验并不好。早期的搜索引擎，会遇到下面的两类问题：

返回结果质量不高：搜索结果不考虑网页的质量，而是通过时间顺序进行检索；

容易被人钻空子：搜索引擎是基于检索词进行检索的，页面中检索词出现的频次越高，匹配度越高，这样就会出现网页作弊的情况。有些网页为了增加搜索引擎的排名，故意增加某个检索词的频率。

基于这些缺陷，当时 Google 的创始人拉里·佩奇提出了 PageRank 算法，目的就是要找到优质的网页，这样 Google 的排序结果不仅能找到用户想要的内容，而且还会从众多网页中筛选出权重高的呈现给用户。

Google 的两位创始人都是斯坦福大学的博士生，他们提出的 PageRank 算法受到了论文影响力因子的评价启发。当一篇论文被引用的次数越多，证明这篇论文的影响力越大。正是这个想法解决了当时网页检索质量不高的问题。

PageRank 的简化模型

我们先来看下 PageRank 是如何计算的。

我假设一共有 4 个网页 A、B、C、D。它们之间的链接信息如图所示：

这里有两个概念你需要了解一下。

出链指的是链接出去的链接。入链指的是链接进来的链接。比如图中 A 有 2 个入链，3 个出链。

简单来说，一个网页的影响力 = 所有入链集合的页面的加权影响力之和，用公式表示为：

u 为待评估的页面，BuBu 为页面 u 的入链集合。针对入链集合中的任意页面 v，它能给 u 带来的影响力是其自身的影响力 PR(v) 除以 v 页面的出链数量，即页面 v 把影响力 PR(v) 平均分配给了它的出链，这样统计所有能给 u 带来链接的页面 v，得到的总和就是网页 u 的影响力，即为 PR(u)。

所以你能看到，出链会给被链接的页面赋予影响力，当我们统计了一个网页链出去的数量，也就是统计了这个网页的跳转概率。

在这个例子中，你能看到 A 有三个出链分别链接到了 B、C、D 上。那么当用户访问 A 的时候，就有跳转到 B、C 或者 D 的可能性，跳转概率均为 1/3。

B 有两个出链，链接到了 A 和 D 上，跳转概率为 1/2。

这样，我们可以得到 A、B、C、D 这四个网页的转移矩阵 M：

我们假设 A、B、C、D 四个页面的初始影响力都是相同的，即：

当进行第一次转移之后，各页面的影响力 w1w1 变为：

然后我们再用转移矩阵乘以 w1w1 得到 w2w2 结果，直到第 n 次迭代后 wnwn 影响力不再发生变化，可以收敛到 (0.3333，0.2222，0.2222，0.2222），也就是对应着 A、B、C、D 四个页面最终平衡状态下的影响力。

你能看出 A 页面相比于其他页面来说权重更大，也就是 PR 值更高。而 B、C、D 页面的 PR 值相等。

至此，我们模拟了一个简化的 PageRank 的计算过程，实际情况会比这个复杂，可能会面临两个问题：

1. 等级泄露（Rank Leak）：如果一个网页没有出链，就像是一个黑洞一样，吸收了其他网页的影响力而不释放，最终会导致其他网页的 PR 值为 0。

2. 等级沉没（Rank Sink）：如果一个网页只有出链，没有入链（如下图所示），计算的过程迭代下来，会导致这个网页的 PR 值为 0（也就是不存在公式中的 V）。

针对等级泄露和等级沉没的情况，我们需要灵活处理。

比如针对等级泄露的情况，我们可以把没有出链的节点，先从图中去掉，等计算完所有节点的 PR 值之后，再加上该节点进行计算。不过这种方法会导致新的等级泄露的节点的产生，所以工作量还是很大的。

有没有一种方法，可以同时解决等级泄露和等级沉没这两个问题呢？

PageRank 的随机浏览模型

为了解决简化模型中存在的等级泄露和等级沉没的问题，拉里·佩奇提出了 PageRank 的随机浏览模型。他假设了这样一个场景：用户并不都是按照跳转链接的方式来上网，还有一种可能是不论当前处于哪个页面，都有概率访问到其他任意的页面，比如说用户就是要直接输入网址访问其他页面，虽然这个概率比较小。

所以他定义了阻尼因子 d，这个因子代表了用户按照跳转链接来上网的概率，通常可以取一个固定值 0.85，而 1-d=0.15 则代表了用户不是通过跳转链接的方式来访问网页的，比如直接输入网址。

其中 N 为网页总数，这样我们又可以重新迭代网页的权重计算了，因为加入了阻尼因子 d，一定程度上解决了等级泄露和等级沉没的问题。

通过数学定理（这里不进行讲解）也可以证明，最终 PageRank 随机浏览模型是可以收敛的，也就是可以得到一个稳定正常的 PR 值。

PageRank 在社交影响力评估中的应用

网页之间会形成一个网络，是我们的互联网，论文之间也存在着相互引用的关系，可以说我们所处的环境就是各种网络的集合。

只要是有网络的地方，就存在出链和入链，就会有 PR 权重的计算，也就可以运用我们今天讲的 PageRank 算法。

我们可以把 PageRank 算法延展到社交网络领域中。比如在微博上，如果我们想要计算某个人的影响力，该怎么做呢？

一个人的微博粉丝数并不一定等于他的实际影响力。如果按照 PageRank 算法，还需要看这些粉丝的质量如何。如果有很多明星或者大 V 关注，那么这个人的影响力一定很高。如果粉丝是通过购买僵尸粉得来的，那么即使粉丝数再多，影响力也不高。

同样，在工作场景中，比如说脉脉这个社交软件，它计算的就是个人在职场的影响力。如果你的工作关系是李开复、江南春这样的名人，那么你的职场影响力一定会很高。反之，如果你是个学生，在职场上被链入的关系比较少的话，职场影响力就会比较低。

同样，如果你想要看一个公司的经营能力，也可以看这家公司都和哪些公司有合作。如果它合作的都是世界 500 强企业，那么这个公司在行业内一定是领导者，如果这个公司的客户都是小客户，即使数量比较多，业内影响力也不一定大。

除非像淘宝一样，有海量的中小客户，最后大客户也会找上门来寻求合作。所以权重高的节点，往往会有一些权重同样很高的节点在进行合作。

PageRank 给我们带来的启发

PageRank 可以说是 Google 搜索引擎重要的技术之一，在 1998 年帮助 Google 获得了搜索引擎的领先优势，现在 PageRank 已经比原来复杂很多，但它的思想依然能带给我们很多启发。

比如，如果你想要自己的媒体影响力有所提高，就尽量要混在大 V 圈中；如果想找到高职位的工作，就尽量结识公司高层，或者认识更多的猎头，因为猎头和很多高职位的人员都有链接关系。

同样，PageRank 也可以帮我们识别链接农场。链接农场指的是网页为了链接而链接，填充了一些没有用的内容。这些页面相互链接或者指向了某一个网页，从而想要得到更高的权重。

总结

今天我给你讲了 PageRank 的算法原理，对简化的 PageRank 模型进行了模拟。针对简化模型中存在的等级泄露和等级沉没这两个问题，PageRank 的随机浏览模型引入了阻尼因子 d 来解决。

同样，PageRank 有很广的应用领域，在许多网络结构中都有应用，比如计算一个人的微博影响力等。它也告诉我们，在社交网络中，链接的质量非常重要。

学完今天的内容，你不妨说说 PageRank 的算法原理？另外在现实生活中，除了我在文中举到的几个例子，你还能说一些 PageRank 都有哪些应用场景吗？

欢迎在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

33丨PageRank（下）：分析希拉里邮件中的人物关系

上节课我们讲到 PageRank 算法经常被用到网络关系的分析中，比如在社交网络中计算个人的影响力，计算论文的影响力或者网站的影响力等。

今天我们就来做一个关于 PageRank 算法的实战，在这之前，你需要思考三个问题：

如何使用工具完成 PageRank 算法，包括使用工具创建网络图，设置节点、边、权重等，并通过创建好的网络图计算节点的 PR 值；

对于一个实际的项目，比如希拉里的 9306 封邮件（工具包中邮件的数量），如何使用 PageRank 算法挖掘出有影响力的节点，并且绘制网络图；

如何对创建好的网络图进行可视化，如果网络中的节点数较多，如何筛选重要的节点进行可视化，从而得到精简的网络关系图。

如何使用工具实现 PageRank 算法

PageRank 算法工具在 sklearn 中并不存在，我们需要找到新的工具包。实际上有一个关于图论和网络建模的工具叫 NetworkX，它是用 Python 语言开发的工具，内置了常用的图与网络分析算法，可以方便我们进行网络数据分析。

上节课，我举了一个网页权重的例子，假设一共有 4 个网页 A、B、C、D，它们之间的链接信息如图所示：

针对这个例子，我们看下用 NetworkX 如何计算 A、B、C、D 四个网页的 PR 值，具体代码如下：

import networkx as nx

# 创建有向图

G = nx.DiGraph()

# 有向图之间边的关系

edges = [("A", "B"), ("A", "C"), ("A", "D"), ("B", "A"), ("B", "D"), ("C", "A"), ("D", "B"), ("D", "C")]

for edge in edges:

    G.add_edge(edge[0], edge[1])

pagerank_list = nx.pagerank(G, alpha=1)

print("pagerank 值是：", pagerank_list)

NetworkX 工具把中间的计算细节都已经封装起来了，我们直接调用 PageRank 函数就可以得到结果：

pagerank 值是： {'A': 0.33333396911621094, 'B': 0.22222201029459634, 'C': 0.22222201029459634, 'D': 0.22222201029459634}

我们通过 NetworkX 创建了一个有向图之后，设置了节点之间的边，然后使用 PageRank 函数就可以求得节点的 PR 值，结果和上节课中我们人工模拟的结果一致。

好了，运行完这个例子之后，我们来看下 NetworkX 工具都有哪些常用的操作。

1. 关于图的创建

图可以分为无向图和有向图，在 NetworkX 中分别采用不同的函数进行创建。无向图指的是不用节点之间的边的方向，使用 nx.Graph() 进行创建；有向图指的是节点之间的边是有方向的，使用 nx.DiGraph() 来创建。在上面这个例子中，存在 A→D 的边，但不存在 D→A 的边。

2. 关于节点的增加、删除和查询

如果想在网络中增加节点，可以使用 G.add_node(‘A’) 添加一个节点，也可以使用 G.add_nodes_from([‘B’,‘C’,‘D’,‘E’]) 添加节点集合。如果想要删除节点，可以使用 G.remove_node(node) 删除一个指定的节点，也可以使用 G.remove_nodes_from([‘B’,‘C’,‘D’,‘E’]) 删除集合中的节点。

那么该如何查询节点呢？

如果你想要得到图中所有的节点，就可以使用 G.nodes()，也可以用 G.number_of_nodes() 得到图中节点的个数。

3. 关于边的增加、删除、查询

增加边与添加节点的方式相同，使用 G.add_edge(“A”, “B”) 添加指定的“从 A 到 B”的边，也可以使用 add_edges_from 函数从边集合中添加。我们也可以做一个加权图，也就是说边是带有权重的，使用 add_weighted_edges_from 函数从带有权重的边的集合中添加。在这个函数的参数中接收的是 1 个或多个三元组 [u,v,w] 作为参数，u、v、w 分别代表起点、终点和权重。

另外，我们可以使用 remove_edge 函数和 remove_edges_from 函数删除指定边和从边集合中删除。

另外可以使用 edges() 函数访问图中所有的边，使用 number_of_edges() 函数得到图中边的个数。

以上是关于图的基本操作，如果我们创建了一个图，并且对节点和边进行了设置，就可以找到其中有影响力的节点，原理就是通过 PageRank 算法，使用 nx.pagerank(G) 这个函数，函数中的参数 G 代表创建好的图。

如何用 PageRank 揭秘希拉里邮件中的人物关系

了解了 NetworkX 工具的基础使用之后，我们来看一个实际的案例：希拉里邮件人物关系分析。

希拉里邮件事件相信你也有耳闻，对这个数据的背景我们就不做介绍了。你可以从 GitHub 上下载这个数据集：https://github.com/cystanford/PageRank。

整个数据集由三个文件组成：Aliases.csv，Emails.csv 和 Persons.csv，其中 Emails 文件记录了所有公开邮件的内容，发送者和接收者的信息。Persons 这个文件统计了邮件中所有人物的姓名及对应的 ID。因为姓名存在别名的情况，为了将邮件中的人物进行统一，我们还需要用 Aliases 文件来查询别名和人物的对应关系。

整个数据集包括了 9306 封邮件和 513 个人名，数据集还是比较大的。不过这一次我们不需要对邮件的内容进行分析，只需要通过邮件中的发送者和接收者（对应 Emails.csv 文件中的 MetadataFrom 和 MetadataTo 字段）来绘制整个关系网络。因为涉及到的人物很多，因此我们需要通过 PageRank 算法计算每个人物在邮件关系网络中的权重，最后筛选出来最有价值的人物来进行关系网络图的绘制。

了解了数据集和项目背景之后，我们来设计到执行的流程步骤：

首先我们需要加载数据源；

在准备阶段：我们需要对数据进行探索，在数据清洗过程中，因为邮件中存在别名的情况，因此我们需要统一人物名称。另外邮件的正文并不在我们考虑的范围内，只统计邮件中的发送者和接收者，因此我们筛选 MetadataFrom 和 MetadataTo 这两个字段作为特征。同时，发送者和接收者可能存在多次邮件往来，需要设置权重来统计两人邮件往来的次数。次数越多代表这个边（从发送者到接收者的边）的权重越高；

在挖掘阶段：我们主要是对已经设置好的网络图进行 PR 值的计算，但邮件中的人物有 500 多人，有些人的权重可能不高，我们需要筛选 PR 值高的人物，绘制出他们之间的往来关系。在可视化的过程中，我们可以通过节点的 PR 值来绘制节点的大小，PR 值越大，节点的绘制尺寸越大。

设置好流程之后，实现的代码如下：

# -*- coding: utf-8 -*-

# 用 PageRank 挖掘希拉里邮件中的重要任务关系

import pandas as pd

import networkx as nx

import numpy as np

from collections import defaultdict

import matplotlib.pyplot as plt

# 数据加载

emails = pd.read_csv("./input/Emails.csv")

# 读取别名文件

file = pd.read_csv("./input/Aliases.csv")

aliases = {}

for index, row in file.iterrows():

    aliases[row['Alias']] = row['PersonId']

# 读取人名文件

file = pd.read_csv("./input/Persons.csv")

persons = {}

for index, row in file.iterrows():

    persons[row['Id']] = row['Name']

# 针对别名进行转换

def unify_name(name):

    # 姓名统一小写

    name = str(name).lower()

    # 去掉, 和 @后面的内容

    name = name.replace(",","").split("@")[0]

    # 别名转换

    if name in aliases.keys():

        return persons[aliases[name]]

    return name

# 画网络图

def show_graph(graph, layout='spring_layout'):

    # 使用 Spring Layout 布局，类似中心放射状

    if layout == 'circular_layout':

        positions=nx.circular_layout(graph)

    else:

        positions=nx.spring_layout(graph)

    # 设置网络图中的节点大小，大小与 pagerank 值相关，因为 pagerank 值很小所以需要 *20000

    nodesize = [x['pagerank']*20000 for v,x in graph.nodes(data=True)]

    # 设置网络图中的边长度

    edgesize = [np.sqrt(e[2]['weight']) for e in graph.edges(data=True)]

    # 绘制节点

    nx.draw_networkx_nodes(graph, positions, node_size=nodesize, alpha=0.4)

    # 绘制边

    nx.draw_networkx_edges(graph, positions, edge_size=edgesize, alpha=0.2)

    # 绘制节点的 label

    nx.draw_networkx_labels(graph, positions, font_size=10)

    # 输出希拉里邮件中的所有人物关系图

    plt.show()

# 将寄件人和收件人的姓名进行规范化

emails.MetadataFrom = emails.MetadataFrom.apply(unify_name)

emails.MetadataTo = emails.MetadataTo.apply(unify_name)

# 设置遍的权重等于发邮件的次数

edges_weights_temp = defaultdict(list)

for row in zip(emails.MetadataFrom, emails.MetadataTo, emails.RawText):

    temp = (row[0], row[1])

    if temp not in edges_weights_temp:

        edges_weights_temp[temp] = 1

    else:

        edges_weights_temp[temp] = edges_weights_temp[temp] + 1

# 转化格式 (from, to), weight => from, to, weight

edges_weights = [(key[0], key[1], val) for key, val in edges_weights_temp.items()]

# 创建一个有向图

graph = nx.DiGraph()

# 设置有向图中的路径及权重 (from, to, weight)

graph.add_weighted_edges_from(edges_weights)

# 计算每个节点（人）的 PR 值，并作为节点的 pagerank 属性

pagerank = nx.pagerank(graph)

# 将 pagerank 数值作为节点的属性

nx.set_node_attributes(graph, name = 'pagerank', values=pagerank)

# 画网络图

show_graph(graph)

# 将完整的图谱进行精简

# 设置 PR 值的阈值，筛选大于阈值的重要核心节点

pagerank_threshold = 0.005

# 复制一份计算好的网络图

small_graph = graph.copy()

# 剪掉 PR 值小于 pagerank_threshold 的节点

for n, p_rank in graph.nodes(data=True):

    if p_rank['pagerank'] < pagerank_threshold:

        small_graph.remove_node(n)

# 画网络图, 采用 circular_layout 布局让筛选出来的点组成一个圆

show_graph(small_graph, 'circular_layout')

运行结果如下：

针对代码中的几个模块我做个简单的说明：

1. 函数定义

人物的名称需要统一，因此我设置了 unify_name 函数，同时设置了 show_graph 函数将网络图可视化。NetworkX 提供了多种可视化布局，这里我使用 spring_layout 布局，也就是呈中心放射状。

除了 spring_layout 外，NetworkX 还有另外三种可视化布局，circular_layout（在一个圆环上均匀分布节点），random_layout（随机分布节点 ），shell_layout（节点都在同心圆上）。

2. 计算边权重

邮件的发送者和接收者的邮件往来可能不止一次，我们需要用两者之间邮件往来的次数计算这两者之间边的权重，所以我用 edges_weights_temp 数组存储权重。而上面介绍过在 NetworkX 中添加权重边（即使用 add_weighted_edges_from 函数）的时候，接受的是 u、v、w 的三元数组，因此我们还需要对格式进行转换，具体转换方式见代码。

3.PR 值计算及筛选

我使用 nx.pagerank(graph) 计算了节点的 PR 值。由于节点数量很多，我们设置了 PR 值阈值，即 pagerank_threshold=0.005，然后遍历节点，删除小于 PR 值阈值的节点，形成新的图 small_graph，最后对 small_graph 进行可视化（对应运行结果的第二张图）。

总结

在上节课中，我们通过矩阵乘法求得网页的权重，这节课我们使用 NetworkX 可以得到相同的结果。

另外我带你用 PageRank 算法做了一次实战，我们将一个复杂的网络图，通过 PR 值的计算、筛选，最终得到了一张精简的网络图。在这个过程中我们学习了 NetworkX 工具的使用，包括创建图、节点、边及 PR 值的计算。

实际上掌握了 PageRank 的理论之后，在实战中往往就是一行代码的事。但项目与理论不同，项目中涉及到的数据量比较大，你会花 80% 的时间（或 80% 的代码量）在预处理过程中，比如今天的项目中，我们对别名进行了统一，对边的权重进行计算，同时还需要把计算好的结果以可视化的方式呈现。

今天我举了一个网页权重的例子，假设一共有 4 个网页 A、B、C、D。它们之间的链接信息如文章中的图示。我们假设用户有 15% 的概率随机跳转，请你编写代码重新计算这 4 个节点的 PR 值。

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

34丨AdaBoost（上）：如何使用AdaBoost提升分类器性能？

今天我们学习 AdaBoost 算法。在数据挖掘中，分类算法可以说是核心算法，其中 AdaBoost 算法与随机森林算法一样都属于分类算法中的集成算法。

集成的含义就是集思广益，博取众长，当我们做决定的时候，我们先听取多个专家的意见，再做决定。集成算法通常有两种方式，分别是投票选举（bagging）和再学习（boosting）。投票选举的场景类似把专家召集到一个会议桌前，当做一个决定的时候，让 K 个专家（K 个模型）分别进行分类，然后选择出现次数最多的那个类作为最终的分类结果。再学习相当于把 K 个专家（K 个分类器）进行加权融合，形成一个新的超级专家（强分类器），让这个超级专家做判断。

所以你能看出来，投票选举和再学习还是有区别的。Boosting 的含义是提升，它的作用是每一次训练的时候都对上一次的训练进行改进提升，在训练的过程中这 K 个“专家”之间是有依赖性的，当引入第 K 个“专家”（第 K 个分类器）的时候，实际上是对前 K-1 个专家的优化。而 bagging 在做投票选举的时候可以并行计算，也就是 K 个“专家”在做判断的时候是相互独立的，不存在依赖性。

AdaBoost 的工作原理

了解了集成算法的两种模式之后，我们来看下今天要讲的 AdaBoost 算法。

AdaBoost 的英文全称是 Adaptive Boosting，中文含义是自适应提升算法。它由 Freund 等人于 1995 年提出，是对 Boosting 算法的一种实现。

什么是 Boosting 算法呢？Boosting 算法是集成算法中的一种，同时也是一类算法的总称。这类算法通过训练多个弱分类器，将它们组合成一个强分类器，也就是我们俗话说的“三个臭皮匠，顶个诸葛亮”。为什么要这么做呢？因为臭皮匠好训练，诸葛亮却不好求。因此要打造一个诸葛亮，最好的方式就是训练多个臭皮匠，然后让这些臭皮匠组合起来，这样往往可以得到很好的效果。这就是 Boosting 算法的原理。

我可以用上面的图来表示最终得到的强分类器，你能看出它是通过一系列的弱分类器根据不同的权重组合而成的。

假设弱分类器为 Gi(x)Gi(x)，它在强分类器中的权重 αiαi，那么就可以得出强分类器 f(x)：

有了这个公式，为了求解强分类器，你会关注两个问题：

如何得到弱分类器，也就是在每次迭代训练的过程中，如何得到最优弱分类器？

每个弱分类器在强分类器中的权重是如何计算的？

我们先来看下第二个问题。实际上在一个由 K 个弱分类器中组成的强分类器中，如果弱分类器的分类效果好，那么权重应该比较大，如果弱分类器的分类效果一般，权重应该降低。所以我们需要基于这个弱分类器对样本的分类错误率来决定它的权重，用公式表示就是：

其中 eiei 代表第 i 个分类器的分类错误率。

然后我们再来看下第一个问题，如何在每次训练迭代的过程中选择最优的弱分类器？

实际上，AdaBoost 算法是通过改变样本的数据分布来实现的。AdaBoost 会判断每次训练的样本是否正确分类，对于正确分类的样本，降低它的权重，对于被错误分类的样本，增加它的权重。再基于上一次得到的分类准确率，来确定这次训练样本中每个样本的权重。然后将修改过权重的新数据集传递给下一层的分类器进行训练。这样做的好处就是，通过每一轮训练样本的动态权重，可以让训练的焦点集中到难分类的样本上，最终得到的弱分类器的组合更容易得到更高的分类准确率。

我们可以用 Dk+1Dk+1 代表第 k+1 轮训练中，样本的权重集合，其中 Wk+1,1Wk+1,1 代表第 k+1 轮中第一个样本的权重，以此类推 Wk+1,NWk+1,N 代表第 k+1 轮中第 N 个样本的权重，因此用公式表示为：

第 k+1 轮中的样本权重，是根据该样本在第 k 轮的权重以及第 k 个分类器的准确率而定，具体的公式为：

AdaBoost 算法示例

了解 AdaBoost 的工作原理之后，我们看一个例子，假设我有 10 个训练样本，如下所示：

现在我希望通过 AdaBoost 构建一个强分类器。

该怎么做呢？按照上面的 AdaBoost 工作原理，我们来模拟一下。

首先在第一轮训练中，我们得到 10 个样本的权重为 1/10，即初始的 10 个样本权重一致，D1=(0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1)。

假设我有 3 个基础分类器：

我们可以知道分类器 f1 的错误率为 0.3，也就是 x 取值 6、7、8 时分类错误；分类器 f2 的错误率为 0.4，即 x 取值 0、1、2、9 时分类错误；分类器 f3 的错误率为 0.3，即 x 取值为 3、4、5 时分类错误。

这 3 个分类器中，f1、f3 分类器的错误率最低，因此我们选择 f1 或 f3 作为最优分类器，假设我们选 f1 分类器作为最优分类器，即第一轮训练得到：

根据分类器权重公式得到：

然后我们对下一轮的样本更新求权重值，代入 Wk+1,iWk+1,i 和 Dk+1Dk+1 的公式，可以得到新的权重矩阵：D2=(0.0715, 0.0715, 0.0715, 0.0715, 0.0715, 0.0715, 0.1666, 0.1666, 0.1666, 0.0715)。

在第二轮训练中，我们继续统计三个分类器的准确率，可以得到分类器 f1 的错误率为 0.1666*3，也就是 x 取值为 6、7、8 时分类错误。分类器 f2 的错误率为 0.0715*4，即 x 取值为 0、1、2、9 时分类错误。分类器 f3 的错误率为 0.0715*3，即 x 取值 3、4、5 时分类错误。

在这 3 个分类器中，f3 分类器的错误率最低，因此我们选择 f3 作为第二轮训练的最优分类器，即：

根据分类器权重公式得到：

同样，我们对下一轮的样本更新求权重值，代入 Wk+1,iWk+1,i 和 Dk+1Dk+1 的公式，可以得到 D3=(0.0455,0.0455,0.0455,0.1667, 0.1667,0.01667,0.1060, 0.1060, 0.1060, 0.0455)。

在第三轮训练中，我们继续统计三个分类器的准确率，可以得到分类器 f1 的错误率为 0.1060*3，也就是 x 取值 6、7、8 时分类错误。分类器 f2 的错误率为 0.0455*4，即 x 取值为 0、1、2、9 时分类错误。分类器 f3 的错误率为 0.1667*3，即 x 取值 3、4、5 时分类错误。

在这 3 个分类器中，f2 分类器的错误率最低，因此我们选择 f2 作为第三轮训练的最优分类器，即：

我们根据分类器权重公式得到：

假设我们只进行 3 轮的训练，选择 3 个弱分类器，组合成一个强分类器，那么最终的强分类器 G(x) = 0.4236G1(x) + 0.6496G2(x)+0.7514G3(x)。

实际上 AdaBoost 算法是一个框架，你可以指定任意的分类器，通常我们可以采用 CART 分类器作为弱分类器。通过上面这个示例的运算，你体会一下 AdaBoost 的计算流程即可。

总结

今天我给你讲了 AdaBoost 算法的原理，你可以把它理解为一种集成算法，通过训练不同的弱分类器，将这些弱分类器集成起来形成一个强分类器。在每一轮的训练中都会加入一个新的弱分类器，直到达到足够低的错误率或者达到指定的最大迭代次数为止。实际上每一次迭代都会引入一个新的弱分类器（这个分类器是每一次迭代中计算出来的，是新的分类器，不是事先准备好的）。

在弱分类器的集合中，你不必担心弱分类器太弱了。实际上它只需要比随机猜测的效果略好一些即可。如果随机猜测的准确率是 50% 的话，那么每个弱分类器的准确率只要大于 50% 就可用。AdaBoost 的强大在于迭代训练的机制，这样通过 K 个“臭皮匠”的组合也可以得到一个“诸葛亮”（强分类器）。

当然在每一轮的训练中，我们都需要从众多“臭皮匠”中选择一个拔尖的，也就是这一轮训练评比中的最优“臭皮匠”，对应的就是错误率最低的分类器。当然每一轮的样本的权重都会发生变化，这样做的目的是为了让之前错误分类的样本得到更多概率的重复训练机会。

同样的原理在我们的学习生活中也经常出现，比如善于利用错题本来提升学习效率和学习成绩。

最后你能说说你是如何理解 AdaBoost 中弱分类器，强分类器概念的？另外，AdaBoost 算法是如何训练弱分类器从而得到一个强分类器的？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

35丨AdaBoost（下）：如何使用AdaBoost对房价进行预测？

今天我带你用 AdaBoost 算法做一个实战项目。AdaBoost 不仅可以用于分类问题，还可以用于回归分析。

我们先做个简单回忆，什么是分类，什么是回归呢？实际上分类和回归的本质是一样的，都是对未知事物做预测。不同之处在于输出结果的类型，分类输出的是一个离散值，因为物体的分类数有限的，而回归输出的是连续值，也就是在一个区间范围内任何取值都有可能。

这次我们的主要目标是使用 AdaBoost 预测房价，这是一个回归问题。除了对项目进行编码实战外，我希望你能掌握：

AdaBoost 工具的使用，包括使用 AdaBoost 进行分类，以及回归分析。

使用其他的回归工具，比如决策树回归，对比 AdaBoost 回归和决策树回归的结果。

如何使用 AdaBoost 工具

我们可以直接在 sklearn 中使用 AdaBoost。如果我们要用 AdaBoost 进行分类，需要在使用前引用代码：

from sklearn.ensemble import AdaBoostClassifier

我们之前讲到过，如果你看到了 Classifier 这个类，一般都会对应着 Regressor 类。AdaBoost 也不例外，回归工具包的引用代码如下：

from sklearn.ensemble import AdaBoostRegressor

我们先看下如何在 sklearn 中创建 AdaBoost 分类器。

我们需要使用 AdaBoostClassifier(base_estimator=None, n_estimators=50, learning_rate=1.0, algorithm=’SAMME.R’, random_state=None) 这个函数，其中有几个比较主要的参数，我分别来讲解下：

base_estimator：代表的是弱分类器。在 AdaBoost 的分类器和回归器中都有这个参数，在 AdaBoost 中默认使用的是决策树，一般我们不需要修改这个参数，当然你也可以指定具体的分类器。

n_estimators：算法的最大迭代次数，也是分类器的个数，每一次迭代都会引入一个新的弱分类器来增加原有的分类器的组合能力。默认是 50。

learning_rate：代表学习率，取值在 0-1 之间，默认是 1.0。如果学习率较小，就需要比较多的迭代次数才能收敛，也就是说学习率和迭代次数是有相关性的。当你调整 learning_rate 的时候，往往也需要调整 n_estimators 这个参数。

algorithm：代表我们要采用哪种 boosting 算法，一共有两种选择：SAMME 和 SAMME.R。默认是 SAMME.R。这两者之间的区别在于对弱分类权重的计算方式不同。

random_state：代表随机数种子的设置，默认是 None。随机种子是用来控制随机模式的，当随机种子取了一个值，也就确定了一种随机规则，其他人取这个值可以得到同样的结果。如果不设置随机种子，每次得到的随机数也就不同。

那么如何创建 AdaBoost 回归呢？

我们可以使用 AdaBoostRegressor(base_estimator=None, n_estimators=50, learning_rate=1.0, loss=‘linear’, random_state=None) 这个函数。

你能看出来回归和分类的参数基本是一致的，不同点在于回归算法里没有 algorithm 这个参数，但多了一个 loss 参数。

loss 代表损失函数的设置，一共有 3 种选择，分别为 linear、square 和 exponential，它们的含义分别是线性、平方和指数。默认是线性。一般采用线性就可以得到不错的效果。

创建好 AdaBoost 分类器或回归器之后，我们就可以输入训练集对它进行训练。我们使用 fit 函数，传入训练集中的样本特征值 train_X 和结果 train_y，模型会自动拟合。使用 predict 函数进行预测，传入测试集中的样本特征值 test_X，然后就可以得到预测结果。

如何用 AdaBoost 对房价进行预测

了解了 AdaBoost 工具包之后，我们看下 sklearn 中自带的波士顿房价数据集。

这个数据集一共包括了 506 条房屋信息数据，每一条数据都包括了 13 个指标，以及一个房屋价位。

13 个指标的含义，可以参考下面的表格：

这些指标分析得还是挺细的，但实际上，我们不用关心具体的含义，要做的就是如何通过这 13 个指标推导出最终的房价结果。

如果你学习了之前的算法实战，这个数据集的预测并不复杂。

首先加载数据，将数据分割成训练集和测试集，然后创建 AdaBoost 回归模型，传入训练集数据进行拟合，再传入测试集数据进行预测，就可以得到预测结果。最后将预测的结果与实际结果进行对比，得到两者之间的误差。具体代码如下：

from sklearn.model_selection import train_test_split

from sklearn.metrics import mean_squared_error

from sklearn.datasets import load_boston

from sklearn.ensemble import AdaBoostRegressor

# 加载数据

data=load_boston()

# 分割数据

train_x, test_x, train_y, test_y = train_test_split(data.data, data.target, test_size=0.25, random_state=33)

# 使用 AdaBoost 回归模型

regressor=AdaBoostRegressor()

regressor.fit(train_x,train_y)

pred_y = regressor.predict(test_x)

mse = mean_squared_error(test_y, pred_y)

print(" 房价预测结果 ", pred_y)

print(" 均方误差 = ",round(mse,2))

运行结果：

房价预测结果  [20.2        10.4137931  14.63820225 17.80322581 24.58931298 21.25076923

 27.52222222 17.8372093  31.79642857 20.86428571 27.87431694 31.09142857

 12.81666667 24.13131313 12.81666667 24.58931298 17.80322581 17.66333333

 27.83       24.58931298 17.66333333 20.90823529 20.10555556 20.90823529

 28.20877193 20.10555556 21.16882129 24.58931298 13.27619048 31.09142857

 17.08095238 26.19217391  9.975      21.03404255 26.74583333 31.09142857

 25.83960396 11.859375   13.38235294 24.58931298 14.97931034 14.46699029

 30.12777778 17.66333333 26.19217391 20.10206186 17.70540541 18.45909091

 26.19217391 20.10555556 17.66333333 33.31025641 14.97931034 17.70540541

 24.64421053 20.90823529 25.83960396 17.08095238 24.58931298 21.43571429

 19.31617647 16.33733333 46.04888889 21.25076923 17.08095238 25.83960396

 24.64421053 11.81470588 17.80322581 27.63636364 23.59731183 17.94444444

 17.66333333 27.7253886  20.21465517 46.04888889 14.97931034  9.975

 17.08095238 24.13131313 21.03404255 13.4        11.859375   26.19214286

 21.25076923 21.03404255 47.11395349 16.33733333 43.21111111 31.65730337

 30.12777778 20.10555556 17.8372093  18.40833333 14.97931034 33.31025641

 24.58931298 22.88813559 18.27179487 17.80322581 14.63820225 21.16882129

 26.91538462 24.64421053 13.05       14.97931034  9.975      26.19217391

 12.81666667 26.19214286 49.46511628 13.27619048 17.70540541 25.83960396

 31.09142857 24.13131313 21.25076923 21.03404255 26.91538462 21.03404255

 21.16882129 17.8372093  12.81666667 21.03404255 21.03404255 17.08095238

 45.16666667]

均方误差 =  18.05

这个数据集是比较规范的，我们并不需要在数据清洗，数据规范化上花太多精力，代码编写起来比较简单。

同样，我们可以使用不同的回归分析模型分析这个数据集，比如使用决策树回归和 KNN 回归。

编写代码如下：

# 使用决策树回归模型

dec_regressor=DecisionTreeRegressor()

dec_regressor.fit(train_x,train_y)

pred_y = dec_regressor.predict(test_x)

mse = mean_squared_error(test_y, pred_y)

print(" 决策树均方误差 = ",round(mse,2))

# 使用 KNN 回归模型

knn_regressor=KNeighborsRegressor()

knn_regressor.fit(train_x,train_y)

pred_y = knn_regressor.predict(test_x)

mse = mean_squared_error(test_y, pred_y)

print("KNN 均方误差 = ",round(mse,2))

运行结果：

决策树均方误差 =  23.84

KNN 均方误差 =  27.87

你能看到相比之下，AdaBoost 的均方误差更小，也就是结果更优。虽然 AdaBoost 使用了弱分类器，但是通过 50 个甚至更多的弱分类器组合起来而形成的强分类器，在很多情况下结果都优于其他算法。因此 AdaBoost 也是常用的分类和回归算法之一。

AdaBoost 与决策树模型的比较

在 sklearn 中 AdaBoost 默认采用的是决策树模型，我们可以随机生成一些数据，然后对比下 AdaBoost 中的弱分类器（也就是决策树弱分类器）、决策树分类器和 AdaBoost 模型在分类准确率上的表现。

如果想要随机生成数据，我们可以使用 sklearn 中的 make_hastie_10_2 函数生成二分类数据。假设我们生成 12000 个数据，取前 2000 个作为测试集，其余作为训练集。

有了数据和训练模型后，我们就可以编写代码。我设置了 AdaBoost 的迭代次数为 200，代表 AdaBoost 由 200 个弱分类器组成。针对训练集，我们用三种模型分别进行训练，然后用测试集进行预测，并将三个分类器的错误率进行可视化对比，可以看到这三者之间的区别：

import numpy as np

import matplotlib.pyplot as plt

from sklearn import datasets

from sklearn.metrics import zero_one_loss

from sklearn.tree import DecisionTreeClassifier

from sklearn.ensemble import  AdaBoostClassifier

# 设置 AdaBoost 迭代次数

n_estimators=200

# 使用

X,y=datasets.make_hastie_10_2(n_samples=12000,random_state=1)

# 从 12000 个数据中取前 2000 行作为测试集，其余作为训练集

test_x, test_y = X[2000:],y[2000:]

train_x, train_y = X[:2000],y[:2000]

# 弱分类器

dt_stump = DecisionTreeClassifier(max_depth=1,min_samples_leaf=1)

dt_stump.fit(train_x, train_y)

dt_stump_err = 1.0-dt_stump.score(test_x, test_y)

# 决策树分类器

dt = DecisionTreeClassifier()

dt.fit(train_x,  train_y)

dt_err = 1.0-dt.score(test_x, test_y)

# AdaBoost 分类器

ada = AdaBoostClassifier(base_estimator=dt_stump,n_estimators=n_estimators)

ada.fit(train_x,  train_y)

# 三个分类器的错误率可视化

fig = plt.figure()

# 设置 plt 正确显示中文

plt.rcParams['font.sans-serif'] = ['SimHei']

ax = fig.add_subplot(111)

ax.plot([1,n_estimators],[dt_stump_err]*2, 'k-', label=u'决策树弱分类器 错误率')

ax.plot([1,n_estimators],[dt_err]*2,'k--', label=u'决策树模型 错误率')

ada_err = np.zeros((n_estimators,))

# 遍历每次迭代的结果 i 为迭代次数, pred_y 为预测结果

for i,pred_y in enumerate(ada.staged_predict(test_x)):

     # 统计错误率

    ada_err[i]=zero_one_loss(pred_y, test_y)

# 绘制每次迭代的 AdaBoost 错误率

ax.plot(np.arange(n_estimators)+1, ada_err, label='AdaBoost Test 错误率', color='orange')

ax.set_xlabel('迭代次数')

ax.set_ylabel('错误率')

leg=ax.legend(loc='upper right',fancybox=True)

plt.show()

运行结果：

从图中你能看出来，弱分类器的错误率最高，只比随机分类结果略好，准确率稍微大于 50%。决策树模型的错误率明显要低很多。而 AdaBoost 模型在迭代次数超过 25 次之后，错误率有了明显下降，经过 125 次迭代之后错误率的变化形势趋于平缓。

因此我们能看出，虽然单独的一个决策树弱分类器效果不好，但是多个决策树弱分类器组合起来形成的 AdaBoost 分类器，分类效果要好于决策树模型。

总结

今天我带你用 AdaBoost 回归分析对波士顿房价进行了预测。因为这是个回归分析的问题，我们直接使用 sklearn 中的 AdaBoostRegressor 即可。如果是分类，我们使用 AdaBoostClassifier。

另外我们将 AdaBoost 分类器、弱分类器和决策树分类器做了对比，可以看出经过多个弱分类器组合形成的 AdaBoost 强分类器，准确率要明显高于决策树算法。所以 AdaBoost 的优势在于框架本身，它通过一种迭代机制让原本性能不强的分类器组合起来，形成一个强分类器。

其实在现实工作中，我们也能找到类似的案例。IBM 服务器追求的是单个服务器性能的强大，比如打造超级服务器。而 Google 在创建集群的时候，利用了很多 PC 级的服务器，将它们组成集群，整体性能远比一个超级服务器的性能强大。

再比如我们讲的“三个臭皮匠，顶个诸葛亮”，也就是 AdaBoost 的价值所在。

今天我们用 AdaBoost 分类器与决策树分类做对比的时候，使用到了 sklearn 中的 make_hastie_10_2 函数生成数据。实际上在第 19 篇，我们对泰坦尼克号的乘客做生存预测的时候，也讲到了决策树工具的使用。你能不能编写代码，使用 AdaBoost 算法对泰坦尼克号乘客的生存做预测，看看它和决策树模型，谁的准确率更高？

你也可以把这篇文章分享给你的朋友或者同事，一起切磋一下。

36丨数据分析算法篇答疑

算法篇更新到现在就算结束了，因为这一模块比较难，所以大家提出了形形色色的问题。我总结了同学们经常遇到的问题，精选了几个有代表性的来作为答疑。没有列出的问题，我也会在评论区陆续解答。

17-19 篇：决策树

答疑 1：在探索数据的代码中，print(boston.feature_names) 有什么作用？

boston 是 sklearn 自带的数据集，里面有 5 个 keys，分别是 data、target、feature_names、DESCR 和 filename。其中 data 代表特征矩阵，target 代表目标结果，feature_names 代表 data 对应的特征名称，DESCR 是对数据集的描述，filename 对应的是 boston 这个数据在本地的存放文件路径。

针对 sklearn 中自带的数据集，你可以查看下加载之后，都有哪些字段。调用方法如下：

boston=load_boston()

print(boston.keys())

通过 boston.keys() 你可以看到，boston 数据集的字段包括了 [‘data’, ‘target’, ‘feature_names’, ‘DESCR’, ‘filename’]。

答疑 2：决策树的剪枝在 sklearn 中是如何实现的？

实际上决策树分类器，以及决策树回归器（对应 DecisionTreeRegressor 类）都没有集成剪枝步骤。一般对决策树进行缩减，常用的方法是在构造 DecisionTreeClassifier 类时，对参数进行设置，比如 max_depth 表示树的最大深度，max_leaf_nodes 表示最大的叶子节点数。

通过调整这两个参数，就能对决策树进行剪枝。当然也可以自己编写剪枝程序完成剪枝。

答疑 3：对泰坦尼克号的乘客做生存预测的时候，Carbin 字段缺失率分别为 77% 和 78%，Age 和 Fare 字段有缺失值，是如何判断出来的？

首先我们需要对数据进行探索，一般是将数据存储到 DataFrame 中，使用 df.info() 可以看到表格的一些具体信息，代码如下：

# 数据加载

train_data = pd.read_csv('./Titanic_Data/train.csv')

test_data = pd.read_csv('./Titanic_Data/test.csv')

print(train_data.info())

print(test_data.info())

这是运行结果：

<class 'pandas.core.frame.DataFrame'>

RangeIndex: 891 entries, 0 to 890

Data columns (total 12 columns):

PassengerId    891 non-null int64

Survived       891 non-null int64

Pclass         891 non-null int64

Name           891 non-null object

Sex            891 non-null object

Age            714 non-null float64

SibSp          891 non-null int64

Parch          891 non-null int64

Ticket         891 non-null object

Fare           891 non-null float64

Cabin          204 non-null object

Embarked       889 non-null object

dtypes: float64(2), int64(5), object(5)

memory usage: 83.6+ KB

None

<class 'pandas.core.frame.DataFrame'>

RangeIndex: 418 entries, 0 to 417

Data columns (total 11 columns):

PassengerId    418 non-null int64

Pclass         418 non-null int64

Name           418 non-null object

Sex            418 non-null object

Age            332 non-null float64

SibSp          418 non-null int64

Parch          418 non-null int64

Ticket         418 non-null object

Fare           417 non-null float64

Cabin          91 non-null object

Embarked       418 non-null object

dtypes: float64(2), int64(4), object(5)

memory usage: 36.0+ KB

None

你可以关注下运行结果中 Carbin 的部分，你能看到在训练集中一共 891 行数据，Carbin 有数值的只有 204 个，那么缺失率为 1-204/891=77%，同样在测试集中一共有 418 行数据，Carbin 有数值的只有 91 个，那么缺失率为 1-91/418=78%。

同理你也能看到在训练集中，Age 字段有缺失值。在测试集中，Age 字段和 Fare 字段有缺失值。

答疑 4：在用 pd.read_csv 时报错“UnicodeDecodeError utf-8 codec can’t decode byte 0xcf in position 15: invalid continuation byte”是什么问题？

一般在 Python 中遇到编码问题，尤其是中文编码出错，是比较常见的。有几个常用的解决办法，你可以都试一下：

将 read_csv 中的编码改为 gb18030，代码为：data = pd.read_csv(filename, encoding = ‘gb18030’)。

代码前添加 # -- coding: utf-8 --。

我说一下 gb18030 和 utf-8 的区别。utf-8 是国际通用字符编码，gb18030 是新出的国家标准，不仅包括了简体和繁体，也包括了一些不常见的中文，相比于 utf-8 更全，容错率更高。

为了让编辑器对中文更加支持，你也可以在代码最开始添加 # -- coding: utf-8 -- 的说明，再结合其他方法解决编码出错的问题。

第 20-21 篇：朴素贝叶斯

答疑 1：在朴素贝叶斯中，我们要统计的是属性的条件概率，也就是假设取出来的是白色的棋子，那么它属于盒子 A 的概率是 2/3。这个我算的是 3/5，跟老师的不一样，老师可以给一下详细步骤吗？

不少同学都遇到了这个问题，我来统一解答下。

这里我们需要运用贝叶斯公式（我在文章中也给出了），即：

假设 A 代表白棋子，B1 代表 A 盒，B2 代表 B 盒。带入贝叶斯公式，我们可以得到：

其中 P(B1)P(B1) 代表 A 盒的概率，7 个棋子，A 盒有 4 个，所以 P(B1)P(B1)=4/7。

P(B2)P(B2) 代表 B 盒的概率，7 个棋子，B 盒有 3 个，所以 P(B2)P(B2)=3/7。

最终求取出来的是白色的棋子，那么它属于 A 盒的概率 P(B1|A)P(B1|A)= 2/3。

22-23 篇：SVM 算法

答疑 1：SVM 多分类器是集成算法么？

SVM 算法最初是为二分类问题设计的，如果我们想要把 SVM 分类器用于多分类问题，常用的有一对一方法和一对多方法（我在文章中有介绍到）。

集成学习的概念你这样理解：通过构造和使用多个分类器完成分类任务，也就是我们所说的博取众长。

以上是 SVM 多分类器和集成算法的概念，关于 SVM 多分类器是否属于集成算法，我认为你需要这样理解。

在 SVM 的多分类问题中，不论是采用一对一，还是一对多的方法，都会构造多个分类器，从这个角度来看确实在用集成学习的思想，通过这些分类器完成最后的学习任务。

不过我们一般所说的集成学习，需要有两个基本条件：

每个分类器的准确率要比随机分类的好，即准确率大于 50%；

每个分类器应该尽量相互独立，这样才能博采众长，否则多个分类器一起工作，和单个分类器工作相差不大。

所以你能看出，在集成学习中，虽然每个弱分类器性能不强，但都可以独立工作，完成整个分类任务。而在 SVM 多分类问题中，不论是一对一，还是一对多的方法，每次都在做一个二分类问题，并不能直接给出多分类的结果。

此外，当我们谈集成学习的时候，通常会基于单个分类器之间是否存在依赖关系，进而分成 Boosting 或者 Bagging 方法。如果单个分类器存在较强的依赖关系，需要串行使用，也就是我们所说的 Boosting 方法。如果单个分类器之间不存在强依赖关系，可以并行工作，就是我们所说的 Bagging 或者随机森林方法（Bagging 的升级版）。

所以，一个二分类器构造成多分类器是采用了集成学习的思路，不过在我们谈论集成学习的时候，通常指的是 Boosing 或者 Bagging 方法，因为需要每个分类器（弱分类器）都有分类的能力。

26-27 篇：K-Means

答疑 1：我在给 20 支亚洲球队做聚类模拟的时候，使用 K-Means 算法需要重新计算这三个类的中心点，最简单的方式就是取平均值，然后根据新的中心点按照距离远近重新分配球队的分类。对中心点的重新计算不太理解。

实际上是对属于这个类别的点的特征值求平均，即为新的中心点的特征值。

比如都属于同一个类别里面有 10 个点，那么新的中心点就是这 10 个点的中心点，一种简单的方式就是取平均值。比如文章中的足球队一共有 3 个指标，每个球队都有这三个指标的特征值，那么新的中心点，就是取这个类别中的这些点的这三个指标特征值的平均值。

28-29 篇：EM 聚类

答疑 1：关于 EM 聚类初始参数设置的问题，初始参数随机设置会影响聚类的效果吗。会不会初始参数不对，聚类就出错了呢？

实际上只是增加了迭代次数而已。

EM 算法的强大在于它的鲁棒性，或者说它的机制允许初始化参数存在误差。

举个例子，EM 的核心是通过参数估计来完成聚类。如果你想要把菜平均分到两个盘子中，一开始 A 盘的菜很少，B 盘的菜很多，我们只要通过 EM 不断迭代，就会让两个盘子的菜量一样多，只是迭代的次数多一些而已。

另外多说一句，我们学的这些数据挖掘的算法，不论是 EM、Adaboost 还是 K-Means，最大的价值都是它们的思想。我们在使用工具的时候都会设置初始化参数，比如在 K-Means 中要选择中心点，即使一开始只是随机选择，最后通过迭代都会得到不错的效果。所以说学习这些算法，就是学习它们的思想。

30-31 篇：关联规则挖掘

答疑 1：看不懂构造 FP 树的过程，面包和啤酒为什么会拆分呢？

FP-Growth 中有一个概念叫条件模式基。它在创建 FP 树的时候还用不上，我们主要通过扫描整个数据和项头表来构造 FP 树。条件模式基用于挖掘频繁项。通过找到每个项（item）的条件模式基，递归挖掘频繁项集。

答疑 2：不怎么会找元素的 XPath 路径。

XPath 的作用大家应该都能理解，具体的使用其实就是经验和技巧的问题。

我的方法就是不断尝试，而且 XPath 有自己的规则，绝大部分的情况下都是以 // 开头，因为想要匹配所有的元素。我们也可以找一些关键的特征来进行匹配，比如 class='item-root’的节点，或者 id='root’都是很好的特征。通过观察 id 或 class，也可以自己编写 XPath，这样写的 XPath 会更短。总之，都是要不断尝试，才能找到自己想要找的内容，寻找 XPath 的过程就是一个找规律的过程。

答疑 3：最小支持度可以设置小一些，如果最小支持度小，那么置信度就要设置得相对大一点，不然即使提升度高，也有可能是巧合。这个参数跟数据量以及项的数量有关。理解对吗？

一般来说最小置信度都会大一些，比如 1.0，0.9 或者 0.8。最小支持度和数据集大小和特点有关，可以尝试一些数值来观察结果，比如 0.1，0.5。

34-35 篇：AdaBoost 算法

答疑 1：关于 ZkZk 和 yiyi 的含义

第 k+1 轮的样本权重，是根据该样本在第 k 轮的权重以及第 k 个分类器的准确率而定，具体的公式为：

其中 ZkZk, yiyi 代表什么呢？

ZkZk 代表规范化因子，我们知道第 K+1 轮样本的权重为：

为了让样本权重之和为 1，我们需要除以规范化因子 ZkZk，所以：

yiyi 代表的是目标的结果，我在 AdaBoost 工作原理之后，列了一个 10 个训练样本的例子：

你能看到通常我们把 X 作为特征值，y 作为目标结果。在算法篇下的实战练习中，我们一般会把训练集分成 train_X 和 train_y，其中 train_X 代表特征矩阵，train_y 代表目标结果。

我发现大家对工具的使用和场景比较感兴趣，所以最后留两道思考题。

第一道题是，在数据挖掘的工具里，我们大部分情况下使用的是 sklearn，它自带了一些数据集，你能列举下 sklearn 自带的数据集都有哪些么？我在第 18 篇使用 print(boston.feature_names) 来查看 boston 数据集的特征名称（数据集特征矩阵的 index 名称），你能查看下其他数据集的特征名称都是什么吗？列举 1-2 个 sklearn 数据集即可。

第二个问题是，对于数据挖掘算法来说，基础就是数据集。Kaggle 网站之所以受到数据科学从业人员的青睐就是因为有众多比赛的数据集，以及社区间的讨论交流。你是否有使用过 Kaggle 网站的经历，如果有的话，可以分享下你的使用经验吗？如果你是个数据分析的新人，当看到 Kaggle 网站时，能否找到适合初学者的 kernels 么 (其他人在 Kaggle 上成功运行的代码分享)？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

37丨数据采集实战：如何自动化运营微博？

今天我来带你做一个数据采集的实战。之前我讲到可以使用第三方工具，比如八爪鱼做数据采集，也可以自己编写脚本，比如使用 Python。编写 Python 做数据采集和自动化最大的好处就是可控性强，每个步骤都可以调试，而且可以找到问题所在并一一突破。

今天我就带你使用 Python 自动化运营微博。关于今天的学习，希望你能达成以下的 3 个学习目标：

掌握 Selenium 自动化测试工具，以及元素定位的方法；

学会编写微博自动化功能模块：加关注，写评论，发微博；

对微博自动化做自我总结。

Selenium 自动化测试工具

当我们做 Web 自动化测试的时候，可以选用 Selenium 或者 Puppeteer 工具。我在第 10 篇的时候简单介绍过 Selenium 这个工具，你可以再回顾一下。Puppeteer 通过控制无头 Chrome 来完成浏览器的工作。这两个工具之间的区别在于：Selenium 更关注程序执行的流程本身，比如找到指定的元素，设置相应的值，然后点击操作。而 Puppeteer 是浏览者的视角，比如光标移动到某个元素上，键盘输入某个内容等。

今天我们继续使用 Selenium 工具自动化模拟浏览器，重点是学习对元素的定位。在第 10 篇讲到 Selenium WebDriver 的使用时，重点是对 HTML 进行获取和解析，然后通过 HTML 中的 XPath 进行提取，读取相应的内容。

在今天的实战里，我们需要在微博上自动登录，加关注，发评论，发微博等，这些操作都需要在浏览器上完成，所以我们可以使用 Webdriver 自带的元素定位功能。

如果我们想定位一个元素，可以通过 id、name、class、tag、链接上的全部文本、链接上的部分文本、XPath 或者 CSS 进行定位，在 Selenium Webdriver 中也提供了这 8 种方法方便我们定位元素。

通过 id 定位：我们可以使用 find_element_by_id() 函数。比如我们想定位 id=loginName 的元素，就可以使用 browser.find_element_by_id(“loginName”)。

通过 name 定位：我们可以使用 find_element_by_name() 函数，比如我们想要对 name=key_word 的元素进行定位，就可以使用 browser.find_element_by_name(“key_word”)。

通过 class 定位：可以使用 find_element_by_class_name() 函数。

通过 tag 定位：使用 find_element_by_tag_name() 函数。

通过 link 上的完整文本定位：使用 find_element_by_link_text() 函数。

通过 link 上的部分文本定位：使用 find_element_by_partial_link_text() 函数。有时候超链接上的文本很长，我们通过查找部分文本内容就可以定位。

通过 XPath 定位：使用 find_element_by_xpath() 函数。使用 XPath 定位的通用性比较好，因为当 id、name、class 为多个，或者元素没有这些属性值的时候，XPath 定位可以帮我们完成任务。

通过 CSS 定位：使用 find_element_by_css_selector() 函数。CSS 定位也是常用的定位方法，相比于 XPath 来说更简洁。

在我们获取某个元素之后，就可以对这个元素进行操作了，对元素进行的操作包括：

清空输入框的内容：使用 clear() 函数；

在输入框中输入内容：使用 send_keys(content) 函数传入要输入的文本；

点击按钮：使用 click() 函数，如果元素是个按钮或者链接的时候，可以点击操作；

提交表单：使用 submit() 函数，元素对象为一个表单的时候，可以提交表单；

了解 WebDriver 元素定位和功能之后，我们模拟一下微博的自动登录，具体代码如下：

from selenium import webdriver

import time

browser = webdriver.Chrome()

# 登录微博

def weibo_login(username, password):

     # 打开微博登录页

     browser.get('https://passport.weibo.cn/signin/login')

     browser.implicitly_wait(5)

     time.sleep(1)

     # 填写登录信息：用户名、密码

     browser.find_element_by_id("loginName").send_keys(username)

     browser.find_element_by_id("loginPassword").send_keys(password)

     time.sleep(1)

     # 点击登录

     browser.find_element_by_id("loginAction").click()

     time.sleep(1)

# 设置用户名、密码

username = 'XXXX'

password = "XXXX"

weibo_login(username, password)

需要说明的是，你需要填写自己的微博用户名和密码（对应 username 和 password）。

通过 HTML 标签分析，我们能看到 id=loginName 和 id=loginPassword 对应着用户名和密码的输入框，获取这两个元素之后，使用 send_keys() 设置用户名和密码，然后通过 id=loginAction 找到登录按钮，获取该元素，使用 click() 函数点击提交。

这样我们就完成了微博的自动化登录，具体效果如下：

微博自动化运营：加关注，写评论，发微博

我们已经基本了解了 Selenium WebDriver 是如何获取指定元素，并且对元素进行操作的。下面我们撰写一下微博自动加关注，写评论，发微博的功能代码，具体模块如下：

我们刚才完成了 weibo_login 微博登录模块的编写，现在来看下加关注的功能，具体代码如下：

# 添加指定的用户

def add_follow(uid):

    browser.get('https://m.weibo.com/u/'+str(uid))

    time.sleep(1)

    #browser.find_element_by_id("follow").click()

    follow_button = browser.find_element_by_xpath('//div[@class="m-add-box m-followBtn"]')

    follow_button.click()

    time.sleep(1)

    # 选择分组

    group_button = browser.find_element_by_xpath('//div[@class="m-btn m-btn-white m-btn-text-black"]')

    group_button.click()

    time.sleep(1)

# 每天学点心理学 UID

uid = '1890826225'

add_follow(uid)

这里有两点你需要注意。

第一点是如何找到用户的 UID 呢？

在微博中，用户是用 UID 做唯一标识的。代码中我随机指定了一个 UID，你也可以指定其他的 UID。那么如何获取用户的 UID 的呢？

你可以点击任何一个微博用户，查看他的 URL 链接，比如链接是https://weibo.com/u/5020181423 ，那么 u 后面的数字 5020181423 即为用户的 UID。你也可能遇到https://weibo.com/ixinli（每天学点心理学）这样的链接情况，那么通过查看他的粉丝即可以显示出 UID，比如这个微博的粉丝链接是https://weibo.com/1890826225/fans，那么 UID 对应的就是 1890826225。

第二个需要注意的是使用 XPath 定位元素定位。

我们如何找到“关注”这个按钮的元素标识呢？在 Chrome 浏览器中，在“关注”按钮用鼠标右键点击，选择“检查”查看这个元素对应的代码。通过分析，你能看到这个元素的 div 标签中的 class 属性为 m-add-box m-followBtn，那么你可以通过 find_element_by_xpath(’//div[@class=“m-add-box m-followBtn”]’) 获取这个元素。关注之后，程序会弹出选择分组的页面，如下所示。

通过同样的方法，你可以查看“取消”这个按钮对应的 HTML 标签特征，然后通过 find_element_by_xpath 定位，使用 click() 函数提交。这样我们就关注了一个指定的用户。

接下来，我们继续完成写评论和发微博的模块代码：

# 给指定某条微博添加内容

def add_comment(weibo_url, content):

    browser.get(weibo_url)

    browser.implicitly_wait(5)

    content_textarea = browser.find_element_by_css_selector("textarea.W_input").clear()

    content_textarea = browser.find_element_by_css_selector("textarea.W_input").send_keys(content)

    time.sleep(2)

    comment_button = browser.find_element_by_css_selector(".W_btn_a").click()

    time.sleep(1)

# 发文字微博

def post_weibo(content):

    # 跳转到用户的首页

    browser.get('https://weibo.com')

    browser.implicitly_wait(5)

    # 点击右上角的发布按钮

    post_button = browser.find_element_by_css_selector("[node-type='publish']").click()

    # 在弹出的文本框中输入内容

    content_textarea = browser.find_element_by_css_selector("textarea.W_input").send_keys(content)

    time.sleep(2)

    # 点击发布按钮

    post_button = browser.find_element_by_css_selector("[node-type='submit']").click()

    time.sleep(1)

# 给指定的微博写评论

weibo_url = 'https://weibo.com/1890826225/HjjqSahwl'

content = 'Gook Luck! 好运已上路！'

# 自动发微博

content = '每天学点心理学'

post_weibo(content)

这个环节里，同样也有一些需要说明的地方。

如何找到某条微博的链接呢？你可以在某个用户微博页面中点击时间的链接，这样就可以获取这条微博的链接。

如何定位评论区中输入框元素的位置呢？

我们将鼠标移动到评论框中，在 Chrome 浏览器中点击右键“查看”，可以看到这个 textarea 的 class 是 W_input，使用 find_element_by_css_selector 函数进行定位，然后通过 send_keys 设置内容。

最后就是发微博的流程。我们可以观察到点击微博页面的右上角的按钮后，会弹出一个发微博的文本框，设置好内容，点击发送即可。发微博的函数和写评论的代码类似，使用 find_element_by_css_selector() 函数定位，通过 send_keys() 设置内容的设置，最后通过 click() 发送。

上面的代码你可以自己模拟下，在实际运行过程中，你可能会遇到各种情况，比如下面这种情况：

微博自动化运营是一个系统的工程，需要考虑到各种情况，比如相同的内容发布需要间隔 10 分钟以上；关注了一个用户之后，就无法对他二次关注，可以判断是否已经关注过，再关注操作；因为操作频繁导致需要输入验证码的情况等。

另外，微博自动化运营只是自动化运营的开始，实际上在微信上我们也可以做同样的操作。比如监听微信群的消息，自动发微信等。实际要考虑的问题比微博的要复杂。

总结

今天我带你进行了微博自动化运营的实战，通过实战你对使用 Selenium WebDriver 应该更有了解，包括如何定位指定的元素，对元素进行各种操作等。同时我们使用了 implicitly_wait 函数以及 time.sleep() 函数让浏览器和程序等待一段时间，完成数据加载之后再进行后续的操作，这样就避免了数据没有加载完，导致获取不到指定元素的情况。

通过三个模块（加关注、写评论、发微博）的编写，你能了解如何使用工具完成自动化的操作。在实际的过程中，可能会遇到各种复杂情况，这些都需要你在运行过程中慢慢体会。

自动化运营是个细致的活儿，我在之前的加餐文章中也提到过。如果真的想要实现自动化，还需要解决反垃圾的清理问题等，你可以再回顾一下。

最后留两道题吧。今天我们对微博加关注这一模块编写了代码，同样我们也可能会对某个指定用户的 UID 做取消关注的操作，请你使用今天讲的元素定位和操作功能，编写相应的代码。

通过今天自动化测试工具的学习，你有怎样的收获和总结呢？

欢迎在评论区与我分享你的答案，也欢迎点击”请朋友读“，把这篇文章分享给你的朋友或者同事，一起实战交流一下。

38丨数据可视化实战：如何给毛不易的歌曲做词云展示？

今天我们做一个数据可视化的项目。

我们经常需要对分析的数据提取常用词，做词云展示。比如一些互联网公司会抓取用户的画像，或者每日讨论话题的关键词，形成词云并进行展示。再或者，假如你喜欢某个歌手，想了解这个歌手创作的歌曲中经常用到哪些词语，词云就是个很好的工具。最后，只需要将词云生成一张图片就可以直观地看到结果。

那么在今天的实战项目里，有 3 个目标需要掌握：

掌握词云分析工具，并进行可视化呈现；

掌握 Python 爬虫，对网页的数据进行爬取；

掌握 XPath 工具，分析提取想要的元素 。

如何制作词云

首先我们需要了解什么是词云。词云也叫文字云，它帮助我们统计文本中高频出现的词，过滤掉某些常用词（比如“作曲”“作词”），将文本中的重要关键词进行可视化，方便分析者更好更快地了解文本的重点，同时还具有一定的美观度。

Python 提供了词云工具 WordCloud，使用 pip install wordcloud 安装后，就可以创建一个词云，构造方法如下：

wc = WordCloud(

    background_color='white',# 设置背景颜色

    mask=backgroud_Image,# 设置背景图片

    font_path='./SimHei.ttf',  # 设置字体，针对中文的情况需要设置中文字体，否则显示乱码

    max_words=100, # 设置最大的字数

    stopwords=STOPWORDS,# 设置停用词

	max_font_size=150,# 设置字体最大值

	width=2000,# 设置画布的宽度

	height=1200,# 设置画布的高度

    random_state=30# 设置多少种随机状态，即多少种颜色

)

实际上 WordCloud 还有更多的构造参数，代码里展示的是一些主要参数，我在代码中都有注释，你可以自己看下。

创建好 WordCloud 类之后，就可以使用 wordcloud=generate(text) 方法生成词云，传入的参数 text 代表你要分析的文本，最后使用 wordcloud.tofile(“a.jpg”) 函数，将得到的词云图像直接保存为图片格式文件。

你也可以使用 Python 的可视化工具 Matplotlib 进行显示，方法如下：

import matplotlib.pyplot as plt

plt.imshow(wordcloud)

plt.axis("off")

plt.show()

需要注意的是，我们不需要显示 X 轴和 Y 轴的坐标，使用 plt.axis(“off”) 可以将坐标轴关闭。

了解了如何使用词云工具 WordCloud 之后，我们将专栏前 15 节的标题进行词云可视化，具体的代码如下：

#-*- coding:utf-8 -*-

from wordcloud import WordCloud

import matplotlib.pyplot as plt

import jieba

from PIL import Image

import numpy as np

# 生成词云

def create_word_cloud(f):

     print('根据词频计算词云')

     text = " ".join(jieba.cut(f,cut_all=False, HMM=True))

     wc = WordCloud(

           font_path="./SimHei.ttf",

           max_words=100,

           width=2000,

           height=1200,

    )

     wordcloud = wc.generate(text)

     # 写词云图片

     wordcloud.to_file("wordcloud.jpg")

     # 显示词云文件

     plt.imshow(wordcloud)

     plt.axis("off")

     plt.show()

f = '数据分析全景图及修炼指南\

学习数据挖掘的最佳学习路径是什么？\

Python 基础语法：开始你的 Python 之旅\

Python 科学计算：NumPy\

Python 科学计算：Pandas\

学习数据分析要掌握哪些基本概念？\

用户画像：标签化就是数据的抽象能力\

数据采集：如何自动化采集数据？\

数据采集：如何用八爪鱼采集微博上的“D&G”评论？\

Python 爬虫：如何自动化下载王祖贤海报？\

数据清洗：数据科学家 80% 时间都花费在了这里？\

数据集成：这些大号一共 20 亿粉丝？\

数据变换：大学成绩要求正态分布合理么？\

数据可视化：掌握数据领域的万金油技能\

一次学会 Python 数据可视化的 10 种技能'

运行结果：

你能从结果中看出，还是有一些常用词显示出来了，比如“什么”“要求”“这些”等，我们可以把这些词设置为停用词，编写 remove_stop_words 函数，从文本中去掉：

# 去掉停用词

def remove_stop_words(f):

     stop_words = ['学会', '就是', '什么']

     for stop_word in stop_words:

           f = f.replace(stop_word, '')

     return f

然后在结算词云前调用 f = remove_stop_words(f) 方法，最后运行可以得到如下的结果：

你能看出，去掉停用词之后的词云更加清晰，更能体现前 15 章的主要内容。

给毛不易的歌词制作词云

假设我们现在要给毛不易的歌词做个词云，那么需要怎么做呢？我们先把整个项目的流程梳理下：

在准备阶段：我们主要使用 Python 爬虫获取 HTML，用 XPath 对歌曲的 ID、名称进行解析，然后通过网易云音乐的 API 接口获取每首歌的歌词，最后将所有的歌词合并得到一个变量。

在词云分析阶段，我们需要创建 WordCloud 词云类，分析得到的歌词文本，最后可视化。

基于上面的流程，编写代码如下：

# -*- coding:utf-8 -*-

# 网易云音乐 通过歌手 ID，生成该歌手的词云

import requests

import sys

import re

import os

from wordcloud import WordCloud

import matplotlib.pyplot as plt

import jieba

from PIL import Image

import numpy as np

from lxml import etree

headers = {

       'Referer'  :'http://music.163.com',

       'Host'     :'music.163.com',

       'Accept'   :'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',

       'User-Agent':'Chrome/10'

    }

# 得到某一首歌的歌词

def get_song_lyric(headers,lyric_url):

    res = requests.request('GET', lyric_url, headers=headers)

    if 'lrc' in res.json():

       lyric = res.json()['lrc']['lyric']

       new_lyric = re.sub(r'[\d:.[\]]','',lyric)

       return new_lyric

    else:

       return ''

       print(res.json())

# 去掉停用词

def remove_stop_words(f):

    stop_words = ['作词', '作曲', '编曲', 'Arranger', '录音', '混音', '人声', 'Vocal', '弦乐', 'Keyboard', '键盘', '编辑', '助理', 'Assistants', 'Mixing', 'Editing', 'Recording', '音乐', '制作', 'Producer', '发行', 'produced', 'and', 'distributed']

    for stop_word in stop_words:

       f = f.replace(stop_word, '')

    return f

# 生成词云

def create_word_cloud(f):

    print('根据词频，开始生成词云!')

    f = remove_stop_words(f)

    cut_text = " ".join(jieba.cut(f,cut_all=False, HMM=True))

    wc = WordCloud(

       font_path="./wc.ttf",

       max_words=100,

       width=2000,

       height=1200,

    )

    print(cut_text)

    wordcloud = wc.generate(cut_text)

    # 写词云图片

    wordcloud.to_file("wordcloud.jpg")

    # 显示词云文件

    plt.imshow(wordcloud)

    plt.axis("off")

    plt.show()

# 得到指定歌手页面 热门前 50 的歌曲 ID，歌曲名

def get_songs(artist_id):

    page_url = 'https://music.163.com/artist?id=' + artist_id

    # 获取网页 HTML

    res = requests.request('GET', page_url, headers=headers)

    # 用 XPath 解析 前 50 首热门歌曲

    html = etree.HTML(res.text)

    href_xpath = "//*[@id='hotsong-list']//a/@href"

    name_xpath = "//*[@id='hotsong-list']//a/text()"

    hrefs = html.xpath(href_xpath)

    names = html.xpath(name_xpath)

    # 设置热门歌曲的 ID，歌曲名称

    song_ids = []

    song_names = []

    for href, name in zip(hrefs, names):

       song_ids.append(href[9:])

       song_names.append(name)

       print(href, '  ', name)

    return song_ids, song_names

# 设置歌手 ID，毛不易为 12138269

artist_id = '12138269'

[song_ids, song_names] = get_songs(artist_id)

# 所有歌词

all_word = ''

# 获取每首歌歌词

for (song_id, song_name) in zip(song_ids, song_names):

    # 歌词 API URL

    lyric_url = 'http://music.163.com/api/song/lyric?os=pc&id=' + song_id + '&lv=-1&kv=-1&tv=-1'

    lyric = get_song_lyric(headers, lyric_url)

    all_word = all_word + ' ' + lyric

    print(song_name)

# 根据词频 生成词云

create_word_cloud(all_word)

运行结果：

这个过程里有一些模块，我需要说明下。

首先我编写 get_songs 函数，可以得到指定歌手页面中热门前 50 的歌曲 ID，歌曲名。在这个函数里，我使用 requests.request 函数获取毛不易歌手页面的 HTML。这里需要传入指定的请求头（headers），否则获取不到完整的信息。然后用 XPath 解析并获取指定的内容，这个模块中，我想获取的是歌曲的链接和名称。

其中歌曲的链接类似 /song?id=536099160 这种形式，你能看到字符串第 9 位之后，就是歌曲的 ID。

有关 XPath 解析的内容，我在第 10 节讲到过，如果你忘记了，可以看一下。一般来说，XPath 解析 99% 的可能都是以 // 开头，因为你需要获取所有符合这个 XPath 的内容。我们通过分析 HTML 代码，能看到一个关键的部分：id=‘hotsong-list’。这个代表热门歌曲列表，也正是我们想要解析的内容。我们想要获取这个热门歌曲列表下面所有的链接，XPath 解析就可以写成 //*[@id=‘hotsong-list’]//a。然后你能看到歌曲链接是 href 属性，歌曲名称是这个链接的文本。

获得歌曲 ID 之后，我们还需要知道这个歌曲的歌词，对应代码中的 get_song_lyric 函数，在这个函数里调用了网易云的歌词 API 接口，比如http://music.163.com/api/song/lyric?os=pc&id=536099160&lv=-1&kv=-1&tv=-1。

你能看到歌词文件里面还保存了时间信息，我们需要去掉这部分。因此我使用了 re.sub 函数，通过正则表达式匹配，将 [] 中数字信息去掉，方法为 re.sub(r’[\d:.[]]’,’’,lyric)。

最后我们还需要设置一些歌词中常用的停用词，比如“作词”“作曲”“编曲”等，编写 remove_stop_words 函数，将歌词文本中的停用词去掉。

最后编写 create_word_cloud 函数，通过歌词文本生成词云文件。

总结

今天我给你讲了词云的可视化。在这个实战中，你能看到前期的数据准备在整个过程中占了很大一部分。如果你用 Python 作为数据采集工具，就需要掌握 Python 爬虫和 XPath 解析，掌握这两个工具最好的方式就是多做练习。

我们今天讲到了词云工具 WordCloud，它是一个很好用的 Python 工具，可以将复杂的文本通过词云图的方式呈现。需要注意的是，当我们需要使用中文字体的时候，比如黑体 SimHei，就可以将 WordCloud 中的 font_path 属性设置为 SimHei.ttf，你也可以设置其他艺术字体，在给毛不易的歌词做词云展示的时候，我们就用到了艺术字体。

你可以从 GitHub 上下载字体和代码：https://github.com/cystanford/word_cloud。

最后给你留一道思考题吧。我抓取了毛不易主页的歌词，是以歌手主页为粒度进行的词云可视化。实际上网易云音乐也有歌单的 API，比如http://music.163.com/api/playlist/detail?id=753776811。你能不能编写代码对歌单做个词云展示（比如歌单 ID 为 753776811）呢？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

39丨数据挖掘实战（1）：信用卡违约率分析

今天我来带你做一个数据挖掘的项目。在数据挖掘的过程中，我们经常会遇到一些问题，比如：如何选择各种分类器，到底选择哪个分类算法，是 SVM，决策树，还是 KNN？如何优化分类器的参数，以便得到更好的分类准确率？

这两个问题，是数据挖掘核心的问题。当然对于一个新的项目，我们还有其他的问题需要了解，比如掌握数据探索和数据可视化的方式，还需要对数据的完整性和质量做评估。这些内容我在之前的课程中都有讲到过。

今天的学习主要围绕下面的三个目标，并通过它们完成信用卡违约率项目的实战，这三个目标分别是：

创建各种分类器，包括已经掌握的 SVM、决策树、KNN 分类器，以及随机森林分类器；

掌握 GridSearchCV 工具，优化算法模型的参数；

使用 Pipeline 管道机制进行流水线作业。因为在做分类之前，我们还需要一些准备过程，比如数据规范化，或者数据降维等。

构建随机森林分类器

在算法篇中，我主要讲了数据挖掘十大经典算法。实际工作中，你也可能会用到随机森林。

随机森林的英文是 Random Forest，英文简写是 RF。它实际上是一个包含多个决策树的分类器，每一个子分类器都是一棵 CART 分类回归树。所以随机森林既可以做分类，又可以做回归。当它做分类的时候，输出结果是每个子分类器的分类结果中最多的那个。你可以理解是每个分类器都做投票，取投票最多的那个结果。当它做回归的时候，输出结果是每棵 CART 树的回归结果的平均值。

在 sklearn 中，我们使用 RandomForestClassifier() 构造随机森林模型，函数里有一些常用的构造参数：

当我们创建好之后，就可以使用 fit 函数拟合，使用 predict 函数预测。

使用 GridSearchCV 工具对模型参数进行调优

在做分类算法的时候，我们需要经常调节网络参数（对应上面的构造参数），目的是得到更好的分类结果。实际上一个分类算法有很多参数，取值范围也比较广，那么该如何调优呢？

Python 给我们提供了一个很好用的工具 GridSearchCV，它是 Python 的参数自动搜索模块。我们只要告诉它想要调优的参数有哪些以及参数的取值范围，它就会把所有的情况都跑一遍，然后告诉我们哪个参数是最优的，结果如何。

使用 GridSearchCV 模块需要先引用工具包，方法如下：

from sklearn.model_selection import GridSearchCV

然后我们使用 GridSearchCV(estimator, param_grid, cv=None, scoring=None) 构造参数的自动搜索模块，这里有一些主要的参数需要说明下：

构造完 GridSearchCV 之后，我们就可以使用 fit 函数拟合训练，使用 predict 函数预测，这时预测采用的是最优参数情况下的分类器。

这里举一个简单的例子，我们用 sklearn 自带的 IRIS 数据集，采用随机森林对 IRIS 数据分类。假设我们想知道 n_estimators 在 1-10 的范围内取哪个值的分类结果最好，可以编写代码：

# -*- coding: utf-8 -*-

# 使用 RandomForest 对 IRIS 数据集进行分类

# 利用 GridSearchCV 寻找最优参数

from sklearn.ensemble import RandomForestClassifier

from sklearn.model_selection import GridSearchCV

from sklearn.datasets import load_iris

rf = RandomForestClassifier()

parameters = {"n_estimators": range(1,11)}

iris = load_iris()

# 使用 GridSearchCV 进行参数调优

clf = GridSearchCV(estimator=rf, param_grid=parameters)

# 对 iris 数据集进行分类

clf.fit(iris.data, iris.target)

print(" 最优分数： %.4lf" %clf.best_score_)

print(" 最优参数：", clf.best_params_)

运行结果如下：

最优分数： 0.9667

最优参数： {'n_estimators': 6}

你能看到当我们采用随机森林作为分类器的时候，最优准确率是 0.9667，当 n_estimators=6 的时候，是最优参数，也就是随机森林一共有 6 个子决策树。

使用 Pipeline 管道机制进行流水线作业

做分类的时候往往都是有步骤的，比如先对数据进行规范化处理，你也可以用 PCA 方法（一种常用的降维方法）对数据降维，最后使用分类器分类。

Python 有一种 Pipeline 管道机制。管道机制就是让我们把每一步都按顺序列下来，从而创建 Pipeline 流水线作业。每一步都采用 (‘名称’, 步骤) 的方式来表示。

我们需要先采用 StandardScaler 方法对数据规范化，即采用数据规范化为均值为 0，方差为 1 的正态分布，然后采用 PCA 方法对数据进行降维，最后采用随机森林进行分类。

具体代码如下：

from sklearn.model_selection import GridSearchCV

pipeline = Pipeline([

        ('scaler', StandardScaler()),

        ('pca', PCA()),

        ('randomforestclassifier', RandomForestClassifier())

])

那么我们现在采用 Pipeline 管道机制，用随机森林对 IRIS 数据集做一下分类。先用 StandardScaler 方法对数据规范化，然后再用随机森林分类，编写代码如下：

# -*- coding: utf-8 -*-

# 使用 RandomForest 对 IRIS 数据集进行分类

# 利用 GridSearchCV 寻找最优参数, 使用 Pipeline 进行流水作业

from sklearn.ensemble import RandomForestClassifier

from sklearn.model_selection import GridSearchCV

from sklearn.datasets import load_iris

from sklearn.preprocessing import StandardScaler

from sklearn.pipeline import Pipeline

rf = RandomForestClassifier()

parameters = {"randomforestclassifier__n_estimators": range(1,11)}

iris = load_iris()

pipeline = Pipeline([

        ('scaler', StandardScaler()),

        ('randomforestclassifier', rf)

])

# 使用 GridSearchCV 进行参数调优

clf = GridSearchCV(estimator=pipeline, param_grid=parameters)

# 对 iris 数据集进行分类

clf.fit(iris.data, iris.target)

print(" 最优分数： %.4lf" %clf.best_score_)

print(" 最优参数：", clf.best_params_)

运行结果：

最优分数： 0.9667

最优参数： {'randomforestclassifier__n_estimators': 9}

你能看到是否采用数据规范化对结果还是有一些影响的，有了 GridSearchCV 和 Pipeline 这两个工具之后，我们在使用分类器的时候就会方便很多。

对信用卡违约率进行分析

我们现在来做一个信用卡违约率的项目，这个数据集你可以从 GitHub 上下载：https://github.com/cystanford/credit_default。

这个数据集是台湾某银行 2005 年 4 月到 9 月的信用卡数据，数据集一共包括 25 个字段，具体含义如下：

现在我们的目标是要针对这个数据集构建一个分析信用卡违约率的分类器。具体选择哪个分类器，以及分类器的参数如何优化，我们可以用 GridSearchCV 这个工具跑一遍。

先梳理下整个项目的流程：

加载数据；

准备阶段：探索数据，采用数据可视化方式可以让我们对数据有更直观的了解，比如我们想要了解信用卡违约率和不违约率的人数。因为数据集没有专门的测试集，我们还需要使用 train_test_split 划分数据集。

分类阶段：之所以把数据规范化放到这个阶段，是因为我们可以使用 Pipeline 管道机制，将数据规范化设置为第一步，分类为第二步。因为我们不知道采用哪个分类器效果好，所以我们需要多用几个分类器，比如 SVM、决策树、随机森林和 KNN。然后通过 GridSearchCV 工具，找到每个分类器的最优参数和最优分数，最终找到最适合这个项目的分类器和该分类器的参数。

基于上面的流程，具体代码如下：

# -*- coding: utf-8 -*-

# 信用卡违约率分析

import pandas as pd

from sklearn.model_selection import learning_curve, train_test_split,GridSearchCV

from sklearn.preprocessing import StandardScaler

from sklearn.pipeline import Pipeline

from sklearn.metrics import accuracy_score

from sklearn.svm import SVC

from sklearn.tree import DecisionTreeClassifier

from sklearn.ensemble import RandomForestClassifier

from sklearn.neighbors import KNeighborsClassifier

from matplotlib import pyplot as plt

import seaborn as sns

# 数据加载

data = data = pd.read_csv('./UCI_Credit_Card.csv')

# 数据探索

print(data.shape) # 查看数据集大小

print(data.describe()) # 数据集概览

# 查看下一个月违约率的情况

next_month = data['default.payment.next.month'].value_counts()

print(next_month)

df = pd.DataFrame({'default.payment.next.month': next_month.index,'values': next_month.values})

plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签

plt.figure(figsize = (6,6))

plt.title('信用卡违约率客户\n (违约：1，守约：0)')

sns.set_color_codes("pastel")

sns.barplot(x = 'default.payment.next.month', y="values", data=df)

locs, labels = plt.xticks()

plt.show()

# 特征选择，去掉 ID 字段、最后一个结果字段即可

data.drop(['ID'], inplace=True, axis =1) #ID 这个字段没有用

target = data['default.payment.next.month'].values

columns = data.columns.tolist()

columns.remove('default.payment.next.month')

features = data[columns].values

# 30% 作为测试集，其余作为训练集

train_x, test_x, train_y, test_y = train_test_split(features, target, test_size=0.30, stratify = target, random_state = 1)

# 构造各种分类器

classifiers = [

    SVC(random_state = 1, kernel = 'rbf'),

    DecisionTreeClassifier(random_state = 1, criterion = 'gini'),

    RandomForestClassifier(random_state = 1, criterion = 'gini'),

    KNeighborsClassifier(metric = 'minkowski'),

]

# 分类器名称

classifier_names = [

            'svc',

            'decisiontreeclassifier',

            'randomforestclassifier',

            'kneighborsclassifier',

]

# 分类器参数

classifier_param_grid = [

            {'svc__C':[1], 'svc__gamma':[0.01]},

            {'decisiontreeclassifier__max_depth':[6,9,11]},

            {'randomforestclassifier__n_estimators':[3,5,6]} ,

            {'kneighborsclassifier__n_neighbors':[4,6,8]},

]

# 对具体的分类器进行 GridSearchCV 参数调优

def GridSearchCV_work(pipeline, train_x, train_y, test_x, test_y, param_grid, score = 'accuracy'):

    response = {}

    gridsearch = GridSearchCV(estimator = pipeline, param_grid = param_grid, scoring = score)

    # 寻找最优的参数 和最优的准确率分数

    search = gridsearch.fit(train_x, train_y)

    print("GridSearch 最优参数：", search.best_params_)

    print("GridSearch 最优分数： %0.4lf" %search.best_score_)

	predict_y = gridsearch.predict(test_x)

    print(" 准确率 %0.4lf" %accuracy_score(test_y, predict_y))

    response['predict_y'] = predict_y

    response['accuracy_score'] = accuracy_score(test_y,predict_y)

    return response

for model, model_name, model_param_grid in zip(classifiers, classifier_names, classifier_param_grid):

    pipeline = Pipeline([

            ('scaler', StandardScaler()),

            (model_name, model)

    ])

    result = GridSearchCV_work(pipeline, train_x, train_y, test_x, test_y, model_param_grid , score = 'accuracy')

运行结果：

(30000, 25)

                 ID             ...              default.payment.next.month

count  30000.000000             ...                            30000.000000

mean   15000.500000             ...                                0.221200

std     8660.398374             ...                                0.415062

min        1.000000             ...                                0.000000

25%     7500.750000             ...                                0.000000

50%    15000.500000             ...                                0.000000

75%    22500.250000             ...                                0.000000

max    30000.000000             ...                                1.000000

[8 rows x 25 columns]

GridSearch 最优参数： {'svc__C': 1, 'svc__gamma': 0.01}

GridSearch 最优分数： 0.8174

准确率 0.8172

GridSearch 最优参数： {'decisiontreeclassifier__max_depth': 6}

GridSearch 最优分数： 0.8186

准确率 0.8113

GridSearch 最优参数： {'randomforestclassifier__n_estimators': 6}

GridSearch 最优分数： 0.7998

准确率 0.7994

GridSearch 最优参数： {'kneighborsclassifier__n_neighbors': 8}

GridSearch 最优分数： 0.8040

准确率 0.8036

从结果中，我们能看到 SVM 分类器的准确率最高，测试准确率为 0.8172。

在决策树分类中，我设置了 3 种最大深度，当最大深度 =6 时结果最优，测试准确率为 0.8113；在随机森林分类中，我设置了 3 个决策树个数的取值，取值为 6 时结果最优，测试准确率为 0.7994；在 KNN 分类中，我设置了 3 个 n 的取值，取值为 8 时结果最优，测试准确率为 0.8036。

总结

今天我给你讲了随机森林的概念及工具的使用，另外针对数据挖掘算法中经常采用的参数调优，也介绍了 GridSearchCV 工具这个利器。并将这两者结合起来，在信用卡违约分析这个项目中进行了使用。

很多时候，我们不知道该采用哪种分类算法更适合。即便是对于一种分类算法，也有很多参数可以调优，每个参数都有一定的取值范围。我们可以把想要采用的分类器，以及这些参数的取值范围都设置到数组里，然后使用 GridSearchCV 工具进行调优。

今天我们讲了如何使用 GridSearchCV 做参数调优，你可以说说你的理解，如果有使用的经验也可以分享下。

另外针对信用卡违约率分析这个项目，我们使用了 SVM、决策树、随机森林和 KNN 分类器，你能不能编写代码使用 AdaBoost 分类器做分类呢？其中 n_estimators 的取值有 10、50、100 三种可能，你可以使用 GridSearchCV 运行看看最优参数是多少，测试准确率是多少？

欢迎你在评论区与我分享你的答案，如果有问题也可以写在评论区。如果你觉得这篇文章有价值，欢迎把它分享给你的朋友或者同事。

40丨数据挖掘实战（2）：信用卡诈骗分析

上一篇文章中，我们用随机森林以及之前讲过的 SVM、决策树和 KNN 分类器对信用卡违约数据进行了分析，这节课我们来研究下信用卡欺诈。

相比于信用卡违约的比例，信用卡欺诈的比例更小，但是危害极大。如何通过以往的交易数据分析出每笔交易是否正常，是否存在盗刷风险是我们这次项目的目标。

通过今天的学习，你需要掌握以下几个方面：

了解逻辑回归分类，以及如何在 sklearn 中使用它；

信用卡欺诈属于二分类问题，欺诈交易在所有交易中的比例很小，对于这种数据不平衡的情况，到底采用什么样的模型评估标准会更准确；

完成信用卡欺诈分析的实战项目，并通过数据可视化对数据探索和模型结果评估进一步加强了解。

构建逻辑回归分类器

逻辑回归虽然不在我们讲解的十大经典数据挖掘算法里面，但也是常用的数据挖掘算法。

逻辑回归，也叫作 logistic 回归。虽然名字中带有“回归”，但它实际上是分类方法，主要解决的是二分类问题，当然它也可以解决多分类问题，只是二分类更常见一些。

在逻辑回归中使用了 Logistic 函数，也称为 Sigmoid 函数。Sigmoid 函数是在深度学习中经常用到的函数之一，函数公式为：

函数的图形如下所示，类似 S 状：

你能看出 g(z) 的结果在 0-1 之间，当 z 越大的时候，g(z) 越大，当 z 趋近于无穷大的时候，g(z) 趋近于 1。同样当 z 趋近于无穷小的时候，g(z) 趋近于 0。同时，函数值以 0.5 为中心。

为什么逻辑回归算法是基于 Sigmoid 函数实现的呢？你可以这样理解：我们要实现一个二分类任务，0 即为不发生，1 即为发生。我们给定一些历史数据 X 和 y。其中 X 代表样本的 n 个特征，y 代表正例和负例，也就是 0 或 1 的取值。通过历史样本的学习，我们可以得到一个模型，当给定新的 X 的时候，可以预测出 y。这里我们得到的 y 是一个预测的概率，通常不是 0% 和 100%，而是中间的取值，那么我们就可以认为概率大于 50% 的时候，即为发生（正例），概率小于 50% 的时候，即为不发生（负例）。这样就完成了二分类的预测。

逻辑回归模型的求解这里不做介绍，我们来看下如何使用 sklearn 中的逻辑回归工具。在 sklearn 中，我们使用 LogisticRegression() 函数构建逻辑回归分类器，函数里有一些常用的构造参数：

penalty：惩罚项，取值为 l1 或 l2，默认为 l2。当模型参数满足高斯分布的时候，使用 l2，当模型参数满足拉普拉斯分布的时候，使用 l1；

solver：代表的是逻辑回归损失函数的优化方法。有 5 个参数可选，分别为 liblinear、lbfgs、newton-cg、sag 和 saga。默认为 liblinear，适用于数据量小的数据集，当数据量大的时候可以选用 sag 或 saga 方法。

max_iter：算法收敛的最大迭代次数，默认为 10。

n_jobs：拟合和预测的时候 CPU 的核数，默认是 1，也可以是整数，如果是 -1 则代表 CPU 的核数。

当我们创建好之后，就可以使用 fit 函数拟合，使用 predict 函数预测。

模型评估指标

我们之前对模型做评估时，通常采用的是准确率 (accuracy)，它指的是分类器正确分类的样本数与总体样本数之间的比例。这个指标对大部分的分类情况是有效的，不过当分类结果严重不平衡的时候，准确率很难反应模型的好坏。

举个例子，对于机场安检中恐怖分子的判断，就不能采用准确率对模型进行评估。我们知道恐怖分子的比例是极低的，因此当我们用准确率做判断时，如果准确率高达 99.999%，就说明这个模型一定好么？

其实正因为现实生活中恐怖分子的比例极低，就算我们不能识别出一个恐怖分子，也会得到非常高的准确率。因为准确率的评判标准是正确分类的样本个数与总样本数之间的比例。因此非恐怖分子的比例会很高，就算我们识别不出来恐怖分子，正确分类的个数占总样本的比例也会很高，也就是准确率高。

实际上我们应该更关注恐怖分子的识别，这里先介绍下数据预测的四种情况：TP、FP、TN、FN。我们用第二个字母 P 或 N 代表预测为正例还是负例，P 为正，N 为负。第一个字母 T 或 F 代表的是预测结果是否正确，T 为正确，F 为错误。

所以四种情况分别为：

TP：预测为正，判断正确；

FP：预测为正，判断错误；

TN：预测为负，判断正确；

FN：预测为负，判断错误。

我们知道样本总数 =TP+FP+TN+FN，预测正确的样本数为 TP+TN，因此准确率 Accuracy = (TP+TN)/(TP+TN+FN+FP)。

实际上，对于分类不平衡的情况，有两个指标非常重要，它们分别是精确度和召回率。

精确率 P = TP/ (TP+FP)，对应上面恐怖分子这个例子，在所有判断为恐怖分子的人数中，真正是恐怖分子的比例。

召回率 R = TP/ (TP+FN)，也称为查全率。代表的是恐怖分子被正确识别出来的个数与恐怖分子总数的比例。

为什么要统计召回率和精确率这两个指标呢？假设我们只统计召回率，当召回率等于 100% 的时候，模型是否真的好呢？

举个例子，假设我们把机场所有的人都认为是恐怖分子，恐怖分子都会被正确识别，这个数字与恐怖分子的总数比例等于 100%，但是这个结果是没有意义的。如果我们认为机场里所有人都是恐怖分子的话，那么非恐怖分子（极高比例）都会认为是恐怖分子，误判率太高了，所以我们还需要统计精确率作为召回率的补充。

实际上有一个指标综合了精确率和召回率，可以更好地评估模型的好坏。这个指标叫做 F1，用公式表示为：

F1 作为精确率 P 和召回率 R 的调和平均，数值越大代表模型的结果越好。

对信用卡违约率进行分析

我们来看一个信用卡欺诈分析的项目，这个数据集你可以从百度网盘（因为数据集大于 100M，所以采用了网盘）中下载：

链接：https://pan.baidu.com/s/14F8WuX0ZJntdB_r1EC08HA 提取码：58gp

数据集包括了 2013 年 9 月份两天时间内的信用卡交易数据，284807 笔交易中，一共有 492 笔是欺诈行为。输入数据一共包括了 28 个特征 V1，V2，……V28 对应的取值，以及交易时间 Time 和交易金额 Amount。为了保护数据隐私，我们不知道 V1 到 V28 这些特征代表的具体含义，只知道这 28 个特征值是通过 PCA 变换得到的结果。另外字段 Class 代表该笔交易的分类，Class=0 为正常（非欺诈），Class=1 代表欺诈。

我们的目标是针对这个数据集构建一个信用卡欺诈分析的分类器，采用的是逻辑回归。从数据中你能看到欺诈行为只占到了 492/284807=0.172%，数据分类结果的分布是非常不平衡的，因此我们不能使用准确率评估模型的好坏，而是需要统计 F1 值（综合精确率和召回率）。我们先梳理下整个项目的流程：

加载数据；

准备阶段：我们需要探索数据，用数据可视化的方式查看分类结果的情况，以及随着时间的变化，欺诈交易和正常交易的分布情况。上面已经提到过，V1-V28 的特征值都经过 PCA 的变换，但是其余的两个字段，Time 和 Amount 还需要进行规范化。Time 字段和交易本身是否为欺诈交易无关，因此我们不作为特征选择，只需要对 Amount 做数据规范化就行了。同时数据集没有专门的测试集，使用 train_test_split 对数据集进行划分；

分类阶段：我们需要创建逻辑回归分类器，然后传入训练集数据进行训练，并传入测试集预测结果，将预测结果与测试集的结果进行比对。这里的模型评估指标用到了精确率、召回率和 F1 值。同时我们将精确率 - 召回率进行了可视化呈现。

基于上面的流程，具体代码如下：

# -*- coding:utf-8 -*-

# 使用逻辑回归对信用卡欺诈进行分类

import pandas as pd

import numpy as np

import seaborn as sns

import matplotlib.pyplot as plt

import itertools

from sklearn.linear_model import LogisticRegression

from sklearn.model_selection import train_test_split

from sklearn.metrics import confusion_matrix, precision_recall_curve

from sklearn.preprocessing import StandardScaler

import warnings

warnings.filterwarnings('ignore')

# 混淆矩阵可视化

def plot_confusion_matrix(cm, classes, normalize = False, title = 'Confusion matrix"', cmap = plt.cm.Blues) :

    plt.figure()

    plt.imshow(cm, interpolation = 'nearest', cmap = cmap)

    plt.title(title)

    plt.colorbar()

    tick_marks = np.arange(len(classes))

    plt.xticks(tick_marks, classes, rotation = 0)

    plt.yticks(tick_marks, classes)

    thresh = cm.max() / 2.

    for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])) :

        plt.text(j, i, cm[i, j],

                 horizontalalignment = 'center',

                 color = 'white' if cm[i, j] > thresh else 'black')

    plt.tight_layout()

    plt.ylabel('True label')

    plt.xlabel('Predicted label')

    plt.show()

# 显示模型评估结果

def show_metrics():

    tp = cm[1,1]

    fn = cm[1,0]

    fp = cm[0,1]

    tn = cm[0,0]

    print('精确率: {:.3f}'.format(tp/(tp+fp)))

    print('召回率: {:.3f}'.format(tp/(tp+fn)))

    print('F1 值: {:.3f}'.format(2*(((tp/(tp+fp))*(tp/(tp+fn)))/((tp/(tp+fp))+(tp/(tp+fn))))))

# 绘制精确率 - 召回率曲线

def plot_precision_recall():

    plt.step(recall, precision, color = 'b', alpha = 0.2, where = 'post')

    plt.fill_between(recall, precision, step ='post', alpha = 0.2, color = 'b')

    plt.plot(recall, precision, linewidth=2)

    plt.xlim([0.0,1])

    plt.ylim([0.0,1.05])

    plt.xlabel('召回率')

    plt.ylabel('精确率')

    plt.title('精确率 - 召回率 曲线')

    plt.show();

# 数据加载

data = pd.read_csv('./creditcard.csv')

# 数据探索

print(data.describe())

# 设置 plt 正确显示中文

plt.rcParams['font.sans-serif'] = ['SimHei']

# 绘制类别分布

plt.figure()

ax = sns.countplot(x = 'Class', data = data)

plt.title('类别分布')

plt.show()

# 显示交易笔数，欺诈交易笔数

num = len(data)

num_fraud = len(data[data['Class']==1])

print('总交易笔数: ', num)

print('诈骗交易笔数：', num_fraud)

print('诈骗交易比例：{:.6f}'.format(num_fraud/num))

# 欺诈和正常交易可视化

f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(15,8))

bins = 50

ax1.hist(data.Time[data.Class == 1], bins = bins, color = 'deeppink')

ax1.set_title('诈骗交易')

ax2.hist(data.Time[data.Class == 0], bins = bins, color = 'deepskyblue')

ax2.set_title('正常交易')

plt.xlabel('时间')

plt.ylabel('交易次数')

plt.show()

# 对 Amount 进行数据规范化

data['Amount_Norm'] = StandardScaler().fit_transform(data['Amount'].values.reshape(-1,1))

# 特征选择

y = np.array(data.Class.tolist())

data = data.drop(['Time','Amount','Class'],axis=1)

X = np.array(data.as_matrix())

# 准备训练集和测试集

train_x, test_x, train_y, test_y = train_test_split (X, y, test_size = 0.1, random_state = 33)

# 逻辑回归分类

clf = LogisticRegression()

clf.fit(train_x, train_y)

predict_y = clf.predict(test_x)

# 预测样本的置信分数

score_y = clf.decision_function(test_x)

# 计算混淆矩阵，并显示

cm = confusion_matrix(test_y, predict_y)

class_names = [0,1]

# 显示混淆矩阵

plot_confusion_matrix(cm, classes = class_names, title = '逻辑回归 混淆矩阵')

# 显示模型评估分数

show_metrics()

# 计算精确率，召回率，阈值用于可视化

precision, recall, thresholds = precision_recall_curve(test_y, score_y)

plot_precision_recall()

运行结果：

                Time            V1      ...               Amount          Class

count  284807.000000  2.848070e+05      ...        284807.000000  284807.000000

mean    94813.859575  1.165980e-15      ...            88.349619       0.001727

std     47488.145955  1.958696e+00      ...           250.120109       0.041527

min         0.000000 -5.640751e+01      ...             0.000000       0.000000

25%     54201.500000 -9.203734e-01      ...             5.600000       0.000000

50%     84692.000000  1.810880e-02      ...            22.000000       0.000000

75%    139320.500000  1.315642e+00      ...            77.165000       0.000000

max    172792.000000  2.454930e+00      ...         25691.160000       1.000000

[8 rows x 31 columns]

总交易笔数:  284807

诈骗交易笔数： 492

诈骗交易比例：0.001727

精确率: 0.841

召回率: 0.617

F1 值: 0.712

你能看出来欺诈交易的笔数为 492 笔，占所有交易的比例是很低的，即 0.001727，我们可以通过数据可视化的方式对欺诈交易和正常交易的分布进行呈现。另外通过可视化，我们也能看出精确率和召回率之间的关系，当精确率高的时候，召回率往往很低，召回率高的时候，精确率会比较低。

代码有一些模块需要说明下。

我定义了 plot_confusion_matrix 函数对混淆矩阵进行可视化。什么是混淆矩阵呢？混淆矩阵也叫误差矩阵，实际上它就是 TP、FP、TN、FN 这四个数值的矩阵表示，帮助我们判断预测值和实际值相比，对了多少。从这个例子中，你能看出 TP=37，FP=7，FN=23。所以精确率 P=TP/(TP+FP)=37/(37+7)=0.841，召回率 R=TP/(TP+FN)=37/(37+23)=0.617。

然后使用了 sklearn 中的 precision_recall_curve 函数，通过预测值和真实值来计算精确率 - 召回率曲线。precision_recall_curve 函数会计算在不同概率阈值情况下的精确率和召回率。最后定义 plot_precision_recall 函数，绘制曲线。

总结

今天我给你讲了逻辑回归的概念和相关工具的使用，另外学习了在数据样本不平衡的情况下，如何评估模型。这里你需要了解精确率，召回率和 F1 的概念和计算方式。最后在信用卡欺诈分析的项目中，我们使用了逻辑回归工具，并对混淆矩阵进行了计算，同时在模型结果评估中，使用了精确率、召回率和 F1 值，最后得到精确率 - 召回率曲线的可视化结果。

从这个项目中你能看出来，不是所有的分类都是样本平衡的情况，针对正例比例极低的情况，比如信用卡欺诈、某些疾病的识别，或者是恐怖分子的判断等，都需要采用精确率 - 召回率来进行统计。

最后留两道思考题吧，今天我们对信用卡欺诈数据集进行了分析，它是一个不平衡数据集，你知道还有哪些数据属于不平衡数据么？另外，请你使用线性 SVM（对应 sklearn 中的 LinearSVC）对信用卡欺诈数据集进行分类，并计算精确率、召回率和 F1 值。

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

41丨数据挖掘实战（3）：如何对比特币走势进行预测？

今天我带你用数据挖掘对比特币的走势进行预测和分析。

我们之前介绍了数据挖掘算法中的分类、聚类、回归和关联分析算法，那么对于比特币走势的预测，采用哪种方法比较好呢？

可能有些人会认为采用回归分析会好一些，因为预测的结果是连续的数值类型。实际上，数据挖掘算法还有一种叫时间序列分析的算法，时间序列分析模型建立了观察结果与时间变化的关系，能帮我们预测未来一段时间内的结果变化情况。

那么时间序列分析和回归分析有哪些区别呢？

首先，在选择模型前，我们需要确定结果与变量之间的关系。回归分析训练得到的是目标变量 y 与自变量 x（一个或多个）的相关性，然后通过新的自变量 x 来预测目标变量 y。而时间序列分析得到的是目标变量 y 与时间的相关性。

另外，回归分析擅长的是多变量与目标结果之间的分析，即便是单一变量，也往往与时间无关。而时间序列分析建立在时间变化的基础上，它会分析目标变量的趋势、周期、时期和不稳定因素等。这些趋势和周期都是在时间维度的基础上，我们要观察的重要特征。

那么针对今天要进行的预测比特币走势的项目，我们都需要掌握哪些目标呢？

了解时间序列预测的概念，以及常用的模型算法，包括 AR、MA、ARMA、ARIMA 模型等；

掌握并使用 ARMA 模型工具，对一个时间序列数据进行建模和预测；

对比特币的历史数据进行时间序列建模，并预测未来 6 个月的走势。

时间序列预测

关于时间序列，你可以把它理解为按照时间顺序组成的数字序列。实际上在中国古代的农业社会中，人们就将一年中不同时间节点和天气的规律总结了下来，形成了二十四节气，也就是从时间序列中观察天气和太阳的规律（只是当时没有时间序列模型和相应工具），从而使得农业得到迅速发展。在现代社会，时间序列在金融、经济、商业领域拥有广泛的应用。

在时间序列预测模型中，有一些经典的模型，包括 AR、MA、ARMA、ARIMA。我来给你简单介绍一下。

AR 的英文全称叫做 Auto Regressive，中文叫自回归模型。这个算法的思想比较简单，它认为过去若干时刻的点通过线性组合，再加上白噪声就可以预测未来某个时刻的点。

在我们日常生活环境中就存在白噪声，在数据挖掘的过程中，你可以把它理解为一个期望为 0，方差为常数的纯随机过程。AR 模型还存在一个阶数，称为 AR（p）模型，也叫作 p 阶自回归模型。它指的是通过这个时刻点的前 p 个点，通过线性组合再加上白噪声来预测当前时刻点的值。

MA 的英文全称叫做 Moving Average，中文叫做滑动平均模型。它与 AR 模型大同小异，AR 模型是历史时序值的线性组合，MA 是通过历史白噪声进行线性组合来影响当前时刻点。AR 模型中的历史白噪声是通过影响历史时序值，从而间接影响到当前时刻点的预测值。同样 MA 模型也存在一个阶数，称为 MA(q) 模型，也叫作 q 阶移动平均模型。我们能看到 AR 和 MA 模型都存在阶数，在 AR 模型中，我们用 p 表示，在 MA 模型中我们用 q 表示，这两个模型大同小异，与 AR 模型不同的是 MA 模型是历史白噪声的线性组合。

ARMA 的英文全称是 Auto Regressive Moving Average，中文叫做自回归滑动平均模型，也就是 AR 模型和 MA 模型的混合。相比 AR 模型和 MA 模型，它有更准确的估计。同样 ARMA 模型存在 p 和 q 两个阶数，称为 ARMA(p,q) 模型。

ARIMA 的英文全称是 Auto Regressive Integrated Moving Average 模型，中文叫差分自回归滑动平均模型，也叫求合自回归滑动平均模型。相比于 ARMA，ARIMA 多了一个差分的过程，作用是对不平稳数据进行差分平稳，在差分平稳后再进行建模。ARIMA 的原理和 ARMA 模型一样。相比于 ARMA(p,q) 的两个阶数，ARIMA 是一个三元组的阶数 (p,d,q)，称为 ARIMA(p,d,q) 模型。其中 d 是差分阶数。

ARMA 模型工具

上面介绍的 AR，MA，ARMA，ARIMA 四种模型，你只需要了解基础概念即可，中间涉及到的一些数学公式这里不进行展开。

在实际工作中，我们更多的是使用工具，我在这里主要讲解下如何使用 ARMA 模型工具。

在使用 ARMA 工具前，你需要先引用相关工具包：

from statsmodels.tsa.arima_model import ARMA

然后通过 ARMA(endog,order,exog=None) 创建 ARMA 类，这里有一些主要的参数简单说明下：

endog：英文是 endogenous variable，代表内生变量，又叫非政策性变量，它是由模型决定的，不被政策左右，可以说是我们想要分析的变量，或者说是我们这次项目中需要用到的变量。

order：代表是 p 和 q 的值，也就是 ARMA 中的阶数。

exog：英文是 exogenous variables，代表外生变量。外生变量和内生变量一样是经济模型中的两个重要变量。相对于内生变量而言，外生变量又称作为政策性变量，在经济机制内受外部因素的影响，不是我们模型要研究的变量。

举个例子，如果我们想要创建 ARMA(7,0) 模型，可以写成：ARMA(data,(7,0))，其中 data 是我们想要观察的变量，(7,0) 代表 (p,q) 的阶数。

创建好之后，我们可以通过 fit 函数进行拟合，通过 predict(start, end) 函数进行预测，其中 start 为预测的起始时间，end 为预测的终止时间。

下面我们使用 ARMA 模型对一组时间序列做建模，代码如下：

# coding:utf-8

# 用 ARMA 进行时间序列预测

import pandas as pd

import matplotlib.pyplot as plt

import statsmodels.api as sm

from statsmodels.tsa.arima_model import ARMA

from statsmodels.graphics.api import qqplot

# 创建数据

data = [5922, 5308, 5546, 5975, 2704, 1767, 4111, 5542, 4726, 5866, 6183, 3199, 1471, 1325, 6618, 6644, 5337, 7064, 2912, 1456, 4705, 4579, 4990, 4331, 4481, 1813, 1258, 4383, 5451, 5169, 5362, 6259, 3743, 2268, 5397, 5821, 6115, 6631, 6474, 4134, 2728, 5753, 7130, 7860, 6991, 7499, 5301, 2808, 6755, 6658, 7644, 6472, 8680, 6366, 5252, 8223, 8181, 10548, 11823, 14640, 9873, 6613, 14415, 13204, 14982, 9690, 10693, 8276, 4519, 7865, 8137, 10022, 7646, 8749, 5246, 4736, 9705, 7501, 9587, 10078, 9732, 6986, 4385, 8451, 9815, 10894, 10287, 9666, 6072, 5418]

data=pd.Series(data)

data_index = sm.tsa.datetools.dates_from_range('1901','1990')

# 绘制数据图

data.index = pd.Index(data_index)

data.plot(figsize=(12,8))

plt.show()

# 创建 ARMA 模型 # 创建 ARMA 模型

arma = ARMA(data,(7,0)).fit()

print('AIC: %0.4lf' %arma.aic)

# 模型预测

predict_y = arma.predict('1990', '2000')

# 预测结果绘制

fig, ax = plt.subplots(figsize=(12, 8))

ax = data.ix['1901':].plot(ax=ax)

predict_y.plot(ax=ax)

plt.show()

运行结果：

AIC: 1619.6323

我创建了 1901 年 -1990 年之间的时间序列数据 data，然后创建 ARMA(7,0) 模型，并传入时间序列数据 data，使用 fit 函数拟合，然后对 1990 年 -2000 年之间的数据进行预测，最后绘制预测结果。

你能看到 ARMA 工具的使用还是很方便的，只是我们需要 p 和 q 的取值。实际项目中，我们可以给 p 和 q 指定一个范围，让 ARMA 都运行一下，然后选择最适合的模型。

你可能会问，怎么判断一个模型是否适合？

我们需要引入 AIC 准则，也叫作赤池消息准则，它是衡量统计模型拟合好坏的一个标准，数值越小代表模型拟合得越好。

在这个例子中，你能看到 ARMA(7,0) 这个模型拟合出来的 AIC 是 1619.6323（并不一定是最优）。

对比特币走势进行预测

我们都知道比特币的走势除了和历史数据以外，还和很多外界因素相关，比如用户的关注度，各国的政策，币圈之间是否打架等等。当然这些外界的因素不是我们这节课需要考虑的对象。

假设我们只考虑比特币以往的历史数据，用 ARMA 这个时间序列模型预测比特币的走势。

比特币历史数据（从 2012-01-01 到 2018-10-31）可以从 GitHub 上下载：https://github.com/cystanford/bitcoin。

你能看到数据一共包括了 8 个字段，代表的含义如下：

我们的目标是构造 ARMA 时间序列模型，预测比特币（平均）价格走势。p 和 q 参数具体选择多少呢？我们可以设置一个区间范围，然后选择 AIC 最低的 ARMA 模型。

我们梳理下整个项目的流程：

首先我们需要加载数据。

在准备阶段，我们需要先探索数据，采用数据可视化方式查看比特币的历史走势。按照不同的时间尺度（天，月，季度，年）可以将数据压缩，得到不同尺度的数据，然后做可视化呈现。这 4 个时间尺度上，我们选择月作为预测模型的时间尺度，相应的，我们选择 Weighted_Price 这个字段的数值作为观察结果，在原始数据中，Weighted_Price 对应的是比特币每天的平均价格，当我们以“月”为单位进行压缩的时候，对应的 Weighted_Price 得到的就是当月的比特币平均价格。压缩代码如下：

df_month = df.resample('M').mean()

最后在预测阶段创建 ARMA 时间序列模型。我们并不知道 p 和 q 取什么值时，模型最优，因此我们可以给它们设置一个区间范围，比如都是 range(0,3)，然后计算不同模型的 AIC 数值，选择最小的 AIC 数值对应的那个 ARMA 模型。最后用这个最优的 ARMA 模型预测未来 8 个月的比特币平均价格走势，并将结果做可视化呈现。

基于这个流程，具体代码如下：

# -*- coding: utf-8 -*-

# 比特币走势预测，使用时间序列 ARMA

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from statsmodels.tsa.arima_model import ARMA

import warnings

from itertools import product

from datetime import datetime

warnings.filterwarnings('ignore')

# 数据加载

df = pd.read_csv('./bitcoin_2012-01-01_to_2018-10-31.csv')

# 将时间作为 df 的索引

df.Timestamp = pd.to_datetime(df.Timestamp)

df.index = df.Timestamp

# 数据探索

print(df.head())

# 按照月，季度，年来统计

df_month = df.resample('M').mean()

df_Q = df.resample('Q-DEC').mean()

df_year = df.resample('A-DEC').mean()

# 按照天，月，季度，年来显示比特币的走势

fig = plt.figure(figsize=[15, 7])

plt.rcParams['font.sans-serif']=['SimHei'] # 用来正常显示中文标签

plt.suptitle('比特币金额（美金）', fontsize=20)

plt.subplot(221)

plt.plot(df.Weighted_Price, '-', label='按天')

plt.legend()

plt.subplot(222)

plt.plot(df_month.Weighted_Price, '-', label='按月')

plt.legend()

plt.subplot(223)

plt.plot(df_Q.Weighted_Price, '-', label='按季度')

plt.legend()

plt.subplot(224)

plt.plot(df_year.Weighted_Price, '-', label='按年')

plt.legend()

plt.show()

# 设置参数范围

ps = range(0, 3)

qs = range(0, 3)

parameters = product(ps, qs)

parameters_list = list(parameters)

# 寻找最优 ARMA 模型参数，即 best_aic 最小

results = []

best_aic = float("inf") # 正无穷

for param in parameters_list:

    try:

        model = ARMA(df_month.Weighted_Price,order=(param[0], param[1])).fit()

    except ValueError:

        print('参数错误:', param)

        continue

    aic = model.aic

    if aic < best_aic:

        best_model = model

        best_aic = aic

        best_param = param

    results.append([param, model.aic])

# 输出最优模型

result_table = pd.DataFrame(results)

result_table.columns = ['parameters', 'aic']

print('最优模型: ', best_model.summary())

# 比特币预测

df_month2 = df_month[['Weighted_Price']]

date_list = [datetime(2018, 11, 30), datetime(2018, 12, 31), datetime(2019, 1, 31), datetime(2019, 2, 28), datetime(2019, 3, 31),

             datetime(2019, 4, 30), datetime(2019, 5, 31), datetime(2019, 6, 30)]

future = pd.DataFrame(index=date_list, columns= df_month.columns)

df_month2 = pd.concat([df_month2, future])

df_month2['forecast'] = best_model.predict(start=0, end=91)

# 比特币预测结果显示

plt.figure(figsize=(20,7))

df_month2.Weighted_Price.plot(label='实际金额')

df_month2.forecast.plot(color='r', ls='--', label='预测金额')

plt.legend()

plt.title('比特币金额（月）')

plt.xlabel('时间')

plt.ylabel('美金')

plt.show()

运行结果：

           Timestamp       ...        Weighted_Price

Timestamp                   ...

2011-12-31 2011-12-31       ...              4.471603

2012-01-01 2012-01-01       ...              4.806667

2012-01-02 2012-01-02       ...              5.000000

2012-01-03 2012-01-03       ...              5.252500

2012-01-04 2012-01-04       ...              5.208159

[5 rows x 8 columns]

我们通过 product 函数创建了 (p,q) 在 range(0,3) 范围内的所有可能组合，并对每个 ARMA(p,q) 模型进行了 AIC 数值计算，保存了 AIC 数值最小的模型参数。然后用这个模型对比特币的未来 8 个月进行了预测。

从结果中你能看到，在 2018 年 10 月之后 8 个月的时间里，比特币会触底到 4000 美金左右，实际上比特币在这个阶段确实降低到了 4000 元美金甚至更低。在时间尺度的选择上，我们选择了月，这样就对数据进行了降维，也节约了 ARMA 的模型训练时间。你能看到比特币金额（美金）这张图中，按月划分的比特币走势和按天划分的比特币走势差别不大，在减少了局部的波动的同时也能体现出比特币的趋势，这样就节约了 ARMA 的模型训练时间。

总结

今天我给你讲了一个比特币趋势预测的实战项目。通过这个项目你应该能体会到，当我们对一个数值进行预测的时候，如果考虑的是多个变量和结果之间的关系，可以采用回归分析，如果考虑单个时间维度与结果的关系，可以使用时间序列分析。

根据比特币的历史数据，我们使用 ARMA 模型对比特币未来 8 个月的走势进行了预测，并对结果进行了可视化显示。你能看到 ARMA 工具还是很好用的，虽然比特币的走势受很多外在因素影响，比如政策环境。不过当我们掌握了这些历史数据，也不妨用时间序列模型来分析预测一下。

最后依然是思考题环节，今天我们讲了 AR、MA、ARMA 和 ARIMA，你能简单说说它们之间的区别么？

另外我在GitHub中上传了沪市指数的历史数据（对应的 shanghai_1990-12-19_to_2019-2-28.csv），请你编写代码使用 ARMA 模型对沪市指数未来 10 个月（截止到 2019 年 12 月 31 日）的变化进行预测（将数据转化为按月统计即可）。

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

42丨当我们谈深度学习的时候，我们都在谈什么？

在算法篇中，我们已经讲了数据挖掘十大经典算法，在实战篇中也了解了随机森林、逻辑回归的概念及工具使用。这些算法都属于传统的机器学习算法。你肯定听说过这两年很火的深度学习，那么机器学习算法和深度学习有什么关联呢？

在这篇文章中，我们会通过以下几个方面了解深度学习：

数据挖掘、机器学习和深度学习的区别是什么？这些概念都代表什么？

我们通过深度学习让机器具备人的能力，甚至某些技能的水平超过人类，比如图像识别、下棋对弈等。那么深度学习的大脑是如何工作的？

深度学习是基于神经网络构建的，都有哪些常用的网络模型？

深度学习有三个重要的应用领域，这三个应用领域分别是什么？

数据挖掘，机器学习，深度学习的区别是什么？

实际上数据挖掘和机器学习在很大程度上是重叠的。一些常用算法，比如 K-Means、KNN、SVM、决策树和朴素贝叶斯等，既可以说是数据挖掘算法，又可以说是机器学习算法。那么数据挖掘和机器学习之间有什么区别呢？

数据挖掘通常是从现有的数据中提取规律模式（pattern）以及使用算法模型（model）。核心目的是找到这些数据变量之间的关系，因此我们也会通过数据可视化对变量之间的关系进行呈现，用算法模型挖掘变量之间的关联关系。通常情况下，我们只能判断出来变量 A 和变量 B 是有关系的，但并不一定清楚这两者之间有什么具体关系。在我们谈论数据挖掘的时候，更强调的是从数据中挖掘价值。

机器学习是人工智能的一部分，它指的是通过训练数据和算法模型让机器具有一定的智能。一般是通过已有的数据来学习知识，并通过各种算法模型形成一定的处理能力，比如分类、聚类、预测、推荐能力等。这样当有新的数据进来时，就可以通过训练好的模型对这些数据进行预测，也就是通过机器的智能帮我们完成某些特定的任务。

深度学习属于机器学习的一种，它的目标同样是让机器具有智能，只是与传统的机器学习算法不同，它是通过神经网络来实现的。神经网络就好比是机器的大脑，刚开始就像一个婴儿一样，是一张白纸。但通过多次训练之后，“大脑”就可以逐渐具备某种能力。这个训练过程中，我们只需要告诉这个大脑输入数据是什么，以及对应的输出结果是什么即可。通过多次训练，“大脑”中的多层神经网络的参数就会自动优化，从而得到一个适应于训练数据的模型。

所以你能看到在传统的机器学习模型中，我们都会讲解模型的算法原理，比如 K-Means 的算法原理，KNN 的原理等。而到了神经网络，我们更关注的是网络结构，以及网络结构中每层神经元的传输机制。我们不需要告诉机器具体的特征规律是什么，只需把我们想要训练的数据和对应的结果告诉机器大脑即可。深度学习会自己找到数据的特征规律！而传统机器学习往往需要专家（我们）来告诉机器采用什么样的模型算法，这就是深度学习与传统机器学习最大的区别。

另外深度学习的神经网络结构通常比较深，一般都是 5 层以上，甚至也有 101 层或更多的层数。这些深度的神经网络可以让机器更好地自动捕获数据的特征。

神经网络是如何工作的

神经网络可以说是机器的大脑，经典的神经网络结构可以用下面的图来表示。

这里有一些概念你需要了解。

节点：神经网络是由神经元组成的，也称之为节点，它们分布在神经网络的各个层中，这些层包括输入层，输出层和隐藏层。

输入层：负责接收信号，并分发到隐藏层。一般我们将数据传给输入层。

输出层：负责输出计算结果，一般来说输出层节点数等于我们要分类的个数。

隐藏层：除了输入层和输出层外的神经网络都属于隐藏层，隐藏层可以是一层也可以是多层，每个隐藏层都会把前一层节点传输出来的数据进行计算（你可以理解是某种抽象表示），这相当于把数据抽象到另一个维度的空间中，可以更好地提取和计算数据的特征。

工作原理：神经网络就好比一个黑盒子，我们只需要告诉这个黑盒子输入数据和输出数据，神经网络就可以自我训练。一旦训练好之后，就可以像黑盒子一样使用，当你传入一个新的数据时，它就会告诉你对应的输出结果。在训练过程中，神经网络主要是通过前向传播和反向传播机制运作的。

什么是前向传播和反向传播呢？

前向传播：数据从输入层传递到输出层的过程叫做前向传播。这个过程的计算结果通常是通过上一层的神经元的输出经过矩阵运算和激活函数得到的。这样就完成了每层之间的神经元数据的传输。

反向传播：当前向传播作用到输出层得到分类结果之后，我们需要与实际值进行比对，从而得到误差。反向传播也叫作误差反向传播，核心原理是通过代价函数对网络中的参数进行修正，这样更容易让网络参数得到收敛。

所以，整个神经网络训练的过程就是不断地通过前向 - 反向传播迭代完成的，当达到指定的迭代次数或者达到收敛标准的时候即可以停止训练。然后我们就可以拿训练好的网络模型对新的数据进行预测。

当然，深度神经网络是基于神经网络发展起来的，它的原理与神经网络的原理一样，只不过强调了模型结构的深度，通常有 5 层以上，这样模型的学习能力会更强大。

常用的神经网络都有哪些

按照中间层功能的不同，神经网络可以分为三种网络结构，分别为 FNN、CNN 和 RNN。

FNN（Fully-connected Neural Network）指的是全连接神经网络，全连接的意思是每一层的神经元与上一层的所有神经元都是连接的。不过在实际使用中，全连接的参数会过多，导致计算量过大。因此在实际使用中全连接神经网络的层数一般比较少。

CNN叫作卷积神经网络，在图像处理中有广泛的应用，了解图像识别的同学对这个词一定不陌生。CNN 网络中，包括了卷积层、池化层和全连接层。这三个层都有什么作用呢？

卷积层相当于一个滤镜的作用，它可以把图像进行分块，对每一块的图像进行变换操作。

池化层相当于对神经元的数据进行降维处理，这样输出的维数就会减少很多，从而降低整体的计算量。

全连接层通常是输出层的上一层，它将上一层神经元输出的数据转变成一维的向量。

RNN称为循环神经网络，它的特点是神经元的输出可以在下一个时刻作用到自身，这样 RNN 就可以看做是在时间上传递的神经网络。它可以应用在语音识别、自然语言处理等与上下文相关的场景。

深度学习网络往往包括了这三种网络的变种形成，常用的深度神经网络包括 AlexNet、VGG19、GoogleNet、ResNet 等，我总结了这些网络的特点，你可以看下：

你能看出随着时间的推进，提出的深度学习网络层数越来越深，Top-5 错误率越来越低。

你可能会问什么是 Top-5 错误率，实际上这些网络结构的提出和一个比赛相关，这个比赛叫做 ILSVRC，英文全称叫做 Large Scale Visual Recognition Challenge。它是一个关于大规模图像可视化识别的比赛，所基于的数据集就是著名的 ImageNet 数据集，一共包括了 1400 万张图片，涵盖 2 万多个类别。

表格中的 AlexNet 就是 2012 年的 ILSVRC 冠军，当时的 Top-5 正确率是 84.7%，VGG 和 GoogleNet 是 2014 年 ILSVRC 比赛的模型，其中 GoogleNet 是当时比赛的冠军，而 VGG 是当时比赛的亚军，它的效率低于 GoogleNet。VGG 有两个版本，VGG16 和 VGG19，分别是 16 层和 19 层的 VGG 网络，这两者没有本质的区别，只是网络深度不同。到了 2015 年，比赛冠军是 ResNet，Top-5 正确率达到了 96.43%。ResNet 也有不同的版本，比如 ResNet50、ResNet101 和 ResNet152 等，名称后面的数字代表的是不同的网络深度。之后 ResNet 在其他图像比赛中也多次拿到冠军。

深度学习的应用领域

从 ImageNet 跑出来的这些优秀模型都是基于 CNN 卷积神经网络的。实际上深度学习有三大应用领域，图像识别就是其中之一，其他领域分别是语音识别和自然语言处理。

这三个应用领域有一个共同的特性，就是都来自于信号处理。我们人类平时会处理图像信息，语音信息以及语言文字信息。机器可以帮助我们完成这三个应用里的某些工作。比如图像识别领域中图像分类和物体检测就是两个核心的任务。我们可以让机器判断图像中都有哪些物体，类别是什么，以及这些物体所处的位置。图像识别被广泛应用在安防检测中。此外人脸识别也是图像识别重要的应用场景。

Siri 大家一定不陌生，此外还有我们使用的智能电视等，都采用了语音识别技术。语音识别技术可以识别人类的语音指令并进行交互。在语音导航中，还采用了语音合成技术，这样就可以让机器模拟人的声音为我们服务，Siri 语音助手也采用了语音识别和合成的技术。

自然语言处理的英文缩写是 NLP，它被广泛应用到自动问答、智能客服、过滤垃圾邮件和短信等领域中。在电商领域，我们可以通过 NLP 自动给商品评论打标签，在用户决策的时候提供数据支持。在自动问答中，我们可以输入自己想问的问题，让机器来回答，比如在百度中输入“姚明的老婆”，就会自动显示出”叶莉“。

此外这些技术还可以相互组合为我们提供服务，比如在无人驾驶中就采用了图像识别、语音识别等技术。在超市购物中也采用了集成图像识别、意图识别等技术等。

总结

今天我们大概了解了一下深度学习。深度学习也是机器学习的一种。我们之前讲解了数据挖掘十大经典算法，还有逻辑回归、随机森林算法等，这些都是传统的机器学习算法。在日常工作中，可以满足大部分的机器学习任务。但是对于数据量更大，更开放性的问题，我们就可以采用深度学习的算法，让机器自己来找规律，而不是通过我们指定的算法来找分类规律。

所以深度学习的普适性会更强一些，但也并不代表深度学习就优于机器学习。一方面深度学习需要大量的数据，另一方面深度学习的学习时间，和需要的计算资源都要大于传统的机器学习。你能看到各种深度学习的训练集一般都还是比较大的，比如 ImageNet 就包括了 1400 万张图片。如果我们没有提供大量的训练数据，训练出来的深度模型识别结果未必好于传统的机器学习。

实际上神经网络最早是在 1986 年提出来的，之后不温不火，直到 ImageNet 于 2009 年提出，在 2010 年开始举办每年的 ImageNet 大规模视觉识别挑战赛（ILSVRC），深度学习才得到迅猛发展。2016 年 Google 研发的 AlphaGo 击败了人类冠军李世石，更是让人们看到了深度学习的力量。一个好问题的提出，可以激发无穷的能量，这是科技进步的源泉，也是为什么在科学上，我们会有各种公开的数据集。一个好的数据集就代表了一个好的问题和使用场景。正是这些需求的出现，才能让我们的算法有更好的用武之地，同时也有了各种算法相互比拼的平台。

今天我们聊了聊神经网络和深度学习，它们就像智能的黑盒子一样，通过训练可以帮我们处理各种问题，通过今天的学习，你能不能谈谈对深度学习工作原理的理解呢？以及深度学习都有哪些使用场景呢？

欢迎在评论区与我分享你的答案，也欢迎点击”请朋友读“，把这篇文章分享给你的朋友或者同事。

43丨深度学习（下）：如何用Keras搭建深度学习网络做手写数字识别？

通过上节课的讲解，我们已经对神经网络和深度学习有了基本的了解。这节课我就用 Keras 这个深度学习框架做一个识别手写数字的练习。

你也许还有印象，在 KNN 算法那节中，我讲到过 Mnist 手写数字识别这个数据集，当时我们采用的是 mini 版的手写数字数据集。实际上完整版的 Mnist 一共有 60000 个训练样本和 10000 个测试样本，这么庞大的数据量更适合用深度学习框架完成训练。

今天的学习目标主要有以下的几个方面：

进一步了解 CNN 网络。CNN 网络在深度学习网络中应用很广，很多网络都是基于 CNN 网络构建的，你有必要进一步了解 CNN 的网络层次，尤其是关于卷积的原理。

初步了解 LeNet 和 AlexNet。它们都是经典的 CNN 网络，我们今天的任务就是认识这些经典的 CNN 网络，这样在接触更深度的 CNN 网络的时候，比如 VGG、GoogleNet 和 ResNet 这些网络的时候，就会更容易理解和使用。

对常用的深度学习框架进行对比，包括 Tensorflow、Keras、Caffe、PyTorch、 MXnet 和 Theano。当选择深度学习框架的时候到底该选择哪个？

使用 Keras 这个深度学习框架编写代码，完成第一个深度学习任务，也就是 Mnist 手写数字识别。

如何理解 CNN 网络中的卷积作用

CNN 的网络结构由三种层组成，它们分别是卷积层、池化层和全连接层。

在上篇文章中，我讲到卷积层相当于滤镜的作用，它可以把图像分块，对每一块的图像进行卷积操作。

卷积本身是一种矩阵运算，那什么是卷积呢？

假设我有一个二维的图像 X，和卷积 K，把二维矩阵 X 进行卷积 K 操作之后，可以得到矩阵 Z，如下图所示：

我简单说下计算的原理。

第一步，我们需要将卷积核翻转 180 度（只有翻转之后才能做矩阵运算），也就是变成：

第二步，将卷积核的第一个元素，对准矩阵 X 左上角的第一个元素，对应元素相乘，然后再相加可以就可以得到 10*1+10*1+10*0+10*1+5*0+5*-1+10*0+5*-1+5*-1=15。

第三步，每个元素都重复第二步的计算过程，可以得到如下的矩阵结果 Z：

这样我们就完成了一个卷积的操作。如果编写代码的话，你可以这样写：

import pylab

import numpy as np

from scipy import signal

# 设置原图像

img = np.array([[10, 10, 10, 10, 10],

                     [10, 5, 5, 5, 10],

                     [10, 5, 5, 5, 10],

                     [10, 5, 5, 5, 10],

                     [10, 10, 10, 10, 10]])

# 设置卷积核

fil = np.array([[ -1,-1, 0],

                [ -1, 0, 1],

                [  0, 1, 1]])

# 对原图像进行卷积操作

res = signal.convolve2d(img, fil, mode='valid')

# 输出卷积后的结果

print(res)

运行结果：

[[ 15  10   0]

 [ 10   0 -10]

 [  0 -10 -15]]

这里我用到了 convolve2d 函数对图像 img 和卷积核 fil 做卷积运算，最后输出结果 res。你可能还是会问，为什么我们要对图像进行卷积操作呢？你可以看看下面一段代码：

import matplotlib.pyplot as plt

import pylab

import cv2

import numpy as np

from scipy import signal

# 读取灰度图像

img = cv2.imread("haibao.jpg", 0)

# 显示灰度图像

plt.imshow(img,cmap="gray")

pylab.show()

# 设置卷积核

fil = np.array([[ -1,-1, 0],

                [ -1, 0, 1],

                [  0, 1, 1]])

# 卷积操作

res = signal.convolve2d(img, fil, mode='valid')

print(res)

# 显示卷积后的图片

plt.imshow(res,cmap="gray")

pylab.show()

运行结果：

这里我对专栏的海报做了卷积的操作，你能看到卷积操作是对图像进行了特征的提取。实际上每个卷积核都是一种滤波器，它们把图像中符合条件的部分筛选出来，也就相当于做了某种特征提取。

在 CNN 的卷积层中可以有多个卷积核，以 LeNet 为例，它的第一层卷积核有 6 个，因此可以帮我们提取出图像的 6 个特征，从而得到 6 个特征图（feature maps）。

激活函数的作用

做完卷积操作之后，通常还需要使用激活函数对图像进一步处理。在逻辑回归中，我提到过 Sigmoid 函数，它在深度学习中有广泛的应用，除了 Sigmoid 函数作为激活函数以外，tanh、ReLU 都是常用的激活函数。

这些激活函数通常都是非线性的函数，使用它们的目的是把线性数值映射到非线性空间中。卷积操作实际上是两个矩阵之间的乘法，得到的结果也是线性的。只有经过非线性的激活函数运算之后，才能映射到非线性空间中，这样也可以让神经网络的表达能力更强大。

池化层的作用

池化层通常在两个卷积层之间，它的作用相当于对神经元的数据做降维处理，这样就能降低整体计算量。

假设池化的窗大小是 2x2，就相当于用一个 2x2 的窗口对输出数据进行计算，将原图中 2x2 矩阵的 4 个点变成一个点。常用的池化操作是平均池化和最大池化。平均池化是对特征点求平均值，也就是用 4 个点的平均值来做代表。最大池化则是对特征点求最大值，也就是用 4 个点的最大值来做代表。

在神经网络中，我们可以叠加多个卷积层和池化层来提取更抽象的特征。经过几次卷积和池化之后，通常会有一个或多个全连接层。

全连接层的作用

全连接层将前面一层的输出结果与当前层的每个神经元都进行了连接。

这样就可以把前面计算出来的所有特征，通过全连接层将输出值输送给分类器，比如 Softmax 分类器。在深度学习中，Softmax 是个很有用的分类器，通过它可以把输入值映射到 0-1 之间，而且所有输出结果相加等于 1。其实你可以换种方式理解这个概念，假设我们想要识别一个数字，从 0 到 9 都有可能。那么通过 Softmax 层，对应输出 10 种分类结果，每个结果都有一个概率值，这些概率相加为 1，我们就可以知道这个数字是 0 的概率是多少，是 1 的概率是多少……是 9 的概率又是多少，从而也就帮我们完成了数字识别的任务。

LeNet 和 AlexNet 网络

你能看出 CNN 网络结构中每一层的作用：它通过卷积层提取特征，通过激活函数让结果映射到非线性空间，增强了结果的表达能力，再通过池化层压缩特征图，降低了网络复杂度，最后通过全连接层归一化，然后连接 Softmax 分类器进行计算每个类别的概率。

通常我们可以使用多个卷积层和池化层，最后再连接一个或者多个全连接层，这样也就产生了不同的网络结构，比如 LeNet 和 AlexNet。

我将 LeNet 和 AlexNet 的参数特征整理如下：

LeNet 提出于 1986 年，是最早用于数字识别的 CNN 网络，输入尺寸是 32*32。它输入的是灰度的图像，整个的网络结构是：输入层→C1 卷积层→S2 池化层→C3 卷积层→S4 池化层→C5 卷积层→F6 全连接层→Output 全连接层，对应的 Output 输出类别数为 10。

AlexNet 在 LeNet 的基础上做了改进，提出了更深的 CNN 网络模型，输入尺寸是 227*227*3，可以输入 RGB 三通道的图像，整个网络的结构是：输入层→(C1 卷积层→池化层)→(C2 卷积层→池化层)→C3 卷积层→C4 卷积层→(C5 池化层→池化层)→全连接层→全连接层→Output 全连接层。

实际上后面提出来的深度模型，比如 VGG、GoogleNet 和 ResNet 都是基于下面的这种结构方式改进的：输出层→（卷积层 + -> 池化层？）+ → 全连接层 +→Output 全连接层。

其中“+”代表 1 个或多个，“？”代表 0 个或 1 个。

你能看出卷积层后面可以有一个池化层，也可以没有池化层，“卷积层 + → 池化层？”这样的结构算是一组卷积层，在多组卷积层之后，可以连接多个全连接层，最后再接 Output 全连接层。

常用的深度学习框架对比

了解了 CNN 的网络结构之后，我们来看下常用的深度学习框架都有哪些。

下面这张图是常用框架的简单对比。

从 GitHub 上的热门程序排序来看，Tensorflow、Keras 和 Caffe 是三个排名最高的深度学习框架，其中 Tensorflow 是 Google 出品，也是深度学习最常用的库。关于 Keras，你可以理解成是把 Tensorflow 或 Theano 作为后端，基于它们提供的封装接口，这样更方便我们操作使用。Caffe、PyTorch、MXNet 和 Theano 也是常用的深度学习库，你在接触深度学习的时候可能也会遇到，这里不做介绍。

如果你刚进入深度学习这个领域，我更建议你直接使用 Keras，因为它使用方便，更加友好，可以方便我们快速构建网络模型，不需要过多关注底层细节。

用 Keras 做 Mnist 手写数字识别

Keras 也是基于 Python 语言的。在使用 Keras 之前，我们需要安装相应的工具包：

pip install keras

pip install tensorflow

这里需要注明的是 Keras 需要用 tensorflow 或者 theano 作为后端，因此我们也需要引入相关的工具。同时你还需要注意 NumPy 版本是否为最新的版本，我们需要采用最新的 NumPy 版本才能正常运行 keras，更新 NumPy 工具的方法：

pip install -U numpy

安装好 Keras 工具包之后，就可以创建一个 Sequential 序贯模型，它的作用是将多个网络层线性堆叠起来，使用方法：

from keras.models import Sequential

model = Sequential()

然后就可以在网络中添加各种层了。

创建二维卷积层

使用 Conv2D(filters, kernel_size, activation=None) 进行创建, 其中 filters 代表卷积核的数量，kernel_size 代表卷积核的宽度和长度，activation 代表激活函数。如果创建的二维卷积层是第一个卷积层，我们还需要提供 input_shape 参数，比如：input_shape=(28, 28, 1) 代表的就是 28*28 的灰度图像。

对 2D 信号做最大池化层

使用 MaxPooling2D(pool_size=(2, 2)) 进行创建，其中 pool_size 代表下采样因子，比如 pool_size=(2,2) 的时候相当于将原来 22 的矩阵变成一个点，即用 22 矩阵中的最大值代替，输出的图像在长度和宽度上均为原图的一半。

创建 Flatten 层

使用 Flatten() 创建，常用于将多维的输入扁平化，也就是展开为一维的向量。一般用在卷积层与全连接层之间，方便后面进行全连接层的操作。

创建全连接层

使用 Dense(units, activation=None) 进行创建，其中 units 代表的是输出的空间维度，activation 代表的激活函数。

我这里只列举了部分常用的层，这些层在今天手写数字识别的项目中会用到。当我们把层创建好之后，可以加入到模型中，使用 model.add() 函数即可。

添加好网络模型中的层之后，我们可以使用 model.compile(loss, optimizer=‘adam’, metrics=[‘accuracy’]) 来完成损失函数和优化器的配置，其中 loss 代表损失函数的配置，optimizer 代表优化器，metrics 代表评估模型所采用的指标。

然后我们可以使用 fit 函数进行训练，使用 predict 函数进行预测，使用 evaluate 函数对模型评估。

针对 Mnist 手写数字识别，用 keras 的实现代码如下：

# 使用 LeNet 模型对 Mnist 手写数字进行识别

import keras

from keras.datasets import mnist

from keras.layers import Conv2D, MaxPooling2D

from keras.layers import Dense, Flatten

from keras.models import Sequential

# 数据加载

(train_x, train_y), (test_x, test_y) = mnist.load_data()

# 输入数据为 mnist 数据集

train_x = train_x.reshape(train_x.shape[0], 28, 28, 1)

test_x = test_x.reshape(test_x.shape[0], 28, 28, 1)

train_x = train_x / 255

test_x = test_x / 255

train_y = keras.utils.to_categorical(train_y, 10)

test_y = keras.utils.to_categorical(test_y, 10)

# 创建序贯模型

model = Sequential()

# 第一层卷积层：6 个卷积核，大小为 5∗5, relu 激活函数

model.add(Conv2D(6, kernel_size=(5, 5), activation='relu', input_shape=(28, 28, 1)))

# 第二层池化层：最大池化

model.add(MaxPooling2D(pool_size=(2, 2)))

# 第三层卷积层：16 个卷积核，大小为 5*5，relu 激活函数

model.add(Conv2D(16, kernel_size=(5, 5), activation='relu'))

# 第二层池化层：最大池化

model.add(MaxPooling2D(pool_size=(2, 2)))

# 将参数进行扁平化，在 LeNet5 中称之为卷积层，实际上这一层是一维向量，和全连接层一样

model.add(Flatten())

model.add(Dense(120, activation='relu'))

# 全连接层，输出节点个数为 84 个

model.add(Dense(84, activation='relu'))

# 输出层 用 softmax 激活函数计算分类概率

model.add(Dense(10, activation='softmax'))

# 设置损失函数和优化器配置

model.compile(loss=keras.metrics.categorical_crossentropy, optimizer=keras.optimizers.Adam(), metrics=['accuracy'])

# 传入训练数据进行训练

model.fit(train_x, train_y, batch_size=128, epochs=2, verbose=1, validation_data=(test_x, test_y))

# 对结果进行评估

score = model.evaluate(test_x, test_y)

print('误差:%0.4lf' %score[0])

print('准确率:', score[1])

运行结果：

……（省略中间迭代的结算结果，即显示每次迭代的误差 loss 和准确率 acc）

误差:0.0699

准确率: 0.9776

我用 epochs 控制了训练的次数，当训练 2 遍的时候，准确率达到 97.76%，还是很高的。

总结

今天我们用 keras 对手写数字进行了识别，具体的代码部分讲解的不多，其中涉及到 API，你可以参考下 Keras 中文手册。

在这个过程里，我们只是使用了 LeNet 的网络模型，实际上 AlexNet、VGG、GoogleNet 和 ResNet 都是基于 CNN 的网络结构。在 CNN 网络中包括了卷积层、池化层和全连接层。一个基于 CNN 的深度学习网络通常是几组卷积层之后，再连接多个全连接层，最后再接 Output 全连接层，而每组的卷积层都是“卷积层 + →池化层？”的结构。

另外，通过今天的学习你应该能体会到卷积在图像领域中的应用。今天我对专栏的海报进行了一个 3*3 的卷积核操作，可以看到卷积之后得到的图像是原图像某种特征的提取。在实际的卷积层中，会包括多个卷积核，对原图像在不同特征上进行提取。通过多个卷积层的操作，可以在更高的维度上对图像特征进一步提取，这样可以让机器在不同层次、不同维度理解图像特征。

另外在 Keras 使用中，你能看到与 sklearn 中的机器学习算法使用不同。我们需要对网络模型中的层进行配置，将创建好的层添加到模型中，然后对模型中使用的损失函数和优化器进行配置，最后就可以对它进行训练和预测了。

今天讲的知识点比较多，其中我讲到了卷积、卷积核和卷积层，你能说一下对这三者的理解吗？你之前有使用 Keras 或 Tensorflow 的经验么，你能否谈谈你的使用感受？

欢迎你在评论区与我分享你的答案，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

44丨如何培养你的数据分析思维？

数据分析可以是一个职业，一份工作，也可以是一种思维方式。在专栏里，我们更多的是讲解了数据分析工具的使用。从 Python 爬虫到 Python 可视化，再到数据清洗、数据挖掘算法等，而在日常工作中，我们除了需要熟练掌握这些工具的使用外，更主要的是培养自己的数据分析思维。

培养数据分析思维不仅对找一份和数据分析相关的工作有帮助，在日常生活中同样会有帮助。

今天的内容会从以下几个方面进行分享：

我们做一个有关生命线的游戏。你可以把生命线看作是数据可视化，能从中发现什么规律呢？

当你想知道事情的答案，但不知道从何处下手的时候，要怎么办呢？要学会提问。好的问题就是好的开始。遇到茫然的情况，不妨从提问开始。

“我平时也有一些关于数据分析的思考，但是效率不高，有什么方法可以提升效率么？”分享是最快的成长，通过反向传播可以让我们更快得到收敛。

“我也知道数据分析思维的训练很重要，但是平时工作很忙该怎么办？”

一个关于生命线的游戏

举个例子，如果你想知道自己是如何挣钱的，你可以分析自己以往挣钱的经历，也可以是赔钱的经历，把它们写在一个时间轴上，纵坐标是发生的事件，这个事件对你的影响越大，纵坐标的绝对值就越大。通过生命线的分析，我们先把这些事件按照时间的顺序记录下来，然后记录它们的影响力。实际上这些事件，影响力 y 和时间 x 就是你的生命线历史数据，画出生命线之前，你不必思考它们之间的规律是什么。画出来之后，你有 30 分钟的时间，仔细思考和分析它们之间有什么关联。

其实你能看出来，画生命线之前，我们首先需要有客观的记录数据，生命线就相当于数据可视化，更容易让我们找到规律。你可以对这些事件打上不同的标签，比如 12 岁的时候给报社投稿挣到了 180 元，26 岁做自媒体，每个月有 2 万收入等等，那么两件事都可以打上“写作”这个标签。

我们之前讲过打标签是一种抽象能力。当你对这些事件逐一分析打标签的时候，就有可能从更高的维度上观察到这些事件的规律。

上面这个是关于挣钱方向的生命线游戏，有空的话你可以做一下，分析分析适合自己的挣钱模式是什么。

此外还有一个生命线的游戏，你肯定不陌生，那就是简历。

在面试之前，你最重要的信息就是简历。HR 会通过简历筛选符合要求的人，一般来说会根据简历来看职业经历是否具有连续性，比如说这个人做过行政，又做过销售，现在面试数据分析的工作，那么对于 HR 来说，他就没有找到职业方向。所以有些人在投递某个职位前，会特地对简历做有针对性的修改，比如重点呈现和数据分析相关的经历，其他关系不大的经历都一一删除，哪怕经历再丰富。

不相关的经历其实就是干扰数据，这些并不是 HR 想要看到的！

除了分析挣钱、找工作以外，通过生命线做数据分析还能帮我们做什么呢？它可以分析你的感情经历、是否有偏财运等等。数据是非常重要的宝藏，只是你需要知道如何观察它，使用它。

通过历史才能看到未来，如果我们不去分析这些历史，就没有办法找到未来的规律。大到国家，小到个人，都是如此。这也是为什么很多成功人士经常读书的原因之一吧。通过总结别人的成功或者失败的经验，可以启迪自己的人生道路。

提问是最好的老师

当了解数据分析的价值之后，你可能会问，学会提问和数据分析思维有什么联系？

实际上提问本身就是一种维度的观察。很多人在做数据分析的时候，首先遇到的问题是没有数据怎么办？数据从哪里来？其实在找数据之前，我们应该先问自己一个问题，我要解决什么问题？要分析什么规律？比如说，你想观察自己挣钱模式的规律，或者想解决个人的情感问题，再或者，想找到一份适合自己的工作等。我们首先需要定义一个目标。

然后围绕这个目标再问自己，这些数据可能会在哪里？是通过分析自己过去的经历找，还是从网上找相关的信息？都有哪些渠道可以收集到这些信息？有一个好的问题，才会有好的答案。问题可以帮助我们关注事物的不同方面，而且通常是一些重要的维度，对我们全面客观地分析一件事是非常有好处的。

从科技进步来看，很多时候都是先有一个问题，再有无数的人前赴后继去解决它。比如世界三大数学猜想，费马猜想、四色猜想和哥德巴赫猜想。比如费马大定理是费马在 1637 年提出的，此后的 300 年间有无数数学家试图去验证它。

学会提问不仅可以帮助我们对事物有更全面的认识，还可以让我们变被动为主动。要知道在职场上，大部分人的工作状态都属于被动性，比如等着领导下任务、数据分析结果没出来就怪数据不完整，质量不够好等。被动的状态往往能量很低，或者说创造性很低。只有当你主动思考，寻找答案的时候，才更可能会有有创造力的发现。

以我的学习经历为例，很多人在上学期间，基本上都是老师在课上讲，自己只是听，很少提问，信息仅仅限于单向传递。而我经常会把不懂的问题整理下来，下课的时候主动向老师提问，这样做的好处是，勤于思考，可以让知识尽量没有盲点，另外通过提问和思考的方式 ，也可以让我对这个知识掌握得更牢固。我成绩通常不错，后来保送到了清华计算机系，很多人认为我平时学习是不是很晚，其实并没有，我只是善于找学习的规律，提问思考就是最好的学习方式。它更容易让我们对一件事物建立多维度的认知。

学会分享是最快的成长

如果说培养数据思维从提问开始，那么把总结分享作为结束则是最适合不过的。把学到的知识分享给身边的朋友，可以锻炼我们的逻辑性，分享的过程也是对知识重新梳理的过程。另一方面也可以让我们获得别人的反馈，更容易得到正反馈的愉悦。就像我们在做机器学习训练的时候，如果训练没有结果反馈，我们就无法客观地了解对知识的掌握程度。如果能得到别人的反馈，就更容易有收获，训练的收敛速度也会越快。

所以在某种程度上，你可以把分享的过程，理解是在测试集上做验证的过程。它会让你收获更多，成长更快。

培养数据分析思维是重要不紧急的事

你可能会说：“道理我都懂，可就是做的时候想不起来。”那是怎么回事呢？实际上，培养数据分析思维是重要不紧急的事。在工作中，我们经常会被紧急的事情占据带宽。这些紧急的事情对当下很重要，但是放长远来看重要性就很弱了。而拉开我们人生差距的，恰恰是那些重要不紧急的事情上，而不是在于我们每天处理了多少紧急的事。

这点很容易理解，毕竟人都有惰性，紧急的事情来了一般都会优先处理。不过你要换一种思考方式，既然我们人生的差距不是在于做过多少紧急的事，而是在于做过多少重要的事，那么从工作的第一天开始，我就应该着重积累重要的事，即使它目前并不紧急。

这样你会发现，当你做过的重要事情越来越多的时候，紧急的事情也就越来越少了。比如你想着如何找到一份更高薪酬更适合自己工作的时候，就不用着急每个月还贷款的事情了。

总结

今天我们做了一个有关生命线的游戏，你能了解到我们每个人、每个公司、每件事，只要有历史数据，都有可能从中发现规律，从而指导未来。所以说数据分析这件事，就好比是生命线一样闪耀着价值。

而培养自己的数据化思维虽然不是一天能练就的，却是重要的事情。很多时候，我们容易被紧急的事情牵着走，毕竟紧急事情的优先级会更高。但人生差距不是在于处理多少紧急的事，而是在于做过多少重要的事。从人性的角度来看，重要不紧急的事是容易被拖延的。

不过我有两个工具教你摆脱惰性，一个就是学会提问，它从提问的角度训练我们的数据化思维，让我们对事物看得更清楚，另一个就是学会分享，它从反馈的角度让我们的训练过程更加收敛，效率得到提升，也更容易获得成就感。

今天我讲到了生命线，它对我们发现自身的规律很有帮助。你不妨画下自己的生命线，从 0 岁开始到目前为止，把你认为对你影响最大的时刻下来，不论是正向，还是负向的事情。横坐标 X 轴代表时间，纵坐标 Y 轴标注事件点，绝对值越大代表事件对你的影响越大。画完之后，你能从中发现了什么规律吗？比如你的高能时刻，通常都是因为什么事情引起的？

我在专栏的开始就提到过分享是最好的老师，学会做总结笔记并分享出来，对自己的收获也会很大。专栏已经临近尾声，关于这个专栏的学习，你都做过哪些笔记总结呢？

欢迎你在评论区与我分享一下你的心得，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

45丨求职简历中没有相关项目经验，怎么办？

上节课我讲到了如何培养数据分析思维，它是一个重要但不紧急的事。在工作求职中，你可能会遇到各种又重要又紧急的事，比如填写求职简历中的项目经验。

它的重要性在于，HR 一般都会依据简历中的项目经验初步筛选候选人是否符合面试要求，紧急性在于求职找工作往往就是眼前的事，但简历中的项目经验又很难临时抱佛脚。项目经验一般没有弹性，一是一，二是二，一方面要保证真实性，是自己做过的项目，另一方面又很难在短时间内积攒这些经验。

如果没有项目经验，很多人就会感觉无从下手，这时候该怎么办呢？我自己面试过的技术人员少说也有上百人，我想以自己的经验做一些分享，在经验积累上和你分享以下三个需要注意的地方：

我们求职找工作的时候，要理解 HR 看项目经验的逻辑是什么？

明确要完善项目经验这个目标后，我们该如何快速定位要积累的内容，并通过实战和训练快速进行提升经验值？

如何在项目经验中融入自己的心得体会，让你的经验显得与众不同？

HR 看相关项目简历，背后的逻辑是什么

上篇文章中我讲到过，HR 之所以要看相关的项目经验，是因为这些历史信息可以帮助他预估一个人相关的工作能力。

知识不等于项目经验，即使你对知识都了解了，在实际项目过程中，还是会遇到各种问题。比如工具包安装不上、中文编码错误、画图显示不出来、算法运行过慢、数据拟合结果不好等各种问题。项目经历相当于一种训练，当你得到了更好的训练之后，数据分析的模型能力也就会越强，然后在“新公司”这个测试集中，就越有可能发挥好的效果。

做过训练和没有训练的人是完全不同的。如果你没有相关的经验，那么你现在找的这份工作就好比是训练集一样，没有一个公司会把他们的项目当做是你练手的数据集。大家都期望你是已经训练好的模型，可以马上开展新的工作，并且产生价值。

所以在经验积累上，你要证明给 HR，我做过这样的项目，具备这样的能力。

你可能想问，项目从哪里来呢？第一个肯定是以往类似的工作经历，第二个就是自己做过类似的项目。但是在简历中呈现数据分析的项目也是需要技巧的，简历不是流水账，你需要重点把当时的项目目标、采用的解决方案、实现的代码以及项目过程的总结体会拿给 HR 看。

这样，即使你没有相关的工作经历，如果你能通过专栏实战积累上面的 4 点，对 HR 来说也是有说服力的，这样总比一张白纸要强得多。要知道 HR 背后的逻辑是要通过简历证明你是已经被训练过的模型，可以上手工作了，而不是把新公司当成训练集。

如何完善简历里的项目经历

现在我们需要简历中有更多的项目经验。如果你跟着专栏从头到尾完整学习了，在爬虫、数据可视化、数据清洗和集成、数据挖掘算法、图像识别等多个维度进行了实战训练，那么恭喜你，实际上你已经具有数据分析相关的工作经验了。

这方面我来简单帮你总结下，梳理出一个项目简历的模板。但最根本的是，你需要自己跑一遍项目代码，完整了解项目目标和解决方案。只有这样，放到简历中的时候才会比较充实。

乳腺癌检测：采用 SVM 方法，对美国威斯康星州的乳腺癌诊断数据集进行分类，最终实现一个针对乳腺癌检测的分类器：https://github.com/cystanford/breast_cancer_data

内容抓取：通过 Python 爬虫对豆瓣电影中的电影数据和海报等信息进行抓取：https://github.com/cystanford/pachong

邮件数据分析：通过 PageRank 算法分析邮件中的人物关系图谱，并针对邮件数量较大的情况筛选出重要的人物，进行绘制：https://github.com/cystanford/PageRank

微博文档分类：采用朴素贝叶斯的方法，对微博的内容进行分类，最终实现一个简单的文档分类器：https://github.com/cystanford/text_classification

电影数据集关联规则挖掘：采用 Apriori 算法，分析电影数据集中的导演和演员信息，从而发现导演和演员之间的频繁项集及关联规则：https://github.com/cystanford/Apriori

歌词词云可视化：动态抓取指定明星的歌曲列表，保存歌词文件，去除歌词中的常用词，并对歌词进行词云展示，分析歌曲的作词风格：https://github.com/cystanford/word_cloud

信用卡违约率分析：针对台湾某银行信用卡的数据，构建一个分析信用卡违约率的分类器。采用 Random Forest 算法，信用卡违约率识别率在 80% 左右：https://github.com/cystanford/credit_default

信用卡欺诈分析：针对欧洲某银行信用卡交易数据，构建一个信用卡交易欺诈识别器。采用逻辑回归算法，通过数据可视化方式对混淆矩阵进行展示，统计模型的精确率，召回率和 F1 值，F1 值为 0.712，并绘制了精确率和召回率的曲线关系：https://github.com/cystanford/credit_fraud

比特币走势分析：分析 2012 年 1 月 1 日到 2018 年 10 月 31 日的比特币价格数据，并采用时间序列方法，构建自回归滑动平均模型（ARMA 模型），预测未来 8 个月比特币的价格走势。预测结果表明比特币将在 8 个月内降低到 4000 美金左右，与实际比特币价格趋势吻合（实际最低降到 4000 美金以下）：https://github.com/cystanford/bitcoin

不一样的项目经历和体会

上面我整理了 9 个项目简历的示例，如果认真学习专栏，并且坚持练习的话，那么不用愁相关的项目经验。如果你希望有不一样的项目经历，那么能融入自己的项目体会和总结的话，就会更好。

比如分析比特币走势这一篇文章中，我还提供了沪市指数的历史数据（从 1990 年 12 月 19 日到 2019 年 2 月 28 日），你完全可以采用 ARMA 模型自己跑一遍，然后整理出相关的经历。

再或者，我们对毛不易歌词进行词云分析的时候，你也可以分析其他的歌手，或者某个歌手的某张专辑的词云。模型方法是相同的，但不同的数据集出来的结果是不同的。

另外你也可以在项目实战中，融入自己的心得体会。比如在预测比特币走势这个项目中，我们对原始数据进行了降维，按月为粒度进行了统计，实际预测结果与按天进行统计的结果相差并不大，但是数据量降到了 1/30，大大提升了效率。在这个过程中，你应该能体会到数据降维的作用。

在信用卡欺诈分析这个项目中，我们观察到数据集的分类样本是不平衡的，针对这种情况，我们到底该采用哪个评价标准呢？为什么采用准确率作为评价标准会有问题？有关这方面的经验总结你也可以简单做个说明，这样不光可以证明你具备这种项目的经验，也能证明针对这类的问题，你都找到了哪些规律。

总之自己的心得体会和总结能给项目经验加分不少。

总结

在专栏的讲解过程中，很多同学都反馈过他们正在找工作，但项目经历这块是自己的软肋。我们关键要弄明白 HR 招人背后的逻辑，把相关的训练经验总结下来写在简历中，最后拆解专栏的实战项目。

在这个过程中你需要：

了解每个实战项目的目标；

理解每个算法的原理；

跑一遍项目代码，将运行结果放到 GitHub 上；

做项目的心得总结。

当你自己把这些内容整理出来的时候，你发现自己会更有信心。简历的完善只是表象，实际上最重要的是自己的能力也得到了提升，这也是通过学习专栏，我希望你能收获的价值。

我在专栏里讲解了理论知识、工具方法和实战项目，希望你把专栏作为一个工具，带你走入数据科学的大门。掌握了这个工具之后，平时遇到问题的时候，你就可以用数据的视角来分析它，使用工具来做模拟，总结结果，进一步完善你的简历。

我在专栏中讲解了理论知识，工具使用和实战项目，在 Python 爬虫、数据可视化和数据挖掘算法这三个部分中，除了专栏中讲解到的项目，你还做过哪些项目或者练习（采用相同的算法模型用于不同的数据集也可以），欢迎你分享一下。

另外，简历是最好的工作梳理，通过专栏的学习，你是否已经开始完善你的项目简历了呢，还有哪些地方是可以完善的？

欢迎你在评论区与我分享一下你的心得体会，也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。

加餐丨在社交网络上刷粉刷量，技术上是如何实现的？

我们都知道很多社交网络上可以刷粉，也可以刷阅读量，这已经形成了一个“产业链”，我们也经常会看到很多记者报道过这样的灰色产业链。

你也许很好奇，这些技术都是怎么实现的？

首先我梳理了一下整个流程，可以分成 3 个步骤。

在这个流程里，需要有下面三个准备。

1. 多个手机号

尽管早期注册只需要邮箱就可以，但现在账号注册都是需要绑定手机号的，所以手机号是必备的。

2. 多个 IP

因为很多社交网站都会有反垃圾的措施。共用同一个 IP，一定会被封号。

3. 模拟操作

因为我们的需求是个性化的。所以在这一步，可以封装出一些基本的操作，比如关注、发布动态、转发、阅读文章等。

那么多手机号从哪弄？

于是问题来了，从哪里弄这么多手机号？比如说 1 万个！

虚拟手机号：被歧视的号码段

很多从事相关产业的人首先想到的，便是虚拟手机号，因为虚拟手机号不限数量，其他号码段都需要绑定身份证。不过虚拟手机号有个最大的问题，就是“会被歧视”。在社交网络里，虚拟手机号注册的账号被封的概率远超其他账号，比如说以“170”开头的手机号。

阿里小号：一个看似可行的解决方案

既然虚拟手机号容易被封，那怎样才能找到既不会被封，还便宜的号码呢？阿里小号是个选择。阿里小号的价格比较亲民，5 元 / 月。可以自己选择号码段，这些号码段很多都不是 170 号码段的。但是阿里小号有个问题，就是需要用身份证来绑定。

国外号码，贵但价值明显

那有没有既不会被封，又不用绑定身份证的办法呢？国外的手机号是可以的，但最大的问题就是贵，差不多 5 美金一个月，相当于一个账号就要 35 元。

我调查了一下，其实国外也有类似的刷量刷粉的操作，比如刷 Facebook、Twitter、YouTube 等。这些网站刷粉、刷量的收费更高，所以相比之下，手机号带来的成本其实不算什么。

一个手机号就相当于你雇了一个工人，在雇佣的这段期间里，你需要让它的任务“充实”起来。一般的做法都是先从高价值的网站开始刷，在完成后，再来刷国内的 APP。如果有足够多的刷粉刷量的任务，这个“工人”就会 7X24 小时不间断地工作。

这些号码各有特点，根据实际情况，不同人手里用到的号码是不同的。我总结了一下。

如何自动切换 IP？

在解决多个手机号的情况后，那下个问题就来了，如何自动切换 IP 呢？这是一个自动化运营绕不开的问题。

有三种方式可以选择。

IP 代理。

飞行模式。

小区宽带。

IP 代理：所有人都能想到，但并不靠谱的方案

IP 代理应该是很多人首先想到的解决方案，因为在编写 Python 爬虫的时候，是可以使用代理机制的。网上就有很多做 IP 代理的。但这里有两个认知，你需要注意。

IP 代理没有你想得那么便宜。以不重复的 100 万 IP 为例，单日价格在 2700 元，相当于 1 万 IP=27 元。所以你会有这样一个疑惑：那些刷网站流量的，1 万流量只需要几元的是怎么做到的？

免费 IP 比你想象得要好用。实际上，有很多免费 IP 代理可以使用，它们主要的问题在于数量比较少。

所以在量少的情况下，IP 代理是可以使用的。在量大的情况下，IP 代理就没那么好用了，因为成本太高，并不是一个靠谱的方案。

飞行模式，一个让人飞起来的 idea！

当你发现，购买 IP 代理的价格比淘宝上售卖流量的还要贵的时候，你就知道他们用的根本不是 IP 代理。换个思维，有没有免费的 IP 呢？这里会用到手机的飞行模式，它和路由器断开重连一样完美！

这是为什么呢？

当我们的手机采用飞行模式后再关闭，你会发现手机的 IP 发生了变化。同样，当你断开路由器后，再进行重连，IP 也自动发生了变化。这个就是动态 IP。

WIFI 和 MIFI

那么问题来了，怎样写程序来控制手机呢？这里你可以使用 MIFI 设备，MIFI 其实就是 Mobile WIFI 的意思。MIFI 设备最大的好处，就是脱离了手机，你不需要一台手机，而只需要一台 MIFI 设备和一张 SIM 卡。

MIFI 设备集成了路由器和调制解调器的功能，使用的流量还是 SIM 卡的流量。所以当我们断开 MIFI 设备重连的时候，就相当于自动换了 IP。当然，这个过程需要定制 MIFI 设备，也就是多一个网线接口，把数据传输出来，这样就可以自动进行控制了。

MIFI 可能存在的问题

MIFI 可以说是个很方便的解决方案，但是依然存在一个问题，就是当流量大的时候，手机的流量费是很高的。那这样的话，就不能采用 MIFI 的方式了，可以考虑使用小区宽带。小区宽带最大的好处，就是不限流量。

当然除了控制 MIFI 设备外，还需要控制交换器，才能做到自动切换 IP。所以在流量较小的情况下，MIFI 是个好的解决方案。流量大的情况，比如要访问视频网站，小区宽带是更好的方案。

在自由切换 IP 这个部分，我整理了以下的 3 种方案，一般来说手机飞行适合轻度的并发访问，而重度的流量访问方式还需要采用小区宽带的方案。

如何模拟操作，是一个技术活

模拟操作，就是文章开头中，我提到的流程中的最后一步。

所有的流程，如果想要实现机器自动化，就需要一个利器。这里 Python 最适合不过。那么该怎么做呢？

首先这里需要用到 Python 的几个工具。在做自动化运营的过程中，会经常使用这些工具，我简单给你介绍下。

Selenium：用于 Web 测试的工具，支持多种浏览器和自动化测试。

lxml：网页解析利器，支持 HTML、XML、XPath 解析，而且解析效率很高。

Scrapy：强大的爬虫框架，提升开发效率。

PhantomJS：基于 WebKit 的无头浏览器，无头就是没有 UI 界面的意思。同时 PhantomJS 提供了 JavaScript API 接口，可以直接与 WebKit 内容交互。通过它，你可以完成无界面的自动化测试、网页截屏等。通过网页截屏，就可以帮水军做结案报告。通过结案报告，就可以看到刷量的直观数据结果。

通过以上的工具，我们就可以达到实时抓取，实时刷量的目的。当然这些工具只是表象，更重要的是模块化的思维。也就是如何使用这些工具，具体都做哪些事。一般来说，我们可以把自动化运营拆解成不同的模块。

下图是我以微信、微博为例，整理的自动化运营所需模块的全景图，这些模块都需要编写相应的代码来实现，从而打造整个社交网络上自动化运营的机器人团队。

虽然这篇文章给你讲了这个灰色地带，但我还是想强调一下，我只想通过这篇文章让你直观地体验数据思维是什么样的以及实现的方式是什么，这样才能更好地帮助你解决工作或者生活中遇到的问题。

作为技术出身的从业人员，我倡导不作恶的理念。所以我不建议你去购买这么多的手机号和 MIFI 设备，我希望你把重点放到如何掌握 Python 中数据分析的工具上，以及慢慢培养你的数据化思维。

我是一个数据分析爱好者，总是被各种问题吸引，带着好奇心，脑海中提出各种问题，然后通过思考一步一步进行解决。

所以，我希望你能在实际工作中，和我一样具有数据思维，以及数据分析的解决能力，这也是我们在《数据分析实战 45 讲》这个专栏里想要讨论的内容。

而我也希望你能通过这个专栏获得这样的能力。如果你觉得这篇文章有帮助，欢迎点击“请朋友读”，把它分享给你的朋友或者同事。

结束语丨当大家都在讲知识和工具的时候，我更希望你重视思维和实战

你好，我是陈旸。

我从去年十一前开始准备专栏，到今年 3 月底樱花飞舞的季节，转眼之间，专栏的内容已经全部更新完毕了。这期间将近有 200 天的时间，而专栏的更新也有 100 多天。非常开心能与大家度过这 100 多天的时间，这段时间里我看到了你们学习的热情，有很多同学认真做总结笔记，并分享出来，还有同学认真手绘专栏的思维导图，实践每一段代码……所有这些都让我看到了你们学习的热情。

我也要感谢洆洆的妈妈（我的老婆），当我准备专栏的时候，洆洆是一个不到 6 个月的宝宝，只有在夜晚洆洆睡着的时候，我才能开始录音工作。最应该感谢的就是我的老婆，她一个人带孩子。本来晚上回家我可以分担一些，不过为了让我安心地准备专栏，她晚上都是一个人哄宝宝睡觉。

同时，也要感谢我们的编辑和运营的同事。为了保证文章语义的准确和通俗易懂，编辑对每一篇文章都进行认真地修改，插图部分更是自己手绘出来的。多亏了运营，才有了大家互相交流的平台。

回想起来，这些经历依然历历在目。我很庆幸能与你们共同度过这段学习时间。从我最开始准备专栏的时候，我就在想，我希望通过专栏交付什么。我希望你通过专栏，不仅仅可以获得知识，掌握工具的使用，更希望你能得到思维和实战经验的提升。

理论到处有，实战最重要

当大家都在讲知识和工具的时候，我更希望你重视思维和实战。因为知识和工具是别人的，思维和实战才是自己的。

以我自己的经历来说，我从 10 岁开始编程，从最开始的 Basic 语言学起，再到 Pascal、C++、Java、PHP 和 Python。每种语言的学习切换基本上 2 天就可以完成，因为它们有很多共同的地方，不同地方仅仅在于第三方库和使用的框架。

如果我们只关注知识和工具的话，你会发现这些东西更新迭代的速度非常快。可能刚掌握了一个工具，新的工具又出来了。所以我们更应该关注那些不变的东西。

从数据分析的角度上来说，当我们刚从书本上学完了数据挖掘算法之后，你会发现又出现了深度学习。这两年深度学习框架，比如 TensorFlow、Keras、Caffe 大行其道，于是无数讲人工智能的课诞生了，而这些课基本上都是在给你讲解语法和工具的使用。

当大家都在贩卖知识和焦虑的时候，我更希望你能有独立思考的能力。深度学习固然很火，但本质上，仍然是机器学习的一种，都是帮我们创建分类器。你可以把传统的机器学习理解是专家级的方式，我们观察数据符合哪些特征，然后用各种分类、聚类算法处理这些数据。算法都是我们事先指定的。同样，深度学习更像一个黑盒子，我们无须事先指定提取特征的方法，而是通过深度学习这个大脑让机器自我训练，完成特征的提取。

当你开始思考这些方法之间的共同和差异点的时候，你将会收获更多。首先，你无须为新工具的出现产生无畏的焦虑感，因为每个工具都有适用的条件，深度学习虽然普适性强，但是对数据量的依赖性大，计算量大，收敛时间慢。在数据量不大的情况下，采用传统的机器学习可以更快收敛，同时还能得到不错的结果。

当你对这些知识和工具建立不同思考的时候，你更有可能理解和掌握它们。因为这些知识和工具的相同点，就会更容易完成知识和工具的迁移。它们的差异性可以让你更加了解每个知识和工具的特点。

所以当知识和工具爆炸出现的时候，我希望你不要随波逐流，可以独立思考，与这些东西建立多维度的连接。

同时，实战也是重要的成果体现。这就好比学习开车一样，学会开车和自驾旅行是完全不同的体验。只有通过实战，你才能解决一个特定的问题，领略到路途中的风采，为你的项目简历增加光彩的一笔。

方法比努力更重要

知识和工具是别人的，而思维和实战是自己的。那么在提升思维和实战经验的过程中，我希望你能掌握适合的方法。

我见过很多人，选了很多课，自己也很努力，但是收获的效果一般。实际上，好的学习方法比努力更重要。

我在专栏的开篇和结束的时候都提到了提问和分享，在开篇中也讲到了 MAS 学习法。实际上我们不是单向地被动接受知识和工具，更要建立思考和连接。建立多维度的连接，一个最好的方式就是学会提问以及学会分享。分享就好比是在测试集上做验证。分享的过程就是重新梳理知识的过程，还能得到别人的反馈，既受益别人，也获益自己。

投入越多，收获越多

当我们建立了正确的学习方法之后，你会发现投入越多，收获越多。

在专栏更新的过程里，我很高兴地看到，有些同学自己开始用工具分析王者荣耀的英雄属性，分析他们之间的关联；也看到有同学在用 ARMA 模型对股市指数进行预测，使用爬虫抓取数据……

如果说工作是公司的事情，那么思维和实战经验的积累则是自己的事情。在思考和实战这条道路上，投入越多，收获就会越多。我看到在不少文章的评论区，都有同学们自己总结的笔记，还有人把实战的代码放到了 GitHub 上。这都是在为自己的体验负责。

专栏本身只是一个开始，虽然专栏文章已经更新完毕，但大家的笔记分享不会结束。

不论你以后是否会从事一份和数据分析相关的工作，我都希望你可以把思考作为一种学习的领悟，把实战当做是一次项目的旅行。在思维和实战经验上，有些许的提升。