密西根大学-Python-数据科学笔记-全-
密西根大学 Python 数据科学笔记(全)
1:专项课程导论 🎯

在本节课中,我们将要学习密歇根大学《Python用于数据科学实践》专项课程的整体介绍。我们将了解课程结构、选择Python作为教学语言的原因,以及第一门课程的具体内容安排。
欢迎来到Python数据科学导论。
这门课程是一个更大的Python数据科学专项课程五门课程中的第一门。
每门课程都基于之前课程的知识逐步构建,旨在让你全面了解数据科学,同时帮助你培养实践数据科学的技能。
这个专项课程属于中级难度。
我们期望你过去学习过一些基础的编程和统计学知识。
在这个专项课程中,我们专注于使用Python编程语言教授应用技能。
数据科学中可以使用许多其他工具。
例如像R这样的专门统计分析语言,或者像Java和C这样的更通用的编程语言。
我们选择Python作为本专项课程的基础,主要有三个原因。
第一,它易于学习。
Python现在是向大学生介绍编程的首选语言,在美国排名前十的计算机科学项目中,有八个使用它。
Python程序通常具有最少的模板代码,这些模板代码你可能在其他语言中见过,并且对于你可能需要完成的典型任务,它提供了更自然的构造。
如果你有编程经验但没有Python特定经验。
你可以非常快速地掌握Python。
第二,它功能全面。
Python是一种非常通用的编程语言,拥有大量内置库,并且在处理数据、网络编程和数据库方面表现出色。
它很成熟,并且有大量资源可用,从书籍到在线课程。
最后,Python拥有大量重要的数据科学库可供使用。
这些库的基础被称为SciPy生态系统,它甚至有自己的系列会议。
我们将用于完成作业的界面叫做Jupyter Notebooks,以及前两门课程的主要库(Pandas和Matplotlib)都是SciPy栈的一部分,为进入机器学习、文本挖掘和网络分析领域提供了极好的基础。
这第一门课程分为四个模块。
第一个模块侧重于准备先决条件,并回顾Python语言的一些基础知识。
如果你已经掌握了Python并且希望接受挑战,请不要担心。
我们这里也包含了一些高级Python内容。
这些高级Python内容对于专项课程的其余部分并非严格必需。
但你在网络上或更广泛的数据科学主题(如大数据和实时分析)中可能看到的许多例子,可能需要了解其中一些更专业的功能。
上一节我们介绍了课程的整体结构和选择Python的原因,本节中我们来看看第一门课程的具体模块内容。
在第二个模块中,我们将深入研究pandas工具包。
pandas工具包是Python数据科学的基础,它提供了一种以表格形式思考数据的数据结构。
这个工具包帮助将R语言中存在的功能引入Python世界,并在过去五年中得到了广泛采用。
pandas背后的许多思想与关系理论相似。
因此,如果你有数据库背景,你会发现pandas环境相当自然。
同时,一些更高级的查询和操作pandas数据框的方法(如布尔掩码和分层索引)与数据库中的方法不同,需要仔细讨论。
所以我们将在本课程的第三个模块中讨论这些。
本课程的最后一个模块专门用于课程项目,在那里你将获取一些数据集,合并并清理它们,然后处理数据并回答问题。
在这一周,我们将讨论基本的统计测试和方法,并确保你为进入下一门课程打下坚实的基础。
同时,课程项目的目的是展示你将混乱数据操作成连贯内容所获得的技能。
在深入编程基础之前,我们将更多地讨论什么是数据科学以及它为何席卷全球。

本节课中我们一起学习了《Python用于数据科学实践》专项课程的总体介绍,了解了课程目标、Python的优势以及第一门课程四个模块(Python基础、pandas工具包、高级数据操作和课程项目)的核心内容。这为我们后续的学习奠定了清晰的路线图。
2:课程简介 📘


在本节课中,我们将要学习这门课程的整体介绍、学习目标以及推荐的学习方法。课程的核心是使用Python的pandas库进行数据操作,这是数据科学工作的基础。
课程概述与目标 🎯
我是克里斯·布鲁克斯,密歇根大学信息学院的教员。这门课程是关于数据操作的,数据操作是数据科学家工作的基石。即使随着你在该领域调查和技能的深入,这一点也不会改变。
在本课程中,我们将教你使用出色的pandas库。它非常适合数据操作,是Python领域事实上的标准。完成本课程后,你应该能熟练使用这个库。
我们将教你如何获取数据、清理数据、如何操作和连接数据,以及如何对该数据进行基本的推断。
我们期望你已经具备一些Python技能和一些统计学技能。但本课程是一个通过实践来构建这些技能的地方。课程平台侧边栏会有一些课程资源,描述了你获得Python经验的其他途径。
独特的教学与学习方法 💡
上一节我们介绍了课程目标,本节中我们来看看本课程采用的教学方法。
我为这门课程录制讲座的方式非常特别。我采用了一种教程风格,你看到的大部分内容实际上是我在Jupyter笔记本中键入代码的屏幕录像。
我在这其中做了一些独特的事情。首先,你会看到我所有的文本,我代码中的所有注释都是实时的。因此,你既能阅读也能听讲。
但我想与你分享一个我认为对学习以及使用Jupyter笔记本学习非常有效的策略。现在让我们来看看课程平台的一些内容。
这是左侧的Coursera课程平台。在右侧,我打开了Jupyter笔记本。这实际上是我用来演示的一门MOOC课程。
当你在课程中打开一个视频时,比如说这个关于数据框的视频,你会看到它是一个常规的教学视频。视频中会有一些讲解,但大部分内容会是这样的:我在Jupyter笔记本中逐步讲解示例。
我认为学习本课程最有效的方式实际上是进入你的Jupyter系统。右侧这里就是。会有一个关于Coursera Jupyter平台的单独视频。但为了真正创建一个新的Python笔记本,它是空的,你可以以讲座名称命名它。

例如,这个讲座是关于数据框的。然后从头开始跟着我做,并自己输入代码。所以在这里,我们可能会回到视频开头。在这里你可以看到,在这个例子中我正在查看一些热爱板球的国家。你可以在这里输入“cricket loving countries”,然后跟着示例操作。

这样做的好处是,你可以在任何时候暂停视频,进行探索,开始查看替代方案,并开始形成问题。这在观看视频时非常有效,能进行主动学习,而不是被动地观看视频。
视频功能与学习建议 📺
上一节我们介绍了主动学习的方法,本节中我们来看看视频提供的其他功能和学习建议。
视频还有一些其他功能。特别是,你可以增加或减少视频的播放速度。这对于复习也很有用。实际上,我认为高速播放视频非常棒。我经常以这种方式观看许多教育视频,主要是在我已经完成练习并体验过一次之后。当我只想快速回顾内容时,我会以大约1.5倍或2倍的速度播放,并尝试吸收那些内容。
我们为这门课程构建视频的方式(坦率地说,这对我来说也是新的,我很乐意得到你的反馈)是,视频中也包含了完整的文字记录。因此,你应该能够同时阅读和收听视频。
反复练习绝不会错。所以,请在观看教学视频时,打开一个空白的笔记本,跟着操作,看看你是否可能误解了某些内容。
互动与分享 🤝
以下是关于课程互动的一些建议。
你可以进行不同的探索,与你偏离的方向,并与班上其他同学分享你的发现或问题。

总结 📝

本节课中我们一起学习了《Python用于数据科学实践》第一课的简介。我们了解了课程的核心目标是掌握pandas库进行数据操作,包括数据获取、清理、连接和基本推断。我们介绍了一种通过Jupyter笔记本进行主动学习的有效方法,即边看视频边动手实践。我们还探讨了利用视频变速播放和文字记录进行复习的技巧。最后,我们鼓励大家在实践中探索,并与同学交流分享。
3:Jupyter笔记本系统入门指南 🚀


在本节课中,我们将学习Coursera平台内置的Jupyter笔记本系统。这个系统允许你在网页浏览器中完成作业、编写代码,甚至查看课程内容。它是一个非常强大的工具,能极大地提升你的学习体验。
概述
Jupyter笔记本系统是这门课程的核心工具之一。它提供了一个交互式的编程环境,让你能够直接在浏览器中编写和运行Python代码。本节将带你快速了解该系统的基本功能和使用方法。
目录树界面
当你登录Coursera的Jupyter系统后,首先看到的是目录树界面。你可以随时点击Coursera徽标返回这个界面。
这个界面显示了你文件系统的根目录。在这里,你可以看到已创建的文件夹和文件,例如一个名为readme.md的Markdown文本文件。
以下是创建新内容的方法:
- 点击“New”按钮旁的下拉箭头。
- 选择创建“Folder”(文件夹)、“Text File”(文本文件)、“Terminal”(终端)或“Python 3 notebook”(Python 3笔记本)。
Python笔记本核心功能
上一节我们介绍了如何创建新文件,本节中我们来看看最核心的Python 3笔记本。
创建Python 3笔记本后,你会进入编辑界面。界面中的每一行被称为一个“单元格”。单元格主要有两种类型:代码单元格和Markdown单元格。
在代码单元格中,你可以直接编写Python代码。例如:
x = 5
print(x)
编写代码后,点击运行按钮即可执行该单元格,输出结果会直接显示在单元格下方。
Jupyter笔记本的一个关键特性是它维护了一个持续的Python解释器状态。这意味着你运行过的变量和结果会被保留。例如,在定义了x = 5之后,在另一个单元格中运行print(x + 1),会得到输出6。
在界面右上角,你可以看到内核的状态提示,例如“内核空闲”。
内核管理与调试
有时你可能会运行一些耗时或出错的代码。例如,运行一个无限循环:
while True:
i = 1
此时,单元格旁边的星号*和右上角变黑的圆圈都表示内核正在运行。
如果你遇到代码无响应的情况,可以采取以下操作:
- 中断内核:类似于在键盘上按
Ctrl+C,可以随时停止当前运行。 - 重启内核:这相当于重启Python解释器。重启后,之前定义的变量状态会丢失。例如,重启后再打印
x,会得到NameError错误。
此外,你还可以使用“运行所有上方单元格”、“清除输出”等功能来管理笔记本。
Markdown单元格与文本编辑
除了代码,你还可以将单元格切换为Markdown格式,用于编写带格式的文本。
在Markdown单元格中,你可以使用特定语法来设置标题、加粗文本等。运行该单元格后,它会渲染成美观的格式。本课程中的许多说明文本都采用这种方式呈现。
终端功能
让我们回到目录树界面。另一个强大的功能是你可以打开一个终端。

终端让你以用户Jovian的身份登录到一个底层的Linux虚拟机(Docker容器)。你可以使用基本的Linux命令,例如:
ls:列出文件系统。cat [文件名]:查看文件内容,例如cat untitled.ipynb会显示笔记本的JSON源码。

你还可以使用conda或pip来安装新的Python包。虽然在第一门课程中可能不需要频繁使用终端,但了解它的存在和功能很有必要。
作业提交与验证
最后,我们来看看作业在Jupyter系统中的样子。



加载一个作业后,你会看到一个蓝色的“提交”按钮。点击它即可将编程作业部分提交到Coursera。
为了帮助你完成作业,系统中内置了一些单元测试。在做作业时,你可以点击“验证”按钮,系统会运行这些测试并给出反馈,提示你哪些单元格还需要修改。


提交作业后,你还会获得一个URL链接,可以随时回去查看作业的提交状态。
总结
本节课我们一起学习了Coursera Jupyter笔记本系统的基本操作。我们了解了如何创建和运行代码单元格与Markdown单元格,如何管理内核状态,如何使用终端,以及如何提交和验证作业。
这个系统功能丰富,支持许多快捷键和插件。虽然你也可以在个人电脑上安装Jupyter,但使用课程内置的这个集成环境非常方便。你的文件存储是以课程为单位的,但你也可以从目录树下载文件,并转移到其他课程中使用。


希望你会喜欢这个课程和Coursera平台提供的强大功能。
4:Python基础入门 🐍

概述
在本节课中,我们将要学习Python编程语言的基础知识。课程内容涵盖Python的基本特性、交互式编程环境的使用方法,以及如何定义和使用函数。无论你是否有编程经验,本教程都将以简单直白的方式引导你理解核心概念。
Python语言简介
上一节我们介绍了课程概述,本节中我们来看看Python语言的基本特性。
Python是一种高级语言,这意味着它更易于人类阅读而非机器执行。它也是一种解释型语言,这意味着代码无需直接编译为机器码即可运行。Python通常以交互方式使用,这与Java或C等需要先编译后运行的语言不同。
在Python中,你可以启动交互式解释器,逐行编写代码,解释器会即时评估每一条语句。这种方式对于需要大量探索的任务(如数据清洗)非常有用。
Python是动态类型语言,类似于JavaScript。这意味着声明变量时,无需预先指定其类型,你可以在后续代码中将其重新赋值为不同类型的值。
x = 10 # x是一个整数
x = "hello" # x现在是一个字符串
由于没有编译步骤,你需要在使用功能时检查其是否存在,或者尝试使用并捕获可能发生的错误。
交互式编程环境
上一节我们了解了Python的特点,本节中我们来看看如何在交互式环境中使用它。
Coursera平台允许你通过两种方式在浏览器中运行Python代码。第一种是使用交互式提示,问题会出现,你可以在浏览器中尝试解决。第二种是使用集成的Jupyter Notebook。
Jupyter Notebook允许你将代码划分为多个“单元格”,并按需执行这些单元格。这些笔记本为你的学习提供了半永久性的存储,你可以在不安装任何软件的情况下进行编程实验。
所有作业都将在Jupyter Notebook中完成。当然,你也可以在自己的计算机上下载并安装Python、Jupyter以及本课程所需的所有相关库。
编写第一个Python代码
以下是编写第一个Python代码的步骤:
- 打开Jupyter Notebook环境。
- 在第一个单元格中,输入你的代码。
- 按
Shift + Enter或点击工具栏中的播放按钮来运行单元格。
让我们看一个简单的例子。以下代码设置两个变量并将它们相加:
x = 1
y = 2
x + y
运行此单元格后,输出结果3会立即显示在下方。Python解释器是有状态的,这意味着变量在不同单元格之间持续存在。
如果你返回修改了之前单元格中的内容,必须重新执行该单元格才能使更改生效。“重启并运行全部”功能特别有用,它会清除解释器状态并重新运行当前笔记本中的所有单元格。
Python函数基础
上一节我们运行了简单的代码,本节中我们来看看如何组织代码——使用函数。
Python使用传统的软件结构,如函数。以下是一个将之前代码重构为函数的例子:
def add_numbers(a, b):
return a + b
def语句表示我们正在定义一个函数。属于该函数的每一行代码都需要使用制表符或空格进行缩进。
在交互式环境中,当使用Shift + Enter评估语句时,结果会立即打印在下方。
函数的特性与细节
以下是Python函数的一些重要特性和细节:
- 无返回类型声明:由于Python是动态类型语言,你无需指定函数的返回类型。
- 可选的返回语句:函数可以不使用
return语句。在这种情况下,函数会返回一个特殊值None,它类似于Java中的null,表示没有值。 - 默认参数值:你可以为函数参数设置默认值。例如:
这意味着你可以用两个或三个参数调用def add_numbers(a, b, c=None): if c is None: return a + b else: return a + b + cadd_numbers函数。 - 关键字参数:你可以通过参数名来传递值,这称为关键字参数。例如:
def configure(verbose=True, flag=False): # 函数体 pass configure(flag=True) # 仅设置flag参数 - 函数是一等公民:在Python中,你可以将函数赋值给变量。这意味着你可以将函数作为参数传递给其他函数,从而实现一些基本的函数式编程。例如:
my_function = add_numbers result = my_function(5, 3) # 调用变量,实际上调用了add_numbers
总结


本节课中我们一起学习了Python编程语言的基础知识。我们了解了Python作为高级、解释型、动态类型语言的特点,熟悉了在Jupyter Notebook交互式环境中编写和运行代码的方法。我们重点学习了如何定义和使用函数,包括函数的参数、返回值、默认值以及将函数作为变量使用的概念。掌握这些基础知识是进行后续数据科学实践的重要第一步。
5:Python类型与序列 🐍

在本节课中,我们将要学习Python中的基本数据类型以及三种核心的集合类型:元组、列表和字典。理解这些类型是进行数据操作和分析的基础。
概述
Python虽然没有静态类型,但每个值都有其类型。type() 内置函数可以显示给定引用的类型。常见的类型包括字符串、整数、浮点数以及函数类型。对象拥有与其关联的属性,这些属性可以是数据或函数。
Python围绕不同类型的序列或集合构建。我们将讨论三种原生的集合类型:元组、列表和字典。
元组
上一节我们介绍了Python的类型系统,本节中我们来看看第一种集合类型——元组。
元组是一个变量序列,其本身是不可变的。这意味着元组中的项目有顺序,但一旦创建就不能更改。我们使用圆括号 () 来书写元组,并且可以混合其内容的类型。
my_tuple = (1, 2, ‘hello‘, ‘world‘)
请注意,字符串可以使用单引号或双引号表示。
列表
列表与元组非常相似,但它是可变的,因此你可以更改其长度、元素数量和元素值。列表使用方括号 [] 声明。
以下是修改列表内容的几种方法:
append()函数:允许你将新项目追加到列表的末尾。
列表和元组都是可迭代类型,因此你可以编写循环来遍历它们持有的每个值。通常,如果你想查看列表中的每个项目,可以使用 for 语句。这类似于Java和C#中的 foreach 循环,但请注意不需要指定类型。
my_list = [1, 2, 3]
for item in my_list:
print(item)
序列的访问与操作
列表和元组也可以像其他语言中的数组一样,通过使用方括号运算符(称为索引运算符)进行访问。列表的第一个项目从位置 0 开始。要获取列表的长度,我们使用内置的 len() 函数。
还有一些其他常见的函数,如 min() 和 max(),它们可以查找给定列表或元组中的最小值或最大值。
Python列表和元组也允许一些基本的数学运算:
- 加号
+:连接列表。 - 星号
*:重复列表的值。
一个非常常见的运算符是 in 运算符。它检查集合成员资格,并根据一个项目是否在给定列表中返回布尔值 True 或 False。
我们将在未来几周深入研究运算符和特殊类型的序列,届时我们将探讨一种称为“广播”的技术。
切片操作
也许你可以对列表执行的最有趣的操作称为切片。虽然用于访问元素的方括号数组语法可能看起来与你其他语言中看到的相似,但在Python中,索引运算符允许你提交多个值。
切片参数说明如下:
- 第一个参数是起始位置。如果这是唯一元素,则返回列表中的一个项目。
- 第二个参数是切片的结束位置,并且是不包含的。因此,如果你用第一个参数为0,下一个参数为1进行切片,那么你只会得到一个项目。

通过一个例子来解释会容易得多。Python的一个方便之处在于,所有字符串实际上都只是字符列表。因此,切片在它们身上效果很好。
x = “hello”
print(x[0]) # 输出 ‘h‘
print(x[0:1]) # 输出 ‘h‘
print(x[0:2]) # 输出 ‘he‘
我们的索引值也可以是负数,这非常酷,这意味着从字符串的末尾开始索引。
print(x[-1]) # 输出 ‘o‘ (最后一个字母)
print(x[-4:-2]) # 输出 ‘el‘ (从倒数第四个到倒数第二个位置)

最后,如果我们想隐式地引用字符串的开头或结尾,可以通过将参数留空来实现。
print(x[:3]) # 输出 ‘hel‘ (从第一个字符到位置3)
print(x[3:]) # 输出 ‘lo‘ (从第四个字符到列表末尾)
切片是Python语言的核心,也是Python科学计算的重要组成部分,尤其是在你开始操作矩阵时。我们将在下一个模块中更多地讨论切片。
字符串操作
现在,我稍微离题一下,谈谈操作字符串。切片并不是操作字符串的唯一方式。一个常见的活动是基于子字符串分割字符串,即遍历字符串,寻找模式并适当地分割它。这称为正则表达式评估,我们将在专门处理文本挖掘的部分详细讨论这一点,因为它是一个非常常见的操作。
但Python有一些用于文本分析的基本工具,我将在这里向你展示它们。正如我们所见,字符串只是字符列表。因此,你可以对列表执行的操作,也可以对字符串执行。这意味着你可以使用加号运算符连接两个字符串,并且乘以字符串将重复给定的字符串。你也可以使用 in 运算符搜索字符串。
字符串类型有一个名为 split() 的相关函数。此函数基于简单模式将字符串分解为子字符串。
full_name = “Christopher Paul John Smith“
name_parts = full_name.split(‘ ‘)
print(name_parts) # 输出 [‘Christopher‘, ‘Paul‘, ‘John‘, ‘Smith‘]
结果是一个包含四个元素的列表。我们可以使用索引运算符选择第一个元素作为名字,最后一个元素作为我的姓氏。
字典
在进一步讨论字符串之前,我想谈谈字典。字典与列表和元组相似,因为它们都包含项目集合,但字典是带标签的集合,没有顺序。这意味着对于你插入字典的每个值,还必须提供一个键来取出该值。在其他语言中,这种结构通常称为映射。在Python中,我们使用花括号 {} 表示字典。
email_dict = {‘Chris‘: ‘chris@umich.edu‘, ‘Jenny‘: ‘jenny@umich.edu‘}
print(email_dict[‘Chris‘]) # 输出 ‘chris@umich.edu‘
字典中用于键或值的类型可以是任何类型,如果你愿意,也可以是混合类型。我们可以使用我们习惯的相同索引运算符(在赋值语句的左侧)向字典添加新项目。
你可以通过多种方式迭代字典中的所有项目:
- 首先,你可以迭代所有键,然后根据需要取出内容。
- 或者,你可以迭代值而忽略键。
- 最后,你可以使用
items()函数同时迭代值和键。
最后一个例子有点不同,它是一个称为解包的示例。在Python中,你可以有一个序列(一个值列表或元组),并且可以通过一个语句中的赋值将这些项目解包到不同的变量中。
person_tuple = (‘Chris‘, ‘Smith‘, ‘chris@umich.edu‘)
first_name, last_name, email = person_tuple
在底层,Python解包了元组并按顺序分配了这些变量。如果我们给元组添加第四个项,Python不知道如何解包它,因此会出现错误。

总结
本节课中我们一起学习了Python的核心内置类型和集合。我们探讨了不可变的元组、可变的列表以及键值对结构的字典。我们学习了如何访问序列元素、使用切片操作数据、进行基础的字符串处理,以及利用解包功能高效地处理数据。这些是Python编程,特别是数据科学中数据处理的基础。在下一讲中,我们将简要回顾字符串,然后开始处理一些数据文件。
6:Python用于数据科学实践 📚

概述:Python字符串进阶 🧵
在本节课中,我们将要学习Python字符串处理的进阶知识。我们将探讨Python 3中字符串的Unicode本质,并学习如何使用强大的字符串格式化语言来优雅地组合和输出文本信息。
字符串与Unicode 🌍
上一节我们介绍了列表和切片,其中也谈到了字符串。你已经见过通过split函数和直接索引来拆分字符串的方法。
Python中的字符串处理有时会令人感到困惑,因此我想分享更多关于字符串处理机制的细节,以便你了解它们。
在Python 3中,字符串默认基于Unicode。在早期计算中,字符串的字符被限制在256个不同的值内。这足以涵盖所有大小写的拉丁字母以及单个数字。这种编码被称为ASCII,相当紧凑。
但世界不仅仅运行在拉丁字符上。需要支持非英语语言,以及那些不常用于单词但常用于其他地方的字符,例如数学运算符。
Unicode转换格式(UTF)试图解决这个问题。它可以用来表示超过一百万个不同的字符。这不仅包括你可能预期的人类语言,还包括像表情符号这样的符号。
Python 3默认使用Unicode,因此在处理国际字符集时没有问题。
字符串格式化语言 ✨
除了Unicode,Python还使用一种特殊的语言来格式化字符串的输出。动态类型的一个挑战是,当你必须自己进行类型转换时,情况会有点不明确。
我们在上一讲中看到,如果我们想打印一个名字和一个数字,如果不先调用str函数将数字转换为字符串,就无法使用连接操作。
这会产生很多看起来混乱的代码,其中每个你想要连接的运算符都被包裹在这个str函数中。
Python字符串格式化迷你语言允许你编写一个包含变量占位符的字符串语句。然后,你可以按名称或按顺序传递这些变量,Python会为你处理字符串操作。
以下是一个例子。
想象我们有一个包含订单详情的字典,其中包括商品数量、价格和一个人的名字。我们可以编写一个销售声明字符串,使用花括号来包含这些项目。然后,我们可以在该字符串上调用format方法,并传入我们想要替换的适当值。
现在,字符串格式化语言允许你做的远不止这些。你可以控制许多不同的方面,例如浮点数的小数位数,是否想在正数前加上加号,设置字符串的对齐方式为左对齐或右对齐,甚至启用科学计数法。

我已经在课程资源中链接了文档页面,能够使用字符串库和格式化语言将是你第一次作业的重要组成部分。
总结与展望 🔮
本节课中我们一起学习了Python字符串的Unicode基础以及强大的字符串格式化功能。这是一个简短的讲座,但字符串操作是数据清洗的重要组成部分。在本课程的作业中,你将学到更多关于字符串操作的知识。
在下一个视频中,我们的研究生助理将向你展示如何通过读写分隔数据文件来进行一些基本的数据分析。
7:Python CSV文件读写与数据汇总 📊

在本教程中,我们将学习如何通过迭代CSV文件来创建字典并计算汇总统计量。我们将从读取CSV文件开始,逐步学习如何提取数据、进行计算,并通过分组分析来获取更深入的洞察。
导入必要模块与设置
首先,我们需要导入Python的csv模块,它提供了读取和写入CSV文件的功能。
import csv
为了方便后续的数据展示,我们设置浮点数打印精度为两位小数。
%precision 2
读取CSV文件并转换为字典列表
现在,我们使用csv.DictReader来读取名为“MPG”的CSV文件,并将其转换为一个字典列表。每个字典代表CSV文件中的一行数据。
with open('mpg.csv') as csvfile:
mpg_data = list(csv.DictReader(csvfile))
让我们查看列表中的前三个元素,以了解数据的结构。
print(mpg_data[:3])
输出显示,列表中的每个字典都以CSV的列名作为键,每辆车的具体数据作为对应的值。列表的长度是234,这意味着我们的数据集中包含了234辆汽车的信息。
print(len(mpg_data))
要查看CSV文件的所有列名,可以使用字典的.keys()方法。
print(mpg_data[0].keys())
计算整体平均值
假设我们想计算数据集中所有汽车的平均城市油耗(MPG)。我们需要遍历字典列表,对每辆车的城市MPG值进行求和,然后除以车辆总数。
由于字典中的所有值最初都是字符串类型,我们需要先将它们转换为浮点数才能进行数学运算。
sum_city_mpg = sum(float(d['cty']) for d in mpg_data)
avg_city_mpg = sum_city_mpg / len(mpg_data)
print(avg_city_mpg)
类似地,我们可以计算所有汽车的平均高速公路油耗。
sum_hwy_mpg = sum(float(d['hwy']) for d in mpg_data)
avg_hwy_mpg = sum_hwy_mpg / len(mpg_data)
print(avg_hwy_mpg)
结果显示,平均高速公路油耗高于城市油耗,这与实际情况相符。
按气缸数分组计算平均值
上一节我们计算了整体平均值,本节中我们来看看一个更复杂的例子:按汽车的气缸数分组计算平均城市MPG。
首先,我们需要找出数据集中气缸数的所有唯一值。
cylinders = set(d['cyl'] for d in mpg_data)
print(cylinders)
我们看到数据集中包含4缸、5缸、6缸和8缸的汽车。
以下是计算每个气缸数级别平均城市MPG的步骤:
- 创建一个空列表来存储计算结果。
- 遍历每个唯一的气缸数级别。
- 对于每个级别,再次遍历所有字典,匹配具有该气缸数的车辆。
- 累加匹配车辆的MPG值并计数。
- 计算平均值并将其添加到结果列表中。
avg_mpg_by_cyl = []
for cyl in cylinders:
sum_mpg = 0
count = 0
for d in mpg_data:
if d['cyl'] == cyl:
sum_mpg += float(d['cty'])
count += 1
avg_mpg = sum_mpg / count
avg_mpg_by_cyl.append((int(cyl), avg_mpg))
# 按气缸数从低到高排序
avg_mpg_by_cyl.sort()
print(avg_mpg_by_cyl)
从结果可以看出,随着气缸数的增加,平均城市油耗呈下降趋势。
按车型类别分组计算平均值
接下来,让我们看另一个类似的例子:计算不同车型类别的平均高速公路MPG。
首先,查看数据集中有哪些车型类别。
classes = set(d['class'] for d in mpg_data)
print(classes)
数据集中包含两座跑车、紧凑型、中型、小型货车、皮卡、超小型和SUV等类别。
计算每个车型类别平均高速公路MPG的逻辑与上一个例子类似:
- 遍历每个唯一的车型类别。
- 对于每个类别,遍历所有字典,匹配属于该类别的车辆。
- 累加高速公路MPG值并计数。
- 计算平均值并存储。
avg_hwy_by_class = []
for v_class in classes:
sum_hwy = 0
count = 0
for d in mpg_data:
if d['class'] == v_class:
sum_hwy += float(d['hwy'])
count += 1
avg_hwy = sum_hwy / count
avg_hwy_by_class.append((v_class, avg_hwy))

# 按MPG从低到高排序
avg_hwy_by_class.sort(key=lambda x: x[1])
print(avg_hwy_by_class)
结果显示,皮卡的平均油耗最差,而紧凑型车的油耗表现最佳。
总结与展望 🎯
本节课中,我们一起学习了如何使用Python的csv模块读取CSV文件,并将其转换为字典列表进行数据处理。我们掌握了计算整体平均值、以及按特定字段(如气缸数、车型类别)进行分组并计算汇总统计量的方法。
如果你觉得这种通过手动迭代进行数据分析的方式有些低效或繁琐,不必担心。在下周的课程中,我们将学习Pandas库,它是一个功能强大、效率更高的Python数据分析工具,能让类似的任务变得简单快捷。
感谢观看,期待下次再见!
8:Python日期与时间处理 📅⏰

在本节课中,我们将要学习Python中处理日期和时间的基础知识。许多数据分析任务都与日期和时间相关,例如计算特定时间段内的平均销售额,或筛选在给定期间内购买的产品。掌握日期时间的处理是数据科学实践中的重要一环。
从“纪元时间”说起
上一节我们介绍了数据分析中日期时间的重要性,本节中我们来看看计算机系统中一种常见的日期时间存储方式。
首先,你需要知道日期和时间可以用多种不同的方式存储。在线交易系统中最常见的传统存储方法之一是基于“纪元”的偏移量。“纪元”指的是1970年1月1日。虽然这背后有很多历史原因,但在系统中看到以自该日期起的秒数或毫秒数来存储交易时间的情况并不少见。
因此,如果你在期望看到日期和时间的地方看到了很大的数字,就需要对它们进行转换才能理解数据的含义。
在Python中处理时间
了解了纪元时间的概念后,我们来看看如何在Python中操作它。
在Python中,你可以使用 time 模块获取自纪元以来的当前时间。
import time
current_time = time.time()
然后,你可以使用datetime对象上的 fromtimestamp 函数创建一个时间戳。
from datetime import datetime
timestamp = datetime.fromtimestamp(current_time)
当我们打印这个值时,会看到年、月、日等信息也被打印出来。
datetime对象具有方便的属性来获取对应的小时、天、秒等。
year = timestamp.year
month = timestamp.month
day = timestamp.day
hour = timestamp.hour
使用时间差进行计算
datetime对象允许使用时间差进行简单的数学运算。例如,这里我们可以创建一个100天的时间差,然后与datetime对象进行减法和比较。
以下是使用时间差进行计算的步骤:
- 从
datetime模块导入timedelta。 - 创建一个表示100天的时间差对象。
- 对日期进行加减运算或比较。
from datetime import timedelta
hundred_days = timedelta(days=100)
future_date = timestamp + hundred_days
past_date = timestamp - hundred_days
is_future = future_date > timestamp
在数据科学中的应用:创建时间窗口
时间差计算在数据科学中常用于创建时间窗口。例如,你可能想找出销售额最高的任意五天时间段,并将其标记出来以供后续分析。
其核心思路是定义一个固定的时间长度(如5天),然后在时间序列上滑动这个窗口进行计算或筛选。

# 假设我们有一个日期列表和对应的销售额列表
# 这里简化表示滑动窗口的概念
window_size = timedelta(days=5)
# 在实际分析中,你会遍历日期,计算每个5天窗口内的总销售额
本节课中我们一起学习了Python处理日期和时间的基础。我们了解了“纪元时间”的概念,学习了如何使用 datetime 模块创建和操作时间对象,以及如何利用 timedelta 进行时间计算并创建用于分析的时间窗口。在后续课程中,我们将使用Pandas库更深入地研究日期和时间。
9:Python高级对象与映射

在本节课中,我们将要学习Python中面向对象编程的基础知识以及map函数的功能与应用。这些概念是构建复杂数据科学工作流的重要基石。
面向对象Python简介
到目前为止,我们尚未深入讨论面向对象的Python。虽然函数在Python生态系统中扮演重要角色,但Python确实拥有类,这些类可以附加方法,并能被实例化为对象。
本课程并非深入探讨Python对象或面向对象编程的繁琐细节。实际上,尽管你会在Python中大量使用对象,但在交互式环境中创建新类的可能性较小,因为这通常较为冗长。但了解Python对象的一些细节仍然很重要,以免在遇到时感到意外。
定义类
首先,你可以使用class关键字定义类,并以冒号结尾。在此关键字下方缩进的任何内容都属于该类的范围。
class ClassName:
# 类定义体
Python中的类通常使用驼峰命名法,即每个单词的首字母大写。
类变量与实例变量
你无需在对象内部声明变量,可以直接开始使用它们。也可以声明类变量,这些变量在所有实例之间共享。
class Person:
school = "School of Information" # 类变量
def set_name(self, name):
self.name = name # 实例变量
def set_location(self, location):
self.location = location # 实例变量
在上面的例子中,我们为所有人设置了一个默认的学校。虽然在此处用处不大,但为了完整性我们展示了它。
定义方法
定义方法与定义函数类似。一个关键变化是,为了让方法能够访问调用它的实例,必须在方法签名中包含self参数。同样,如果你想引用对象上设置的实例变量,需要在变量前加上self和一个点号。
在这个Person类的定义中,我们编写了两个方法:set_name和set_location,它们分别更改名为name和location的实例绑定变量。
运行此代码单元时,我们看不到任何输出。类已存在,但我们尚未创建任何对象。
实例化与使用
我们可以通过在类名后加上空括号来实例化这个类。然后,我们可以使用在大多数语言中常见的点号表示法来调用函数和打印类的属性。
person1 = Person()
person1.set_name("Alice")
person1.set_location("New York")
print(person1.name)
print(person1.school)
重要注意事项
从这个简短的例子中,你应该了解Python面向对象编程的几个含义:
第一,Python中的对象没有私有或受保护的成员。如果你实例化一个对象,你就拥有访问该对象任何方法或属性的完全权限。
第二,在Python中创建对象时不需要显式的构造函数。如果需要,你可以通过声明__init__方法来添加构造函数。
class Person:
def __init__(self, name, location):
self.name = name
self.location = location
现在,我不会更深入地探讨Python对象,因为其中有很多细微之处。坦白说,对于数据科学入门而言,Python的大多数面向对象特性并不那么突出。如果你对此更感兴趣,我建议查阅Python教程中的文档,它对语言的面向对象特性有相当全面的概述,课程资源中也会提供参考。
函数式编程与map函数
上一节我们介绍了Python中面向对象的基础,本节中我们来看看函数式编程的一个核心工具——map函数。
map函数是Python中函数式编程的基础之一。函数式编程是一种编程范式,在这种范式中,你需要显式声明所有可能通过给定函数执行而改变的参数。因此,函数式编程被称为无副作用的,因为它有一个软件契约来描述调用函数时实际可以改变的内容。
现在,Python并非纯粹意义上的函数式编程语言,因为函数可能产生许多副作用,而且你当然不必传入所有你希望更改的内容。但函数式编程促使人们更深入地思考如何将操作链接在一起,这确实是数据科学(尤其是数据清洗)中的一个基本主题。
因此,函数式编程方法在Python中经常使用,看到一个函数的参数本身是另一个函数的情况并不少见。
内置的map函数是Python函数式编程特性的一个例子,我认为它结合了该语言的多个方面。
map函数签名
map函数的签名如下所示。第一个参数是你想要执行的函数,第二个及之后的参数是可以迭代的对象。
map(function, iterable, ...)
所有可迭代的参数会被一起解包并传入给定的函数。
这听起来有点晦涩,让我们看一个例子。假设我们有两个数字列表,可能是两个不同商店对完全相同商品的价格。我们想找出在两个商店之间购买更便宜商品所需支付的最低价格。
使用map进行比较
为此,我们可以遍历每个列表,比较商品并选择最便宜的。使用map,我们可以在单个语句中完成这种比较。
store1_prices = [10, 20, 30]
store2_prices = [15, 18, 35]
cheapest_prices = map(min, store1_prices, store2_prices)
但当我们尝试打印这个map对象时,会看到一个奇怪的引用值,而不是我们期望的项目列表。
print(cheapest_prices) # 输出类似:<map object at 0x...>
惰性求值
这被称为Python中的惰性求值。map函数返回一个map对象,并且在你查看内部值之前,实际上并不会尝试对两个项目运行给定的函数。
这是语言中一个有趣的设计模式,在处理大数据时常用。这使我们能够实现非常高效的内存管理,即使某些计算可能很复杂。
迭代map对象
map对象是可迭代的,就像列表和元组一样,因此我们可以使用for循环来查看map中的所有值。
for price in cheapest_prices:
print(price)
这种传递函数及其应应用于的数据结构的模式,是函数式编程的标志,在数据分析和清洗中非常常见。
实践练习
以下是一个供你尝试的问题,它结合了你可能在数据清洗中期望执行的一些任务。

问题:你有两个列表,一个包含产品名称,另一个包含对应的价格。使用map函数和一个自定义函数,创建一个新的迭代器,其中每个元素是一个字符串,格式为“产品: $价格”。
products = ["Apple", "Banana", "Cherry"]
prices = [1.2, 0.5, 2.1]
def format_item(product, price):
return f"{product}: ${price:.2f}"
formatted_items = map(format_item, products, prices)
for item in formatted_items:
print(item)
总结
本节课中我们一起学习了Python中面向对象编程的基础,包括如何定义类、实例变量、类变量和方法。我们还深入探讨了函数式编程的核心概念,特别是map函数的使用、其惰性求值特性以及它在数据清洗和分析中的实际应用。掌握这些高级对象和映射技术,将为你构建高效、可维护的数据科学代码奠定坚实基础。
10:Python Lambda表达式与列表推导式 🐍✨

在本节课中,我们将要学习Python中两个强大而简洁的特性:Lambda表达式与列表推导式。它们能帮助我们以更少的代码完成常见的数据处理任务。
Lambda表达式:匿名函数 🔄
在本周的内容中,你可能已经看到了关键字lambda的出现。随着你越来越多地使用Python进行数据科学工作,你肯定会更频繁地见到它。
Lambda是Python创建匿名函数的方式。它们与其他函数相同,但没有名称。其意图在于它们是简单或短命的函数,直接在一行中写出函数比费力创建一个命名函数更容易。
Lambda的语法相当简单,但可能需要一些时间来适应。
你使用关键字lambda声明一个lambda函数,后跟一个参数列表,接着是一个冒号,然后是一个单个表达式。这是关键:在lambda中只能计算一个表达式。表达式的值会在lambda执行时被返回。
lambda的返回值是一个函数引用。因此,在这个例子中,你将执行my_function并传入三个不同的参数。
核心概念:Lambda表达式的基本结构
lambda arguments: expression
请注意,Lambda参数不能有默认值,并且Lambda内部不能有复杂的逻辑,因为你被限制为只能使用单个表达式。

Lambda表达式的局限性与用途 📝
上一节我们介绍了Lambda表达式的基本语法,本节中我们来看看它的特点。
因此,Lambda确实比完整的函数定义受限得多。但我认为它们对于简单的小型数据清理任务非常有用。你会在网上看到大量使用它们的例子。所以,你应该能够阅读和编写Lambda表达式。让我们在这里尝试一下。

列表推导式:简洁的序列构建 🚀
我们已经学习了Python中很多关于序列的知识:元组、列表、字典等等。
序列是我们可以迭代遍历的结构,我们通常通过循环或从文件读取数据来创建它们。Python内置支持使用一种更简短的语法来创建这些集合,这种语法称为列表推导式。
以下是一个例子。首先,我们在这里写一个小型for循环,我在0到1000之间迭代,然后使用取模运算符检查数字除以2是否会产生任何小数。如果数字对2取模的结果是0,那么我知道它能被整除。所以这一定是一个偶数,我会把它添加到我们的列表中。
我们可以通过将迭代写在一行来将其重写为列表推导式。我们以列表中想要的值开始列表推导式。在这个例子中,它是一个数字。然后我们将其放入for循环中。最后,我们添加任何条件子句。
你可以看到这是一种紧凑得多的格式,而且往往也更快。
核心概念:列表推导式的基本结构
[expression for item in iterable if condition]
列表推导式的优势与练习 💡
就像Lambda表达式一样,列表推导式是一种紧凑的格式,可以提供可读性和性能上的好处。你经常会在数据科学教程或Stack Overflow上发现它们被使用,但本课程的作业并不要求你必须使用它们。

在这里,你为什么不尝试将一个函数转换为列表推导式呢?
总结 🎯
本节课中我们一起学习了Python中两个重要的简洁编程工具:Lambda表达式和列表推导式。Lambda允许我们创建简单的匿名函数,而列表推导式则提供了一种快速构建列表(或其他可迭代对象)的优雅方式。理解并熟练运用它们,能让你的代码更加简洁高效。
11:数值计算库NumPy 🧮


在本节课中,我们将学习Python数值计算的基础库——NumPy。我们将了解如何创建和操作数组,进行数学运算,以及如何将数据集加载到数组中进行基本分析。NumPy是许多高级数据科学库(如pandas)的基石,掌握其核心概念至关重要。
概述:什么是NumPy?
NumPy是Python中进行数值计算的基础包。它提供了创建、存储和操作数据的强大方法,使其能够与多种数据库和数据格式无缝、快速地集成。它也是pandas库构建的基础,pandas是一个我们将要学习的高性能、以数据为中心的包。
创建数组
上一节我们介绍了NumPy的基本概念,本节中我们来看看如何创建数组。
NumPy数组可以显示为列表或列表的列表,也可以通过列表来创建。创建数组时,我们向np.array()函数传入一个列表作为参数。
import numpy as np
import math
a = np.array([1, 2, 3])
print(a)
print(a.ndim) # 打印数组的维度数
如果我们向np.array()传入一个列表的列表,就可以创建一个多维数组,例如矩阵。
b = np.array([[1, 2, 3], [4, 5, 6]])
print(b)
print(b.shape) # 打印每个维度的长度
print(a.dtype) # 检查数组中元素的类型
除了整数,NumPy数组也接受浮点数。
c = np.array([2.2, 5, 1.1])
print(c)
print(c.dtype.name)
注意,NumPy会自动将像5这样的整数转换为浮点数,因为这样做不会损失精度。NumPy会尝试提供最佳的数据类型格式,以保持数组中数据类型的同质性(即所有元素类型相同)。
有时我们知道要创建的数组的形状,但不知道其中的内容。NumPy提供了几个函数来创建带有初始占位符的数组,例如zeros或ones。
以下是创建具有相同形状但填充值不同的两个数组的方法:
d = np.zeros((2, 3))
print(d)
e = np.ones((2, 3))
print(e)
我们还可以生成一个随机数数组。
f = np.random.rand(2, 3)
print(f)
zeros、ones和random.rand经常用于创建示例数组,尤其是在Stack Overflow帖子和其他讨论论坛中。
我们还可以使用arange函数在数组中创建数字序列。第一个参数是起始边界,第二个参数是结束边界(不包含),第三个参数是连续数字之间的差值。
g = np.arange(10, 50, 2) # 创建一个从10(包含)到50(不包含)的偶数数组
print(g)
如果我们想生成一个浮点数序列,可以使用linspace函数。在这个函数中,第三个参数不是两个数字之间的差值,而是你想要生成的项目总数。
h = np.linspace(0, 2, 15) # 生成从0到2(包含)的15个数字
print(h)
数组操作
上一节我们学习了如何创建数组,本节中我们来看看如何对数组进行数学和逻辑操作。
我们可以对数组进行许多操作,例如数学运算(加法、减法、平方、指数)以及布尔数组操作(二进制值)。我们还可以进行矩阵操作,如乘积、转置、求逆等。
算术运算符在数组上按元素应用。
a = np.array([10, 20, 30, 40])
b = np.array([1, 2, 3, 4])
c = a - b # 减法
d = a * b # 乘法(逐元素)
print(c)
print(d)
通过算术操作,我们可以将现有数据转换为我们想要的形式。例如,将华氏温度转换为摄氏温度。
fahrenheit = np.array([0, 10, -5, -15, 0])
celsius = (fahrenheit - 32) * 5 / 9 # 转换公式
print(celsius)
另一个有用且重要的操作是布尔数组。我们可以在数组上应用一个运算符,将为原始数组中的每个元素返回一个布尔数组,如果满足条件,则对应位置为True。
例如,如果我们想获取一个布尔数组来检查摄氏温度是否大于-20度:
print(celsius > -20)
我们还可以使用取模运算符来检查数组中的数字是否为偶数。
print(celsius % 2 == 0)
除了逐元素操作,了解NumPy支持矩阵操作也很重要。让我们看看矩阵乘积。
如果我们想做逐元素乘积,我们使用星号*。
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(a * b) # 逐元素乘积
如果我们想做矩阵乘积(点积),我们使用@符号。
print(a @ b) # 矩阵乘积
星号*用于逐元素操作,而@符号用于点积。在本课程中,你不需要担心复杂的矩阵运算,但重要的是要知道NumPy是Python科学计算库的基础,它能够进行逐元素操作(星号)和矩阵级操作(@符号)。
数组类型与聚合
上一节我们介绍了数组的基本操作,本节中我们来看看数组的数据类型转换和一些聚合函数。
当操作不同类型的数组时,结果数组的类型将对应于两种类型中更通用的一种,这称为向上转型。
array1 = np.array([[1, 2, 3], [4, 5, 6]]) # 整数数组
print(array1.dtype)
array2 = np.array([[7.1, 8.2, 9.1], [10.4, 11.2, 12.3]]) # 浮点数数组
print(array2.dtype)
array3 = array1 + array2 # 加法,整数向上转型为浮点数
print(array3)
print(array3.dtype)
整数是仅包含整数的数字,而浮点数可以包含整数部分和小数部分。示例中的64指的是操作系统为表示该数字而保留的位数,它决定了可以表示的数字的大小或精度。
NumPy数组还有一些有趣的聚合函数,例如sum、max、min和mean。
print(array3.sum())
print(array3.max())
print(array3.min())
print(array3.mean())
对于二维数组,我们可以对每一行或每一列进行相同的操作。
b = np.arange(1, 16).reshape(3, 5) # 创建一个包含1到15元素的3x5数组
print(b)
我们通常认为二维数组由行和列组成,但你也可以将这些数组视为只是数字的巨大有序列表,而数组的形状(行数和列数)只是我们为特定目的而建立的抽象。
实际上,这正是计算机环境中存储基本图像的方式。
实战:图像处理示例
让我们看一个例子,看看NumPy如何在像图像处理这样的场景中发挥作用。
对于这个演示,我将使用Python图像库PIL(Pillow)和一个在Jupyter笔记本中显示图像的函数。
from PIL import Image
from IPython.display import display
# 打开并显示图像
img = Image.open('Christot_TF.png')
display(img)
# 将PIL图像转换为NumPy数组
array = np.array(img)
print(array.shape)
print(array)
我们看到形状是200x200,值都是uint8。u表示无符号整数(没有负数),8表示每字节8位。这意味着每个值最大可以为2^8=256,但实际上只有255,因为我们从0开始计数。
对于黑白图像,黑色从0开始,白色从255开始。如果我们只想反转这个图像,我们可以使用NumPy数组来实现。
# 创建一个与原始数组形状相同、填充值为255的数组
mask = np.full(array.shape, 255, dtype=np.uint8)
print(mask)
# 从原始数组中减去掩码(逐元素减法)
modified_array = array - mask
# 将所有负值转换为正值(逐元素乘以-1)
modified_array = modified_array * -1
# 确保数据类型正确
modified_array = modified_array.astype(np.uint8)
print(modified_array)
# 显示新图像
display(Image.fromarray(modified_array))
记住我一开始谈到这如何可以被认为只是一个巨大的字节数组,而形状是一种抽象。我们可以决定重塑数组并尝试渲染。Pillow将单独的行解释为线条,因此我们可以根据需要更改行数和列数。
# 将数组重塑为100x400(原始为200x200,总单元格数不变)
reshaped = np.reshape(modified_array, (100, 400))
print(reshaped.shape)
display(Image.fromarray(reshaped))
通过将数组重塑为只有100行高但400列宽,我们基本上是通过每隔一行取一次并将其堆叠在宽度上来使图像加倍,这使得图像看起来更拉伸。
这不是一门图像处理课程,但重点是向你展示这些NumPy数组实际上只是数据之上的抽象,而这些数据具有底层格式(在本例中为uint8)。此外,我们可以在其上构建更多的抽象,例如将字节渲染为黑色或白色的计算机代码,这对人来说是有意义的。在某种程度上,整个学位都是关于数据以及我们可以在该数据之上构建的抽象,从单个字节表示到复杂的函数神经网络或交互式可视化。
你作为数据科学家的角色是理解数据的含义、其在集合中的上下文,并将其转换为不同的表示形式以用于理解。
索引、切片与迭代
索引、切片和迭代对于数据操作和分析极其重要,因为这些技术允许我们根据条件选择数据,并复制或更新数据。
首先,我们来看整数索引。一维数组的工作方式与列表类似。要获取一维数组中的元素,我们只需使用偏移索引。
a = np.array([3, 4, 5, 6, 7])
print(a[2]) # 获取索引为2的元素(第三个元素,值为5)
对于多维数组,我们需要使用整数数组索引。
a = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3数组
print(a)
print(a[1, 1]) # 选择第1行第1列的元素(值为5,索引从0开始)
如果我们想获取多个元素(例如1、4和6)并将它们放入一个一维数组中,我们可以直接将索引输入到np.array函数中。
# 方法一:直接索引
new_array = np.array([a[0, 0], a[1, 0], a[1, 2]])
print(new_array)
# 方法二:使用数组索引(将行索引和列索引列表“压缩”在一起)
print(a[[0, 1, 1], [0, 0, 2]]) # 结果相同:[1, 4, 6]
布尔索引允许我们根据条件选择任意元素。例如,在我们刚才讨论的矩阵中,我们想找到大于5的元素。
print(a > 5) # 返回一个布尔数组,显示对应索引处大于5的值
print(a[a > 5]) # 将布尔数组作为掩码应用于原始数组,返回一个与True值相关的一维数组
正如我们所看到的,这个功能在pandas工具包中是必不可少的,而pandas是本课程的主要内容,所以我们会经常使用它。
切片是一种基于原始数组创建子数组的方法。对于一维数组,切片的工作方式与列表类似。要进行切片,我们使用冒号:。
a = np.array([0, 1, 2, 3, 4, 5])
print(a[:3]) # 获取从索引0到索引3(不包含3)的元素
print(a[2:4]) # 获取从索引2到索引4(不包含4)的元素
对于多维数组,它的工作方式类似。
a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
print(a)
print(a[:2]) # 获取前两行(第0行和第1行)的所有元素
print(a[:2, 1:3]) # 获取前两行,但只获取第1列和第2列的值
在二维数组中,第一个参数用于选择行,第二个参数用于选择列。
重要的是要认识到,数组的切片是对相同数据的视图。这称为引用传递。因此,修改子数组也会随之修改原始数组。
subarray = a[:2, 1:3]
print(subarray[0, 0]) # 子数组中第0行第0列的值(对应原始数组a[0, 1])
subarray[0, 0] = 50 # 修改子数组
print(subarray) # 子数组已更改
print(a) # 原始数组也被更改!
加载与分析数据集
现在我们已经学习了NumPy的基础知识,让我们在几个数据集上使用它。
这里我们有一个非常流行的葡萄酒质量数据集,我们只查看红葡萄酒。数据字段包括固定酸度、挥发性酸度、残留糖分、氯化物等。这里重要的是酒精含量和质量。
要将数据集加载到NumPy中,我们可以使用genfromtxt函数。我们可以指定数据文件名、分隔符(可选,但我们经常使用)以及要跳过的行数(如果我们有标题行,这里为1)。
genfromtxt函数有一个名为dtype的参数,用于指定每列的数据类型。这个参数是可选的。如果不指定类型,所有类型都将被转换为更通用或更精确的类型(即会进行一些推断)。
wines = np.genfromtxt('winequality-red.csv', delimiter=';', skip_header=1)
print(wines)
我们可以使用整数索引来获取特定的列或行。例如,如果我们想选择固定酸度列(第一列),我们可以通过将索引输入数组来实现。
同样记住,对于多维数组,第一个参数指的是行,第二个参数指的是列。如果我们只给出一个参数,那么我们将得到一个一维列表。
# 获取所有行的第一列(结果为一维数组)
print(wines[:, 0])
# 获取所有行的第一列,但保持二维形状(单列)
print(wines[:, 0:1])
第一个语句给我们一个单一的数字列表。第二个语句保留了这是一个单列的一般形状。这是数据形状实际上只是一种抽象的另一个很好的例子,我们可以有意地将其叠加在我们正在处理的数据之上。
如果我们想要一个按顺序排列的列范围,比如第0到第3列(回想一下,这意味着第一、第二和第三列,因为我们从0开始,并且不包含结束索引值),我们也可以这样做。
print(wines[:, 0:3]) # 第0、1、2列
如果我们想要几个不连续的列,我们可以将我们想要的列的索引放入一个数组中,并将该数组作为第二个参数传递。
print(wines[:, [0, 2, 5]]) # 第0、2、5列
我们还可以对这些数据进行一些基本的汇总。例如,如果我们想找出红葡萄酒的平均质量,我们可以选择质量列。我们可以用几种方法做到这一点,但最合适的是对索引使用-1值,因为负数意味着从列表的后面开始切片。
quality = wines[:, -1] # 最后一列(质量)
print(quality.mean())
让我们看看另一个关于研究生入学情况的数据集。它包含诸如GRE分数、TOEFL分数、大学评级等字段,最后是录取机会。利用这个数据集,我们可以进行数据操作和基本分析,以推断哪些条件与更高的录取机会相关。
我们可以使用genfromtxt在加载CSV数据时指定数据字段名称,也可以通过将dtype参数设置为None来让NumPy推断列的类型。
grad_admit = np.genfromtxt('Admission_Predict.csv', dtype=None, delimiter=',', skip_header=1,
names=('Serial No.', 'GRE Score', 'TOEFL Score', 'University Rating',
'SOP', 'LOR', 'CGPA', 'Research', 'Chance of Admit'))
print(grad_admit)
print(grad_admit.shape) # 结果是一个包含400个元组的一维数组
我们可以使用列名从数组中检索列。
# 获取CGPA列的前五个值
print(grad_admit['CGPA'][:5])
由于数据中的GPA范围是1到10,而在美国更常用的是4分制,一个常见的任务可能是通过除以10再乘以4来转换GPA。
grad_admit['CGPA'] = grad_admit['CGPA'] / 10 * 4 # 归一化到4分制
print(grad_admit['CGPA'][:20])
请记住,我们已经实际更改了这些数据。我们赋值给grad_admit['CGPA']会改变底层数组。
记住布尔掩码。我们可以用它来找出有多少学生有研究经验,方法是创建一个布尔掩码并将其传递给数组索引运算符。
has_research = grad_admit[grad_admit['Research'] == 1]
print(len(has_research)) # 有研究经验的学生数量
由于我们有一个录取机会的数据字段,范围从0到1,我们可以尝试看看录取机会高的学生(比如80%)的平均GRE分数是否高于录取机会低的学生(比如40%)。
首先,我们将使用布尔掩码仅提取那些我们感兴趣的、基于其录取机会的学生,然后我们只提取他们的GRE分数,然后我们将打印平均值。
high_chance_gre = grad_admit[grad_admit['Chance of Admit'] > 0.8]['GRE Score']
low_chance_gre = grad_admit[grad_admit['Chance of Admit'] < 0.4]['GRE Score']
print(high_chance_gre.mean())
print(low_chance_gre.mean())
让我们也对GPA做同样的事情。
high_chance_gpa = grad_admit[grad_admit['Chance of Admit'] > 0.8]['CGPA']
low_chance_gpa = grad_admit[grad_admit['Chance of Admit'] < 0.4]['CGPA']
print(high_chance_gpa.mean())
print(low_chance_gpa.mean())

基于我们粗略的观察,录取机会较高的学生的GPA和GRE分数似乎也更高。
总结


本节课中我们一起学习了Python核心科学计算库NumPy的快速导览。你将会看到更多使用这个库的讨论,并且
12:正则表达式文本处理 📝


在本节课中,我们将要学习如何使用正则表达式进行模式匹配和字符串处理。正则表达式是一种强大的工具,可以帮助我们从文本数据中查找、提取和清理信息,是数据科学中数据清洗的核心技术之一。
概述
正则表达式是一种浓缩的格式化语言。你可以将其视为一个模式,将其提供给正则表达式处理器以及一些源数据。处理器随后使用该模式解析源数据,并将文本块返回给数据科学家或程序员以供进一步操作。
使用正则表达式主要有三个原因:检查源数据中是否存在某个模式;从源数据中获取复杂模式的所有实例;或者通常通过字符串分割,使用模式来清理源数据。
正则表达式并非微不足道,但它们是数据科学应用中数据清洗的基础技术。扎实理解正则表达式将帮助你快速有效地处理文本数据,以用于后续的数据科学应用。
导入模块与基础函数
首先,我们需要导入Python中存储正则表达式库的re模块。
import re
re模块中有几个主要的处理函数。match函数检查字符串开头是否匹配,并返回布尔值。类似地,search函数检查字符串中任意位置是否匹配,并返回布尔值。
让我们创建一个示例文本。
text = "This is a good day."
现在,让我们看看这是否是一个好日子。
if re.search("good", text):
print("wonderful")
else:
print("alas :(")
字符串分割与查找
除了检查条件,我们还可以分割字符串。正则表达式在这里所做的工作称为分词,即根据模式将字符串分割成子字符串。
分词是自然语言处理中的核心活动。findall和split函数将为我们解析字符串并返回文本块。让我们尝试一个例子。
text = "Amy works diligently. Amy gets good grades. Our student Amy is successful."
这是一个有点人为的例子,但让我们根据所有“Amy”的实例来分割。
re.split("Amy", text)
你会注意到,split返回了一个空字符串,后面跟着一系列关于Amy的陈述,这些都是列表的元素。如果我们想计算提到Amy的次数,可以使用findall。
re.findall("Amy", text)
复杂模式:锚点
我们已经看到search查找某个模式并返回布尔值,split使用模式创建子字符串列表,findall查找模式并提取所有出现项。
现在我们知道Python正则表达式API的工作原理了,让我们进一步讨论复杂模式。正则表达式规范标准定义了一种标记语言来描述文本中的模式。
让我们从锚点开始。锚点指定要匹配的字符串的开头和/或结尾。
^字符表示开始,$字符表示结尾。如果在字符串前加上^,意味着正则表达式处理器检索的文本必须以你指定的字符串开头。对于结尾,必须在字符串后加上$字符,意味着处理器检索的文本必须以你指定的字符串结尾。
这是一个例子。
text = "Amy works diligently. Amy gets good grades. Our student Amy is successful."
让我们看看这是否以“Amy”开头。
re.search("^Amy", text)
注意,search实际上返回给我们一个新对象,称为匹配对象。匹配对象总是具有布尔值True(因为找到了某些内容),因此你总是可以像之前那样在if语句中评估它。匹配对象的呈现还告诉你匹配了什么模式(本例中是单词“Amy”)以及匹配的位置(即跨度)。
字符集
让我们进一步讨论模式,从字符集开始。
让我们创建一个字符串,表示一个学生在所有作业中一门课程整个学期的单个字母成绩。
grades = "ACAAAABCBCBAA"
如果我们想回答“成绩列表中有多少个B”这个问题,我们可以直接使用B作为模式。
re.findall("B", grades)
如果我们想计算列表中A或B的数量,不能使用AB,因为这用于匹配所有紧跟着B的A。相反,我们将字符A和B放在方括号内。
re.findall("[AB]", grades)
这被称为集合运算符。你还可以包含按字母数字顺序排列的字符范围。例如,如果我们要引用所有小写字符,可以在方括号内使用a-z。
让我们构建一个简单的正则表达式,解析出学生获得A后接B或C的所有实例。
re.findall("A[BC]", grades)
注意,[AB]模式描述了一组可能的字符,可以是A或B,而A[BC]模式表示两组字符,它们必须背靠背匹配。
你可以使用管道运算符|(表示“或”)来编写此模式。
re.findall("AB|AC", grades)
我们可以使用^与集合运算符来否定结果。例如,如果我们想只解析出不是A的成绩,可以这样做。
re.findall("[^A]", grades)
这里我们只有B和C。请注意,^之前作为锚点匹配字符串的开头,但在集合运算符内部,^(以及我们将要讨论的其他特殊字符)会失去其特殊含义。你认为这个操作的结果会是什么?
re.findall("^[^A]", grades)
这是一个空列表,因为正则表达式表示我们想要匹配字符串开头不是A的任何值,而我们的字符串以A开头,因此没有找到匹配。
记住,当你使用集合运算符时,你是在进行基于字符的匹配,即以“或”的方式匹配单个字符。
量词
我们已经讨论了锚点以及匹配模式的开头和结尾,还讨论了使用集合表示法的字符匹配、字符否定以及管道字符如何允许我们在操作中使用“或”。现在让我们继续讨论量词。
量词是你希望模式被匹配的次数,以便实际计为一次匹配。最基本的量词是表达式E{m,n},其中E是表达式或字符匹配,m是你希望匹配的最小次数,n是该项目可以匹配的最大次数。
让我们以这些成绩为例。这个学生有多少次连续获得A的记录?我们可以这样做。
re.findall("A{2,10}", grades)
我们看到有两次记录,一次学生有四个A,另一次他们只有两个A。
我们可能会尝试使用单个值并重复模式来做到这一点。
re.findall("A{1}A{1}", grades)
如你所见,这与第一个例子不同。第一个模式寻找两个A到十个A连续组合,因此它将四个A视为一个连续记录。第二个模式寻找两个A背靠背,因此它看到两个A紧跟着另外两个A。我们说正则表达式处理器从字符串开头开始,并在处理时消耗匹配模式的变量。
重要的是要注意,正则表达式量词语法不允许你偏离{m,n}模式。特别是,如果你在大括号之间有额外的空格,你会得到一个空结果。
re.findall("A{2, 2}", grades)
正如我们已经看到的,如果我们不包含量词,默认是{1,1}。如果你在大括号中只有一个数字,它被视为既是m值也是n值。
re.findall("A{2}", grades)
使用这个,我们可以找到学生成绩下降的趋势。
re.findall("A{1,10}B{1,10}C{1,10}", grades)
这有点取巧,因为我们包含了一个任意大的最大值。还有三个其他量词可以用来简写,我们可以考虑在这里使用:*用于匹配零次或多次,?用于匹配零次或一次,+用于匹配一次或多次。
实际应用:解析维基百科数据
让我们看一个更复杂的例子,加载一些从维基百科抓取的数据。
with open("datasets/wiki_frapa.txt") as file:
wiki = file.read()
扫描此文档,我们注意到所有标题后面都有“edit”一词放在方括号中,后面跟着一个换行符。因此,如果我们想获取本文中所有标题的列表,可以使用re.findall。
re.findall("[a-zA-Z]{1,100}\[edit\]", wiki)
这并没有完全奏效,它获取了所有标题,但只获取了标题的最后一个词,而且确实相当笨拙。让我们迭代改进这一点。
首先,我们可以使用\w来匹配任何字母(包括数字)。
re.findall("\w{1,100}\[edit\]", wiki)
\w是一个元字符,它表示任何字母或数字的特殊模式。实际上,文档中列出了许多不同的元字符,例如\s匹配任何空白字符。
接下来,我们可以使用另外三个量词来缩短花括号语法。我们可以使用*来匹配零次或多次。
re.findall("\w*\[edit\]", wiki)
现在我们已经缩短了正则表达式,让我们稍微改进一下。我们可以使用空格字符添加空格。
re.findall("[\w ]*\[edit\]", wiki)
现在我们得到了维基百科页面中的章节标题列表。你现在可以通过遍历这个列表并应用另一个正则表达式来创建标题列表。
for title in re.findall("[\w ]*\[edit\]", wiki):
print(re.split("\[", title)[0])
这可行,但有点麻烦。到目前为止,我们一直在讨论正则表达式作为单个匹配的模式,但实际上你可以同时匹配不同的模式(称为分组),然后稍后根据需要引用这些分组。
要分组模式,请使用括号,这实际上非常自然。让我们使用分组重写我们的findall。
re.findall("([\w ]*)(\[edit\])", wiki)
很好,我们看到Python的re模块按组分解结果,并且我们实际上也可以通过返回的匹配对象中的编号来引用分组。
但是,我们如何获取匹配对象列表呢?到目前为止,我们已经看到findall返回字符串,search和match返回单个匹配对象。如果我们想要匹配对象列表该怎么办?在这种情况下,我们使用函数finditer。
for item in re.finditer("([\w ]*)(\[edit\])", wiki):
print(item.groups())
groups方法返回一个组的元组。我们可以使用group(数字)获取单个组,其中group(0)是整个匹配,其他数字是我们感兴趣的部分匹配。在本例中,我们想要group(1),即第一个项目。
for item in re.finditer("([\w ]*)(\[edit\])", wiki):
print(item.group(1))
命名分组
关于正则表达式分组,还有一个我很少使用但很好的概念,即标记或命名分组。在前面的例子中,我向你展示了如何使用组的位置,但给它们一个标签并将结果视为字典实际上非常有用。为此,我们使用这种语法:括号后跟一个问号、一个大写P,然后是尖括号内的名称。其中括号开始分组,问号加大写P表示这是基本正则表达式的扩展,尖括号中的名称是我们使用的字典键,并包裹在那些尖括号中。
for item in re.finditer("(?P<title>[\w ]*)(?P<edit_link>\[edit\])", wiki):
print(item.groupdict()['title'])
当然,我们可以打印出整个字典。
for item in re.finditer("(?P<title>[\w ]*)(?P<edit_link>\[edit\])", wiki):
print(item.groupdict())
更多元字符与前后查找
我们已经看到了如何使用集合运算符匹配单个字符模式,如何使用括号将匹配分组,以及如何使用量词(如*、?或{m,n})来描述模式。
我在前面的例子中略过了\w,它代表任何单词字符。正则表达式有许多用于不同类型字符的简写,包括.表示任何非换行符的单个字符,\d表示任何数字,\s表示任何空白字符(如空格和制表符)。完整列表可以在Python正则表达式文档中找到。
另一个需要熟悉的概念是前向查找和后向查找。在这种情况下,匹配到正则表达式引擎的模式是我们实际试图隔离的文本之前或之后的文本。
例如,在我们的标题中,我们想要隔离“edit”渲染之前的文本。但实际上我们并不关心“edit”文本本身。到目前为止,我们一直在丢弃“edit”,但如果我们想使用它们来匹配但不想捕获它们,可以将它们放在一个组中,并使用?=语法进行前向查找。
让我们看一个例子。
for item in re.finditer("(?P<title>[\w ]*)(?=\[edit\])", wiki):
print(item)
这个正则表达式表示匹配两个组,第一个将被命名并称为“title”,包含任意数量的空白或常规单词字符;第二个将是字符“edit”,但我们实际上不希望将此项目放入输出匹配对象中。
详细模式与解析结构化数据
让我们看一些更多的维基百科数据。这是一些关于美国佛教大学的数据。
with open("datasets/buddhist.txt") as file:
wiki = file.read()
我们可以看到每所大学都遵循相当相似的模式:名称后跟一个破折号,然后是“located in”一词,接着是城市和州。
我将使用这个例子向你展示Python正则表达式的详细模式。详细模式允许你编写多行正则表达式并提高可读性。对于此模式,我们必须明确指示所有空白字符,要么通过用反斜杠转义它们,要么使用\s特殊值。然而,这意味着我们可以更像代码一样编写正则表达式,甚至可以使用#号包含注释。
pattern = """
(?P<title>.*) # 大学名称
\ –\ located\ in\ # 分隔符
(?P<city>\w*) # 城市
,\ # 逗号和空格
(?P<state>\w*) # 州
"""
for item in re.finditer(pattern, wiki, re.VERBOSE):
print(item.groupdict())
解析社交媒体数据
这是《纽约时报》关于健康推文的另一个例子。这些数据来自UC Irvine机器学习存储库,这是一个很好的不同种类数据来源。
with open("datasets/nytimeshealth.txt") as file:
health = file.read()
这里我们可以看到有推文,字段由竖线分隔。让我们尝试获取此数据中包含的所有主题标签列表。在Twitter中,主题标签以#号(即哈希标记)开头,并在遇到一些空白时结束。
让我们创建一个模式。我们首先想包含#号,然后是任意数量的字母数字字符,当我们看到一些空白时结束。
pattern = "#[\w\d]*\s"
注意,结尾是一个前向查找,因为我们实际上对匹配此空白并返回值不感兴趣。还要注意,我使用了*而不是+来匹配字母字符或数字,因为+要求至少有一个。
让我们搜索并显示所有主题标签。
re.findall(pattern, health)
我们可以看到,在这个特定的数据集中有很多与埃博拉相关的推文。
总结
本节课我们概述了正则表达式,实际上我们只是触及了你能做的事情的表面。我发现正则表达式确实令人沮丧,它们非常强大,但如果你一段时间不使用,就会忘记一些细节,尤其是命名分组和前向查找搜索。
但是网上有很多很好的例子和参考指南,包括Python的正则表达式文档。有了这些,你应该能够编写简洁、可读且性能良好的代码。


掌握基本的正则表达式读写能力是应用数据科学家的核心技能。
13:Pandas库入门 🐼


在本节课中,我们将深入学习如何使用Python的Pandas工具包来操作、清理和查询数据。Pandas是数据科学领域的核心库之一,掌握它将为你后续的数据处理工作打下坚实基础。
概述 📋
本周,我们将通过研究Pandas数据工具包,深入探讨Python如何用于操作、清理和查询数据。
Pandas由Wes McKinney于2008年创建,它是一个采用宽松许可证的开源项目。作为一个开源项目,它拥有一个强大的社区,超过100名软件开发人员为其贡献代码以使其变得更好。
学习资源与社区支持 📚
现在,你应该使用课程内的工具与讲师、课程助理和同学互动,以获得关于Pandas的帮助。但我也鼓励你超越课程范围,坦率地说,你可能需要这样做,并利用诸如Stack Overflow这样的问答网站。
Stack Overflow在软件开发社区中被广泛用于发布关于编程、编程语言和编程工具包的问题。Stack Overflow的特殊之处在于它由社区精心管理,特别是Pandas社区,将其作为帮助新成员的首要资源。
如果你在Stack Overflow上发布一个问题,并将其标记为与Pandas和Python相关,那么很有可能会有Pandas开发人员回答你的问题。除了发布问题,Stack Overflow也是一个查看人们遇到什么问题以及如何解决这些问题的好地方。
第二个你可能想考虑的资源是书籍。2012年,Wes McKinney撰写了权威的Pandas参考书,名为《Python for Data Analysis》,由O'Reilly出版,并在2017年更新至第二版。
我认为这是本课程的权威教科书,也是理解Pandas工作原理的重要资源。但我也很欣赏Matt Harrison所著的更简洁的《Learning the Pandas Library》。这不是一本关于数据分析和统计的全面书籍,但如果你只想快速学习Pandas的基础知识,我认为这是一本布局很好的书。

跟上数据科学领域的发展 🚀
数据科学领域正在迅速发展。每天都有新的工具包和方法被创造出来。要跟上所有这些发展可能很困难。Marco Rodriguez和Tim Golden维护着一个很棒的博客聚合网站,名为Planet Python。你可以访问Planet Python的网页。通过RSS阅读器订阅或从Planet Python的Twitter feed获取最新文章。
有许多定期的Python数据科学贡献者,如果你使用RSS订阅,我强烈建议你关注它。同样,如果你喜欢播客,我喜欢Michael Kennedy和Brian Oen的《Python Bytes》。他们的剧集很短,更侧重于Python新闻,这是了解Python生态系统中一些动态的好方法。
好了,这是我关于如何深化学习的最后一个推荐。Kyle Polock运营着一个优秀的播客,名为《Data Skeptic》。虽然它本身是基于Python的,但它制作精良,并很好地融合了对该领域专家的采访以及简短的教育课程。他描述的许多工作都特定于机器学习方法,并将对你整个学位学习大有裨益。
开始学习Pandas 🎯
掌握了所有这些资源后,让我们开始深入学习Python数据操作工具包——Pandas。
总结 ✨

本节课中,我们一起学习了Pandas库的起源、其强大的开源社区支持,以及如何利用各种在线资源(如Stack Overflow、权威书籍、博客聚合网站和播客)来深化对Pandas和数据科学的学习。准备好这些工具和知识后,我们就可以正式开启Pandas数据操作之旅了。在接下来的课程中,我们将具体探索Pandas的核心数据结构和功能。
14:探索Pandas Series数据结构 📊


在本节课中,我们将学习Pandas库中的核心数据结构之一:Series。你将了解如何创建Series对象,如何存储和操作一维带索引的数据,以及如何处理缺失值。
概述
Series是Pandas中的核心数据结构,可以将其视为列表和字典的交叉体。数据按顺序存储,并且每个数据项都有一个标签(索引)用于检索。理解Series是学习Pandas进行数据操作的基础。
创建Series对象
要开始使用Pandas,我们首先需要导入它。
import pandas as pd
从列表创建Series
创建Series最简单的方法之一是传入一个列表。Pandas会自动分配一个从0开始的整数索引,并将Series的名称设置为None。
以下是创建Series的步骤:
- 创建一个列表。
- 将列表传递给
pd.Series()函数。
让我们看一个例子。假设我们有一个包含学生姓名的列表:
students = ['Alice', 'Jack', 'Molly']
s = pd.Series(students)
print(s)
输出结果将显示一个Series对象。Pandas会自动识别Series中数据的类型(本例中为object,即字符串),并相应地设置dtype参数。索引是从0开始的整数。
我们也可以传入数字列表:
numbers = [1, 2, 3]
s_num = pd.Series(numbers)
print(s_num)
在这种情况下,Pandas会将dtype设置为int64。Pandas底层使用NumPy库的类型化数组来存储Series值,这比传统的Python列表处理数据速度更快。
处理缺失值
在数据处理中,缺失值是一个常见问题。Pandas和底层的NumPy对此有特殊的处理方式。
None与NaN的区别
在Python中,我们使用None类型来表示缺失数据。但在Pandas的类型化数组中,处理方式有所不同:
-
如果创建一个包含字符串和
None的列表,Pandas会将None保留为None,并将底层数组类型设为object。students_with_none = ['Alice', 'Jack', None] s_students = pd.Series(students_with_none) print(s_students) -
如果创建一个包含数字(整数或浮点数)和
None的列表,Pandas会自动将None转换为一个特殊的浮点值NaN(Not a Number)。numbers_with_none = [1, 2, None] s_numbers = pd.Series(numbers_with_none) print(s_numbers)你会注意到两件事:首先,值显示为
NaN;其次,Pandas将这个Series的dtype设置为浮点数(例如float64),而不是整数。这是因为NaN在底层被表示为浮点数,而整数可以被安全地转换为浮点数。
重要提示:NaN和None在Pandas中不相等,并且测试方式不同。
import numpy as np
# NaN不等于None
print(np.nan == None) # 输出:False
# NaN甚至不等于它自己
print(np.nan == np.nan) # 输出:False
# 必须使用特殊函数来检查NaN
print(np.isnan(np.nan)) # 输出:True
因此,当你看到Series中的整数变成了浮点数时,很可能是因为数据中存在缺失值,被转换成了NaN。
从字典创建Series并自定义索引
上一节我们介绍了从简单列表创建Series。本节中我们来看看如何从字典创建Series,这允许我们使用有意义的标签作为索引。
使用字典键作为索引
如果你有带标签的数据(例如字典),可以直接用它创建Series。字典的键会自动成为Series的索引。
student_scores = {
'Alice': 'Physics',
'Jack': 'Chemistry',
'Molly': 'English'
}
s = pd.Series(student_scores)
print(s)
创建后,可以通过.index属性访问索引对象:
print(s.index)
存储复杂数据类型
Series的dtype为object时,不仅可以存储字符串,还可以存储任意Python对象,例如元组列表:
students_complex = [('Alice', 'Brown'), ('Jack', 'White'), ('Molly', 'Green')]
s_complex = pd.Series(students_complex)
print(s_complex)
显式指定索引
你可以将数据创建和索引创建分开,通过index参数显式传递一个索引列表。
# 数据是课程
data = ['Physics', 'Chemistry', 'English']
# 索引是学生姓名
custom_index = ['Alice', 'Jack', 'Molly']
s_custom = pd.Series(data, index=custom_index)
print(s_custom)
索引与数据的对齐
当使用字典创建Series并同时提供自定义索引时,Pandas会以你提供的索引为准。

规则如下:
- 忽略字典中所有不在你提供的索引列表中的键。
- 对于你提供的索引列表中存在,但字典中不存在的键,Pandas会添加
NaN(对于数值/混合类型)或None(对于纯对象类型)作为缺失值。
让我们看一个例子:
student_scores = {
'Alice': 'Physics',
'Jack': 'Chemistry',
'Molly': 'English'
}
# 我们只对部分学生感兴趣,并且加入一个字典里没有的新学生‘Sam’
selected_index = ['Alice', 'Molly', 'Sam']
s_aligned = pd.Series(student_scores, index=selected_index)
print(s_aligned)
在这个结果中:
Jack虽然存在于原始字典中,但不在我们指定的索引里,所以被忽略。Sam在我们指定的索引中,但原始字典中没有,因此其值显示为NaN(因为值是字符串,这里实际显示为NaN,但在打印时可能显示为None,取决于Pandas版本和上下文)。
总结
本节课中我们一起学习了Pandas Series数据结构。我们涵盖了以下核心概念:
- 创建Series:可以从列表或字典使用
pd.Series()创建。 - 索引:Series具有索引,可以是自动生成的整数,也可以是自定义的标签(如字符串)。索引可以通过
.index访问。 - 数据类型:Pandas使用NumPy类型化数组(如
int64,float64,object)来高效存储数据。dtype属性显示了数据的类型。 - 处理缺失值:
- 在数值Series中,缺失值用
NaN(Not a Number)表示,其dtype会变为浮点数。 - 在对象Series中,缺失值用
None表示。 NaN与None不等价,且NaN != NaN。检查NaN需使用np.isnan()。
- 在数值Series中,缺失值用
- 索引与数据对齐:当使用字典并指定
index参数时,Pandas会以指定的索引为准,进行数据对齐,缺失处填充NaN/None。


理解Series是掌握Pandas DataFrame(二维表格结构)的重要基础,因为DataFrame的每一列本质上就是一个Series。
15:Series查询方法 🐼




在本节课中,我们将学习pandas库中的核心数据类型之一:Series。你将了解Series的结构,如何查询Series对象,如何合并Series对象,以及在数据科学编程中考虑并行化的重要性。
概述 📋
Series是pandas库中用于存储一维数据的主要数据结构。理解如何高效地操作Series是进行数据分析和处理的基础。本节将重点介绍Series的查询方法、数据操作技巧以及性能优化的概念。
查询Series数据 🔍
Series可以通过索引位置或索引标签进行查询。如果在创建Series时没有指定索引,那么位置和标签在数值上是相同的。
- 按位置查询:使用
iloc属性,索引从0开始。 - 按标签查询:使用
loc属性。
以下是具体操作方法。
首先,我们导入pandas并创建一个示例Series。
import pandas as pd
# 创建一个字典
student_classes = {
‘Alice‘: ‘Physics‘,
‘Jack‘: ‘Chemistry‘,
‘Molly‘: ‘English‘,
‘Sam‘: ‘History‘
}
# 从字典创建Series
s = pd.Series(student_classes)
print(s)
现在,我们有了一个名为 s 的Series。以下是查询方法。
- 要查看第四个条目(索引位置3),使用
s.iloc[3]。 - 要查看Molly的课程,使用
s.loc[‘Molly‘]。
需要记住,iloc 和 loc 是属性,不是方法。因此,查询时使用方括号 [],而不是圆括号 ()。这被称为索引操作符。
索引操作符的智能语法 🤖
Python的索引操作符根据上下文调用项的获取或设置方法。pandas为了让代码更易读,提供了一种智能语法:直接在Series对象上使用索引操作符 []。
- 如果传入整数参数,操作符的行为就像你想通过
iloc查询。例如,s[3]等同于s.iloc[3]。 - 如果传入一个对象(如字符串),操作符的行为就像你想通过
loc查询。例如,s[‘Molly‘]等同于s.loc[‘Molly‘]。
但是,当你的索引本身就是一个整数列表时,情况会变得复杂。pandas可能无法自动判断你是想按位置查询还是按标签查询。
以下是一个示例,其中索引是整数形式的课程代码。
# 创建索引为整数的Series
class_code = {
99: ‘Physics‘,
100: ‘Chemistry‘,
101: ‘English‘,
102: ‘History‘
}
s = pd.Series(class_code)
print(s)
如果我们尝试 s[0],会得到一个KeyError,因为索引中没有标签为0的项。此时,我们必须显式地使用 iloc 来获取第一个项:s.iloc[0]。
因此,为了安全起见,在索引可能混淆时,最好直接使用 iloc 和 loc 属性。
操作Series数据:迭代与向量化 ⚡
一个常见的任务是对Series中的所有值进行某种操作,例如查找特定数字、汇总数据或以某种方式转换数据。
一种典型的编程方法是遍历Series中的所有项,并对每个项执行操作。例如,计算学生成绩的平均分。
# 创建一个成绩Series
grades = pd.Series([90, 80, 70, 60])
# 迭代方法计算平均分
total = 0
for grade in grades:
total = total + grade
average = total / len(grades)
print(average)
这种方法可行,但速度较慢。现代计算机可以同时执行许多任务。pandas及其底层的NumPy库支持向量化计算。
向量化允许对整个数据数组执行操作,而不是循环遍历每个元素。以下是使用NumPy的 sum 函数进行向量化计算的方法。
import numpy as np
# 向量化方法计算平均分
total = np.sum(grades)
average = total / len(grades)
print(average)
两种方法结果相同,但速度有差异。为了展示这一点,我们创建一个大型的随机数Series并进行速度测试。
性能比较:使用 %timeit ⏱️
Jupyter Notebook有一个魔法函数 %timeit,可以测量代码的执行时间。我们将用它来比较迭代方法和向量化方法的性能。
首先,创建一个包含10,000个随机数的大型Series。
# 创建大型随机Series
numbers = pd.Series(np.random.randint(0, 1000, 10000))
print(numbers.head()) # 查看前5项
print(len(numbers)) # 验证长度
现在,使用 %%timeit(单元格魔法函数)来测量两种方法的运行时间。
%%timeit -n 100
# 迭代方法
total = 0
for number in numbers:
total += number
average = total / len(numbers)
%%timeit -n 100
# 向量化方法
total = np.sum(numbers)
average = total / len(numbers)
你会观察到向量化方法的速度显著快于迭代方法。这展示了为什么在数据科学中需要关注并行计算特性并开始以函数式编程的思维思考。
简单来说,向量化是计算机同时执行多条指令的能力。借助高性能芯片(尤其是显卡),可以获得显著的加速效果。
广播:对整个Series应用操作 📡
一个与向量化相关的特性叫做广播。通过广播,我们可以将一个操作应用到Series中的每个值,从而改变整个Series。
例如,如果我们想将Series中的每个随机数都增加2,可以直接在Series对象上使用 += 操作符。
# 查看原始数据
print(s.head())
# 使用广播增加所有值
s += 2
# 查看操作后的数据
print(s.head())
过程式的方法则是遍历Series中的所有项并直接增加值。pandas支持像遍历字典一样遍历Series。
# 迭代方法增加所有值
for label, value in s.items():
s.loc[label] = value + 2
# 查看结果
print(s.head())
结果相同,但如果你发现自己经常在pandas中迭代,应该思考是否有更好的方法。
让我们再次进行速度比较。
%%timeit -n 10
# 迭代方法
s_new = pd.Series(np.random.randint(0, 100, 1000))
for label, value in s_new.items():
s_new.loc[label] = value + 2
%%timeit -n 10
# 广播方法
s_new = pd.Series(np.random.randint(0, 100, 1000))
s_new += 2
广播方法不仅速度更快,而且代码更简洁、更易读。你期望的典型数学运算都是向量化的,NumPy文档概述了如何创建自己的向量化函数。
使用 loc 添加新数据 ➕
关于使用索引操作符访问Series数据,最后一点需要注意的是:loc 属性不仅允许你原地修改数据,还可以添加新数据——如果你传入的索引值不存在。
需要记住,索引可以具有混合类型。虽然需要注意底层的类型,但pandas会在适当的时候自动更改底层的NumPy类型。
以下是一个示例。
# 创建一个数字Series
s = pd.Series([1, 2, 3])
print(s)
# 添加一个字符串索引和整数值的新条目
s.loc[‘History‘] = 102
print(s)
我们看到,对于数据值或索引标签的混合类型,pandas没有问题。由于 ‘History‘ 不在原始索引列表中,s.loc[‘History‘] = 102 实际上创建了一个新的系列元素,索引名为 ‘History‘,值为102。
处理非唯一索引与合并Series 🔗
到目前为止,我只展示了索引值唯一的Series示例。我想通过一个索引值不唯一的例子来结束本讲,这使得pandas Series在概念上与关系型数据库等有所不同。
让我们创建一个记录学生所选课程的Series。
# 创建学生课程Series
student_classes = pd.Series([‘Physics‘, ‘Chemistry‘, ‘English‘, ‘History‘],
index=[‘Alice‘, ‘Jack‘, ‘Molly‘, ‘Sam‘])
print(student_classes)
现在,为一名新学生Kelly创建一个Series,列出她所修的所有课程。我们将索引设置为Kelly,数据为课程名称。
# 创建Kelly的课程Series (索引不唯一)
kelly_classes = pd.Series([‘Philosophy‘, ‘Arts‘, ‘Math‘],
index=[‘Kelly‘, ‘Kelly‘, ‘Kelly‘])
print(kelly_classes)
最后,我们可以使用 .append() 函数将这个新Series中的所有数据追加到第一个Series中。
# 合并两个Series
all_student_classes = student_classes.append(kelly_classes)
print(all_student_classes)
使用 .append() 时有几个重要的注意事项。
- pandas会尝试推断要使用的最佳数据类型。在这个例子中,所有内容都是字符串,所以没有问题。
append方法实际上不会更改底层的Series对象。相反,它返回一个由两个Series连接而成的新Series。这是pandas中的一个常见模式——默认返回一个新对象而不是原地修改一个对象,你应该习惯这一点。- 当我们查询追加后的Series中的 ‘Kelly‘ 时,我们得到的不是一个单一值,而是一个Series本身。
# 查询‘Kelly‘,返回一个Series
print(all_student_classes.loc[‘Kelly‘])


总结 🎯
在本节课中,我们重点介绍了pandas库的主要数据类型之一:Series。
我们一起学习了:
- 如何使用
loc和iloc查询Series。 - Series是一种索引数据结构。
- 如何使用
append合并两个Series对象。 - 向量化的重要性及其带来的性能提升。
Series对象还有许多我们未讨论到的方法,但掌握了这些基础知识后,我们将继续讨论pandas的二维数据结构:DataFrame。DataFrame与Series对象非常相似,但包含多列数据,是在清洗和聚合数据时你将花费大部分时间处理的结构。


16:Pandas核心数据结构DataFrame详解 📊


在本节课中,我们将要学习Pandas库中最核心的数据结构——DataFrame。DataFrame是进行数据分析和清洗任务时主要操作的对象。我们将了解其概念、创建方法以及如何从中选择和操作数据。
概述
DataFrame数据结构是Pandas库的核心。它是进行数据分析和清洗任务时主要操作的对象。从概念上讲,DataFrame是一个二维的Series对象,它包含一个索引和多列内容,每一列都有一个标签。实际上,行和列的区别仅仅是概念上的,你可以将DataFrame本身简单地视为一个带有两个轴标签的数组。
创建DataFrame
让我们从导入Pandas库开始。
import pandas as pd
我们将通过一个例子来切入:创建三条包含学生姓名、课程名称和成绩的学校记录。每条记录都是一个Series。
record1 = pd.Series({'name': 'Alice', 'class': 'physics', 'score': 85})
record2 = pd.Series({'name': 'Jack', 'class': 'chemistry', 'score': 82})
record3 = pd.Series({'name': 'Helen', 'class': 'biology', 'score': 90})
与Series类似,DataFrame对象也是带索引的。我将使用一组Series,其中每个Series代表一行数据。就像Series函数一样,我们可以传入数组中的各个项,并将索引值作为第二个参数传入。
df = pd.DataFrame([record1, record2, record3], index=['school1', 'school2', 'school1'])
我们可以使用head函数查看DataFrame的前几行,包括两个轴的索引,并以此来验证列和行。
df.head()
你会注意到,Jupyter创建了一段漂亮的HTML来渲染DataFrame的结果。最左边是索引列(学校名称),然后是数据行,每行都有一个列标题,这些标题来自我们初始记录字典的键。
另一种方法是使用字典列表,其中每个字典代表一行数据。
students = [{'name': 'Alice', 'class': 'physics', 'score': 85},
{'name': 'Jack', 'class': 'chemistry', 'score': 82},
{'name': 'Helen', 'class': 'biology', 'score': 90}]
df = pd.DataFrame(students, index=['school1', 'school2', 'school1'])
df.head()
从DataFrame中选择数据
上一节我们介绍了如何创建DataFrame,本节中我们来看看如何从中提取数据。与Series类似,我们可以使用.iloc和.loc属性来提取数据。因为DataFrame是二维的,所以向.loc索引操作符传递单个值将返回一个Series(如果只有一行要返回的话)。
例如,如果我们想选择与school2相关的数据,我们只需用一个参数查询.loc属性。
df.loc['school2']
请注意,返回的Series名称是索引值,而列名包含在输出中。我们可以使用Python的type函数检查返回的数据类型。
type(df.loc['school2'])
重要的是要记住,沿水平或垂直轴的索引和列名可能不是唯一的。在这个例子中,我们看到school1有两条不同的记录。
如果我们在DataFrame的.loc属性中使用单个值,将返回DataFrame的多行,不是作为一个新的Series,而是作为一个新的DataFrame。
df.loc['school1']
type(df.loc['school1'])
基于多轴选择数据
Pandas DataFrame的强大功能之一是可以基于多个轴快速选择数据。例如,如果你只想列出school1的学生姓名,你可以向.loc提供两个参数,一个是行索引,另一个是列名。
df.loc['school1', 'name']
请记住,就像Series一样,Pandas开发者是使用索引操作符而不是函数参数来实现这一点的。
选择单列
如果我们想选择单个列呢?有几种机制。首先,我们可以转置矩阵,这将所有行转换为列,所有列转换为行,这是通过.T属性完成的。
df.T
df.T.loc['name']
然而,由于.iloc和.loc用于行选择,Pandas将直接在DataFrame上使用索引操作符(方括号[])保留给列选择。在Pandas DataFrame中,列总是有名称的。因此,这种选择总是基于标签的,不像在Series对象上使用方括号操作符时那样令人困惑。
对于那些熟悉关系数据库的人来说,这个操作符类似于列投影。
df['name']
在实践中,这非常有效,因为你经常尝试添加或删除新列。然而,这意味着当你尝试使用.loc加列名时,可能会得到一个KeyError。
# df.loc['name'] # 这将产生KeyError
同样要注意,单列投影的结果是一个Series对象。
type(df['name'])
链式操作与注意事项
由于使用索引操作符的结果(无论是DataFrame还是Series),你可以将操作链接在一起。例如,我们可以先使用.loc选择所有与school1相关的行,然后只为这些行投影name列。
df.loc['school1']['name']
如果你感到困惑,可以使用type来检查操作的结果类型。
type(df.loc['school1']) # 这应该是一个DataFrame
type(df.loc['school1']['name']) # 这应该是一个Series
然而,在一个索引的返回类型上进行索引的链式操作可能会带来一些代价,如果可以使用其他方法,最好避免使用。特别是,链式操作往往导致Pandas返回DataFrame的副本,而不是DataFrame的视图。对于选择数据来说,这没什么大不了的,尽管可能比必要的慢。但如果你在链式操作中修改数据,这是一个重要的区别,因为这可能是错误的来源。
使用.loc进行切片和选择多列
正如我们所见,.loc进行行选择,并且可以接受两个参数:行索引和列名列表。.loc属性也支持切片。
如果我们想选择所有行,可以使用冒号:表示从开始到结束的完整切片,这就像在Python中切片列表一样。然后,我们可以将列名作为第二个参数(字符串)添加。如果我们想包含多个列,可以在一个列表中指定,Pandas将只返回我们要求的列。
以下是一个例子,我们使用.loc操作符请求所有学校的name和score。
df.loc[:, ['name', 'score']]
再看一下:冒号意味着我们想要获取所有行,第二个参数位置的列表是我们想要返回的列的列表。
删除数据
在结束关于访问DataFrame数据的讨论之前,让我们谈谈删除数据。在Series和DataFrame中删除数据很容易,我们可以使用.drop()函数来实现。
这个函数接受一个参数,即要删除的索引或行标签。这是新用户的另一个棘手之处:默认情况下,.drop()函数实际上并不改变DataFrame。相反,.drop()函数会返回一个删除了给定行的DataFrame副本。
df.drop('school1')
df # 原始数据仍然完整
.drop()有两个有趣的可选参数。第一个叫做inplace,如果设置为True,DataFrame将被原地更新,而不是返回一个副本。第二个参数是axis,它指定应该删除哪个轴,默认值是0,表示行轴,但如果你想删除列,可以将其更改为1。
例如,让我们使用.copy()函数创建一个DataFrame的副本。
copy_df = df.copy()
copy_df.drop('name', axis=1, inplace=True)
copy_df
还有第二种删除列的方法,那就是直接通过使用del关键字和索引操作符。然而,这种删除数据的方式会立即对DataFrame生效,不返回视图。
del copy_df['class']
copy_df
添加新列
最后,向DataFrame添加新列就像使用索引操作符为其分配某个值一样简单。例如,如果我们想添加一个默认值为None的class ranking列,我们可以通过在方括号后使用赋值操作符来实现。这会将默认值立即广播到新列。
df['class ranking'] = None
df
总结


本节课中我们一起学习了Pandas中最常用的数据结构——DataFrame。DataFrame按行和列进行索引,你可以使用从Series类中熟悉的索引方法,轻松选择单独的行并投影你感兴趣的列。在后续内容中,你将获得大量使用DataFrame的经验。
17:DataFrame索引与数据加载 📊


在本节课中,我们将学习如何将外部数据(特别是CSV文件)加载到Pandas的DataFrame中,并掌握一些基本的数据操作技巧,例如设置索引和重命名列。
概述
数据科学工作流通常始于从外部文件(如CSV文件)读取数据集,然后进行数据清洗和整理,为后续分析做准备。本节将演示如何将逗号分隔值文件加载到DataFrame中,并介绍一些基本的数据操作。
CSV文件简介
CSV文件是一种轻量级且广泛使用的数据格式。任何电子表格软件(如Excel或Google Sheets)都可以将数据保存为CSV格式。这种格式虽然灵活,但缺乏统一标准,因此在处理时可能需要一些额外工作。
以下是CSV文件的一个示例内容:
!cat datasets/admission_predictions.csv
输出显示,文件的第一行是列标识符(字符串形式),随后是数据行,各列之间用逗号分隔。
将CSV文件加载到DataFrame
上一节我们介绍了CSV文件的基本结构,本节中我们来看看如何使用Pandas将其加载为DataFrame对象。
首先,需要导入Pandas库:
import pandas as pd
Pandas提供了read_csv函数,可以轻松地将CSV文件转换为DataFrame:
df = pd.read_csv('datasets/admission_predictions.csv')

我们可以使用head方法查看数据的前几行:
df.head()
默认情况下,DataFrame的索引从0开始,而数据中的“学生序列号”可能从1开始。
设置索引
从上面的输出可以看出,Pandas创建了新的整数索引。如果我们希望将数据中的“序列号”列设置为索引,可以在读取文件时指定index_col参数。
以下是设置索引的方法:
df = pd.read_csv('datasets/admission_predictions.csv', index_col=0)
df.head()
这样,指定的列(此处为第一列)就成为了DataFrame的索引。
重命名列
观察数据,我们发现列名SOP和LOR的含义可能不明确。为了提高可读性,我们可以重命名这些列。
在Pandas中,可以使用rename函数来重命名列。该函数接受一个columns参数,其值是一个字典,键为旧列名,值为新列名。
以下是重命名列的方法:
new_df = df.rename(columns={
'SOP': 'Statement of Purpose',
'LOR ': 'Letter of Recommendation'
})
new_df.head()
注意,原始列名LOR后面可能包含空格,导致重命名失败。这是一个常见问题,需要特别注意。
处理列名中的空格
如果列名中包含多余的空格(或其他空白字符),重命名时可能会遇到问题。有几种方法可以处理这种情况。
一种方法是直接在重命名字典中包含空格:
new_df = new_df.rename(columns={'LOR ': 'Letter of Recommendation'})
但这种方法不够健壮,如果空白字符是制表符或多个空格,则可能失效。
另一种更通用的方法是使用字符串的strip函数来清理所有列名。我们可以将函数引用传递给rename的mapper参数,并指定应用于列轴(axis=1)。
以下是使用strip函数清理列名的方法:
new_df = new_df.rename(mapper=str.strip, axis='columns')
new_df.head()
这样,所有列名两端的空白字符都会被移除。
需要注意的是,rename函数默认返回一个新的DataFrame,原始DataFrame不会被修改。如果需要查看原始DataFrame的列名,可以这样做:
df.columns
直接修改列名
除了使用rename函数,我们还可以通过直接给df.columns属性赋值来修改列名。这种方法会直接修改原始DataFrame,效率较高,尤其适用于需要修改大量列名的情况。
以下是将所有列名转换为小写并去除空格的示例:
# 获取当前列名列表
cols = list(df.columns)
# 使用列表推导式处理每个列名
cols = [x.lower().strip() for x in cols]
# 将处理后的列表赋值回df.columns
df.columns = cols
# 查看结果
df.head()
这种方法不受列名中细微错误的影响,因为我们可以直接操作字符串列表。
总结
本节课中我们一起学习了如何将CSV文件导入到Pandas的DataFrame中,并掌握了设置索引和重命名列等基本数据操作技能。Pandas的CSV导入功能提供了许多选项,熟练掌握这些选项对于高效进行数据操作至关重要。

一旦设置好数据格式并调整了DataFrame的结构,就为进一步的数据分析和建模打下了坚实的基础。虽然还有其他数据源(如HTML网页、数据库等)可以直接加载到DataFrame中,但CSV是目前最常见的数据格式,掌握在Pandas中处理CSV文件的方法非常重要。
18:DataFrame查询技术 🎯


在本节课中,我们将学习如何使用布尔掩码(Boolean Masking)技术来查询Pandas DataFrame。这是数据科学中筛选和提取感兴趣数据的核心技能。
概述:理解布尔掩码
上一节我们介绍了DataFrame的基本结构,本节中我们来看看如何高效地从DataFrame中查询数据。这个过程的第一步是理解布尔掩码。
布尔掩码是高效查询NumPy和Pandas数据的核心。它在概念上类似于计算机科学其他领域中使用的位掩码。通过本节课的学习,你将理解布尔掩码的工作原理,以及如何将其应用于DataFrame以获取你感兴趣的数据。
一个布尔掩码是一个数组,可以是一维的(如Series)或二维的(如DataFrame),其中数组的每个值要么是True,要么是False。这个数组本质上覆盖在我们正在查询的其他数据结构之上。任何与True值对齐的单元格将被纳入最终结果,任何与False值对齐的单元格则不会。
让我们来看一个例子。
创建布尔掩码
首先,我们导入研究生录取数据集作为示例。
以下是导入和准备数据的步骤:
import pandas as pd
df = pd.read_csv('graduate_admissions.csv', index_col=0)
df.columns = [x.lower().strip() for x in df.columns]
布尔掩码是通过将运算符直接应用于Pandas Series或DataFrame对象来创建的。
例如,在我们的研究生录取数据集中,我们可能只对查看录取概率高于0.7的学生感兴趣。为了构建这个查询的布尔掩码,我们使用索引运算符投影“录取概率”列,并应用大于运算符与比较值0.7。这本质上是广播一个比较运算符(大于),结果以布尔Series的形式返回。
admit_mask = df['chance_of_admit'] > 0.7
生成的Series被索引,其中每个单元格的值是True或False,取决于学生的录取概率是否高于0.7。
这是非常基础的操作。花点时间看看这个。广播比较运算符的结果是一个布尔掩码,根据比较结果返回True或False值。
在底层,Pandas通过向量化应用你指定的比较运算符。它高效且并行地处理你指定数组中的所有值(在本例中是DataFrame的“录取概率”列)。结果是一个Series对象(因为只操作了一列),其中填充了True或False值,这就是比较运算符返回的内容。
应用布尔掩码
一旦形成了布尔掩码,你可以用它做什么呢?你可以简单地将其覆盖在数据上,以隐藏你不想要的数据(由所有False值表示)。
我们通过在原始DataFrame上使用.where()函数来实现这一点。
filtered_df = df.where(admit_mask).head()
我们看到生成的DataFrame保留了原始的索引值,只有满足条件的数据被保留。所有不满足条件的行都有NaN数据,但这些行并没有从我们的数据集中删除。它们仍然在那里,只是不是数字。
下一步当然是,如果我们不想要NaN数据,就使用.dropna()函数。这很常见。
filtered_df = df.where(admit_mask).dropna().head()
现在返回的DataFrame已经删除了所有NaN行。注意,索引现在包括1到4和6,但不包括5。
简写索引语法
尽管.where()非常方便,但实际上并不常用。相反,Pandas开发者创建了一种简写语法,它结合了.where()和.dropna(),一次性完成两个操作。以典型的方式,他们重载了索引运算符来实现这一点。
以下是一个例子:
filtered_df = df[df['chance_of_admit'] > 0.7].head()
我个人觉得这个更难阅读,但当你阅读其他人的代码时非常常见,所以理解它并能编写它很重要。
回顾一下DataFrame上的索引运算符,它现在可以做三件事:
- 可以用字符串参数调用以投影单个列:
df['gre_score'].head() - 可以发送一个字符串列表作为列:
df[['gre_score', 'toefl_score']] - 可以发送一个布尔掩码:
df[df['gre_score'] > 320].head()
这些中的每一个都模仿了.loc或.where().dropna()的功能。
组合多个布尔掩码
在我们结束之前,让我们讨论一下组合多个布尔掩码,例如包含的多个条件。在计算机科学其他地方的位掩码中,这是通过AND(如果两个掩码都必须为True才能得到最终掩码中的True值)或OR(如果只需要一个为True)来完成的。
不幸的是,在Pandas中感觉不那么自然。例如,如果你想对两个布尔Series进行AND操作:
# 这是错误的写法,会导致错误
df['chance_of_admit'] > 0.7 and df['chance_of_admit'] < 0.9
尽管使用Pandas有一段时间了,我实际上发现我经常尝试做我刚才做的事情。问题是,你有一个Series对象,而底层的Python不知道如何使用and或or来比较两个Series。相反,Pandas的作者重写了管道|和&运算符来处理这个问题。
所以我们必须这样写:
combined_mask = (df['chance_of_admit'] > 0.7) & (df['chance_of_admit'] < 0.9)
这里我们可以看到这是我们两个布尔掩码的组合,即按位AND。
需要注意的另一件事是操作顺序。这也经常困扰我。新Pandas用户的一个常见错误是尝试使用&运算符进行布尔比较,但没有在感兴趣的各个项周围加上括号。
# 这也是错误的,会导致错误
df['chance_of_admit'] > 0.7 & df['chance_of_admit'] < 0.9
问题是Python试图对0.7和一个Pandas DataFrame进行按位AND操作,而你真正想要的是将DataFrame广播在一起进行按位AND。
使用内置函数
另一种方法是完全摆脱比较运算符,转而使用内置函数来模拟这种方法。
combined_mask = df['chance_of_admit'].gt(0.7) & df['chance_of_admit'].lt(0.9)
这些函数内置于Series和DataFrame对象中,所以你实际上也可以链式调用它们,这会产生相同的答案,并且不使用视觉运算符。
combined_mask = df['chance_of_admit'].gt(0.7).lt(0.9)
你可以决定哪种看起来最好。这只在你的运算符(如小于或大于)内置于DataFrame中时才有效,但我当然发现最后一个代码示例比带有&和括号的那个可读性更强,所以你可能想考虑使用这个。
你需要能够阅读和编写所有这些,并理解你选择的路径的含义。值得回过头来重新观看这节课,以确保你掌握了它。我会说,你在数据清理工作中50%或更多的工作都涉及查询DataFrame。
总结


本节课中我们一起学习了使用布尔掩码查询DataFrame,这在数据科学领域极其重要且经常使用。通过布尔掩码,我们可以根据定义的标准选择数据,坦白说,你会在任何地方使用这个。我们还看到了查询DataFrame的多种不同方式,以及这样做时出现的有趣影响。
19:Pandas DataFrame索引操作详解 🗂️


在本节课中,我们将要学习Pandas中DataFrame索引的核心概念与操作方法。索引是高效数据访问和操作的基础,理解其工作原理对于数据科学实践至关重要。
概述
索引本质上是行级别的标签。在Pandas中,行对应轴0。索引可以是自动生成的,例如创建没有指定索引的Series时会得到数值索引;也可以显式设置,例如使用字典对象创建Series,或从CSV文件加载数据时设置相应参数。
设置索引
设置索引的另一种方法是使用set_index函数。该函数接收一个列名列表,并将这些列提升为索引。本节我们将深入探索索引在Pandas中的工作原理。
set_index函数是一个破坏性过程,它不会保留当前索引。如果想保留当前索引,需要手动创建新列并将索引属性的值复制到该列中。
以下是设置索引的步骤:
- 导入Pandas并加载数据
- 将现有索引保存到新列
- 使用
set_index设置新索引
import pandas as pd
# 加载数据
df = pd.read_csv('data/admissions_predict.csv', index_col=0)
# 查看数据前几行
df.head()
假设我们不想用序列号索引DataFrame,而是用录取概率。同时,我们想保留序列号以备后用。为此,我们可以将序列号保存在新列中。
# 将当前索引复制到名为'serial_number'的新列
df['serial_number'] = df.index
# 将索引设置为'chance_of_admit'列
df = df.set_index('chance_of_admit')
df.head()
可以看到,当从现有列创建新索引时,索引会有一个名称,即该列的原始名称。
重置索引
我们可以通过调用reset_index函数完全移除索引。这会将索引提升为一列,并创建一个默认的数字索引。
df = df.reset_index()
df.head()
现在,“chance_of_admit”被提升回一列,我们有了一个数字索引。
多级索引
Pandas的一个优秀特性是多级索引,这类似于关系数据库系统中的复合键。要创建多级索引,我们只需调用set_index并提供一个我们想要提升为索引的列名列表。Pandas将按顺序搜索这些列,找到不同的数据并形成复合索引。处理按地区或人口统计信息排序的地理数据时,常能见到这种索引的好例子。
为了更好地说明,让我们换个数据集,查看一些人口普查数据。
# 加载人口普查数据
df = pd.read_csv('data/census.csv')
df.head()
在这个数据集中,有两个汇总级别:一个包含整个国家的汇总数据,另一个包含每个州的汇总数据。
我想查看此DataFrame中给定列的所有唯一值列表。我们看到“SUMLEV”列的可能值可以使用DataFrame的unique函数查看,这类似于SQL中的DISTINCT操作符。
# 查看'SUMLEV'列的唯一值
df['SUMLEV'].unique()
我们看到实际上只有两个不同的值:40和50。
让我们排除所有州级别的汇总行,只保留县级数据。
# 只保留县级数据(SUMLEV == 50)
df = df[df['SUMLEV'] == 50]
df.head()
此外,虽然这些数据因多种原因而有趣,但让我们将查看的数据减少到仅包含总人口估计值和总出生人数。我们可以通过创建一个要保留的列名列表,然后投影这些列,并将结果DataFrame赋值给变量来实现。
# 定义要保留的列
columns_to_keep = ['STNAME', 'CTYNAME',
'BIRTHS2010', 'BIRTHS2011', 'BIRTHS2012', 'BIRTHS2013', 'BIRTHS2014', 'BIRTHS2015',
'POPESTIMATE2010', 'POPESTIMATE2011', 'POPESTIMATE2012', 'POPESTIMATE2013', 'POPESTIMATE2014', 'POPESTIMATE2015']
# 投影这些列
df = df[columns_to_keep]
df.head()
数据量变小了,但仍然足够大。
创建州和县的多级索引
美国人口普查数据按州和县细分人口估计。我们可以加载数据并将索引设置为州和县值的组合,看看Pandas如何在DataFrame中处理它。
我们通过创建一个想要构成索引的列标识符列表,然后使用此列表调用set_index,并适当分配输出来实现。我们可以有一个双重索引:首先是州名,其次是县名。
# 设置多级索引:州名和县名
df = df.set_index(['STNAME', 'CTYNAME'])
df.head()
这种呈现方式很好地展示了县是如何在州内组织的。
查询多级索引数据
一个直接出现的问题是我们如何查询这个DataFrame。之前我们看到,DataFrame的.loc属性可以接受多个参数,并且可以同时查询行和列。使用多级索引时,必须按要查询的级别顺序提供参数。
在索引内部,每一列称为一个级别,最外层的列是级别0。
如果我想查看密歇根州沃什特瑙县(我居住的地方)的人口结果,第一个参数应该是“Michigan”,第二个是“Washtenaw County”。
# 查询特定州和县的数据
df.loc['Michigan', 'Washtenaw County']
如果想比较两个县,例如沃什特瑙县和韦恩县,我们可以将一个描述我们想要查询的索引的元组列表传递给.loc属性。
由于我们有一个包含两个值(州和县)的多级索引,我们需要为过滤列表中的每个元素提供两个值。每个元组应有两个元素:第一个元素是第一个索引,第二个元素是第二个索引。
在这种情况下,我们想要一个包含两个元组的列表。在每个元组中,第一个元素是“Michigan”,第二个元素是“Washtenaw County”或“Wayne County”。
# 查询多个县的数据
df.loc[[('Michigan', 'Washtenaw County'), ('Michigan', 'Wayne County')]]

多级索引的灵活性
以上就是分层索引的简要工作原理。它们是Pandas库的一个特殊部分,我认为可以使数据的管理和推理更容易。
当然,分层标签不仅适用于行。例如,你可以转置这个矩阵,现在就有了分层的列标签。投影具有这些标签的单个列的工作方式完全符合你的预期。
实际上,我并不经常使用分层索引,而是将所有内容都保留为列并进行操作。但这是Pandas一个独特而复杂的方面,了解它很有用,尤其是在以表格形式查看数据时。
总结

本节课中,我们一起学习了Pandas DataFrame索引的核心操作。我们从设置和重置单个索引开始,然后深入探讨了强大的多级索引功能,它允许我们使用州和县的组合来高效地组织和查询数据。我们学习了如何使用.set_index()创建索引,使用.reset_index()恢复默认索引,以及如何使用.loc访问器精确查询多级索引数据。理解这些索引技术是有效管理和分析结构化数据的关键。
20:缺失值处理 📊


概述
在本节课中,我们将要学习如何使用Pandas库处理数据中的缺失值。缺失值是数据清洗过程中的常见问题,理解其成因并掌握有效的处理方法至关重要。我们将探讨缺失值的类型、检测方法以及多种填充和替换策略。
缺失值的成因
上一节我们介绍了缺失值的概念,本节中我们来看看缺失值出现的常见原因。缺失值可能由多种原因导致。
以下是两个主要示例:
- 随机缺失:例如,在调查中受访者未回答问题。如果缺失情况与其他变量(如性别、种族)存在关联,则称为“随机缺失”。
- 完全随机缺失:如果缺失情况与其他任何变量都无关,则称为“完全随机缺失”。
此外,数据缺失还可能是因为未被收集。这在合并来自多个来源的数据框时极为常见。例如,将大学人员名单与大学办公室名单合并时,学生通常没有办公室,但仍然是大学的一员。
在Pandas中检测缺失值
了解了缺失值的成因后,我们来看看如何在Pandas中识别它们。首先导入Pandas库。
import pandas as pd
Pandas能够很好地从CSV等底层数据格式中直接检测缺失值。通常,缺失值被格式化为NaN、null、None或NA。但有时缺失值的标记并不明确,例如在某些数据中用99表示缺失。read_csv函数的na_values参数允许我们指定缺失值的格式。
让我们加载一个名为class_grades.csv的数据文件。
df = pd.read_csv('class_grades.csv')
print(df.head(10))
我们可以使用.isnull()函数为整个数据框创建一个布尔掩码。这个函数会广播到数据框的每个单元格。
mask = df.isnull()
print(mask.head())
这有助于基于特定数据列处理行。另一个有用的操作是使用dropna()函数删除所有包含缺失数据的行。
print(df.dropna().head())
注意,索引为2、3、7和11的行都消失了。
填充缺失值
检测到缺失值后,下一步通常是处理它们。Pandas提供了一个方便的fillna()函数来填充缺失值。
该函数接受多个参数。你可以传入一个标量值,将所有缺失数据更改为该值。例如,将所有缺失值填充为0。
df.fillna(0, inplace=True) # inplace=True表示原地修改,不返回副本
print(df.head())
inplace属性使Pandas直接修改原数据框,而不是返回一个副本。
此外,如果空格本身是有意义的值,可以使用na_filter=False选项来关闭空格过滤。在没有缺失值的大型文件中,这可以提高读取性能。
缺失值中的信息
有时,缺失值本身可能包含信息。让我举一个自己研究中的例子。我经常处理在线学习系统的日志。在讲座录制系统中,播放器通常有心跳功能,每隔一段时间(例如30秒)向服务器发送播放统计信息。
如果加载数据文件log.csv,我们可以看到这种情况。
df = pd.read_csv('log.csv')
print(df.head())
在这个数据中,第一列是Unix时间戳,接着是用户名、访问的网页和播放的视频。每一行都有一个播放位置。随着播放位置每次增加1,时间戳大约增加30秒。然而,用户Bob暂停了播放,因此时间增加时,播放位置没有变化。注意,数据并未按时间戳排序,这在并行度高的系统中并不少见。
paused(暂停)和volume(音量)列中有很多缺失值。如果信息没有变化,通过网络发送这些信息并不高效,因此系统通常只在变化时发送,否则在数据库中插入空值。
向前填充与向后填充
接下来是fillna()的method参数。两种常见的填充方法是ffill(向前填充)和bfill(向后填充)。
ffill:用前一行的有效值填充当前缺失值。bfill:用后一行的有效值填充当前缺失值。
需要注意的是,为了使填充达到预期效果,数据需要排序。来自传统数据库管理系统的数据通常没有顺序保证。在Pandas中,我们可以按索引或值排序。
# 将时间戳设为索引并排序
df = df.set_index('timestamp')
df = df.sort_index()
print(df.head())
现在我们有按时间戳排序的数据了。但仔细观察输出,会发现索引并不唯一,两个用户可能同时使用系统。这是常见情况。让我们重置索引,并使用时间和用户一起建立多级索引来处理这个问题。
df = df.reset_index()
df = df.set_index(['timestamp', 'user'])
print(df.head())
现在数据已适当索引和排序,我们可以使用ffill填充缺失值。处理缺失值时,可以只处理单个或部分列,而不必一次性修复所有缺失值。
df = df.fillna(method='ffill')
使用replace函数替换值
除了填充,我们还可以使用replace函数进行自定义替换。它支持多种替换方式:值对值、列表、字典、正则表达式。
让我们生成一个简单的例子。
df = pd.DataFrame({
'A': [1, 1, 2, 3, 4],
'B': [3, 6, 3, 8, 9],
'C': list('abcde')
})
值对值替换:将1替换为100。
df.replace(1, 100, inplace=True)
列表替换:将1替换为100,将3替换为300。
df.replace([1, 3], [100, 300], inplace=True)
正则表达式替换:Pandas的替换功能也支持正则表达式。让我们再看一下来自log.csv的数据集。
df = pd.read_csv('log.csv')
print(df.head())
要使用正则表达式替换,我们将第一个参数设为要匹配的模式,第二个参数设为替换值,并传递第三个参数regex=True。
假设我们想检测video列中所有以.html结尾的HTML页面,并将其替换为关键词“web page”。我们该如何实现?
以下是解决方案:匹配任意数量的字符,然后以.html结尾。
df.replace('.*\.html$', 'web page', regex=True, inplace=True)
我们看到这达到了预期效果。
关于缺失值的最后说明
在数据框上使用统计函数时,这些函数通常会忽略缺失值。例如,在计算数据框的平均值时,底层的NumPy函数可能会忽略这些缺失值。这通常是期望的行为,但你应该清楚哪些值被排除了。
缺失值的成因至关重要,它取决于你要解决的问题。在某些情况下,推断缺失值可能是不合理的,例如,如果数据本身就不应该存在。
总结



本节课中我们一起学习了Pandas中处理缺失值的核心方法。我们探讨了缺失值的不同类型和成因,学习了如何使用.isnull()检测缺失值,以及使用.dropna()删除含缺失值的行。重点掌握了使用.fillna()进行标量填充、向前填充和向后填充的技巧,并了解了如何使用.replace()函数进行灵活的值替换,包括支持正则表达式。最后,我们认识到在统计分析中,缺失值通常会被自动忽略,但理解其背后的原因对正确分析数据至关重要。掌握这些技能将为你后续的数据清洗和分析工作打下坚实基础。
21:DataFrame操作实例 🧹


在本节课中,我们将学习一个基本的数据清洗流程,并介绍几个实用的Pandas API函数。我们将使用一个从维基百科获取的美国总统名单数据集,演示如何拆分姓名、清理日期格式,并探索apply函数和str模块的用法。
导入数据
首先,我们需要导入Pandas库并加载数据。
import pandas as pd
df = pd.read_csv('datasets/presidents.csv')
df.head()
数据集中包含总统姓名、日期等信息。B列包含许多脚注,可能会带来问题。我们将从清理“姓名”列开始,将其拆分为“名”和“姓”。
拆分姓名列
我们的目标是将“President”列拆分成“First Name”和“Last Name”两列。我们将尝试几种不同的方法。
方法一:使用replace函数
一种方法是复制“President”列,然后使用正则表达式替换掉姓氏部分。
df['first'] = df['president']
df['first'] = df['first'].replace(r'\s\w+', '', regex=True)
df.head()
这种方法虽然有效,但效率较低,因为它需要复制整个列然后更新字符串。
方法二:使用apply函数
apply函数可以将一个自定义函数应用到DataFrame的每一行或每一列。这是一种更通用的方法。
首先,删除之前创建的列。
del df['first']
接下来,定义一个函数来拆分姓名。
def split_name(row):
row['first'] = row['president'].split(' ')[0]
row['last'] = row['president'].split(' ')[-1]
return row
df = df.apply(split_name, axis='columns')
df.head()
这个函数提取了空格分隔的第一个单词作为名,最后一个单词作为姓。apply函数会负责将结果合并回DataFrame。
方法三:使用str.extract函数
Pandas的str模块提供了专门处理字符串的向量化函数,效率更高。extract函数可以使用正则表达式的捕获组来提取内容。
首先,再次删除之前创建的列。
del df['first']
del df['last']
然后,使用extract函数。我们需要一个能捕获“名”和“姓”的正则表达式。
pattern = r'^(\w+)\s(?:.*\s)?(\w+)$'
names = df['president'].str.extract(pattern)
names.head()
为了得到更好的列名,我们可以使用命名捕获组。
pattern = r'^(?P<first>\w+)\s(?:.*\s)?(?P<last>\w+)$'
names = df['president'].str.extract(pattern)
df['first'] = names['first']
df['last'] = names['last']
df.head()
str模块包含许多用于数据清洗的实用函数,建议查阅官方文档的“Working with text”部分以了解更多。
清理日期列
接下来,我们清理“Born”列,使其符合“日 月 年”的格式,并将其转换为更易处理的日期时间类型。
我们使用str.extract来提取标准日期格式的部分。
df['born'] = df['born'].str.extract(r'([A-Za-z]+\s\d{1,2}\s\d{4})')
df.head()
虽然格式已清理,但该列的数据类型仍是“object”(字符串)。为了便于后续的日期操作(例如,筛选特定时间范围内出生的总统),我们将其转换为Pandas的datetime类型。
df['born'] = pd.to_datetime(df['born'])
Pandas的日期时间功能非常强大,是处理时间序列数据的利器。
总结与练习建议
本节课中,我们一起学习了DataFrame的基本数据清洗操作。
我们介绍了三种拆分姓名列的方法:replace、apply和str.extract。其中,str模块的函数基于正则表达式且是向量化的,通常具有更高的效率。我们还演示了如何清理和转换日期列。
对于数据集中的其他列,可以采用类似的模式进行清洗。建议你暂停视频,打开配套的Notebook,尝试独立完成整个DataFrame的清理工作,这将是一个很好的练习。
核心要点回顾:
apply函数:适用于复杂的自定义行/列操作。str模块(如extract):专门用于向量化的字符串处理,效率高。pd.to_datetime函数:将字符串列转换为日期时间类型,便于时间序列分析。


通过掌握这些工具,你将能够更有效地准备和清洗数据,为后续的数据分析工作打下坚实基础。
22:DataFrame 合并技术 📊


在本节课中,我们将学习如何将多个 DataFrame 对象组合在一起,无论是通过水平合并还是垂直拼接。
概述
数据通常存储在不同的表格或文件中。为了进行分析,我们需要将这些数据整合到一起。Pandas 提供了两种主要方法:merge(合并)和 concat(拼接)。merge 用于基于共同列(键)水平连接数据,而 concat 用于垂直堆叠具有相同结构的数据。
关系理论与术语
在深入代码之前,我们需要了解一些关系型数据库的理论和术语约定。这里用一张维恩图来帮助理解这些概念。
维恩图传统上用于展示集合的成员关系。例如,左边的圆圈代表一所大学的所有学生,右边的圆圈代表所有教职工,中间重叠的区域则是那些既是学生又是教职工的人(例如担任助教或参与研究的学生)。
这张图展示了我们可能拥有数据的两个群体,但它们之间存在重叠。在 Pandas 中,我们可以将这两个群体视为两个独立的 DataFrame,可能以人名作为索引。当我们想要合并这些 DataFrame 时,需要做出一些选择。
以下是几种主要的合并类型:
- 完全外连接:获取所有人的列表,无论他们是教职工还是学生,并包含数据库中能获取到的所有信息。在集合论中,这被称为并集。在维恩图中,它代表任何一个圆圈内的所有人。
- 内连接:只获取那些同时是教职工和学生的、信息最全的人员列表。在数据库术语中,这被称为内连接,在集合论中称为交集。在维恩图中,它代表两个圆圈重叠的部分。
有了这个背景知识,让我们看看在 Pandas 中如何实现。
使用 merge 函数
首先导入 Pandas。
import pandas as pd
创建示例数据
创建两个 DataFrame:staff_df 和 students_df。
# 创建教职工 DataFrame
staff_df = pd.DataFrame([{'Name': 'Kelly', 'Role': 'Director of HR'},
{'Name': 'Sally', 'Role': 'Course Liaison'},
{'Name': 'James', 'Role': 'Grader'}])
# 设置姓名为索引
staff_df = staff_df.set_index('Name')
# 创建学生 DataFrame
students_df = pd.DataFrame([{'Name': 'James', 'School': 'Business'},
{'Name': 'Mike', 'School': 'Law'},
{'Name': 'Sally', 'School': 'Engineering'}])
# 设置姓名为索引
students_df = students_df.set_index('Name')
# 查看数据
print(staff_df.head())
print(students_df.head())
这两个 DataFrame 在 “James” 和 “Sally” 上存在重叠,但 “Mike” 和 “Kelly” 是唯一的。重要的是,两个 DataFrame 都按我们想要合并的列(即“姓名”)建立了索引。
执行合并操作
1. 完全外连接
要获取并集(所有人),我们使用 merge 函数,指定 how='outer',并告知函数使用左右两侧的索引作为连接键。
# 完全外连接
outer_merged = pd.merge(staff_df, students_df, how='outer', left_index=True, right_index=True)
print(outer_merged)
结果 DataFrame 列出了所有人。由于 “Mike” 没有角色信息,“Kelly” 没有学校信息,这些单元格显示为缺失值(NaN)。
2. 内连接
要获取交集(仅是既是教职工又是学生的人),我们将 how 参数设置为 'inner'。
# 内连接
inner_merged = pd.merge(staff_df, students_df, how='inner', left_index=True, right_index=True)
print(inner_merged)
结果 DataFrame 只包含 “James” 和 “Sally”。
左连接与右连接
合并 DataFrame 时还有另外两种常见用例,它们都是集合加法的例子。
3. 左连接
获取所有教职工的列表,无论他们是否是学生。但如果他们也是学生,我们也希望获取其学生详细信息。这需要用到左连接。注意,函数中第一个 DataFrame 是左表,第二个是右表。
# 左连接 (以教职工表为基准)
left_merged = pd.merge(staff_df, students_df, how='left', left_index=True, right_index=True)
print(left_merged)
4. 右连接
获取所有学生的列表,以及他们的角色(如果他们同时也是教职工)。这需要用到右连接。
# 右连接 (以学生表为基准)
right_merged = pd.merge(staff_df, students_df, how='right', left_index=True, right_index=True)
print(right_merged)
使用 on 参数合并列
merge 方法不需要一定使用索引进行连接,也可以使用列。on 参数可以指定两个 DataFrame 共有的列名作为连接键。
首先,重置索引,使“姓名”变回列。
staff_df_reset = staff_df.reset_index()
students_df_reset = students_df.reset_index()
现在使用 on 参数进行合并。
# 使用 on 参数进行右连接
merged_on_name = pd.merge(staff_df_reset, students_df_reset, how='right', on='Name')
print(merged_on_name)
使用 on 参数而不是索引,是更常见的 merge 使用方式。
处理列名冲突
当两个 DataFrame 有同名的列,但含义不同时会发生什么?让我们创建包含位置信息的新 DataFrame 来看看。
# 创建包含位置信息的新 DataFrame
staff_df_new = pd.DataFrame([{'Name': 'Kelly', 'Role': 'Director of HR', 'Location': 'State Street'},
{'Name': 'Sally', 'Role': 'Course Liaison', 'Location': 'Washington Avenue'},
{'Name': 'James', 'Role': 'Grader', 'Location': 'Washington Avenue'}])
students_df_new = pd.DataFrame([{'Name': 'James', 'School': 'Business', 'Location': '1024 Billiard Avenue'},
{'Name': 'Mike', 'School': 'Law', 'Location': 'Fraternity House #22'},
{'Name': 'Sally', 'School': 'Engineering', 'Location': '512 Wilson Crescent'}])
在教职工 DataFrame 中,“Location” 是办公室地址;在学生 DataFrame 中,“Location” 是家庭住址。merge 函数会保留这些信息,但通过添加后缀 _x 或 _y 来区分数据来自哪个 DataFrame。_x 总是代表左表信息,_y 代表右表信息。
# 左连接,观察列名冲突
conflict_merged = pd.merge(staff_df_new, students_df_new, how='left', on='Name')
print(conflict_merged)
输出中会出现 Location_x(来自左表 staff_df_new)和 Location_y(来自右表 students_df_new)两列。
基于多列合并
有时仅凭一个列(如“名”)不足以唯一标识一行,可能需要结合“姓”等其他列。这时,可以向 on 参数传递一个列名列表。
# 创建包含名和姓的 DataFrame
staff_multi = pd.DataFrame([{'First Name': 'Kelly', 'Last Name': 'Desjardins', 'Role': 'Director of HR'},
{'First Name': 'Sally', 'Last Name': 'Brooks', 'Role': 'Course Liaison'},
{'First Name': 'James', 'Last Name': 'Wilde', 'Role': 'Grader'}])
students_multi = pd.DataFrame([{'First Name': 'James', 'Last Name': 'Hammond', 'School': 'Business'},
{'First Name': 'Mike', 'Last Name': 'Smith', 'School': 'Law'},
{'First Name': 'Sally', 'Last Name': 'Brooks', 'School': 'Engineering'}])
# 基于多列进行内连接
multi_merged = pd.merge(staff_multi, students_multi, how='inner', on=['First Name', 'Last Name'])
print(multi_merged)
由于 “James Wilde” 和 “James Hammond” 的姓氏不同,内连接的结果将只包含 “Sally Brooks”。
使用 concat 进行垂直拼接
上一节我们介绍了基于键值的水平合并 (merge),本节中我们来看看垂直拼接 (concat)。如果将合并视为水平连接(基于两个 DataFrame 中共有的列值),那么拼接就是垂直连接(将一个 DataFrame 堆叠在另一个的顶部或底部)。
例如,你有一个按年份跟踪信息的数据集,每年的记录是一个独立的 CSV 文件,且每个文件都有完全相同的列结构。如果你想将所有年份的数据整合在一起,就可以使用拼接。
让我们以美国教育部大学记分卡数据为例,该数据包含每所美国大学的学生毕业率、毕业后债务、收入等信息。数据按年份存储在不同的 CSV 文件中。
假设我们需要 2011 到 2013 年的记录。我们首先创建三个 DataFrame,每个包含一年的记录。由于 CSV 文件可能有些问题,我们使用 error_bad_lines=False 来忽略错误行。
# 读取三个年份的数据
df_2011 = pd.read_csv('college_scorecard_2011_12.csv', error_bad_lines=False)
df_2012 = pd.read_csv('college_scorecard_2012_13.csv', error_bad_lines=False)
df_2013 = pd.read_csv('college_scorecard_2013_14.csv', error_bad_lines=False)
# 查看每个 DataFrame 的行数
print(len(df_2011), len(df_2012), len(df_2013))
现在,将这三个 DataFrame 放入一个列表,并传递给 pd.concat() 函数。
# 将 DataFrame 放入列表
frames = [df_2011, df_2012, df_2013]
# 垂直拼接
concatenated_df = pd.concat(frames)
print(len(concatenated_df)) # 检查总行数是否等于三个年份行数之和
拼接后,我们得到了一个包含更多观察值(行)的 DataFrame,而列保持不变。但有一个问题:我们不知道每条记录来自哪一年了。
concat 函数有一个 keys 参数可以解决这个问题,它能添加一个额外的索引层级来标识原始数据来源。

# 使用 keys 参数添加年份标识
concatenated_df_with_keys = pd.concat(frames, keys=['2011', '2012', '2013'])
print(concatenated_df_with_keys.head())
print(concatenated_df_with_keys.index) # 查看多级索引
现在,索引中包含了年份信息,我们可以区分每条观察值来自哪一年。
需要注意的是,concat 也有 join 参数(默认为 'outer')。如果你拼接的两个 DataFrame 列不完全相同,选择 join='outer' 时,缺失的单元格将是 NaN;选择 join='inner' 时,将只保留共有的列,并可能因为 NaN 值而丢弃一些行。这类似于 merge 函数的外连接和内连接概念。
总结
本节课中我们一起学习了 Pandas 中组合 DataFrame 的两种核心技术。
merge函数:用于基于一个或多个共同列(键)水平合并 DataFrame。我们探讨了不同类型的连接:how='outer':完全外连接(并集)。how='inner':内连接(交集)。how='left'/how='right':左连接/右连接。- 使用
on参数指定连接列,处理列名冲突,以及基于多列进行合并。
concat函数:用于垂直拼接具有相同列结构的 DataFrame。我们学习了如何使用keys参数来标记拼接后数据的来源。


熟练掌握如何合并与拼接数据,对于数据获取、清洗和整理至关重要。这些技能能帮助你从不同来源整合数据,进行更复杂深入的分析。建议你查阅 Pandas 官方文档,以了解更多关于连接和拼接的高级选项和细节。
23:Pandas 惯用操作 🐼


在本节课中,我们将学习如何编写更符合 Pandas 风格的代码,即“Pandoable”的代码。我们将重点探讨两种核心的 Pandas 惯用法:方法链式调用和 apply 函数的应用。掌握这些技巧能让你的代码更简洁、高效,也更符合社区规范。
方法链式调用 🔗
上一节我们介绍了编写 Pandas 代码的一些基本概念。本节中,我们来看看第一种惯用法:方法链式调用。其核心思想是,对象上的每个方法都会返回对该对象的引用。这使得我们可以将多个对数据框的操作串联成一行或一个语句,从而让代码更紧凑。
以下是使用链式调用的一个“Pandoable”示例。这段代码的目标是:筛选出汇总级别为50(即县级数据)的行,删除缺失值,将“州名”和“县名”设置为多级索引,并重命名一个列以增强可读性。
(df.where(df['SUMLEV'] == 50)
.dropna()
.set_index(['STNAME', 'CTYNAME'])
.rename(columns={'ESTIMATESBASE2010': 'Estimates Base'}))
让我们逐步解析这段代码:
- 首先,使用
where函数并传入一个布尔掩码,该掩码仅在SUMLEV列等于50的行上为真。 - 接着,对
where函数的结果调用dropna()以删除缺失值。 - 然后,使用
set_index将索引设置为['STNAME', 'CTYNAME']。 - 最后,使用
rename将列名'ESTIMATESBASE2010'改为更易读的名称。
注意:为了代码的可读性,我使用括号将整个语句包裹起来,使其可以跨越多行,而不是将所有代码写在一行。
相比之下,下面是一种更传统、非“Pandoable”的写法。它在功能上没有问题,甚至对新手来说可能更易理解,但不如第一个例子那样符合 Pandas 的惯用风格。
# 非链式调用写法
df = df[df['SUMLEV'] == 50]
df.set_index(['STNAME', 'CTYNAME'], inplace=True)
df.rename(columns={'ESTIMATESBASE2010': 'Estimates Base'}, inplace=True)
理解任何惯用法的关键在于知道它何时不适用。在这个例子中,我们可以通过计时来比较两种方法的性能。使用 timeit 模块进行测试后,我们可能会发现第二种(非链式)方法运行速度更快。这是一个典型的时间与可读性之间的权衡。
你会在 Stack Overflow 和官方文档中看到大量使用链式调用的例子。理解这种语法非常重要,也值得你花时间去研究。但请记住,遵循看似风格化的惯用法可能会带来性能问题,这一点也需要考虑,尤其是在处理大规模数据清洗时。
使用 apply 函数进行数据转换 🔄
上一节我们探讨了方法链式调用,本节中我们来看看另一个强大的工具:apply 函数。Python 有一个出色的 map 函数,它是函数式编程的基础。Pandas 有类似的功能,称为 applymap,它会对数据框的每个单元格应用一个函数。但我个人很少使用 applymap,更常用的是 apply 函数,它可以跨数据框的行或列应用函数。
让我们看一个例子。在我们的普查数据框中,有五个列分别对应不同年份的人口估计值。我们很可能需要创建一些新列来存储每行的最小值和最大值,apply 函数可以轻松实现这一点。
首先,我们需要编写一个函数,它接收一行数据,找出其中的最小值和最大值,并返回一个新的序列(Series)。
import numpy as np
def min_max(row):
data = row[['POPESTIMATE2010', 'POPESTIMATE2011',
'POPESTIMATE2012', 'POPESTIMATE2013',
'POPESTIMATE2014', 'POPESTIMATE2015']]
return pd.Series({'min': np.min(data), 'max': np.max(data)})
然后,我们在数据框上调用 apply 函数。apply 接受要应用的函数和操作的轴作为参数。需要小心的是,参数 axis=1 或 axis=‘columns’ 表示跨所有行应用(即对每一行的所有列进行操作)。
df.apply(min_max, axis='columns').head()
当然,你不必局限于返回一个新的序列对象。如果你在进行数据清洗,可能会希望将新数据添加到现有数据框中。在这种情况下,你可以直接修改传入的行数据,为其添加新列。
def min_max(row):
data = row[['POPESTIMATE2010', 'POPESTIMATE2011',
'POPESTIMATE2012', 'POPESTIMATE2013',
'POPESTIMATE2014', 'POPESTIMATE2015']]
row['max'] = np.max(data)
row['min'] = np.min(data)
return row
df.apply(min_max, axis='columns').head()
apply 是你工具箱中一个极其重要的工具。我在这里介绍它,是因为你很少会看到它像我们刚才那样与大型函数定义一起使用。相反,你通常会看到它与 Lambda 表达式 一起使用,以最大限度地简化代码。
以下是一个使用 Lambda 表达式和 apply 计算每行最大值的单行示例:
rows = ['POPESTIMATE2010', 'POPESTIMATE2011',
'POPESTIMATE2012', 'POPESTIMATE2013',
'POPESTIMATE2014', 'POPESTIMATE2015']
df.apply(lambda x: np.max(x[rows]), axis=1).head()
如果你不记得 Lambda 表达式,可以简单回顾一下:Lambda 是 Python 中的匿名函数。在这个例子中,它接受一个参数
x(即一行数据),并返回该行指定列的最大值。
apply 函数的优点在于其灵活性,你可以传入任何自定义函数来完成所需的操作。例如,假设我们想根据州名将各州划分为东北部、中西部、南部和西部四个区域。
首先,我们编写一个自定义函数来根据州名返回区域:
def get_state_region(x):
northeast = ['Connecticut', 'Maine', 'Massachusetts', ...]
midwest = ['Illinois', 'Indiana', 'Iowa', ...]
south = ['Alabama', 'Arkansas', 'Delaware', ...]
west = ['Alaska', 'Arizona', 'California', ...]
if x in northeast:
return 'Northeast'
elif x in midwest:
return 'Midwest'
elif x in south:
return 'South'
elif x in west:
return 'West'
然后,我们可以使用 apply 函数将这个自定义函数应用到 STNAME 列上,以创建一个名为 state_region 的新列:
df['state_region'] = df['STNAME'].apply(lambda x: get_state_region(x))
df[['STNAME', 'state_region']].head()
总结 📝
本节课中,我们一起学习了两种关键的 Pandas 惯用操作。
- 方法链式调用:通过将多个操作串联成一个流畅的语句,使代码更加简洁和可读。但需要注意,在某些情况下,传统的分步写法可能具有更好的性能。
apply函数:这是一个极其灵活的工具,允许你跨数据框的行或列应用任何自定义函数,无论是用于计算汇总统计量还是进行复杂的数据转换。结合 Lambda 表达式使用,可以写出非常简洁有力的代码。


Pandas 社区还有许多其他的惯用法。我给大家留一个非正式的任务:去 Stack Overflow 上查看一些关于 Pandas 的高票问题,观察经验丰富的作者是如何回答的。你能发现哪些有趣的模式?欢迎与我和课程中的其他同学分享,让我们一起探索更多“Pandoable”的代码风格。
24:分组聚合 (GroupBy) 📊

在本节课中,我们将学习 Pandas 库中一个极其强大的功能:groupby。我们将了解如何根据特定条件将数据分割成组,然后对每个组进行聚合、转换或过滤操作,最后将结果合并。这是数据分析中“拆分-应用-合并”模式的经典实现。
概述
有时我们需要基于分组来选择数据,并在组级别上理解聚合后的数据。虽然可以通过迭代数据框的每一行来实现,但这种方法通常非常慢。幸运的是,Pandas 的 groupby 函数可以极大地加速这类任务。
groupby 的核心思想是:它接收一个数据框,根据某些键值将其拆分成块,然后对这些块应用计算,最后将结果组合回另一个数据框中。在 Pandas 中,这被称为“拆分-应用-合并”模式。
导入库与数据准备
首先,我们需要导入必要的库并加载数据。
import pandas as pd
import numpy as np
我们将使用一些美国人口普查数据。我们读取数据,并排除州级别的汇总(其 sumlev 值为 40),只保留县级数据(sumlev 值为 50)。
df = pd.read_csv('datasets/census.csv')
df = df[df['sumlev'] == 50]
print(df.head())
传统迭代方法 vs. GroupBy 方法
传统迭代方法
在第一个例子中,我们想计算每个州的平均人口。传统方法是获取所有唯一州的列表,然后遍历每个州,筛选数据并计算平均值。
以下是使用传统方法的代码,并使用 %%timeit 魔法命令测量其运行时间:
%%timeit -n 3
for state in df['state_name'].unique():
avg = np.average(df.where(df['state_name'] == state).dropna()['CENSUS2010POP'])
print(f'Counties in state {state} have an average population of {avg}')
可以看到,这种方法需要相当长的时间才能完成。
使用 GroupBy 方法
现在,让我们尝试使用 groupby 的另一种方法。
%%timeit -n 3
for group, frame in df.groupby('state_name'):
avg = np.average(frame['CENSUS2010POP'])
print(f'Counties in state {group} have an average population of {avg}')
groupby 返回一个元组,其中第一个值是分组键(例如州名),第二个值是属于该组的数据框。这种方法在速度上有巨大的提升。
使用函数进行分组
大多数时候,你会根据一列或多列进行分组。但你也可以提供一个函数给 groupby 来分割数据。
这是一个稍微构造的例子。假设你有一个包含大量处理的大批量作业,并且你只想在给定时间处理大约三分之一的数据。我们可以创建一个函数,根据州名的首字母返回 0 到 2 之间的数字。
首先,需要将数据框的索引设置为要分组的列。
def set_batch(item):
if item[0] < 'M':
return 0
elif item[0] < 'Q':
return 1
else:
return 2
df = df.set_index('state_name')
for group, frame in df.groupby(set_batch):
print(f'There are {len(frame)} records in group {group} for processing.')
请注意,这次我们没有向 groupby 传递列名,而是将数据框的索引设置为 state_name。如果没有传递列标识符,groupby 会自动使用该索引。
多级索引与复杂分组
让我们再看一个如何分组数据的例子。在这个例子中,我们将使用 Airbnb 的房源数据集。我们感兴趣的两列是 cancellation_policy(取消政策)和 review_scores_value(评分值)。
df = pd.read_csv('datasets/listings.csv')
print(df.head())
使用多级索引分组
一种方法是将其提升为多级索引,然后调用 groupby。
df = df.set_index(['cancellation_policy', 'review_scores_value'])
for group, frame in df.groupby(level=[0, 1]):
print(group)
当我们有一个多级索引时,需要传入我们感兴趣的分组级别。默认情况下,groupby 不知道也不会假设你想要按所有级别分组。
使用函数管理分组
如果我们想按取消政策和评分分组,但将 10 分与其他分数分开,可以使用函数来管理分组。
def grouping_fun(item):
if item[1] == 10:
return (item[0], 10.0)
else:
return (item[0], 'Not 10')
for group, frame in df.groupby(grouping_fun):
print(group)
这样我们就将数据分成了两组:评分为 10 分的和不是 10 分的。
应用步骤:聚合、转换与过滤
到目前为止,我们在拆分后对数据应用了非常简单的处理(主要是打印语句)。Pandas 开发者为应用步骤定义了三大类数据处理:聚合、转换和过滤。
1. 聚合 (Aggregation)
最直接的应用步骤是数据的聚合。这使用 groupby 对象上的 .agg() 方法。
# 重置索引以便演示
df = df.reset_index()
# 按取消政策分组,并计算每组的平均评分
result = df.groupby('cancellation_policy').agg({'review_scores_value': np.nanmean})
print(result)
.agg() 方法接收一个字典,键是我们要聚合的列名,值是要应用的函数(例如 np.nanmean,它会忽略 NaN 值)。
我们可以扩展这个字典来聚合多个函数或多个列。
result = df.groupby('cancellation_policy').agg({
'review_scores_value': [np.nanmean, np.nanstd],
'reviews_per_month': np.nanmean
})
print(result)
理解这个语句很重要:
- 我们在数据框对象上按
cancellation_policy列进行groupby,创建了一个新的groupby对象。 - 然后在该对象上调用
.agg()函数。 .agg()函数将应用我们指定的一个或多个函数到分组数据框,并返回每个数据框/组的单行结果。- 我们传递了一个字典,其键指示要将函数应用到哪一列。对于第一列,我们实际上提供了一个包含两个函数的元组。
groupby对象会识别这个元组,并依次对同一列调用每个函数,结果将位于一个分层索引中。
2. 转换 (Transformation)
转换与聚合不同。.agg() 每列返回一个值(即每组一行),而 .transform() 返回一个与组大小相同的对象。它本质上将你提供的函数广播到组数据框上,返回一个新的数据框。
例如,假设我们想在按取消政策分组的组中包含平均评分值,但保留数据框的形状,以便计算单个观测值与组均值之间的差异。
# 选择感兴趣的列
cols = ['cancellation_policy', 'review_scores_value']
df_sub = df[cols].copy()
# 使用 transform 计算组均值,并保持原数据框形状
transform_df = df_sub.groupby('cancellation_policy')['review_scores_value'].transform(np.nanmean)
transform_df = transform_df.rename('mean_review_score')
# 将结果合并回原数据框
df = df.merge(transform_df, left_index=True, right_index=True)
# 计算每个观测值与组均值的绝对差异
df['mean_diff'] = np.abs(df['review_scores_value'] - df['mean_review_score'])
print(df.head())
3. 过滤 (Filtering)
groupby 对象也内置了对过滤组的支持。你通常会先按某些特征分组,然后对组进行转换,最后在清理过程中丢弃某些组。
.filter() 函数接收一个函数,该函数应用于每个组数据框,并返回 True 或 False,取决于该组是否应包含在结果中。
例如,如果我们只想在结果中包含平均评分高于 9.2 的组:
filtered_df = df.groupby('cancellation_policy').filter(lambda x: np.nanmean(x['review_scores_value']) > 9.2)
print(filtered_df.head())
通用应用:Apply 函数
到目前为止,我在 groupby 对象上最常调用的操作是 .apply() 函数。它允许你将任意函数应用于每个组,并将每个应用的结果缝合回一个单独的数据框中,同时保留索引。
让我们看一个使用 Airbnb 数据的例子。我们将加载数据的一个干净副本。
df_clean = pd.read_csv('datasets/listings.csv', usecols=['cancellation_policy', 'review_scores_value'])
在之前的工作中,我们想计算每个房源的平均评分及其与组均值的偏差。这是一个两步过程。使用 .apply(),我们可以将逻辑包装在一个地方。
def calc_mean_review_scores(group):
avg = np.nanmean(group['review_scores_value'])
group['review_score_mean'] = avg
group['mean_diff'] = np.abs(avg - group['review_scores_value'])
return group
result_df = df_clean.groupby('cancellation_policy').apply(calc_mean_review_scores)
print(result_df.head())
使用 .apply() 可能比使用一些专用函数(尤其是 .agg())慢,但如果你的数据框不是特别大,这是一个可靠的通用方法。
总结
本节课我们一起学习了 Pandas 中强大的 groupby 功能。
groupby 是数据清洗和数据分析中一个强大且常用的工具。一旦你按某个类别对数据进行了分组,你就得到了仅包含这些值的数据框,可以对你感兴趣的片段进行聚合分析。
groupby 函数遵循“拆分-应用-合并”的方法:
- 拆分:数据被拆分成若干组。
- 应用:对每组应用某种转换、过滤或聚合操作。
- 合并:Pandas 会自动为你合并结果。



通过掌握聚合(.agg)、转换(.transform)、过滤(.filter)和通用应用(.apply)这些方法,你可以高效地处理各种复杂的分组数据分析任务。
25:数据尺度处理 📊


在本节课中,我们将学习数据科学中一个基础但至关重要的概念:数据尺度。我们将探讨四种主要的数据尺度类型,并学习如何在Pandas中处理不同尺度的数据,包括类型转换和分箱操作。
概述
上一节我们介绍了Pandas的许多基础操作。本节中,我们将暂停一下,讨论数据类型和尺度。我们已经知道Pandas支持多种计算数据类型,如字符串、整数、浮点数。但这并未涵盖我们所说的数据尺度。
假设我们有一个包含学生及其年级(如一年级、二年级、三年级)的DataFrame。一年级和二年级学生之间的差异,与八年级和九年级学生之间的差异相同吗?或者考虑学生在作业中获得的期末考试成绩。A和A-之间的差异,与A-和B+之间的差异相同吗?至少在密歇根大学,答案通常是否定的,尤其是在转换为基于百分比的尺度时。
我们直观地感受到了一些不同的尺度。随着我们从数据清洗进入到统计分析和机器学习,澄清我们的知识和术语非常重要。作为数据科学家,至少需要了解四种不同的尺度。
四种数据尺度
以下是数据科学中四种重要的尺度类型。
1. 比率尺度
在比率尺度中,测量单位是等距的,并且数学运算(如减法、除法和乘法)都有效。比率尺度的好例子包括身高和体重。
核心概念:数值具有真实的绝对零点,可以进行所有数学运算。
2. 区间尺度
在区间尺度中,测量单位像比率尺度一样是等距的,但没有明确的“零值”概念。也就是说,不存在真正的零点,因此乘法和除法等运算无效。区间尺度的例子包括以摄氏度或华氏度测量的温度,因为温度永远不会“不存在”,0度本身就是一个有意义的数值。指南针上的方向可能是另一个好例子,指南针上的0度并不表示没有方向,而是描述了一个方向本身。
核心概念:数值没有绝对的零点,加减法有意义,乘除法无意义。
对于您将进行的大部分数据挖掘工作,比率尺度和区间尺度之间的差异对于您所应用的算法来说可能并不明显或重要。但在应用高级统计检验时,在脑海中明确这种区别非常重要。
3. 序数尺度
在序数尺度中,值的顺序很重要,但值之间的差异不是等距的。密歇根大学许多课程使用的评分方法就是一个很好的例子,其中字母成绩带有加号和减号。但当你将其与百分比值进行比较时,你会发现一个单独的字母涵盖了可用成绩的4%,而带有加号或减号的字母通常只占可用成绩的3%。基于此,如果获得A+或A-的学生数量与获得纯A的学生数量一样多,那将是很奇怪的(当然,假设我们期望课程中的每个百分比点均匀出现)。
序数数据在机器学习中非常常见,有时处理起来可能有点挑战性。
核心概念:数据有顺序,但差值不等距。
4. 名义尺度
最后要提到的尺度是名义尺度,通常简称为分类数据。这里,体育运动中球队的名称可能是一个很好的例子。球队数量有限,但改变它们的顺序、对它们应用数学函数是没有意义的。
分类值非常常见,我们通常将只有两个可能值的类别称为二元类别。
核心概念:数据代表类别,无顺序和数学意义。
为什么讨论尺度?
那么,为什么我要暂停讨论Pandas而跳入关于尺度的讨论呢?鉴于它们在统计学和机器学习中的重要性,Pandas有许多有趣的函数来处理测量尺度之间的转换。
在Pandas中处理分类和序数数据
让我们首先从名义数据开始,在Pandas中称为分类数据。Pandas实际上有一个用于分类数据的内置类型,您可以通过使用astype方法简单地将数据列设置为分类类型。
astype尝试更改数据的底层类型,在本例中更改为分类数据。您还可以通过传入ordered=True标志并按顺序传入类别,将其进一步更改为序数数据。
让我们在Pandas中看一个例子。
import pandas as pd
这是一个例子。让我们创建一个按降序排列的字母成绩DataFrame。我们还可以设置一个索引值,这里我们只是根据对学生表现的一些人为判断来设置。
# 创建示例DataFrame
grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D', 'F']
student_quality = ['Excellence', 'Great', 'Good', 'Above Average', 'Average', 'Below Average', 'Poor', 'Very Poor', 'Fail', 'Bad Fail', 'No Submission']
df = pd.DataFrame({'grades': grades}, index=student_quality)
print(df.head())
print(df.dtypes)
现在,如果我们检查此列的数据类型,会发现它只是一个对象,因为我们设置了字符串值。我们可以使用astype函数告诉Pandas我们希望将类型更改为分类。
# 转换为分类类型
df['grades_cat'] = df['grades'].astype('category')
print(df['grades_cat'].head())
现在我们看到有11个类别,Pandas知道这些类别是什么。更有趣的是,我们的数据在这种情况下不仅是分类的,而且实际上是有序的,即A-在B+之后,B在B+之前。
我们可以通过首先创建一个新的分类数据类型(包含有序类别列表和ordered=True标志)来告诉Pandas数据是有序的。
# 创建有序分类数据类型
my_categories = pd.CategoricalDtype(categories=['F', 'D', 'C-', 'C', 'C+', 'B-', 'B', 'B+', 'A-', 'A', 'A+'], ordered=True)
grades_ordered = df['grades'].astype(my_categories)
print(grades_ordered.head())
现在我们看到Pandas不仅知道有11个类别,而且还知道这些类别的顺序。
有序分类数据的用途
那么,你能用这个做什么呢?因为存在排序,这可以帮助进行布尔掩码中的一些比较。例如,如果我们有一个成绩列表并将它们与C进行比较,我们可以看到词典比较(字符串的默认比较)会产生我们不想要的结果。
# 字符串比较(非分类数据)
print(df['grades'] > 'C')
# 有序分类数据比较
print(grades_ordered > 'C')
我们可以看到,在设置为有序分类类型的DataFrame上进行广播时,我们得到了可能期望的结果。然后,我们可以在此序数数据上使用一组特定的数学运算符,如最小值、最大值等。
虚拟变量
有时,将分类值表示为每个类别为一列(用True或False表示该类别是否适用)是有用的。这在特征提取中尤其常见,这是数据挖掘课程中的一个主题。具有布尔值的变量通常称为虚拟变量,Pandas有一个名为get_dummies的内置函数,它将单个列的值转换为多列的0和1,指示虚拟变量的存在。我很少使用它,但当使用时,它非常方便。
# 创建虚拟变量
dummy_df = pd.get_dummies(df['grades'])
print(dummy_df.head())
数值数据分箱
我想讨论的另一个常见的基于尺度的操作是将区间或比率尺度(如数字成绩)转换为分类尺度。现在,这对您来说可能有点违反直觉,因为您丢失了关于值的信息,但在几个地方通常这样做。例如,如果您正在可视化类别的频率,这可能是一种非常有用的方法,直方图通常与转换后的区间或比率数据一起使用。此外,如果您在数据上使用机器学习分类方法,则需要使用分类数据,因此降低维度可能有助于应用给定的技术。
Pandas有一个名为cut的函数,它接受一个类似数组的结构(如DataFrame的列或Series)作为参数。它还接受要使用的箱数,并且所有箱都保持等距。
让我们回到一些人口普查数据作为例子。我们看到,我们可以按州分组,然后聚合以获得按州划分的平均县大小列表。如果我们进一步对此应用cut,比如10个箱,我们可以看到使用平均县大小列为分类的州。
import numpy as np
# 读取数据
df_census = pd.read_csv('datasets/census.csv')
# 筛选县级数据
df_county = df_census[df_census['SUMLEV'] == 50]
# 按州分组并计算平均人口
state_pop = df_county.set_index('STNAME').groupby(level=0)['CENSUS2010POP'].mean()
print(state_pop.head())
# 使用cut进行分箱
binned = pd.cut(state_pop, 10)
print(binned.head())
在这里,我们看到像阿拉巴马州和阿拉斯加州这样的州属于同一类别,而加利福尼亚州和哥伦比亚特区则属于非常不同的类别。
等频分箱与等宽分箱
分箱只是从数据构建分类的一种方式,还有许多其他方法。例如,cut为您提供区间数据,其中每个类别之间的间距大小相等。但有时您希望基于频率形成类别,希望每个箱中的项目数量相同。Pandas提供了qcut函数来实现等频分箱。
# 使用qcut进行等频分箱
quantile_binned = pd.qcut(state_pop, 10)
print(quantile_binned.head())
这真的取决于您的数据形状以及您计划用它做什么。
总结
本节课中,我们一起学习了数据科学中的核心概念——数据尺度。我们探讨了比率、区间、序数和名义四种尺度,并掌握了在Pandas中处理它们的关键技能:

- 使用
astype和CategoricalDtype将数据转换为有序/无序分类类型。 - 利用
get_dummies创建虚拟变量进行特征提取。 - 运用
cut和qcut函数对数值数据进行等宽或等频分箱,将其转换为分类数据。


理解并正确转换数据尺度,是进行准确的数据分析、统计建模和机器学习的基础。在接下来的课程中,我们将运用这些知识进行更深入的数据操作和分析。
26:数据透视表 📊


在本节课中,我们将要学习如何使用Pandas库创建数据透视表。数据透视表是一种强大的数据汇总工具,它能帮助我们快速分析不同维度数据之间的关系。
概述
数据透视表是一种为特定目的汇总数据框中数据的方式。它大量使用了我们之前讨论过的聚合函数。数据透视表本身也是一个数据框,其行代表一个你感兴趣的变量,列代表另一个变量,而单元格则代表某个聚合值。
数据透视表通常还会包含边际值,即每行每列的总和。这让你能够一目了然地看到两个变量之间的关系。
在Pandas中查看数据透视表
我们将导入pandas和numpy库。
import pandas as pd
import numpy as np
这里我们拥有《泰晤士高等教育》世界大学排名数据集,这是最具影响力的大学数据集之一。让我们导入这个数据集并查看其内容。
dataframe = pd.read_csv('data/cwurData.csv')
dataframe.head()
我们可以看到每个机构的排名、国家、教育质量、其他指标以及总分。
假设我们想创建一个名为“排名等级”的新列,将世界排名1-100的机构归类为第一梯队,101-200的为第二梯队,201-300的为第三梯队,301名之后的则归类为“其他顶尖大学”。
你现在实际上已经掌握了足够的知识来完成这个任务。你可以暂停视频尝试一下。
以下是我的解决方案。我将创建一个名为create_category的函数,它将操作数据框中的第一列“world_rank”。
def create_category(ranking):
if 1 <= ranking <= 100:
return 'First Tier Top University'
elif 101 <= ranking <= 200:
return 'Second Tier Top University'
elif 201 <= ranking <= 300:
return 'Third Tier Top University'
else:
return 'Other Top University'
现在我们可以将此函数应用于数据的单个列以创建新的序列。
dataframe['rank_level'] = dataframe['world_rank'].apply(lambda x: create_category(x))
dataframe.head()
创建数据透视表
数据透视表允许我们将这些列中的一列转换为新的列标题,并将其与另一列作为行索引进行比较。

假设我们想比较大学的排名等级与国家,并希望通过总分来进行比较。
为此,我们告诉Pandas,我们希望值(values)是“score”,索引(index)是“country”,列(columns)是“rank_levels”。这里涉及三个要素。然后我们指定聚合函数,这里我们将使用numpy.mean来获取该国大学的平均评分。
dataframe.pivot_table(values='score',
index='country',
columns='rank_level',
aggfunc=[np.mean]).head()
我们可以看到一个分层数据框,其中索引或行按国家排列,列有两个层级,顶层表示使用的是平均值,第二层是我们的排名等级。
在这个例子中,我们只关注一个变量(平均值),因此我们并不真正需要分层索引。我们注意到存在一些NaN值。例如,阿根廷的第一行,NaN值表明阿根廷只在“其他顶尖大学”类别中有观测值。
应用多个聚合函数
数据透视表不仅限于应用一个函数。你可以传递一个名为aggfunc的参数,它是一个要应用的不同函数的列表,Pandas将使用分层列名为你提供结果。
让我们尝试相同的查询,但也传入最大值函数。
dataframe.pivot_table(values='score',
index='country',
columns='rank_level',
aggfunc=[np.mean, np.max]).head()
现在我们同时看到了平均值和最大值。
添加边际值
如前所述,我们还可以汇总给定顶层组内的值。例如,如果我们想查看国家的总体平均值(使用均值函数),并想查看最大值中的最大值。我们可以通过提供边际值来指示Pandas执行此操作。
我们的函数看起来基本相同,我们传入带有两个值的aggfunc,然后只需添加一个额外的参数margins=True。
dataframe.pivot_table(values='score',
index='country',
columns='rank_level',
aggfunc=[np.mean, np.max],
margins=True).head()
查询数据透视表
数据透视表只是一个多层数据框,我们可以以与常规数据框类似的方式访问其中的序列或单元格。
让我们从之前的示例创建一个新的数据框。
new_df = dataframe.pivot_table(values='score',
index='country',
columns='rank_level',
aggfunc=[np.mean, np.max],
margins=True)
然后让我们查看索引和列。
print(new_df.index)
print(new_df.columns)
我们可以看到列是分层的,顶层列索引有两个类别(mean和max),较低层列索引有四个类别,即四个排名等级。
如果我们想获取每个国家第一梯队顶尖大学级别的平均分数,应该如何查询?我们只需要进行两次数据框投影:第一次针对平均值,第二次针对第一梯队。
new_df['mean']['First Tier Top University'].head()
我们可以看到这里的输出是一个序列对象,我们可以通过打印类型来确认。请记住,当你从数据框中投影出单列值时,你会得到一个序列对象。
print(type(new_df['mean']['First Tier Top University']))
如果我们想找到在第一梯队顶尖大学级别上平均分数最高的国家,可以使用idxmax函数。
new_df['mean']['First Tier Top University'].idxmax()
idxmax函数并非数据透视表的特殊功能,它是序列对象的内置函数。本课程没有时间详细介绍所有Pandas函数和属性,我强烈鼓励你探索API以更深入地了解可用的功能。
使用堆叠(Stack)与反堆叠(Unstack)
如果你想改变数据透视表的形状,可以使用stack和unstack函数。堆叠是将最低层列索引转换为最内层行索引,而反堆叠是堆叠的逆操作,即将最内层行索引转换为最低层列索引。一个例子将有助于更清楚地说明这一点。
让我们先查看我们的数据透视表以刷新记忆。
new_df.head()
现在让我们尝试堆叠,这应该将最低层列(即大学排名的梯队)移动到最内层行。
stacked_df = new_df.stack()
stacked_df.head()
在这里我们可以看到该列基本上被转置到了行中。在原始数据透视表中,排名等级是最低层列,堆叠后,排名等级成为最内层索引,出现在国家之后。
现在让我们尝试反堆叠这个。
unstacked_df = stacked_df.unstack()
unstacked_df.head()
这似乎将我们的数据框恢复到了原始形状。那么,如果我们连续反堆叠两次会发生什么?
new_df.unstack().unstack().head()
我们实际上最终反堆叠到只剩下一列,因此返回的是一个序列对象。这个列只是一个值,其含义由操作、排名和国家的分层索引表示。
总结
本节课中我们一起学习了数据透视表。这是一个相当简短的描述,但它们在处理数值数据时极其有用,特别是当你试图以某种形式汇总数据时。
无论是你自己探索数据,还是为他人报告准备数据,你都会经常在不同的数据切片上创建新的数据透视表。当然,你可以将任何你想要的函数传递给聚合函数,包括你自己定义的函数。

数据透视表是数据分析和汇总的强大工具,掌握它将极大地提升你处理和分析结构化数据的能力。
27:Pandas 日期与时间功能详解 📅


在本节课中,我们将学习 Pandas 库中处理日期和时间序列的强大功能。掌握这些功能对于进行时间序列分析至关重要,而 Pandas 在这方面提供了非常灵活和健壮的工具集。
Pandas 最初就是由 Wes McKinney 为处理金融时间序列数据而创建的,因此它在处理日期和时间方面非常出色。我们将从导入必要的库开始。
import pandas as pd
import numpy as np
1. 核心时间类介绍
Pandas 有四个与时间相关的主要类:Timestamp、DatetimeIndex、Period 和 PeriodIndex。首先,我们来了解 Timestamp。
Timestamp:单一时间点
Timestamp 代表一个单一的时间戳,将数值与特定的时间点关联起来。
例如,我们可以用一个字符串来创建一个时间戳:
pd.Timestamp('9/1/2019 10:05AM')
我们也可以通过分别传递年、月、日、时、分等参数来创建:
pd.Timestamp(2019, 12, 20, 0, 0)
Timestamp 对象拥有一些有用的属性。例如,isoweekday 可以返回星期几,其中 1 代表星期一,7 代表星期日。
t = pd.Timestamp(2019, 12, 20, 0, 0)
t.isoweekday() # 输出:5,表示星期五
我们也可以轻松地提取时间戳中的特定部分,如年、月、日、小时、秒等。
t.year
t.second
上一节我们介绍了表示时间点的 Timestamp,本节中我们来看看如何表示一个时间段。
Period:单一时间段
Period 类代表一个单一的时间跨度,例如特定的一天或一个月。
创建一个代表“2016年1月”的期间对象:
pd.Period('1/2016') # 粒度是 ‘M’ (月)
创建一个代表“2016年3月5日”的期间对象:
pd.Period('3/5/2016') # 粒度是 ‘D’ (日)
Period 对象封装了其粒度信息,因此在其上进行算术运算非常直观。例如,计算“2016年1月”之后的5个月:
pd.Period('1/2016') + 5 # 结果是 ‘2016-06’
计算“2016年3月5日”之前的两天:
pd.Period('3/5/2016') - 2 # 结果是 ‘2016-03-03’
2. 时间序列索引
了解了基本的时间对象后,我们来看看如何用它们来构建索引,这对于处理时间序列数据非常方便。
DatetimeIndex
我们可以创建一个以 Timestamp 为索引的 Series。
t1 = pd.Series(list('abc'), [pd.Timestamp('2016-09-01'),
pd.Timestamp('2016-09-02'),
pd.Timestamp('2016-09-03')])
type(t1.index) # 输出:pandas.core.indexes.datetimes.DatetimeIndex
PeriodIndex
同样,我们也可以创建以 Period 为索引的 Series。
t2 = pd.Series(list('def'), [pd.Period('2016-09'), pd.Period('2016-10'), pd.Period('2016-11')])
type(t2.index) # 输出:pandas.core.indexes.period.PeriodIndex
3. 转换与解析日期字符串
在实际数据分析中,我们经常遇到各种格式的日期字符串。Pandas 提供了 to_datetime 函数来智能地解析它们。
假设我们有以下格式各异的日期字符串列表:
d1 = ['June 2013', 'August 29, 2014', '20150626', '7/12/16']
我们可以创建一个 DataFrame 并尝试转换这些日期:
df_dates = pd.DataFrame(np.random.randint(10, size=(4,2)),
index=d1,
columns=list('ab'))
使用 pd.to_datetime 进行转换:
pd.to_datetime(d1)
to_datetime 还支持参数来明确解析顺序,例如处理欧洲格式的日期(日/月/年):
pd.to_datetime('4.7.12', dayfirst=True)
4. 时间差与偏移量
Timedelta:时间差
Timedelta 表示两个时间点之间的差异。
pd.Timestamp('9/3/2016') - pd.Timestamp('9/1/2016') # 输出:Timedelta('2 days 00:00:00')
我们也可以在时间戳上直接加减时间差:
pd.Timestamp('9/2/2016 8:10AM') + pd.Timedelta('12 days 3 hours')
DateOffset:日历偏移
DateOffset 与 Timedelta 类似,但它遵循特定的日历规则,支持更丰富的间隔类型,如“工作日”、“月末”、“月初”等。
ts = pd.Timestamp('2016-09-04')
# 增加一周
ts + pd.offsets.Week()
# 移动到当月最后一天
ts + pd.offsets.MonthEnd()
5. 生成日期范围与重采样
使用 date_range 生成日期序列
date_range 函数可以方便地生成一个 DatetimeIndex。以下是生成频率的示例:
生成每两周一次(周日)的9个日期点:
dates = pd.date_range('10-01-2016', periods=9, freq='2W-SUN')
生成10个工作日:
pd.date_range('10-01-2016', periods=10, freq='B')
生成以6月为季度开始的12个季度点:
pd.date_range('10-01-2016', periods=12, freq='QS-JUN')
在 DataFrame 中应用
让我们用生成的日期创建一个示例 DataFrame 并进行操作。
dates = pd.date_range('10-01-2016', periods=9, freq='2W-SUN')
df = pd.DataFrame({'Count_1': 100 + np.random.randint(-5, 10, 9).cumsum(),
'Count_2': 120 + np.random.randint(-5, 10, 9).cumsum()},
index=dates)
检查星期几:
df.index.weekday_name
计算差值:
df.diff()
重采样(Resampling): 将数据从高频聚合到低频(称为降采样)。
计算每月计数的平均值:
df.resample('M').mean()
6. 基于时间的索引与切片
Pandas 允许使用字符串进行灵活的部分索引和切片。
按年份索引:
df['2017']
按年份和月份索引:
df['2016-12']
按日期范围切片:
df['2016-12':]
总结


本节课我们一起学习了 Pandas 中处理日期和时间序列的核心功能。我们从四个基本时间类(Timestamp, DatetimeIndex, Period, PeriodIndex)入手,学习了如何创建、转换和解析日期时间数据。接着,我们探讨了表示时间间隔的 Timedelta 和遵循日历规则的 DateOffset。然后,我们掌握了使用 date_range 生成规则日期序列以及使用 resample 进行数据频率转换的方法。最后,我们学习了如何利用基于时间的索引和切片功能,高效地查询和筛选时间序列数据。这些功能共同构成了 Pandas 强大而灵活的时间序列处理能力,是进行数据分析,尤其是时间序列分析的基础。在后续课程中,我们将更深入地探讨重采样等高级主题。
28:Python用于数据科学实践 📊


概述:基础统计检验
在本节课中,我们将学习如何在Python中进行基础的统计检验。我们将讨论假设检验、统计显著性的概念,并使用SciPy库来运行学生t检验。数据科学中经常使用统计方法,本节课旨在回顾假设检验这一实验背后的核心数据分析活动。假设检验的目标是确定实验中的两种不同条件是否导致了不同的影响。
导入必要的库
首先,我们需要导入常用的NumPy和pandas库。接着,从SciPy库中导入统计模块。SciPy是一个有趣的数据科学库集合,它包含了NumPy和pandas,以及Matplotlib等绘图库和其他科学计算函数。

import numpy as np
import pandas as pd
from scipy import stats
理解假设检验
当我们进行假设检验时,实际上涉及两个陈述。第一个是我们的实际解释,称为备择假设。第二个陈述是我们的解释不充分,称为原假设。我们的实际检验方法是确定原假设是否为真。如果我们发现组间存在差异,就可以拒绝原假设并接受备择假设。
加载并探索数据
让我们看一个例子。我们将使用一些成绩数据。首先创建一个新的数据框,读取CSV文件grades.csv,并查看其前几行。
df = pd.read_csv('datas/grades.csv')
print(df.head())
数据框内有六项不同的作业。让我们查看一些数据摘要统计信息。
print(df.shape)
分割数据
为了本次课程的目的,我们将总体分为两部分。假设在2015年12月底之前完成第一次作业的人称为“早完成者”,在此之后完成的人称为“晚完成者”。
# 将提交时间列转换为datetime格式
df['assignment1_submission'] = pd.to_datetime(df['assignment1_submission'])
# 创建早完成者数据框
early_finishers = df[df['assignment1_submission'] < '2016-01-01']
# 查看早完成者数据
print(early_finishers.head())
现在,如何获取晚完成者的数据框呢?以下是一种方法:
# 创建晚完成者数据框
late_finishers = df[~df.index.isin(early_finishers.index)]
print(late_finishers.head())
还有其他方法可以实现,例如复制第一个条件并更改符号,或者使用连接操作,或者编写一个函数并使用.apply()方法在数据框中添加新列。
比较均值
pandas数据框对象具有多种相关的统计函数。如果直接在数据框上调用mean函数,可以看到每项作业的平均值。
print(df.mean())
现在,比较两个群体的均值。
print(early_finishers['assignment1_grade'].mean())
print(late_finishers['assignment1_grade'].mean())
这些均值看起来非常相似,但它们相同吗?什么叫做“相似”?这就是学生t检验发挥作用的地方。它允许我们形成备择假设(它们是不同的)和原假设(它们是相同的),然后检验该原假设。
进行t检验
在进行假设检验时,我们必须选择一个显著性水平作为我们愿意接受的偶然性阈值。这个显著性水平通常称为alpha。在本例中,我们使用阈值0.05(即5%)。虽然这很常用,但它实际上是相当任意的。
SciPy库包含许多不同的统计检验和基于Python的假设检验形式。我们将使用ttest_ind函数进行独立样本t检验,这意味着两组中的群体彼此无关。
ttest_ind的结果是t统计量和p值。后者(概率值)对我们来说最重要,因为它表示原假设为真的概率(介于0和1之间)。
from scipy.stats import ttest_ind
# 对第一次作业成绩进行t检验
t_stat, p_val = ttest_ind(early_finishers['assignment1_grade'], late_finishers['assignment1_grade'])
print(f"t统计量: {t_stat}, p值: {p_val}")
这里我们看到概率是0.18,高于我们的alpha值0.05。这意味着我们不能拒绝原假设。原假设是这两个群体相同,并且由于概率大于alpha,我们没有足够的证据确定性得出相反的结论。但这并不意味着我们已经证明群体是相同的。
检查其他作业
让我们检查其他作业的成绩。
assignments = ['assignment1_grade', 'assignment2_grade', 'assignment3_grade', 'assignment4_grade', 'assignment5_grade', 'assignment6_grade']
for assignment in assignments:
t_stat, p_val = ttest_ind(early_finishers[assignment], late_finishers[assignment])
print(f"{assignment}: p值 = {p_val}")
看起来在这份数据中,我们没有足够的证据表明群体在成绩方面存在差异。
理解p值
然而,让我们看看这些p值,因为它们可以告诉我们一些信息,这些信息可能影响未来的实验设计。例如,其中一项作业(作业3)的p值约为0.1。这意味着如果我们接受11%的偶然相似性水平,这将被认为具有统计显著性。作为研究者,这提示我这里有几点值得考虑后续跟进。例如,如果我们参与者数量较少(这里并非如此),或者这项作业与我们的实验有独特关联,那么可能需要进行后续实验以更好地理解这一现象。
p值的局限性
最近,p值因无法充分告诉我们正在发生的交互作用而受到批评,另外两种技术——置信区间和贝叶斯分析——正被更频繁地使用。p值的一个问题是,随着你进行更多测试,你很可能仅仅因为偶然性而得到一个具有统计显著性的值。
让我们通过一个小模拟来看看这一点。
首先,创建一个包含100列、每列100个数字的数据框。
df1 = pd.DataFrame([np.random.random(100) for x in range(100)])
print(df1.head())
现在,创建第二个数据框。
df2 = pd.DataFrame([np.random.random(100) for x in range(100)])
这两个数据框相同吗?也许更好的问题是:对于df1中的给定行,它与df2中的同一行相同吗?让我们看看。假设我们的临界值是0.1(即alpha为10%),我们将比较df1中的每一列与df2中相同编号的列,并报告p值小于10%的情况,这意味着我们有足够的证据说这些列是不同的。
让我们将其写成一个函数。
def test_columns(alpha=0.1):
num_different = 0
for col in df1.columns:
t_stat, p_val = ttest_ind(df1[col], df2[col])
if p_val <= alpha:
print(f"列 {col} 在 alpha={alpha} 水平上具有统计显著性差异 (p={p_val})")
num_different += 1
print(f"总共有 {num_different} 列不同,占总列数的 {num_different/len(df1.columns)*100:.1f}%")
test_columns()
有趣的是,我们看到实际上有很多列是不同的。事实上,这个数字看起来很像我们选择的alpha值。这是怎么回事?难道所有列不都应该相同吗?请记住,ttest_ind所做的只是检查两组数据在给定一定置信水平下是否相似。在我们的例子中是10%。你进行的随机比较越多,仅仅因为偶然性而相同的就越多。在这个例子中,我们检查了100列,所以如果我们的alpha是0.1,我们预计大约有10列会相同。
我们也可以测试其他alpha值。
test_columns(0.05)
使用非正态分布
为了好玩,让我们使用非正态分布重新创建第二个数据框。我随意选择卡方分布。
df2 = pd.DataFrame([np.random.chisquare(1, 100) for x in range(100)])
test_columns()
现在我们看到,所有或大多数列在10%的水平上检验都具有统计显著性。
总结

在本节课中,我们讨论了Python中假设检验的一些基础知识。我向大家介绍了SciPy库,你可以用它来进行学生t检验。我们还讨论了查看统计显著性时出现的一些实际问题。关于假设检验还有很多需要学习的内容,例如,根据数据的形状使用不同的检验方法,以及除了p值之外,还有不同的结果报告方式,如置信区间或贝叶斯分析。但这应该能让你对比较两个群体差异这一数据科学常见任务有一个基本的了解。
29:其他结构化数据格式 🌳


在本节课中,我们将要学习除了表格形式之外的其他数据抽象与结构化方式。我们将探讨网络图和树结构这两种常见的数据表示方法,并理解数据科学家如何在不同表示形式之间进行转换以应用已知的分析技术。
在之前的课程中,我们主要讨论了以表格形式构建数据,使用数据框作为我们的核心结构。这种形式建立在过去30年广泛使用的电子表格基础之上,是展示实例(有时称为观测值、实体,或在pandas中称为行)与其属性(通常称为特征或在pandas中称为列)之间关系的绝佳方式。
但重要的是要认识到,数据本身并非天生就是这种结构。表格数据表示法是我们数据科学家应用的一种抽象,它使我们能够更容易地构建具有特定属性的操作例程。这种抽象允许我们快速执行诸如汇总数据字段或应用机器学习算法等操作。
然而,还有许多其他形式的抽象需要我们了解,本节中我们将简要介绍其中几种。
网络图结构
我认为我们最常用的抽象形式之一是网络图。你可以将网络视为由具有属性的个体组成,并且这些个体与其他个体相连接,连接本身也具有属性。
以Twitter数据为例。个体用户拥有诸如Twitter用户ID、头像和姓名等属性,并且他们可以与其他个体连接。例如,我可能连接到Paul Resnick,因为我在Twitter上关注他;而他可能连接回我,因为他在Twitter上屏蔽了我。
因此,我们看到这些连接甚至可以具有某种方向性,连接上的属性(例如Paul是否屏蔽某人)在连接个体时是重要的区分因素,因为我们想要表示的是Paul屏蔽了我,而不是相反。
更一般地说,我们可以将网络视为由节点组成,这些节点可以代表任何事物:人、运动队、行星等,这完全取决于你的数据是什么。节点通过边连接,边可以是有向的或无向的。有时网络被称为图,有时节点被称为顶点,但从概念上讲,它们都是相同的东西。
对于你来说,真正重要的是要知道我们可以在不同表示形式之间进行转换。成为一名扎实的数据科学家意味着你能够将一种表示形式转换为另一种,以应用你可能已经掌握的技术。在与可能具有不同学科背景的各类客户和协作者交流时,这一点尤其重要。
对于网络,一种常见的第二种表示形式是邻接矩阵。在这种情况下,我们可能有两个矩阵,一个用于“关注”关系,一个用于“屏蔽”关系。行和列列出了我们可能具有关联的所有潜在人员。
在“关注”矩阵中,我的行和Paul的列之间可能有一个值为True,表示我关注Paul。而在“屏蔽”矩阵中,Paul的行和我的列之间可能有一个值为True。
我们可以像在本课程第一周所做的那样,在Numpy中表示这些矩阵。然后,我们可以使用诸如Python中的NetworkX等库来可视化这些网络,并应用特定算法来回答有趣的问题,例如:我和某位我不关注的院长(比如Bethiako)之间是否存在间接联系?
这在社会科学研究中被广泛用于理解影响力和社会联系,我本人在教育技术研究中也使用它来理解学习者的学习习惯如何影响他们的朋友。
但我不会在这里深入探讨网络,我只是想预示,这将是作为数据科学家工具包中的一种抽象,并且在这些抽象之间转换是很重要的。
树结构
实际上,你还会遇到一些特定的网络子类型。其中最常见的或许是树结构。在一般形式中,树可以被视为具有层次结构的网络,顶部有一个节点(我们称之为根节点),随后的节点出现在下面的层级中,并连接到上方的节点。
有时连接是单一的,即一个给定节点只能连接到一个上方的节点;有时连接是多个的,例如在家谱中,一个节点可以连接到两个父节点,通常用于表示遗传谱系。
实际上,尽管我们称这种结构为“树”,但我们并没有过多地模仿活树。通常活树的根在底部,而不是顶部。但我们确实使用了家谱中的术语,将节点称为具有兄弟姐妹、父母和自己的子女。所有这些语言仅用于描述节点与树中其他节点的关系(就节点的层级而言),而不是为了推断节点的其他属性(如节点具有的属性)。
不过,我们确实将树底部的节点称为叶节点。所以这有点奇怪地回到了森林学中树的概念。
像图和矩阵一样,树只是为我们提供了一种描述关系的语言,然后我们可以将其应用于不同的领域。
以下是树结构的一个例子:
一个你将了解到的树结构例子来自自然语言处理。在NLP中,通常将一段文本表示为解析树,这有助于消除歧义。
以下是Python最流行的文本处理库之一——自然语言工具包文档中的一个示例。他们选取了一个句子“the little bear saw a fine fat trout in the brook”,并根据英语语法构建了解析树。你可以看到叶节点是单词本身,每个单词都有一个父节点,即词性标签。
例如,“little”是形容词,“bear”是名词。这些节点都有一个共同的父节点。在本例中,它被归类为名词性短语,其父节点是名词短语。数据科学家然后可以使用这个解析树来找出单词之间的关系,例如哪些形容词指代特定的名词,这是产品评论分析中的一项重要任务。
总结与核心概念
如果有一个概念性的要点,我希望你能从本讲座中带走,那就是:这些结构是我们强加于底层数据的表示形式,我们从这些表示形式中推导出的数据含义,可能会根据我们决定如何应用该表示形式而改变。

数据科学家需要能够与广泛的其他利益相关者互动,因此需要能够灵活地构思和表示数据。
核心概念总结:
- 网络/图:由节点(实体)和边(关系)组成的数据结构。边可以是有向的或无向的。
- 代码表示(邻接矩阵示例):
import numpy as np # 一个简单的有向图邻接矩阵 # 行i到列j为True表示从节点i到节点j有一条边 adjacency_matrix = np.array([ [False, True, False], # 节点0连接到节点1 [False, False, True], # 节点1连接到节点2 [True, False, False] # 节点2连接到节点0 ])
- 代码表示(邻接矩阵示例):
- 树:一种特殊的无环连通图,具有层次结构,包含根节点、内部节点和叶节点。
- 公式/关系描述:对于树中的任意节点,从根到该节点存在唯一路径。每个节点(除根节点外)有且仅有一个父节点,但可以有零个或多个子节点。

本节课中,我们一起学习了网络图和树结构这两种重要的非表格数据抽象形式,理解了数据表示形式的灵活性及其在不同领域(如社交网络分析、自然语言处理)的应用,并认识到在不同表示形式之间进行转换是数据科学家的一项关键技能。
30:Python数据科学应用 🚀

概述
在本课程中,我们将学习如何更清晰地向各类数据利益相关者传达数据信息。课程将涵盖信息可视化的核心理论与最佳实践,并重点介绍使用Python的Matplotlib库创建图表的方法。
回顾与衔接
在第一门课程中,我们主要学习了如何操作数据,包括从CSV等结构化分隔文件中加载数据、使用Python的pandas和NumPy库清理数据,以及运用apply、aggregate、map和pivot等函数转换数据。如果您对这些工具的理解还不够扎实,建议您回顾第一门课程的内容。
课程核心目标
本课程的核心主题是理解如何更清晰地向对数据感兴趣的各类利益相关者传达数据信息。
您可能需要使用可视化手段与许多不同的利益相关者进行沟通。例如:
- 您需要与能够自行做出推断和解释的同行数据科学家分享数据的无偏表示。
- 您也需要能够与可能熟悉该领域但寻求具体可操作见解的管理层和高管分享数据背后的故事。
- 最后,您还需要能够像记者一样,向对您的特定数据知之甚少的大众进行沟通,他们不仅需要了解您所描述现象的概览,还需要理解您所采用方法的局限性。
信息可视化领域
本课程涉及的具体技术属于信息可视化这一广泛学科的一部分。信息可视化(简称InfoViz)是一个在过去20年随着计算能力发展而迅速崛起的领域,计算能力的提升使得快速构建引人入胜的视觉和交互图形成为可能。
虽然使用可视化来传递数据知识已有数个世纪的历史,但过去20年见证了针对不同受众展示信息的最佳实践的重要理论基础的发展。在本课程中,我们将重点关注这些内容。
第一周:理论与基础
在课程的第一周,我们将探讨一些相关理论和最佳实践,并介绍该领域的奠基性思想家,特别是Alberto Cairo和Edward Tufte。
我们将分析一些优秀和欠佳的可视化案例,并建立一套语言来描述我们如何自己创建有意义的可视化图表。这部分课程将以讲座为主,每周结束时会有一些阅读材料和同伴互评作业。
第二、三周:Matplotlib实践
在课程的第二和第三周,我们将深入探讨用于创建图表的事实标准Python库——Matplotlib。
需要说明的是,Matplotlib并非在Python中创建可视化作品的唯一方式,但它是seaborn和Bokeh等新兴网络工具的基础,为我们讨论提供了坚实的根基。
这两周的大部分时间将用于实际完成作业,辅以少量讲座来概述该工具包本身的工作原理。
您将需要处理混乱的原始数据,使用pandas或其他合适的技术进行处理,并将其渲染成有意义的解释性图像。再次强调,如果您对使用pandas还不熟悉,建议您回顾本系列的第一门课程。
第二周和第三周的作业将主要由同伴互评。
最终项目
最后,本课程将以一个小型项目作为高潮。与第一门课程类似,在这个项目中,您将学习一种新的可视化技术,并被要求扩展Matplotlib代码库以增强其功能。
这对于您的数据科学家成长之路至关重要,它意味着超越现成工具的使用,转而能够修改它们以支持理解数据和问题的新方式。
课程评估与期望
本课程与第一门课程非常相似,但评估方式主要是同伴互评。讲座仅能指导您完成作业的一部分。
您需要学会在讨论区向同伴提问,并通过网络搜索和Stack Overflow寻求新的信息。Stack Overflow上有专门针对Matplotlib问题的标签,鼓励您充分利用这个资源。
最后,对于本课程,我希望您已熟悉第一门课程的所有内容。
总结
本节课中,我们一起学习了本系列第二门课程的总体框架和目标。我们回顾了先修知识,明确了本课程将聚焦于数据沟通与可视化,并概述了后续每周将涵盖的理论、工具和实践项目。我期待与您一同开始这门专业课程的学习。
31:课程更新说明 🆕

在本节课中,我们将了解这门课程自2017年首次发布以来,在2023年版本中所进行的重要更新与调整。
概述
本课程最初于2017年录制,而您现在观看的是2023年发布的最新版本。您可以将本视频视为课程“第二版”的前言。
核心内容更新
上一节我们介绍了课程版本的背景,本节中我们来看看具体有哪些更新内容。
以下是本课程的主要更新点:
-
第一周课程内容保持不变:Edward Tufte和Alberto Cairo关于数据可视化的核心理论与原则,其重要性一如既往,因此这部分内容得以完整保留。
-
绘图库版本升级:我们用于在Python中创建图表的核心软件库——Matplotlib——已经发生了更新。课程中所有的Jupyter Notebook均已更新,以适配该工具包的最新版本。
- 代码示例:课程中的绘图代码已从类似
plt.plot(x, y)的旧式语法更新为当前推荐的最佳实践。
- 代码示例:课程中的绘图代码已从类似
-
视频内容重新录制:为了匹配更新后的Notebook,我们重新录制了部分教学视频。因此,在课程讲座中,您会看到2017年版和2023年版的讲师Chris交替出现。


- 新增教学内容:本次更新额外增加了一些全新的内容,主要包括以下两个部分:
- 在Matplotlib中进行地理绘图的方法。
- 如何结合Jupyter Lab的交互式小组件来构建简单的交互式可视化图表。



总结

本节课中我们一起学习了这门《Python用于数据科学实践》课程在2023年版本中的关键更新。这些更新确保了课程内容与当前最新的技术工具(如Matplotlib)保持同步,并新增了地理绘图和交互式可视化等实用主题。希望您能享受这个全新版本的课程。
32:Christopher Brooks教授介绍 👨🏫

在本节课中,我们将了解本系列课程的讲师Christopher Brooks教授的背景、研究兴趣以及他投身数据科学教育的初衷。

我是Chris Brooks,我是信息学院的研究助理教授。
同时,我也是学术创新办公室的学习分析与研究主任。
我是一名计算机科学家,主要研究领域是学习、学生成就与学生成功。
我的研究既涉及高等教育,也涵盖MOOCs等在线学习情境。
我童年时有一件事记忆犹新。
我的父亲是一名物理学家,因此我像许多孩子一样,也像我的孩子们现在所做的那样,会去大学体验校园生活及其提供的资源。
作为一名物理学家,他能够接触到万维网最早期的实例。
因此,我得以坐在那里,摆弄像NCSA Mosaic、早期版本的Netscape等工具。
我认为正是这种连通性,这种信息在科学世界中成为“一等公民”的理念,深深地吸引了我。
我们如今在数据科学中看到的这个新世界,其核心思想与此一脉相承。
现在,学习者甚至在学前阶段就开始培养相关能力,开始将数据视为探索世界的另一种方式。
对我而言,这一切始于很早的阶段。
在我年幼时,在大学里看到父亲的工作,并有机会接触计算机,这些经历为我播下了种子。
我很高兴能尝试将这种对数据和计算的热情带给尽可能多的人。
而这,正是这门《Python数据科学导论》课程的核心目标之一。


本节课中,我们一起了解了Christopher Brooks教授的学术背景及其与数据科学的渊源。他从小受家庭环境影响,接触到早期的互联网技术,从而对信息的连通性与力量产生了浓厚兴趣。如今,他致力于通过教育,将数据科学这种探索世界的新方式普及给更广泛的学习者。在接下来的课程中,我们将跟随他的指引,正式开启Python数据科学的学习之旅。
33:设计思维工具(Alberto Cairo) 📊

在本节课中,我们将学习Alberto Cairo在其著作《The Functional Art》中提出的一个设计思维工具——“可视化轮盘”。这个工具帮助我们理解在构建信息图表时面临的各种权衡与选择。
🎯 可视化轮盘概述
Alberto Cairo在其著作《The Functional Art》中,提供了一个用于思考信息图表设计权衡的工具。他称这个工具为“可视化轮盘”。


在这个概念化模型中,一个圆环有两个极点。顶部极点代表高度复杂的数据,这些数据在深层次上提供信息。而底部极点则提供更易访问的数据,但仅在浅层次上提供信息。
圆环中包含多个维度,这些维度描述了两种不同方法之间的权衡。虽然Cairo提供了许多有趣的维度,但需要注意的是,这是一个供设计师思考其可视化作品的工具,本身并非分析工具。
对于任何特定问题,你的需求可能会改变哪些维度是重要的,或者可能会引入你应该考虑的新维度。
Cairo实际上建议,我们在组织中的角色或专业背景也可能影响我们想要制作的图表类型。因此,我想问正在学习数据科学的你:你试图通过你的可视化作品触达谁?
🔍 深入探讨权衡维度
让我们深入探讨Cairo考虑的几个权衡维度。
以下是Cairo提出的几个关键设计权衡维度:
-
抽象与具象
高度具象的视觉作品使用现象的实际表现形式(如照片或绘图)来描述现象。随着表现形式变得不那么真实、更具概念性,重点就从具象转向抽象。![]()
![]()

- 功能性与装饰性
一个完全功能性的图表没有任何修饰,更接近数据的直接表现。而一个高度装饰性的图表则具有更多的艺术修饰。对于所有这些维度而言,并没有绝对的好坏之分。装饰可能会增加观众思考视觉作品、探索其细微差别并形成心理联想的时间,这可能会增加熟悉度和记忆性。

-
密度与轻量
这个维度与所展示的信息量有关。在科学视觉作品中有很多很好的例子,其中一些图表旨在深入研究,而另一些则旨在快速补充叙述。比较杂志中的信息图表(读者可能更深入地参与内容)与同一杂志中的广告(读者可能只会快速浏览广告)时,可以看到这个维度在起作用。这里有一个来自我研究领域的例子,由哈佛大学和麻省理工学院发布,描述了edX大规模开放在线课程平台上用户的访问模式。
![]()
花点时间研究这张图片。你能从这张图片中理解到什么?
如果我们考虑Cairo的第三个维度,很难不说这是一个相当密集的图表。图表的主要部分是一个散点图,它有两个带标签的轴,左侧和底部的两个子图(直方图)也是如此。直方图的轴可能有点令人困惑,因为它们与散点图的轴重叠,但改变了度量单位和测量方向。例如,散点图顶部的X轴使用“访问章节的百分比”作为度量单位,并且随着你向图的右侧移动而增加。然而,图表左侧直方图的同一个X轴是以“千人”为单位,并且随着你向图的左侧移动而增加。图表上叠加了一些红线,将散点图大致分成四个象限,但由于标签极少,这些象限最终代表什么有点不清楚。
无论这是否是一个好图表,它肯定会被视为密集图表的一个例子。
-
多维与单维
多维图表将现象作为一个整体来描述,并邀请观众探索现象的许多不同方面。而单维图表则专注于一个或少数几个项目,并以一种或多种方式探索它们。![]()
![]()
-
原创性与熟悉性
在现代世界,我们习惯于看到大量不同类型的信息图表,比如条形图和折线图。以这些表现形式进行思考在很小的时候就被教授。例如,我快5岁的女儿前几天从学前班带回家一页纸,上面显示了一个条形图,这是她和同学们在课堂上制作的,用来庆祝了解美国选举。全班同学聚在一起,投票决定他们吃什么零食,是巧克力冰淇淋还是薄荷巧克力片冰淇淋。当然,薄荷巧克力片冰淇淋赢了,正如人们所预料的那样。但我想说的是,这些是以图形形式思考数据的基本方式,现在很早就开始教授了。这使得它们对广大人群来说很熟悉。😊然而,情况并非总是如此。可以说,条形图对大多数人来说都相当熟悉,但具有更多原创性的图表包含需要用户解释或研究的元素。
这里有一张Charles Minard绘制的非常著名的图表。它描述了拿破仑1812年进军俄罗斯的情况。
![]()
花点时间研究这张图表。 broadly speaking,这张图表中可视化了五种不同类型的信息。你看到了这五种信息中的多少种?
以下是我看到的。首先,是地理元素,表现为沿途的各种河流和城镇。上部棕色条带的宽度代表拿破仑军队的规模,你可以看到它在战役开始时从422,000人缩减到法国人到达莫斯科时的仅100,000人。下部的黑色条带显示了拿破仑从俄罗斯的撤退,沿途有多个点,这些点与日期和摄氏温度相对应。我们看到随着军队规模缩小,条带急剧变细。
因此,Minard可视化中的五种数据包括:位置、方向、温度、军队规模和日期。
-
新颖性与冗余性
冗余性是指图表倾向于以多种不同方式讲述同一个故事。例如,你可能在条形图中使用条形的高度(带有坐标轴)以及颜色来强调最大的条形。这个维度有点棘手。你不想让读者感到无聊或使你的图表过于复杂,但你确实希望以支持他们理解你所描述现象的方式来编码信息。在这个维度上,新颖性是指仅以一种方式描述图表中的每个现象。

📈 可视化轮盘的应用与反思
在可视化轮盘中没有对错之分,轮盘的目的是帮助你理解和比较你可能采取的可视化方法。
作为一项反思活动,Cairo建议你可以沿着这些维度中的每一个来绘制你的想法,然后将这些点连接起来创建一个雷达图。
在这里,Cairo提供了两个可视化轮盘的实际应用示例。在左侧,你可以看到更强调复杂视觉作品,那些密集、多维且高功能性的作品。在右侧的可视化轮盘中,有更多的装饰、轻量和具象元素。
Cairo认为,左侧的轮盘更能体现科学家和工程师所做的工作。而右侧的轮盘则更能体现艺术家、平面设计师和记者所做的工作。科学家和工程师如何看待数据可视化与艺术家和记者如何看待之间的这种张力是一个有趣的话题,我们将在下周结束时重新讨论。
🎓 总结与预告
在本节课中,我们一起学习了Alberto Cairo的“可视化轮盘”,这是一个帮助我们理解和权衡信息图表设计不同维度(如抽象与具象、功能与装饰、密度与轻量等)的思维工具。我们探讨了每个维度的含义,并通过实例加深了理解。
可视化轮盘是我们用来更好地比较两种不同信息可视化方式的工具之一。在下一讲中,我们将更深入地探讨,并看看你如何使用可视化轮盘来评估自己的图表。下一讲,我们将更深入一些,并考虑Edward Tufte关于什么构成良好可视化的一些启发式方法。
34:数据可视化原则 📊

在本节课中,我们将学习爱德华·塔夫特提出的两个重要图形启发式原则:数据墨水比和图表垃圾。这些原则旨在指导我们创建更清晰、更有效的数据可视化图表。
爱德华·塔夫特是关于如何设计定量信息视觉展示的开创性人物。事实上,这是他最受推崇的一本书的标题。我们强烈建议你查阅他的著作。在这本书中,他介绍了两个有趣的图形启发式原则:数据墨水比和图表垃圾。
什么是启发式? 🤔
首先,什么是启发式?启发式是一个旨在指导决策的过程或规则。根据定义,它并非已知最优或完美,但在本质上是实用的。启发式原则旨在被遵循,直到你有理由偏离它们。
数据墨水比原则 ✍️
塔夫特的第一个图形启发式原则是数据墨水比。
塔夫特将数据墨水定义为一个图形的不可擦除的核心,即为了响应所代表数字的变化而排列的非冗余墨水。换句话说,数据墨水对于理解给定变量的意义是必不可少的。
塔夫特将数据墨水比定义为数据墨水量除以打印图形所需的总墨水量。
公式表示为:
数据墨水比 = 数据墨水量 / 打印图形所需的总墨水量
他并非真的建议我们去测量页面上的墨水量,而是建议我们移除那些没有为图表增添新信息的元素。
数据墨水比优化实例 🍽️
展示数据墨水比优化的最著名例子之一是由加拿大阿尔伯塔省埃德蒙顿市的信息可视化与设计公司 Darkhorse Analytics 完成的。他们整理了四个如何提高数据墨水比的例子,我们将在下一个阅读任务中链接这些例子。但在这里,我们将详细讲解第一个例子。
我们从一张关于食物项目及其卡路里含量的数据表开始。
以下是优化步骤:
第一步:移除不必要的背景图像
移除不需要的背景图像,因为它对理解图表没有价值。
第二步:移除条形图后的灰色背景
移除条形图后的灰色背景,因为它没有概念上的价值。
第三步:消除冗余元素
这包括移除图例,因为每个条形图都直接在坐标轴上标注了。同时,由于图像中已有大量关于食物项目卡路里的参考信息,可以精简标题和Y轴标签。
第四步:移除无价值的边框
这给我们留下了一个小得多的图像。有一些形成边框的粗线没有价值,也可以移除。
第五步:谨慎处理颜色
在图表中使用颜色是一个棘手的问题。本课程不会深入探讨色彩理论,但在与可能有色觉缺陷或色盲的人交流时,颜色是一个挑战。有一些应对策略,例如,过去的最佳实践是用图案(在图形中有时称为“阴影线”)代替颜色。但这会带来自身的挑战。相反,解决方案几乎总是移除所有颜色,只保留一种用于强调,并在文本中链接到该颜色。这样,即使脱离上下文查看图表的人也能感受到强调,而不会被数据的“彩虹”所淹没。因此,我们移除除培根以外的所有颜色,并假设文本中明确引用了该数据。
第六步:移除三维效果和阴影
三维条形图和投影也可以移除,因为它们没有增加额外价值。我们还可以移除整个“保龄球”效果。
第七步:评估并移除网格线
图像中仍然有很多网格线,但不太清楚这些网格线的价值是什么,所以我们将移除它们。网格线有时有价值,但通常只是一种干扰。现在,研究一下图像:我们感兴趣的数据点培根,与薯片或辣热狗之间的卡路里数差异是多少?仅仅移除线条并不会让阅读更容易。
第八步:直接标注数据值
由于这里的数据值相当简单,让我们直接在图形中标注每个条形图。现在,培根和薯片或培根和辣热狗之间的差异有多大?你当然需要做一些计算,但通过扫视图形并直接比较数字,你仍然可以快速估计出一个精确值。
优化成果 ✅
通过提高数据墨水比,我们不仅使图形更简单、更易读,而且增加了观看者看到的信息量。
我们鼓励你查看下一个阅读材料中链接的 Darkhorse Analytics 的其他例子,因为他们为各种不同的视觉元素(包括地图)进行了这种清理工作。

在下一讲中,我们将通过查看“图表垃圾”来继续探索塔夫特的启发式原则。
本节课总结:在本节课中,我们一起学习了爱德华·塔夫特提出的数据墨水比原则。我们了解了其定义和计算公式,并通过一个食物卡路里图表的逐步优化实例,学习了如何通过移除冗余背景、颜色、三维效果和网格线等非数据元素,来提高图表的清晰度和信息传递效率。记住,核心目标是最大化用于传达核心数据的“墨水”,让可视化更简洁、有力。
35:图形启发式:图表垃圾(Edward Tufte)📊

在本节课中,我们将要学习由爱德华·塔夫特提出的一个重要概念——“图表垃圾”。我们将了解图表垃圾的三种主要形式,并通过一项用户研究来探讨装饰性图表元素的实际效用。
爱德华·塔夫特向我们介绍的另一个启发式概念被称为图表垃圾。
塔夫特对图表垃圾的批评远比对其他形式的非数据墨水的批评更为严厉。事实上,他认为统计图表上的艺术装饰就像数据图形中的杂草。
他认为图表垃圾主要分为三种。第一种是无意的光学艺术。例如,图表特征的过度阴影或图案化,正如塔夫特在其著作《量化信息的视觉展示》中分享的这张经济学图表所示。
这些图案会使人的眼睛跳动并导致视觉疲劳。这种现象被称为莫尔条纹,这也是为什么在拍摄像本慕课这样的视频内容时,通常看不到人们穿条纹衬衫的原因,因为视频的低分辨率会加剧莫尔条纹问题。
塔夫特建议,与其为内容添加图案,不如直接在图表图形上添加标签。我们在Dark Horse Analytics的数据和G示例中看到了这种做法的应用。
第二种图表垃圾是网格。塔夫特认为网格作为数据墨水是不必要的,并且会与正在分享的实际数据产生竞争。细化、移除或降低网格线的饱和度,可以更容易地看到数据,而不会被页面上的大量线条所淹没。直接标记数据是减少这种图表垃圾的另一个好方法。
但我想重点关注的第三种图表垃圾,才是通常谈到图表垃圾时人们会想到的那种。塔夫特称之为“鸭子”。
广义上,他指的是非数据的创意图形,无论是线条艺术、照片,还是它们被包含在图表中。报纸和杂志是经常使用这类图像的地方。
一位知名的图形艺术家奈杰尔·霍姆斯曾使用“鸭子”以一种令人难忘且具有美学趣味的方式展示数据。他创作的最令人难忘的图像之一名为《钻石是女孩最好的朋友》,这张图出现在1982年的《时代》杂志上。

这张图显示了1978年至1982年钻石价格的趋势。虽然具体的美元金额很容易被遗忘,但由于图中女性腿部的形状,人们很容易记住趋势曾有一个峰值。
那么,“鸭子”真的是一种有用的启发式方法,还是仅仅因为鸭子本身令人难忘呢?
我曾是斯科特·贝特曼领导的一个团队的一员,我们希望更详细地理解这个问题,因此我们进行了一些用户测试。我已在本周的阅读材料中链接了完整的学术论文。
简而言之,我们向参与者提供了各种霍姆斯的图像,包括刚才展示的那张,这些图像可以被视为图表垃圾。我们也提供了一系列没有装饰性修饰、具有高数据墨水比的普通图表。
我们将24名受试者带入实验室,并分配他们查看霍姆斯风格或高数据墨水比条件下的图表。我们要求受试者通过一系列引导性问题来描述和总结这些图表。
随后进行了回忆测试,包括立即进行的测试,以及两到三周后的第二次测试。最后,我们使用眼动追踪来确定受试者在查看这些图表时注意力集中在何处。
我们的发现很有趣:虽然在立即测试时没有回忆差异,但两到三周后,对于展示“鸭子”图表垃圾的霍姆斯图表的回忆明显更好。此外,受试者主观上表示霍姆斯图表更令人愉快、更有吸引力、更容易记住且更容易记住细节。他们认为描述和记住霍姆斯图表的速度更快。未经修饰的设计在任何方面都没有比霍姆斯的设计表现得更好。
这是否意味着你应该在图表中使用装饰呢?也许吧。我不知道是否有人在更大、更多样化的人群中复制了我们的发现。当然,还有其他类型的图表垃圾,比如无意的光学艺术和网格,这些似乎确实是很好的启发式原则。但我认为这个故事还有更多内容有待讲述,更多细微差别有待厘清。
至此,你应该至少对设计师的观点,以及针对特定类型图表的用户研究有了一些思考。在你着手创建数据科学图形时,值得反思的不仅是你所使用的原则和你正在分享的结果,还包括你构思和创建图形的过程。
如今,借助像CrowdFlower和亚马逊土耳其机器人这样的众包服务,建立用户研究要简单得多。对于普通的数据科学家来说,测试更具装饰性的视觉效果是否可能更有效——无论是在时间、记忆性还是准确性方面,相比那些高数据墨水的图表——现在是非常合理的。
在下一讲中,我们将继续探讨塔夫特提出的另外两个概念:谎言因子和火花线。
本节课中我们一起学习了爱德华·塔夫特提出的“图表垃圾”概念,了解了它的三种主要形式:无意的光学艺术、冗余的网格以及被称为“鸭子”的装饰性图形。我们还通过一项用户研究探讨了装饰性元素对图表记忆性的潜在积极影响,并认识到在设计数据可视化时,需要在遵循简洁原则与考虑受众记忆和参与度之间做出平衡的判断。
36:图形启发式:谎言因子与火花线(Edward Tufte)📊

在本节课中,我们将要学习数据可视化领域的两个重要概念:火花线 与 谎言因子。这两个概念由爱德华·塔夫特提出,旨在帮助我们更清晰、更诚实地呈现数据。
爱德华·塔夫特对简洁和极简主义的强调赢得了大量追随者。他被广泛认为是图表和图形可视化领域最具影响力的作家之一。
在他的著作《美丽的证据》中,他提出了一个将图表极简主义推向新高度的想法。他建议不应将图表作为一个需要单独研究的独立产物,而是应该将其简化并嵌入到讨论的上下文中。
他认为,一个小的图形,例如一个时间序列折线图,可以快速传达更多信息,这对读者来说是无价的。他将这些元素称为“火花线”,并称它们为“数据词汇”,这是一种弥合文本与图形之间差距的有趣方式。
上一节我们介绍了火花线的核心思想,本节中我们来看看它的具体应用和形式。
他建议火花线不仅可以直接在文本中表示,还可以与它们所描述的数据一起嵌入到表格中。这个想法变得如此自然,以至于被最常见的电子表格分析软件之一——微软Excel所采纳。
以下是纳斯达克证券交易所的表格数据图片,显示了四只科技股在一个月内的开盘价。
花点时间思考一下这些数据。哪只股票本月表现不佳,呈下降趋势?哪只股票本月表现良好,呈上升趋势?
如果我在下面的单元格中添加火花线,情况就一目了然了。在这里,你可以立即看到苹果公司股价下跌,本月表现不佳;而IBM股价上涨,本月表现良好。至于亚马逊和英特尔,趋势不太明确,我们看到整个月内有一些波动。
现在,这些火花线并不能帮助我们回答谁的股价最高,或者谁赢利或亏损最大,但它们确实让我们对数据背后的趋势有了一个总体感觉。实际上,火花线用于那些趋势或分布特征很重要的数据。
以下是火花线在谷歌财经网站上使用的另一个例子。
请注意,你实际上可以通过这些火花线快速放大查看分布的各个部分,因此它们不仅仅是数据的表示,更是一种与之交互的方法。
火花线有许多不同的用途,定制化界面(如电子游戏)就是一个很好的例子。我认为一个现代的火花线变体,被称为“SparkT”,非常巧妙。它使用Unicode块字符在Twitter分配的140个字符内显示条形图。
例如,这里有一位用户将她四月份的睡眠时间以条形图的形式发布在推文中。
上一节我们探讨了火花线,接下来我们将了解塔夫特的另一个重要原则:谎言因子。
我想与大家分享的最后一个塔夫特原则叫做“谎言因子”。谎言因子是图形中显示的效果大小除以数据中实际效果大小的比值。这通常是无意识地为了帮助讲述一个故事,然而,它会对观察者产生误导。
谎言因子有很多不同的例子。我认为1979年《时代》杂志上的这个例子提供了一个很好的说明。图中展示了不同年份的油桶,用以表示六年间的石油价格,但观察者不清楚一个油桶的大小与其他油桶的关系。部分原因是这里存在透视元素。这些油桶是大小不同,还是仅仅因为有些在前、有些在后而显得不同?是油桶的体积代表成本的增长,还是油桶的高度代表成本的增长?
火花线和谎言因子是爱德华·塔夫特为我们提供的另外两个理解和传达数据的工具。

本节课中我们一起学习了爱德华·塔夫特提出的两个关键可视化概念。火花线 是一种将小型趋势图嵌入文本或表格的极简方法,能快速传达数据趋势。其核心思想是 data_word = text + sparkline。而谎言因子 则是一个衡量图形失真程度的指标,其公式为 lie_factor = (graphic_effect_size) / (data_effect_size),提醒我们在呈现数据时应保持诚实与准确。掌握这两个工具有助于我们创建更有效、更可信的数据可视化作品。
37:优秀信息图表的五个品质 📊

在本节课中,我们将学习阿尔贝托·开罗在其著作《真相的艺术》中提出的核心框架,该框架定义了优秀信息图表的五个关键品质。理解这些品质有助于我们创建更有效、更负责任的数据可视化作品。
概述:开罗的五品质框架
阿尔贝托·开罗的《真相的艺术》一书被认为是数据科学领域的必读之作。他将该领域众多专家的见解浓缩为一系列令人难忘的课程。虽然我们无法涵盖书中的所有内容,但本讲座将重点介绍该书的基础:一个用于评估信息图表质量的五部分框架。
重要的是,开罗指出这些品质并非彼此独立,而是相互关联的。因此,尽管我们倾向于简化分析,但在学习过程中请记住,不同的品质之间是相互作用的。
品质一:真实性 ✅
优秀可视化的首要品质是真实性。开罗承认,“真实”可能具有主观性,并且是一个连续体,某些事物可能比其他事物更真实,要确立绝对真实或许是不可能的。
但他认为,作为数据科学家,我们在维护真相方面有两项义务。
第一项义务是对自己诚实。 在清理和汇总数据时,我们所进行的活动是否会因为施加的限制而掩盖信息?我们应该明确考虑对数据所做的每一项修改,确保不会陷入他所说的“自我欺骗”。怀疑精神是数据科学家的一项重要技能。我们常常过于专注于寻找模式,以至于以不符合所描述现象本质的方式来简化或呈现数据。
第二项义务是对我们的受众诚实。 也就是说,在数据科学和信息可视化中,存在一些技术可以用来突出特定的数据片段。这确实是这些领域的一般目的。但如果我们剥夺了读者更全面探索现象的能力,就会引发怀疑和不信任。
开罗引用了一个由美国有线电视和电信协会发布的图表作为绝佳例子。该图表暗示放松监管后,有线电视公司的行业投资增加了四倍。然而,原图表存在几个问题:货币是否经过通货膨胀调整不明确;两个柱状图代表的时间跨度不同;图表缺失了1997和1998年的数据,且数据止于2003年,但信息却在2014年发布。
一个更诚实的视图是什么样的?开罗追踪数据并提供了一个折线图。从图中可以看到,监管之后确实有持续的投资,放松监管后投资略有下降,随后出现大幅飙升,接着从2002年后显著下降——而原图表排除了大部分下降时期的数据。
这里的教训是:虽然可能没有绝对真理,但存在比其他方式更真实的展示数据的方法。
品质二:功能性 ⚙️
需要考虑的第二个可视化品质是是否具有功能性。这同样是一个连续体。
我们之前见过一个例子,即由Dark Horse Analytics提供的关于NCRAT数据的幻灯片。如果你期望受众想要比较两个数据系列,直接标注它们是提高理解力的一种方式。
虽然有许多提高功能性的启发式方法,但我希望你们记住我们与Chart Junk进行的用户测试。我认为现在是理解特定情境下人类行为的激动人心的时刻。网络极大地提高了我们在不同人群中大规模测试社会和行为假设的能力,Cdflower和Mechanical Turk就是其中的两个例子。
理解某种可视化方法的功能性如何,不一定需要昂贵的民族志研究。现在通过众包直接测试假设要容易得多。
品质三:美观性 🎨
开罗分享的第三个可视化品质是是否美观。我认为这需要非常了解你的受众。基于人们的生活经历以及性别、文化等自身因素,会有不同的感知。构建一个美观的信息可视化体验涉及很多方面,这也取决于情境和背景因素。
例如,如果你在为一份报告制作图表,你选择的颜色、样式,甚至字体选择,可能更多地与美观性相关,而非功能性。但这些元素的相互作用是难以忽视的。
我想知道,美观性这个广泛的主题是否是“图表垃圾”与“记忆性”问题中的一个混淆因素?虽然塔夫特认为图表垃圾会分散注意力并产生误导,但贝特曼等人的研究表明,它可以使可视化作品更令人难忘。这种脱节是否是信息图表中美观性的问题?
关于美观性和信息,这里有一个或许能阐明这一品质的临别思考。最受欢迎的技术杂志之一《连线》以其整版的信息图表而闻名。在《连线》的网站上,他们将这些文章标记为“信息色情”,指的是他们希望读者在阅读文章时产生的那种发自内心的反应。
品质四:启发性 💡
开罗提出的第四个品质是信息图表应具有启发性。它们不应仅仅复制数据和表格,而应吸引观看者,使其产生“啊哈!”或“我发现了!”的时刻。
我主要在学术论文的背景下思考这一点,这是我大部分工作的领域。新手作者在这方面的一个常见问题是,为他们拥有的每一份数据都包含一个图表。这完全没有必要,会增加冗余并导致读者疲劳,但正确的图表能迅速阐明研究结果。
在我的领域,同行评审很常见,但评审者没有大量时间进行完整评审。在翻阅提交的论文时,评审者看到一幅跃然纸上的图像并立即获得洞见,这为他们理解论文其余部分要讲述的更细致入微的故事做好了准备。
品质五:教化性 🌟
开罗提出的最后一个可视化品质是信息图表应具有教化性。这与启发性不同。事实上,开罗认为这一品质由前四个品质(真实、功能、美观、启发)构成,但它是独立的,因为它增加了一个社会责任的维度。

总结与课程结束
“信息可视化在道德上是中立的吗?”这个问题可能没有明确的答案。但毫无疑问,开罗提出的优秀可视化的五个品质——真实、功能、美观、启发、教化——非常值得考虑。开罗还有更多优秀的作品,在本讲座之后,我布置了他的阅读材料以及一个讨论问题,鼓励你们就信息可视化与数据科学相关的道德中立性发表看法。

本周的课程到此结束。我们很快地涵盖了大量材料,但我真的相信,阿尔贝托·开罗和爱德华·塔夫特这两位作者,为我们将在课程第二周重点关注的实用数据科学技能奠定了坚实的基础。
38:Matplotlib入门 📊
在本节课中,我们将学习如何使用Python的Matplotlib工具包创建基本图表。我们将从了解Matplotlib的架构开始,然后逐步学习如何绘制散点图、折线图和条形图。
欢迎来到本课程的第二模块:“Python中的应用绘图、图表与数据表示”。

在第一模块中,我们借鉴了该领域有影响力的思想家(如Alberto Cairo和Edward Tufte)的观点,探讨了构建优质信息图表的一些基本原理。
在本模块中,我们将转向重点,学习使用基于Python的Matplotlib工具包构建基本图表的技能。
Matplotlib是一个用于表示和可视化数据的强大开源工具包。其创建者John Hunter深受Matlab编程环境的启发,并借鉴了其中的许多元素。
然而,从网络资源学习Matplotlib可能会有些令人困惑。它有一个传统的面向对象API,但也有一个类似Matlab的脚本模型。我发现,有时并不清楚为什么博客文章中的某个特定脚本能运行或不能运行。
我将尝试通过提供对该工具的结构化介绍,为您揭开一些神秘面纱。
在架构介绍之后,我们将直接深入创建图表。
在本模块中,我们将重点介绍一些基础知识:散点图、折线图和条形图。我们还将探讨如何为图表的各个部分添加标签以提高可读性,如何引导用户关注特定细节,以及如何在绘图环境中操作数据元素。
从现在开始,讲座内容会减少,模块的重点将更多地放在绘图和图表作业上。
在模块2、3和4结束时,您应该能够使用广泛的数据制作出具有出版质量的图表和图形。
让我们开始吧。
Matplotlib架构简介 🏗️
上一节我们概述了本模块的目标,本节中我们来看看Matplotlib的基本架构。理解其设计有助于更有效地使用它。
Matplotlib的核心是一个分层系统。最底层是后端(Backend),它负责在不同的输出设备(如屏幕或文件)上实际渲染图形。中间层是艺术家(Artist)层,它处理所有高级绘图元素,如坐标轴、线条和文本。最顶层是脚本(Scripting)层,即pyplot接口,它提供了类似MATLAB的快速绘图命令。
以下是两种主要的使用方式:
1. 面向对象接口(推荐)
这种方式显式地创建图形(Figure)和坐标轴(Axes)对象,然后在这些对象上调用方法。它更清晰、更灵活,适合复杂的图表。
import matplotlib.pyplot as plt
fig, ax = plt.subplots() # 创建图形和坐标轴对象
ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) # 在坐标轴对象上绘图
plt.show()
2. Pyplot脚本接口
这种方式使用plt模块下的函数(如plt.plot())隐式地操作“当前图形”和“当前坐标轴”。它更简单快捷,适合交互式环境和简单脚本。
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4], [1, 4, 2, 3]) # pyplot隐式管理图形和坐标轴
plt.show()
初学者常感到困惑的原因是混合使用了这两种风格。本教程将主要使用面向对象接口,因为它概念更清晰,代码更易维护。
创建基本图表 📈
了解了架构之后,我们现在可以开始动手创建图表了。本节将介绍三种最基础的图表类型。
以下是创建任何图表的基本步骤框架:
- 导入库:
import matplotlib.pyplot as plt - 准备数据。
- 创建图形和坐标轴:
fig, ax = plt.subplots() - 在坐标轴
ax上调用绘图方法(如ax.plot())。 - 自定义图表(添加标题、标签等)。
- 显示或保存图表:
plt.show()或fig.savefig()。
散点图
散点图用于展示两个连续变量之间的关系,观察数据的分布或聚类情况。
import matplotlib.pyplot as plt
import numpy as np
# 生成示例数据
x = np.random.rand(50)
y = np.random.rand(50)
fig, ax = plt.subplots()
ax.scatter(x, y) # 创建散点图
ax.set_title('随机散点图')
ax.set_xlabel('X轴')
ax.set_ylabel('Y轴')
plt.show()
折线图
折线图通常用于显示数据随时间或有序类别的变化趋势。
# 继续使用上面的fig, ax
years = [2010, 2012, 2014, 2016, 2018, 2020]
sales = [100, 120, 90, 150, 200, 180]
ax.clear() # 清除之前的图形
ax.plot(years, sales, marker='o') # 创建折线图,'o'表示数据点标记
ax.set_title('年度销售额趋势')
ax.set_xlabel('年份')
ax.set_ylabel('销售额(万)')
plt.show()
条形图
条形图适用于比较不同类别之间的离散数据。
categories = ['产品A', '产品B', '产品C', '产品D']
values = [23, 45, 56, 78]
fig, ax = plt.subplots()
ax.bar(categories, values) # 创建垂直条形图
# ax.barh(categories, values) # 创建水平条形图
ax.set_title('产品销量对比')
ax.set_ylabel('销量')
plt.show()
增强图表可读性 🎨
仅仅画出图形还不够,专业的图表需要清晰的标注和适度的美化来引导读者。本节我们来看看如何实现这一点。
以下是为图表添加常见标注和美化的方法:
- 添加标题和轴标签:使用
ax.set_title()、ax.set_xlabel()、ax.set_ylabel()。 - 添加图例:在绘图时指定
label参数,然后调用ax.legend()。 - 设置坐标轴范围:使用
ax.set_xlim([xmin, xmax])和ax.set_ylim([ymin, ymax])。 - 添加网格:使用
ax.grid(True)。 - 自定义颜色和样式:在绘图函数中使用
color、linestyle、linewidth等参数。
# 综合示例
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
fig, ax = plt.subplots(figsize=(8, 5)) # 设置图形大小
# 绘制两条线,并指定标签
ax.plot(x, y1, label='sin(x)', color='blue', linewidth=2)
ax.plot(x, y2, label='cos(x)', color='red', linestyle='--')
# 添加标注
ax.set_title('正弦与余弦函数曲线', fontsize=14)
ax.set_xlabel('X轴', fontsize=12)
ax.set_ylabel('Y轴', fontsize=12)
ax.legend()
ax.grid(True, linestyle=':', alpha=0.6) # 添加半透明虚线网格
plt.show()
总结 🎯
本节课中我们一起学习了Matplotlib的基础知识。我们首先介绍了Matplotlib的分层架构,并对比了面向对象和Pyplot脚本两种编程接口,推荐使用更清晰的面向对象方式。
接着,我们实践了三种基本图表的创建:观察关系的散点图、展示趋势的折线图以及进行比较的条形图。最后,我们学习了如何通过添加标题、标签、图例和网格等元素来显著增强图表的可读性和专业性。
掌握这些基础是迈向制作出版质量图表的第一步。在接下来的练习和模块中,你将有机会应用这些技能处理更复杂的数据和图表类型。
39:Matplotlib架构解析 📊

在本节课中,我们将要学习Matplotlib的核心架构。理解其层次结构对于高效使用这个强大的绘图库至关重要。我们将从后端层开始,逐步深入到艺术家层和脚本层,并探讨程序化与声明式可视化方法的区别。
🏗️ Matplotlib的层次结构
我们将像使用Pandas一样,在Jupyter Notebook中使用Matplotlib。Jupyter为Matplotlib提供了一些专门的支持,这是通过使用IPython魔术命令 %matplotlib inline 启用的。
在Jupyter Notebook中,IPython魔术命令是辅助函数,它们负责设置环境,以便启用基于Web的渲染。Matplotlib本身在Jupyter环境之外也能很好地工作,但本课程我们将重点介绍其在Web环境中的使用,部分原因是Jupyter在数据科学实践中已变得非常流行。
当你运行带有 inline 参数的魔术函数 %matplotlib inline 时,实际上发生的是Matplotlib被配置为在浏览器中渲染。这种配置被称为后端,Matplotlib有许多不同的后端可用。

一个后端是一个抽象层,它知道如何与操作系统环境(无论是操作系统本身还是像浏览器这样的环境)交互,并知道如何渲染Matplotlib命令。实际上,有许多不同的交互式后端,但也有被称为硬拷贝后端的后端,它们支持渲染为可缩放矢量图形(SVG)或PNG等图形格式。

并非所有后端都支持所有功能,尤其是交互式功能,我们将在本课程后面部分简要提及。
🔧 后端层
让我们导入Matplotlib库(我们将以 plt 作为别名导入),并调用 plt.get_backend() 来确保我们正在使用正确的后端。
import matplotlib.pyplot as plt
print(plt.get_backend())
你会注意到Matplotlib有很多非Python风格的命名约定。特别是,你将使用 get 和 set 模式来访问变量,这种访问器模式在Java等语言中更常见。这只是你需要习惯的事情,但请记住,在Jupyter Notebook中你可以按Tab键进行自动补全,这可以帮助你找到所需的函数。
现在,我们不会在后端层花费太多时间,但了解它很重要。例如,如果你在Mac OS上,使用Matplotlib时的默认后端可能是 MacOSX,而在Linux上,默认后端可能是 GTK。
🎨 艺术家层
接下来我们将花费大部分时间的层是艺术家层。艺术家层是围绕绘图和布局原语的抽象。
可视化的根源是一组容器,其中包括一个图形对象,该对象包含一个或多个子图,每个子图又包含一个或多个坐标轴。最后一个对象——坐标轴,是你最常与之交互的对象,例如更改给定轴的范围或在上面绘制形状。
需要简要说明一下,尤其是在这个有来自世界各地、英语可能不是其母语的学员参与的全球课堂中。Matplotlib严重依赖 axes 对象,它是 axis 的复数形式。但Matplotlib也有一个 axis 对象,而一个 axes 实际上由两个 axis 对象组成:一个用于X(水平)维度,一个用于Y(垂直)维度。因此,你可能会听到我比平时更刻意地发音这两个词(axes 和 axis),以帮助大家更容易理解。
艺术家层还包含原语和集合。这些是基础的绘图项,比如矩形、椭圆或线条,以及项的集合,例如路径,它可能将多条线组合成一个多边形形状。集合很容易识别,因为它们的名称往往以 collection 结尾。
值得花点时间看看有哪些类型的艺术家可用。以下是Matplotlib文档中关于艺术家的一张图片。你会看到艺术家对象有很多子对象,其中一个叫做 patches.Patch。这个名字来源于该项目的Matlab根源,一个 Patch 是任何具有面颜色和边缘颜色的二维对象。Patch 类的子类就是我们讨论过的原语。
好的,所以Matplotlib有两层:后端层,它了解底层图形例程,可以渲染到屏幕或文件,我们的大部分工作将使用 inline 后端;以及艺术家层,它描述原语、集合和容器,知道图形如何由子图组成,以及对象在给定坐标轴坐标系中的位置。
有了这些,我们实际上就可以开始构建图形并渲染它们了。

📝 脚本层

但还有一层对我们数据科学家来说尤其重要,这被称为脚本层。你看,如果我们要编写一个使用Matplotlib的应用程序,我们可能永远不会关心脚本层,但这一层有助于简化和加速我们与环境的交互,以便快速构建图表。
坦率地说,它通过为我们做一堆“魔法”来实现这一点。一个人能否有效使用Matplotlib,通常取决于他对脚本层这种“魔法”的理解程度。我们在本课程中使用的脚本层叫做 pyplot。
这就描述了Matplotlib的层次结构:有一个处理实际绘图的后端,后端之上有一组描述数据如何排列的艺术家,以及一个实际创建这些艺术家并将它们编排在一起的脚本层。
🤔 程序化与声明式可视化

在继续之前,我还想再讨论一个话题,那就是程序化与声明式可视化库(以及更广泛的用户界面)之间的区别。
pyplot 脚本层是一种构建可视化的程序化方法,我们告诉底层软件我们希望它执行哪些绘图操作来渲染我们的数据。
也存在声明式的数据可视化方法。HTML就是一个很好的例子。它不是向后端渲染代理(对于HTML来说是浏览器)发出一连串的命令,而是将文档格式化为文档中关系的模型,通常称为DOM(文档对象模型)。这是创建和表示图形界面的两种根本不同的方式。
例如,流行的JavaScript库D3.js就是声明式信息可视化方法的一个例子,而Matplotlib的 pyplot 则是程序化信息可视化方法的一个例子。
在下一讲中,我们将开始动手制作一些图表。但在那之前,我希望你查阅一下John Hunter和Michael Droettboom的一篇文章。虽然现在有点过时,但这篇文章发表在《开源应用架构》第二卷中,为Matplotlib的架构提供了更多背景信息。
📚 总结

本节课中,我们一起学习了Matplotlib的三层核心架构:后端层负责与系统交互和渲染;艺术家层定义了图形、坐标轴、原语和集合等绘图元素;脚本层(特别是 pyplot)提供了简化、程序化的接口来快速创建图表。我们还探讨了程序化与声明式可视化方法的区别。理解这些层次将帮助你更高效、更灵活地使用Matplotlib进行数据可视化。
40:Matplotlib基础绘图 🎨

在本节课中,我们将要学习Matplotlib库的基础知识,了解其架构的不同层次,并掌握使用脚本层进行基本绘图的方法。我们将从设置后端开始,逐步探索如何创建图表、添加数据点以及理解Matplotlib在幕后是如何工作的。
后端设置
在之前的Matplotlib版本中,我们会在Jupyter中使用一种名为“NB A”的后端样式。但现在情况已经改变,我们使用一个内联后端引擎。你不需要深入了解其细节,但知道可以自行查看是有帮助的。
让我们导入Matplotlib并检查今天将使用的后端。
import matplotlib as mpl
mpl.get_backend()
运行上述代码会告诉我们正在使用哪个模块。今天,我们使用的是 backend_inline 模块,这是目前在Jupyter中通常使用的后端。
开始绘图之旅:使用plot函数
我们将通过使用 plot 函数制作图表来开始我们的绘图之旅。一个图表包含两个轴:底部的x轴和垂直的y轴。
首先,导入pyplot脚本层,通常别名为 plt。我们将要运行的、针对pyplot模块的所有函数,都是其架构中脚本层的一部分。
让我们通过查看文档字符串来了解一下 plot 函数。
import matplotlib.pyplot as plt
plt.plot?
运行此命令后,Jupyter会为我们查找并显示该函数的文档字符串。你可以看到方法签名 plot.plot,它包含 *args、scalex、scaley 和 **kwargs 参数。文档内容非常详尽,与在线文档一致。
理解函数参数
对于不熟悉Python函数声明的人来说,带有 *args 和 **kwargs 参数的函数可能有些晦涩。*args 表示该函数支持任意数量的未命名参数。**kwargs 则表示它支持任意数量的命名参数。这使得函数声明非常灵活,因为你可以传入任意数量的参数,无论是否命名。但这使得我们难以知道应该使用哪些合适的参数。
继续阅读文档,我们看到在这种情况下,参数将被解释为 X, Y 对。让我们从这里开始。
绘制第一个数据点
让我们尝试在位置 (3, 2) 绘制一个数据点。
plt.plot(3, 2)
返回值是绘图对象本身,Jupyter会自动在笔记本中显示并渲染它。然而,我们没有看到任何数据点,这有点奇怪。原来,plot 的第三个参数应该是一个字符串,用于指定我们希望如何渲染该数据点。让我们使用一个句点 . 来表示一个点。
plt.plot(3, 2, '.')
现在,我们的数据点显示出来了。文档告诉我们不同的字符将如何渲染,这里我们只是使用句点作为标记。
关于后端和交互性
你会注意到,后续对 plot 的调用并没有更新我们之前的可视化。我们使用的后端不是交互式的,因此这些后续调用会在笔记本中创建新的图表作为新的单元格。这是迭代式探索数据的一种便捷方式。
Matplotlib的架构:脚本层与艺术家层
Matplotlib库的一些混淆之处往往源于此。在上节课中,我解释了存在一个艺术家层,它包含带有子图、坐标轴和数据点的图形,它们都作为“补丁”渲染在这些坐标轴上。但我们在这里并没有实际看到任何这些对象,我们只是在一个名为 plot 的模块上调用了一个函数。这是怎么回事?
虽然pyplot脚本接口为你管理了许多这些对象,它会跟踪最新的图形、子图和坐标轴对象,但它实际上隐藏了这些方法的一些细节。pyplot模块本身有一个名为 plot 的函数,但它将对此函数的调用重定向到当前的坐标轴对象。这造成了显著的学习曲线,你会在许多网络教程和Stack Overflow上看到人们对于这两种显示图形的方法感到困惑。
在数据科学工作流中,我认为更常见的是像我们刚才那样使用这个脚本层,但花点时间了解一下另一种更冗长的面向对象方法也是值得的。
面向对象方法(艺术家层接口)
实际上,虽然有些人称之为Matplotlib对象API,但我认为更准确的说法是直接与艺术家层接口。
下面我将做一个小演示。首先,我将导入一个名为 FigureCanvasAgg 的新后端,然后引入 Figure 对象,这是我们将要使用的顶级对象。
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.figure import Figure
fig = Figure()
canvas = FigureCanvasAgg(fig)
然后,我们可以直接向这个图形添加一个子图。我们将在未来的课程中更多地讨论子图。这里的数字 1, 1, 1 实际上意味着我们想添加一个图。add_subplot 的返回值是坐标轴对象,它包含绘图方法,我们通常称之为 ax。
ax = fig.add_subplot(111)
现在,我们可以开始绘图了。我们可以像往常一样在坐标轴上绘图。
ax.plot(3, 2, '.')
最后,我们有了这个图形,但我们的后端实际上无法直接在Jupyter Lab中渲染它。记住,我们这里的后端是 FigureCanvasAgg。所以我们必须将其保存到一个PNG文件。
canvas.print_png('test.png')
执行后,你看不到任何东西。所有这些对象都已创建,但除了保存在文件中,它们没有在任何地方渲染。我们可以使用一个快速的HTML单元格魔法来执行并查看渲染的图像。
%%html
<img src='test.png' />
这比使用脚本层要多做很多工作,但你可以看到效果是相同的。要在这里使用HTML单元格魔法,我们只需在单元格开头加上 %%html,这告诉Jupyter将剩余的代码渲染为HTML。
脚本层的工作原理
重要的是要注意,脚本层并非魔法,它只是为我们做了一些幕后工作。例如,当我们调用 plt.plot 时,脚本层实际上会查看当前是否存在图形,如果没有,则创建一个新的。然后它返回该图形的坐标轴对象。
我们不必存储任何这些,因为我们总是可以使用 gcf 函数(代表“获取当前图形”)来访问当前图形。类似地,我们有另一个函数 gca(获取当前坐标轴),它将返回当前的坐标轴以供渲染。
让我们用pyplot创建一个新图形,获取坐标轴并设置一些XY限制。
plt.figure()
plt.plot(3, 2, 'o') # 使用圆圈标记
ax = plt.gca()
ax.axis([0, 6, 0, 6])
现在我们可以看到点被渲染了,它看起来有点不同,并且如果你回头看,x轴和y轴的坐标值已经被约束了。
组合图表的基础知识
你可以在任何时候向坐标轴对象添加“艺术家”。当我们调用 plot 函数时,pyplot实际上正在为我们做这件事。它根据字符串确定我们想要的形状、与该形状关联的位置,并创建一个“补丁”对象并将其添加到坐标轴。
如果我们在渲染之前对 plot 函数进行后续调用,数据实际上将作为不同的序列添加,并适当着色。
plt.figure()
plt.plot(1.5, 1.5, 'o')
plt.plot(2, 2, 'o')
plt.plot(2.5, 2.5, 'o')
现在你可以看到我们有三个不同的点排成一条线,并且我们的坐标轴被约束以适应这些数据。
探索坐标轴的子对象
很多复杂性对你来说是隐藏的,但你可以通过坐标轴对象更进一步,甚至可以获得坐标轴包含的所有子对象。
我们可以使用 get_children 函数来实现这一点,我想向你展示一下这是什么样子,因为我认为知道它的存在是很有趣的,即使你不会经常使用它。
ax = plt.gca()
ax.plot(1.5, 1.5, 'o')
print(ax.get_children())
对于这个图像,我们实际上有很多东西:一个 Line2D 对象、一堆 Spine 对象、一个 XAxis 对象、一个 YAxis 对象。你可以在文档中查找每一个。在这种情况下,包含在坐标轴中的 Line2D 对象实际上是我们的数据点。我们还有这些 Spine 对象,它们是框架边框的实际渲染,包括所有刻度标记、两个坐标轴对象和一堆文本(图表的标签)。甚至还有一个 Rectangle,那实际上是坐标轴的背景。

总结与展望
本节课我们一起学习了Matplotlib的基础知识。我们了解了如何设置后端、使用 plt.plot 函数创建基本图表、理解脚本层与面向对象方法的区别,以及如何探索图表的内部结构。
我们进行了一次关于如何高效使用Matplotlib并制作第一个图表的快速导览。实际上,你可能不会花很多时间去获取单个“艺术家”或与“Spine”对象交互,但如果你愿意,你可以这样做。这个系统并非魔法,我们已经走过了如何更好地调查以理解底层实际发生的过程。
在下一讲中,我们将介绍Matplotlib中一些不同的内置图表选项。
41:散点图绘制 🎯

在本节课中,我们将要学习如何使用Matplotlib库绘制散点图。我们将从回顾上一讲的知识点开始,然后深入探讨散点图的基本绘制方法、自定义属性,以及如何操作和理解Matplotlib的底层结构。
回顾与基础概念
上一节我们介绍了plot函数,它生成一系列点并在坐标轴对象上渲染。
Matplotlib在脚本层实际上有许多有用的绘图方法,对应我们可能想要使用的不同图表类型。我们不会在这里介绍所有方法,但会涉及几个主要的。
无论如何,我们需要记住在上一讲中学到的几点内容。
首先,Pylot使用gcf函数获取当前图形,然后使用gca函数获取当前坐标轴。Pylot会为你跟踪所有这些坐标轴对象。但不要忘记它们的存在,我们可以在需要时获取它们来执行操作。
其次,pyplot模块的API与坐标轴对象的API完全一致,因此你可以直接调用pyplot模块的plot函数,这实际上是在底层调用坐标轴对象的plot函数。
最后,请记住Matplotlib中大多数函数的声明都带有一组开放的关键字参数。你可以使用许多不同的属性来控制这些关键字参数,但你在文档中不会为每个函数找到它们的描述。这可能有点令人沮丧,因为你最终只能通过示例来学习。
本周我将开始穿插使用一些这样的参数,以演示Matplotlib文档及其价值。你可以使用问号查看每个函数的文档。
考虑到这些,让我们继续首先讨论散点图。
散点图基础
散点图是一种二维图,类似于我之前展示的线图。scatter绘图函数将X轴值作为第一个参数,Y轴值作为第二个参数。
如果两个参数相同,我们会得到这些点沿对角线整齐排列的效果。
和之前一样,我将导入Matplotlib的pyplot模块作为plt,并引入该脚本接口。
我还将导入NumPy,稍后会用到它。Matplotlib实际上非常依赖这些NumPy数组,因此你会看到很多使用它的例子。
我要做的第一件事是创建一些模拟数据。我将创建一个包含八个数据点的列表,从1到8,然后从中创建一个数组。接着,我将使其对称,因此y值将与x相同。
然后,我想为这个单元格创建一个全新的图形,并让Matplotlib生成一个散点图。这与plt.plot函数类似,但此处的底层对象不是Line2D。
我们可以看到这些点的整齐排列,对称且沿对角线分布。X轴和Y轴已被Matplotlib自动约束以适应我们的数据。
现在,scatter不像plot那样将项目表示为系列,相反,我们实际上可以传递一个颜色列表,该列表对应于给定的点。
让我们使用一些基本的列表运算来创建一个新列表,长度略少于我们所需的数据点数,并将所有这些值设置为绿色,然后我们将最后一个点设置为红色。
我们使用相同的数据1到8。现在我还想创建这个列表:绿色、绿色、绿色……一直到最后一个红色。我本可以直接在这里硬编码这个列表,但我在文档中写出来是为了让你能看到。你也可以只取一个包含字符串“green”的单元素列表,然后乘以我们数据长度减1,这将在这里创建七个绿色元素,然后我只需向其附加一个红色。我只是想向你展示,你可以在这里使用正常的NumPy参数。
然后我们将创建一个新图形,这次当我们调用scatter时,我们将传入X和Y。我将在这里展示一个新参数S代表大小,然后我将传入我们为这些点所需的颜色列表。
我们看到我们得到了所有这些点。它们稍微大了一点。除了最后一个点是红色的,其他都是绿色的。
数据组织模式
将数据点分离到列表中是Matplotlib中一个相当常见的模式。你有一组项目,但它们实际上被描述为跨多个不同数据源的相同切片。
熟悉面向对象编程的人可能期望每个数据点实际上都有自己的实例来表示,该实例封装了它的所有属性。例如,一个点具有x值、y值、颜色和大小。但这里的情况并非如此。
这就是为什么了解我们在第一门课程中讨论过的列表推导式和Lambda表达式会很有用。同样重要的是zip函数和列表解包。因为这在Python数据科学领域非常常见,我将在这里稍微离题讨论一下。
回想一下,zip方法接受多个可迭代对象,并根据索引匹配元素,从中创建元组。因此,如果我们有两个数字列表,zip将取每个列表的第一个元素创建一个元组,然后取每个列表的第二个元素创建另一个元组,依此类推。
还要记住,zip具有惰性求值,因为它在Python 3中实际上是一个生成器。这意味着如果我们想看到迭代zip的结果,我们需要使用list函数,我们称之为“实现”生成器。
让我在这里演示一下。我们将使用zip函数创建一个新的生成器,传入两个数字列表。当我们将这个生成器转换为列表时,我们会看到一个成对元组的列表。
我有两个列表在这里,1到5和6到10。我把它们传入zip,其返回值实际上是一个生成器。然后在这里我将打印该生成器的列表,这将“实现”生成器,它实际上会遍历生成器并创建新列表,并产生这个输出。
现在,我们可以再做一次。但这次,我们可以使用解包而不是列表。我有完全相同的生成器函数,创建了完全相同的生成器,但这次我调用print,并且我使用zip_generator加星号。在这种情况下,它实际上将该集合解包为位置参数。你会看到我们得到了所有相同的对:(1,6), (2,7), (3,8)……但它们不是一个单一的列表,而是单独的参数。
将点数据存储为元组很常见,你应该熟悉能够将数据转换为此形式以及从此形式转换回来。
如果我们想将数据转换回两个列表,一个包含X分量,一个包含Y分量,我们可以使用带zip的参数解包。当你向函数传递一个列表或可迭代对象并在其前面加上星号时,每个项目实际上都会从可迭代对象中取出并作为单独的参数传递。
好了,这是一个关于一个有价值工具的简短离题,在Python的实际数据操作和清理中你会经常看到它。
绘制多个数据系列
现在让我们回到绘图。让我们取这两个列表,并使用scatter将它们作为一个新图形绘制。我们不是将它们绘制为一个数据系列,而是对列表进行切片,并将它们绘制为两个数据系列。
与之前为每个数据点设置颜色不同,如果我们愿意,我们可以用单个值为每个系列着色。我们还可以做一些事情,比如更改整个系列的颜色或透明度,当然也可以添加描述性标签。
在这个例子中,我将创建一个新图形。然后我将用红色绘制一个“高个子学生”的数据系列,使用X和Y的前两个元素。所以我只取之前的x变量和y变量,记住这是切片符号,它只取出两个元素,即前两个元素。我将大小设置为100,颜色设置为红色。如果你像这样只传递一个项目,这将把所有项目着色为红色。然后我设置了一个标签“tall students”。然后我将为这里的“矮个子学生”做同样的事情,但实际上是从第二个元素开始,也就是第三个位置,因为我们从零开始索引。我将把它设为蓝色。让我们执行它。
我们可以看到图像中有两个红色的高个子学生,然后所有这些蓝色的矮个子学生。你会注意到标签目前没有显示,它实际上只是为每个系列内部保存着,我稍后会讲到这一点。但现在让我们更多地讨论坐标轴的属性。
坐标轴标签与图例
坐标轴通常有标签来解释它们代表什么或它们描述的单位。图表也往往有标题。所以让我们把这些放到位。由于pyplot镜像了坐标轴对象的大部分API,我们可以直接在pyplot对象上进行这些调用。
所以我要做完全相同的事情,创建一个新图形。我将创建我们的两个散点图。现在如果我只想添加一个标签,我将添加X轴标签。所以它会出现在底部。“孩子踢球的次数”是我在这里写的。然后我们也可以为Y轴添加一个标签。我们可以直接使用plt.xlabel、plt.ylabel和plt.title。在底层发生的是,plt正在调用gca获取当前坐标轴,然后调用set_xlabel和set_ylabel。
让我们也添加一个图例。你会看到Matplotlib将把我们的图例放在左上角,并在那里放置一些关于我们学生的视觉信息。所以我这里有完全相同的代码,只是复制粘贴了它。但在这里我将添加图例。要做到这一点,我们只需调用plt.legend。它指示我们应该添加该图例,并使用数据上已存在的标签。所以如果你有那些标签,Matplotlib就会使用它们。
如果你查看图例的文档,你会看到有许多不同的参数,其中一个叫做loc,值为4实际上会将其放在坐标轴的右下角。我们还可以去掉边框,添加标题,并进行任意数量的不同视觉转换。
所以我在这里取了完全相同的代码,现在对于plt.legend,我不是告诉Matplotlib随意处理,而是将其位置设置为4。我说我想要边框关闭,所以frameon=False,并且我为图例设置了一个标题。
探索Matplotlib底层结构

到目前为止,我向你展示的所有东西都是与坐标轴关联的可渲染对象。例如,图例是一个“艺术家”,这意味着它可以包含子对象。
让我们利用这一点,写一个小例程来递归地遍历艺术家及其子对象列表。
这是一个有趣的活动,我喜欢用它来真正演示Matplotlib在底层是如何工作的。所以我们将从Matplotlib导入Artist类。再次强调,在你进行常规数据科学工作时不会这样做,但有时你想调试一个图表或图形,并想真正理解它在底层是如何构建的,我认为这非常强大。所以我们只是要创建这个名为rec_g的递归函数。它将接受某个艺术家和某个深度参数。它将检查传入的对象是否是艺术家,如果是,它将只打印出它的名字,然后它将使用get_children递归地调用自身。所以我们在这里使用了一些有趣的东西。我们打印出几个空格,然后是深度,然后是艺术家的名字。所以我们会得到一个漂亮的缩进列表。
最后,让我们在传奇艺术家上调用这个函数,看看图例实际上是由什么组成的。
你可以看到那个简单的小图例,一个小标题,一个小盒子,几个点,作为一个艺术家,实际上是由许多不同的用于绘制的偏移框,以及文本区域和路径集合组成的。
我想让你从这里带走的是,Matplotlib所做的事情并没有什么神奇之处。对脚本接口的调用只是创建图形、子图和坐标轴。然后用各种艺术家填充这些坐标轴,这些艺术家是后端渲染器,被输出到屏幕或我们之前看到的文件等其他媒介。
总结与展望
现在,虽然你95%的时间都会愉快地在脚本层创建图表和图形,但为了另外5%的时间,理解库在底层的工作原理很重要。
你将使用这些知识的时候是当你想要对图表有非常精细的控制,并创建你自己的图表函数时。在本课程结束时,你将有机会这样做。
在下一讲中,我们将回到线图,再创建几个,然后进入条形图。

本节课中我们一起学习了如何使用Matplotlib绘制散点图,包括基本绘制、自定义颜色和大小、处理多个数据系列、添加坐标轴标签和图例,并初步探索了Matplotlib的底层对象结构,为后续更复杂的图表制作打下了基础。
42:折线图绘制 📊
在本节课中,我们将学习如何使用Matplotlib库绘制折线图。折线图是数据可视化中常用的图表类型,特别适合展示数据随时间或其他连续变量的变化趋势。我们将从基础绘制开始,逐步介绍如何添加多个数据系列、设置图表样式、处理日期数据以及添加高级功能,如填充区域和数学公式标签。

📈 折线图基础
上一节我们介绍了散点图,本节中我们来看看折线图。我们已经多次见过折线图,但它们看起来像散点图。事实上,我展示的第一个图表就是折线图,因为我们使用plot函数创建折线图。它会渲染多个数据系列,并用线条连接每个系列中的数据点。在第一个例子中,我只展示了一个点,所以没有看到任何线条。
让我们直接开始。与之前看到的散点图相比,折线图有几个新特点。

首先,我们只向plot函数提供了Y轴值,没有提供X轴值。然而,plot函数足够智能,能够理解我们的意图,并使用数据系列的索引作为X值。这在需要快速绘图时非常方便。
其次,我们看到图表识别出这是两个数据系列,并且来自不同系列的数据点和连接线颜色不同。这与散点图不同,散点图需要我们直接标记这些线条。
🛠️ 绘制多系列折线图
现在,我们将导入Matplotlib的Pyplot模块,并使用NumPy创建数据。
import matplotlib.pyplot as plt
import numpy as np
我们将创建一个线性增长的数据系列和一个二次增长的数据系列作为演示。
linear_data = np.arange(1, 9)
exponential_data = linear_data ** 2
这次,我将使用迷你格式化语言来描述标记和线条的渲染方式。之前你看到我使用O,它只创建圆形标记。但我们可以在前面加上破折号,以在圆形标记之间使用实线。
注意,我传递数据后跟每个系列的格式化字符串。我们将看到两个数据系列的结果:底部的线性系列和顶部的二次系列。
plt.plot(linear_data, '-o', exponential_data, '-o')
这看起来有点奇怪,值得研究一下。这里我调用plot,传入数据,告诉图表如何渲染数据。然后我传入另一组数据,并告诉它如何渲染。我们可以为任意多的数据系列重复此操作。
我们可以看到这两个数据系列,二次和线性数据,被绘制在那里。
➕ 添加额外线条
现在很容易扩展这个图表并添加任意新线条。例如,我将使用--r,它告诉plot添加红色的虚线。
plt.plot(linear_data, '-o', exponential_data, '-o')
plt.plot([1, 2, 3, 4], '--r')
我必须把它放在后续行吗?不,我可以直接在这里添加额外的参数。我只是想展示你可以这样做。
现在你可以看到我们有两个最初的数据系列,但现在我们有第三个系列,它基本上没有标记,只有虚线。
你注意到我做了什么吗?我没有调用plt.figure。默认情况下,Pyplot脚本接口会在你尝试做某事时调用plt.gcf。如果没有图形,它会为你创建一个。默认配置的Jupyter Lab会在每个单元格执行后自动关闭图形。这意味着Pyplot每次都会为我们创建一个新图形。这是一个很大的时间节省。
🏷️ 添加标签和图例
我们可以使用常规的轴函数为整个图形创建标签,也可以创建图例。但由于我们没有像散点图那样标记数据点,我们需要在添加图例本身时创建图例条目。
我喜欢在直接添加系列时标记我的数据点,但你不必这样做。我想展示这一点,因为你会看到很多不同的例子。当然,这些顺序大多无关紧要,因为它是在实际渲染之前设置这些轴对象。
这里我将在有任何数据之前创建一个X标签、Y标签和标题。事实上,这里的第一个将在我们做任何事情之前创建一个新图形和新轴对象并设置标签。
然后我将添加一个图例,并说图例中有三个东西:基线、竞争和我们。然后我将进行你刚才看到的完全相同的plot调用。
plt.xlabel('X轴')
plt.ylabel('Y轴')
plt.title('示例折线图')
plt.legend(['线性数据', '指数数据', '额外数据'])
plt.plot(linear_data, '-o', exponential_data, '-o')
plt.plot([1, 2, 3, 4], '--r')
你可以看到它在这里很好地渲染,我们有一些数据,有其他数据,还有标题。
🎨 填充区域
这是介绍Matplotlib的fill_between函数的好时机。这个函数并非折线图专用,但通常与折线图一起使用以突出显示某个区域。我想在这个上下文中介绍它。
假设我们想突出显示绿色和蓝色曲线之间的差异。我们可以告诉轴使用fill_between函数在这些系列之间绘制颜色。
我可以有相同的代码:X标签、Y标签、标题、图例、绘图。然后现在我们需要获取当前的轴对象并调用fill_between。
我们没有为plot调用指定任何X值。默认情况下,它将使用传递的列表或数组的索引。所以我实际上将使用它已经使用的相同数据点范围,这是对称的。然后我们将把它作为我们的下界和上界,以及我们想要绘制的颜色。为了好玩,我将添加透明度。
ax = plt.gca()
ax.fill_between(range(len(linear_data)), linear_data, exponential_data, facecolor='blue', alpha=0.25)
这里我说plt.gca,获取当前轴,然后调用fill_between。我想让你创建一个从线性数据的范围,那是一堆数字,那是我们的X值。这是我们的线性数据和指数数据。我想让你将这个填充区域的颜色设置为蓝色,并设置alpha为0.25。线性和指数数据将分别显示这个的下界和上界。
这渲染得很好,我们有这条蓝线,橙线,点,还有这个很好的填充阴影区域,我们还有所有的标题等等。我们的图表在功能上真的变得更加复杂了。
📅 处理日期数据
最有可能的是,你有一系列X值和Y值,你正在查看绘图,通常对于折线图,这以X轴上的日期时间形式出现。在早期版本的Matplotlib中,这很痛苦,你可以回到我之前关于这个的讲座视频,但实际上我已经能够大大缩短这个,因为Matplotlib正在不断发展。
所以让我们把这里的X轴改为一系列八个日期时间实例,间隔一天。
我将使用之前见过的NumPy的arange函数,你会在Stack Overflow问题、教科书或像这样的示例视频中经常看到这个。我将要求一个以天为间隔的日期排列,所以我给出开始日期和结束日期作为时间,然后将D类型设置为日期时间64,然后arange将创建那个排列。
dates = np.arange('2017-01-01', '2017-01-09', dtype='datetime64[D]')
现在我只是重新绘制我们的线性和指数数据,所以我们有我们的观察,我们的线性数据,我们的观察和我们的指数数据。再次强调,这对于不习惯位置参数的人来说可能非常陌生。之前我们有数据格式化,然后数据格式化。现在因为我们希望这些日期沿着底部,我们有那些日期,数据格式化,日期,数据格式化。所以plot的参数位置随着我们指定的参数数量而变化,这些参数的格式在许多编程语言中非常非正统,包括许多Python,但它在Python中得到支持,这真的说明了Matplotlib在MATLAB中的根源。
这产生了一个相当不错的图形,有我们想要的日期。日期重叠得很厉害。这里有几件事我们可以做,例如,一切都来自2017年,所以我们可以采取简单的方法,只是遍历并更改标签,去掉年份,然后添加一个X轴标签,澄清这都是2017年的。这相当合理,但我想向你展示Matplotlib的几个功能,以及我们如何处理文本。
🔧 自定义刻度标签
轴对象有很多有趣的属性,你应该在作业中使用其中一些,我鼓励你现在去文档中探索它们,因为你开始掌握一些技能并理解如何构建这些基本图表。
例如,你可以获取网格线,主要和次要刻度的位置等等。就像所有艺术家一样,一个轴有一堆子对象,它们本身就是艺术家。事实上,如果你在Coursera的Jupyter Lab笔记本上跟着这个讲座,为什么不暂停视频并运行我们之前写的rec_gc函数,探索这个X轴对象上有哪些艺术家,它实际包含什么。
不过,我想展示的是,你可以使用get_ticklabels函数访问刻度的文本。每个刻度标签都是一个文本对象,它本身是一个艺术家,这意味着你可以使用许多不同的艺术家函数,一个特定于文本的是能够设置旋转函数,它允许以度为单位设置旋转。所以让我们遍历轴标签并更改每一个以旋转它。
ax = plt.gca()
for label in ax.xaxis.get_ticklabels():
label.set_rotation(45)
这看起来很容易。它很快,很流畅,你可以想象如果我们想要,我们可以如何进一步自定义这个。所以现在让我们重新添加我们的标题。
完全相同的事情,但现在我添加了底部的标签,X,Y和标题。我倾向于在底部添加我的标签,但这只是习惯,正如你之前看到的,你可以在任何时候添加它们。
好了,我们在Matplotlib中有了非常易读的日期。
📝 文本和可读性
当我们谈论文本和可读性时,让我分享另外两个见解。第一个涉及方程。Matplotlib与LaTeX有相当强的联系,LaTeX是数学家和科学家使用的排版语言。这意味着你可以直接在标签上使用LaTeX的一个子集,Matplotlib会将它们渲染为方程。
例如,这里我们可以设置轴的标题,以便其中直接有一个X平方。为此,我们用美元符号转义到LaTeX数学模式。注意,无论你是否安装了LaTeX,这都有效。很长一段时间,你在Mac OS或Windows上不会安装LaTeX,但有了完整的LaTeX安装,你实际上对文本格式有显著的控制。
对于第二个见解,我想谈谈图形本身的大小。为什么我们看这些小的邮票大小的图形?这取决于你的屏幕尺寸和分辨率。但关键是我们可以告诉Matplotlib,当我们第一次制作那个图形时,通过传递参数,我们希望图形是某个大小。
在这个下一个例子中,我将以英寸指定它,但你可以使用各种其他尺寸,你可以将它们设置为厘米或DPI等等。
所以这次我将显式创建一个新图形,但我将传入这个figsize关键字参数作为一个元组,我将使它为8逗号6,这意味着8英寸乘6英寸。然后我将从上面放入我通常的数据,所以这基本上是从上面复制和粘贴的,但我想在这里展示标题,当你想转义到LaTeX数学模式时,你可以只放美元符号,然后放在任何文本格式化字符串内,然后放你想要的。所以在LaTeX中这意味着X的二次方,你不必在LaTeX中做这个,LaTeX本身就是一个巨大的学习曲线,但对于基本公式来说,它很好,当然,如果你在科学行业,你可以做更广泛的事情。
plt.figure(figsize=(8, 6))
plt.plot(dates, linear_data, '-o', dates, exponential_data, '-o')
plt.xlabel('日期')
plt.ylabel('数值')
plt.title('带有日期和$X^2$公式的图表')


🎯 总结
本节课中我们一起学习了如何使用Matplotlib绘制折线图。我们从基础绘制开始,逐步掌握了添加多个数据系列、自定义线条和标记样式、添加标签和图例的方法。我们还探讨了如何处理日期时间数据、旋转刻度标签以提高可读性,以及如何在图表中使用LaTeX渲染数学公式。最后,我们学习了如何调整图形尺寸以适应不同的展示需求。这些技能将帮助你创建更专业、更易读的数据可视化图表,无论是用于报告还是数据探索。
43:柱状图绘制 📊

在本节课中,我们将学习如何使用 Matplotlib 库绘制多种类型的柱状图。我们将涵盖基本柱状图、分组柱状图、误差棒、堆叠柱状图以及水平柱状图的创建方法。
基本柱状图绘制
Matplotlib 支持多种柱状图。在最一般的情况下,我们可以通过传入 X 轴坐标参数和柱体高度参数来绘制柱状图。
以下是绘制基本柱状图的步骤。
首先,我们导入必要的库并创建一些线性数据。
import matplotlib.pyplot as plt
import numpy as np
# 创建线性数据
linear_data = np.array([1, 2, 3, 4, 5, 6, 7, 8])
# 生成 X 轴标签
x_vals = np.arange(len(linear_data))
接着,我们使用 plt.bar 函数绘制柱状图。x_vals 是 X 轴的位置,linear_data 是柱体的高度。
plt.bar(x_vals, linear_data, width=0.3)
plt.show()
运行上述代码,我们将得到一系列从 0 开始、高度从 1 递增到 8 的蓝色柱体。请注意,由于我们使用了 np.arange 函数,X 轴标签实际上是 0 到 7。
绘制分组柱状图
上一节我们介绍了基本柱状图,本节中我们来看看如何绘制分组柱状图。要添加第二组柱体,我们需要再次调用 plt.bar 函数,并调整新柱体的 X 轴位置,以避免与第一组柱体重叠。
以下是具体步骤。
首先,我们创建第二组数据(例如指数数据)并计算其 X 轴位置。
# 创建第二组数据(指数数据)
exponential_data = linear_data ** 2
# 调整新柱体的 X 轴位置(向右偏移一个柱体宽度)
new_x_vals = x_vals + 0.3
然后,我们分别绘制两组柱体。
# 绘制第一组柱体
plt.bar(x_vals, linear_data, width=0.3)
# 绘制第二组柱体
plt.bar(new_x_vals, exponential_data, width=0.3, color='red')
plt.show()
这样我们就得到了一个分组柱状图。但当前的图表可能不够美观,因为 X 轴标签位于柱体左侧而非居中。虽然可以使用 align 参数来调整单个柱体的对齐方式,但在处理多组数据时,手动计算位置会非常繁琐。
为柱状图添加误差棒
除了基本柱状图,Matplotlib 还支持为每个柱体添加误差棒,这通常用于表示数据的波动范围(例如标准差)。
以下是添加误差棒的方法。
首先,我们生成一组随机误差值。
import random
# 为线性数据生成一组随机误差值(范围 1 到 4)
linear_err = [random.randint(1, 4) for _ in range(len(linear_data))]
然后,在绘制柱状图时,通过 yerr 参数传入误差值。
plt.bar(x_vals, linear_data, width=0.3, yerr=linear_err)
plt.show()
运行代码后,每个柱体上方将显示一个误差棒。由于误差值是随机的,你的图表可能与示例略有不同。Matplotlib 会自动调整图表范围以容纳误差棒。
绘制堆叠柱状图
上一节我们处理了分组柱状图的位置问题,本节中我们来看看更简单的堆叠柱状图。堆叠柱状图用于显示累积值,同时保持数据系列的独立性,且无需手动调整柱体位置。
以下是创建堆叠柱状图的步骤。
我们使用相同的两组数据。绘制第二组柱体时,通过 bottom 参数将其底部设置为第一组柱体的高度。
# 绘制第一组柱体(底部)
plt.bar(x_vals, linear_data, width=0.3, color='blue')
# 绘制第二组柱体(堆叠在第一组之上)
plt.bar(x_vals, exponential_data, width=0.3, bottom=linear_data, color='red')
plt.show()
这样,红色柱体将从蓝色柱体的顶部开始绘制,形成堆叠效果。两组数据共享相同的 X 轴坐标。

绘制水平柱状图
最后,我们可以将垂直柱状图转换为水平柱状图。这需要使用 plt.barh 函数,并将参数名称从 width 和 bottom 相应地改为 height 和 left。
以下是创建水平堆叠柱状图的方法。
# 绘制水平堆叠柱状图
plt.barh(x_vals, linear_data, height=0.3, color='blue')
plt.barh(x_vals, exponential_data, height=0.3, left=linear_data, color='red')
plt.show()
通过上述代码,我们得到了一个基本的水平堆叠柱状图。参数 left 在这里的作用与垂直柱状图中的 bottom 类似,用于指定柱体的起始位置。
总结与核心概念
本节课中我们一起学习了 Matplotlib 中柱状图绘制的核心知识。需要理解的是,Matplotlib 的脚本层(如 plt.bar)实际上是对象层上的一组便捷函数。两者操作的是相同的数据元素,它们协同工作而非相互对立。
以下是本课的核心操作总结:
- 基本柱状图:使用
plt.bar(x, height)。 - 分组柱状图:多次调用
plt.bar并手动调整x值。 - 误差棒:在
plt.bar中使用yerr参数。 - 堆叠柱状图:在后续
plt.bar调用中使用bottom参数。 - 水平柱状图:使用
plt.barh函数,参数对应为height和left。
本模块介绍了一些基础图表:散点图、折线图和柱状图。虽然还有更多高级图表将在后续模块探讨,但这些是构建数据可视化能力的良好基础。本模块的作业具有一定挑战性,旨在让你解决真实世界的数据可视化问题。你需要结合第一门课程中学到的 Pandas 库知识和新掌握的 Matplotlib 技能,提出一个优雅的解决方案。最终成果应该是一份可以放入作品集的作品。
44:图表去冗余处理 📊


在本节课中,我们将学习如何应用Tufte的数据墨水比和图表垃圾原则,对一个常规的Matplotlib图表进行优化,使其更加清晰和高效。
我们将使用Jupyter Notebook逐步演示所有步骤。过程中会穿插视频测验,你可以先尝试自己解决问题,再看讲解。Matplotlib的设计者也认为图表垃圾和数据墨水比非常重要。事实上,Matplotlib已经显著进化,有了许多合理的默认设置,但掌握轴对象、脚本与对象层,以及查阅文档解决问题的能力,依然能让你高度自定义图表。
现在,让我们开始吧。
我们将使用Stack Overflow上关于编程语言流行度的数据来绘制图表,这里使用的是2016年的原始数据。
首先,导入必要的库并准备数据。
import matplotlib.pyplot as plt
import numpy as np
这里考虑了五种编程语言:Python、SQL、Java、C++和JavaScript。我们将使用numpy的range函数生成排名作为x轴位置,并手动输入流行度数值。
languages = ['Python', 'SQL', 'Java', 'C++', 'JavaScript']
positions = np.arange(len(languages))
popularity = [56, 39, 34, 34, 29]
接下来,创建一个基础的条形图。
plt.figure(figsize=(10, 8))
bars = plt.bar(positions, popularity, align='center')
plt.xticks(positions, languages)
plt.ylabel('Popularity')
plt.title('Programming Language Popularity')
plt.show()
运行上述代码,你会得到一个默认样式的图表。
现在,我们面临第一个挑战:图表周围的边框框架显得有些沉重且不必要。根据Tufte的数据墨水比原则,我们应该移除这些多余的“墨水”。
请思考一下,你会如何尝试解决这个问题?可以暂停视频自己动手试试。
我的解决方案是获取当前坐标轴,然后遍历所有边框线,将其可见性设置为False。这样边框对象依然存在,但不会被渲染出来。
# 在创建图表的基础代码后,添加以下内容
ax = plt.gca()
for spine in ax.spines.values():
spine.set_visible(False)
这个改动让图表看起来更轻量了一些,但变化不大。所有条形都是蓝色,这无助于区分它们。我们正在比较这些条形,但它们颜色相同。
因此,下一步是将所有硬黑色线条变为灰色,同时将条形颜色也改为灰色。但为了让排名第一的Python语言突出显示,我们将其保留为原来的蓝色。
以下是修改后的代码:
plt.figure(figsize=(10, 8))
bars = plt.bar(positions, popularity, align='center', color='slategray')
# 将第一个条形(Python)设置为蓝色
bars[0].set_color('#1f77b4')
plt.xticks(positions, languages)
plt.ylabel('Popularity')
plt.title('Programming Language Popularity')
# 移除边框
ax = plt.gca()
for spine in ax.spines.values():
spine.set_visible(False)
plt.show()
现在图表看起来清晰多了。Python被突出显示,同时我们仍能看清所有条形的高度,并参考X轴和Y轴。整个图表感觉轻量、透气,数据墨水比较低。
但是,让我们再看看Y轴。为什么不直接移除它,并将数值直接标注在对应的条形上呢?这是优化数据标签的一个原则,可以避免读者费力地在Y轴和条形之间来回对照猜测高度。
你打算如何实现?请暂停视频尝试一下。
我的解决方法如下。基础代码相同,但在设置X轴刻度后,通过传递一个空列表来隐藏Y轴刻度值。
plt.yticks([]) # 移除Y轴刻度标签
移除Y轴标签很容易,但为条形添加数据标签则需要更多步骤。我们需要遍历每个条形,获取其高度,然后创建一个包含数据信息的新文本对象并渲染到屏幕上。
# 在创建条形图并设置样式后,添加以下循环来标注数值
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2., height - 5,
f'{int(height)}%',
ha='center', color='white', fontsize=11)
解释一下这段代码:我们获取条形的高度,然后计算文本的x位置(条形x坐标加上宽度的一半)和y位置(高度减去5个像素单位)。接着,创建一个包含高度整数值和百分号的字符串标签。最后,设置文本水平居中、颜色为白色(因为条形是灰色或蓝色)、字体大小为11。
处理Matplotlib图表时,经常需要进行这类数学计算来自定义布局,尤其是在处理文本定位时,可能需要一些尝试和调整。
现在,这个图表看起来比最初那个好多了。我们可以清楚地看到Python与其他语言的对比,所有语言都得到了呈现,每个人都能直接看到每种语言的具体数值。图表非常轻量,易于嵌入报告或演示文稿中。
以上就是全部内容。通过一系列简单的步骤,可以让你的条形图更具可用性。

观看视频时,你是否找到了不同的实现方法?或者你是否以其他方式解决了问题?也许你认为还可以应用Tufte或Cairo的其他原则,让这个简单的可视化图表更具可读性和直观性。
欢迎到讨论区与我和其他同学分享你的想法。

本节课中,我们一起学习了如何应用数据墨水比原则优化Matplotlib图表。具体步骤包括:移除不必要的图表边框、调整颜色以突出重点数据,以及移除Y轴并将数值直接标注在条形上。这些技巧能有效提升图表的清晰度和信息传递效率。
45:子图系统详解 🐍


在本节课中,我们将深入学习Matplotlib的子图系统。我们将探讨如何在同一图形中创建并排的多个图表,这对于数据比较和可视化探索至关重要。
概述
在第一模块中,我们讨论了优秀可视化的基本原则。在第二模块中,我们开启了Matplotlib Python库的学习,从架构开始,并学习了最常见的图表类型:散点图、折线图和条形图。在本模块中,我们将进行更深入的探讨,讨论同一图形中的多个图表、交互、动画,以及你在数据科学旅程中会发现有用的更多不同类型的图表。
如果你一直在完成作业,你无疑已经访问过课程讨论论坛、Stack Overflow并阅读了Matplotlib API文档。我想指出,Matplotlib邮件列表是另一个很好的支持资源。课程中将发布关于如何浏览该列表的资源。对于开源项目来说,通常有两个不同的邮件列表:一个供开发者使用,一个供用户使用。用户列表是我们询问有关如何使用该工具包的大部分问题的地方。但我真的鼓励你也看看开发者档案,以了解这个项目是如何演变的,并看看幕后的情况。
深入子图
到目前为止,我们一直在使用单个轴对象来绘制单个图表或图形。有时,并排显示两个复杂的图表以进行视觉比较,并让观众得出自己的理解是很有用的。Matplotlib在单个图形对象内处理这个问题。
首先,我们将渲染设置回笔记本中,然后导入我们的pyplot模块和NumPy,因为本模块中两者都需要。
import matplotlib.pyplot as plt
import numpy as np
我想看看并展示文档的样子。请记住,如果你想知道某个函数的更多信息,可以像这里一样在函数后面加上问号。
plt.subplot?
我们看到它在这里被渲染出来,可以看到我们有plt.subplot,它接受参数和关键字参数,我们有了文档字符串,然后可以看到关于不同参数及其含义以及如何使用它们的各种信息。这应该与网络文档相同,但会非常具体于你安装的版本,因此你不必尝试弄清楚你正在使用哪个版本。
创建基本子图
如果我们查看subplot的文档,我们会看到第一个参数是行数,第二个参数是列数,第三个是绘图编号。在Matplotlib中,一个概念性的网格被覆盖在图形上,而subplot命令允许你在此网格的不同部分创建轴。
例如,如果我们想要并排的两个图,我们将使用参数1, 2, 1调用subplot。这将允许我们使用一行两列,并将第一个轴设置为当前轴。
让我们开始操作。创建一个新图形,然后创建一个具有一行两列的新子图。pyplot将绘制的第一个轴对象是左侧的轴。
plt.figure()
plt.subplot(1, 2, 1)
data = np.array([1, 2, 3, 4, 5])
plt.plot(data)
我们得到了左侧的细长图。现在,如果我们第二次调用subplot,我们可以指示我们也想在右侧绘图。
plt.subplot(1, 2, 2)
exp_data = np.exp(data)
plt.plot(exp_data)
现在我们有了两个图,每个都有自己的轴对象。在Matplotlib中,通常的做法是存储从subplot返回的轴对象,然后你可以再次调用subplot,并且可以随时返回到该轴,因此你可以在脚本中的轴对象之间来回切换。你不必先处理一个轴的所有内容,然后再处理下一个,依此类推。
看看这个图形。你注意到这个图像有什么奇怪的地方吗?特别是回想一下我们课程的第一周。这两个图像有不同的Y轴值。如果我们没有找到一种方法在两个图之间锁定这些轴,这可能会产生问题并可能误导读者。
共享坐标轴
当你创建一个新的子图时,实际上可以指示你希望使用sharex和sharey参数共享X轴和Y轴或两者。让我们清理一下并尝试这个。
fig = plt.figure()
ax1 = plt.subplot(1, 2, 1)
ax1.plot(data)
ax2 = plt.subplot(1, 2, 2, sharey=ax1)
ax2.plot(exp_data)
现在我们有了并排的两个图,并且我们锁定了Y轴。你可以看到左侧的图被稍微压缩了一点,这使得右侧的数据高得多这一点更加清晰。
子图参数语法
那些一直密切关注的人会注意到,我第二次使用了subplot函数,但没有传入三个参数,我只传入了一个。Matplotlib开发者允许你使用三个参数(如我们开始时使用的1, 2, 1)或单个参数(如我现在使用的122)来指定你想要的图的行、列和编号。在这种情况下,百位值是第一个参数,十位值将被视为第二个参数,个位值是第三个参数。
坦率地说,我不太喜欢第二种语法,它感觉相当粗糙,虽然它确实节省了输入两个逗号,但实际上它限制了每个参数只能是一位数。计算机科学领域的人可能会觉得这种表示法有些不对劲,我必须说每次看到它我都会感到困扰。但我希望你能意识到它,这样当你在Stack Overflow或文档中遇到它时,你就能读懂它。
需要记住的一个重要事实是,矩阵中绘图位置的索引从1开始,而不是0,这与使用NumPy等工具时的惯例不同。因此,如果你正在遍历矩阵或列表来创建子图,请记住从位置加一开始。
使用 subplots 函数
有一个很好的函数叫做subplots(注意是复数),它允许你一次获取多个轴对象,我认为这非常棒。你可以设置所有想要的子图,获取所有这些轴的引用,然后开始填充它们。
如果我们想要一个所有X和Y范围都锁定的3x3网格,我们可以相当简单地做到这一点。我将使用元组解包创建一个3x3的子图网格。
fig, axes = plt.subplots(3, 3, sharex=True, sharey=True)
axes[1, 1].plot(data)
语法看起来可能有点奇怪,因为我们直接解包了subplots函数的结果,但这确实是构建一个所有内容共享坐标轴的网格的有效方法。结果看起来很不错。但请注意,此方法会关闭Y轴和X轴标签,除了位于图形左侧或底部的图。这实际上是故意的。你正在共享坐标轴,因此Matplotlib试图使其更具可读性,为你的每个图提供更多空间。
遍历创建子图
当然,我们实际上可以遍历一个列表并一次绘制一个图,如果我们不想存储对轴的引用,也可以不存储。
plt.gcf()
for i in range(1, 7):
if i not in [3, 5]:
ax = plt.subplot(2, 3, i)
ax.text(0.5, 0.5, str(i), ha='center', va='center')
我们可以看到这里有两行三列,并且在第1、2、4和6个绘图位置有数字,而不是第5或第3个。那些是空白的。这里我们没有共享任何坐标轴,所以每个图都有自己的坐标轴值。
散点图矩阵
现在我们理解了为什么图形中有轴的抽象,因为一个图形可能有多个轴对象,显示数据的多个视图。一个常见的数据科学可视化探索技术称为散点图矩阵。这对于快速浏览多个不同变量之间的关系特别有用。
散点图矩阵实际上类似于Edward Tufty所称的“小倍数”:一组查看相关数据的可视化,但将数据切片成不同的小可视化,以便你可以同时看到“树木”和“森林”。
让我们看一个散点图矩阵,我将手动制作它,并使用Iris数据集中的一些数据。
import pandas as pd
df = pd.read_csv('iris.csv')
columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width']
fig, axes = plt.subplots(len(columns), len(columns), figsize=(10, 10))
for i in range(len(columns)):
for j in range(len(columns)):
ax = axes[i, j]
ax.scatter(df[columns[i]], df[columns[j]], s=5)
ax.xaxis.set_visible(False)
ax.yaxis.set_visible(False)
if i == len(columns) - 1:
ax.xaxis.set_visible(True)
if j == 0:
ax.yaxis.set_visible(True)
我们有一个很好的散点图矩阵示例,可以轻松比较萼片和花瓣的长度和宽度,并一目了然地寻找趋势。一个突出的趋势是,花瓣宽度(底部行)与花瓣长度(第三列散点图)有相当线性的关系。如果你比较萼片宽度和萼片长度(例如,看第二行第一列),情况似乎并非如此。
总结

在本节课中,我们一起学习了Matplotlib强大的子图系统。我们探讨了如何使用subplot和subplots函数在单个图形中创建多个坐标轴,学习了共享坐标轴以进行有效比较的方法,并手动创建了一个散点图矩阵来可视化多变量关系。理解子图是创建复杂、信息丰富的仪表板和探索性数据分析图的关键。在下一个模块中,我们将利用关于子图的新知识,同时向你介绍一个非常基础的数据科学图表:直方图。
46:直方图绘制 📊


在本节课中,我们将学习如何使用Python的Matplotlib库绘制直方图。直方图是一种条形图,用于展示数据分布的频率。我们将从基本概念开始,逐步深入到如何通过调整参数(如分箱数量)来优化图表,并最终学习如何使用网格布局创建包含多个子图的复杂可视化图表。
📈 直方图基础
直方图是一种条形图,它展示了给定现象的频率。一个很好的例子是概率分布。例如,在本系列课程的第一部分,我们提到了随机分布、均匀分布、正态分布和卡方分布之间的区别。
概率函数可以可视化为一条曲线,其中Y轴表示某个特定值出现的概率,X轴表示该值本身。这被称为概率密度函数。
Y轴的值被限制在0到1之间,其中0表示给定值没有出现的可能性,1表示该值总是会出现。X轴的值根据分布函数进行标记,在正态分布的情况下,通常以标准差为单位。
直方图就是一种条形图,其中X轴是给定的观测值,Y轴是该观测值出现的频率。
因此,我们应该能够通过从分布中采样来绘制给定的概率分布。现在,回想一下,采样意味着我们将从分布中选取一个数字,就像掷骰子或从一副牌中抽出一张牌。随着我们反复进行这个过程,我们就能更准确地描述分布。
💻 通过代码理解采样
让我们停止空谈,通过编写一些代码来实际观察这个过程。
我将像往常一样导入matplotlib和numpy。现在,我将从正态分布中抽取一些样本,并将四个不同的直方图绘制为子图。
首先,我将创建一个2x2的图形网格。在这种情况下,我们不希望子图之间共享Y轴,因为我们有意查看不同样本大小的数量。但我确实希望共享X轴。因此,我在subplots函数中设置了sharex=True。然后,我得到了这个很好的轴对象集合。
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(2, 2, sharex=True)
现在,我们主要关心的是分布看起来有多均匀。因此,我们将遍历一个包含四个不同值的列表:10、100、1000和10000,并从正态分布中抽取样本。请记住,numpy.random.normal函数只是基于这个基础的正态分布创建一个数字列表。
sample_sizes = [10, 100, 1000, 10000]
for i, a in enumerate(ax.flatten()):
# 从正态分布中抽取样本
samples = np.random.normal(size=sample_sizes[i])
# 在当前轴上绘制直方图
a.hist(samples)
a.set_title(f'Sample Size: {sample_sizes[i]}')
plt.tight_layout()
plt.show()
让我们看一下结果。第一个图只有10个样本,所以它看起来非常锯齿状。在我的版本中,我认为没有人会说这显然是一个正态分布。但当我们增加到100个样本时,情况有所改善,但仍然看起来有点向右倾斜。你的结果可能略有不同,因为这是一个随机过程。你应该在自己的笔记本上运行这段代码并进行比较。
然后,在1000和10000个样本的图中,分布似乎变得更加平滑。但如果我们仔细观察,会发现10000个样本图中的条形实际上比10或100个样本图中的条形更宽。那么,这是怎么回事?为什么底部轴上的条形会发生变化?
🧮 理解分箱
默认情况下,Matplotlib中的直方图使用10个分箱,也就是10个不同的条形。在这里,我们创建了一个共享的X轴。随着我们从分布中抽取更多样本,我们更有可能得到远离均值的异常值。因此,对于n=10,10个分箱最多只能捕获10个唯一值。而对于n=10000,许多值必须被合并到一个分箱中。
让我们用分箱宽度设置为100来执行相同的函数,这样我们实际上就会有100个不同的分箱。这里的代码看起来都一样,唯一改变的是在绘制每个直方图时,我指定了bins=100。
for i, a in enumerate(ax.flatten()):
samples = np.random.normal(size=sample_sizes[i])
# 指定使用100个分箱
a.hist(samples, bins=100)
a.set_title(f'Sample Size: {sample_sizes[i]}')
现在我们可以看到,10000个样本的图看起来比所有其他图都平滑得多,而10个样本的图显示每个样本基本上都在自己的分箱里。我们只有10个样本,但我们愿意查看10个不同的分箱,所以它们基本上都落在同一个分箱里。
这引出了一个重要的问题:在使用直方图时,应该绘制多少个分箱?恐怕答案并不那么明确。这两种图都是真实的。一个是在粗粒度上的数据可视化,另一个是在更细粒度上的可视化。当我们以最细的粒度(例如10000个分箱)查看数据时,直方图对于决策制定基本上变得无用,因为它们没有显示样本之间的任何趋势,而只是显示了样本大小本身。这类似于使用均值和标准差等汇总统计量来描述总体样本。这些值是粗略的,它们是否合适,很大程度上取决于你的问题和兴趣。
🎨 使用网格布局创建复杂图表
现在,我想利用我们新学到的直方图和子图知识,向你介绍更灵活的GridSpec子图布局。GridSpec允许你将一个轴映射到网格中的多个单元格,从而可以创建一些看起来非常自定义的可视化效果。
例如,让我们在这里创建一个散点图,其中Y值来自正态分布,X值来自随机分布。
# 创建一些随机数据
y_vals = np.random.normal(size=10000)
x_vals = np.random.random(size=10000)
仅仅看这个散点图,并不完全清楚每个轴的实际分布是什么,但我们实际上可以添加两个较小的直方图来使这一点更加清晰。
让我们直接开始做这件事。我将定义一个3x3的网格,总共9个单元格。我希望第一个直方图占据右上角的空间,第二个直方图占据左下角两个空间并旋转到侧面。原始的散点图则可以占据右下角的2x2正方形。
要使用这个GridSpec,我们首先必须导入它,然后创建我们想要的整体形状的新GridSpec。
from matplotlib.gridspec import GridSpec
# 设置图形大小
fig = plt.figure(figsize=(10, 10))
# 创建3x3的网格规格
gs = GridSpec(3, 3, figure=fig)
# 创建子图
# 顶部直方图:第0行,第1列到最后一列
ax_top = fig.add_subplot(gs[0, 1:])
# 侧面直方图:第1行到最后一行,第0列
ax_side = fig.add_subplot(gs[1:, 0])
# 主散点图:第1行到最后一行,第1列到最后一列
ax_main = fig.add_subplot(gs[1:, 1:])
当我们添加子图内的新项目时,不是指定三个数字(行、列和位置),而是传入我们希望覆盖的GridSpec对象的元素。这里非常重要,因为我们使用的是列表的元素,所有的索引都从0开始,所以使用切片来表示列表的开头或结尾是非常合理的。
GridSpec使用方括号索引运算符按行和列进行索引。因此,我们将在第0行创建第一个子图和直方图,覆盖第一个元素、中间位置,然后一直到该行的末尾。这里我决定它将是顶部的直方图。对于侧面的直方图,我想从第1行开始,但实际上一直延续到行元素的末尾,但我想将其限制在第0列。最后,这个散点图我想放在右下角。
让我们看一下布局。但现在我们需要用一些数据来填充它。首先,我们要处理散点图,然后我去处理顶部直方图(用于散点图的X值),然后是侧面直方图(用于Y值)。我们希望那个侧面直方图能够对齐,所以我将通过设置方向来旋转布局。由于这很常见,Matplotlib实际上有一个orientation参数,所以我可以将该图中的所有值都横向显示。
# 填充数据
ax_main.scatter(x_vals, y_vals, alpha=0.5)
ax_top.hist(x_vals, bins=100, density=True)
ax_side.hist(y_vals, bins=100, orientation='horizontal', density=True)
# 调整侧面直方图的X轴,使其看起来是从数据中“生长”出来的
ax_side.invert_xaxis()
# 调整坐标轴范围以去除空白
ax_top.set_xlim(0, 1)
ax_side.set_ylim(-5, 5)
ax_main.set_xlim(0, 1)
ax_main.set_ylim(-5, 5)
plt.tight_layout()
plt.show()
通过这个图表,我们可以立即清楚地看到我们实际上有两种不同的分布。顶部直方图的Y轴值或底部直方图的X轴值我们并不真正关心,因为这些都是概率密度直方图,我们只关心相对值。在Matplotlib中,我们可以通过设置density=True来做到这一点。这样,它就不太关心观测的实际频率,而更关心这些频率的概率或比率。
我们还可以反转左侧直方图的X轴,这可能使其更清晰,因为它代表了右侧的数据,这使得它看起来像是从散点图数据中“生长”出来的。我们可以直接在坐标轴上使用invert_xaxis函数来实现这一点。最后,如果我们回头看,会发现坐标轴上有一点空白,并且自动添加了一些填充,这似乎是不必要的,可能还有点令人困惑。我们可以通过更改坐标轴限制来消除这种情况。
🔄 迭代式可视化过程
我处理数据、调查数据以及如何绘制或查看数据的方法,实际上是一个迭代的过程。我不会坐下来就确切地知道我想看到什么,但随着我发现、思考或观察事物,我可以使用Matplotlib来改变它的呈现方式。这在Jupyter笔记本中效果非常好。
数据和网格规格与上面相同,子图以及所有侧面直方图、顶部直方图和右下角散点图也都相同。但然后我想获取那个侧面直方图并反转其X轴,接着我只想更改坐标轴限制以消除空白。因此,我将在这里遍历顶部直方图和右下角的所有坐标轴,将X轴范围设置为0到1,将Y轴范围设置为-5到+5。
我们可以看到,我们的顶部直方图是随机的,相当均匀,而Y值的直方图确实看起来像正态分布。事实上,这张图粗略地看起来像我们在本课程第一个模块中看到的来自MIT和哈佛关于OC参与度研究的图像。现在你应该很清楚,我们如何调整这个图表的各个方面,如果我们想让它看起来更像那个参与度图像,我们实际上可以尝试添加标题到这些子图,使其更类似于MIT和哈佛出版物中的标题,或者改变单个点的大小,因为我们处理的是相当大的数据,所以可以让点变小一些,或者改变样式,你能改变图表的颜色以模仿MIT和哈佛的颜色吗?用你新学到的Matplotlib技能试一试,查阅API文档,看看你能创造出什么。
📝 总结

在本节课中,我们一起学习了直方图的基本概念及其在可视化数据分布中的应用。我们通过代码实践了如何从分布中采样并绘制直方图,理解了分箱数量对图表效果的影响。最后,我们探索了使用GridSpec创建复杂多子图布局的方法,并了解了如何通过迭代调整来优化最终的可视化效果。希望你现在对使用Matplotlib进行数据可视化有了更深入的理解。
47:箱线图绘制 📊


在本节课中,我们将要学习如何使用Matplotlib库绘制箱线图。箱线图是一种简洁展示数据分布和汇总统计信息的强大可视化工具,在数据科学中极为常见。
箱线图简介 📦
箱线图,有时也被称为盒须图,是一种以简洁方式展示不同样本聚合统计信息的方法。
箱线图的目标是通过可视化所谓的“五数概括法”来总结数据的分布情况。这五个数包括:极值(通常是数据的最小值和最大值)、中心(通常是数据的中位数)以及数据的第一和第三四分位数。
数据的分位数将其大致划分为四个大小相似的区间。第一和第三四分位数的标记(有时也称为“铰链”)展示了数据的中间50%部分。
通过箱线图,我们可以在一个相当简洁、紧凑的可视化表示中感知数据的权重分布。
在Matplotlib中绘制箱线图 🎨
上一节我们介绍了箱线图的基本概念,本节中我们来看看如何在Matplotlib中实际绘制它。
首先,我们需要导入必要的库:pandas、matplotlib和numpy。
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
接下来,我将使用numpy创建三个不同的样本:一个来自正态分布,一个来自均匀随机分布,一个来自伽马分布。这样我们就可以观察这些分布在五数概括法下的差异。
normal = np.random.normal(size=10000)
random = np.random.random(size=10000)
gamma = np.random.gamma(2, size=10000)
然后,我将把这些数据放入一个pandas数据框中,并分别命名为“normal”、“random”和“gamma”。
df = pd.DataFrame({'normal': normal, 'random': random, 'gamma': gamma})
让我们看看数据框的样子。每个样本都有10000个数据点。我们可以看到正态分布、随机分布和伽马分布的样本。
pandas有一个方便的函数叫做describe,它允许我们查看五数概括法以及其他一些我们感兴趣的汇总统计信息。
df.describe()
我们可以看到,这些分布之间的均值差异很大。正态分布的均值非常接近0(因为我们在生成时没有指定参数)。随机分布的均值接近0.5(因为它在0到1之间均匀选取随机值)。伽马分布的均值则在2左右。我们还可以看到标准差、最小值、最大值以及四分位数值(25%、50%、75%)。
describe函数显示了最小值、最大值和三个百分位数。这些百分位数值构成了所谓的四分位距。
以下是数据被划分为四个区间的解释:
- 第一四分位数(Q1)是数据的25%分位点。
- 第二四分位数(Q2)是数据的中位数,即50%分位点。
- 第三四分位数(Q3)是数据的75%分位点。
- 第四四分位数是Q3到最大值之间的数据。
与标准差类似,四分位距是衡量数据变异性的一个指标,通常使用箱线图来绘制。
在箱线图中,数据的均值或中位数被绘制为一条直线。形成两个箱体:一个在上方,代表50%到75%的数据组;一个在下方,代表25%到50%的数据组。然后,细线(须)被相应地延伸到最小值和最大值。
绘制基本箱线图 ✏️
当然,看一个箱线图比描述它要简单得多。这里,我们只需选择我们感兴趣的一列数据,并将其传递给pyplot的boxplot函数。
plt.boxplot(df['normal'])
Matplotlib使用numpy数组处理数据,但pandas是构建在numpy之上的,因此可以无缝衔接。你可以传入一个pandas列,因为它底层实际上就是一个ND数组。
你会看到Matplotlib打印出一堆关于“artists”的信息。通常我们并不想看到这些,但有时了解绘图底层的构建过程会很有帮助。
如果我们想抑制这个输出,只需在最后一条语句的末尾加上一个分号。这是一个Jupyter Notebook的小技巧,它可以抑制单元格中最后一个变量的打印输出。但请注意,这不是标准的Python语法。
plt.boxplot(df['normal']);
这样我们就得到了一个基本的箱线图。
绘制多组数据的箱线图 📈
现在,让我们把另外两个样本也添加到箱线图中。遗憾的是,我们不能直接将整个pandas数据框传递给Matplotlib。相反,我们必须提取这些列,并将它们作为一个值列表传入。
以下是具体做法:
plt.boxplot([df['normal'], df['random'], df['gamma']]);
我们可以看到三个分布。注意,我们没有对比例进行归一化,所以看起来有点奇怪。例如,观察伽马分布,我们可以看到它的尾部非常长,最大值离得很远。
添加嵌入子图(Inset Axes) 🔍
让我们通过绘制伽马分布的直方图来更仔细地观察它。我将以此为契机演示一个叫做“嵌入坐标轴”的功能。
回想一下,我们有一个图形,里面有一个子图。由于我们没有对子图做任何复杂的设置,这意味着我们只有一个坐标轴对象。实际上,我们可以在一个图形内的另一个坐标轴之上叠加一个新的坐标轴。我们可以通过在图形本身上调用inset_axes函数并传入我们想要创建的新坐标轴的详细信息来实现。
我们传入的详细信息是新坐标轴在原始图形XY空间中的位置,以及这个新坐标轴绘图的宽度和高度。
以下是创建嵌入子图的步骤:
fig = plt.figure()
# 在主图上绘制箱线图
plt.boxplot([df['normal'], df['random'], df['gamma']])
# 创建一个嵌入坐标轴
# 参数:[左下角x坐标, 左下角y坐标, 宽度, 高度] (均为相对于主图的比例,0到1之间)
ax2 = plt.gca().inset_axes([0.6, 0.6, 0.35, 0.35])
# 在嵌入坐标轴上绘制伽马分布的直方图
ax2.hist(df['gamma'], bins=100, density=True)
# 将y轴刻度标签移到右侧,避免与主图冲突
ax2.yaxis.tick_right()
plt.show()
这样,在一个图形中,我们既有三个不同分布的箱线图,又有一个漂亮的小嵌入图像显示了最右侧箱线图(伽马分布)的直方图。
再次记住,每个箱线图都是我们的五数概括法:中间的红线是中位数,两边的箱体分别代表25%的数据,而延伸出去的“须”则到达数据的最大值和最小值。
识别异常值 ⚠️
我们通常希望查看箱线图时,不是看最小值和最大值,而是强调异常值。如何检测异常值取决于具体方法,有多种机制来确定一个观测值是否为异常值。
如果我们查看Matplotlib的文档,其默认的异常值检测方法是:所有大于上铰链(第三四分位数) + 1.5 * IQR 或小于下铰链(第一四分位数) - 1.5 * IQR 的数据点都被视为异常值。其中,IQR(四分位距) 就是两个铰链之间的距离,它包含了50%的数据。
如果我们省略之前添加到boxplot调用中的whis参数,我们将看到使用此方法识别的异常值。
plt.figure(figsize=(8, 6))
plt.boxplot([df['normal'], df['random'], df['gamma']])
plt.show()
箱线图中的每个圆圈代表一个单独的异常值观测点。
箱线图是数据科学家将使用的更常见的图表之一,Matplotlib对不同类型的箱线图有广泛的支持。Matplotlib文档是关键,你可以在课程资源中找到API链接,其中描述了如何使用箱线图功能。
总结 📝

本节课中我们一起学习了箱线图的原理及其在Matplotlib中的实现。我们了解了五数概括法,学会了绘制单个及多个数据组的箱线图,探索了使用嵌入坐标轴来增强可视化效果,并理解了箱线图中异常值的默认识别方法。箱线图是快速理解数据分布和识别潜在异常值的核心工具,掌握它将为你的数据探索分析提供有力支持。
48:热力图可视化 📊

在本节课中,我们将学习如何使用热力图来可视化三维数据,并理解其适用场景与限制。热力图是一种强大的工具,尤其适合展示具有空间或顺序关系的二维数据上的第三维信息。
什么是热力图?

上一节我们介绍了数据可视化的基础,本节中我们来看看热力图。热力图是一种可视化三维数据的方法,它利用了数据维度在空间上的邻近性。
在修订本课程时,我曾考虑删除热力图部分,因为我见过太多糟糕的热力图示例。问题是,当数据合适时,热力图确实非常强大。天气数据就是一个很好的例子:你有两个维度(纬度和经度),我们可以在这之上叠加第三个维度(例如温度或降雨量),并通过颜色的强度来指示这个维度。
事实上,任何具有二维空间属性的数据都天然适合用热力图表示。例如,研究人员和营销专家经常使用眼动追踪的注视点来理解人们在网站上观看的内容。
但是,当维度之间没有连续关系,或者至少没有顺序关系时,热力图就会失效。例如,为分类数据使用热力图是完全错误的。它会误导观众通过空间邻近性来寻找模式和顺序,而任何此类模式都纯粹是虚假的。
我决定将这部分内容保留在课程中,因为它确实有用。我使用订单数据构建了一个新示例。无论如何,让我们来谈谈Matplotlib中的技术。
在Matplotlib中,热图本质上是一个二维直方图,其中X和Y值表示可能的点,而绘制的颜色是观测值的频率。
准备数据与环境
以下是开始之前需要导入的库和功能。
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datetime import datetime
在这个例子中,我想向你展示纽约市的一些交通数据,这些数据全部来自纽约市开放数据门户。我将把这些数据读入一个数据框,然后将日期列转换为日期时间格式。
# 读取数据
df = pd.read_csv('nyc_traffic_data.csv') # 假设文件名为 nyc_traffic_data.csv
# 转换日期列
df['Date'] = pd.to_datetime(df['Date'])
让我们查看一下数据。如果你在Coursera上跟随学习,加载可能需要一点时间,因为这实际上是一个相当大的数据集。
现在我们来看一下。这里有广场标识符,这些是设置交通摄像头的不同区域。然后我们有日期,并且实际上按一天中的小时细分。对于每个广场,数据中的每个小时都被细分,这里可能有大约10年的数据。然后他们有两个不同的方向:进站(此处显示为“I”)和出站(针对广场),以及两个不同的数字:一个是E-ZPass的计数(我们最终称之为密度),另一个是Vtoll(一个不同的收费系统)。所以这是两个不同的收费系统。
基础数据探索:直方图
让我们精简数据,并用直方图进行一些基础探索性数据分析。
假设我对单个广场(一个摄像头位置)和2017年初的一些日期感兴趣。我将在这里编写一个pandas查询来提取这些数据。
filtered_df = df.query('PlazaID == 5 and Date > "2016-12-31" and Date < "2017-05-01"')
这种语法可能看起来与我们第一门课程中的有些不同。这是查询数据框的另一种方式,它使用一个名为numexpr的库来将查询作为字符串接收并应用于数据框。如果你熟悉SQL,它的语法有点像SQL,但有很多小注意事项。我想向你介绍它,以便你了解,但你完全可以使用我在课程1中展示的常规布尔掩码方法。实际上,现在是暂停视频、打开笔记本并尝试的好时机:你能根据我所说的和你所看到的,利用你已有的布尔掩码知识重写这个查询吗?
我们这里有“一天中的小时”,让我们来看看一天中活动的直方图。
由于一天有24小时,我将把bins设置为24,并且我想看到我们的频率(每个柱的权重)作为使用这个E-ZPass系统的车辆数量。这个系统自动为使用道路的司机计费,并且数据中的个体观察结果已经为我们汇总好了。在这里,我们只需调用plot.hist。我将指定一个数组或pandas中的列(因为pandas构建在Numpy数组之上)作为hour,这些值将从0到23。我想要24个bins,因为我知道一天有24小时,并且我将设置weights等于另一列vehicles_easypass。
plt.hist(filtered_df['Hour'], bins=24, weights=filtered_df['Vehicles_EZPass'])
plt.xlabel('Hour of Day')
plt.ylabel('Vehicle Count (EZPass)')
plt.title('Hourly Traffic Volume at Plaza 5')
plt.show()
我看到两个高峰:早上7:30左右和下午3点左右开始直到6点。所以这是纽约的交通高峰时间,这符合我们的预期。
在这个例子中,我们的“小时”是有序的,从0开始到23,贯穿一天,所以以这种连续直方图的方式查看数据是有意义的。
但我们还有“星期几”,这也是有序的(周一到周日)。所以让我们提取星期几并查看其直方图。
我们可以使用pandas的datetime功能.dt从日期列中提取星期几。请记住,pandas中的单个列是一个Series对象,它实际上有一个.dt属性,为我们存储了许多日期时间转换功能,因为这实际上是一个非常常见的需求。在这种情况下,我们想获取这个日期列,访问这些日期时间功能,我们只想从中获取星期几。
我将把它分配到一个名为day_of_week的新列中,这将返回一个介于0和6之间的数字(取决于星期几)。然后,一旦我们有了这个,我们就可以稍微调整一下我们的直方图:将bins设置为7,表明我们对星期几感兴趣,并且我们仍然继续查看E-ZPass的数字。
filtered_df['day_of_week'] = filtered_df['Date'].dt.dayofweek
plt.hist(filtered_df['day_of_week'], bins=7, weights=filtered_df['Vehicles_EZPass'])
plt.xlabel('Day of Week (0=Monday)')
plt.ylabel('Vehicle Count (EZPass)')
plt.title('Weekly Traffic Volume at Plaza 5')
plt.xticks(range(7), ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
plt.show()
我们可以看到,除了第5天和第6天(恰好是周末),交通流量相当稳定。现在,我们可以隔离这些天,查看每小时交通的单独直方图,但我们也可以查看小时和日变量的联合直方图或热力图。
创建热力图
当我们这样做时,我们设置一个变量为x轴,另一个为y轴,然后我们使用不同的颜色渲染我们的频率(权重)来显示这第三个维度。虽然听起来工作量很大,但在Matplotlib中,API看起来几乎与常规直方图相同。
但在这种情况下,我们必须为每个轴指定bins的大小,并在这里将图形设置得更大一些以便于阅读,然后我们只需调用hist2d而不是hist。我们传入我们的x和y:在x轴上,我想查看小时;在y轴上,我想查看星期几。然后对于bins,我们必须告诉它有24个bins对应小时,7个对应星期几。我们保持weights不变。
plt.figure(figsize=(10, 6))
# 创建二维直方图(热力图)
counts, xedges, yedges, im = plt.hist2d(filtered_df['Hour'],
filtered_df['day_of_week'],
bins=[24, 7],
weights=filtered_df['Vehicles_EZPass'])
plt.colorbar(im) # 显示颜色条
plt.xlabel('Hour of Day')
plt.ylabel('Day of Week (0=Monday)')
plt.title('Heatmap: Traffic Volume by Hour and Day at Plaza 5')
plt.yticks(range(7), ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'])
plt.show()
接下来的部分实际上是可选的,但它告诉Matplotlib向我们显示一个颜色条,一个告诉我们这些权重的图例。

很好,让我们剖析一下这个图。首先,我们看到在所有天中,我们的y轴(即前四行左右)的前几个小时是深蓝色的。这表示前四列左右,表明交通量相对较少。这是有道理的,那个时候开车的人不多。
然后我们看到,对于第0到第4天(周一到周五),交通量出现高峰。这些在图像中显示为更偏黄色的强度单元格。但对于最后两天(周末,即最上面两行),情况并非如此。
时间数据是热力图的一个有趣用例,因为我们经常在一个时间段内有活动周期。在这种情况下,我们有天和周,这使我们能够拥有这两个不同的有序维度。
练习与总结
现在是你尝试的好时机,看看你是否能查看不同的维度,比如月份。你能绘制一个热力图,其中一个轴是月份,另一个轴是,比如说,一个月中的周吗?试一试。


本节课中我们一起学习了热力图的可视化方法。我们了解到热力图非常适合展示具有空间或顺序关系的二维数据上的第三维信息,例如时间序列中的小时和星期几。我们通过纽约市交通数据的实例,演示了如何使用Matplotlib的hist2d函数创建热力图,并解读了其中的模式。记住,避免对分类数据使用热力图,因为这会产生误导。
49:动画制作 🎬


在本节课中,我们将学习如何使用Matplotlib库创建动态图表。我们将从静态图像转向动画,探索如何通过逐步采样来可视化数据分布的形成过程。
之前我们主要关注静态图像,但Matplotlib确实支持动画功能。
这需要在Jupyter Notebook中使用一个名为ipympl的独立库,我已经为大家安装好了。
动画功能依赖于这个库,因为它为我们提供了一个新的后端——widget后端。
现在让我们启用它。在Jupyter中启用新后端,我们使用Matplotlib的魔术函数。这是一个为我们编写的函数,以百分号开头,是一个单元格函数。我们输入%matplotlib widget,这表示使用widget后端初始化Matplotlib。你必须在笔记本的最开始,或者在使用Matplotlib之前执行此操作,一旦设置就无法更改。
我还将导入一些其他必要的库,包括numpy。
同时,我将导入matplotlib.animation模块,并命名为animation,我们将使用它。
Matplotlib的动画模块包含了构建动画的重要辅助工具。对于今天的讨论,我们将使用最重要的对象——FuncAnimation。它通过迭代调用一个由你定义的函数来构建动画。该函数的任务是清除坐标轴对象并重绘动画的下一帧。
然后,这些帧被堆叠在一起,形成一个可播放的视频。
我喜欢在密歇根大学的课堂上使用这个演示,向学生展示如何从分布中采样。我们将构建一个基础动画,从正态分布中抽取100个样本。

现在让我们生成数据。我将设置n = 100,这是样本数量。
然后,我将创建x作为样本列表,我们将从随机正态分布中抽取这些样本。
接下来,我们需要创建执行绘图功能的函数。我将这个函数命名为update,你可以随意命名。Matplotlib的FuncAnimation对象将每隔几毫秒调用此函数,并传入从0开始的帧编号。我们可以将其用作我们称为x的值数组的索引。
我们首先要检查当前帧是否已到达列表末尾。如果是,我们将告诉动画停止。我们通过调用附加在此FuncAnimation对象上的事件源对象的stop方法来实现。
让我们来看看具体实现。
我将创建执行绘图的函数,命名为update,它将传入curr参数,curr是当前帧,是一个从0开始的整数值。
我首先要检查动画是否到达最后一帧。如果是,我将停止动画。因此,如果curr等于n(即100),那么我将调用a.event_source.stop()。这里的a是什么?实际上,它是我们稍后将定义的一个对象,它将位于此函数外部,但由于Python允许我们访问全局作用域中的变量,我们仍然可以访问它。
现在开始实际工作。我们首先要清除当前坐标轴,可以通过调用plt.cla()(清除坐标轴)来实现。

现在我只想绘制一个直方图,但我会将bins(柱状图的分箱)设置为一个可预测的值,这样它就不会跳动。你可以调整这个值,我将使用np.arange(-4, 4, 0.5)。
然后,我们将使用传入函数的当前帧编号进行hist调用。这里,我将从x中切片出数据,即直到当前帧的所有数据。第一次循环时只有一个项目,第二次循环时有两个项目,第三次循环时有前三个项目,依此类推。我将始终将bins设置为100,这样分箱数就不会跳动。我还想设置坐标轴的限制。最后,我将添加一些漂亮的标签,使其看起来更美观。
我们的大部分繁重工作已经完成,但现在我们需要查看它。为此,我们只需生成一个新图形,然后调用FuncAnimation构造函数,并将其赋值给变量a,因为我们需要能够在刚刚编写的update函数中停止它。

第一个参数是我们正在处理的图形。这并不十分重要,因此我们将使用pyplot脚本接口来管理图形。
然后是函数名称和我们希望更新之间的时间间隔。我将设置为100毫秒,这个速度应该较快但不会太快。
一旦我们全部设置好,启动实际上非常快。我将调用animation.FuncAnimation,并传入一个图形。记住,plt.figure()将创建一个新图形。我也可以使用plt.gcf()(获取当前图形),或者创建一个存储在对象中的图形然后传入该对象。这里有很多选项。
然后传入我们使用的函数引用,在这个例子中是update(我们编写的函数,如果你更改了名称,可以在这里更改),接着是我们的时间间隔(以毫秒为单位)。最后,我们告诉widget后端是时候开始展示了。
我们可以看到,我们从x中提取这些值,并逐渐构建出该分布的样子。如果你在自己的系统上运行此代码,应该会看到不同的结果,但大致应该与此相似。它很可能不同,因为我没有设置种子值,所以我们得到的是不同的随机值。这很好地演示了当你从分布中采样时,随着样本数量增加,它越来越清晰地呈现出传统的钟形曲线形状。
我们看到这个后端还提供了其他一些功能,我们可以保存图像、平移图像等等,这些功能在各种不同情况下都很有用。

好的,我们完成了。这是一个很好的例子,展示了如何使用动画FuncAnimation来演示从分布中采样的过程。
与动画包中的大多数其他类一样,FuncAnimation是Animation对象的子类。
Animation对象有一个方便的函数,允许你将图像写入文件,即save函数。但这需要额外的第三方库,如FFmpeg,安装和设置可能需要一些时间。不过,结果是你可以相当轻松地将动画直接从Jupyter网络编程环境导出为M4V、MOV、动态GIF等格式。
想象一下,一个包含四个子图的图形动画,每个子图对应一种我们可能感兴趣理解的分布类型,这实际上会非常酷。我想我们可以在一个子图中绘制正态分布的样本,在另一个子图中绘制伽马分布的样本,也许再绘制几个参数化分布,比如具有不同标准差水平的正态分布。
这实际上将是练习你在本模块中学到的技能的好方法,因为它要求你使用直方图管理带有动画的多个子图。如果你想接受这个挑战,我将在课程页面中提供一个链接,指向一个可选的练习笔记本供你查看。

在本节课中,我们一起学习了如何在Jupyter Notebook中使用Matplotlib的FuncAnimation工具创建动画。我们了解了启用widget后端、定义更新函数来绘制每一帧、以及控制动画流程的基本步骤。通过构建一个从正态分布逐步采样的动画示例,我们直观地看到了数据分布是如何随着样本增加而逐渐形成的。
50:交互组件演示 🎛️


在本节课中,我们将学习如何在Jupyter Notebook中使用交互式组件(Widgets)。这些组件允许我们在笔记本内创建简单的仪表板,从而更直观地探索和分析数据。我们将重点介绍interact装饰器的使用,并通过一个真实的骑行数据集来演示如何创建交互式图表。
导入必要的库
首先,我们需要导入一些必要的库。我们将使用ipywidgets库来创建交互式组件,pandas来处理数据,以及matplotlib来进行绘图。
from ipywidgets import interact
import ipywidgets as widgets
import pandas as pd
import matplotlib.pyplot as plt
加载数据集
接下来,我们将加载一个真实的骑行数据集。这个数据集包含了骑行过程中的时间戳和各种传感器数据,如位置、海拔、速度、心率等。
df = pd.read_csv('biking_data.csv', index_col='timestamp')
df.head()
数据预处理
并非所有列都适合用折线图进行可视化。因此,我们将筛选出数值类型的列,以便后续进行交互式绘图。
potential_columns = [col for col in df.columns if df[col].dtype != 'object']
创建绘图函数
现在,我们将创建一个绘图函数plot_data。这个函数可以绘制一列或两列数据。如果传入一列数据,它将用默认的蓝色绘制;如果传入两列数据,它将用蓝色和红色分别绘制,并确保x轴(时间)相同,而y轴可以不同。
def plot_data(column_names):
fig, ax = plt.subplots(figsize=(10, 6))
ax.set_title(' vs '.join(column_names))
if len(column_names) == 2:
ax.plot(df.index, df[column_names[0]], color='blue', label=column_names[0])
ax_twin = ax.twinx()
ax_twin.plot(df.index, df[column_names[1]], color='red', label=column_names[1])
ax.set_ylabel(column_names[0])
ax_twin.set_ylabel(column_names[1])
else:
ax.plot(df.index, df[column_names[0]], color='blue', label=column_names[0])
ax.set_ylabel(column_names[0])
plt.show()
使用interact装饰器
为了使绘图函数具有交互性,我们可以使用interact装饰器。装饰器是Python中一种特殊的函数,它可以修改或增强其他函数的行为。ipywidgets库提供的interact装饰器允许我们为函数参数创建交互式控件。
以下是使用SelectMultiple小部件创建交互式绘图的示例:
@interact(column_names=widgets.SelectMultiple(
options=potential_columns,
value=[potential_columns[0]],
description='Columns'
))
def interactive_plot(column_names):
plot_data(column_names)
运行上述代码后,Jupyter Notebook会显示一个下拉列表,允许用户选择一列或多列数据进行可视化。图表会根据用户的选择动态更新。
更多交互示例
除了SelectMultiple小部件,我们还可以使用其他类型的小部件,如IntRangeSlider和Text小部件,来创建更复杂的交互式图表。
例如,我们可以创建一个允许用户根据心率范围和时间范围筛选数据的交互式图表:
@interact(
heart_rate_bounds=widgets.IntRangeSlider(
min=80,
max=200,
value=[80, 100],
step=1,
description='Heart Rate'
),
start=widgets.Text(value=df.index.min(), description='Start Time'),
end=widgets.Text(value=df.index.max(), description='End Time')
)
def plot_heart_rate(heart_rate_bounds, start, end):
filtered_df = df[(df['heart_rate'] >= heart_rate_bounds[0]) &
(df['heart_rate'] <= heart_rate_bounds[1]) &
(df.index >= start) &
(df.index <= end)]
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(filtered_df.index, filtered_df['heart_rate'], alpha=0.5)
ax.set_xlabel('Time')
ax.set_ylabel('Heart Rate')
ax.set_title('Heart Rate Over Time')
plt.show()
在这个示例中,用户可以通过滑块调整心率的范围,并通过文本框输入起始和结束时间来筛选数据。图表会根据用户的选择动态更新,从而帮助用户更深入地探索数据。
总结
本节课中,我们一起学习了如何在Jupyter Notebook中使用交互式组件(Widgets)来创建动态的数据可视化图表。我们介绍了interact装饰器的基本用法,并通过实际示例演示了如何利用SelectMultiple、IntRangeSlider和Text等小部件来增强数据探索的交互性。

交互式组件为数据科学工作流增添了强大的可视化工具,使用户能够更直观、更灵活地分析数据。虽然ipywidgets库仍在不断发展中,但其现有功能已足够强大,值得我们在日常工作中尝试和应用。
希望你能进一步探索ipywidgets库的其他功能,并与同学分享你的发现和创作。
51:Pandas绘图方法 📊

在本节课中,我们将要学习如何使用Pandas内置的可视化功能来快速探索和展示数据。Pandas的绘图功能基于Matplotlib,但提供了更简洁的接口,特别适合处理Series和DataFrame。
概述:Pandas绘图基础
上一节我们介绍了Matplotlib的基本使用,本节中我们来看看Pandas如何在其基础上提供便捷的绘图方法。Pandas的绘图方法能帮助我们快速生成图表,是数据探索阶段的得力工具。
首先,Pandas在底层使用了Matplotlib,因此我们可以通过设置Matplotlib的样式来改变Pandas图表的默认外观。
Matplotlib自带多种预定义样式,我们可以从中选择以更改图表的默认样式。由于Pandas基于Matplotlib,这也会改变Pandas图表的默认样式。
我们可以使用 plt.style.available 来查看所有可用的预定义样式。这里我们选择 seaborn-colorblind 样式,它将图表的默认颜色改为对色觉障碍更友好的调色板。
创建示例数据
在深入可视化之前,让我们先创建一个DataFrame作为示例数据。
首先,我们设置随机数生成器的种子,以确保数据可重现。
import numpy as np
import pandas as pd
np.random.seed(0)
接下来,我们添加三列随机时间序列数据。我们可以通过对随机数进行累积求和来生成数据。NumPy的 cumsum 函数可以完成数组的累积求和。
dates = pd.date_range('2017-01-01', periods=365)
df = pd.DataFrame(
np.random.randn(365, 3).cumsum(axis=0),
columns=['A', 'B', 'C'],
index=dates
)
df['B'] = df['B'] + 20
df['C'] = df['C'] - 20
我们使用 date_range 将索引设置为2017年的每一天。现在,让我们更直观地查看这些数据。
使用Plot方法绘图
要绘制数据,我们可以在DataFrame上使用 plot 方法。Series和DataFrame的 plot 方法只是 plt.plot() 的一个简单封装。
当我们调用 df.plot() 时,会得到DataFrame中所有列的折线图,并带有标签。
import matplotlib.pyplot as plt
plt.style.use('seaborn-colorblind')
df.plot();
请注意,由于我们使用了新样式,颜色与默认的Matplotlib颜色略有不同。同时,注意这个Jupyter Notebook技巧:在绘图调用末尾添加分号可以抑制不必要的文本输出,这在常规Matplotlib中也适用。
选择不同的图表类型
DataFrame.plot 允许我们绘制多种不同类型的图表。我们可以通过向 kind 参数传递值来选择所需的图表类型。
让我们尝试创建一个散点图,使用df的A列和B列。
df.plot(kind='scatter', x='A', y='B');
我们通过向 kind 参数传递 'scatter' 来更改图表类型。这等同于使用点标记参数调用 plt.plot() 绘制A列和B列。
你也可以通过使用 DataFrame.plot.kind 方法而不是提供 kind 关键字参数来选择图表类型。
创建复杂的散点图
现在,让我们尝试创建更复杂一点的图表。这次,我们想创建一个点的大小和颜色都变化的散点图。
我们将使用 df.plot.scatter 方法。
df.plot.scatter(
x='A',
y='C',
c='B', # 颜色基于B列的值
s=df['B']*2, # 大小基于B列的值(放大2倍)
colormap='viridis' # 使用viridis色图
);
这里,我们可以看到A列和C列相互对照绘制,点的大小和颜色根据B列的值变化。viridis 色图在视觉上特别舒适。
因为 df.plot.scatter 返回一个Matplotlib的 AxesSubplot 对象,我们可以像操作Matplotlib返回的对象一样对其进行修改。
例如,让我们看看将XY子图的长宽比设置为相等时的效果。
ax = df.plot.scatter(x='A', y='C', c='B', s=df['B']*2, colormap='viridis')
ax.set_aspect('equal')
将长宽比设置为相等可以让观察者更容易看出序列A的范围比序列C小得多。
其他有用的图表类型
使用Pandas,我们还可以轻松绘制箱线图、直方图和核密度估计图。
以下是每种图表的简要介绍和示例:
核密度估计图在数据科学应用中非常有用,当你希望从给定样本中推导出平滑、连续的函数时。
# 绘制箱线图
df.plot(kind='box');
# 绘制直方图
df.plot(kind='hist', alpha=0.7, bins=30);
# 绘制核密度估计图
df.plot(kind='kde');
可视化高维数据
Pandas还拥有有助于可视化大量数据或高维数据的绘图工具。
让我们通过加载经典的鸢尾花数据集来探索其中几个工具。
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data"
iris = pd.read_csv(url, header=None)
iris.columns = ['sepal_length', 'sepal_width', 'petal_length', 'petal_width', 'species']
鸢尾花数据集是一个经典的多元数据集,包含三种鸢尾花数百个样本的萼片长度、萼片宽度、花瓣长度和花瓣宽度。
散点图矩阵
Pandas有一个绘图工具,允许我们从DataFrame创建散点图矩阵。散点图矩阵是一种以成对方式比较DataFrame中每一列与其他列的方法。
让我们用它来可视化鸢尾花DataFrame,看看能从数据中获得什么见解。
from pandas.plotting import scatter_matrix
scatter_matrix(iris, alpha=0.8, figsize=(10, 10), diagonal='hist');
散点图矩阵在不同变量之间创建散点图,并在对角线上绘制直方图。这使我们能够快速看到数据中一些更明显的模式。
观察散点图,似乎点存在一些明显的分组,这可能表明存在一些聚类现象。
虽然这看起来令人印象深刻且确实有用,但请意识到,如果你想,你在过去两周已经获得了手动构建此类图表的所有技能。这不过是一个4x4的子图网格,其中一些是直方图,另一些是散点图。
平行坐标图
让我们看看Pandas中的另一个绘图工具,它将帮助我们可视化多元数据。Pandas包含一个用于创建平行坐标图的绘图工具。
平行坐标图是可视化高维多元数据的常用方法。数据集中的每个变量对应于一条等距的平行垂直线。然后,每个观测值的每个变量的值通过线连接起来。
在此示例中,按花的种类对线进行着色,使观察者更容易看到任何模式或聚类。
from pandas.plotting import parallel_coordinates
plt.figure(figsize=(10, 6))
parallel_coordinates(iris, 'species', colormap='viridis');
例如,查看我们的鸢尾花数据集,我们可以看到花瓣长度和花瓣宽度是两个能相当清晰地区分不同物种的变量,山鸢尾的花瓣最长最宽,而维吉尼亚鸢尾的花瓣最短最窄。
总结

本节课中我们一起学习了Pandas强大的内置可视化功能。我们了解了如何设置图表样式、使用 plot 方法快速绘制折线图、散点图、箱线图等。我们还探索了用于高维数据探索的高级工具,如散点图矩阵和平行坐标图。这些工具能帮助我们在数据科学工作流中快速获得对数据的直观理解。
在下一个视频中,我们将学习Seaborn,一个用于统计绘图的强大包。
52:Seaborn可视化库 📊

在本节课中,我们将要学习Seaborn,这是一个基于Matplotlib的Python数据可视化库。我们将了解它如何美化默认图表,以及如何简化复杂图表的创建过程。
概述
Seaborn本质上是Matplotlib的一个封装。它为默认的数据可视化图表添加了更美观的样式,并使得创建特定类型的复杂图表变得更加简单。
为了开始学习,我们首先导入Seaborn库。
import seaborn as sns
设置数据与基础直方图
上一节我们介绍了Seaborn的基本概念,本节中我们来看看如何用它来创建和美化基础图表。首先,我们需要生成一些示例数据。
以下是生成两个相关数据序列的步骤:
- 设置随机数种子以确保结果可复现。
- 创建第一个序列
v1,它包含1000个来自均值为0、标准差为10的正态分布的随机数。 - 创建第二个序列
v2,它是v1的两倍,再加上1000个来自均值为60、标准差为15的正态分布的随机数。
import numpy as np
import pandas as pd
np.random.seed(0) # 设置随机种子
v1 = pd.Series(np.random.normal(0, 10, 1000))
v2 = pd.Series(2 * v1 + np.random.normal(60, 15, 1000))
现在,让我们并排绘制这两个变量的直方图。我们可以设置 alpha=0.7 使直方图半透明,避免相互遮挡。对于 bins 参数,我们可以传入一个由 np.arange 生成的序列来指定具体的分箱边界,这有助于确保两个直方图使用相同的分箱大小进行比较。我们还会添加图例来区分两个变量。
观察直方图,我们可以快速看出 v1 的均值低于 v2,并且 v2 的数据分布比 v1 更分散。值得注意的是,即使我们只使用了Matplotlib进行绘图,仅仅导入Seaborn这一操作就已经将图表的样式从Matplotlib的默认风格改变为Seaborn的风格。
堆叠直方图与核密度估计图

接下来,让我们以另一种方式可视化这些直方图。首先,我们将 v1 和 v2 放在一个列表中传入绘图函数,并将直方图类型设置为 barstacked。设置 normed=True 会将直方图归一化为概率密度。

然后,我们创建一个组合了 v1 和 v2 的新变量 v3,并在堆叠直方图的上方绘制其核密度估计图。核密度估计图用于估计变量 v3 的概率密度函数,将其与 v1 和 v2 的堆叠直方图叠加,可以帮助我们理解这两种可视化方式之间的关系。
Seaborn为这类图表提供了一个便捷的函数 sns.distplot。让我们看看它是如何工作的。我们可以传入想要可视化的变量,以及用于设置图表各个组成部分的关键字参数。
v3 = pd.concat([v1, v2]) # 合并数据
sns.distplot(v3, hist_kws={'color': 'teal'}, kde_kws={'color': 'navy'})
联合分布图
现在,让我们看一个Seaborn为复杂图表提供便捷接口的例子:联合分布图。联合分布图会创建一个散点图,并在每个坐标轴上显示对应变量的直方图。实际上,你在第二模块中已经见过联合分布图,并且自己手动创建过。
要创建联合分布图,我们只需输入 sns.jointplot 并传入两个序列 v1 和 v2。设置 alpha=0.4 有助于可视化重叠的点。联合分布图允许我们同时观察两个变量的单独分布以及它们之间的关系。通过联合分布图,我们可以看到 v1 和 v2 似乎是呈正相关的正态分布变量。
因为Seaborn基于Matplotlib,我们可以使用Matplotlib工具来调整图表。Seaborn中的一些绘图函数返回一个Matplotlib轴对象,而另一些则对整个图形进行操作,并生成包含多个面板的图表,返回一个Seaborn网格对象。在这两种情况下,都可以使用Matplotlib来进一步调整图表。
例如,sns.jointplot 返回一个Seaborn网格对象。我们可以使用 grid.ax_joint 从中提取出一个Matplotlib轴子图对象,然后使用 set_aspect('equal') 将纵横比设置为相等。
六边形箱图与二维核密度图
六边形箱图是直方图的双变量对应物。六边形箱图显示落在六边形箱内的观测数量。sns.jointplot 包含一种六边形箱样式,我们可以通过向 kind 参数传入 'hex' 来使用它。六边形箱图样式适用于相对较大的数据集。
可视化数据集分布的另一个好选择是核密度估计图。你可以将二维KDE图视为六边形联合分布图的连续版本。
首先,我们使用 sns.set_style('white') 告诉Seaborn使用不同的样式。之后的所有图表都将关闭灰色网格线。
现在,我们将像之前一样创建一个联合分布图,但这次将 kind 参数设置为 'kde'。最后,设置 space=0,这会将边缘分布直接绘制在散点图的边框上。
我们可以看到,Seaborn有一些内置选项可以轻松实现自定义,并为我们提供了易于创建且美观的图表,用于探索数据的分布。
分类数据可视化:鸢尾花数据集
在本教程的最后一部分,让我们加载鸢尾花数据集,看看Seaborn如何处理分类数据的可视化。
与Pandas类似,Seaborn有一个内置函数可以创建散点图矩阵。我们传入鸢尾花数据框,使用 hue 参数告诉它将 name 映射到不同的颜色,并告诉它沿对角线使用KDE图而不是默认的直方图。
观察散点图矩阵,可以清楚地看到数据集中存在一些聚类。看起来花瓣长度和花瓣宽度是区分观测值的良好选择,而萼片宽度则不是一个强区分因子。使用散点图矩阵查看数据框是探索性数据分析中一个非常有用的工具。
我想展示的最后一个图表叫做小提琴图。你可以把小提琴图看作是一个信息更丰富的箱线图。为了演示,让我们将小提琴图与小提琴图旁边的蜂群图一起绘制,你可以将蜂群图视为分类数据的散点图。
首先,创建一个新的图形和一个子图。接下来,创建蜂群图,传入 name、petal_length 和 iris 数据框。对于下一个子图,我们再次创建小提琴图,传入 name、petal_length 和 iris 数据框。
观察蜂群图,每个物种都有自己的列,每个观测值的花瓣长度都被显示出来,更常见的值表现为簇中较宽的部分,很像直方图。小提琴图就像是一个箱线图,两侧各有一个旋转的核密度估计。小提琴图比箱线图传达了更多的信息,并且能够显示箱线图无法传达的分布中的特定现象,例如多峰性。

总结
本节课中我们一起学习了Seaborn可视化库。Seaborn不仅增加了新的样式和视觉元素,还引入了新的功能,如联合分布图、散点图矩阵和小提琴图。Seaborn正在积极开发中,易于安装,这使其成为数据科学家工具包中不可或缺的一部分。
但请记住,这些功能是建立在Matplotlib之上的,并且很大程度上使用的是Matplotlib的绘图元素。因此,如果你对新的统计可视化有任何想法,或者在出版物或期刊中读到过相关的内容,你应该有能力创建自己的可视化工具包。

Seaborn教程到此结束。祝你绘图愉快!📈
53:地理信息与地图绘制 🗺️

在本节课中,我们将学习如何将地理数据可视化。我们将探索两种不同的方法:一种是在静态地图图像上叠加数据点,另一种是使用交互式地图库来创建动态、可缩放的地图。
概述
以书面形式捕捉物理景观特征,或许是信息可视化最古老、最常见的例子之一,至今仍在使用。地理信息系统(GIS)是高度专业化且复杂的,由无数独特的技术和工具组成。在本讲中,我们将初步涉足这个世界,展示如何利用GIS在计算叙事中获取洞察。
我们将从导入几个常用库开始:Pandas、NumPy和Matplotlib的Pyplot接口。我们还会通过设置RC参数来设定一些Matplotlib的默认值,以确保在整个分析过程中图形尺寸保持一致。请务必在开始使用Matplotlib之前,在笔记本的顶部进行此设置。
import matplotlib.pyplot as plt
import numpy as np
plt.rcParams['figure.figsize'] = [16, 8] # 设置图形尺寸为16x8英寸
加载和检查数据
现在,我将导入一些关于骑行的数据。这是一个有趣的数据集,名为 wipeout.csv。我将用Pandas读取它,并查看一下。
import pandas as pd
data = pd.read_csv('wipeout.csv')
data.head()
该数据由纬度和经度、时间戳(看起来大约是秒级频率)以及测量值(如心率、每分钟心跳次数、增强海拔高度(可能以英尺为单位)、速度等)组成。
理解坐标与投影
在绘制地图时,我们首先需要考虑经度和纬度。数千年来,制图师们提出了不同的方法,将一个基本是圆形的球体转换成二维矩形,这些方法被称为“投影”。如果你要处理大量制图数据,无疑需要学习其中一些投影。
我并非这些不同投影和地图坐标存储方式的专家,但即使在这个简单的例子中,数据看起来也有些奇怪:为什么我们只有一个数字?我习惯在使用谷歌地图时看到带有小数点或小数位的数字。
原来,这个特定的数据源试图以无符号整数的形式捕获最大精度。因此,我们首先需要将这些数据转换为更传统的十进制格式。为此,你需要查阅文档来确定如何进行转换,但这里我将为你省去这一步,因为我已经知道方法了。
# 将坐标转换为更常见的GPS坐标格式
data['position_lat_degrees'] = data['position_lat'] * 180 / (2**31)
data['position_long_degrees'] = data['position_long'] * 180 / (2**31)
但这并不能完全满足我们的需求。地球是近似球形的,而我们要在屏幕上查看它,屏幕是平面的。因为我们将在二维图像上绘图,所以希望使用墨卡托投影。我将分享将纬度(以度为单位)转换为平面墨卡托投影的代码,这实际上来自OpenStreetMap项目。
import math
def lat_to_y(lat):
return math.log(math.tan(lat / 360.0 * math.pi + math.pi/4.0)) / (math.pi / 180.0) * 20037508.34 / 180.0
data['position_lat_degrees_mercator'] = data['position_lat_degrees'].apply(lat_to_y)
最后,我想删除任何缺失值。目前这是一个简化处理,在实践中可能并非最佳选择,但我只是想稍微减少数据量。
data = data.dropna()

方法一:在静态地图图像上叠加数据
我想展示的第一个绘图方法可能是最简单的。其核心思想是,我们将在一个轴对象后面渲染一张图像,然后在这个轴对象上使用常规的绘图功能。为此,我们需要一张图像,并且需要知道该图像的坐标边界。然后,我们可以设置所谓的“范围”,它代表了地图的边界。这意味着轴对象后面的图像将与轴对象本身使用相同的坐标系,绘图将被锁定。
首先,我通过从OpenStreetMap导出获得了我的图像,并将其保存在Coursera中,名为 map.png。你也可以直接从OpenStreetMap获取地图。为了显示它,我们将使用Pyplot的 imread 函数,并与 imshow 配对使用。
# 读取地图图像
map_img = plt.imread('map.png')
# 显示图像并设置其范围(坐标边界)
plt.imshow(map_img, extent=[-122.55, -122.35, 37.68, 37.85])
plt.show()

很好,这创建了一张地图,我们可以看到x轴和y轴受我设置的范围约束。范围本身并不重要,你可以选择任何值,只需确保它与你的地图对齐即可。在这种情况下,我知道我从OpenStreetMap导出的地图的范围是什么。
在地图上叠加数据
现在,将我们的数据叠加到这个图上实际上非常简单。我们只需使用Pyplot中我们感兴趣的任何绘图函数。在这种情况下,我将使用散点图函数,并添加一个颜色条,根据数据中的心率列来改变绘制点的颜色。
以下是具体步骤。首先,我重新显示图像(因为在Jupyter中,默认情况下每个单元格运行后会关闭图像)。然后,我使用经度和墨卡托投影后的纬度数据,通过 c 参数指定点的颜色序列,并通过 cmap 参数选择不同的颜色映射。同时,我只绘制那些实际出现在地图上的点。
# 重新显示地图
plt.imshow(map_img, extent=[-122.55, -122.35, 37.68, 37.85])
# 筛选出在地图范围内的数据点
mask = (data['position_long_degrees'].between(-122.55, -122.35)) & \
(data['position_lat_degrees_mercator'].between(37.68, 37.85))
filtered_data = data[mask]
# 绘制散点图,颜色表示心率
scatter = plt.scatter(x=filtered_data['position_long_degrees'],
y=filtered_data['position_lat_degrees_mercator'],
s=10, # 点的大小
c=filtered_data['heart_rate'],
cmap='Blues',
alpha=0.6)
# 添加颜色条和标签
plt.colorbar(scatter, label='Heart Rate (BPM)')
plt.title('Cycling Route with Heart Rate Overlay')
plt.show()
很好!我们得到了一张地图,上面绘制了一些点,显示了不同的心率测量值。观察这张图,你会发现颜色较浅、透明度较高的圆圈接近每分钟100次心跳,而颜色较深、实心的圆圈接近每分钟135次心跳。根据内部知识(因为骑自行车的人就是我),我可以告诉你左下角的街道实际上是我骑车上坡的路段,而这里的部分是我下坡或滑行的路段,所以我的心率在这里没有那么高。
方法一的局限性
这种使用带有范围的图像创建地图的方法简单可靠,但速度不快,也不够无缝。如果我们把完整的数据框放上去,就会明显看出我们需要找到具有正确范围的地图来进行绘图,并且需要不断查找我们想要导出作为背景图像的不同地图。
让我们看看完全相同的代码,但使用所有数据。这实际上对我们来说更容易,因为我先显示图像,然后直接散点绘制所有内容,不需要那个大的布尔掩码。
plt.imshow(map_img, extent=[-122.55, -122.35, 37.68, 37.85])
plt.scatter(x=data['position_long_degrees'],
y=data['position_lat_degrees_mercator'],
s=5,
c=data['heart_rate'],
cmap='Blues',
alpha=0.3)
plt.colorbar(label='Heart Rate (BPM)')
plt.title('Full Cycling Route (Limited Map View)')
plt.show()
现在我们看到了我的整个骑行路线,但地图上只有右下角这一小块区域。
方法二:使用交互式瓦片地图
大多数网络系统中采用的另一种地图方法是使用所谓的“瓦片服务器”。瓦片服务器实际上创建了一个不同缩放级别的地图矩阵,然后根据客户端的请求提供地图的部分(瓦片)。例如,谷歌地图就是这样工作的,它创建了一种响应式体验,代价是稍微脆弱一些,因为需要网络访问。
这种范式在Jupyter笔记本中也可以通过一个名为Folium的项目获得。这个项目完全是客户端的JavaScript,负责地图请求和渲染。为了将其连接到我们的Python后端,我们可以使用Folium项目。但请注意,这是一个新的库,它使我们脱离了Matplotlib的世界,但我觉得你可能会发现它相当好用、自然,并且如果你真的开始查看地图并在地图上绘图,可能会想使用它。
import folium
现在,我们想从之前的数据中渲染一个点。为此,我选择了地图的中心点,然后设置一个缩放级别。这里有不同的缩放级别可供选择,你将能够在这里上下滚动。需要注意的关键点是,对于这个库,我必须反转我们的经度和纬度顺序,并且我没有使用经度的墨卡托值。欢迎来到GIS系统,每个系统的做法都略有不同。
# 创建地图,中心点为数据的第一行坐标
center_lat = data.iloc[0]['position_lat_degrees']
center_lon = data.iloc[0]['position_long_degrees']
m = folium.Map(location=[center_lat, center_lon], zoom_start=14)
m
你会立即注意到用户体验很好。一旦我们绘制数据,你仍然可以看到数据,但你实际上可以像在谷歌地图中一样放大、缩小和移动地图。当你移动到没有瓦片的地方时,系统会自动获取它们。
在地图上添加标记和路线
我们可以使用 Marker 类在地图上添加标注。让我们为起点和终点设置这个。
# 重新创建地图
m = folium.Map(location=[center_lat, center_lon], zoom_start=14)
# 添加起点标记
start_lat = data.iloc[0]['position_lat_degrees']
start_lon = data.iloc[0]['position_long_degrees']
folium.Marker([start_lat, start_lon], popup='Start').add_to(m)
# 添加终点标记
end_lat = data.iloc[-1]['position_lat_degrees']
end_lon = data.iloc[-1]['position_long_degrees']
folium.Marker([end_lat, end_lon], popup='End').add_to(m)
m
这些操作会放置小图钉,指示路线的起点和终点。
但我也想映射整个骑行路线。Folium文档指出,Polyline 是适合使用的类。Polyline 接受一个由元组组成的位置列表,这意味着我们必须将纬度和经度值成对组合。这实际上可以通过Python的 zip 函数轻松实现。
# 创建包含所有坐标点的列表
locations = list(zip(data['position_lat_degrees'], data['position_long_degrees']))
# 在地图上添加折线(骑行路线)
folium.PolyLine(locations=locations, weight=5, color='blue').add_to(m)
m
这是一个很好的渲染效果!你可以在这里看到我的骑行路线,可以放大缩小我的骑行路线,可以看到起点和终点的小图钉,还可以看到我去过的地方。当你开始查看数据时,这是一种非常流畅的方法。即使我们使用的是折线,这些数据点中的每一个实际上都直接绘制在上面,然后作为折线的一部分自动为我们连接起来。
总结
在本节课中,我们初步探讨了可以混合GIS结构和数据完成的一些信息映射。我们看到了几种不同的方法:从在简单地图上叠加数据图,到使用Folium在基于JavaScript的地图上绘图。GIS系统是一个巨大的领域,但了解一些关于数据空间表示的知识,对于物理世界成为你调查一部分的那些时刻非常有帮助。
本节课中我们一起学习了:
- 如何加载和转换原始地理坐标数据。
- 使用Matplotlib在静态地图图像上叠加和可视化数据(如心率)。
- 使用Folium库创建交互式、可缩放的网络地图,并添加标记和路线。
- 理解了不同地图投影和库之间坐标处理的差异。


地理可视化是一个强大的工具,可以将位置数据转化为直观的洞察。
54:Python用于数据科学实践(第4课)📊

独立数据科学家成长指南 🧭

在本节课中,我们将学习如何完成一项独特的模块作业。这项作业旨在帮助你构建个人作品集,并展示你在数据科学领域的实践能力。
作业概述
本模块的作业是为你量身定制的。这项作业将允许你在作品集中展示你的成果。
你可以将作业设计得富有挑战性和洞察力。你可以尝试解决一个现实世界的问题。你可以让作业停留在表面层面,并展示你在这门课程中学到的技术。
选择权在你手中。
作业启动与定制化问题
当你登录到第4次作业的Jupyter笔记本时,你会收到一个定制化的问题。
这个问题会给你一个世界上的区域,以及一个主题。
我们尝试在城市级别生成区域。但你也可以使用州或省级别的数据,或者全国级别的数据,只要它能涵盖实际分配给你的区域。
主题将从一系列我感兴趣的列表中选出。这些主题包括体育与运动、政治事件或天气与环境主题。我们未来可能会添加更多主题。
数据收集与分析任务
在此之后,你将被要求在网上找到至少两个可用的数据集,并使用它们来回答一个你选择的问题。
现在,我设计这项作业的动机很简单。
我想更多地了解这个世界。我希望你通过优秀的信息可视化来帮助我学习。
我希望你开始将你的技能应用于现实世界的数据。我希望你获得从定义模糊的问题开始,经历使用pandas进行数据收集和清理,再到使用Matplotlib进行数据表示的全过程经验。
成果分享与展望
最后,我很乐意与其他人分享这门课程中一些出色的可视化成果。
如果你不介意我分享你的解决方案,只需在你将其上传到同行评审系统时注明即可。
我期待着看到你的成果。😊
总结
本节课中,我们一起学习了如何完成一项个性化的数据科学作业。我们了解了作业的目标是构建作品集和解决现实问题,掌握了从接收定制化问题(涉及特定区域和主题)、自主寻找数据集、应用数据清洗与分析技术(使用pandas),到最终创建信息可视化(使用Matplotlib)并可能分享成果的完整流程。这项作业的核心是鼓励你将所学技能应用于实践,并开始你的独立数据科学家之旅。
55:机器学习导论 🚀

在本节课中,我们将要学习机器学习的基本概念、其重要性以及它在数据科学中的应用。我们将探讨机器学习如何解决传统编程难以处理的问题,并了解几个关键的应用领域。
大家好,欢迎来到密歇根大学在线课程《应用数据科学》的应用机器学习部分。
我是凯文·柯林斯·汤普森,是密歇根大学信息与计算机科学的副教授。
我将担任本部分课程的讲师。
我拥有约20年的研究和开发经验,将机器学习应用于学术界和工业界的各种问题,从语言辅导软件到商业搜索引擎。
这是一个参与机器学习极其令人兴奋的时代。
我希望本部分课程能激励你更深入地探索这个迷人的世界。
本周,你将学习什么是机器学习以及它为何对数据科学至关重要。
你将了解机器学习如何应用于我们信息经济中的关键问题,以及如何在Python中设置你的第一个机器学习应用。
什么是机器学习?🤔
上一节我们介绍了课程概述,本节中我们来看看机器学习的定义。
在许多情况下,当需要解决一个计算问题时,例如从数据库中存储和检索数据,我们通常的解决方法是编写一个程序,手动指定一系列需要运行的编程步骤来解决该特定问题。
这种方法对于绝大多数计算机科学问题非常有效。
然而,并非所有问题都适合通过编写手工制作的程序或一套规则来有效解决。
例如,你如何用编程语言写下一套规则,来准确地将人类语音转换为文本?这个过程被称为语音识别,现在已在数百万智能手机上使用,或应用于世界各地的客户支持系统。
考虑到人类语音的微妙和复杂性,包括大量不同的发音、词汇、口音等,手工编写一套程序规则来识别音频信号的部分内容并决定信号中包含哪些单词等,将是一项艰巨的任务。
即便如此,它在识别不同类型的语音时可能仍然不够灵活,也不够稳健。

此外,如果我们需要定制系统,使其能够识别新单词或我们现有规则中未编码的其他特征,我们将不得不编写一套全新的规则,这将是一项极其困难的任务。
另一方面,机器学习为我们提供了技术,使我们能够从称为训练数据的标记示例中,自动高效地学习这些复杂的规则。这种方式比尝试手工编程所有规则要准确和灵活得多。
而且,由于我们未来看到的任何示例都不太可能与训练集中的内容完全匹配,因此有效机器学习算法的一个主要目标是能够泛化,即正确预测或识别在训练期间未见过的新对象。
所以,机器学习的基本问题是探索计算机如何自我编程以执行任务,并在获得更多经验时自动提高其性能。

这种经验可以以多种不同格式或情境的数据形式出现,例如用于训练系统初始结构的标记示例(如电子邮件垃圾邮件检测系统)。
系统可以从用户那里获得反馈,例如搜索引擎获取搜索页面上的点击数据。
系统也可以从随时间收集的周围环境中获取数据,例如自动驾驶汽车可以检测附近的物体和事件,并学会更可靠地移动。
机器学习的学科基础 🧠
为了有效且高效地实现这一目标,机器学习大量借鉴了统计学和计算机科学。
这些统计方法为机器学习算法提供了从数据中推断结论、从数据中学习以及评估这些结论可靠性的方法。
另一方面,计算机科学方法为机器学习算法提供了解决问题的计算能力,包括有效的大规模计算架构以及用于捕获、操作、索引、组合数据和进行预测的算法。
试想一下,商业搜索引擎如何能快速地将你的查询与数十亿个网页进行匹配,并几乎瞬间返回一组有用的结果。
这正是结合统计方法与计算机科学力量的理想例证。
机器学习与其他领域之间的联系也在不断增长,这非常引人入胜。
例如,关于个体或系统如何在给定环境中优化其性能的问题,也与生物学中关于生物体如何觅食的研究有共通之处。
它还与经济学的相关研究有联系,涉及如何计算最优定价和市场结构。
甚至与心理学,特别是人类学习的模型和因素相关,并利用这些来影响机器学习技术的设计。
因此,这个不断发展的机器学习领域真正汇集了多个科学领域的见解。
这也是我发现它如此迷人的原因之一。
机器学习的应用实例 💡
机器学习算法现在越来越多地参与到日常生活的各个方面,从你阅读和观看的内容,到你购物的方式,再到你遇见的人以及你的出行方式。
以下是几个例子。
欺诈检测:每次你使用信用卡购物时,机器学习算法会立即检查你的购买行为,以验证这是否可能是一笔欺诈交易。为此,这些算法会查看你刚刚完成的当前交易的特征,例如时间、地点和金额。它们会根据该购买行为是否与你先前购买的特征一致,来预测其是否为欺诈。系统还会记录用户关于交易是否确实为欺诈的任何反馈,并利用该反馈在未来做出更好的预测。
搜索和推荐系统:这也是机器学习应用的一个巨大领域。事实上,机器学习算法是商业搜索引擎工作的核心。从你开始输入查询的那一刻起,一个算法可能会监控你的按键,以预测在你输入时最佳的自动补全查询。然后,一旦你完成查询,其他一些机器学习算法将被应用,以确定你为该查询看到的网页的选择和排名。此外,其他机器学习算法将决定你在页面上看到哪些广告(如果有的话),系统为你建议哪些相关查询等等。此外,搜索引擎通常使用你与搜索站点交互的数据,例如你点击了哪些页面、阅读页面的时长,来提高其未来的有效性。
以类似的方式,电影推荐网站使用机器学习算法,根据你过去的评论和与网站的互动模式,以及你的偏好与其他用户偏好的关系,来建模你过去喜欢什么。它利用这些数据学习一个关于你个人品味的模型,以期为你提供更好的选择,让你更投入地使用网站,或引导你随着时间的推移购买更多电影。
最后,第三个在过去几年中在识别质量方面取得显著进步的示例领域是语音和图像识别。尽管研究人员已经致力于语音和图像识别数十年,但最近,计算能力的提升、更好的算法(包括深度学习的使用)以及数据量的大幅增加,共同导致了识别算法在过去五年左右在准确性和灵活性上的显著飞跃。现在,识别技术已经足够可靠和快速,可以作为智能手机和家庭设备的基本功能,这些设备可以响应语音命令或问题。计算机视觉和图像识别现在可以越来越准确地识别图像或视频中的物体和动作。甚至有一段时间以来,超过85%的手写邮件由美国邮政服务以非常高的准确率自动分拣。在医学领域也有非常令人兴奋的进展,正在开发图像识别算法来将癌细胞分类为恶性或良性,估计最佳个性化治疗方案,或在医学图像中发现难以检测的肿瘤。当然,自动驾驶或自主车辆广泛使用实时图像识别和视频处理,以及其他技术如强化学习。
这些例子仅仅触及了机器学习在当今社会中如何应用的表面。在本课程的进展过程中,你将会遇到并处理其他一些例子。
关于“应用”机器学习 📘
本课程模块的名称实际上是“应用机器学习”。那么“应用”部分意味着什么?当我们设计这个数据科学系列时,我们觉得需要一门课程,更侧重于如何在高层次上正确应用和解释机器学习算法的结果,而较少关注这些算法内部具体如何工作的技术细节。
已经有许多优秀的现有核心机器学习课程提供了更深层次的技术细节。而这种详细的知识对于使用机器学习算法并将其用于特定应用总是必要的。
事实上,一个非常令人兴奋的趋势正在发生:用于语音识别、语言翻译、文本分类和许多其他任务的即用型机器学习算法,现在正作为基于网络的服务在云计算平台上提供。这极大地增加了能够使用它们的开发人员受众,并使构建应用机器学习的高层次解决方案变得比以往任何时候都更容易。
以下书籍《Python机器学习入门》不是完成本课程所必需的,但我们推荐它作为一个额外的资源,可能对你在学习过程中以及后续参考时有用。像本课程一样,这本书侧重于使用Python中的scikit-learn构建你自己的机器学习任务解决方案的实践细节。它提供了机器学习概念的进一步背景知识,对本课程涵盖的特定主题以及一些附加主题提供了更深入的讲解,并提供了一些额外的编码示例。
总结 ✨

本节课中我们一起学习了机器学习的核心定义,即计算机通过经验(数据)自我编程和改进性能的能力。我们探讨了机器学习与统计学和计算机科学的紧密联系,并了解了它在欺诈检测、搜索推荐、语音图像识别等领域的强大应用。最后,我们明确了本“应用”课程的重点在于如何正确使用和解释机器学习工具,而非其底层算法的全部技术细节。
56:Python数据科学专项课程更新说明 📢

在本节课中,我们将了解密歇根大学《Python用于数据科学实践》专项课程(特别是机器学习课程部分)的最新更新内容。这些更新旨在提升学习体验,确保课程内容与当前技术发展保持同步。
自从我们首次发布Python数据科学专项课程以来,我们非常高兴地看到它帮助了全球数十万学习者追求他们对数据科学的兴趣。
在此过程中,我们收到了大量有益的反馈,并已尽力将这些反馈融入机器学习课程的这一新版本中。
我们投入了大量工作,在此更新版本中为您带来了许多新内容。
以下是本次更新的主要方面:
- 作业与测验:我们大幅修订了作业内容,并同步更新了测验题目。
- 课程材料与代码:我们修订了课程阅读材料,并更新了所有Notebook,以确保它们与Scikit-learn等库的最新版本兼容。这些库在过去几年中也经历了重大发展。
- 讲座视频:最后同样重要的是,我们对现有讲座视频进行了审核,以阐明某些观点、增加更多示例,并在此过程中进行了一些修订和更正。
我们希望您能发现课程的这个新版本非常实用。

请告诉我们您的学习体验,并祝您在数据科学之旅的下一步中好运。
在本节课中,我们一起了解了本次课程更新的背景、目标以及涵盖的具体方面,包括作业、测验、学习材料和讲座内容的全面优化。
57:机器学习核心概念 🎯

在本节课中,我们将学习机器学习的两大主要类型:监督学习和无监督学习。我们将探讨它们的基本定义、典型应用场景以及解决机器学习问题的三个基本步骤。
概述
机器学习任务,例如信用卡欺诈检测、电影推荐和语音识别,可以分为两大主要类型。
监督学习 📊
监督学习的目标是预测与每个输入项相关联的某个输出变量。
如果输出是一个类别,即有限数量的可能性,例如判断信用卡交易是否为欺诈,或者识别语音信号对应的英文单词,我们称之为分类问题。此时,我们学习的函数称为分类器。
如果我们要预测的输出变量不是类别,而是一个实数值,例如汽车从0加速到100公里/小时所需的时间(以秒为单位),我们称之为回归问题。此时,我们学习的是回归函数。
更正式地,我们通常用大写字母 X 表示数据项表格,每行代表一个数据项。与每个数据项关联的标签存储在变量 Y 中。我们的目标是学习一个函数 f,将 X 中的数据项映射到 Y 中的标签。
为此,系统需要一组带有标签的训练样本,即输入 x_i 和输出 y_i。这组带标签的训练样本用于确定将输入映射到期望输出的最佳函数。
例如,在图像识别的监督学习问题中,这涉及构建一个分类器。输入 x_i 可以是描述单张图像的像素集合,期望的标签 y_i 可能是图像中物体的名称。
科学家们开发了许多算法来进行监督学习,用于从训练数据中估计函数 f。我们将在课程中介绍其中一些算法。
监督学习需要有这个带标签对象的训练集来进行预测。但是,如果整个目标就是预测这些标签,那么最初的这组带标签项从何而来?
答案是,训练标签通常由人工标注者提供。为某些问题获取标签的难易程度取决于所需标签数据的数量、提供准确标签所需的人类专业知识水平以及标注任务的复杂性等因素。
像亚马逊的Mechanical Turk或CrowdFlower这样的众包平台,已成为从人工工作者那里获取明确提供标签的重要来源。客户可以将需要标注的机器学习任务与能够利用人类智能提供标签的工人群体联系起来。
这些是更明确获取的标签。我们也可以获取隐式标签。例如,如果搜索引擎检测到用户点击了一个结果链接,然后看到用户在返回搜索引擎之前有一两分钟没有其他活动,系统可能会将该活动用作该页面的某种隐式标签。换句话说,如果用户花了一些时间访问该页面,那么该页面更可能与他们的查询相关。
无监督学习 🧩
在许多情况下,我们只有输入数据,没有任何与之相关的标签。在这些情况下,我们可以解决的问题涉及获取输入数据并尝试在其中找到某种有用的结构。
“结构”是什么意思?通常,这意味着在数据中找到有趣的聚类或分组。一旦我们能以聚类、分组或其他有趣子集的形式发现这种结构,该结构就可以用于任务,例如生成输入数据的有用摘要,或者可视化该结构。
假设你运营一个向客户销售产品的电子商务网站,你可能有成千上万甚至数百万的客户。你可能想知道是否可以将客户分类或分组为不同的类型。例如,可能有使用网站更高级功能的“高级用户”,可能有关注廉价折扣且在网站上停留时间很短的“快速浏览型用户”,也可能有花费大量时间比较不同商品的“谨慎研究型用户”。
如果你能获取人们与网站互动的数据,并使用无监督学习来发现这些不同的群体,那么你可以想象,也许可以为每个群体定制网站的产品推荐,从而提高该群体用户购买产品或获得更好体验的机会。
你事先并不知道有多少个群体,甚至不知道它们是什么样子,而且你没有任何带标签的示例。因此,这是一个经典的无监督学习问题。
另一种非常重要的无监督学习问题是检测对Web服务器的异常访问。出于安全原因,你可能希望在网站用户发出可能是网络攻击的请求时收到通知,或者其行为与网站上典型用户行为非常不同。
由于可能存在许多不同类型的黑客攻击或入侵尝试,我们无法获得可靠的训练标签来使用监督学习训练分类器。相反,我们需要一种无监督的方法,允许我们执行所谓的异常检测。这种方法不假设未来的攻击会与以前的攻击形式相同,但假设网站攻击的某些特征在某种程度上会与平均用户行为不同。
应用机器学习的三个步骤 🔄
假设你遇到一种情况,认为机器学习可能适用,无论是使用监督方法还是无监督方法。你将如何应用机器学习来解决你的问题?有三个基本步骤。我将使用分类作为典型的机器学习场景,因此我经常只使用“分类器”这个术语作为机器学习任务的例子。但我要描述的内容同样适用于其他形式的监督学习(如我们稍后将介绍的回归)和无监督学习(如聚类)。
第一步:问题表示与算法选择
解决机器学习问题的第一步是,你必须弄清楚如何以计算机能够理解的方式来表示学习问题。你需要能够获取你的数据,甚至对你感兴趣识别的对象(例如)进行描述,以便将其用作算法的输入。你还需要决定将哪种类型的学习算法应用于这些数据。
例如,表示图像的方式有很多种。通常,它被表示为一个彩色像素数组。也可能有与图像相关的元数据。如果你想进行欺诈检测,可以考虑如何表示信用卡交易,这可能由交易的时间、地点和金额来表示。
因此,你需要某种方式来表示你的数据,并选择你想应用于数据的算法类型。
第二步:确定评估方法
你需要做的第二件事是确定一种评估方法,为机器学习算法(通常是分类器)的预测或输出提供某种质量或准确性评分。
如果你有一个评估方法,这允许你评估和比较不同分类器的有效性,从而判断哪些分类器对你的特定问题表现良好,哪些是好的,哪些是差的。
例如,一个好的分类器具有高准确率,即它在高比例的情况下做出的预测与正确的真实标签相匹配。
第三步:搜索最优分类器
在应用机器学习解决问题时,你需要做的第三件事是,一旦我们决定了如何表示输入数据、分类器类型和评估方法,我们就需要搜索能给出该问题最佳评估结果的最优分类器。
在本课程中,我们将深入探讨这三个方面,并使用Python解决一些具体示例。我们即将看到一个具体示例,展示如何在Python中使用机器学习库来解决分类问题。
现在,让我们更详细地了解这三个步骤。
问题表示:特征与模型 🛠️
首先,我们来谈谈将问题转换为计算机可以处理的表示意味着什么。这涉及两件事:你需要将每个对象(每个输入对象,我们通常称之为样本)转换为一组描述该对象的特征。其次,你需要选择一个学习模型,即你希望系统学习的那种分类器。
让我们更仔细地看看对象的特征表示是什么意思。
数据集中的每个数据点都代表某个事物、某个对象、情况、事件或实体,由一系列属性列表表示。例如,一封电子邮件可以表示为消息中包含的单词列表。一张图片可以表示为构成图像的像素的颜色值矩阵。一个像这个苹果一样的水果,可以用它的颜色、形状、质地等来表示。对象的这些属性值称为特征。
你可以将包含这种特征表示的输入数据视为函数的输入。你可以很容易地将其可视化为一个表格,其中表的每一行代表一个数据实例,表的列代表对象的特征。
使用特征来表示一个对象可能有很多不同的选择。例如,我展示了一个水果,让我们拿起一个柠檬。柠檬有形状、宽高比,也有质量、味道。我想如果你现在在商店买柠檬,它们还带有一个方便的条形码,用于标识水果的类型。因此,我们可以收集关于一个事物的许多不同类型的信息,并以计算机能够理解的形式呈现。
因此,为机器学习算法找出如何表示对象的问题实际上是一个挑战,它被称为特征工程或特征提取,我们将在课程的第四周介绍。
表示机器学习问题的另一个关键部分是选择适合该问题的分类器类型。本课程将介绍许多不同类型的分类器。它们在准确性、可解释性、速度等方面都有不同的权衡。我们将在本课程的第二周介绍这些。
迭代过程:优化与改进 🔁
因此,解决机器学习任务的过程通常是一个循环,涉及一个迭代过程,如下图所示。我们首先对问题的良好特征和可能合适的分类器做出初步猜测。然后,我们使用训练数据训练系统,进行评估,看看分类器的效果如何。接着,基于哪些有效、哪些无效,哪些示例被正确或错误分类,我们可以进行失败分析,看看系统仍在哪些地方出错。然后,根据失败分析的结果,我们通常会优化特征集。例如,我们可能会发现缺少一个重要特征,而这个特征有助于纠正一些错误。
根据我的经验,这种迭代过程非常常见。事实上,这是用机器学习解决问题的典型方式。通常,你可能需要多次经历这个循环,以不断优化特征并评估它们对准确性的影响,或者根据你选择的评估方法尝试不同类型的分类器,以确定你是否为你的问题找到了正确的方法。

总结

本节课中,我们一起学习了机器学习的核心概念。我们明确了监督学习和无监督学习的区别:监督学习用于预测已知标签,包括分类和回归;无监督学习用于在无标签数据中发现结构,如聚类和异常检测。我们还详细探讨了应用机器学习的三个基本步骤:问题表示与算法选择、确定评估方法以及搜索最优模型。最后,我们了解到解决机器学习问题通常是一个迭代优化特征和模型的过程。
58:Python机器学习工具 🛠️

在本节课中,我们将学习如何开始使用Python进行机器学习应用。我们将介绍几个关键的Python库,这些库是构建机器学习项目的基础。掌握这些工具是后续实践的重要前提。
概述
我们已经了解了机器学习的基本背景和一些主要的问题类型。现在,最好的学习方式就是动手在Python中开始我们自己的机器学习应用。我们将立即开始,为此,我们需要利用几个重要的Python库来支持我们的工作。
核心Python库介绍
以下是支持我们机器学习工作的几个核心Python库。我们将逐一介绍它们的主要功能和用途。
Scikit-learn
我们将用于机器学习的最重要的库叫做 Scikit-learn。Scikit-learn是使用最广泛的Python机器学习库,也是本课程的基础。它是一个持续开发和改进的开源项目,支持非常广泛的、重要的机器学习算法。它拥有优秀的在线文档和非常活跃的用户社区。由于被广泛采用,Scikit-learn在网上有许多示例应用、教程和代码示例。因此,我们建议你在学习本课程时使用的一个有价值的参考资料是 Scikit-learn用户指南 和 API文档,其中包含了库中不同算法的更多细节,以及这些算法支持的各种选项的详细信息。
Scikit-learn使用了另外两个Python科学计算库,称为 SciPy 和 NumPy。
SciPy 与 NumPy

SciPy 是一个Python库,支持科学计算中常用的数据操作和分析方法。这包括对统计分布、函数优化、线性代数和各种专用数学函数的支持。在本课程的某些部分,我们将利用SciPy提供所谓的稀疏矩阵,这是我们存储大部分为零的大型表格的一种方式。关于这一点,稍后会有更多介绍。
NumPy 是一个用于科学计算的Python库,它包含了对Scikit-learn使用的一些基本数据结构的支持,例如多维数组。通常,输入到Scikit-learn的数据将是NumPy数组的形式。
Pandas
Pandas 是一个用于数据操作和分析的Python库。如果你学习了本系列的第一门课程,你已经对Pandas的功能有了一些经验。我们将使用的Pandas支持的关键数据结构叫做数据框,它基本上就像一个带有行和命名列的电子表格。与NumPy支持的数组不同,数据框中的列可以是完全不同的类型。你可以有一列保存字符串值,另一列保存日期,另一列保存浮点数,等等。
Pandas还对以各种格式读写数据有很好的支持,从逗号分隔的CSV文件到数据库使用的结构化查询语言SQL等等。
Matplotlib
Matplotlib 是一个广泛使用的Python 2D绘图库。它可以在各种平台上的不同格式和交互式环境中生成高质量的图形。Matplotlib可以用于Python脚本、Python和IPython Shell、Web应用服务器以及各种不同的图形用户界面工具包。如果你学习了本数据科学系列的第二门课程,你已经有一些使用Matplotlib的经验。在这里,我们将主要使用 matplotlib.pyplot 进行数据分析,因为它只需几行代码就可以创建直方图、条形图、箭头图、散点图等等。

库版本与检查

以下是本课程中使用的主要库的版本。请确保你使用的Scikit-learn和其他库的版本与此处列出的相同或更高。

如果你不确定自己拥有哪些版本,这里有一些示例代码,展示了如何检查你拥有的Scikit-learn、NumPy和Pandas的版本。你可以使用类似的代码来检查其他库的版本。
# 示例代码:检查库版本
import sklearn
import numpy as np
import pandas as pd
import matplotlib
print(f"Scikit-learn version: {sklearn.__version__}")
print(f"NumPy version: {np.__version__}")
print(f"Pandas version: {pd.__version__}")
print(f"Matplotlib version: {matplotlib.__version__}")
安装建议

我们建议使用 Anaconda Python发行版 来安装所有这些库,因为它包含了我们在课程这一部分所需的所有库。如果你有其他现有的Python安装,你可以使用Pip从命令行安装我们将要使用的库。
总结
本节课中,我们一起学习了开始Python机器学习应用所需的核心工具。我们介绍了Scikit-learn作为主要的机器学习库,以及其依赖的SciPy和NumPy库。我们还回顾了用于数据处理的Pandas库和用于数据可视化的Matplotlib库。最后,我们了解了如何检查库的版本以确保环境兼容。掌握这些库是构建有效机器学习模型的第一步。
59:机器学习问题实例 🍎🍊🍋

在本节课中,我们将构建一个极其简单的物体识别系统,以此作为我们首次探索机器学习的实践。虽然我们将使用的例子非常简单,但它确实反映了构建真实世界商业系统时所涉及的许多关键机器学习概念。
概述
我们将使用一个名为“Fruit data with Col.TXT”的小型数据集。该数据集最初由爱丁堡大学的Ian Murray博士创建,用于训练一个分类器来区分不同类型的水果。为了创建这个数据集,Murray博士测量了数十个橙子、柠檬和苹果的高度、宽度、质量等特征。我们对其原始数据进行了略微重新格式化,并添加了一两个模拟特征(如颜色评分)用于教学。
你可能会认为水果预测是一个有点不切实际的场景。考虑到数据集的有限性,这确实是一个“玩具”例子。但实际上,食品公司现在确实依赖机器学习系统进行自动化质量控制,其概念与我们即将构建的系统并无太大不同。例如,确实存在水果运输公司使用的真实系统,可以在加工过程中筛查腐烂的橙子。当然,这些系统使用的特征比我们这里看到的要复杂一些。
理解数据:表格化视角
要解决机器学习问题,你可以将输入数据视为一个表格。在我们的案例中,每一行代表一个对象(即一个水果),而对象的属性(即测量值、颜色、大小等)则由各列中的值表示。
在监督学习问题中,数据集通常还会包含一个特殊的列,即对象的标签。如果数据集本身没有这个字段,有时可以从一个或多个列的信息中推导出来。
在继续之前,请确保你已经运行了以下代码片段,加载我们将要使用的必要库。
import pandas as pd
from sklearn.model_selection import train_test_split

加载与查看数据
我们要做的第一件事是使用pandas中非常方便的read_table命令加载水果数据集。这将从磁盘读取数据集,并将其存储到一个我们称为fruits的DataFrame变量中。
fruits = pd.read_table('fruit_data_with_colors.txt')
现在,让我们查看这个数据集,并输出DataFrame的前几行。
print(fruits.head())
我们可以看到,数据集的每一行代表一个水果,由表格列中的几个特征表示。按顺序,我们看到的列是:
- fruit_label:这是我们将使用的训练标签。它是一个数字,对应水果的一般类型(例如,1代表苹果,2代表橘子,3代表普通橙子等)。这个标签由数据集的创建者提供。
- fruit_name 和 fruit_subtype:这两列包含水果一般和具体类别的文本描述。
fruit_name是同一行中对应fruit_label的文本形式。我们不会将这些名称列用作特征,包含它们只是为了让我们更容易阅读数据集。 - mass、width、height:这些是每个水果的测量值,分别以克和厘米为单位记录其质量、宽度和高度。
- color_score:这个特征存储在一个名为
color_score的列中,它是一个单一的数字,旨在粗略地表示水果的颜色。在真实系统中,这可能会是更复杂的东西,比如颜色分布的直方图,或者来自水果实际图像或视频的像素。但为了我们的目的,我们将只用一个光谱尺度来总结颜色。接近1的分数表示水果是红色的,0.7左右的分数表示黄色,依此类推。
查看这个DataFrame,我们可以看到它包含59行,对应59个被测量并录入表格的不同水果。
定义目标与评估挑战
我们的目标是根据这些数据构建一个分类器,该分类器可以基于任何给定的特征观测值(如质量、高度、宽度和颜色评分)来预测正确的水果类型。例如,我们能否根据颜色评分和尺寸来区分橙子和柠檬,并让分类器仅根据观测到的测量值正确预测水果类型?
现在,假设我们已经有了一个可以使用的分类器,我们如何知道它的预测是否可能准确呢?我们可以选择一个我们已经知道标签的水果样本(称为测试样本),将该水果的特征输入分类器,然后将分类器预测的标签与水果类型的实际真实标签进行比较。
这里有一个非常重要的点:如果我们使用数据中用于训练分类器的带标签水果示例之一,我们就不能将同一个水果样本也用作测试样本来评估分类器。 为什么?
我们的分类器需要具备的一个关键能力是,它需要在任何输入样本、任何我们未来可能看到的新水果上都能良好工作,而不仅仅是在训练集中的那些样本上。因为我们的分类器可以简单地记住训练集中的每一个样本,那么之后对于任何一个相同的样本,它都能轻易地给出正确的标签。因此,使用我们最初用来训练分类器的相同样本来衡量分类器的性能,并不能告诉我们分类器对于从未见过的新水果可能工作得如何。它只会告诉我们关于训练集中已有的信息。
划分训练集与测试集
由于我们唯一的标签数据来源是给定的数据集,为了估计分类器在未来样本上的表现,我们需要将原始数据集分成两部分:
- 一个称为训练集的带标签样本数组,用于训练分类器。
- 保留剩余的带标签样本,并将它们放入第二个独立的数组,称为测试集,用于评估训练好的分类器。
为了从输入数据集中创建训练集和测试集,scikit-learn提供了一个方便的函数来完成这种划分,毫不意外,它叫做train_test_split。以下是我们如何使用它的示例:
X = fruits[['mass', 'width', 'height', 'color_score']]
y = fruits['fruit_label']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=0)
这个函数随机打乱数据集,并将输入样本的一定百分比划分出来作为训练集,然后将剩余的样本放入另一个变量作为测试集。在这个例子中,我们使用了75%(训练)对25%(测试)的划分比例。这是一个相当标准的相对划分比例,是决定训练与测试比例时一个很好的经验法则。
提醒一下,当我们使用scikit-learn时,我们将使用变量X(大写)的不同形式来表示我们拥有的数据,它通常是一个二维数组或DataFrame。而用于标签的符号通常基于小写y,它通常是一个一维数组或标量。

请注意train_test_split函数中random_state参数的使用。这个random_state参数为函数内部的随机数生成器提供了一个种子值。如果我们为这个种子值选择不同的值,将导致不同的随机化训练和测试划分。如果我们希望每次都能得到相同的训练和测试划分,只需确保传入相同的random_state参数值。因此,在我们的所有示例中,我们将该参数设置为0。
train_test_split函数会将训练集放入X_train,测试集放入X_test,训练标签放入y_train,测试标签放入y_test。这是将原始数据按75%-25%的比例划分成这两部分。这几乎是我们所有编码中都会使用的变量名和约定。
我们将不带标签的数据行(训练实例)放入这个大写X变量中,并将这些行对应的标签列表放入一个名为小写y的变量中。在使用训练集和测试集时,我们将使用X_train来训练分类器,使用X_test在分类器训练后对其进行评估。
现在我们可以看到应用这个train_test_split函数的结果。它确实将我们的水果数据集按照正确的样本比例划分成了训练集和测试集。
总结


本节课中,我们一起学习了机器学习问题的基本构建模块。我们介绍了如何将数据视为表格,其中行代表样本,列代表特征和标签。我们明确了分类器的目标是能够泛化到新数据,而不仅仅是记住训练样本。为了实现这一点,我们学习了使用train_test_split函数将数据集划分为训练集和测试集的关键步骤。训练集用于“教导”模型,而测试集则用于客观地评估模型在未见过的数据上的表现。现在我们已经准备好了训练和测试数据,在下一节中,我们将在将数据输入机器学习算法之前,更深入地查看数据本身。
60:数据探查方法 🔍

概述
在本节课中,我们将要学习数据探查的基本方法。数据探查是应用机器学习前的重要步骤,它帮助我们理解数据内容、发现潜在问题,并评估机器学习模型成功的可能性。
为什么要进行数据探查? 🤔
在考虑将机器学习应用于数据集时,首先实际查看数据集是一个非常好的第一步。
以下是进行数据探查的几个原因。
理解数据内容
首先,探查有助于了解数据集中实际包含的内容。
通过检查每个对象的特征,你可以更好地了解数据仍需进行何种清洗或预处理,以及每个属性或特征的典型值范围或分布。
当处理复杂对象(如文本)时,这种初步探索尤其有价值,因为这些对象可能由许多特征表示,而这些特征是通过一系列预处理步骤提取的。
例如,你可能会发现你获得的数据集有一个包含人名的列,而这个列仍需拆分为单独的名和姓列。如果你将姓名用作预测特征之一,这可能很重要。
发现数据问题
其次,你可能会注意到缺失或嘈杂的数据,或者一些特定的不一致之处。
例如,列使用了错误的数据类型,特定列或特定特征的测量单位不正确或不一致。
或者,你可能会注意到某个特定标记类的示例数量不足。
例如,假设你正在处理一个每行代表一个患者记录的应用程序。你可能会发现,某些人的体重测量值可能意外地以克为单位,而不是千克,这显然会对结果的准确性产生巨大影响。
因此,检查和可视化数据将帮助你检测和理解这些潜在的噪声或错误来源。
评估机器学习必要性
最后,对于你的数据集,你的问题实际上可能无需机器学习即可解决。这种情况并不常见,但如果发生,通过查看数据中存在的内容,你可以节省大量时间。
例如,你的数据集可能包含一个特征,该特征明显是你想要预测的标签的有力指标。
假设你的目标是根据房屋的销售价格、海拔高度、房间大小等属性来预测其位置是在纽约市还是旧金山。你的数据可能还包括房屋照片的URL。该图像可能在其元数据中包含房屋的GPS坐标,或者URL可能以一种不太明显但人类可识别的方式编码了位置信息。
这样,你只需查看数据就能解决问题。正如我所说,这种情况并不常见,但确实会发生。因此,简要检查你的数据集可以为你节省大量不必要的工作。
准备训练集 📊
我们将确保使用 train_test_split 从原始数据集中获得一个训练集。
我们将在这个训练集上进行所有的初始可视化和特征分析。
我们只会在分类器训练完成后,使用测试集来实际评估分类器。
训练集和测试集的完全分离非常重要。我们将在后面的课程中更深入地探讨其重要性的一些具体原因。
创建可视化图表 📈
现在我们已经选择了训练集,让我们创建一些简单的可视化图表,来查看训练集中对象的特征(在我们的案例中是不同水果的特征)如何相互关联以及如何与标签关联。
通过这些可视化,我们至少可以获得两大好处。
了解特征值范围与异常值
首先,我们可以了解每个特征的值范围,并且可以立即看到与其他点非常不同的任何异常值。这可能表明数据集中存在噪声、缺失特征或其他问题。
评估机器学习可行性
其次,通过观察不同类型对象在特征空间中的聚类和分离程度,我们可以更好地了解机器学习算法在预测不同类别时表现良好的可能性。
特征空间指的是使用我们数据中特定列的特征来表示对象。
例如,如果具有相同标签的对象(例如所有柠檬)具有相似的特征值,我们应该在可视化中看到一个定义明确的聚类。
而如果具有不同标签的对象的特征往往差异很大,我们应该看到它们被清晰地分离到图表的不同区域。
因此,在特征空间中,类别定义明确且分离良好的对象,是一个很好的迹象,表明分类器很可能能够根据特征以良好的准确性预测类别标签。
可视化技术 🎨
我将在这里展示的可视化技术在你拥有相对较少数量的特征(例如少于20个)时效果很好。稍后,当我们介绍无监督学习时,你将学习如何创建使用大量特征维度(数百、数千甚至数百万)来表示每个对象的数据集的可视化。
但现在,我们将使用的第一个可视化工具称为特征配对图。
特征配对图
这个图表显示了所有可能的特征对,并为每一对生成一个散点图,显示特征之间是否相关以及如何相关。
散点图中的每个点代表一个水果,根据其所属类别着色,并使用分配给该散点图的一对特征进行定位。
对角线是一个直方图,显示该特征的特征值分布。
在这个配对图中,按顺序显示的维度是我们训练集中水果示例的高度、宽度、质量和颜色分数。
因此,左上角的直方图显示了训练集中所有样本的高度特征分布。
紧邻其右侧的散点图在x轴上绘制每个样本的宽度,在y轴上绘制样本的高度。
仅通过查看这个配对图,我们已经可以看到一些特征对(例如右上角的高度和颜色分数)对于分离不同类别的水果效果很好。
这表明使用这些特征训练的分类器很可能能够相当好地学会分类各种水果类型。
以下是用于创建此图的代码。现在让我们在训练集上运行它。
# 示例代码:创建特征配对图
import seaborn as sns
import matplotlib.pyplot as plt
# 假设 `train_data` 是训练集 DataFrame,包含特征列和 'label' 列
sns.pairplot(train_data, hue='label')
plt.show()
请注意,像这样的配对图只显示特征对之间可能存在的交互,而不是所有特征之间的交互。
因此,图表本身可能不会显示特征之间存在的所有有趣关系,但它确实让你对可能存在的一些交互有一个大致的了解。
三维特征空间图
我们还可以通过创建三维图来查看使用三个特征子集的特征空间。
以下是我们可以用来实现这一点的代码。
# 示例代码:创建三维散点图
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 假设我们使用宽度、高度和颜色分数三个特征
x = train_data['width']
y = train_data['height']
z = train_data['color_score']
labels = train_data['label']
# 根据标签着色
scatter = ax.scatter(x, y, z, c=labels, cmap='viridis')
legend1 = ax.legend(*scatter.legend_elements(), title="Classes")
ax.add_artist(legend1)
ax.set_xlabel('Width')
ax.set_ylabel('Height')
ax.set_zlabel('Color Score')
plt.show()
在这个例子中,我们使用三个坐标绘制不同的水果。这里我们将显示宽度、高度和颜色分数。
同样,每个点代表一个水果,并根据其水果标签值着色。
在这个3D图中,你可以通过按住鼠标按钮并拖动来沿各个轴旋转图表。你可以清楚地看到,不同的水果类型形成了定义相当明确的聚类,并且在特征空间中也分离得很好。
总结
本节课中,我们一起学习了数据探查的重要性与方法。我们了解到,在应用机器学习之前,对数据进行初步检查和可视化是至关重要的步骤。这有助于我们理解数据内容、发现缺失值、噪声或不一致之处,甚至可能发现无需复杂模型即可解决问题的线索。

我们介绍了如何准备独立的训练集进行分析,并探索了两种可视化技术:特征配对图和三维特征空间图。这些工具帮助我们直观地评估特征之间的关系、数据的聚类情况,以及机器学习模型可能取得的成功程度。

记住,良好的数据探查是构建有效机器学习模型的坚实第一步。在接下来的课程中,我们将基于这些探查结果,开始构建和评估具体的预测模型。
61:K近邻分类算法 🍎📏

在本节课中,我们将学习一种流行且易于理解的机器学习算法——K近邻算法。我们将使用一个水果数据集作为示例,训练一个分类器,使其能够根据颜色、大小和质量等特征自动识别未来的水果。
概述
我们已经初步了解了数据集。现在,我们将使用这个数据集来训练一个分类器。该分类器将基于对象的颜色、大小和质量等可用特征,自动识别未来可能遇到的任何水果。为此,我们将使用一种流行且易于理解的机器学习算法,称为K近邻算法。
K近邻算法可用于分类和回归任务。目前,我们将重点介绍其在分类中的应用。K近邻分类器是基于实例或基于记忆的监督学习的一个例子。这意味着基于实例的学习方法通过记忆训练集中看到的带标签示例来工作,然后利用这些记忆的示例对新对象进行分类。
K近邻中的“K”指的是分类器为了进行预测而检索和使用的最近邻的数量。具体来说,K近邻算法包含三个可指定的步骤。
K近邻算法的工作原理
上一节我们介绍了K近邻算法的基本概念,本节中我们来看看其具体的工作步骤。
以下是K近邻分类器对新实例进行分类的三个步骤:
- 寻找最近邻:当给定一个新的、未见过的待分类实例时,K近邻分类器会在其记忆的训练示例集中,寻找特征最接近的K个示例。我们稍后将讨论“最接近”的含义。
- 查找标签:分类器会查找这K个最近邻示例的类别标签。
- 组合标签进行预测:完成上述步骤后,分类器会组合这些示例的标签,以预测新对象的标签。通常,这通过简单的多数投票来完成。
可视化示例
让我们看一个基于水果数据集的K近邻分类器可视化示例。
这里,我使用两个特征——宽度和高度——绘制了训练集中的每个水果。这两个特征共同构成了分类器的特征空间。图例中的颜色显示了每个数据点的标签。可以看到,有四种不同的颜色,对应数据集中的四种水果类型。
这些大块的彩色区域显示了根据“1-最近邻”规则,任何给定宽度和高度的点将被如何分类。换句话说,这里我设置 K=1。
例如,一个高度为6厘米、宽度为9厘米的数据点将被分类为苹果,因为它落在这个红色区域内。或者,如果分类器看到一个高度为4厘米、宽度为7厘米的水果,它将被分类为橘子,因为它落在分配给橘子的决策区域内。
理解“最近邻”
那么,“1-最近邻”规则意味着什么?这些不同的类别分配最初是如何得出的?
让我们看看这里的苹果例子。对于高度6厘米、宽度9厘米的对象,如果给分类器这个点,它会在所有见过的不同训练示例中搜索,查看哪个点最接近。我们寻找的是最近的单个邻居,即 K=1。我们试图找到最接近查询点(即我们想要分类的点)的那个点。
在这种情况下,我们可以看到特征空间中这个点可能是查询点的最近邻。类似地,如果我们看这里的橘子例子,训练集中最接近查询点的对象是这些点中的一个。
对于特征空间中的任何查询点,例如在柠檬区域这里,这个查询点的最近邻将是这里的这个训练集点。由于训练集点带有标签,我们知道它们的类别。
在1-最近邻的情况下,分类器会简单地将最近邻对象的类别分配给查询点。因为这里的最近邻是苹果,所以这里的查询点将被分类为苹果。如果我们看那边的橘子点,最近的数据点是橘子,因此它将被分类到那个区域。如果我们对每个可能的查询点都这样做,就会得到这种类区域模式。
从一个类别过渡到另一个类别的区域,这条线被称为决策边界,因为在线一侧的查询点被映射到一个类别,而在线另一侧的点被映射到另一个类别。
距离度量与特征权重
我想指出,在这个例子中,我们一直使用普通的欧几里得距离函数来寻找最近邻,该函数给予两个特征相等的权重。我们也可以使用加权欧几里得距离函数,在距离计算中给予不同特征不同的权重,这可能会在某些应用中提高准确性。
使用加权距离函数可以显著改变分类器的行为。下面是一个例子。
在左侧,是我们的标准设置,即对高度和宽度特征使用具有相等特征权重的欧几里得距离。在右侧,我改变了距离函数,使宽度特征(Y轴测量值)的权重是高度特征的10倍。
我并没有改变训练数据,所有训练点仍在相同位置。我们改变的是点之间的距离定义。现在,宽度特征的微小变化对分类决策的影响比之前大得多,因为在计算距离时有了10倍的乘数因子。
你可以看到,在右侧,这导致了决策边界和区域现在相当扭曲,并且在宽度方向上相比原始情况有所压缩。这是否是你想要的效果,将取决于你的具体应用。
加权距离的影响
如果你改用加权欧几里得距离度量,我之前提到的关于使用普通欧几里得距离的K近邻的一些事实也可能改变。
例如,使用加权距离度量,类别之间的决策边界可能发生显著变化。它们可能不再位于连接不同类别训练点的线的垂直等分线上。
让我解释一下。在原始具有相等特征权重的欧几里得距离中,当我们有不同类别点之间的决策边界时,我们可以画一条线连接这两个点,然后决策边界将是垂直于连接这两点的线的等距线。你可以在这里和这里的例子中看到这种情况。
但是,一旦我们切换到加权欧几里得距离,就像你在这里看到的,决策边界不再垂直于连接这两个不同类别点的线的等分线,它现在有些倾斜了。这就是某些事实如何随加权欧几里得距离而变化的一个例子。
数据标准化的重要性
与此相关的一点是,K近邻对具有不同量纲的特征值可能很敏感。如果你的数据是这种情况,你应该考虑在拟合分类器之前对训练数据进行标准化,以确保所有特征都在同一尺度上。我们将在第二周回归模块中介绍如何对数据进行标准化。
决策边界与K值
因为这是一个K近邻分类器,并且我们看的是 K=1 的情况,我们可以看到这里的类别边界,即决策边界。
让我们看这里的一个例子。这里有一个点是橘子,另一个点是柠檬。我们可以看到决策边界,因为它基于欧几里得距离并且我们平等对待各点,所以决策边界正好位于与这两个点等距的位置。它正好在这两个点的中间。因为如果它更靠近这个点,它将被映射到橘子类;如果更靠近那边的柠檬实例,它将被映射到柠檬类。你可以在这里的所有决策边界看到这种模式。这条线与这两个最近的点是等距的。
这基本上就是K近邻分类新实例的方式。你可以想象,对于K大于1的查询点,我们可能会使用一个稍微复杂一点的决策规则。
K>1 时的多数投票规则
让我们看这里的这个例子。假设我们有一个查询点是橘子,并且我们正在进行2-最近邻分类。在这种情况下,两个最近的点是这里的这两个点。
在K大于1的情况下,我们使用简单的多数投票。我们取邻居示例标签中最主要的类别。在这种情况下,标签恰好一致,它们都是橘子。因此,在2-最近邻情况下,这个点将被分类为橘子。
当你得到更靠近某个点的点时,例如这边的点,在这种情况下,两个最近邻可能是这里的这些点。在这种情况下,你可以在它们之间随机选择以打破平局。通常K值选择为奇数,这样在 K=3 时,情况可能看起来像这样。
你可以看到,这里的三个最近邻是一个红点(苹果)、一个蓝点(橘子)和另一个红点(苹果)。因此,在K=3的情况下,投票将倾向于将其标记为苹果。
这基本上就是K近邻分类器的基本机制。
使用K近邻算法的四个要素
更一般地说,要使用最近邻算法,我们需要指定四件事:
- 距离定义:我们需要定义特征空间中的距离含义,以便知道如何正确选择附近的邻居。在我刚才展示的水果数据集示例中,我们使用简单的直线距离或欧几里得距离来测量点之间的距离。
- 邻居数量K:我们必须告诉算法在预测时使用多少个最近邻。这个数字必须至少为1。
- 邻居权重:我们可能希望给予某些邻居对结果更大的影响力。例如,我们可能决定,距离我们试图分类的新实例更近的邻居,在最终标签上应该拥有更大的影响力或更多的投票权。
- 标签组合方式:一旦我们有了K个邻近点的标签,我们必须指定如何组合它们以产生最终预测。
典型参数选择示例
以下是针对这四件事可能做出的一些典型选择的具体示例。
最常用的距离度量,也是Scikit-learn默认使用的,是欧几里得距离或直线距离。从技术上讲,欧几里得度量实际上是更一般的闵可夫斯基度量的一个特例,其中有一个参数P被设置为2,就会得到欧几里得度量。当你在笔记本中查看实际设置时,你会看到这是以闵可夫斯基度量的形式完成的。
我们可能会选择使用5个最近邻作为我们的K值。我们可能指定对更近的邻居没有特殊处理,因此我们使用均匀权重。同样,默认情况下,Scikit-learn会应用第四个标准,即简单多数投票,并预测最近邻中代表最多的类别。
下周的监督学习方法课程中,我们将学习许多其他分类器。
在Python中应用K近邻分类器
现在,让我们看看笔记本,了解如何在Python中将K近邻分类器应用于我们的示例水果数据集。
提醒一下,你可以在输入方法时使用 Shift+Tab 来显示该方法的文档弹出窗口,以帮助提醒你不同的参数选项和语法。
回想一下,我们的任务是正确分类示例案例中的新传入对象(水果)。因此,我们新训练的分类器将接收数据实例作为输入,并为我们提供预测标签作为输出。
在Python中,我们需要做的第一步是加载必要的模块,并将数据加载到Pandas的DataFrame中。完成此操作后,我们可以使用 head 方法转储前几行,以检查列名并获取前几个实例的示例。
接下来,我在笔记本中定义了一个字典,该字典以数字水果标签作为输入键,并返回一个字符串值,即水果的名称。这个字典使得将分类器预测的输出转换为人更容易解释的内容(本例中为水果名称)变得更加容易。
对于此示例,我们将定义一个变量 X 来保存没有标签的数据集特征。这里我将使用水果的质量、宽度和高度作为特征。这个特征集合称为特征空间。我们定义第二个变量 y 来保存 X 中实例的对应标签。
现在,我们可以将 X 和 y 传递给Scikit-learn中的 train_test_split 函数。通常,分割成训练集和测试集是随机进行的,但为了本次讲座,我希望确保我们都得到相同的结果,因此我将 random_state 参数设置为一个特定值(本例中我选择了0)。
train_test_split 函数的结果被放入左侧的四个变量中,这些变量标记为 X_train、X_test、y_train 和 y_test。在整个课程中,我们将使用这种 X 和 y 变量命名约定来指代数据和标签。
一旦我们有了训练测试分割,我们就需要创建分类器对象的一个实例,本例中是一个K近邻分类器。然后设置重要参数,本例中是将邻居数量设置为分类器要使用的特定值。
我们通过将训练集数据 X_train 和标签 y_train 传递给分类器的 fit 方法来训练分类器。本例中使用的K近邻分类器是Scikit-learn中称为估计器的更通用类的一个例子。所有估计器都有一个 fit 方法,该方法接收训练数据,然后改变分类器或估计器对象的状态,以便在训练完成后能够进行预测。换句话说,它更新了这里K近邻变量的状态,这意味着对于K近邻,它将以某种内部存储形式记忆训练集示例以供将来使用。
训练K近邻分类器基本上就是这样。我们可以用这个新训练的分类器做的第一件事是,看看它在一些新的、未见过的实例上可能有多准确。为此,我们可以将分类器应用于我们预留的测试集中的所有实例。由于这些测试实例没有明确包含在分类器的训练中,评估分类器是否可能擅长预测未来未见过的数据实例标签的一个简单方法是,计算分类器在测试集数据项上的准确率。
请记住,K近邻分类器在训练阶段没有看到测试集中的任何水果。为此,我们使用分类器对象的 score 方法。这将把测试集点作为输入并计算准确率,准确率定义为真实标签被分类器正确预测的测试集项目的比例。
对新实例进行分类
我们还可以使用新分类器对单个水果实例进行分类。事实上,这本来就是我们的目标——能够获取单个对象实例并为它们分配标签。
例如,这里我输入一个相当小的假设水果的质量、宽度和高度。如果我们要求分类器使用 predict 方法预测标签,我们可以看到输出是它预测为橘子。
然后,我可以传递一个不同的例子,可能是一个更大、略长的水果,其高度大于宽度,质量也更大。在这种情况下,对此实例使用 predict 方法的结果是,分类器认为这个对象是柠檬。
可视化决策边界
现在,让我们使用本课程附带的共享实用程序模块中包含的一个名为 plot_fruit_knn 的实用函数。这将生成我之前展示过的带有决策边界的彩色图。然后,你可以自己尝试不同的K值,看看对决策边界有什么影响。
我在这里作为最后一个参数传入的 uniform 参数是要使用的加权方法。这里我传入字符串 uniform,这意味着在组合邻居标签时平等对待所有邻居。如果你愿意,可以尝试将其更改为 distance 来尝试距离加权方法。你也可以传递自己的函数,但我们将留到以后再说。
我们还可以看到新分类器在不同K值下的行为。在这一系列图中,我们可以看到随着K从1变化到5再到10所产生的不同决策边界。
我们可以看到,当K值较小(如1)时,分类器善于学习训练集中单个点的类别,但决策边界是碎片化的,变化很大。这是因为当 K=1 时,预测对噪声、异常值、错误标记的数据以及单个数据点中的其他变异源很敏感。
对于较大的K值,分配给不同类别的区域更平滑,不那么碎片化,并且对单个点中的噪声更鲁棒,但可能在个别点上犯更多错误。这是所谓的偏差-方差权衡的一个例子,我们将在下周的课程中更深入地研究这一现象及其含义。
K值对准确率的影响
鉴于我们改变K时观察到的分类器决策边界的变化,一个自然的问题可能是K值的选择如何影响分类器的准确率。
我们可以使用这段简短的代码轻松地将准确率绘制为K的函数。我们看到,对于这个特定的数据集和固定的单次训练测试分割,较大的K值确实会导致更差的准确率。
但请记住,这些结果仅针对这个特定的训练测试分割。为了获得对特定K值未来可能准确率的更可靠估计,我们需要查看多个可能训练测试分割的结果。我们将在下周深入探讨这个模型选择问题。
一般来说,导致最高准确率的最佳K值可能因数据集而异。通常,对于K近邻,使用较大的K可以抑制噪声个体标签的影响,但会导致分类边界不那么详细。
总结

在本节课中,我们一起学习了K近邻分类算法。我们首先了解了算法的基本概念和工作原理,包括其三个核心步骤。然后,我们通过可视化示例深入理解了“最近邻”和“决策边界”的含义。
我们探讨了距离度量(特别是欧几里得距离和加权欧几里得距离)对分类结果的影响,并强调了数据标准化的重要性。我们还分析了不同K值(从K=1到更大的值)如何影响决策边界的形状和分类器的鲁棒性,并引入了偏差-方差权衡的概念。
最后,我们在Python中实践了如何使用Scikit-learn库加载数据、分割数据集、训练K近邻分类器、评估其准确率并对新实例进行预测。恭喜你,你刚刚在Python中创建并运行了你的第一个机器学习应用!
在下周的课程中,我们将更深入地研究一些监督学习方法,并超越K近邻,学习如何以及为何将其他类型的分类器应用于机器学习问题。
62:监督式机器学习导论 🧠

在本节课中,我们将要学习监督式机器学习的基本概念、核心术语以及两种主要任务类型:分类与回归。我们将了解模型如何从数据中学习,并初步探讨模型复杂度与性能之间的关系。
概述
上一模块中,我们看到了一个使用K最近邻分类器的监督式机器学习基础示例。该示例根据水果的各种物理属性对它们进行分类。
这类机器学习算法被称为监督学习算法,因为它们使用训练集中的带标签样本来学习如何预测新的、未见过的样本的标签。
“监督”一词指的是每个训练样本都需要有一个标签,以便算法学习如何对未来样本做出准确预测。这与无监督机器学习形成对比,后者的训练数据样本没有标签。我们将在本课程后续部分介绍无监督学习。
本周,我们将更深入地探索监督学习,超越K最近邻分类器,介绍其他几种广泛使用的监督学习算法。
核心术语与概念
在继续之前,让我们回顾一些基本术语。
特征表示
我们将频繁使用的一个术语是特征表示。这意味着将一个对象(如一个水果)转换为计算机可以理解的数字。
在数据集中,一个水果的特征表示由这些列组成:质量、宽度、高度和颜色分数。这些将作为各种机器学习算法中的特征,用于预测水果的标签。
通常,数据集中的每一列对应一个与每个实例相关联的不同特征。
实例、样本与标签
我使用术语实例、样本或示例,它们大多可以互换使用。通常我会谈论实例或样本。因此,数据集的每一行对应一个水果,或一个特定的数据实例/样本。
在Python中处理这些实例时,我们将遵循一个约定:将所有代表特征的变量放在变量X中。因此,大写的X指的是所有将作为预测器输入的特征列。
然后,我们有一个非常重要的概念:标签。每个实例(每个水果)都有一个标签,在这个例子中是由人类分配的。标签是目标值的一个例子。在分类中,目标值是对象的标签;在回归中,它是你可能希望从输入中预测的特定连续值。
在Python中,我们使用小写y作为变量名,来保存与每个实例相关联的目标值。
训练集与测试集
我们从第一个例子中看到的另一个概念是训练集和测试集。
这意味着,如果我们把整个数据集看作一个矩形,我们通常会取原始数据集的一部分作为训练集,然后保留(通常较小)一部分作为测试集。Scikit-learn中的默认分割比例通常是75%训练,25%测试。
以下是使用Scikit-learn中train_test_split函数的示例代码:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25)
我们将使用X_train表示训练集特征,y_train表示训练集标签,X_test表示测试集特征,y_test表示测试集标签。
模型拟合与评估
一旦我们将训练数据传递给一个分类器,训练集就被用来估计模型的参数。在Scikit-learn中,有一个称为估计器的对象,它可以是分类器或回归器。这个模型的目标是学习如何对新数据实例进行分类或预测目标值。
我们将训练集传入以学习估计器内部参数的过程称为模型拟合。模型拟合的结果是一个训练好的模型。
以下是K最近邻分类器的拟合代码示例:
from sklearn.neighbors import KNeighborsClassifier
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train, y_train)
拟合完成后,KNN分类器的内部状态已更新,反映了训练数据对其内部参数的影响,现在可以用于预测。
评估方法通常是下一步。在拟合模型后,我们想看看它在测试数据上的实际表现如何。在本课程的第四周,我们将详细介绍评估分类器和回归器的不同方法。目前,我们主要看准确率。
对分类器应用score方法,传入测试数据X_test和y_test,将输出该分类器在测试集上的准确率。
accuracy = knn.score(X_test, y_test)
最后,我们还可以对KNN对象应用predict方法,来获取先前未见过的实例的标签。
predictions = knn.predict(X_new)
这些术语和变量命名约定将在整个课程中保持一致。
监督学习的任务类型
监督学习可以分为两种不同类型的任务:分类和回归。
分类和回归都接受一组训练实例,并学习到目标值的映射。
分类
对于分类,目标值是离散的类别值。
- 二分类问题:有两种类型的目标值,例如0代表负类,1代表正类。检测欺诈性信用卡交易就是一个二分类例子,交易要么有效要么无效。
- 多分类问题:目标值不止是“是/否”或“1/0”,而是一组离散值中的一个。例如,我们之前课程中给水果贴标签的例子就是多分类。我们试图预测标签的对象只能拥有一个可能的标签类型,一个东西不能既是苹果又是橙子。
- 多标签分类:存在多个目标值。例如,我们可能想给一个网页标注其讨论的所有不同主题。
回归
与分类相反,对于回归,我们试图预测的目标是一个连续值。例如,一个根据房间数量、位置、占地面积等预测房屋市场价格的模型就是一个回归问题,因为市场价格是连续的,也称为实值变量。
如何选择任务类型
你可以通过思考目标值的含义来判断应该对给定问题应用哪种类型的监督学习方法。
如果目标值代表一组互斥的类别值之一(如给水果贴标签或判断交易是否欺诈),那么这就是一个分类问题。
另一方面,如果目标是实值量,如成本、收入或高度,那么它可以被视为回归问题。
最后,我们会看到许多监督学习方法同时具有分类和回归两种形式。例如,我们即将介绍的支持向量分类器,也有一个回归版本,称为支持向量回归。
在本课程中,我们将涵盖分类和回归方法,但我们将把大部分时间花在分类上,因为它代表了一组重要的机器学习任务,并且分类涉及一些额外的概念和方法(特别是在评估等领域),这些在回归中不那么需要。
本周将学习的算法
我们将从两个简单但强大的预测算法开始:
- K最近邻:我们在第一周见过。
- 使用最小二乘法的线性模型拟合。
这两种预测方法代表了监督学习的两种互补方法。K最近邻对数据结构不做太多假设,能给出可能准确但有时不稳定的预测(“不稳定”指的是它对训练数据中的微小变化可能很敏感)。另一方面,线性模型对数据结构做出强假设(即目标值可以仅使用输入变量的加权和——一个线性函数来预测),它能给出稳定但可能不准确的预测。
除了K最近邻和线性模型,我们还将涵盖各种广泛使用的用于分类和回归的监督学习方法,包括决策树、支持向量机和神经网络。
对于每种方法,我们将在较高层次上探索其工作原理,而不深入过多的数学细节。我们还将看看通常需要什么样的特征预处理。对于我们将要研究的大多数监督学习算法,都有一个或多个控制其模型复杂度的参数。因此,我们将看到如何设置这些参数以帮助避免欠拟合和过拟合。最后,我们将讨论每种监督学习方法的优缺点,以帮助理解在哪些场景下最适合(或不适合)应用特定方法。
模型复杂度与准确性
在我们研究所有这些不同方法时,你将看到一个一致的模式:模型复杂度与模型准确性之间的关系,如下图所示。

这种关系根据是在训练集(此处标记为“训练集分数”)还是测试集(此处标记为“测试集分数”)上测量分类器性能而有所不同。
当针对用于训练分类器的同一训练集测量性能时,该图显示,随着模型复杂度增加(例如KNN分类器中K值减小,决策边界越来越可变和详细),模型在训练集上的准确性呈上升趋势,因为更复杂的模型能越来越好地拟合训练数据。
然而,当同一个训练好的模型在留出的测试集上进行评估时,通常随着模型复杂度增加到某个最佳点,测试集准确性会有一个初始的增益,但随后测试集准确性会下降,因为日益复杂的模型开始对训练数据过拟合,过于具体,而没有捕捉到能帮助其很好地泛化到未见过的测试数据的更全局的模式。
这些趋势的具体情况会因情况而异,但总体而言,我们将在所研究的监督学习方法中看到这种模式。
什么是模型?

在本课程中,你会经常听到“模型”这个词。那么,什么是模型?它是一个特定的数学或计算描述,表达了一组输入变量与一个或多个正在研究或预测的结果变量之间的关系。
在统计术语中,输入变量称为自变量,结果变量称为因变量。在机器学习中,我们使用术语特征来指代输入或自变量,使用目标值或目标标签来指代输出或因变量。
模型既可以用来理解和探索给定数据集内的结构(正如我们将在无监督学习中看到的),也可以用于开发预测模型(在监督学习方法的情况下),其目标是准确预测先前未见过的输入数据的结果(即目标值或标签)。
总结
本节课中,我们一起学习了监督式机器学习的基础。我们明确了特征、标签、训练集、测试集等核心术语,并理解了模型拟合与评估的过程。我们区分了监督学习的两种主要任务:分类(预测离散标签)和回归(预测连续值)。最后,我们初步探讨了模型复杂度与泛化性能之间的关键关系,即模型需要在拟合训练数据与泛化到新数据之间找到平衡,避免欠拟合和过拟合。在接下来的课程中,我们将深入具体的算法来应用这些概念。
63:过拟合与欠拟合问题 🎯

在本节课中,我们将学习监督学习模型构建中的两个核心问题:过拟合与欠拟合。理解这两个概念对于构建能够良好泛化到新数据的模型至关重要。
概述
当我们构建监督学习模型时,无论是用于分类还是回归,模型的目标不仅是准确预测训练数据中的样本。我们真正期望的是,模型能够对从未见过的未来测试集样本,正确预测其类别或目标值。这种在预留测试集上表现良好的能力,称为模型的泛化能力。
机器学习通常假设未来的测试集与训练集具有相同的属性,或者说来自相同的底层分布。这意味着,如果模型在训练集上表现出高准确率,并且训练集与测试集相似,我们通常可以预期它在测试集上也会有良好表现。然而,由于过拟合问题的存在,情况并非总是如此。
什么是过拟合?
非正式地说,过拟合通常发生在我们试图用不足的训练数据拟合一个复杂模型时。过拟合的模型利用其捕获复杂模式的能力,擅长预测训练集中大量特定的数据样本或局部变化,但它常常忽略了训练集中有助于在未见测试集上良好泛化的全局模式。
由于没有足够的数据来约束模型以遵循这些全局趋势,训练集准确率在模型过拟合时,会成为对测试集准确率毫无希望的乐观指标。理解、检测和避免过拟合,或许是应用监督机器学习算法时你需要掌握的最重要方面。
过拟合与欠拟合的视觉示例
为了更好地理解过拟合现象,让我们看几个视觉示例。
回归问题示例
第一个示例涉及回归问题。在下图的X轴上,我们有一个输入变量(例如,房产面积)。在Y轴上,我们有一个目标变量(例如,该房产上房屋的市场售价)。
在回归中,我们试图找到输入变量与目标变量之间的关系,并从一个可能解释这种关系的模型开始。
- 尝试线性模型:我们可能尝试拟合一个线性模型,即预测输入变量与目标变量之间存在线性关系。在这种情况下,模型可能对这些点拟合一条直线。此时,模型欠拟合了数据。模型对于数据中存在的实际趋势来说过于简单,甚至在训练点(图中蓝点)上表现也不好。因此,它不太可能很好地泛化到测试数据。
- 尝试二次模型:如果我们选择一个更好的模型,例如认为这可能是一个二次关系,我们可能会得到一个看起来拟合得更好的结果。这是一个合理良好模型拟合的示例。我们既捕捉了点的总体趋势,同时也忽略了可能由噪声引起的小变化。
- 尝试复杂多项式模型:第三个示例是,我们可能假设输入变量与目标变量之间的关系是一个具有多个参数的多项式函数。如果我们尝试对这个训练数据集拟合一个更复杂的模型,我们可能会得到一条非常曲折的线。这个更复杂的模型有更多参数,因此能够捕捉更细微的行为,但方差也更高。它更侧重于捕捉训练数据中更局部的变化,而不是试图找到我们人类能从数据中看到的更全局的趋势。这就是过拟合的示例。
分类问题示例
第二个示例将展示分类中的过拟合。下图展示了一个简单的二维分类问题,每个数据实例由一个点表示,并有两个关联特征。这是一个二分类问题,点被标记为红色或蓝色。
分类问题的目标是找到一个决策边界。
- 欠拟合模型:欠拟合模型的一个例子可能是一个过于简单的模型,例如只考虑了一个特征的直线。同样,这是一个过于简单化的模型,它甚至不能很好地捕捉训练数据中类别之间的划分模式,因此不太可能泛化。
- 良好拟合模型:一个拟合良好的合理模型可能是一个找到正类(红色)和负类(蓝色)之间总体差异的线性模型。它意识到了这种全局模式(大多数蓝色负类点在左上角,大多数红色正类点更靠近右下角),并且足够稳健,忽略了红色区域偶尔出现蓝点或蓝色区域偶尔出现红点的事实。这是一个合理拟合的模型。
- 过拟合模型:另一方面,过拟合模型通常是一个具有大量参数、能够捕捉复杂行为的模型。因此,它会试图找到一种非常“聪明”的方式,以高度可变的决策边界完全分离红点和蓝点。这样做的好处(值得怀疑的好处)是它能非常好地捕捉训练数据的类别,几乎完美地预测训练数据的类别。但是,如果类别之间的实际划分是由那个线性模型捕捉的,那么过拟合模型将在它试图过于“完美”的区域犯很多错误。过拟合模型具有高方差,它试图捕捉过多的局部波动,并且没有足够的数据来看到能带来更好整体泛化性能的全局趋势。
K近邻分类器中的K值影响
第三个示例展示了修改K近邻分类器中K参数的效果。这里显示的三张图分别展示了K=10、K=5和K=1时的决策边界。我们再次使用水果数据集,X轴是水果的高度,Y轴是宽度。
- K=10:K=10意味着对于每个我们想要预测的查询点,我们需要查看该查询点的10个最近邻点,并综合它们的投票来预测该点的标签。我们需要训练集中10个不同数据实例的投票来进行预测。
- K=5:当K=5时,我们只需要5个最近邻来进行预测。
- K=1:K=1是最不稳定的情况,对于任何查询点,我们只查看该点的单个最近邻。
降低K近邻分类器中的K值,会增加决策边界的方差。因为决策边界更容易受到异常值的影响。在K=1的情况下,一个遥远的点可能对决策边界产生更大的影响,而在K=10的情况下,还需要其他9个邻居的投票。
通过调整K近邻分类器的K值,我们可以在一定程度上控制模型拟合数据集的程度。实际效果最佳的K值只能通过在测试集上进行评估来确定。但总体思想是,对于K近邻分类器,随着我们减小K值,过拟合的风险会增加。原因如前所述,例如当K=1时,我们试图捕捉决策边界中非常局部的变化,这可能不会对未来数据带来良好的泛化行为。
总结
本节课中,我们一起学习了监督学习中的两个关键问题:欠拟合与过拟合。
- 欠拟合发生在模型过于简单,无法捕捉数据中的基本模式时,导致在训练集和测试集上表现都差。
- 过拟合发生在模型过于复杂,过度捕捉训练数据中的噪声和局部波动,而忽略了全局趋势时,导致在训练集上表现很好,但在未见过的测试集上表现不佳。

我们通过回归和分类的视觉示例,直观地理解了这些概念。最后,我们以K近邻算法为例,看到如何通过调整模型复杂度(如K值)来在欠拟合和过拟合之间寻找平衡。构建机器学习模型的核心目标,是获得良好的泛化能力,而理解并避免过拟合与欠拟合是实现这一目标的基础。
64:监督学习数据集 🧪

在本节课中,我们将学习监督学习中常用的几种数据集类型。我们将从简单的人工合成数据集开始,逐步过渡到更复杂的真实世界数据集,以帮助理解不同算法的工作原理。
概述
为了探索不同的监督学习算法,我们将结合使用小型合成数据集作为示例,以及一些较大的真实世界数据集。
Scikit-learn 在其 sklearn.datasets 库中提供了多种创建合成数据的方法。这些合成数据集通常用于演示目的。它们通常是低维度的,只使用少量特征(通常是一到两个),这使得它们易于解释和可视化。
另一方面,许多真实世界的数据集具有更高维度的特征空间。换句话说,它们有数十、数百甚至数千、数百万个特征。因此,我们从低维示例中获得的一些直觉并不总能直接应用到高维数据集上,我们稍后会对此进行更多讨论。例如,在某种意义上,高维数据集的大部分数据都位于角落,中间存在大量空白区域,这很难可视化。我们将在课程后面通过一些例子来说明。
尽管如此,低维示例仍然非常有用,可以帮助我们理解诸如模型复杂度如何随某些关键参数变化而变化等问题。
回归数据集示例 📈
上一节我们介绍了数据集的基本概念,本节中我们来看看用于回归问题的数据集。
对于基础回归问题,我们将从一个简单的问题开始,它有一个信息性输入变量、一个带噪声的线性输出和100个数据样本。下图是一个使用散点图绘制的数据集,每个点用一个圆点表示。X轴显示特征值,Y轴显示回归目标。
要创建这个数据集,我们使用 sklearn.datasets 中的 make_regression 函数。以下是笔记本中的代码:
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=100, n_features=1, noise=20, random_state=42)
二分类数据集示例 (简单线性可分) ⚖️
为了说明二分类问题,我们将引入一个简单的两类数据集,它包含两个信息性特征。
下图是一个散点图,每个数据实例显示为一个点,第一个特征值对应X轴,第二个特征值对应Y轴。点的颜色表示该数据实例被标记为哪个类别。
我称这个数据集为“简单”,是因为它只有两个特征,并且这两个特征都是信息性的。在这种情况下,这两个类别大致是线性可分的,这意味着在它们之间放置一个基本的线性分类器就能很好地区分两个类别的点。
以下是创建此数据集的代码:
from sklearn.datasets import make_classification
X, y = make_classification(n_samples=100, n_features=2, n_informative=2,
n_redundant=0, n_clusters_per_class=1,
flip_y=0.1, random_state=42)
我们使用了 sklearn.datasets 中的 make_classification 函数,创建了100个点,大致将数据样本按类别分组为每个类一个簇,并且有10%的概率随机翻转任何点的正确标签,以使分类器的任务更具挑战性。
二分类数据集示例 (复杂非线性) 🌀
接下来,我们来看一个更复杂的二分类问题。它使用两个特征,但两个类别并非真正线性可分,而是在特征空间的不同部分形成了多个簇。
这个数据集是分两步创建的。首先,使用 sklearn.datasets 中的 make_blobs 函数,在八个不同的簇中随机生成100个样本。然后,通过使用模2函数转换 make_blobs 分配的簇标签(一个从1到8的数字),将其更改为二进制标签,将偶数索引点分配给类别0,奇数索引点分配给类别1。
以下是创建此数据集的代码:
from sklearn.datasets import make_blobs
X, y_blobs = make_blobs(n_samples=100, centers=8, random_state=42)
y = y_blobs % 2 # 转换为二分类标签
多分类数据集示例 🍎🍌🍊🍇
为了说明多分类问题,我们将使用我们熟悉的水果数据集。你可能还记得,它有四个特征和四个可能的目标标签。
在左侧,我展示了我们在第1周看到的散点图矩阵,它显示了所有可能的特征对与类别标签之间的关系,以及每个特征值的分布在对角线上。
真实世界回归数据集示例 🏙️

为了说明一个真实世界的回归问题,我们将使用一个源自UCI存储库中的“社区与犯罪”数据集的数据集。
我们的数据使用了原始特征和目标值的一个子集,这些数据最初是通过结合几个美国政府数据源(如美国人口普查)创建的。每个数据实例对应一个特定的地理区域,通常是一个城镇或城市的一个区域。
我们使用的这个数据集版本有88个特征,编码了每个位置的各种人口统计和社会经济属性,包含1,994个位置数据实例。我们将尝试预测的目标值是人均暴力犯罪率。
要使用这些数据,我们使用为本课程包含在共享实用程序模块中的 load_crime_data 函数。
# 假设存在一个自定义的加载函数
from shared_utilities import load_crime_data
X, y = load_crime_data()
总结
本节课中,我们一起学习了监督学习中几种关键的数据集类型。我们从易于可视化的低维合成数据集开始,了解了用于回归的线性数据集、用于简单二分类的线性可分数据集,以及用于复杂二分类的非线性可分数据集。接着,我们回顾了用于多分类问题的水果数据集。最后,我们介绍了一个高维的真实世界回归数据集——社区犯罪数据,它包含了大量特征和实例。理解这些不同类型的数据集是选择和应用合适监督学习算法的重要基础。在接下来的课程中,我们将学习如何在这些数据集上应用各种算法。
65:K近邻分类与回归 🧠📈

在本节课中,我们将学习K近邻模型。这是一种简单直观的监督学习方法,可用于分类和回归任务。我们将通过示例理解其工作原理,并探讨模型复杂度与泛化能力之间的关系。
回顾K近邻分类器
上一节我们介绍了监督学习的基本概念,本节中我们来看看K近邻分类器。K近邻分类器的工作原理是记忆整个训练集。当需要对新实例进行分类时,它执行以下三个步骤:
- 在训练集中找到与新实例最相似的K个最近邻实例。
- 获取这些训练实例的标签。
- 根据这些邻近训练实例的标签来预测新实例的标签,通常采用简单的多数投票方式。
以下是使用K=1(即最近邻)的K近邻分类器在一个简单的二分类合成数据集上进行预测的示意图。

如图所示,类别0的点用黄色圆点标记,类别1的点用黑色圆点标记。整个特征空间根据分类器在每个点的预测结果被划分为不同的决策区域。例如,黄色区域中的点将被分类器预测为类别0,而黑色区域中的点将被预测为类别1。
对于K=1的分类器,要对任何给定的查询点进行预测,分类器只需在训练集中找到距离最近的那个点,并将该点的类别作为预测结果。因此,决策边界会显得非常不规则且具有高方差,这表明模型复杂度较高。在这种情况下,最近邻分类器可能过拟合了训练数据,试图对每个训练点都做出正确预测,而忽略了两类数据分布的整体趋势。
调整K值以控制模型复杂度
上一节我们看到了K=1时可能导致的过拟合问题,本节中我们来看看增加K值的影响。当我们将K从1增加到11时,分类器必须综合考虑11个最近邻点的投票结果,而不仅仅是1个。这使得单个训练数据点对预测的影响不再那么显著。
结果是决策边界变得更加平滑,这代表了一个模型复杂度较低的模型,决策边界的方差也大大降低。实际上,如果我们将K增加到等于训练集中点的总数,结果将是一个单一的决策区域,所有预测都将是训练数据中最频繁出现的类别。
以下是不同K值下训练分数和测试分数的对比:
- K=1:训练分数为完美的1.0,但测试分数仅为0.80。
- K=3:训练分数降至0.88,但测试分数略微上升至0.88,表明模型对新数据的泛化能力更好。
- K=11:训练分数进一步降至0.81,但测试分数更好,达到0.92。
这表明,当K值适中时,模型能够更好地忽略训练数据中的微小变化,捕捉到类别分布更重要的全局趋势,从而获得最佳的泛化性能。
K近邻回归
K近邻方法不仅可用于分类,也可用于回归。以下是针对一个简单回归问题(一个输入特征和对应的目标值)的示意图。

左图显示了原始的训练数据点(绿色圆圈)。中间和右图分别显示了K=1和K=3时K近邻回归算法的预测结果(蓝色三角形)。
K近邻回归的计算方式与分类类似:
- 对于K=1,查询点的预测值就是其最近邻训练点的目标值。
- 对于K=3,查询点的预测值是其三个最近邻训练点目标值的平均值。
为了评估回归模型的拟合效果,我们使用一个介于0到1之间的回归分数——R平方(也称为决定系数)。R平方值为1表示完美预测,值为0表示模型始终预测训练目标值的平均值。
回归中的模型复杂度
与分类情况类似,让我们看看在这个简单回归数据集上,模型复杂度与泛化能力(通过R平方训练值和测试值衡量)之间的关系。
以下是不同K值下K近邻回归的表现:
- K=1:回归模型完美拟合训练数据(R平方训练分数为1.0),但在预测新数据时表现很差(R平方测试分数仅为0.155)。
- K增大:随着K值增加(如3,7,15),模型平滑了局部变化,更能捕捉全局趋势。训练集分数下降,但模型泛化到新数据的能力增强,测试分数上升。
- K=15:在该系列中,K=15的模型具有最佳的测试集性能(R平方分数为0.485)。
- K过大(如55):当K值过大时,训练和测试集分数都会回落到较低水平,模型开始欠拟合,即模型过于简单,即使在训练数据上表现也不好。
K近邻的优缺点与参数
K近邻方法的优点是简单直观,容易理解特定预测是如何做出的。它可以作为一个合理的基线,用于比较更复杂方法的性能。
然而,当训练数据有很多实例或每个实例有很多特征时,这会显著降低K近邻模型的性能。因此,如果你的数据有数百或数千个特征,尤其是当数据是稀疏的(即每个实例有很多特征,但大多数为零)时,应考虑K近邻模型的替代方案。
总结来说,K近邻模型(包括回归和分类)的两个关键参数是:
n_neighbors:控制要考虑的邻居数量,从而控制模型复杂度。metric:控制点之间的距离函数,从而决定在寻找邻居时哪些点被认为是“最近”的。本课程未深入探讨此参数,在大多数情况下,默认的欧几里得距离设置对大多数数据集效果良好。

本节课中我们一起学习了K近邻模型在分类和回归中的应用。我们理解了其核心机制是通过查找最近邻来进行预测,并深入探讨了K值对模型复杂度及泛化能力的核心影响:较小的K值导致高复杂度可能过拟合,较大的K值降低复杂度但可能欠拟合,需要选择一个适中的K值以获得最佳泛化性能。
66:线性回归与最小二乘法 📈

在本节课中,我们将学习监督学习中最简单的模型之一——线性模型,特别是线性回归。我们将了解其核心概念,并重点介绍一种名为“最小二乘法”的参数估计方法。
线性模型简介
上一节我们介绍了监督学习的基本概念,本节中我们来看看一种最基础的模型:线性模型。
线性模型通过输入变量的加权和来表达目标输出值。例如,我们的目标可能是预测房屋的市场价值,即其下个月的预期售价。
假设我们有两个输入变量:
- 地方政府每年评估的房产税金额。
- 房屋的年龄(年数)。
可以想象,房屋的这两个特征各自都包含有助于预测市场价格的信息。因为在大多数地方,房屋的纳税评估与其市场价值之间存在正相关关系。同时,房屋的年龄与市场价值之间可能存在负相关关系,例如,较老的房屋可能需要更多的维修和升级。
一个线性模型的示例如下:
预测价格 = 212000 + 109 * 去年纳税额 - 2000 * 房屋年龄
例如,根据这个线性模型,一栋去年纳税额为10,000美元、房龄为75年的房屋,其估计市场价格约为120万美元。
需要明确的是,这个具体的线性模型只是我作为示例编造的。但一般来说,当我们谈论训练一个线性模型时,我们指的是估计模型的参数(有时也称为系数)的值。在这个例子中,参数就是常数项212000,以及权重109和-2000。我们的目标是,通过这些参数计算出的对不同房屋的结果变量Y(价格)的预测,能够很好地拟合过去实际销售的数据。我们稍后将讨论“良好拟合”的含义。
线性回归模型
预测房价是使用线性模型进行回归任务的一个例子,这种模型毫不意外地被称为线性回归。
更一般地说,在线性回归模型中,可能有多个输入变量或特征,我们将其表示为 x0, x1 等。每个特征 Xi 都有一个对应的权重 Wi。
预测输出(我们记为 ŷ)是特征的加权和,加上一个常数项 b̂。我在这里所有在训练过程中估计的量上都加了一个“帽子”符号。ŵ 和 b̂ 值(我们称之为训练参数或系数)是从训练数据中估计出来的。而 ŷ 是根据输入特征值和训练参数的线性函数估计出来的。
例如,在我们刚才看到的简单房价例子中:
- ŵ0 是 109,x0 代表纳税额。
- ŵ1 是 -2000,x1 代表房龄。
- b̂ 是 212000。
我们称这些 Wi 值为模型系数或特征权重。b̂ 被称为模型的偏置项或截距。
单变量线性回归
让我们来看一个非常简单的线性回归模型形式,它只有一个用于预测的输入变量或特征。
在这种情况下,向量 x 只有一个分量,我们称之为 x0,即输入变量或特征。此时,因为只有一个变量,预测输出 ŷ 简单地等于权重 w0 与输入变量 x0 的乘积,再加上偏置项 b。
所以,x0 是数据提供的值,而我们必须估计的参数是 w0 和 b。这个公式你可能很熟悉,它是一条直线的方程,其中斜率对应于权重 w0,而 b 对应于 y 轴截距(我们称之为偏置项)。
这里模型的任务是接收输入(例如 x 轴上的一个点),通过 w0(斜率)和 b(截距)这两个参数,在特征空间中定义一条直线。重要的是要记住,模型有训练阶段和预测阶段。训练阶段使用训练数据来估计 w0 和 b。
最小二乘法原理
那么,我们究竟如何估计线性模型的 w 和 b 参数,才能使模型良好拟合呢?w 和 b 参数是使用训练数据估计的,根据你对“良好拟合”的定义标准以及控制模型复杂度的方式,有许多不同的估计方法。
对于线性模型,一种广泛使用的方法称为最小二乘线性回归,也称为普通最小二乘法。
最小二乘线性回归会找到一条穿过数据点的直线,使得模型的均方误差最小。模型的均方误差本质上是训练集中所有点的预测目标值与实际目标值之差的平方和。
下图说明了这一点。蓝点代表训练集中的点。红线代表通过这组训练点找到的最小二乘模型。而这些黑线显示了基于某个训练点的 x 位置预测的 y 值与该训练点的实际 y 值之间的差异。
例如,对于 x 值为 -1.75 的这个点,将其代入线性模型的公式,我们在线上的这个点得到一个预测值(大约在60左右)。但训练集中该点的实际观测值可能更接近10。那么,对于这个特定点,预测目标与实际目标之间的平方差就是 (60 - 10)²。
我们可以对训练集中的每一个点进行这种计算,计算训练点观测到的 y 值与线性模型根据该训练点的 x 值预测出的 ŷ 值之间的平方差。每个点的平方差都可以计算出来,然后如果我们把它们全部加起来,再除以训练点的数量取平均值,那就是模型的均方误差。
因此,最小二乘法的技术旨在找到斜率 w 值和截距 b 值,以最小化这个平方误差,即均方误差。
关于这个线性回归模型需要注意的一点是,没有控制模型复杂度的参数。无论 w 和 b 取什么值,结果始终是一条直线。这既是该模型的优点,也是其弱点,我们稍后会看到。
在 Scikit-Learn 中实现
现在,让我们看看如何在 Scikit-Learn 中实现这一点。
Scikit-Learn 中的线性回归由 sklearn.linear_model 模块中的 LinearRegression 类实现。就像我们对 Scikit-Learn 中的其他估计器(如 K 近邻分类器和回归模型)所做的那样,我们首先在原始数据集上使用 train_test_split 函数,然后使用训练数据 X_train 和相应的目标值 y_train 来创建和拟合线性回归对象。
这里请注意,我们通过将 fit 方法与新对象的构造函数链接起来,在一行代码中完成了线性回归对象的创建和拟合。
LinearRegression 的 fit 方法用于估计特征权重 w(它称之为模型的系数),并将其存储在 coef_ 属性中;以及偏置项 b,它存储在 intercept_ 属性中。请注意,如果 Scikit-Learn 对象的属性以下划线结尾,这意味着这些属性是从训练数据中推导出来的,而不是由用户设置的量。
如果我们查看这个简单示例的 coef_ 和 intercept_ 属性,会发现因为只有一个输入特征变量,coef_ 列表中只有一个元素,值为 45.7。intercept_ 属性的值约为 148.4。我们可以看到,这些值确实对应于图中显示的红线,该直线的斜率为 45.7,y 轴截距约为 148.4。
与 K 近邻回归的比较

既然我们已经了解了 K 近邻回归和最小二乘回归,现在比较一下两者的结果是很有趣的。我们可以看到这两种回归方法代表了两种互补的监督学习类型。
K 近邻回归器对数据的结构不做太多假设,能给出可能准确但有时不稳定的预测,这些预测对训练数据中的微小变化很敏感。因此,与最小二乘线性回归相比,它在训练集上的 R 平方分数相应更高(KNN 为 0.72,最小二乘法为 0.679)。
另一方面,线性模型对数据的结构做出了强有力的假设,即目标值可以通过输入变量的加权和来预测。线性模型给出稳定但可能不准确的预测。然而,在这种情况下,线性模型关于输入和输出变量之间存在线性关系的强假设恰好很适合这个数据集。因此,它更擅长准确预测训练期间未见过的新 X 值所对应的 Y 值。我们可以看到,线性模型在测试集上获得了稍好的分数(0.492 vs KNN 的 0.471),这表明它能够更好地泛化并捕捉这种全局线性趋势。
总结
本节课中,我们一起学习了线性回归模型的核心思想。我们了解到线性模型通过加权和的形式进行预测,并重点掌握了使用最小二乘法来估计模型参数(斜率和截距),该方法通过最小化预测值与真实值之间的均方误差来寻找最佳拟合直线。最后,我们比较了线性回归与 K 近邻回归的特点,认识到线性模型结构简单、假设明确,在符合其假设的数据上往往具有更好的泛化能力。
67:线性回归进阶:岭回归、Lasso与多项式回归 🧮

在本节课中,我们将学习线性回归的几种高级形式。我们将探讨如何通过正则化技术(岭回归和Lasso回归)来防止模型过拟合,以及如何通过多项式特征变换来让线性模型捕捉数据中的非线性关系。这些方法能显著提升模型在复杂数据集上的表现。
岭回归:引入正则化
上一节我们介绍了普通最小二乘法。本节中,我们来看看另一种估计线性模型参数 W 和 B 的方法,称为岭回归。
岭回归使用相同的最小二乘准则,但有一个关键区别:在训练阶段,它会为过大的特征权重 W_i 添加一个惩罚项,如下方公式所示。
公式:目标函数 = 最小二乘误差 + α * Σ(W_i²)
从数学上看,较大的权重意味着其平方和也较大。一旦岭回归估计出线性模型的 W 和 B 参数,对新实例的 y 值预测就与最小二乘法完全相同:只需将输入特征值 X_i 代入常规的线性公式,计算加权特征值之和加上 B 即可。
那么,岭回归这样的方法为何有用呢?
这种在学习算法的目标函数中添加惩罚项的做法,被称为正则化。正则化是机器学习中一个极其重要的概念,它通过限制模型可能的参数设置来防止过拟合,从而提高模型的泛化性能。通常,正则化限制的效果是降低最终估计模型的复杂度。
对于线性回归,添加参数值的平方和(如上框所示)到最小二乘目标中,意味着具有较大特征权重 W 的模型会增加目标函数的整体值。因为我们的目标是最小化整体目标函数,所以正则化项会对那些拥有许多大特征权重的模型施加惩罚。
换句话说,在所有条件相同的情况下,如果岭回归找到两个能同样好地预测训练数据值的线性模型,它会优先选择特征权重平方和较小的那个线性模型。
使用岭回归的实际效果是,找到一组在最小二乘意义上能很好拟合数据的特征权重 W_i,同时将许多特征的权重设置为非常小的值。在单变量线性回归的例子中我们看不到这种效果,但对于具有数十或数百个特征的回归问题,使用像岭回归这样的正则化线性回归所带来的精度提升可能是显著的。
正则化的强度由 alpha 参数控制。较大的 alpha 意味着更强的正则化,以及权重更接近 0 的、更简单的线性模型。alpha 的默认设置为 1.0。注意,将 alpha 设置为 0 对应于我们之前看到的普通最小二乘线性回归的特殊情况。
在 scikit-learn 中,你可以通过从 sklearn.linear_model 导入 Ridge 类来使用岭回归,然后像使用最小二乘法一样使用该估计器对象。唯一的区别是,你可以使用 alpha 参数指定岭回归正则化惩罚的强度,这被称为 L2 惩罚。
以下是应用岭回归到犯罪数据集的代码示例:
from sklearn.linear_model import Ridge
ridge_reg = Ridge(alpha=1.0)
ridge_reg.fit(X_train, y_train)
现在,你会注意到这里的结果并不那么令人印象深刻。测试集上的 R 平方分数与我们用最小二乘回归得到的结果非常接近。然而,在应用岭回归时,我们可以做一些事情来显著改善结果。
特征预处理与归一化的重要性
现在,我们简要讨论一下特征预处理和归一化的必要性。
让我们停下来直观地想一想岭回归在做什么。它通过对 W 系数的大小施加平方和惩罚来正则化线性回归。因此,增加 alpha 的效果是使 W 系数向 0 收缩,并彼此靠近。
但是,如果输入变量(特征)具有非常不同的尺度,那么当这种系数收缩发生时,不同尺度的输入变量对 L2 惩罚的贡献将不同。因为 L2 惩罚是所有系数的平方和。
因此,转换输入特征,使它们都处于相同的尺度,意味着岭惩罚在某种意义上更公平地应用于所有特征,不会因为尺度的差异而不恰当地偏重某些特征。
更一般地说,随着课程的深入,你会发现特征归一化对于许多不同的学习算法(不仅仅是正则化回归)都很重要,这包括 K 近邻、支持向量机、神经网络等。所需的特征预处理和归一化类型也可能取决于数据。
目前,我们将应用一种广泛使用的特征归一化形式,称为最小-最大缩放。这将转换所有输入变量,使它们都处于 0 和 1 之间的相同尺度。
为此,我们计算训练数据上每个特征的最小值和最大值,然后对每个特征应用如下所示的最小-最大变换。
公式:X_scaled = (X - X_min) / (X_max - X_min)
以下是它如何与两个特征一起工作的示例。假设我们有一个特征“高度”,其值落在 1.5 到 2.5 单位之间相当窄的范围内。但第二个特征“宽度”的范围则宽得多,在 5 到 10 单位之间。应用最小-最大缩放后,两个特征的值都被转换到相同的尺度上,最小值映射到 0,最大值映射到 1,其他所有值都映射到这两个极值之间的某个值。
在 scikit-learn 中应用最小-最大缩放,你需要从 sklearn.preprocessing 导入 MinMaxScaler 对象。要准备使用缩放器对象,你需要创建它,然后使用训练数据 X_train 调用 fit 方法。这将计算训练数据集中每个特征的最小和最大特征值。
然后,要应用缩放,你调用它的 transform 方法并传入你想要重新缩放的数据,输出将是输入数据的缩放版本。在本例中,我们想要缩放训练数据并将其保存到一个名为 X_train_scaled 的新变量中,同时缩放测试数据并将其保存到名为 X_test_scaled 的新变量中。然后,我们只需使用这些缩放后的特征数据版本,而不是原始特征数据。
请注意,通过使用缩放器的 fit_transform 方法,可以更高效地在训练集上一步完成拟合和转换,如下所示:
from sklearn.preprocessing import MinMaxScaler
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
关于如何在包含训练集和测试集的学习场景中应用最小-最大缩放或任何类型的特征归一化,这里还有最后一点非常重要。
你可能已经注意到两件事:首先,我们对训练数据和测试数据应用了相同的缩放器对象;其次,我们是在训练数据上训练缩放器对象,而不是在测试数据上。这两点都是特征归一化的关键方面。
如果你不对训练集和测试集应用相同的缩放,你最终会得到或多或少是随机的数据尺度,这将使你的结果无效。如果你通过向缩放器(或其他归一化方法)展示测试数据而不是训练数据来准备它,这会导致一种称为数据泄露的现象,即训练阶段包含了从测试集泄露的信息(例如,测试数据中每个特征的极端值分布),而学习器在训练期间本不应访问这些信息。这反过来又可能导致学习方法在同一个测试集上给出不切实际的好估计。我们将在课程后面更多地讨论数据泄露现象。
执行特征归一化的一个缺点是,基于转换后特征的最终模型可能更难解释。同样,最终应用哪种特征归一化最好,可能取决于数据集、学习任务和要使用的学习算法。我们将在整个课程中继续探讨这个问题。
应用归一化后的岭回归
好的,在我们添加了输入特征的最小-最大缩放代码后,让我们回到岭回归。
我们可以看到最小-最大缩放对岭回归性能的显著影响。在输入特征被适当缩放后,岭回归实现了明显更好的模型拟合,测试集上的 R 平方值约为 0.6,比没有缩放时好得多,现在也比普通最小二乘法好得多。
事实上,如果你对普通最小二乘回归应用相同的最小-最大缩放,你会发现它根本不会改变结果。一般来说,当你的训练数据量相对于模型中的特征数量较小时,正则化效果特别好。随着你拥有的训练数据量增加,正则化变得不那么重要。
在这个例子中,我们可以通过为 alpha 设置不同的值,来观察改变正则化强度对缩放后的训练和测试数据的影响。测试集上最佳的 R 平方值是在 alpha 设置为 20 左右时达到的。显著更大或更小的 alpha 值都会导致模型拟合显著变差。这再次说明了本讲座前面看到的模型复杂度和测试集性能之间的一般关系,即通常存在一个中间的最佳模型复杂度参数值,不会导致欠拟合或过拟合。
Lasso 回归:另一种正则化方法
另一种你可以用来替代岭回归的正则化回归称为 Lasso 回归。
与岭回归类似,Lasso 回归在普通最小二乘目标中添加了一个正则化惩罚项,使模型 W 系数向 0 收缩。Lasso 回归使用一个稍有不同的正则化项,称为 L1 惩罚,而不是岭回归的 L2 惩罚,如下所示。
公式:目标函数 = 最小二乘误差 + α * Σ|W_i|
L1 惩罚看起来与 L2 惩罚有些相似,因为它也计算系数的和,但它是 W 系数绝对值的和,而不是平方和,其结果明显不同。使用 Lasso 回归时,一部分系数被强制恰好为 0,这是一种自动特征选择。因为权重为 0,该特征在模型中基本上被完全忽略。这种稀疏解(只有最重要的特征子集具有非零权重)也使得在输入变量较多的情况下,模型更容易解释。
与岭回归一样,Lasso 回归的正则化强度由参数 alpha 控制,默认值为 1.0。同样类似于岭回归,使用 Lasso 回归的目的是估计 W 和 B 模型系数。一旦完成,预测模型的公式就与普通最小二乘法相同,你只需使用线性模型。
一般来说,如果你认为只有少数几个变量对输出变量有中等或较大的影响,那么 Lasso 回归最有帮助。否则,如果有很多变量贡献了小的或中等的影响,岭回归通常是更好的选择。
让我们在 notebook 中使用犯罪数据集看看 scikit-learn 中的 Lasso 回归。要使用 Lasso 回归,你需要从 sklearn.linear_model 导入 Lasso 类,然后像使用岭回归这样的估计器一样使用它。
对于某些数据集,你可能偶尔会收到收敛警告,在这种情况下,你可以将 max_iter 属性设置为更大的值,通常至少 20000 或可能更多。增加 max_iter 参数将相应增加计算时间。
在这个例子中,我们像对岭回归所做的那样,将 Lasso 应用于犯罪数据集的最小-最大缩放版本。你可以看到,当 alpha 设置为 2.0 时,只剩下 20 个具有非零权重的特征,因为通过 Lasso 正则化,大多数特征的权重被精确地设置为零。
我已经按照权重降序排列列出了具有非零权重的特征。尽管我们需要谨慎解释像犯罪这样的复杂问题的数据结果,但 Lasso 回归结果确实帮助我们看到了这个特定数据集中输入变量与结果之间的一些最强关系。
例如,查看 Lasso 回归发现的具有非零权重的 top 5 特征,我们可以看到位置因素(如密集住房中的人口百分比,这表示城市地区)和社会经济变量(如一个地区空置房屋的比例)与犯罪率呈正相关。而其他变量,如双亲家庭的百分比,则呈负相关。
最后,我们可以看到调整 Lasso 回归的正则化参数 alpha 的效果。就像我们在岭回归中看到的那样,存在一个最佳的 alpha 范围,能提供最佳的测试集性能,既不过拟合也不欠拟合。当然,这个最佳的 alpha 值对于不同的数据集会有所不同,并取决于其他各种因素,例如所使用的特征预处理方法。
多项式回归:捕捉非线性关系
假设我们有一组二维数据点,特征为 x0 和 x1。那么,我们可以通过添加额外的特征来转换每个数据点,这些特征是 x0 和 x1 的三个唯一的乘法组合:x0²、x0*x1 和 x1²。
这样,我们将原始的二维点转换为一组五维点,这些点仅依赖于二维点中的信息。现在,我们可以写一个新的回归问题,尝试预测相同的输出变量 ŷ,但使用这五个特征,而不是两个。这里的关键见解是,这仍然是一个线性回归问题。特征只是加权和中的数字,因此我们可以使用相同的最小二乘技术来估计这五个特征的五个模型系数,就像我们在更简单的二维情况下所做的那样。
那么,我们为什么要做这种转换呢?这被称为多项式特征变换,我们可以用它来将问题转换到更高维的回归空间。实际上,添加这些额外的多项式特征允许我们使用一组更丰富的复杂函数来拟合数据。所以你可以直观地认为,这允许用多项式来拟合训练数据,而不仅仅是一条直线,但仍然使用相同的最小化均方误差的最小二乘准则。
我们稍后会看到,这种添加新特征(如多项式特征)的方法在分类中也非常有效,我们将在核化支持向量机中再次看到这种变换。
当我们添加这些新的多项式特征时,我们本质上是通过将它们作为特征添加到线性模型中,增强了模型捕捉不同变量之间相互作用的能力。例如,理论上,房价可能同时是房屋所在地块大小和房产税金额的二次函数。一个简单的线性模型无法捕捉这种非线性关系,但通过向线性回归模型添加多项式等非线性特征,我们就可以捕捉到这种非线性。
更一般地说,除了多项式,我们还可以使用其他类型的非线性特征变换。这超出了本课程的范围,但从技术上讲,这些被称为回归的线性基函数,并且被广泛使用。
当然,添加大量新特征(尤其是当我们取 K 个变量的所有可能组合时)的一个副作用是,这些更复杂的模型有过拟合的潜力。因此,在实践中,多项式回归通常与正则化学习方法(如岭回归)结合使用。
以下是使用 scikit-learn 进行多项式回归的示例。在 sklearn.preprocessing 模块中已经有一个方便的类叫 PolynomialFeatures,它会为我们生成这些多项式特征。
这个例子展示了一个更复杂的回归数据集上的三次回归,该数据集恰好具有变量之间的一些二次交互作用。
- 第一次回归只使用没有多项式特征变换的最小二乘回归。
- 第二次回归创建了
degree设置为 2 的多项式特征对象,然后在原始特征X_f1上调用多项式特征对象的fit_transform方法,以生成新的多项式变换特征X_f1_poly。然后代码调用普通最小二乘线性回归。你可以看到过拟合的迹象,在这个扩展的特征表示上,模型的训练集 R 平方分数接近 1,但在测试集上低得多。 - 因此,第三次回归展示了通过岭回归在这个扩展特征集上添加正则化的效果。现在,训练和测试的 R 平方分数基本相同,正则化多项式回归的测试集分数在三种回归方法中表现最佳。
系数路径图:可视化正则化效果
我想花一点时间向你展示一个非常有用且有趣的图表,它显示了正则化参数 alpha 的选择如何影响回归模型的估计系数。我称之为系数路径图。
回想一下,如果我们想将正则化回归模型(如岭回归或 Lasso 回归)拟合到一组点,我们还需要指定要使用的特定 alpha 值,该值控制优化目标中正则化惩罚的权重。在这部分幻灯片中,我写下了优化目标,你可以看到,我们有正则化参数 alpha 来控制目标中正则化部分(这个 W_j 平方和)相对于普通最小二乘部分的权重。
我们指定 alpha,拟合模型,就像所有回归场景一样,拟合模型的结果将是一组估计系数。在这个例子中,我们假设我们有 6 个模型权重,比如说对应输入中的六个不同特征。我们将分析一个线性模型,该模型估计这六个系数 W1 到 W6,在本例中我们用岭回归进行拟合。在图中我将忽略系数 B,因为它不贡献正则化惩罚。
好的,这是本例中岭回归的系数路径图。在 x 轴上,我们有 alpha 的值。注意,我将其绘制在对数刻度上,因为我们在相对较小的值和相对较大的值之间变化 alpha 的范围很广,所以请注意这个对数刻度。
在图的 y 轴上,我们有估计系数的值。因为我们有六个系数,我在这张图上绘制了六条曲线。所以每个估计系数 W1、W2 等对应一条曲线。
这张图表显示了当我们使用不同的 alpha 选择拟合模型时,估计系数会发生什么变化。实际上,这个系数路径图显示了 alpha 在 10⁻¹ 到 100 之间的所有可能选择,我们平滑地变化,假装我们可以使用任何我们喜欢的 alpha 选择来计算无限多个可能的模型,这就创建了一条作为 alpha 函数的曲线。
例如,该图显示,对于岭回归,如果我们选择 alpha 等于 10⁻¹,那么我们会得到一个模型,其对 W1 的估计值将略小于 50。如果我们选择 alpha 等于 10,该图显示,结果模型对 W1 的估计系数将大约为 28,并且它还显示了其他估计系数的值。
所以你可以看到,随着我们增加 alpha 的选择值,估计系数 W1 的值变得越来越小,它在收缩。这基本上就是岭回归所做的:它惩罚较大的系数值,使用这个平方惩罚项,从而迫使模型将估计系数向 0 收缩。你给正则化项的权重越大,alpha 值越高,你就越强调估计系数必须小。如果我们对所有六个系数都这样做,这个图就显示了随着我们增加 alpha,岭回归所具有的收缩效应。
这是同一个线性模型对应的系数路径图,但这次我们使用 Lasso 回归进行拟合,它使用 L1 正则化惩罚项。所以你可以在这里看到 L1 惩罚项。就像在岭回归中一样,参数 alpha 控制目标在正则化部分与普通最小二乘部分之间的权重分配。
注意,当 alpha 非常小时,比如 10⁻¹,这里左边得到的估计系数 W1、W2 等的值实际上与我们在岭回归中看到的相同,因为在这两种情况下正则化惩罚 alpha 都非常小,所以我们会得到接近普通最小二乘解的结果。因此,当 alpha 非常小时,Lasso 和岭回归将产生具有非常相似估计系数的模型。

但是随着我们增加 alpha,我们可以看到对于 Lasso 回归,估计系数的收缩效应与岭回归相比非常不同。我们可以看到 Lasso 回归如何为更高的 alpha 值找到稀疏的系数集。例如,这次如果我们选择 alpha 为 10,
68:逻辑回归算法 🧮

在本节课中,我们将学习第二种监督学习方法——逻辑回归。尽管名称中包含“回归”,但它实际上是一种用于分类的算法。我们将从线性回归的基础开始,逐步理解逻辑回归的原理、它与线性回归的区别,以及如何在Python中应用它来解决分类问题。
从线性回归到逻辑回归
上一节我们介绍了线性回归。本节中我们来看看逻辑回归与它的联系和关键区别。
线性回归基于输入变量(特征)的加权和加上一个常数项来预测一个实数值输出 Y。
公式:
Y = B + W₁X₁ + W₂X₂ + ... + WₙXₙ
其中,Xᵢ 是输入特征,Wᵢ 是模型系数(权重),B 是截距项。线性回归的目标是找到最佳的 Ŵᵢ 和 B̂,使模型对训练数据的平方误差最小。
逻辑回归与线性回归相似,但有一个关键的增加步骤。
以下是逻辑回归模型的图示说明:

逻辑回归模型同样计算输入特征 Xᵢ 和截距项 B 的加权和。但它会将这个线性结果输入一个特殊的非线性函数——逻辑函数(或称Sigmoid函数)——以产生输出 Ŷ。
公式:
Ŷ = f(B̂ + Σ ŴᵢXᵢ) = 1 / (1 + e^-(B̂ + Σ ŴᵢXᵢ))
这里的 f 就是逻辑函数。它是一个S形曲线,当输入值远大于0时,输出趋近于1;当输入值远小于0时,输出趋近于0。其效果是将线性函数的输出压缩到0和1之间。
因为基本逻辑回归的任务是预测一个二元输出值,所以它可以用于二元分类。我们可以将目标值为0的数据实例视为负类,将目标值为1的数据实例视为正类。那么,逻辑回归公式的输出 Ŷ 就可以解释为:给定输入特征,该数据实例属于正类的概率。
逻辑回归示例:单特征分类
让我们通过一个具体例子来理解逻辑回归的应用。假设我们想根据学生为考试学习的小时数这一单一特征,来预测其是否能通过考试。
- 未通过考试的学生被归为负类(目标值=0)。
- 通过考试的学生被归为正类(目标值=1)。
下图展示了一个示例训练集:

X轴对应学习小时数,Y轴对应通过考试的概率。红色点(目标值0)代表未通过考试的学生及其学习时长;蓝色点(目标值1)代表通过考试的学生及其学习时长。
使用逻辑回归,我们可以估计出 Ŵ 和 B̂ 系数,从而产生一条最能拟合这些训练点的逻辑曲线(如图中S形曲线)。一旦估计出模型系数,我们就有了一个公式,可以根据任何学生给定的学习小时数,来估计其通过考试的概率。
分类规则:如果学生通过考试的估计概率 Ŷ 大于或等于50%,则预测其属于正类(通过),否则预测为负类(未通过)。在此例中,学习超过3小时的学生将被预测为通过。
逻辑回归示例:双特征与决策边界
现在,我们来看一个使用两个输入特征的例子。下图展示了一个包含两个类的训练集,每个数据点有两个特征:
- 特征1 对应X轴
- 特征2 对应Y轴
红色点(负类)在特征1值低、特征2值高的区域形成簇;蓝色点(正类)具有中等的特征1值和低的特征2值。
我们可以应用逻辑回归,使用与之前考试示例相同的思路来学习一个二元分类器。为此,我们添加第三个维度(垂直的Z轴),对应属于正类的概率。
我们估计出 Ŵ 和 B̂ 参数,得到一个能最佳拟合此训练数据的逻辑函数。不同之处在于,现在的逻辑函数是两个输入特征的函数,因此它在这个空间中形成了一个类似三维的S形曲面。
一旦从训练数据中估计出这个逻辑函数,我们就可以用它来预测任何给定其特征1和特征2值的点的类别成员身份,方法与考试示例相同。
任何逻辑概率估计值 Ŷ 大于等于0.5的数据实例都被预测为属于正类(蓝色),否则属于负类(红色)。
如果我们想象一个代表 y=0.5 的平面与这个逻辑函数相交,你会发现所有 y=0.5 的点都落在一条直线上。换句话说,使用逻辑回归会在类别之间产生一个线性决策边界。
如果从上方向下俯视左侧的三维逻辑函数,你会得到类似右侧的视图。在逻辑函数上 y≥0.5 的点位于直线右侧区域(图中右侧虚线右侧),而 y<0.5 的点则位于该虚线左侧。
在Scikit-learn中应用逻辑回归
要在Scikit-learn中执行逻辑回归,你需要从 sklearn.linear_model 模块导入 LogisticRegression 类,然后创建对象并调用 fit 方法,就像处理K近邻等其他分类器一样。
代码示例:
from sklearn.linear_model import LogisticRegression
logreg = LogisticRegression(C=100).fit(X_train, y_train)
此代码还将参数 C 设置为100,我们稍后会解释。
我们在此使用的数据是水果数据集的修改版,仅使用高度和宽度作为特征空间中的特征,并将目标类别值修改为一个二元分类问题:预测一个物体是苹果(正类)还是非苹果(负类)。
下图展示了结果:
- X轴对应高度特征,Y轴对应宽度特征。
- 黑点代表正类(苹果)训练点。
- 黄点代表训练集中所有其他水果的实例。
- 灰色决策区域表示:根据逻辑回归函数,一个水果被估计为苹果的概率大于0.5,从而被分类为苹果的高度和宽度特征空间区域。
- 黄色决策区域对应被估计为苹果的概率小于0.5的物体的特征空间区域。
你可以看到应用逻辑回归产生的线性决策边界,即灰色区域与黄色区域相接的地方。实际上,逻辑回归的结果通常与线性支持向量机(我们将在分类中探讨的另一种线性模型)得到的结果非常相似。
逻辑回归中的正则化
与岭回归和Lasso回归类似,正则化惩罚也可以应用于逻辑回归的模型系数,并通过参数 C 进行控制。
事实上,用于岭回归的相同 L2正则化惩罚 默认在逻辑回归中是开启的,默认值 C=1。请注意,对于支持向量机和逻辑回归,C 值越高对应正则化程度越轻。

C值较大:逻辑回归会尽可能好地拟合训练数据。C值较小:模型会更努力地寻找更接近0的模型系数,即使该模型对训练数据的拟合稍差一些。
在此可视化图中,你可以看到改变逻辑回归的正则化参数 C 的效果。我们使用相同的苹果分类器,让 C 值从左到右分别为0.1、1.0和100.0。
虽然正则化的真正威力在我们拥有更高维特征空间的数据时才会变得明显,但你可以从中了解到在依赖更简单模型(在此例中更强调两个特征中的一个)但训练集准确率较低(如左侧 C=0.1 的示例)与更好地拟合训练数据(如右侧 C=100 的示例)之间的权衡。
你可以在随附的笔记本中找到创建此示例的代码。
总结
本节课中我们一起学习了逻辑回归算法。我们了解到:
- 核心思想:逻辑回归通过在线性回归的加权和输出上应用逻辑函数(Sigmoid函数),将连续值映射到(0,1)区间,从而输出属于某个类别的概率。
- 与线性回归的区别:线性回归用于预测连续值,而逻辑回归用于分类(特别是二元分类,也可扩展至多类)。
- 决策边界:逻辑回归产生一个线性决策边界,通过设定一个阈值(通常为0.5)将概率转换为类别预测。
- 在Scikit-learn中的实现:使用
LogisticRegression类,其用法与其他分类器一致。 - 正则化:通过参数
C控制L2正则化的强度,C值越小,正则化越强,模型更简单,可能有助于防止过拟合。
最后,我们展示了默认开启L2正则化的逻辑回归如何应用于具有更多特征的乳腺癌真实数据集,并实现了96%的训练集和测试集准确率。逻辑回归因其简单、高效且可解释性强,成为分类任务中一个强大而实用的基础工具。
69:Python用于数据科学实践 📊

概述:线性分类器与支持向量机
在本节课中,我们将要学习线性模型如何应用于分类任务,特别是支持向量机(SVM)的基本原理。我们将从最简单的二元分类问题开始,逐步理解线性分类器的决策规则、分类间隔的概念,以及如何通过调整参数来优化模型性能。
线性分类器基础
上一节我们介绍了线性模型在回归问题中的应用。本节中我们来看看线性模型如何用于分类任务,特别是二元分类。
线性分类器的预测方法使用了与回归相同的线性函数形式。但不同于预测连续的目标值,我们取线性函数的输出并应用符号函数(sine function),以产生一个二元输出。这个输出对应两个可能的类别标签。
如果目标值大于0,函数返回+1。如果目标值小于0,函数返回-1。
以下是线性分类器的工作原理:
- 特征与权重:每个数据实例由一组特征(如x1, x2)描述。分类器会为每个特征学习一个权重(如W1, W2)。
- 线性组合:分类器计算特征的加权和,即
W1*x1 + W2*x2,并加上一个偏置项B。 - 决策函数:将上述线性组合的结果输入符号函数。若结果大于0,则预测为类别+1;若小于0,则预测为类别-1。
其核心公式可以表示为:
F(x) = sign(W · x + B)
其中,W 是权重向量,x 是特征向量,B 是偏置项,· 表示点积运算。
决策边界与分类示例
理解了基本公式后,我们通过一个具体例子来看看分类器如何进行决策。
假设我们有一个简单的二元分类问题,每个数据点由两个特征x1和x2描述。我们定义一条决策边界,其方程为 x1 - x2 = 0。这条线实际上就是所有满足 x1 = x2 的点构成的直线。
我们可以将这个方程改写为线性分类器的形式:
决策值 = (1)*x1 + (-1)*x2 + 0
这里,权重向量 W = [1, -1],偏置 B = 0。
现在,让我们对两个点进行分类:
-
点A (-0.75, -2.25):
- 计算决策值:
(1)*(-0.75) + (-1)*(-2.25) = 1.5 - 由于
1.5 > 0,符号函数输出+1。因此,该点被分类为类别1。
- 计算决策值:
-
点B (-1.75, -0.25):
- 计算决策值:
(1)*(-1.75) + (-1)*(-0.25) = -1.5 - 由于
-1.5 < 0,符号函数输出-1。因此,该点被分类为类别-1。
- 计算决策值:
通过这个简单的线性公式,我们可以为特征空间中的任何点生成一个类别预测值。
分类间隔与最大间隔分类器
我们已经看到线性分类器如何划分数据。那么,如何评价一个分类器的好坏呢?一个核心思想是奖励那些能将两个类别最大程度分开的分类器。为此,我们需要引入分类间隔的概念。
简单来说,对于一个给定的分类器,其间隔是指决策边界可以向两侧移动(垂直于边界方向)而不碰到任何数据点的最大宽度。这个宽度由离决策边界最近的数据点决定。
在所有能够正确区分两个类别的可能分类器中,最大间隔分类器被认为是更优的,因为它对未知数据的泛化能力通常更强。这种寻找最大间隔分类器的算法,就称为线性支持向量机。
在Python的scikit-learn库中,我们可以方便地使用线性支持向量分类器。
# 示例代码:使用scikit-learn的LinearSVC
from sklearn.svm import LinearSVC
# 初始化分类器
clf = LinearSVC()
# 使用训练数据拟合模型
clf.fit(X_train, y_train)
处理不可线性分离的数据与正则化参数C
在之前的理想例子中,两个类别可以被一条直线完美分开。但在实践中,数据通常存在噪声或更复杂的结构,使得完美的线性分离变得不可能。不过,线性分类器仍然能在容忍少量错误的情况下,有效分离大多数数据点。
支持向量机在“最大化分类间隔”和“最小化分类错误”之间的权衡,由一个名为 C 的正则化参数控制。
以下是参数 C 的影响:
C值较大:表示正则化程度较弱。模型会尽可能减少训练集上的分类错误,即使这会导致分类间隔变小。模型更倾向于“拟合”训练数据。C值较小:表示正则化程度较强。模型会倾向于寻找一个具有更大分类间隔的决策边界,即使这意味着会错误分类更多的训练数据点。模型更注重“泛化”能力。
通过调整 C 值,我们可以在模型的复杂度和对训练数据的拟合程度之间取得平衡。
应用于真实数据集:乳腺癌分类
最后,让我们将线性支持向量机应用到一个真实世界的数据集上,例如乳腺癌分类问题。实践表明,即使没有进行复杂的参数调优,线性支持向量机也能在该数据集上取得相当不错的准确率。

这体现了线性模型在实践中的实用价值。
线性模型的优势与特点
在本节课中,我们一起学习了线性分类器,特别是支持向量机的工作原理。作为总结,我们来回顾一下线性模型的主要优势:
- 简单高效:线性模型(包括线性回归、逻辑回归和线性SVM)原理简单,易于训练。
- 预测速度快:由于其预测函数是线性的,进行预测时的计算速度非常快。
- 适用于高维数据:线性模型,包括线性支持向量机,在高维数据集上表现有效,尤其当数据实例是稀疏的时候。
- 可扩展性强:线性模型能够很好地扩展到非常大的数据集。
- 内存效率高(针对SVM):线性支持向量机在决策函数中只使用一部分训练点(称为支持向量),因此算法可以实现很高的内存效率。
总结
本节课中,我们深入探讨了如何将线性模型用于分类任务。我们从二元分类的基本概念出发,理解了线性分类器的决策规则,引入了分类间隔和最大间隔分类器(即支持向量机)的核心思想。我们还学习了如何通过正则化参数 C 来平衡模型的复杂度和拟合度,并了解了线性模型在处理真实数据时的优势和特点。这些知识为我们在数据科学实践中选择合适的分类算法奠定了坚实的基础。
70:多类别分类问题 🍎🍊🍌🍇


概述
在本节课中,我们将要学习如何使用Scikit-learn处理多类别分类问题。我们将了解其背后的工作原理,并通过一个水果分类的具体例子来演示整个过程。
从二分类到多分类
在之前我们使用的分类示例中,我们主要关注的是二分类问题。其中要预测的目标值是一个二元值,它要么是正类,要么是负类。
然而,在许多真实世界的数据集中,要预测的目标值实际上是一个类别。例如,在我们的水果数据集中,有四种不同类别的水果需要预测,而不仅仅是两种。
那么,我们如何使用Scikit-learn来处理这种多类别分类的情况呢?
Scikit-learn的多分类策略
幸运的是,Scikit-learn使得学习多类别分类模型变得非常容易。本质上,它是通过将一个多类别分类问题转化为一系列二分类问题来实现的。
这是什么意思呢?本质上,当你传入一个目标值为分类变量的数据集时,Scikit-learn会自动识别这一点。然后,对于每一个需要预测的类别,Scikit-learn会创建一个二分类器,用于预测该类与所有其他类别。
例如,在水果数据集中,有四种水果类别,因此Scikit-learn会学习四个不同的二分类器。为了预测一个新的数据实例,它会将该实例依次输入到每个二分类器中,得分最高的分类器所对应的类别,就会被用作预测值。
水果数据集示例
让我们通过这个水果数据集来看一个多类别分类的具体例子。
我们只需传入一个普通的数据集,其目标值是从1到4的水果类别。我们拟合模型的方式与处理二分类问题时完全相同。
一般来说,如果我们只是进行拟合和预测,这一切对用户是完全透明的。Scikit-learn会自动做正确的事情:它会学习多个类别并预测多个类别,我们几乎不需要做其他额外工作。
然而,我们也可以深入了解其内部机制。如果我们查看拟合训练数据后得到的线性模型的系数和截距,就能看到这一点。
以下示例展示了这个过程。我们正在做的是将一个线性支持向量机拟合到水果训练数据上。如果我们查看系数值,会发现得到的不是单一线性模型分类器的一组系数,而是四组值。这些值对应于训练集中的四类水果。
因此,Scikit-learn在这里创建了四个二分类器,每个类别对应一个。你可以看到这里有四对系数,以及四个截距值。
解读分类器
在这种情况下,第一对系数对应于一个将苹果与其他水果区分开来的分类器。因此,这对系数和这个截距定义了一条直线(在这个可视化图中,苹果是红点)。苹果模型的系数定义了一个线性决策边界,即图中这条红线。
如果你将其绘制出来,会发现它的截距确实是-3。实际上,你可以使用这个线性公式为任何数据实例计算它将预测为“是苹果”还是“不是苹果”。
让我们看一个具体例子。假设有一个高度为2、宽度为6的查询点(例如图中的某个位置)。这是一个线性分类器,因此我们可以使用第一对系数和第一个截距值,它们构成了“苹果 vs 非苹果”的线性分类器。
以下是预测步骤:
- 我们取高度特征,乘以第一个系数。
- 取宽度特征,乘以第二个系数。
- 然后加上第三个偏置项(即截距)。
为了预测这个高度为2、宽度为6的对象是否是苹果,我们只需计算这个线性公式。计算结果是一个正值(0.59)。因为该值大于等于0,表明模型预测该对象确实是苹果。这是合理的,因为它位于“苹果 vs 非苹果”二分类器决策边界的苹果一侧。
另一个预测示例
类似地,我们可以取另一个对象,比如高度为2、宽度为2的对象(位于图中空间的这个部分)。我们将这两个特征值代入同一个苹果分类器。
当我们这样做时,线性模型在该情况下的预测值是-2.3,这个值小于0。这意味着它位于决策边界的这一侧。因此,此处的“苹果 vs 非苹果”线性模型预测该对象不是苹果。
多分类预测流程
所以,当Scikit-learn需要预测一个新对象的类别(可能是多个类别之一)时,它会依次遍历每一个这样的二分类器。它会预测那个为该实例给出最高得分的分类器所对应的类别。
总结
本节课中,我们一起学习了Scikit-learn如何处理多类别分类问题。核心在于,它将多分类任务分解为多个“一对多”的二分类问题,为每个类别训练一个分类器。在预测时,系统会运行所有二分类器,并选择置信度最高的结果作为最终类别。整个过程对用户高度透明,使得处理像水果分类这样的多类别任务变得简单直接。
71:核化支持向量机 🧠

在本节课中,我们将要学习一种强大的监督学习模型——核化支持向量机。我们将了解它如何通过将数据映射到高维空间来解决线性模型难以处理的复杂分类问题。
概述
上一节我们介绍了线性支持向量机,它通过寻找具有最大间隔的决策边界,在某些数据集上表现良好。线性支持向量机适用于类别线性可分或接近线性可分的简单分类问题,例如左侧的示例。

但在真实数据中,许多分类问题并非如此简单。不同类别在特征空间中的分布方式,使得一条直线或一个超平面无法成为有效的分类器。右侧的示例就是这种情况。对于线性模型(直线或超平面)来说,这个数据集很难或不可能进行良好分类。
因此,为了应对这种情况,我们现在将转向下一种监督学习模型——核化支持向量机。它是线性支持向量机的一个非常强大的扩展。
核化支持向量机的基本思想
核化支持向量机(简称SVM)可以提供超越线性决策边界的更复杂模型。与其他监督机器学习方法一样,SVM可用于分类和回归。但由于时间限制,本讲座将只关注分类。我们不会深入探讨SVM运作的数学细节,但会给出一个高层次的概述,希望能抓住该方法最重要的要素。
本质上,理解核化SVM的一种方式是:它接收原始的输入数据空间,并将其转换到一个新的、更高维的特征空间。在这个新空间中,使用线性分类器对转换后的数据进行分类变得容易得多。
以下是一个简单的一维示例来说明这个想法。这是一个二元分类问题,在一维空间中,一组点沿着X轴分布,黑色代表一个类别,白色代表第二个类别。这里的每个数据点只有一个特征,即它在X轴上的位置。如果我们把这个任务交给线性支持向量机,它会毫无困难地找到在不同类别点之间提供最大间隔的决策边界。这里我特意设计了数据点,使得最大间隔决策边界恰好位于x=0处。
现在,假设我们给线性支持向量机一个更困难的问题,其中类别不再是线性可分的。一个简单的线性决策边界没有足够的表达能力来正确分类所有这些点。那么我们能做什么呢?一个非常强大的想法是将输入数据从一维空间转换到二维空间。
例如,我们可以通过将每个一维输入数据实例 X_i 映射到一个对应的二维有序对 (X_i, X_i^2) 来实现这一点,其第二个新特征是第一个特征的平方值。从某种意义上说,我们并没有增加任何新信息,因为获得这个新的二维版本所需的一切都已经存在于原始的一维数据点中。这可能会让你想起我们在课程早期为线性回归问题添加多项式特征时看到的类似技巧。
我们现在可以在这个新的二维特征空间中学习一个线性支持向量机,其最大间隔和决策边界可能如下图所示,以正确分类这些点。对于任何我们想要预测类别的一维新数据点,我们只需创建其二维转换版本,并使用这个二维线性SVM来预测该二维点的类别。
如果我们应用我们刚才所做变换的逆变换,将数据点带回原始输入空间,我们可以看到二维空间中的线性决策边界对应于抛物线穿过X轴的两个点。
从一维到二维问题的扩展
为了让这个非常重要的概念更清晰,让我们从一个一维问题转向一个二维分类问题,并在此处看到同样的强大思想在起作用。
这里我们有两个类别,由黑色和白色的点表示。每个点都有两个特征:x0 和 x1。两个类别的点都散布在二维平面中的原点 (0, 0) 周围。白点形成一个紧挨着 (0, 0) 的簇,完全被黑点包围。同样,这对于线性分类器(在二维空间中是一条直线)来说,似乎不可能以任何程度的准确性将白点与黑点分开。
但正如我们在二维情况下所做的那样,我们可以通过添加第三个特征将每个二维点 (x0, x1) 映射到一个新的三维点。数学上,第三个特征是 1 - (x0^2 + x1^2)。这个变换将点塑造成一个围绕 (0, 0) 的抛物面。现在,白点由于靠近 (0, 0),被映射到具有较高垂直Z值(即接近1的新第三特征)的点。而黑点距离 (0, 0) 较远,被映射到Z值接近0甚至为负的点。
通过这种变换,就有可能找到一个超平面(例如 Z = 0.9),现在可以轻松地将靠近 Z = 1 的白色数据点与大部分或全部黑色数据点分开。最终,决策边界由三维空间中抛物面与最大间隔超平面决策边界相交的点集组成。这对应于原始输入空间中一个类似椭圆的决策边界,将白点与黑点分开。
核函数与核技巧
将输入数据点转换到一个可以轻松应用线性分类器的新特征空间的想法,是一个非常通用且强大的想法。我们可以对数据应用许多不同的变换,而核化SVM可用的不同核就对应着不同的变换。
在这里,我们将主要关注所谓的径向基函数核(缩写为RBF),并简要了解一下Scikit-Learn的SVM模块中也包含的多项式核。
SVM中的核函数告诉我们,给定原始输入空间中的两个点,它们在新特征空间中的相似度是多少。对于径向基函数核,两个点在转换后特征空间中的相似度是原始输入空间中向量之间距离的指数衰减函数,如下面的公式所示。
径向基函数核公式:
K(x, x') = exp(-γ * ||x - x'||^2)
那么径向基函数特征变换看起来像什么呢?下面的图表应该能给出一个直观的概念。这是另一个二元分类问题的图形说明。同样,此图仅用于说明目的,只是一个近似表示,但本质上,你可以用类似于我们之前看到的2D到3D示例的方式来思考它。
在左侧,输入空间中有一组样本,圆圈代表一个类别的训练点,方块代表第二个类别的训练点。在右侧,使用径向基函数核将相同的样本显示在转换后的特征空间中。实际上,该核将圆圈类一定距离内的所有点转换到特征空间的一个区域,而将方块类一定半径外的所有点移动到特征空间的另一个区域。深色圆圈和方块可能代表转换后特征空间中支持向量机最大间隔上的点,并且它还显示了原始输入空间中对应的点。
因此,正如我们之前在简单的1D和2D示例中看到的那样,核化支持向量机试图在转换后的特征空间(而非原始输入空间)中使用线性分类器来寻找类别间具有最大间隔的决策边界。线性SVM在特征空间中学习到的线性决策边界,对应于原始输入空间中的非线性决策边界。在这个例子中,就是输入空间中一个类似椭圆的闭合区域。
核技巧的优势
核化支持向量机在数学上值得注意的一点(被称为核技巧)是,在内部,算法不必对数据点执行到新的高维特征空间的这种实际变换。相反,核化SVM可以仅根据高维空间中点对之间的相似度计算来计算出这些更复杂的决策边界,而变换后的特征表示是隐式的。这种相似度函数(在数学上是一种点积)就是核化SVM中的“核”。对于某些类型的高维空间,点之间的相似度计算(即核函数)可以具有简单的形式,就像我们在径向基函数计算中看到的那样。这使得在底层变换特征空间复杂甚至是无限维时,应用支持向量机变得可行。
更好的是,我们可以轻松地插入各种不同的核,选择一个适合我们数据特性的核。同样,不同的核选择对应于到那个高维特征空间的不同类型的变换。
应用示例与参数
以下是在我们之前看到的那个更复杂的二元分类问题上使用带有RBF核的支持向量机的结果。你可以看到,与线性分类器不同,带有RBF核的SVM找到了一组更复杂且非常有效的决策边界,非常擅长将一个类别与另一个类别分开。请注意,SVM分类器仍然使用最大间隔原则来寻找这些决策边界,但由于数据的非线性变换,这些边界在原始输入空间中可能不再总是与间隔边缘点等距。
现在让我们看一个在Python中使用Scikit-learn实现此功能的示例。要使用SVM,我们只需从 sklearn.svm 导入 SVC 类,并像使用其他分类器一样使用它,例如,通过调用带有训练数据的 fit 方法来训练模型。
SVC 对象有一个名为 kernel 的参数,允许我们设置SVM使用的核函数。默认情况下,SVM将使用径向基函数核。但也支持许多其他选择。在第二个示例和绘图中,我们展示了使用多项式核而不是RBF核。使用 kernel='poly' 设置的多项式核,本质上代表了一种类似于之前二次方示例的特征变换。在讲座中,这个特征空间用原始输入特征的多项式组合来表示,正如我们在线性回归中也看到的那样。多项式核接受一个额外的参数 degree,用于控制模型复杂度和此变换的计算成本。
你可能已经注意到RBF核有一个参数 gamma。gamma 控制单个训练样本的影响范围,进而影响决策边界最终在输入空间中围绕点的紧密程度。较小的 gamma 意味着较大的相似度半径,因此距离较远的点也被认为是相似的,这导致更多的点被分组在一起,决策边界更平滑。另一方面,对于较大的 gamma 值,核值衰减得更快,点必须非常接近才能被认为是相似的。这导致更复杂、约束更紧的决策边界。你可以在笔记本示例中看到增加 gamma(即锐化核)的效果。较小的 gamma 值给出更宽、更平滑的决策区域,而较大的 gamma 值给出更小、更复杂的决策区域。你可以在创建 SVC 对象时设置 gamma 参数以这种方式控制核宽度,如代码所示。
你可能还记得线性SVM,SVM还有一个正则化参数 C,用于控制在满足最大间隔准则以找到简单决策边界与避免训练集上的错误分类之间进行权衡。C 参数对于核化SVM也很重要,并且它与 gamma 参数相互作用。笔记本中的这个示例显示了同时改变 C 和 gamma 的效果。如果 gamma 很大,那么 C 几乎没有影响。而如果 gamma 很小,模型受到的约束就大得多,C 的效果将类似于它如何影响线性分类器。通常,gamma 和 C 是一起调整的,最优组合通常在中间值范围内,例如,gamma 在0.0001到10之间,C 在0.1到100之间,尽管具体的最优值将取决于你的应用。核化SVM对 gamma 的设置相当敏感。
数据标准化的重要性
应用SVM时最重要的一点是,必须对输入数据进行标准化,使所有特征具有可比较的单位并处于相同的尺度上。我们之前在其他学习方法(如正则化回归)中也看到过这一点。
让我们将带有RBF核的支持向量机应用于一个真实世界的数据集,看看为什么这种标准化很重要。这里我们将把带有RBF核的支持向量机应用于乳腺癌数据集。请注意,我们没有以任何方式处理输入数据,只是传入原始值。我们可以看到结果:训练集准确率为1.00,测试集准确率为0.63,这表明支持向量机在训练数据上过拟合,在测试数据上表现很差。
现在让我们对训练数据添加一个 MinMaxScaler 变换,记住也要对测试数据应用相同的缩放器。应用此缩放器后,所有输入特征现在都位于相同的范围 [0, 1] 内。看看这些新结果,测试集准确率要高得多,达到了96%。这说明了标准化训练数据特征对SVM性能的巨大影响。
支持向量机的优缺点
让我们回顾一下支持向量机的优缺点。
优点:
以下是支持向量机的一些主要优势:
- 支持向量机在一系列数据集上表现良好,已成功应用于从文本到图像等多种类型的数据。
- 支持向量机由于能够指定不同的核函数(包括可能根据数据定制的核函数)而具有潜在的多样性。
- 支持向量机通常对低维和高维数据都有效,包括具有数百、数千甚至数百万稀疏维度的数据。这使其非常适合文本分类等任务。
缺点:
以下是支持向量机的一些主要缺点:
- 随着训练集规模的增加,SVM训练阶段的运行速度和内存使用量也会增加。因此,对于具有数十万或数百万实例的大型数据集,SVM可能变得不太实用。
- 正如我们在将支持向量机应用于真实世界数据集时所看到的,使用SVM需要仔细地对输入数据进行标准化以及参数调优。如果特征尚未处于相似尺度,则应进行标准化,使所有特征具有可比较的单位。
- 支持向量机不直接提供预测的概率估计,而某些应用需要这些概率。现在有一些方法可以使用诸如Platt缩放等技术来估计这些概率,该方法通过将逻辑回归模型拟合到分类器分数,将分类器的输出转换为类别上的概率分布。
- 最后,解释支持向量机的内部模型参数可能很困难。这意味着在解释对人类很重要的场景中,支持向量机的适用性可能有限,例如当我们想理解为什么做出特定预测时。
关键参数总结
作为提醒,控制核化SVM模型复杂度的主要有三个参数。
以下是需要调整的主要参数:
- 核类型:默认为径向基函数(RBF),但Scikit-Learn的SVC模块中还提供了其他几种常见类型。
- 核特定参数:每个核都有一个或多个核特定参数,用于控制诸如训练点根据其距离的影响等方面。对于RBF核,SVM性能对控制核宽度的
gamma参数设置非常敏感。 - 正则化参数 C:对于任何支持向量机,
C正则化参数都起作用,正如我们之前讨论的那样。它通常与核参数(如gamma)一起调整以获得最佳性能。

总结

在本节课中,我们一起学习了核化支持向量机。我们了解了它如何通过核技巧将数据映射到高维空间,从而解决线性不可分的复杂分类问题。我们探讨了径向基函数核和多项式核的基本原理,并通过示例看到了参数 gamma 和 C 对模型的影响。最重要的是,我们认识到数据标准化对于SVM性能至关重要。最后,我们总结了SVM的优势、局限性和关键调优参数,为在实际数据科学项目中应用这一强大工具奠定了基础。
72:交叉验证技术 🧪

在本节课中,我们将要学习一种重要的模型评估方法——交叉验证。我们将了解为什么单一的“训练-测试”数据划分可能不够可靠,以及如何使用交叉验证来获得更稳定、更全面的模型性能评估。
概述
到目前为止,我们已经学习了许多监督学习方法。在应用这些方法时,我们遵循了一系列一致的步骤。首先,使用 train_test_split 函数将数据集划分为训练集和测试集。然后,在训练集上调用 fit 方法来估计模型。最后,使用 predict 方法为新数据实例估计目标值,或使用 score 方法在测试集上评估训练后模型的性能。
我们记得,将原始数据划分为训练集和测试集的原因,是为了使用测试集来估计在训练数据上训练的模型对新的、未见过的数据的泛化能力。测试集代表了在训练期间未见过,但与原始数据集具有相同一般属性的数据。或者用更专业的语言来说,它与训练集来自相同的基础分布。
交叉验证的基本原理
上一节我们回顾了传统的单一“训练-测试”划分方法,本节中我们来看看交叉验证如何改进这一过程。
交叉验证是一种超越使用单一“训练-测试”划分来评估单个模型的方法,它使用多个“训练-测试”划分,每个划分都用于训练和评估一个独立的模型。
那么,为什么这比我们原始的单一“训练-测试”划分方法更好呢?你可能已经注意到,例如,在处理某些示例或作业时,通过为 train_test_split 函数中的 random_state 种子参数选择不同的值,仅仅由于偶然性,运行分类器得到的准确率分数可能会有很大差异,这取决于最终进入训练集的具体样本。
交叉验证通过运行多个不同的“训练-测试”划分,然后对结果取平均值,而不是完全依赖于一个特定的训练集,从而基本提供了关于分类器平均表现如何的更稳定和可靠的估计。
交叉验证的实践应用
理解交叉验证在实践中如何使用非常重要。你需要交叉验证的主要场景是,当你有一个特定的分类任务,并且想要比较一种模型(例如支持向量机)与另一种模型(例如朴素贝叶斯)的准确率时。
在这种情况下,为了做好比较,你不应仅仅依赖于单一“训练-测试”划分的准确率数字。因为可能仅仅是偶然,使用那个特定的“训练-测试”划分时,朴素贝叶斯模型碰巧表现得更好,而实际上,它在总体上可能更差。因此,你应该使用交叉验证来创建多个“训练-测试”划分,从而以更可靠的方式比较这两种方法。然后计算这些“折”上的整体平均评估指标。
例如,如果你使用十折交叉验证,这将使用10个不同的“训练-测试”划分来评估这两种方法,而不是单一的划分。与仅依赖单一“训练-测试”划分相比,这将为你提供每个模型可能表现如何的更稳定估计。
一旦你最终决定了使用哪种建模方法,并且没有更多的调整需要做,那么你就可以使用你拥有的所有数据来训练一个最终的生产分类器。
交叉验证与模型构建
这里一个可能有点令人困惑的点是,如何使用应用交叉验证产生的多个模型。例如,10折交叉验证将产生10个训练好的模型,每个模型都有自己的一组估计系数。
当你应用交叉验证时,你只是为这10个模型中的每一个计算一个评估指标(如准确率),然后取这10个评估数字的平均值。仅此而已。通过交叉验证,你并不是通过某种方式将这10个模型合并在一起来产生一个新的混合模型。
现在有一些特殊场景可以这样做,那是一个独立的主题,称为模型平均,但我们在这里不会深入探讨。
底线是,如果你想比较两种不同的模型类型,请使用K折交叉验证,并且在计算评估数字时不要仅仅依赖单一的“训练-测试”划分。
模型评估与模型调优的区别
同样重要的是,要理解使用交叉验证进行模型评估与使用它进行模型调优之间的区别。
如果你的任务是评估和比较已经单独调优和优化的不同模型类型,你可以使用带有“训练-测试”划分的K折交叉验证。
但是,如果你的任务是调优单个模型,例如当你想为支持向量机找到最佳超参数时,这使用了一个稍微不同的设置。在这个设置中,我们不是仅仅进行“训练-测试”划分,而是将数据分成三个部分,称为训练集、验证集和测试集。
我们将在“模型选择与优化分类器”讲座中介绍如何使用“训练-验证-测试”划分。
K折交叉验证的工作原理
以下是交叉验证如何在数据上运行的图形说明。
最常见的交叉验证类型是K折交叉验证,最常见的是K设置为5或10。例如,要进行五折交叉验证,原始数据集被划分为五个大小相等或接近相等的部分。这些部分中的每一个都称为一个“折”。
然后训练一系列五个模型,每个“折”对应一个。第一个模型(模型1)使用第2到第5折作为训练集进行训练,并使用第1折作为测试集进行评估。第二个模型(模型2)使用第1、3、4、5折作为训练集进行训练,并使用第2折作为测试集进行评估,依此类推。
当这个过程完成后,我们有五个准确率值,每个“折”一个。
在Scikit-learn中实现交叉验证
在Scikit-learn中,你可以使用 model_selection 模块中的 cross_val_score 函数来进行交叉验证。
参数首先是你要评估的模型,然后是数据集,接着是对应的真实目标标签或值。默认情况下,cross_val_score 进行五折交叉验证,因此它返回五个准确率分数,每个“折”一个。如果你想改变“折”的数量,可以设置 cv 参数。例如,cv=10 将执行十折交叉验证。
通常的做法是计算所有“折”上准确率分数的平均值,并报告平均交叉验证分数,作为我们预期模型平均准确程度的衡量标准。
交叉验证的优势与成本
在多个划分上计算模型准确率的一个好处是,与单一划分相比,它为我们提供了关于模型对特定训练集性质的敏感性的潜在有用信息。因此,我们可以查看所有这些交叉验证“折”上多个分数的分布,以了解模型偶然在任何新数据集上表现非常差或非常好的可能性有多大。这样,我们可以从这些多个分数中做出某种最坏情况或最佳情况的性能估计。
这些额外信息确实伴随着额外的成本。进行交叉验证确实需要更多的时间和计算。例如,如果我们执行K折交叉验证,并且不并行计算“折”的结果,那么获得准确率分数所需的时间大约是单一“训练-测试”划分的K倍。然而,我们对模型在未来数据上可能表现如何的了解的增加,通常非常值得这个成本。
分层K折交叉验证
在默认的交叉验证设置中,例如使用5折,前20%的记录用作第一折,接下来的20%用于第二折,依此类推。这样做的一个问题是,数据可能以某种方式创建,使得记录被排序,或者至少在类别标签的顺序上显示出一些偏差。
例如,如果你查看我们的水果数据集,数据文件中恰好所有类别1和2(苹果和橘子)的标签都在类别3和4之前。因此,如果我们简单地将前20%的记录作为第一折(将用作评估模型1的测试集),它将仅在类别1和2的示例上评估分类器,而完全不在其他类别3和4上评估,这将大大降低评估的信息量。
因此,当你要求Scikit-learn为分类任务进行交叉验证时,它实际上做的是所谓的分层K折交叉验证。
分层交叉验证意味着在分割数据时,每个“折”中的类别比例尽可能接近整个数据集中类别的实际比例,如图所示。对于回归问题,Scikit-learn使用常规的K折交叉验证,因为保留类别比例的概念对于日常回归问题来说并不真正相关。
留一法交叉验证
在一个极端情况下,我们可以做一种叫做留一法交叉验证的事情,这其实就是将K设置为数据集中数据样本数量的K折交叉验证。换句话说,每个“折”由一个样本作为测试集,其余数据作为训练集。
当然,这使用了更多的计算,但特别是对于小数据集,它可以提供改进的估计,因为它为模型提供了最大可能数量的训练数据,当训练集很小时,这可能有助于模型的性能。
验证曲线
有时我们想评估模型的一个重要参数对交叉验证分数的影响。非常有用的函数 validation_curve 使得运行这种类型的实验变得容易。
与 cross_val_score 类似,validation_curve 默认会进行五折交叉验证,但你也可以通过 cv 参数进行调整。与 cross_val_score 不同的是,你还可以指定一个分类器参数名称和一组你想要扫描的参数值。
所以你首先传入估计器对象(即要使用的分类器或回归对象),接着是数据集样本 X 和目标值 Y,然后是你想要扫描的参数名称,以及该参数在扫描过程中应取值的参数值数组。
validation_curve 将返回两个二维数组,分别对应于在训练集和测试集上的评估。每个数组对于扫描中的每个参数值都有一行,列数是使用的交叉验证“折”的数量。
例如,这里显示的代码将使用径向基函数支持向量机,在对应于四个不同指定的核 gamma 参数值的数据子集上拟合四个模型。这将返回两个4x5的数组(四个gamma级别和五个交叉验证“折”),包含训练集和测试集的分数。

你可以绘制来自 validation_curve 的这些结果,如图所示,以了解模型性能对给定参数变化的敏感程度。X轴对应于参数值,Y轴给出评估分数(例如分类器的准确率)。
总结
本节课中我们一起学习了交叉验证技术。最后,作为一个提醒,交叉验证用于评估模型,而不是学习或调优一个新模型。要进行模型调优,我们将在后续讲座中学习如何使用称为网格搜索的方法来调整模型的参数。
73:决策树算法 🌳

在本节课中,我们将要学习决策树算法。这是一种流行的监督学习方法,可用于分类和回归任务。我们将了解其工作原理、如何构建、如何防止过拟合,以及如何解释和可视化决策树模型。
概述
决策树是一种通过一系列“如果-那么”规则来预测目标值的监督学习方法。它易于理解和使用,常被用于探索性数据分析,以识别数据集中有影响力的特征。
决策树的基本概念
上一节我们介绍了决策树是一种监督学习方法。本节中我们来看看它的核心思想。
决策树通过学习特征值上的一系列明确的“如果-那么”规则来预测目标值。这类似于一个猜谜游戏:通过提出一系列“是/否”问题,逐步缩小范围,最终确定答案。
以下是决策树构建过程中的核心概念:
- 节点:代表一个决策问题(例如,“花瓣长度 > 2.35 厘米吗?”)。
- 分支:代表问题的答案(“是”或“否”),连接节点到下一层。
- 叶节点:位于树的底部,代表最终的预测结果(例如,花的种类)。
- 分裂点:用于划分数据的特征阈值(例如,
花瓣宽度 > 1.2中的1.2)。 - 信息增益:衡量一次数据分裂效果的数学标准。信息增益高的分裂能很好地将一个类别与其他类别分开。
使用Iris数据集构建决策树
为了理解决策树如何工作,我们使用一个经典数据集:Iris(鸢尾花)数据集。该数据集包含三种鸢尾花的测量数据,我们的任务是基于花的尺寸预测其种类。
以下是Iris数据集的关键信息:
- 特征:花萼长度、花萼宽度、花瓣长度、花瓣宽度(均为连续值)。
- 目标类别:Setosa, Versicolor, Virginica。
- 样本数量:150朵花,每类50朵。
构建决策树的目标是:找到能以最少的步骤、最高准确率分类数据的提问序列。算法从寻找能带来最大信息增益的特征和分裂点开始,然后递归地对子集重复此过程,直到叶节点“纯净”(即节点内所有样本属于同一类)或满足停止条件。
对于一个新样本,我们只需从根节点开始,根据其特征值回答问题,沿着分支向下走,直到到达一个叶节点。该叶节点中多数样本的类别就是预测结果。
例如,对于一朵花:
花瓣长度 = 3厘米, 花瓣宽度 = 2厘米, 花萼宽度 = 2厘米
根据决策树规则,它最终会到达一个所有样本都是Virginica的叶节点,因此预测为Virginica。
在Scikit-learn中使用决策树
在Scikit-learn中,我们可以轻松地构建决策树模型。
以下是使用决策树分类器的基本代码步骤:
# 导入必要的库和模块
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
# 加载数据并划分训练集和测试集
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)
# 创建决策树分类器对象并拟合训练数据
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
# 评估模型在训练集和测试集上的准确率
print("训练集准确率: {:.2f}".format(clf.score(X_train, y_train)))
print("测试集准确率: {:.2f}".format(clf.score(X_test, y_test)))
运行上述代码,你可能会发现训练集准确率是1.0(完美),而测试集准确率稍低。这表明模型可能过拟合了训练数据。

防止过拟合:剪枝
决策树容易生成复杂、过拟合的模型。为了防止过拟合,我们需要控制树的复杂度,这个过程称为剪枝。
Scikit-learn的决策树主要支持预剪枝,即在树完全生长之前就停止其生长。我们可以通过以下关键参数来控制:
max_depth:树的最大深度。限制分裂次数,是最常用的控制复杂度的参数。min_samples_leaf:一个叶节点所需的最小样本数。避免基于极少数样本做出决策。max_leaf_nodes:叶节点的最大数量。间接控制树的整体大小。
例如,设置 max_depth=3 可以简化模型:
clf_pruned = DecisionTreeClassifier(max_depth=3)
clf_pruned.fit(X_train, y_train)
# 此时训练集准确率可能略有下降,但测试集准确率可能提升,泛化能力更好。
解释与可视化决策树
决策树的一大优势是易于解释。我们可以将整个树可视化,清晰地看到决策路径。
以下是一个可视化决策树的示例(假设有辅助函数 plot_decision_tree):
# 假设存在一个可视化函数
plot_decision_tree(clf, iris.feature_names, iris.target_names)
生成的树形图中:
- 每个节点第一行显示决策规则。
- 第二行显示到达该节点的样本总数。
- 第三行显示这些样本的类别分布。
- 第四行显示该节点的多数类。
通过观察节点大小(样本数),我们可以了解大多数数据遵循的决策路径。颜色深浅通常表示该节点中多数类的纯度。
特征重要性分析
除了可视化整棵树,另一种强大的分析工具是计算特征重要性。它量化了每个特征对模型整体预测准确率的贡献程度,是一个介于0到1之间的值,所有特征的重要性之和为1。
在Scikit-learn中,训练好的决策树模型有一个 feature_importances_ 属性来存储这些值。
以下是获取和可视化特征重要性的方法:
import matplotlib.pyplot as plt
import numpy as np
# 获取特征重要性
importances = clf.feature_importances_
indices = np.argsort(importances)
# 绘制水平条形图
plt.figure()
plt.title("特征重要性")
plt.barh(range(X_train.shape[1]), importances[indices])
plt.yticks(range(X_train.shape[1]), [iris.feature_names[i] for i in indices])
plt.xlabel("相对重要性")
plt.show()
对于Iris数据集,你可能会发现“花瓣长度”具有最高的特征重要性,这与它在决策树顶部作为首要分裂特征的地位相符。
需要注意的是:特征重要性低并不绝对意味着该特征与预测无关。它可能只是与另一个高重要性特征高度相关,因此未在早期被选中。

应用于乳腺癌数据集
让我们将决策树应用于之前使用过的乳腺癌数据集,并观察特征重要性。
以下是应用于乳腺癌数据集的示例代码框架:
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier

cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
# 使用预剪枝参数控制复杂度
clf_cancer = DecisionTreeClassifier(max_depth=4, min_samples_leaf=8, random_state=0)
clf_cancer.fit(X_train, y_train)
# 评估并可视化特征重要性
print("测试集准确率: {:.2f}".format(clf_cancer.score(X_test, y_test)))
# ... (绘制特征重要性图)
对于该数据集,“平均凹点”和“最差面积”等特征可能显示出较高的重要性。作为练习,你可以尝试移除 max_depth 和 min_samples_leaf 参数,使用默认设置,观察测试集准确率的变化以及过拟合的加剧情况。
决策树的优缺点

优点 ✅
- 易于理解和解释:规则清晰,可视化直观,适合向非专业人士解释。
- 无需特征预处理:由于基于特征阈值做决策,对特征的尺度不敏感,可以处理混合类型(连续、分类)和不同尺度的特征。
- 可用于分类和回归:通过改变叶节点的预测值(多数类或平均值)即可。
缺点 ❌
- 容易过拟合:即使进行剪枝,单个决策树仍可能过度记忆训练数据的细节,导致泛化性能不佳。
- 不稳定:训练数据的微小变化可能导致生成完全不同的树。
- 可能不是最优:其性能有时不如其他更复杂的集成模型。

为了克服过拟合问题,通常的做法是构建决策树集成(如随机森林、梯度提升树),这将在后续课程中介绍。
总结
本节课中我们一起学习了决策树算法。我们了解到:
- 决策树通过一系列“如果-那么”规则进行预测,核心在于寻找信息增益最大的分裂点。
- 在Scikit-learn中,可以使用
DecisionTreeClassifier轻松构建模型。 - 决策树容易过拟合,必须通过剪枝(如设置
max_depth,min_samples_leaf)来控制模型复杂度。 - 决策树的可解释性极强,可以通过可视化树形图或分析特征重要性来理解模型如何做出决策。
- 决策树对数据预处理要求低,但单个树容易过拟合,常作为集成方法的基础组件。

通过调整关键参数并利用可视化工具,决策树成为了解数据特征和建立初始预测模型的强大工具。
74:19_独热编码(选修)📊


在本节课中,我们将要学习如何处理机器学习中的分类特征或分类标签。许多入门级的机器学习示例都假设所有特征都是数值型的,但在实际应用中,我们经常会遇到分类特征,或者数值与分类特征混合的情况。本节视频将讨论如何处理这种特定情况。
处理分类变量
上一节我们介绍了分类变量的概念,本节中我们来看看一个具体的例子。假设我们正在处理一个数据集,其中有一个“颜色”特征,颜色可以是红色、黄色、绿色等若干可能值中的一个。这是一个典型的分类变量,它属于一组可能值中的一个,而不是一个数值。
许多预测模型无法直接处理这种分类输入。例如,线性回归、逻辑回归或支持向量机无法直接使用包含“红”、“黄”、“绿”值的颜色特征。虽然像决策树这类方法可以直接处理分类变量,但当你使用的预测方法无法直接处理它们时,你就需要找到解决方案。
需要注意的是,有些分类数据可能被编码为数字(例如,红色=1,黄色=2)。因此,即使数据中的某一列看起来是数值特征,它实际上也可能是用数字编码的分类特征。
独热编码解决方案
一种广泛使用的处理分类变量的解决方案叫做独热编码。它的作用是将单个分类值转换为一个二进制值向量。
以下是独热编码的工作原理:
- 查看你的分类特征。
- 列出该特征所有可能取值的列表。在这个例子中,我们假设只有三种可能的颜色值:红、黄、绿。
- 为了得到“红色”的独热编码,我们遍历所有可能的类别,在代表“红色”的列中存储1,在所有其他类别列中存储0。
之所以称为“独热编码”,是因为在生成的向量中,恰好有一列是1(代表被选中的类别),其余列都是0。
让我们看另一个例子。假设我们要为第四行编码“绿色”这个值。在独热编码版本中,红色列对应0,黄色列对应0,绿色列对应1。这是一个非常简单的概念,在统计学中也被称为虚拟编码、K之一编码或指示变量。
这样,你就将分类值转换成了类似线性回归这样的模型可以处理的01值向量。
在Python中实现独热编码
在Python中,无论是使用Pandas还是Scikit-learn,将分类值转换为独热编码都非常容易。
使用Pandas
在Pandas中,有一个名为 get_dummies 的函数。这个名称源于统计学中将独热编码称为虚拟变量。get_dummies 可以将分类变量转换为虚拟指示变量。
以下是一个示例代码:
import pandas as pd
# 创建一个包含分类值的列表
data = [‘A‘, ‘B‘, ‘C‘, ‘A‘]
df = pd.DataFrame(data, columns=[‘Category‘])
# 使用get_dummies进行独热编码
one_hot_encoded_df = pd.get_dummies(df[‘Category‘])
print(one_hot_encoded_df)
在这个例子中,Pandas首先会遍历并列出所有可能的值(A, B, C)。然后,对于每个条目,它会标记对应的列。例如,条目‘A’会使A列等于1,其余列设为0,依此类推。在Pandas中实现起来超级简单。
使用Scikit-learn
在Scikit-learn中实现同样简单,但需要考虑两种情况。
第一种情况:分类变量是特征
也就是说,它位于特征矩阵X中。在这种情况下,你需要使用 sklearn.preprocessing.OneHotEncoder 来为该特征的每一行创建独热编码向量。
以下是操作步骤:
- 使用
fit方法:分析数据,找出该特征列中所有唯一的值。 - 一旦独热编码器被拟合,并找到了该特征列中所有可能的唯一值,你就可以调用
transform方法来实际创建该特征列的独热编码向量。
默认情况下,这些独热编码向量是稀疏的,即该方法默认产生一个稀疏矩阵。如果需要,你可以通过设置 sparse=False 来改变这一点,但默认实现是稀疏的。
第二种情况:预测目标是分类变量
也就是说,标签是分类变量。例如,你可能想将一个多分类标签(如预测颜色)转换为一系列二分类问题。为此,你需要使用 LabelBinarizer 类。
它的使用方式与独热编码器类似,也是通过 fit 和 transform 方法。需要注意的是,在当前Scikit-learn的实现中,默认情况下它产生的是密集矩阵。当然,如果你希望得到稀疏的二进制标签集,也可以关闭此选项并选择稀疏输出。
总之,在Scikit-learn中,当你想将独热编码用作特征或标签时,就分别使用这两种情况下的对应类。
代码示例:OneHotEncoder的fit与transform
以下是一个代码示例,展示了如何配合OneHotEncoder对象使用 fit 和 transform 方法。
和Scikit-learn中的通常步骤一样,第一步是创建对象。在这个例子中,我们还传递了 handle_unknown 选项,稍后会讨论它的作用。
假设我们有一些初始数据,包含3行和2列:
- 第一列是“男性”或“女性”。
- 第二列是整数1、3或2。
当我们调用 fit 方法时,Scikit-learn的OneHotEncoder对象会分析这些数据,并找出第一列和第二列中的唯一值。
- 第一列的唯一值将是“女性”和“男性”。
- 第二列的唯一值将是1、2、3。
我们可以通过查看调用 fit 方法后设置的 categories_ 属性来确认这一点。它会告诉我们,在第一列中找到了“女性”和“男性”作为类别,在第二列中找到了1、2、3作为唯一值。
在了解了每个类别的唯一值之后,我们就可以进行转换了。我们可以要求Scikit-learn为第一列创建一个独热编码向量,然后紧接着为第二列创建独热编码向量。
如果你有两个连续的分类值列,并要求Scikit-learn进行转换,它会将第一个值转换为独热编码,然后拼接第二个值的独热编码向量。
现在,我们要求它转换一个新的小表格,其中第一行是“女性”和数字1,第二行是“男性”和数字4。让我们看看输出是什么。
首先,它会为“女性”创建独热编码。注意,它最初找到的类别是“女性”和“男性”,所以独热编码中只有两列:“女性”列和“男性”列。因为我们有分类值“女性”,所以它正确地将“女性”列设为1,“男性”列设为0。在这个迷你数据集的第二行,你可以看到“男性”被映射到了独热编码向量 [0, 1],即“男性”类别对应的第二列被设为1。
接下来,看看它为数值列所做的独热编码。它将这里的数值列视为一组编号的类别。当第二列的值为1时,它被转换为对应的独热编码向量 [1, 0, 0]。确实,输出也是如此。
但第二个例子很有趣。它要求为一个从未见过的类别(数字4)创建独热编码向量。在它为那些数字找到的唯一值集合中,并没有4。这就是 handle_unknown 设置发挥作用的地方。
如果将 handle_unknown 设置为 ‘ignore‘,那么当它遇到一个在 fit 方法期间从未见过的值,并尝试为其创建独热编码时,它只会简单地在该条目处放入0。因此,在这里它将数字4转换为一个实际上没有1的独热编码向量,结果全是0。这就是当你在测试集中遇到一个训练期间从未见过的类别时会发生的情况(使用 handle_unknown=‘ignore‘ 选项)。它会创建一个全零的独热编码向量。
handle_unknown 设置的另一个选项是 ‘error‘。如果这样做,那么对于未知的类别值,它不会默默地创建全零向量,而是会在代码中引发错误并停止执行。
以上就是OneHotEncoder的基本操作。
应用独热编码的重要规则

最后,这里有一些规则,以确保你正确应用这个独热编码步骤。
首先,先分割数据集
与Scikit-learn中许多其他类型的转换一样,你必须首先分割训练集和测试集,然后再拟合独热编码器对象。你使用训练数据来拟合独热编码器对象,而不是传入包含测试集的整个数据集。你必须在分割之后才能进行拟合,这样独热编码器就不会获得测试集中的任何信息。
然后,使用相同的编码器进行转换
一旦你使用训练数据拟合了独热编码器,你就可以使用 transform 函数将这个刚刚拟合好的相同独热编码器应用到训练数据和测试数据上。
你需要确保对训练和测试使用相同的独热编码器,并且确保只使用训练数据来训练它。这可以保证虚拟变量在训练和测试数据之间匹配,并且所有类别都得到表示。
处理未知类别
一个常见的问题是:如果测试集中有我们在训练期间没有见过的额外类别,会发生什么?这对应着我刚才展示的情况:当我们被要求为数字4创建独热编码时,而用于拟合独热编码器的数据只有值1到3。
在这种情况下,你有两个选择:
- 你可以创建一个特殊的“其他”类别,将测试集中的任何额外类别映射到“其他”。
- 或者,你可以忽略它们,并允许为该测试集中的特定项创建一个全零的独热向量。
本节课中我们一起学习了如何处理机器学习中的分类变量。我们介绍了独热编码的概念,它通过将分类值转换为二进制向量,使得许多模型能够处理这类数据。我们探讨了在Pandas中使用 get_dummies 函数以及在Scikit-learn中使用 OneHotEncoder 和 LabelBinarizer 的具体方法,并通过代码示例加深理解。最后,我们强调了正确应用独热编码的关键规则,特别是先分割数据再拟合,以及如何处理训练集中未出现过的测试集类别。掌握这些技巧,能让你更有效地在数据科学项目中使用包含分类特征的数据。
75:模型评估与选择 📊


概述
在本节课中,我们将要学习为什么仅使用准确率来评估监督学习模型是不够的。我们将探讨准确率这一简单易懂的指标存在的局限性,并学习一系列更全面的评估指标。这些指标能帮助我们更深入地理解模型的性能,并为特定应用场景选择最合适的模型。
为什么准确率可能不够?
上一节我们介绍了监督学习的基本流程。本节中我们来看看为什么仅依赖准确率进行评估可能存在问题。
当我们开始使用监督机器学习方法时,我们使用准确率来评估分类器的性能。准确率,你可能还记得,是被正确分类的样本所占的比例。也就是说,分类器预测的标签与正确或真实标签相匹配的样本比例。
对于回归模型,我们通常使用默认的R平方指标来评估性能。在本模块中,你将了解到这些简单易懂的度量指标也存在缺点,它们无法完整地描绘监督学习模型的性能,并且可能不是衡量你应用成功与否的正确指标。
因此,我们将介绍几种超越准确率的额外评估指标。我们将看到它们是如何定义的,使用它们的动机是什么,以及如何在Scikit-learn中使用它们,以便更好地了解监督模型在给定数据集上的表现。
你还将学习如何为你的应用选择合适的评估指标,这可以帮助你选择最佳模型或找到最优参数。
评估在机器学习工作流中的角色
让我们回顾一下本课程早些时候介绍的这个工作流程图。你可以看到,评估是应用机器学习开发周期中的关键部分。
一旦模型被训练,评估步骤就会提供关于训练模型性能特征的关键反馈,特别是那些对你的应用可能很重要的特征。例如,评估步骤的结果可能帮助你理解哪些数据实例被错误分类或预测,这反过来可能建议在特征和模型优化阶段,为你的学习模型提供更好的特征、不同的核函数或其他改进。
正如我们之前讨论的,在训练阶段优化的目标函数可能是一个不同的、所谓的代理指标,它在实践中比评估指标更容易用于优化目的。例如,一个商业搜索引擎可能使用一个排名算法,该算法被训练来推荐与查询最匹配的相关网页。换句话说,试图预测页面的相关标签。这可能是训练阶段的目标。
但在评估阶段,有许多评估方法可以用来衡量该搜索引擎使用该排名算法的性能的各个方面,这些方面对搜索公司的业务很重要。例如,系统每天有多少独立用户,或者典型用户搜索会话的时长等等。
因此,评估指标是最终用于在不同训练好的模型或设置之间进行选择的指标。实际上,商业搜索应用通常使用多个评估指标的记分卡来做出重要的业务决策或开发决策,以选择使用哪些模型。
所以,选择与你的应用目标相匹配的评估方法非常重要。
准确率的局限性:不平衡类别示例
在开始为二分类定义和使用一些评估指标之前,让我们先看一个例子,说明为什么仅仅看准确率可能不足以很好地了解分类器在做什么。它还将向我们展示,更多地了解学习算法所犯的错误类型如何帮助我们更好地了解模型的预测性能。
首先,考虑我们有一个二分类任务的情况,其中有很多实例被标记为负类,但只有少数实例属于正类。例如,我们可能在在线搜索或推荐系统中看到这种情况,系统必须预测是否显示广告或产品建议,或者根据用户的查询和他们过去点击的内容等,在页面上显示可能相关的查询建议或项目。所以这些将是正例。但当然,有很多很多不相关的项目属于负类,向用户展示这些项目没有意义。这被称为不平衡类别场景。
另一个例子可能是信用卡交易数据集,其中绝大多数交易被归类为正常而非欺诈,只有少数交易可能被归类为欺诈。这些情况也适用于多类分类问题,涉及具有不平衡类别的数据集。不平衡类别在机器学习场景中非常常见,因此了解如何处理它们很重要。
具体来说,假设我们有一个电子商务应用,对于每1000个随机抽样的产品项目,其中一个是与用户需求相关的,其他999个不相关。
回想一下,在一组实例上计算的准确率,只是分类器标签预测正确的实例数量除以实例总数。假设你开发了一个用于预测相关电子商务项目的分类器。开发完成后,你在测试集上测量其准确率为99.9%。乍一看,这似乎好得惊人,对吧?它非常接近完美。
但是,让我们将其与一个总是只预测最可能类别(即不相关类别)的虚拟分类器进行比较。换句话说,无论实际实例是什么,虚拟分类器总是预测一个项目不相关。
所以,如果我们有一个包含1000个项目的测试集。平均而言,其中999个无论如何都是不相关的。因此,我们的虚拟分类器将正确预测所有这些999个项目的“不相关”标签。所以虚拟分类器的准确率也将是99.9%。
实际上,我们自己的分类器性能一点也不令人印象深刻,它甚至不比不看数据就总是预测多数类更好。
使用Notebook演示不平衡类别
让我们使用我们的Notebook查看另一个在真实数据集上使用不平衡类别进行分类的示例。
我将从这里开始使用手写数字数据集,该数据集包含标记有10个类的手写数字图像,代表数字0到9。正如我们通过加载数据集,然后使用NumPy的bincount方法计算每个类中的实例数量所看到的,每个类中的实例数量大致相同,因此这个数据集具有平衡的类别。
然而,使用这个数字数据集,我们现在要做的是创建一个具有两个不平衡类别的新数据集,方法是将所有不是数字1的数字标记为标签0的负类,将数字1标记为标签1的正类。
以下是创建新二进制标签的代码片段:
# 假设 y 是原始标签(0-9)
y_binary = (y == 1).astype(int)
现在,当我们使用bincount时,我们可以看到大约有1600个负例,但只有182个正例。所以,我们确实有一个类别不平衡的数据集,或者正如预期的那样,负例与正例的比例几乎是9:1。
现在让我们在这个不平衡集上创建一个训练测试分区。然后使用径向基函数作为核,用这些二进制标签训练一个支持向量机分类器。
我们使用score方法获得准确率。我们可以看到这刚刚超过90%。再次,乍一看,分类器90%的准确率似乎相当不错。然而,现在让我们创建一个正确反映类别不平衡的虚拟分类器,看看90%是否真的那么令人印象深刻。
Scikit-learn使得创建虚拟分类器变得很容易,只需使用DummyClassifier类,如下所示。虚拟分类器之所以被称为“虚拟”,是因为它们甚至不看数据来进行预测。它们只是使用你在创建它们时指示它们使用的策略或经验法则。
实际上,当你创建分类器时,你设置strategy参数来告诉它使用什么经验法则来进行预测。所以这里我们将其设置为most_frequent策略来预测最频繁的类。
这里的虚拟分类器的使用方式与常规分类器一样。因此,为了准备进行预测,我们在包含训练集实例和标签的X_train和y_train变量上调用fit方法。现在,这个虚拟分类器实际上不会查看这些变量中的单个数据实例,但它确实使用y_train变量来确定训练数据中哪个类最频繁。
最后,就像常规分类器一样,我们可以调用predict方法对测试集进行预测。这个例子显示了虚拟分类器预测的输出。正如所承诺的,你可以看到,它总是为测试集中的每个实例预测0或负类。
现在我们可以调用通常的score方法来获得虚拟分类器恒定负预测的准确率。我们可以看到它也是大约90%,与我们之前使用径向基函数核的支持向量机分类器相同。
所以那个支持向量分类器的性能实际上只比虚拟分类器好一点点。虚拟分类器提供了所谓的零准确率基线,即总是选择最频繁类所能达到的准确率。
你不应该将虚拟分类器用于实际的分类问题,但它确实提供了一个有用的合理性检查和比较点。
还有其他类型的虚拟分类器,它们提供了与strategy参数其他选择相对应的零基线,如下所示:
most_frequent:我们刚刚看到的策略,总是预测最频繁的标签。stratified:与恒定的最频繁预测不同,这是一种基于类别分布的随机预测。例如,如果正类在训练集中出现90%的时间,那么分层虚拟分类器将以90%的概率输出正类标签,否则将输出负类标签。这有助于确保可以计算依赖于正类和负类预测结果计数的指标。uniform:另一种随机预测方法,将均匀随机生成类别预测。也就是说,所有类被输出的机会均等,而不是根据它们在训练集中的频率进行加权。这个策略可能有助于准确估计每个类最常见的预测错误类型。constant:在计算一些指标(如我们稍后将介绍的F分数)时可能很有用。为什么?当我们有一个二分类任务,其中最频繁的类是负类时,使用most_frequent策略将永远不会预测正类,并且永远无法计算正确预测的正实例数量。因此,这种正确预测的正实例的总数将为0。所以,正如你稍后将看到的,这反过来会导致一些重要指标(如F分数)始终为零。因此,使用constant策略,我们可以强制虚拟分类器始终预测正类,即使它是类别集合中的少数类,这将导致F分数的计算更有意义。
理解混淆矩阵
现在,让我们更仔细地看看使用二分类器时可能看到的不同类型的结果。这将让我们深入了解为什么仅使用准确率无法完整地描绘分类器的性能,并将推动我们定义和探索额外的评估指标。
对于正类和负类,有四种可能的结果,我们可以将其分为两种情况,对应于这个矩阵的第一行和第二行。
如果实例的真实标签是负的,分类器可以预测负(这是正确的,称为真阴性),或者它可以错误地预测正(这是一个错误,称为假阳性)。
如果实例的真实标签是正的,分类器可以预测负(这是一个错误,称为假阴性),或者它可以预测正(这是正确的,称为真阳性)。
也许记住这个的一个快速方法是:这些矩阵单元格中的第一个词如果是分类器错误就是false,如果是分类器成功就是true。第二个词如果真实标签是负的就是negative,如果真实标签是正的就是positive。
假阳性的另一个你可能从统计学中知道的名字是第一类错误。假阴性的另一个名字是第二类错误。
我们将使用这些两个字母的组合,TN、FN、FP和TP,作为定义一些新评估指标时的变量名。我们还将在这里使用大写N来表示实例的总数,即矩阵中所有值的总和,也就是我们正在查看的数据点的数量。
这个包含预测标签和真实标签所有组合的矩阵称为混淆矩阵。我们可以将任何分类器对数据实例的预测与这些矩阵单元格中的一个关联起来,具体取决于实例的真实标签和分类器的预测标签。这也适用于多类分类,除了我在这里展示的二分类特殊情况。在多类情况下,有K个类,我们只需有一个K×K矩阵,而不是2×2矩阵。
Scikit-learn使得为你的分类器计算混淆矩阵变得很容易。让我们看看Notebook。这里我们从sklearn.metrics导入confusion_matrix类。我们将使用之前创建的具有二进制不平衡标签的数字数据集的相同训练集。
要获得混淆矩阵,我们只需传入测试集的真实标签y_test和预测标签集y_predicted,然后打印输出。这里小矩阵输出的单元格顺序与我刚刚在幻灯片上展示的相同。真阴性和假阴性在第一列,真阳性和假阳性在第二列。特别是,分类器的成功预测在对角线上,即真实类别与预测类别匹配的地方。对角线外的单元格代表不同类型的错误。
在这里,我们为问题的不同分类器选择计算混淆矩阵,这样我们可以看到它们如何随着模型的不同选择而略有变化。这让我们深入了解每种分类器观察到的成功和失败的性质。
所以首先,我们将应用我们之前看到的最频繁类虚拟分类器。我们在这里可以看到,代表分类器预测正类情况的右列全为零,这对于这个虚拟分类器来说是有道理的,因为它总是预测最频繁的类(负类)。我们看到有407个实例是真阴性,有43个错误是假阴性。


这里我们应用分层虚拟分类器,它根据训练集中标签的比例给出随机输出。现在,右列不再全为0,因为这个虚拟分类器偶尔会预测正类。如果我们把右列的数字加起来,我们看到32 + 6 = 38次分类器预测了正类。在这些次数中,有6次(右下角对角线)是真阳性。
在下一个案例中,我们将应用一个支持向量分类器,其线性核和C参数等于1。我们注意到,与上面的分层虚拟分类器相比,沿着对角线看,分层虚拟分类器总共有375 + 6 = 381个正确预测,而支持向量分类器在相同数据集上有402 + 38 = 440个正确预测。
同样,我们可以应用逻辑回归分类器,并获得与支持向量分类器相似的结果。最后,我们可以应用决策树分类器,并查看由此产生的混淆矩阵。我们注意到的一件事是,与具有平衡的假阴性和假阳性数量的支持向量机或逻辑回归分类器不同,决策树产生的假阴性错误(实际上是17个)是假阳性错误(有7个)的两倍多。
总结
本节课中我们一起学习了模型评估与选择的核心概念。我们了解到,仅使用准确率评估模型,尤其是在面对不平衡数据集时,可能会产生误导。我们引入了虚拟分类器作为性能基线,并深入探讨了混淆矩阵,它揭示了模型预测中真阳性、真阴性、假阳性和假阴性等不同结果。理解这些基本构件是掌握更高级评估指标(如下一节课将介绍的AUC、精确率、召回率和F1分数)的关键。通过选择合适的评估指标,我们能够更准确地衡量模型性能,并做出更好的建模决策。
76:混淆矩阵与基础评估指标 📊

概述
在本节课中,我们将学习如何超越简单的准确率,使用混淆矩阵来深入分析分类器的性能。我们将探讨如何从混淆矩阵中计算出多种评估指标,并理解它们在不同应用场景中的意义。
混淆矩阵回顾
上一节我们介绍了分类结果的基本概念,本节中我们来看看如何用具体数据填充混淆矩阵。
我们回到二元分类的可能结果矩阵。这次,我们用笔记本中决策树输出的实际计数来填充它。我们创建这个矩阵的原始动机是为了超越单一的准确率数字,从而更深入地了解给定分类器在不同类型预测上的成功与失败。现在我们有了这四个数字,可以手动检查和比较。
让我们可视化地查看这个分类结果,以帮助我们把这四个数字与分类器的性能联系起来。
我在这里绘制了数据实例,使用的是构成手写数字数据集中每个实例的64个特征值中的两个特定特征值。图中的黑点是真实类别为正的实例,即数字1。白点的真实类别为负,即除1以外的所有其他数字。黑线显示了一个假设的线性分类器的决策边界,决策边界左侧的任何实例都被预测为正类,决策边界右侧的所有实例都被预测为负类。
真正例是那些位于正预测区域内的黑点。假正例是那些位于正预测区域内的白点。同样,真负例是位于负预测区域内的白点。假负例是位于负预测区域内的黑点。
从混淆矩阵衍生的评估指标
我们已经见过一个可以从混淆矩阵计数中推导出的指标,即准确率。分类器的成功预测,即预测类别与真实类别匹配的情况,位于混淆矩阵的对角线上。因此,如果我们将对角线上的所有计数相加,就会得到所有类别的正确预测总数。将这个总和除以实例总数,就得到了准确率。
但让我们看看从这四个数字中还能计算出哪些其他评估指标。
一个有时会使用的、非常简单的相关数字是分类错误率。它是非对角线上的计数之和,即所有错误,除以总实例数。在数值上,这等价于 1 - 准确率。
公式:
分类错误率 = (假正例 + 假负例) / 总实例数 = 1 - 准确率
现在,让我们看一个更有趣的例子。假设回到我们的医学肿瘤检测分类器,我们想要一个评估指标,它不仅能给实现大量真正例的分类器更高分数,还能避免假负例,即很少漏检真正的癌性肿瘤。
召回率,也称为真正例率、灵敏度或检测概率,就是这样一个评估指标。它通过将真正例的数量除以真正例与假负例之和得到。
公式:
召回率 = 真正例 / (真正例 + 假负例)
从这个公式可以看出,有两种方法可以获得更大的召回率数值:第一,增加真正例的数量;第二,减少假负例的数量,因为这将使分母变小。在这个例子中,有26个真正例和17个假负例,得到召回率为0.6。
精确率与召回率的应用场景
学习精确率和召回率时,一个自然而然的问题是:何时应用它们?在决定使用什么指标时,一个关键问题是:在你的场景中,避免假正例和避免假负例哪个更重要?
一般来说,当我们的目标是最小化假正例时,使用精确率作为指标。当目标是最小化假负例时,使用召回率。
以下是一些具体场景的说明。
以下是可能说明避免过多假正例很重要的场景:
- 执法场景:执法部门使用视频识别算法在特定区域寻找可能的罪犯。在这种情况下,假正例意味着逮捕一个无辜的人,这可能被认为比假负例(即让潜在的罪犯逍遥法外)造成的损害大得多。因此,这是一个以精确率为导向的任务,应最小化假正例。
另一方面,以下是我们可能希望最小化假负例的场景:
- 法律调查场景:律师事务所作为诉讼的一部分,搜索提及某个事件或个人的所有可能电子邮件。如果我们认为分类问题的正类是找到包含关键信息的电子邮件,我们希望避免将电子邮件错误标记为不包含关键信息的分类错误,换句话说,我们希望避免假负例。这是因为即使漏掉一封关键电子邮件也可能遗漏有价值的证据。因此,像这样撒更大的网会找到更多的假正例,即实际上不相关的电子邮件,但在这个场景中是可以接受的,因为我们有专家可以在后期过滤它们。因此,这是一个以召回率为导向的任务,应最小化假负例。
精确率详解
精确率是一种反映上述“避免假正例”情况的评估指标。它通过将真正例的数量除以真正例与假正例之和得到。
公式:
精确率 = 真正例 / (真正例 + 假正例)
因此,要提高精确率,我们必须要么增加分类器预测的真正例数量,要么减少分类器错误预测负类实例为正类的错误数量(即假正例)。这里,分类器犯了7个假正例错误,因此精确率为0.79。
另一个有用的相关评估指标称为假正例率。它给出了分类器错误识别为正类的所有负类实例的比例。
公式:
假正例率 = 假正例 / (假正例 + 真负例) = 假正例 / 所有负类实例
这里我们有7个假正例,总共有407个负类实例,假正例率为0.02。通常称为特异度的统计量就是 1 - 假正例率。
可视化理解精确率与召回率的权衡
回到我们的分类器可视化图。让我们看看如何解释精确率和召回率。这里混淆矩阵中的数字来源于这个分类场景。
我们可以看到,精确率0.68意味着在决策边界左侧的正预测区域中,大约68%的点(19个实例中的13个)被正确标记为正。召回率0.87意味着在所有真实正类实例(即图中所有黑点)中,正预测区域“找到”了大约87%(15个中的13个)。
如果我们想要一个偏向更高精确率的分类器,就像在搜索引擎查询建议任务中那样,我们可能希望决策边界看起来像这样。现在,正预测区域中的所有点(7个中的7个)都是真正例,给我们完美的精确率1.0。但这需要付出代价,因为在总共15个正类实例中,有8个现在是假负例,换句话说,它们被错误地预测为负类。因此,召回率下降到 7 / 15 = 0.47。
另一方面,如果我们的分类任务像肿瘤检测例子,我们希望最小化假负例并获得高召回率,那么我们会希望分类器的决策边界看起来更像这样。现在,所有15个正类实例都被正确预测为正类,这意味着这些肿瘤都被检测到了。然而,这也带来了代价,因为假正例的数量(例如,检测器触发为可能肿瘤但实际上不是的情况)增加了。所以召回率是完美的1.0分,但精确率下降到 15 / 42 ≈ 0.36。
这些例子说明了机器学习应用中经常出现的经典权衡:你通常可以提高分类器的精确率,但缺点是可能会降低召回率;或者你可以提高分类器的召回率,但代价是降低精确率。
以召回率为导向的机器学习任务包括医疗和法律应用,在这些应用中,未能正确识别正例的后果可能很严重。在这些场景中,通常会部署人类专家来帮助过滤掉那些在高召回率应用中几乎不可避免地增加的假正例。
正如我刚才提到的,许多面向客户的机器学习任务通常以精确率为导向,因为在这里假正例的后果可能很严重,例如,通过提供不正确或无用的信息来损害客户在网站上的体验。例子包括搜索引擎排名和分类文档以用主题标签进行标注。
F1分数与F分数
在评估分类器时,计算一个称为F1分数的量通常很方便,它将精确率和召回率结合成一个数字。在数学上,它基于精确率和召回率的调和平均数,使用以下公式。
公式:
F1 = 2 * (精确率 * 召回率) / (精确率 + 召回率)
经过一些代数运算,我们可以用混淆矩阵中的量(真正例、假负例和假正例)来重写F1分数。
公式:
F1 = 2 * 真正例 / (2 * 真正例 + 假正例 + 假负例)
这个F1分数是一个更通用的评估指标F分数的特例,它引入了一个参数 beta。通过调整 beta,我们可以控制在评估中给予精确率与召回率的强调程度。例如,如果我们有以精确率为导向的用户,我们可能设 beta = 0.5,因为我们希望假正例比假负例更损害性能。对于以召回率为导向的情况,我们可能将 beta 设为一个大于1的数字,比如2,以强调假负例应该比假正例更损害性能。beta = 1 的设置对应于我们刚刚看到的F1分数特例,它平等地权衡精确率和召回率。
使用Python(Scikit-learn)计算评估指标
现在让我们看看如何使用Python和Scikit-learn计算这些评估指标。Scikit-learn的metrics模块提供了计算准确率、精确率、召回率和F1分数的函数,如笔记本中所示。这些函数的输入是相同的:第一个参数(这里是 y_test)是测试集数据实例的真实标签数组,第二个参数是测试集数据实例的预测标签数组。这里我们使用一个名为 tree_predicted 的变量,它是使用前一个笔记本步骤中决策树分类器的预测标签。
在分析分类器性能时,一次性计算所有这些指标通常很有用。因此,sklearn.metrics 提供了一个方便的 classification_report 函数。像之前的评分函数一样,classification_report 将真实标签和预测标签作为前两个必需参数。它还接受一些控制输出格式的可选参数。这里,我们使用 target_names 选项来标记输出表中的类别。你可以查看Scikit-learn文档以获取有关其他输出选项的更多信息。最后一列“support”显示测试集中具有该真实标签的实例数量。
这里我们展示了四个不同分类器在二元数字分类问题上的分类报告。第一组结果来自虚拟分类器,我们可以看到,正如预期的那样,正类的精确率和召回率都非常低,因为虚拟分类器只是随机猜测,预测正类实例为正类的概率很低。
总结

本节课中,我们一起学习了混淆矩阵及其在评估分类模型中的核心作用。我们深入探讨了准确率、精确率、召回率、F1分数等关键指标的计算方法和应用场景,并通过可视化理解了精确率与召回率之间的经典权衡关系。最后,我们掌握了如何使用Scikit-learn库在Python中方便地计算这些指标并生成分类报告。理解这些基础评估指标是有效分析和改进机器学习模型性能的重要一步。
77:分类器的决策函数与概率预测 📊

在本节课中,我们将学习分类器如何通过决策函数和预测概率来提供预测的不确定性信息,以及如何利用这些信息来全面评估分类器的性能。
分类器的不确定性输出
许多Scikit-learn中的分类器可以提供与特定预测相关的不确定性信息。这主要通过两种方法实现:decision_function方法和predict_proba方法。
决策函数(decision_function)
当给定一组测试点时,decision_function方法会为每个点提供一个分类器得分。这个得分表示分类器预测正类的置信度。
- 对于被预测为正类的点,得分通常是较大的正值。
- 对于被预测为负类的点,得分通常是较大的负值。
以下是一个使用逻辑回归分类器的示例代码,展示了分类问题中前几个实例的决策函数得分:
# 假设 clf 是一个已训练好的逻辑回归分类器
decision_scores = clf.decision_function(X_test)
print(decision_scores[:10])
我们可以观察到,属于负类的实例通常具有较大绝对值的负得分,而属于正类的实例则具有正得分。
预测概率(predict_proba)
predict_proba方法则提供样本属于各个类别的预测概率。通常,分类器会选择概率更高的类别。在二分类问题中,即选择概率大于50%的类别。
调整这个决策阈值会影响分类器的预测结果。提高阈值意味着分类器需要更有把握才预测某个类别。例如,我们可能只在估计类别1的概率超过70%时才预测它为类别1,这将得到一个更保守的分类器。
以下是获取同一逻辑回归分类器对测试实例预测概率的示例:
predicted_probabilities = clf.predict_proba(X_test)
print(predicted_probabilities[:10])
可以看到,许多真实标签为1(正类)的条目具有很高的概率(如0.995),而许多负类标签的实例预测概率则非常低。
请注意:并非所有模型都能提供这种有用的概率估计。例如,一个对训练集过拟合的模型可能会给出过于乐观的高概率,而这些概率实际上并不准确。
利用得分或概率进行全面评估
上一节我们介绍了分类器如何输出决策得分和预测概率。本节中,我们来看看如何利用这些信息来获得更全面的分类器性能评估图景。
对于一个特定的应用,我们可以根据需求(例如,希望分类器在误报或漏报方面更保守还是更激进)来选择一个特定的决策阈值。
然而,在开发新模型时,我们可能并不完全清楚哪个决策阈值是合适的,以及这个选择将如何影响精确率和召回率等评估指标。
因此,更好的做法是观察分类器在所有可能的决策阈值下的表现。下面的例子展示了这是如何工作的。
理解阈值如何影响预测
假设我们有一个测试实例列表,包含它们的真实标签和分类器得分(来自decision_function)。
实例 | 真实标签 | 分类器得分
-----|----------|-----------
A | 负 | -25
B | 正 | -15
C | 负 | -5
D | 正 | 5
... | ... | ...
如果我们设定一个决策阈值,例如 -20:
- 所有得分高于-20(即大于-20)的实例将被预测为正类。
- 所有得分低于或等于-20的实例将被预测为负类。
通过这种方式划分测试点后,我们可以为被预测为正类的点计算精确率和召回率。
计算不同阈值下的性能指标
以下是针对不同决策阈值计算精确率和召回率的步骤:
1. 阈值设为 -20
- 被预测为正类的实例总数:12个。
- 其中真正为正类(真阳性)的实例:4个。
- 精确率 = 4 / 12 ≈ 0.34
- 召回率 = 4 / 4 = 1.0 (因为所有4个正类实例都被找到了)
2. 阈值设为 -10
- 被预测为正类的实例总数:6个。
- 其中真正为正类(真阳性)的实例:4个。
- 精确率 = 4 / 6 ≈ 0.67
- 召回率 = 4 / 4 = 1.0
3. 阈值设为 0
- 被预测为正类的实例总数:4个。
- 其中真正为正类(真阳性)的实例:3个。
- 精确率 = 3 / 4 = 0.75
- 召回率 = 3 / 4 = 0.75
绘制精确率-召回率曲线
当我们为许多不同的阈值计算出一系列(精确率, 召回率)数据对后,就可以将它们绘制在图表上。
- 阈值 -20 对应点 (0.34, 1.0)
- 阈值 -10 对应点 (0.67, 1.0)
- 阈值 0 对应点 (0.75, 0.75)
将这些点连接起来,就形成了一条曲线。通过这种方式,我们可以通过变化阈值,更完整地了解分类器输出的精确率和召回率如何随决策阈值的变化而变化。
这条最终的曲线被称为 精确率-召回率曲线。我们将在下一节课中对其进行更详细的研究。
总结

本节课中,我们一起学习了:
- 分类器通过
decision_function和predict_proba方法提供预测的不确定性信息。 - 决策阈值的选择直接影响分类器的预测结果和保守程度。
- 通过计算分类器在所有可能阈值下的精确率和召回率,我们可以绘制出精确率-召回率曲线,从而获得对分类器性能更全面、更深入的理解,而不仅仅依赖于单一阈值下的指标。
78:精确率-召回率与ROC曲线 📊

在本节课中,我们将要学习两种广泛使用的机器学习模型评估方法:精确率-召回率曲线与ROC曲线。我们将了解它们如何可视化分类器的性能,以及如何解读这些曲线。
精确率-召回率曲线
上一节我们介绍了精确率和召回率的概念,本节中我们来看看如何将它们可视化。
精确率-召回率曲线是一种非常广泛使用的机器学习评估方法。正如我们在示例中所见,X轴表示精确率,Y轴表示召回率。一个理想的分类器能够达到1.0的完美精确率和1.0的完美召回率。因此,最佳点位于图表的右上角。通常,精确率-召回率曲线越接近右上角,其性能越优,在精确率和召回率之间提供的权衡也越有利。我们已经看到许多分类器在这两个指标之间存在权衡关系的例子。
以下是生成精确率-召回率曲线的核心步骤:
- 获取分类器的决策函数输出。
- 应用不同的决策阈值。
- 计算每个阈值下的精确率和召回率。
- 将结果绘制成曲线。
下图是一个使用以下笔记本代码生成的实际精确率-召回率曲线示例。红色圆圈表示当决策阈值为0时达到的精确率和召回率。

幸运的是,Scikit-learn内置了一个函数,可以计算精确率-召回率曲线,这正是我们在笔记本中使用的。在这个特定应用中,存在一个总体下降趋势:随着分类器精确率的上升,召回率趋于下降。在这个案例中,曲线并不完全平滑,存在一些锯齿状区域,并且随着我们接近最大精确率,跳跃幅度会变得更大。
这是由精确率和召回率的计算公式决定的。它们使用了包含真正例数量的离散计数。随着决策阈值的提高,被预测为正例的点越来越少。为这些较小数字计算的分数会随着决策阈值的微小变化而发生显著变化。这就是为什么精确率-召回率曲线的尾部边缘在绘制时可能显得有些参差不齐。
ROC曲线
了解了精确率-召回率曲线后,我们来看看另一种重要的可视化工具。
ROC曲线,即接收者操作特征曲线,是一种广泛使用的可视化方法,用于说明二分类器的性能。ROC曲线的X轴显示分类器的假正率,范围从0到1.0。Y轴显示分类器的真正率,范围也从0到1.0。值得注意的是,ROC曲线图的Y轴就是召回率指标。
ROC空间中的理想点是分类器达到假正率为0且真正率为1的位置,即左上角。ROC空间中的曲线代表了随着分类器决策阈值变化而产生的不同权衡,就像在精确率-召回率曲线中一样。当我们改变决策阈值时,会得到不同数量的假正例和真正例,从而可以在图表上绘制。
下图中的虚线表示一个仅随机猜测二分类标签的分类器所产生的ROC曲线,这基本上就像抛硬币。因此,这条虚线被用作基线。一个糟糕的分类器其性能将是随机的,甚至可能比随机更差。一个相当好的分类器会给出一个始终优于随机猜测的ROC曲线。一个优秀的分类器则会像这里展示的那样,曲线大幅向左上角凸起。这个特定示例是使用您见过的笔记本示例中的逻辑回归分类器生成的。
曲线的形状也很重要。我们希望分类器能最大化真正率,同时最小化假正率。接下来我们将看到,我们可以通过观察曲线下的面积来量化分类器的“优良”程度。

曲线下面积
上一节我们看到了ROC曲线的形状,本节中我们用一个单一指标来总结它。
我们使用一种称为曲线下面积的指标。这是一个单一数字,用于衡量ROC曲线下的总面积,以此总结分类器的性能。随机分类器下方的面积将是0.5。随着分类器曲线向左上角凸起,曲线下的面积会变得越来越大,并趋近于1。因此,AUC为0代表一个非常糟糕的分类器,AUC为1则代表一个最优的分类器。
像任何其他评估指标一样,AUC也有优点和缺点。

以下是AUC的主要优点:
- AUC提供一个单一数字,便于比较不同分类器。
- AUC不需要指定特定的决策阈值。你可以将AUC视为分类器在所有可能决策阈值下性能的总结。
另一方面,像任何其他单一数字指标一样,AUC会丢失关于分类器权衡和ROC曲线形状的信息。可能两个分类器具有相似的AUC,但它们的ROC曲线形状不同,在不同区域有交叉。例如,在某个特定的真正率范围内实现非常低的假正率可能很重要,但仅使用AUC可能无法检测到这些更细微的性能差异。
总结
本节课中我们一起学习了两种关键的模型评估可视化工具。我们探讨了精确率-召回率曲线,它展示了精确率和召回率之间的权衡,最佳点位于右上角。接着,我们学习了ROC曲线,它以假正率和真正率为轴,理想点位于左上角,并引入了曲线下面积作为量化ROC曲线下整体性能的单一指标。理解这些曲线及其含义,对于选择适合特定应用场景的模型和决策阈值至关重要。
79:多类别评估方法 📊

在本节课中,我们将要学习如何评估多类别分类器。我们将探讨如何将二元分类的评估方法扩展到多个类别,理解混淆矩阵在多类别场景下的应用,并学习两种重要的平均计算方法:宏平均与微平均。
上一节我们介绍了二元分类器的评估方法。本节中,我们来看看更普遍的多类别分类是如何进行评估的。
在许多方面,多类别评估是二元评估方法的直接扩展。区别在于,我们处理的不再是两个类别,而是多个类别。因此,多类别评估的结果相当于每个类别的真实结果与预测结果的二元对比集合。
正如我们在二元案例中看到的,你可以在多类别案例中生成混淆矩阵。当存在多个类别时,混淆矩阵尤其有用,因为一个真实类别被预测为另一个类别会导致多种不同类型的错误。我们稍后会看一个例子。
我们在二元案例中看到的分类报告,也很容易为多类别案例生成。
现在,值得进一步研究的一个领域是跨类别的平均计算是如何进行的。我们将很快介绍平均多类别结果的不同方法。此外,支持度(即每个类别的实例数量)也是需要考虑的重要因素。正如我们在二元案例中关心如何处理类别不平衡一样,在多类别中考虑各类别支持度可能存在的巨大或微小差异也同样重要。
还存在一种多标签分类的情况,其中每个实例可以有多个标签(例如,一个网页可能被标记为来自预定义集合的不同主题或兴趣领域)。本讲座不涵盖多标签分类,我们将专门聚焦于多类别评估。
多类别混淆矩阵 🔍
多类别混淆矩阵是二元分类器2x2混淆矩阵的直接扩展。
例如,在我们的手写数字数据集中,数字有10个类别(0到9)。因此,10类混淆矩阵是一个10x10的矩阵,其中行索引代表真实数字类别,列索引代表预测数字类别。
与2x2情况一样,分类器的正确预测(真实类别与预测类别匹配)都位于对角线上,而错误分类则位于对角线之外。
以下是基于线性核支持向量分类器的代码生成的混淆矩阵示例:
# 示例代码:生成并显示多类别混淆矩阵
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt
# 假设 y_true 和 y_pred 是真实标签和预测标签
cm = confusion_matrix(y_true, y_pred)
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=class_names)
disp.plot(cmap=plt.cm.Blues)
plt.show()
在这个例子中,我们可以看到大多数预测都是正确的,只有零星几个错误分类。这里最常见的错误类型是将真实的数字8错误预测为数字1,这种情况发生了三次。总体准确率很高,约为97%。
有时,将混淆矩阵显示为热力图有助于突出显示不同类型错误的相对频率。为此,我在这里包含了生成热力图的代码。
为了对比,我在同一数据集上包含了第二个混淆矩阵,它来自另一个表现差得多的支持向量分类器(仅将核函数从线性核改为RBF径向基函数核)。
从混淆矩阵下方约49%的准确率数字可以看出,这个分类器的表现比线性核差得多。但仅凭这个数字无法深入了解原因。然而,观察混淆矩阵可以发现,对于每个真实的数字类别,都有相当一部分结果被预测为数字4。这相当令人惊讶。例如,在第二行(真实数字2)的44个实例中,17个被正确分类,但27个被分类为数字4。
显然,这个模型存在问题。我选择第二个例子只是为了展示当事情出错时可能看到的极端情况。这个手写数字数据集是成熟且没有问题的,但特别是在使用新数据集进行开发时,在混淆矩阵中看到这样的模式可以为你提供关于可能问题(例如特征预处理)的宝贵线索。
因此,作为模型评估的一部分,一个通用的经验法则是:始终查看分类器的混淆矩阵,以深入了解它对每个类别犯了哪些类型的错误,包括某些类别是否比其他类别更容易出现特定类型的错误。
分类报告与平均方法 📈
接下来,就像在二元案例中一样,你可以获得一份分类报告,该报告总结了多类别分类器的多个评估指标,并为每个类别计算了一个平均指标。
我即将描述的内容也适用于二元分类情况,但在观察具有多个类别的多类别分类问题时更容易理解。
让我们使用水果数据的特定示例来看看微平均与宏平均的区别。
以下是计算宏平均与微平均的示例数据:
| 真实类别 | 预测类别 |
|---|---|
| 橙子 | 橙子 |
| 橙子 | 柠檬 |
| 橙子 | 苹果 |
| 橙子 | 柠檬 |
| 橙子 | 苹果 |
| 柠檬 | 柠檬 |
| 柠檬 | 苹果 |
| 苹果 | 苹果 |
| 苹果 | 苹果 |
宏平均
使用宏平均,我们在整体计算中将每个类别视为具有相同的权重。
以下是计算宏平均精度的步骤:
- 我们首先为每个类别单独计算指标(本例中是精度)。
- 为每个类别计算完成后,我们对所有类别的结果进行平均,得到最终的宏平均精度数值。
具体到本例:
- 橙子类:分类器预测为“橙子”的案例中,有1次正确(共5次预测),精度 = 1/5 = 0.2。
- 柠檬类:分类器预测为“柠檬”的案例中,有1次正确(共2次预测),精度 = 1/2 = 0.5。
- 苹果类:分类器预测为“苹果”的案例中,有2次正确(共2次预测),精度 = 2/2 = 1.0。
然后,宏平均精度 = (0.2 + 0.5 + 1.0) / 3 ≈ 0.57。
微平均
另一方面,使用微平均,我们赋予数据集中每个实例相等的权重。这意味着实例最多的类别(最大的类别)对最终的微平均数影响最大。
以下是计算微平均精度的步骤:
我们不再需要单独考虑每个类别,而是可以简单地聚合所有类别的所有结果,并在聚合结果上计算精度。
具体到本例:
我们总共有9个示例。我们查看有多少次预测类别与真实类别匹配。这里有4次正确预测。因此,微平均精度 = 4 / 9 ≈ 0.44。
如何选择与解读
- 如果各类别的实例数量大致相同,宏平均和微平均将大致相等。
- 如果某些类别比其他类别大得多(拥有更多实例),并且你希望你的指标向最大的类别倾斜,请使用微平均。
- 如果你希望你的指标向最小的类别倾斜,请使用宏平均。
- 如果微平均远低于宏平均,则应检查较大的类别是否存在较差的指标性能。
- 如果宏平均远低于微平均,则应检查较小的类别,看看它们为何指标性能较差。
在Scikit-learn中,你可以使用评分函数上的average参数来指定平均方法:
from sklearn.metrics import precision_score, f1_score
# 计算微平均精度和宏平均精度
micro_precision = precision_score(y_true, y_pred, average='micro')
macro_precision = precision_score(y_true, y_pred, average='macro')
# 计算微平均F1和宏平均F1
micro_f1 = f1_score(y_true, y_pred, average='micro')
macro_f1 = f1_score(y_true, y_pred, average='macro')

现在我们已经了解了如何计算这些指标,接下来让我们看看如何使用它们来进行模型选择。

本节课中我们一起学习了多类别分类器的评估方法。我们了解到多类别混淆矩阵是分析错误模式的重要工具,它能揭示分类器在各类别上的具体表现。同时,我们深入探讨了宏平均与微平均这两种关键的平均计算方法:宏平均平等对待每个类别,而微平均则平等对待每个实例,因此受大类别影响更大。理解这些概念并正确选择平均方法,对于在多类别场景下准确评估和比较模型性能至关重要。
80:回归模型评估 📊

在本节课中,我们将要学习如何评估回归模型。我们将探讨回归评估与分类评估的区别,介绍几种关键的回归评估指标,并学习如何使用虚拟回归器建立性能基准。
我们之前看到,对于分类问题,在某些场景下(例如医疗诊断预测或面向客户的网站功能),假阳性和假阴性带来的后果截然不同。因此,区分这些错误类型并进行更详细的分析是有意义的。
例如,在评估分类器时,我们查看了精确率-召回率曲线等图表,这些图表可以展示分类器在两种错误类型之间所能达到的权衡。
理论上,我们可以将应用于分类的同类错误分析和更详细的评估方法应用于回归问题。
例如,我们可以分析回归模型的预测,并将错误分为两类:一类是回归模型的预测值远大于目标值,另一类是预测值远小于目标值。
然而,在实践中,对于大多数回归应用而言,区分这些不同类型的错误并不那么重要。
这大大简化了回归的评估。在大多数情况下,默认的R平方分数(在Scikit-learn中可用)足以满足大多数任务的需求,它总结了模型对未来实例的预测能力。
作为提醒,完美预测器的R平方分数是1.0。对于总是输出相同常数值的预测器,R平方分数为0。
尽管R平方分数的名称中有“平方”二字,暗示它总是正数,但对于拟合不佳的模型(例如将非线性函数拟合到数据时),它确实有可能变为负数。
除了R平方分数,您还应该了解一些其他回归评估指标,它们的工作原理略有不同。
以下是几种常见的回归评估指标:
- 平均绝对误差:计算目标值与预测值之间绝对差异的平均值。在机器学习术语中,这对应于L1范数损失的期望值。例如,在时间序列分析中,有时会用它来评估回归的预测结果。
- 公式:
MAE = mean(|y_true - y_pred|)
- 公式:
- 均方误差:计算目标值与预测值之间平方差异的平均值。这对应于L2范数损失的期望值。它广泛用于任何回归问题,较大的误差会以平方形式对平均误差做出更大的贡献。与平均绝对误差一样,均方误差不区分高估和低估。
- 公式:
MSE = mean((y_true - y_pred)^2)
- 公式:
- 中位数绝对误差:一种常见情况是数据中存在异常值,这可能会对整体的R平方或平均分数值产生不良影响。因此,在那些忽略异常值很重要的情况下,您可以使用中位数绝对误差分数。它对异常值的存在具有鲁棒性,因为它使用误差分布的中位数而不是平均值。
- 公式:
MedAE = median(|y_true - y_pred|)
- 公式:
上一节我们介绍了回归评估的核心指标,本节中我们来看看如何为回归模型建立一个简单的性能基准。
我们曾看到,使用虚拟分类器可以为我们提供一个简单但有用的基线,以便在评估分类器时进行比较。回归也存在相同的功能。有一个虚拟回归器类,它使用不查看输入数据的简单策略来提供预测。
本讲笔记本中的回归示例展示了一个基于单个输入变量(沿X轴绘制)的数据散点图,数据来自糖尿病数据集。这些点是测试集分割中的数据实例,形成了一个看起来可能略微向右下方倾斜的点云。
绿色的线,标记为“拟合模型”,是拟合到训练点的默认线性回归。我们可以看到,它对测试数据的拟合效果并不是特别强。标记为“虚拟均值”的红线显示了一个线性模型,它使用的策略是始终预测训练数据的平均值。这就是一个虚拟回归器的例子。
您可以查看笔记本以了解虚拟回归器的创建和使用方式与常规回归模型相同:创建、用训练数据拟合,然后在测试数据上调用predict方法。不过,再次强调,就像虚拟分类器一样,您不应该将虚拟回归器用于实际问题,它的唯一用途是提供一个比较基准。
查看线性模型与虚拟模型的回归指标输出,我们可以看到,正如预期的那样,虚拟回归器实现了R平方分数为0,因为它总是在不看输出的情况下做出恒定预测。在这个例子中,根据均方误差和R平方分数,线性模型提供的拟合效果仅比虚拟回归器略好一些。

除了始终预测训练目标值平均值的策略外,您还可以创建其他几种类型的虚拟回归器,例如始终预测训练目标值的中位数、这些值的特定分位数,或者您提供的特定自定义常数值。
虽然回归的评估需求通常比分类更简单,但仔细检查以确保您为回归问题选择的评估指标能以反映业务、组织或用户需求后果的方式惩罚错误,是非常值得的。
本节课中我们一起学习了回归模型的评估方法。我们了解到,与分类不同,回归评估通常不需要严格区分高估和低估的错误。我们介绍了R平方分数、平均绝对误差、均方误差和中位数绝对误差等核心评估指标,并理解了它们各自的适用场景。最后,我们学习了如何使用虚拟回归器为模型性能建立一个有意义的基准,以便更好地判断我们模型的真实有效性。
81:模型选择与分类器评估指标优化 🎯

在本节课中,我们将学习如何应用不同的评估指标作为选择最佳分类器的标准,即模型选择。我们将回顾已有的评估框架,并重点介绍如何在交叉验证和网格搜索中使用这些指标来优化模型性能。
回顾评估框架
上一节我们介绍了多种用于二分类和多分类的评估指标。本节中,我们来看看如何将它们作为模型选择的标准。
在之前的课程中,我们已经看到了几种不同的模型选择评估框架。
首先,我们简单地在同一数据集上进行训练和测试。但众所周知,这通常会导致严重的过拟合,并且对新数据的泛化能力较差。不过,这可以作为一个有用的完整性检查,以确保你的软件工程和特征生成流程正常工作。
其次,我们经常使用单一的训练-测试分割来产生单一的评估指标。这种方法虽然快速简便,但无法真实地估计模型在未来新数据上的表现,也无法让我们了解在不同测试集上进行预测时评估指标可能产生的方差。
第三,我们使用了K折交叉验证来创建K个随机的训练-测试分割,并将评估指标在多个分割上进行平均。这能产生在未见数据上更可靠的模型。特别是,我们还可以在每次交叉验证折叠中使用网格搜索(例如 GridSearchCV 方法),来寻找模型相对于评估指标的最优参数。
cross_val_score 或 GridSearchCV 默认使用的评估指标是准确率。
在模型选择中应用新指标
那么,如何应用你在这里学到的新指标,如AUC呢?在模型选择中,Scikit-Learn 使这变得非常简单。你只需添加一个 scoring 参数,将其设置为代表你想要使用的评估指标名称的字符串。
让我们先看一个在交叉验证中使用 scoring 参数的例子,然后我们再看模型选择的另一种主要方法——网格搜索。
在下面的代码中,我们有一个交叉验证的例子,我们使用线性核且C参数设为1的支持向量分类器运行5折交叉验证。
# 使用默认准确率作为评估指标
scores_default = cross_val_score(svc, X, y, cv=5)
# 使用AUC作为评估指标
scores_auc = cross_val_score(svc, X, y, cv=5, scoring='roc_auc')
# 使用召回率作为评估指标
scores_recall = cross_val_score(svc, X, y, cv=5, scoring='recall')
第一个 cross_val_score 调用仅使用默认的准确率作为评估指标。第二个调用使用 scoring 参数,设置为字符串 'roc_auc',这将使用AUC作为评估指标。第三个调用将 scoring 参数设置为 'recall' 以使用召回率作为评估指标。你可以看到每个指标产生的五个评估值列表(每个折叠一个)。
这里我们没有进行任何参数调优,我们只是在多个折叠上简单地评估模型的平均性能。
网格搜索中的指标优化
现在,在这个网格搜索的例子中,我们使用一个使用径向基函数核的支持向量分类器,这里的关键参数是 gamma 参数,它直观地设置了核的影响半径或宽度。
我们使用 GridSearchCV 来寻找在两种情况下优化给定评估指标的 gamma 值。
# 优化平均准确率
param_grid = {'gamma': [0.001, 0.01, 0.1, 1, 10]}
grid_acc = GridSearchCV(svc_rbf, param_grid, cv=5, scoring='accuracy')
grid_acc.fit(X_train, y_train)
# 优化AUC
grid_auc = GridSearchCV(svc_rbf, param_grid, cv=5, scoring='roc_auc')
grid_auc.fit(X_train, y_train)
在第一种情况下,我们只优化平均准确率。在第二种情况下,我们优化AUC。在这个特定的例子中,gamma 的最优值恰好相同,都是0.001。但正如我们稍后将看到的,在其他情况下,根据用于优化的评估指标,最优参数值可能完全不同。
你可以通过运行以下代码来查看 scoring 参数支持的评估指标的完整名称列表,该代码使用了从 sklearn.metrics 导入的 SCORERS 变量。
from sklearn.metrics import get_scorer_names
print(get_scorer_names())
你可以看到分类指标,如代表微平均精确率的字符串 'precision_micro',以及回归指标,如用于R平方回归损失的 'r2' 指标。
决策边界如何随优化指标变化
让我们看一个具体的例子,它展示了当为不同的评估指标进行优化时,分类器的决策边界如何变化。
这个分类问题基于我们在整个笔记本中一直用作示例的相同二进制数字分类器训练集和测试集。
在这些分类可视化示例中,正例(数字1)显示为黑点。正类预测区域显示为决策边界右侧的浅色或黄色区域。负例(所有其他数字)显示为白点,在这些图中,负类预测区域位于决策边界的左侧。数据点使用数字数据集64个特征值中的两个进行绘制,并添加了一点抖动(即我添加了一点随机噪声),以便我们更容易地看到特征空间中示例的密度。
以下是生成此图的Python代码。我们在这里应用网格搜索来探索可选的 class_weight 参数的不同值,该参数控制训练期间给予两个类别各自的权重。事实证明,针对不同的评估指标进行优化会导致 class_weight 参数的不同最优值。
随着 class_weight 参数的增加,将更加强调正确分类正类实例。
我们在这里看到的以精确率为导向的分类器(class_weight 为2)努力减少假阳性,同时增加真阳性。因此,它专注于右下角的正类点簇,那里相对较少的负类点。在这里,精确率超过50%。
相比之下,以召回率为导向的分类器(class_weight 为50)努力减少假阴性的数量,同时增加真阳性。也就是说,它试图找到大多数正类点作为其正类预测的一部分。
我们还可以看到,以F1为导向的分类器的决策边界具有最优的 class_weight 值2,介于以精确率和召回率为导向的分类器的最优 class_weight 值之间。从视觉上看,我们可以看到以F1为导向的分类器在决策边界上也处于以精确率和召回率为导向的决策边界之间的中间位置。这是有道理的,因为F1是精确率和召回率的调和平均数。
以AUC为导向的分类器(最优 class_weight 为5)具有与以F1为导向的分类器相似的决策边界,但略微偏向更高的召回率。
对于这个分类场景,我们可以非常清楚地看到精确率-召回率的权衡,以及在同一数据集上针对准确率进行优化并使用 class_weight 参数的 balanced 选项的默认线性核支持向量分类器的精确率-召回率曲线。
让我们看一下生成此图的代码。
花点时间想象一下,这条精确率-召回率曲线上极右下方的部分代表了一个高度以精确率为导向的决策边界,位于分类图的右下角,那里有一簇正例。随着决策阈值被调整得越来越不保守,沿着曲线向上和向左移动,分类器变得越来越像以召回率为导向的支持向量分类器示例。
同样,红色圆圈代表在零分数标记处实现的精确率-召回率权衡,这是为训练好的分类器选择的实际决策边界。
训练-验证-测试:避免数据泄漏
为了简单起见,我们经常在展示评估评分的例子中使用单一的训练-测试分割。然而,仅使用交叉验证或测试集进行模型选择或参数调优,仍可能导致更微妙形式的过拟合,从而对未来未见数据产生过于乐观的评估估计。
对此的一个直观解释可能如下:请记住,在测试集上进行评估的全部目的是估计学习算法在未来未见数据上的表现。我们在选择模型时,作为重复交叉验证过程的一部分,看到的关于数据集的信息越多,任何潜在的保留测试数据对选择最终模型(而不仅仅是评估它)的影响就越大。这有时被称为数据泄漏,我们将在另一个模块中详细描述这种现象。
因此,除非我们承诺保留一个测试分割,直到评估的最后阶段才让任何过程看到,否则我们就没有用真正保留的测试集进行评估。这就是实践中实际所做的。
有三个数据分割:
- 训练集:用于模型构建。
- 验证集:用于模型选择。
- 测试集:用于最终评估。
通常首先划分出训练集和测试集。然后使用训练数据运行交叉验证来进行模型和参数选择。同样,测试集直到评估过程的最后阶段才被看到。
机器学习研究人员非常认真地对待这个协议。训练-验证-测试设计是一个非常重要、普遍应用的框架,用于有效评估机器学习模型。
总结与延伸思考
这把我们带到了机器学习评估课程部分的结尾。
你现在应该理解为什么准确率只能提供分类器性能的部分图景,并且更加熟悉重要替代评估方法和指标(如混淆矩阵、精确率、召回率、F1分数和ROC曲线下面积)的动机和定义。

你也看到了如何应用和选择这些不同的评估指标替代方案,以优化分类器的模型选择或参数调优,从而最大化给定的评估指标。
最后,我想留给你几点思考。
首先,简单的准确率可能并不总是你特定机器学习应用程序的正确目标。正如我们在肿瘤检测或信用卡欺诈中看到的那样,假阳性和假阴性可能对用户或组织结果产生非常不同的现实影响。因此,选择一个能反映这些用户、应用或业务需求的评估指标非常重要。
其次,还有一些其他维度可能对评估你的机器学习算法很重要,我们在这里没有涉及,但你需要意识到。
我在这里特别提两个:
- 学习曲线:用于评估机器学习算法的评估指标如何随着算法获得更多训练数据而变化或改进。学习曲线可以作为成本效益分析的一部分。以标记示例的形式获取训练数据通常耗时且昂贵。因此,能够估计如果你投资将训练数据量翻倍,你的分类器可能获得的性能改进,是一个有用的分析。
- 敏感性分析:相当于观察当对重要模型参数进行微小调整时,评估指标如何变化。这有助于评估模型对参数选择的稳健性。执行这一点可能很重要,特别是如果存在其他成本,例如运行时效率,这些是部署与不同参数值(例如,决策树深度或特征值阈值)相关的操作系统时的关键变量。通过这种方式,对不同性能维度之间可实现的权衡有一个更全面的了解,可以帮助你为机器学习模型做出最佳的实际部署决策。
本节课中,我们一起学习了如何利用不同的评估指标进行模型选择,掌握了在交叉验证和网格搜索中应用这些指标的方法,并理解了避免数据泄漏的“训练-验证-测试”框架的重要性。记住,没有“放之四海而皆准”的最佳指标,选择哪一个取决于你的具体应用场景和业务目标。
82:模型校准(选修)📊

概述
在本节课中,我们将要学习模型校准的概念、重要性以及如何在实践中实现它。模型校准是确保机器学习模型输出的概率预测与现实世界观察到的概率相匹配的过程。这对于依赖概率进行决策或计算的场景至关重要。
什么是模型校准?🤔
上一节我们介绍了模型校准的重要性,本节中我们来看看模型校准的具体定义。
一个经过校准的模型,其输出的预测概率与现实世界中观察到的实际概率非常接近。我们可以用一个熟悉的例子来理解这个概念:天气预报。
假设气象学家在所有预测100%会下雨的日子里,实际上只有50%的时间真的下雨了。那么,气象学家预测的100%概率与现实观察到的50%比例并不匹配。因此,我们说气象学家的预测概率是未经校准的。
如果气象学家的预测是经过校准的,那么在所有他预测有R%概率会下雨的日子里,实际下雨的比例也应该是R%。
我们可以将同样的概念应用于机器学习模型。对于一个二元分类问题,其中实例 Xi 的标签 Yi 可以是0或1。我们说一个预测器 F(Xi) 是经过校准的,如果对于所有预测器以R%的概率预测属于类别1的数据点,这些数据点实际属于类别1的比例也恰好是R%。
用数学公式表示,对于一个校准良好的模型,我们希望满足以下等价关系:
对于所有预测器 F 预测概率为 p 的数据点 Xi,其真实标签 Yi 的期望值(即经验概率)应等于 p。
公式:
E[Yi | F(Xi) = p] = p
其中,E 表示期望值,可以理解为在预测概率为 p 的所有数据点上,真实标签 Yi 的平均值。
为什么模型校准很重要?⚖️
上一节我们定义了模型校准,本节中我们来看看为什么确保预测概率准确如此重要。
在许多依赖准确概率预测的场景中,模型校准至关重要。
以下是几个关键原因:
- 决策支持:如果分类器被用于重要的决策过程(例如司法系统中的再犯风险评估),那么其输出的概率或置信度估计必须准确反映现实。
- 计算期望值:在机器学习中,我们经常需要计算重要量的期望值或平均值(例如,搜索结果的预期点击次数、广告的预期点击次数)。要确保这些期望值的计算有效,就需要输入计算的概率是准确的。
- 模块化概率系统:现代大型系统通常是模块化和概率化的。系统中的各个模型通过概率“语言”进行通信。如果一个模块输出的概率不准确,会影响下游所有依赖该概率的模块。保持每个模块的概率准确性,可以避免重新校准整个系统。
当然,在某些场景下,校准可能不那么重要。例如,如果你的目标只是对一组事物进行排序(如搜索结果排名),那么排序的准确性是关键,而每个文档的具体相关概率可能不需要精确计算。然而,鉴于统计方法在许多系统中的普遍应用,理解并确保使用概率模块的部分输出经过良好校准的概率,是非常重要的。
如何诊断模型校准情况?📈
在审视一个预测模型的校准情况时,第一步通常是进行可视化。我们使用一种称为校准曲线或可靠性曲线的工具。
校准曲线展示了预测概率与经验概率之间的关系。在X轴上,我们放置分类器预测的概率(例如,将所有预测概率为0.4的实例放入一个“分箱”中)。在Y轴上,我们计算并展示对应的经验概率(即,在预测概率为0.4的所有实例中,实际属于正类的比例)。
代码:在Scikit-learn中,可以使用 calibration_curve 函数来计算校准曲线数据。
一个完美校准的分类器,其校准曲线应该是一条直线 y = x。这意味着预测概率完全等于经验概率。
校准曲线偏离 y = x 线的程度,反映了模型校准的优劣。偏离越大,校准越差;越接近这条线,校准越好。
例如,支持向量机(SVM)的校准曲线(图中绿色最上方的线)可能显示,当它预测概率为0.6时,实际观察到的经验概率接近0.8,这表明它低估了真实概率。而逻辑回归(图中蓝色线)的校准曲线非常接近 y = x 线,说明它本身已经校准得很好。
如何校准模型?🛠️
上一节我们学会了如何诊断校准问题,本节中我们来看看如何修复校准不佳的概率。
一个广泛使用的方法是将原始预测模型视为一个“黑盒”。我们训练一个校准函数 G,来学习纠正或改进现有预测器的原始输出分数,将其转换为校准后的预测概率。
我们有一个原始预测函数 F(x),它产生原始输出(可能是概率,也可能是分数)。我们假设这个原始输出是未经校准的。我们不修改原始模型,而是取其输出,学习一个转换函数 G,将原始预测输出转换为校准后的预测概率。
我们使用一个校准集来学习这个转换函数 G。这个数据集必须与用于训练原始模型的数据集不同,以防止过拟合。
一个好的校准函数 G 应该是严格单调递增的,以保持原始预测的排序关系。
以下是两种广泛使用的学习转换函数 G 的方法:
1. Platt缩放(Sigmoid校准)
这种方法假设原始预测分数与经验概率之间的关系符合S型(Sigmoid)曲线。它通过拟合一个Sigmoid函数来学习这种关系。
公式:
P_calibrated = 1 / (1 + exp(A * s_raw + B))
其中 s_raw 是原始预测分数,A 和 B 是通过拟合学习到的参数。
Platt缩放对于支持向量机(SVM)或提升树等最大间隔方法特别有效,因为这些方法本身的预测概率往往具有S型的失真。
2. 等渗回归
这是一种更通用的校准方法。它不假设特定的函数形式(如Sigmoid),而是使用等渗回归来获得函数 G。等渗回归拟合一个单调递增的分段常数函数(阶梯状函数)来捕捉原始分数与经验概率之间的关系。
与Platt缩放相比,等渗回归可以拟合更复杂的形状,功能更强大。但它也更容易过拟合,因此建议在拥有较大数据集(例如超过1000个数据点)时使用。
在Scikit-learn中实现模型校准 🐍
Scikit-learn提供了便捷的工具来实现模型校准。
主要的类是 CalibratedClassifierCV。你需要传入想要校准的基础分类器,设置校准方法(method 参数,可选 'sigmoid' 或 'isotonic')以及用于调优的交叉验证折数(cv 参数)。
CalibratedClassifierCV 支持我们刚才描述的两种方法。Scikit-learn也提供了 calibration_curve 函数来计算校准曲线数据,以及 CalibrationDisplay 函数来绘制校准曲线。
以下是两种常见的使用场景:
场景一:校准一个已训练好的分类器
如果你已经训练好了一个分类器(例如逻辑回归或SVM),只想校正其输出分数或概率,可以按以下步骤操作:
- 确保你有一个独立的校准集,该数据集未用于训练原始模型。
- 创建
CalibratedClassifierCV对象,将已训练好的分类器作为base_estimator传入,并设置cv='prefit'。 - 在
CalibratedClassifierCV对象上调用.fit()方法,并传入独立的校准集数据。
代码示例:
from sklearn.calibration import CalibratedClassifierCV
from sklearn.model_selection import train_test_split
from sklearn.naive_bayes import GaussianNB
# 假设 X, y 是原始数据
X_train, X_calib, y_train, y_calib = train_test_split(X, y, test_size=0.2, random_state=42)
# 1. 在训练集上训练原始模型
base_clf = GaussianNB()
base_clf.fit(X_train, y_train)
# 2. 使用校准集来校准模型
calibrated_clf = CalibratedClassifierCV(base_estimator=base_clf, cv='prefit', method='sigmoid')
calibrated_clf.fit(X_calib, y_calib)
# 现在 calibrated_clf 可以输出校准后的概率
calibrated_probs = calibrated_clf.predict_proba(X_new)
场景二:同时进行训练和校准(使用交叉验证)
如果你还没有训练分类器,希望同时进行模型训练和校准,可以按以下步骤操作:
- 创建
CalibratedClassifierCV对象,传入未训练的基础分类器。 - 设置
cv参数为你想要的交叉验证策略(例如cv=5表示5折分层交叉验证)。 - 在
CalibratedClassifierCV对象上调用.fit()方法,并传入全部数据。它会内部进行交叉验证:在每一折的训练集上训练基础分类器,在验证集上拟合校准函数。
默认情况下(ensemble=True),最终预测的概率是各折校准分类器预测概率的平均值。
代码示例:
from sklearn.calibration import CalibratedClassifierCV
from sklearn.naive_bayes import GaussianNB
# 基础分类器
base_clf = GaussianNB()
# 创建校准分类器对象,使用5折交叉验证和等渗回归方法
calibrated_clf = CalibratedClassifierCV(base_estimator=base_clf, cv=5, method='isotonic')
# 拟合数据(内部完成训练和校准)
calibrated_clf.fit(X_train, y_train)
# 预测校准后的概率
calibrated_probs = calibrated_clf.predict_proba(X_test)

工作流程与总结 🎯
在本节课中,我们一起学习了模型校准的完整知识。最后,我们来总结如何将校准步骤整合到你的工作流程中。
决定是否需要校准:首先,判断校准是否对你的应用重要。如果你工作在概率框架下、概率输出的准确性有重要影响(例如用于决策的置信度或风险评估),或者你需要计算重要的期望值,那么就应该考虑使用校准。
关键步骤:
- 使用独立数据集:始终在一个独立的验证集上运行校准步骤,这个数据集必须与最初训练分类器所用的训练集不同。否则,校准步骤可能会加剧可能存在的过拟合问题。
- 可视化诊断:使用校准可靠性曲线来初步评估你的模型校准情况。
- 选择方法:根据你的模型和数据情况,选择 Platt 缩放(Sigmoid)或等渗回归方法。
- 实施校准:使用 Scikit-learn 的
CalibratedClassifierCV类来实施校准。
其他技术考量(高级):
理想的校准函数除了严格单调递增外,还应最小化“严格适当评分规则”,并最大化“锐度”属性(即预测应远离全局概率平均值)。对于大多数应用,遵循上述关键步骤更为重要。

总而言之,模型校准是概率模型中极其重要的一环,你不能假设开箱即用的机器学习模型都是经过良好校准的。我建议使用校准可靠性图来初步评估模型的校准情况,并应用本节课描述的方法来潜在地提高你的机器学习模型输出概率估计的准确性。
83:朴素贝叶斯分类器 🧮

在本节课中,我们将要学习监督学习模型的另一个家族——朴素贝叶斯分类器。我们将了解其基本原理、不同类型以及在Python中的实现方式。
概述
朴素贝叶斯分类器是一类基于概率模型的监督学习模型。它们通过假设数据在每个类别中是如何生成的来进行分类。尽管其核心假设在现实中通常不成立,但这类模型因其训练速度快、对高维数据有效而广受欢迎。
上一节我们介绍了线性分类模型,本节中我们来看看与之相关的朴素贝叶斯家族。
核心概念与“朴素”假设
朴素贝叶斯分类器之所以被称为“朴素”,是因为它们做了一个简化的假设:在给定类别的情况下,一个实例的每个特征都与其他所有特征相互独立。
在实践中,特征之间通常存在相关性。例如,在预测房屋售价是否会高于业主报价时,室内面积可能与土地大小或卧室数量相关,而这些特征又可能与房产位置相关。
这种“朴素”的简化假设带来两方面影响:
- 一方面,学习一个朴素贝叶斯分类器非常快,因为只需要为每个特征独立地估计和应用简单的、基于类别的统计量。
- 另一方面,这种高效性的代价是,朴素贝叶斯分类器的泛化性能通常比其他更复杂的方法(甚至线性分类模型)稍差一些。
尽管如此,对于高维数据集,朴素贝叶斯分类器在某些任务上的表现常常能与支持向量机等其他复杂方法相竞争。
Scikit-learn中的三种朴素贝叶斯模型
以下是Scikit-learn库中提供的三种主要朴素贝叶斯模型:
- 伯努利朴素贝叶斯模型:使用一组二元出现特征。例如,在文本分类中,可以用一个二元特征来表示某个词在文本中是否出现。
# 特征示例:[词A出现?, 词B出现?, ...] - 多项式朴素贝叶斯模型:使用一组基于计数的特征。每个特征会考虑特定词(或其他特征)在训练样本(如文档)中出现的次数。
# 特征示例:[词A出现次数, 词B出现次数, ...] - 高斯朴素贝叶斯模型:假设特征是连续的或实数值的。这是本课重点,适用于一般数值数据。
伯努利和多项式模型特别适合文本数据,我们将在专项课程的文本挖掘部分深入探讨。本节课将聚焦于高斯朴素贝叶斯分类器。
高斯朴素贝叶斯分类器原理
高斯朴素贝叶斯分类器假设每个类别的数据都是由一个简单的、特定于该类的高斯(正态)分布生成的。
训练时,分类器会为每个特征估计每个类别的均值(μ)和标准差(σ)。
预测时,分类器将待预测数据点的特征与每个类别的特征统计量进行比较,并选择最可能生成该数据点的类别。从数学上讲,这对应于估计每个类别的高斯分布生成该数据点的概率,然后选择概率最高的类别。
其决策边界通常是一条抛物线。在特殊情况下,当每个特征在两个类别中的方差相同时,决策边界将是线性的。
在Python中使用高斯朴素贝叶斯
在Python中使用高斯朴素贝斯分类器非常简单:
from sklearn.naive_bayes import GaussianNB
# 1. 实例化模型
gnb = GaussianNB()
# 2. 在训练数据上拟合模型
gnb.fit(X_train, y_train)
# 3. 进行预测
y_pred = gnb.predict(X_test)

值得注意的是,朴素贝叶斯模型是Scikit-learn中少数支持 partial_fit 方法的分类器之一。当处理无法一次性装入内存的巨大数据集时,可以使用此方法进行增量训练。对于GaussianNB类,没有控制模型复杂度的特殊参数。
性能与适用场景
在一个简单的二分类示例中,高斯朴素贝叶斯分类器可以取得很好的性能。然而,当类别不再容易区分时,与线性模型类似,其性能会下降。
在一个真实世界的数据集(如乳腺癌数据集)上,高斯朴素贝叶斯分类器也表现良好,与其他方法(如支持向量分类器)具有竞争力。
典型应用场景:
- 高斯朴素贝叶斯:适用于高维数据,每个实例有数百、数千甚至更多特征。
- 伯努利/多项式朴素贝叶斯:适用于文本分类,其中不同单词作为特征的数量非常庞大,且特征向量是稀疏的(因为任何给定文档只使用总词汇的一小部分)。
总结与优缺点
本节课中我们一起学习了朴素贝叶斯分类器。从数学上可以证明,朴素贝叶斯分类器与线性模型相关,因此线性模型的许多优缺点也适用于它。
优点:
- 训练和预测速度快。
- 非常适合高维数据(包括文本)和涉及大型数据集的应用,在这些场景下效率至关重要,计算成本可能排除了其他分类方法。
缺点:
- 当特征之间的条件独立假设不成立时(即特征间存在显著协方差,许多真实数据集正是如此),其他能够考虑这些依赖关系的、更复杂的分类方法可能会优于朴素贝叶斯。
- 在获取与预测相关的置信度或概率估计时,朴素贝叶斯分类器给出的估计通常不可靠。

尽管如此,朴素贝叶斯分类器在某些任务上表现依然极具竞争力,并且通常作为基线模型非常有用,可以与之比较更复杂模型的性能。
84:随机森林算法 🌲🤖

概述
在本节课中,我们将要学习一种在机器学习中广泛使用且非常有效的方法——集成学习,并重点介绍其核心应用之一:随机森林算法。我们将了解其工作原理、优势、局限性以及如何在Python中应用它。
什么是集成学习?
集成学习是一种机器学习方法,它通过组合多个独立的“学习模型”来创建一个更强大的“聚合模型”。这个聚合模型的预测能力通常优于其中任何一个单独的模型。
核心公式:聚合模型 = 模型1 + 模型2 + ... + 模型N
为什么集成学习有效?
上一节我们介绍了集成学习的概念,本节中我们来看看它为何有效。
集成学习有效的主要原因在于,不同的学习模型虽然各自可能表现良好,但它们倾向于在数据集上犯不同类型的错误。这通常是因为每个单独的模型可能对数据的不同部分“过拟合”。
通过将不同的模型组合成一个集成模型,我们可以平均掉它们各自的错误,从而在保持强大预测性能的同时,降低过拟合的风险。
随机森林:决策树的集成
随机森林是集成学习思想应用于决策树的一个具体例子。它在实践中被广泛使用,并在各种问题上取得了非常好的效果。
在Scikit-learn中,我们可以通过ensemble模块中的RandomForestClassifier类将其用作分类器,或使用RandomForestRegressor类进行回归。
正如我们之前所见,使用单一决策树的一个缺点是它容易对训练数据过拟合。随机森林通过在训练集上创建大量(通常是几十或几百棵)独立的决策树来解决这个问题。
随机森林如何工作?
随机森林的目标是让森林中的每棵树都能较好地预测训练集中的目标值,但同时要以某种方式与其他树有所不同。这种差异是通过在构建每棵决策树的过程中引入随机性来实现的。
以下是构建过程中的两个关键随机步骤:
- 随机选择数据:每棵树使用一个“自助采样”数据集进行训练。
- 随机选择特征:在树的每个节点进行分裂时,只从所有特征的一个随机子集中寻找最佳分裂点。
代码示例:创建随机森林分类器
from sklearn.ensemble import RandomForestClassifier
# n_estimators 控制树的数量
# max_features 控制每次分裂时考虑的特征数量
rf_model = RandomForestClassifier(n_estimators=100, max_features='sqrt', random_state=0)

关键参数解析
以下是使用随机森林时需要了解的一些关键参数及其作用:
n_estimators:设置森林中树的数量。默认值为10。对于更大的数据集,增加这个数字几乎总是一个好主意,因为集成更多树可以平均误差,减少过拟合。但请注意,增加树的数量也会增加训练的计算成本(时间和内存)。max_features:这个参数对性能有很强的影响,它决定了森林中树的多样性。对于分类,默认值是总特征数的平方根(sqrt);对于回归,默认值是总特征数以2为底的对数(log2)。在实践中通常效果很好。较小的max_features值倾向于减少过拟合。max_depth:控制集成中每棵树的深度。默认设置为None,意味着树节点会一直分裂,直到所有叶子节点都属于同一类,或者包含的样本数少于min_samples_split参数值(默认为2)。n_jobs:告诉算法使用多少个CPU核心并行训练模型。设置为-1将使用系统所有核心。通常可以获得接近线性的加速。random_state:鉴于随机森林的随机性,如果你想获得可重现的结果,为这个参数设置一个固定值(例如0)尤其重要。
随机森林的预测过程
一旦随机森林模型训练完成,它通过以下方式对新实例进行预测:
- 首先,让森林中的每棵树都做出一个预测。
- 对于回归任务,总体预测通常是所有树预测值的平均值。
- 对于分类任务,总体预测基于加权投票。每棵树为每个可能的类别标签给出一个概率,然后对所有树的每个类别的概率进行平均,概率最高的类别就是最终的预测类别。
实践应用与示例
让我们将随机森林应用到一个具有更多特征的大型数据集上,以便与其他监督学习方法进行比较。我们将再次使用乳腺癌数据集。
代码示例:在乳腺癌数据集上应用随机森林
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
# 加载数据
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
# 创建随机森林分类器,约有30个特征,设置max_features=8
rf = RandomForestClassifier(n_estimators=100, max_features=8, random_state=0)
rf.fit(X_train, y_train)
print("训练集精度: {:.3f}".format(rf.score(X_train, y_train)))
print("测试集精度: {:.3f}".format(rf.score(X_test, y_test)))
我们可以看到,随机森林在不进行特征缩放或大量参数调优的情况下,就能在此数据集上取得非常好的测试集性能。事实上,它的表现与我们目前见过的所有其他监督方法(包括需要更仔细调优的核支持向量机和神经网络)一样好,甚至更好。
优势与局限性
优势:
- 强大且性能优异:在各种问题上都能提供出色的预测性能。
- 无需特征缩放:与许多其他监督学习方法不同,通常不需要对特征数据进行仔细的缩放或其他预处理。
- 易于并行化:尽管构建许多不同的树需要相应的计算量增加,但随机森林的构建可以轻松地在多个CPU上并行进行。
局限性:
- 可解释性差:随机森林模型对人类来说可能非常难以解释,很难看清特征的预测结构或了解决策的具体原因。
- 不适用于高维稀疏特征:对于像文本分类这样具有非常高维稀疏特征的任务,随机森林不是一个好选择,而线性模型可以提供更高效的训练和快速、准确的预测。
总结
本节课中我们一起学习了随机森林算法。我们了解到,随机森林是一种基于决策树的强大集成学习方法,它通过引入数据采样和特征选择的随机性来构建多棵不同的树,并通过集体决策(投票或平均)来提高预测精度并减少过拟合。它在实践中易于使用,通常无需复杂的预处理和调参就能获得优秀的表现,但其“黑箱”特性也带来了可解释性上的挑战。
85:梯度提升决策树 🌳➡️🚀

在本节课中,我们将学习梯度提升决策树。这是一种基于树的集成方法,在现实世界的应用中获得了广泛使用。我们将了解其核心思想、关键参数,并通过代码示例展示其应用。
概述
梯度提升决策树与随机森林类似,都使用多棵树的集成来构建更强大的分类和回归预测模型。然而,它们的构建方式有根本不同。上一节我们介绍了随机森林,本节中我们来看看梯度提升决策树是如何工作的。
核心思想
与随机森林并行构建并组合多棵随机不同的树不同,梯度提升决策树的核心思想是按顺序构建一系列树。其中,每一棵后续的树都经过训练,旨在纠正前序树在系列中犯下的错误。
通常,梯度提升树集成使用许多浅层树,在机器学习中称为弱学习器。它们以一种非随机的方式构建,随着添加更多的树,模型犯的错误会越来越少。
模型构建完成后,使用梯度提升树模型进行预测速度很快,且不占用太多内存。
关键参数
以下是控制梯度提升树模型复杂度的两个主要参数:
n_estimators:设置集成中要使用的弱学习器(小决策树)的数量。learning_rate(学习率):这是一个随机森林中没有的新参数。它控制梯度提升树算法如何构建一系列纠正性树。- 当学习率较高时,每棵后续树都会强烈强调纠正其前驱树的错误,这可能导致更复杂的单棵树,从而总体上形成更复杂的模型。
- 当学习率设置较小时,对彻底纠正前一步错误的强调会减少,这往往会在每一步产生更简单的树。
通常,n_estimators 和 learning_rate 这两个参数需要一起调整,因为降低学习率通常需要更多的树来维持模型复杂度。
与随机森林不同,增加 n_estimators 可能导致过拟合。因此,通常 n_estimators 的设置是为了在训练期间最好地利用系统的速度和内存能力,然后在此基础上调整学习率等其他参数。
此外,max_depth 参数通过控制单棵树的深度和复杂度,也会影响模型复杂度。梯度提升方法假设每棵树都是弱学习器,因此 max_depth 参数通常设置得很小,在大多数应用中约为3到5。
代码示例:水果分类任务

以下是如何在 scikit-learn 中使用梯度提升树处理我们的示例水果分类任务,并绘制决策区域。代码与我们用于随机森林的代码大致相同。
# 从集成模块导入梯度提升分类器
from sklearn.ensemble import GradientBoostingClassifier
# 创建梯度提升分类器对象
# 默认参数:learning_rate=0.1, n_estimators=100, max_depth=3
clf = GradientBoostingClassifier().fit(X_train, y_train)
与随机森林一样,您可以看到决策边界具有那种类似盒子的形状,这是决策树或树集成的特征。
应用于乳腺癌数据集
现在让我们将梯度提升决策树应用于乳腺癌数据集。此代码训练两个不同的梯度提升分类器。
# 第一个分类器使用默认设置
clf1 = GradientBoostingClassifier().fit(X_train, y_train)
# 评估结果...
我们可以看到第一个结果在训练集上具有完美的准确率,这表明模型很可能过拟合。
有两种方法可以学习一个复杂度较低的梯度提升树模型:
- 降低学习率,这样每棵树就不会过于努力地去学习一个复杂的模型来纠正前驱的错误。
- 降低集成中单棵树的
max_depth参数。
第二个分类器示例对这些参数进行了调整:
# 第二个分类器:降低学习率和最大深度
clf2 = GradientBoostingClassifier(learning_rate=0.01, max_depth=2).fit(X_train, y_train)
# 评估结果...
您可以看到,训练集准确率确实下降了,而测试集准确率略有提高。
优势与局限
梯度提升决策树是目前可用的最佳现成监督学习方法之一,一旦模型训练完成,仅需适中的内存和运行时间要求即可实现出色的预测准确率。机器学习的一些主要商业应用就是基于梯度提升决策树。
与其他基于决策树的学习方法一样,您不需要进行特征缩放算法就能表现良好,并且特征可以是二进制、分类和连续类型的混合。
然而,提升决策树也有几个缺点:
- 与随机森林一样,树集成模型比单棵决策树更难解释。不过,对于许多以预测准确率为最重要目标的应用来说,这通常可能无关紧要。
- 梯度提升方法可能需要仔细调整学习率和其他参数,并且训练过程可能需要大量计算。
- 与我们看到的其他基于树的学习方法一样,将梯度提升方法用于文本分类或其他特征空间具有数千个稀疏值特征的场景,通常不是准确性和计算成本方面的好选择。
总结
本节课中我们一起学习了梯度提升决策树。我们了解到它是一种顺序构建树集成的强大方法,每棵树都旨在纠正前序树的错误。我们探讨了控制其复杂度的关键参数——n_estimators、learning_rate 和 max_depth,并通过代码示例看到了其应用和调参效果。最后,我们总结了该方法的优势(高精度、无需特征缩放)和局限性(可解释性差、需仔细调参、不适合高维稀疏数据)。
86:神经网络基础 🧠

在本节课中,我们将要学习神经网络的基础知识。神经网络是一系列广泛的算法,构成了近期被称为“深度学习”的计算领域复兴的基础。我们将介绍其基本概念、模型结构,以及如何在Python中应用它们进行分类和回归任务。
概述
神经网络算法最早的研究始于20世纪50年代和60年代。近年来,随着深度学习在图像物体分类、快速准确的机器翻译以及游戏对弈等特定任务上取得了令人瞩目的先进成果,人们对它的兴趣重新高涨。本节将介绍构成神经网络以及当今使用的更复杂深度学习方法基础的基本概念和算法。
我们将学习一种称为多层感知机的基本模型,它在Scikit-learn库中得到支持,可用于分类和回归任务。
回顾线性与逻辑回归
在深入神经网络之前,让我们先简要回顾一下我们已经学过的用于回归和分类的简单方法:线性回归和逻辑回归。
线性回归预测一个连续输出值 Y_hat。其公式为输入变量 X_i 的加权和,加上一个截距或偏置项 B_hat:
Y_hat = Σ(W_i * X_i) + B_hat
我们曾学习过如何使用普通最小二乘法、岭回归或Lasso回归等方法来从训练数据中估计这些模型系数 W_i 和 B_hat。
逻辑回归在此基础上更进一步,它将输入变量 X_i 的线性函数输出,通过一个额外的非线性函数——逻辑函数进行处理,以产生输出 Y。由于逻辑函数的特性,Y 的值被约束在0和1之间。我们使用逻辑回归进行二元分类,因为可以将 Y 解释为在二分类场景中,给定输入数据实例属于正类的概率。
引入多层感知机
现在,我们来看一个用于回归的简单神经网络示例,称为多层感知机,有时缩写为MLP。这些网络也被称为前馈神经网络。
MLP采用了我们之前看到的计算输入特征加权和的思想,但它比逻辑回归更进一步,增加了一个称为“隐藏层”的额外处理步骤。隐藏层中的每个框称为“隐藏单元”,每个隐藏单元计算输入特征加权和的非线性函数,产生中间输出值 V_0, V_1, V_2。然后,MLP计算这些隐藏单元输出的加权和,以形成最终的输出值 Y_hat。
隐藏单元应用的这个非线性函数称为“激活函数”。在这个例子中,激活函数是双曲正切函数,它与逻辑函数相关。可以看到,在预测模型中添加这个额外的隐藏层处理步骤,使得 Y_hat 的公式比逻辑回归的公式复杂得多。
具体来说,每个输入和每个隐藏单元之间有一个权重,每个隐藏单元和输出变量之间也有一个权重。实际上,这种非线性激活函数的添加和组合,使得多层感知机能够学习比简单线性或逻辑函数更复杂的函数。这种额外的表达能力使得神经网络在输入和输出之间的关系本身很复杂时,能够进行更准确的预测。
当然,这种复杂性也意味着在训练阶段有更多的权重(模型系数)需要估计,这意味着与线性模型相比,学习神经网络通常需要更多的训练数据和计算。
激活函数的选择
顺便提一下,神经网络中应用于隐藏单元的激活函数有多种选择。
以下是三种主要的激活函数,我们将在本讲座后面进行比较:
- 双曲正切函数:绿色的S形函数。
- 修正线性单元函数:我们将其缩写为ReLU,图中显示为蓝色的分段线性函数。
- 熟悉的逻辑函数:图中显示为红色。
ReLU激活函数是Scikit-learn中神经网络的默认激活函数。它将任何负输入值映射为零。双曲正切函数将大的正输入值映射到非常接近1的输出,将大的负输入值映射到非常接近-1的输出。激活函数的这些差异可能会对神经网络学习的回归预测图或分类决策边界的形状产生一些影响。
通常,我们将使用双曲正切函数或ReLU函数作为默认激活函数,因为它们在大多数应用中表现良好。
用于分类的神经网络
接下来,我们看看如何在Scikit-learn中使用神经网络进行分类,这里使用一个更复杂的合成二元分类数据集。
要使用神经网络分类器,你需要从 sklearn.neural_network 模块导入 MLPClassifier 类。以下代码示例展示了分类器如何拟合训练数据,这里使用了单个隐藏层,但层中的隐藏单元数量不同:1个单元、10个单元和100个单元。
与我们已经见过的所有其他分类器类型一样,你可以使用适当的参数创建分类器对象,并在训练数据上调用 fit 方法。这里,神经网络分类器的主要参数是 hidden_layer_sizes。这个参数是一个列表,每个隐藏层对应一个元素,给出该层要使用的隐藏单元数量。所以这里我们传递一个包含单个元素的列表,意味着我们想要一个隐藏层,其单元数量由变量 units 指定。
默认情况下,如果你不指定 hidden_layer_sizes 参数,Scikit-learn将创建一个具有100个隐藏单元的单一隐藏层。虽然像我们这里使用的简单数据集,设置为10可能效果很好,但对于真正复杂的数据集,隐藏单元的数量可能达到数千个。我们稍后会看到,也可以通过传递具有多个条目的 hidden_layer_sizes 参数来创建具有多个隐藏层的MLP。
我还想提一下 solver 这个额外参数的使用,它指定了用于学习网络权重的算法。这里我们使用的是LBFGS算法。我们将在本讲座末尾进一步讨论 solver 参数的设置。
另外,请注意,我们在创建MLP分类器对象时传入了 random_state 参数,就像我们对 train_test_split 函数所做的那样,并且我们恰好将这个 random_state 参数设置为固定值0。这是因为对于神经网络,它们的权重是随机初始化的,这会影响学习到的模型。因此,即使不改变相同数据集上的关键参数,相同的神经网络算法也可能学习到两个不同的模型,这取决于所选的内部随机种子值。因此,通过始终为用于初始化权重的随机种子设置相同的值,我们可以确保使用这些示例的每个人都能获得相同的结果。
这个图表展示了运行此代码的结果,显示了单个隐藏层神经网络中隐藏单元的数量如何影响分类的模型复杂度。
- 1个隐藏单元:模型在数学上等同于逻辑回归。我们看到分类器返回了熟悉的、简单的、两类之间的线性决策边界。训练集分数很低,测试分数也没有好多少,所以这个网络模型欠拟合。
- 10个隐藏单元:我们可以看到MLP分类器能够学习到更复杂的决策边界,捕捉到数据中更多面向线性簇的结构,尽管测试集准确率仍然较低。
- 100个隐藏单元:决策边界更加详细,并且在训练集和测试集上都实现了更好的准确率。
增加网络深度:多个隐藏层
这是一个具有两个隐藏层的多层感知机的图形描述。添加第二个隐藏层进一步增加了神经网络可以从更复杂数据集中学习的函数的复杂性。
将这种复杂性进一步推进,具有许多计算阶段的庞大神经网络架构,就是为什么深度学习方法被称为“深度”的原因。我们将在本周即将到来的讲座中总结深度学习。
笔记本中有一个示例,展示了我们如何创建一个两层MLP,每层有10个隐藏单元。我们只需在创建MLP分类器时将 hidden_layer_sizes 参数设置为一个包含两个元素的列表,表示两个隐藏层各有10个单元。
你可以看到在我们之前看到的分类问题上添加第二个隐藏层的结果。左边是原始的具有一个10单元隐藏层的MLP。右边是使用新的具有两个隐藏层(每层10个单元)的MLP的相同数据集。你可以看到具有两个隐藏层的MLP学习到了更复杂的决策边界,并且在这种情况下,对训练数据的拟合更好,测试数据的准确率也略高。
控制复杂度:正则化
一旦我们开始添加更多具有大量隐藏单元的隐藏层,你可以看到神经网络需要估计的权重或模型系数数量会迅速增加。因此,更复杂的神经网络可能拥有成千上万个权重需要估计。
我们可以像处理岭回归和Lasso回归一样,通过添加对权重的L2正则化惩罚来控制这种模型复杂度。请记住,L2正则化惩罚那些所有权重值平方和较大的模型,其效果是神经网络更倾向于权重更接近0的模型。

MLP的正则化参数称为 alpha,与线性回归模型一样。在Scikit-learn中,默认设置为一个较小的值,例如0.0001,以提供一点正则化。
这个代码示例展示了将 alpha 从一个较小的值0.01改变到一个较大的值5.0,对一个具有两个隐藏层(每层100个节点)的较大MLP的影响。为了多样化,这里我们还设置了激活函数使用双曲正切函数。
这是笔记本代码的图形输出。你可以看到随着 alpha 增加,正则化增强的效果。在左侧图中,当 alpha 较小时,决策边界更加复杂和多变,分类器过拟合,这可以从非常高的训练集分数和低的测试分数中看出。另一方面,右侧图使用了这里最大的 alpha 值5.0,该设置导致决策边界更加平滑,同时仍然捕捉数据的全局结构。这种增加的简单性使其能够更好地泛化,而不会对训练集过拟合,这从本例中高得多的测试分数可以明显看出。
数据预处理的重要性
与其他监督学习模型(如正则化回归和支持向量机)一样,在使用神经网络时,对输入特征进行适当的归一化至关重要。
让我们将多层感知机应用于乳腺癌数据集。请注意,我们首先应用 MinMaxScaler 来预处理输入特征。这里,我们将结合使用一个更复杂的网络,该网络具有两个隐藏层,每层100个隐藏单元,alpha 设置为5.0以进行更高的正则化,并再次使用LBFGS求解器。
你可以看到,使用这个多层感知机,训练集和测试集的准确率都是我们在这个数据集上获得的最高值之一。
用于回归的神经网络
像我们见过的许多其他监督学习方法一样,你也可以将多层感知机用于回归以及分类。
我们在这里包含MLP回归作为示例,原因有两个。首先,因为MLP回归本身可能对某些回归问题有用。但更普遍地说,因为一些深度学习问题就是回归问题,因此与分类一样,使用多层感知机是了解用于回归和深度学习的更复杂架构的良好起点。
以下是我们笔记本中一个简单MLP回归模型的示例。
你通过从 sklearn.neural_network 模块导入 MLPRegressor 类来使用多层感知机回归器,然后创建MLP回归器对象。在创建对象时,我们使用与分类相同的 hidden_layer_sizes 参数来设置隐藏层的数量和每个隐藏层内的单元数。此示例使用两个隐藏层,每层有100个隐藏节点。
此笔记本代码有一个循环,遍历激活函数参数和用于L2正则化的 alpha 参数的不同设置。这里,我们包含了回归结果,其中顶行使用双曲正切激活函数,底行使用ReLU激活函数。你可以看到激活函数的平滑度在一定程度上影响了相应回归结果的平滑度。
沿着列,图表还显示了使用不同的 alpha 设置从左到右增加L2正则化量的效果。同样,与分类一样,通过增加 alpha 来增加L2正则化量的效果是约束回归使用越来越简单的模型,这些模型具有越来越少的大权重。你可以在顶部和底部的行中看到这种效果。左侧的回归线比右侧更平滑的正则化模型具有更高的方差。
神经网络的优缺点
在积极方面,除了我们在这里展示的简单示例之外,神经网络构成了高级学习架构的基础,这些架构能够捕捉复杂的特征,并在日益广泛的困难学习任务上提供最先进的性能,从围棋的世界冠军对弈到图像中物体的详细而稳健的识别。
然而,随着这种能力的增强,成本也随之增加。这些更大、更复杂的模型通常需要大量的数据、计算和训练时间来学习。此外,需要对输入数据进行仔细的预处理,以帮助确保快速、稳定、有意义地找到最优权重集。
一般来说,当特征类型相似时(例如,全部来自图像的像素),神经网络是一个不错的选择。而当特征类型非常不同时,则不是一个好的选择。
关键参数总结
最后,让我们回顾一下Scikit-learn中用于控制模型复杂度的多层感知机的关键参数。
控制MLP模型复杂度的主要方法是控制隐藏单元的大小和结构,使用 hidden_layer_sizes 参数,该参数控制隐藏层的数量和每层内的单元数。
alpha 控制正则化的量,通过约束模型权重的大小来帮助约束模型的复杂度。
最后,你可以通过使用 activation 参数,至少尝试三种不同的非线性激活函数选择。
早些时候,我们看到了用于指定学习网络权重的算法的 solver 参数。求解器是实际执行寻找最优权重的数值工作的算法。直观地可视化这个过程的一种方式是,所有的求解器算法都必须在非常崎岖、充满许多局部最小值的“地形”中进行一种“爬山”搜索,每个局部最小值对应一组局部最优的权重,即比附近任何权重选择都更好的权重设置。
因此,在这个充满非常崎岖的局部最小值的整个“地形”中,有些在测试数据上具有较高的验证分数,有些则较低。因此,根据权重的初始随机初始化,以及求解器在这个崎岖地形中搜索路径轨迹的性质,求解器可能最终到达不同的局部最小值,这些局部最小值可能具有不同的验证分数。
默认的求解器 adam 在具有数千个训练示例的大型数据集上往往既高效又有效。对于小型数据集,比如我们在这些示例中使用的许多数据集,LBFGS 求解器往往更快,并能找到更有效的权重。
你可以在Scikit-learn的文档中找到关于这些更高级设置的进一步详细信息。
总结
在本节课中,我们一起学习了神经网络的基础知识。我们从回顾线性回归和逻辑回归开始,引入了多层感知机作为基本的神经网络模型。我们探讨了激活函数的作用和选择,学习了如何在Scikit-learn中构建和训练用于分类和回归的MLP模型。我们还讨论了通过调整隐藏层结构和使用正则化来控制模型复杂度的重要性,并简要介绍了神经网络的优缺点及其关键参数。这些概念构成了理解更复杂的深度学习架构的基础。
87:深度学习(选修)🧠

在本节课中,我们将探讨深度学习的基本概念、其核心优势与挑战,并通过一个简单的代码示例了解如何使用高级框架快速构建深度学习模型。
正如我们在课程第一周所讨论的,机器学习的一个关键挑战是为特定问题找到合适的特征,以作为学习模型的输入。
这被称为特征工程,它既是一门艺术,也是一门科学。
特征工程也常常是决定学习任务成败的最重要因素。有时,它甚至比模型本身的选择更为重要。
我们将在课程的最后一周进一步讨论这一点。
由于特征工程的困难性,学界对所谓的特征学习或特征提取算法进行了大量研究,这些算法能够自动找到好的特征。
这就引出了深度学习。从高层次看,深度学习的优势之一在于,它将一个复杂的自动特征学习阶段作为其监督训练的一部分。
此外,深度学习之所以被称为“深度”,是因为这种特征提取通常不只使用一个特征学习步骤,而是使用多个特征学习层构成的层次结构,每一层都馈入下一层。
以下是一个简化的例子,展示了在图像识别任务(例如手写数字识别)中,深度学习架构在实践中可能的样子。
你可以看到自动特征提取步骤由一系列特征层构成,每一层都基于一个进行卷积的网络,卷积可以被视为针对特定模式的过滤器。随后是一个下采样步骤,也称为池化,它可以在图像的任何位置检测到该特征的平移或旋转版本。
这样,特征就能被正确检测出来,用于最终由全连接网络实现的分类步骤。
下采样步骤还具有降低网络计算复杂度的效果。
根据我们想要预测的对象的属性,例如,如果我们只关心图像中是否存在某个物体,而不关心其具体位置,那么架构中的下采样部分可能被包含,也可能不被包含。
这仅仅是深度学习架构的一个例子。根据具体的学习问题,其规模、结构和其他属性可能看起来非常不同。
这张来自密歇根大学 Hong La Lee 及其同事论文的图片展示了用于人脸识别的多层特征学习示意图。
从左到右有三组,分别对应特征学习的第一、第二和第三阶段。每个阶段的矩阵显示了一组图像特征,每个方块代表一个特征。
每个特征可以被视为一个检测器或过滤器,当底层图像中出现该模式时,它就会被激活。
其深度学习架构的第一层提取最原始的低级特征,例如边缘和不同类型的斑点。第二层则从这些第一层特征的组合中创建新特征。
对于人脸,这可能对应于捕获鼻子或眼睛等更高级特征形状的关键元素。
第三层则从第二层特征的组合中创建新特征,形成捕获典型人脸类型和面部表情的更高级特征。
最后,所有这些特征都被用作最终监督学习步骤(即人脸分类器)的输入。
以下是针对不同类型物体(汽车、大象、椅子以及混合物体)进行训练后得到的特征层。这种复杂的特征无法从少数几层中学习到。
算法和计算能力的进步使得当前的深度学习系统能够训练具有数十层线性层次特征的架构。
事实证明,人类大脑在处理视觉信息时,会进行与此非常相关的操作。有特定的神经回路首先进行低级特征提取,例如边缘检测和寻找重复模式的频率。
这些低级特征随后被用于计算更复杂的特征,以帮助估计简单形状及其方向,或者判断一个形状是在前景还是背景中。
接着是更高级的视觉处理层,支持更复杂的任务,如人脸识别和解释多个运动物体的运动。
从积极的一面看,深度学习系统取得了令人印象深刻的进步,并在许多困难任务上达到了最先进的性能。
深度学习的自动特征提取机制也减少了在寻找好特征时对人类猜测的需求。
最后,利用当前的软件,深度学习架构非常灵活,可以适应不同的任务和领域。
然而,从消极的一面看,深度学习可能需要非常大的训练集和计算能力,这可能会限制其在某些场景下的实用性。
实现的复杂性可以被视为深度学习的缺点之一。这也是为什么开发了许多复杂的高级软件包来协助开发深度学习架构。
此外,尽管我们之前看到的人脸例子给出了清晰、易于解释的特征,但在大多数情况下,典型深度学习系统的特征和权重远没有那么容易解释。
也就是说,我们不清楚为什么或是什么特征导致深度学习系统做出特定的预测。
虽然使用 Scikit-learn 的 MLPClassifier 和 MLPRegressor 类进行学习为学习和应用简单神经网络提供了一个有用的环境,但如果你有兴趣深入了解深度学习及其所需的软件工具,我们提供了一些额外资源的链接。
典型的深度学习开发使用多层软件栈完成。以下是一个例子。
高级层提供了一个高级编程接口,允许你仅用几行代码就指定一个深度学习架构。
在这个例子中,我选择 Keras 作为我的顶层编程层,稍后我会向你展示一个实际的 Keras 示例。
高级编程层调用一个或多个低级服务,用于定义描述算法工作流的计算图,或以向量、矩阵、张量等形式操作数据。
TensorFlow 是提供这类核心机器学习框架服务的软件示例,尽管 TensorFlow 也有一个高级编程层。
高级的 Keras 层利用了 TensorFlow 2 的核心服务。
底层是依赖于硬件的层,它执行最低级别的操作,例如矩阵乘法,这种方式通常针对特定的处理器或计算架构进行了优化。
例如,图形处理单元(GPU)是一种最初为显卡开发的特殊处理器,可以进行极快的矩阵运算。这个最底层可以利用这种专用硬件来加速深度学习模型的训练和运行。
让我解释一下这里的几个缩写。BLAS 代表基本线性代数子程序。这是线性代数事实上的标准低级例程。
BLAS 规范相当通用,但具体的实现通常针对给定处理器进行了高度速度优化。
CUDA 代表统一计算设备架构。这是一个并行计算平台和应用程序编程接口,允许软件使用某些类型的 GPU 进行通用处理。
我们还有为机器学习优化的新硬件形式,称为张量处理单元(TPU),正在被部署。
CUDNN 是一个 GPU 加速库,为深度神经网络应用中经常出现的例程提供了高度优化的实现。
因此,这三层协同工作,对于产生高效且有效的深度学习应用都至关重要。
TensorFlow、PyTorch 和 Keras 是目前使用最广泛的深度学习框架。
TensorFlow 是一个端到端的开源机器学习平台。它拥有一个全面、灵活的工具、库和社区资源生态系统,并支持高级编程接口和低级核心计算服务。
PyTorch 由 Facebook 的 AI 研究小组开发,并于 2017 年在 GitHub 上开源。它也用于各种复杂的机器学习应用,尤其是自然语言处理。PyTorch 以简单、易用、灵活、高效的内存使用和动态计算图而闻名。
Keras 现在是 TensorFlow 生态系统的一部分,为开发深度学习模型提供了一个简单、灵活的顶层编程接口。

因此,能够开发深度学习框架和机器学习具有多重好处。
使用深度学习,你可以非常快速地生成和迭代新模型,并且可以相对容易地进行调试,这在构建有效的机器学习系统时至关重要。
更具体地说,使用这些高级深度学习框架,你可以获得简单性的优势。使用深度学习,无需进行特征工程,表示学习甚至架构迭代都是自动完成的。
你可以仅使用几种不同的向量、矩阵、张量操作来构建流水线。
这些框架允许你进行高度可扩展的操作。
因此,得益于我们刚刚讨论的多层结构,你的代码非常适合在 GPU 或 TPU 等高性能计算硬件上进行并行化。
你可以通过在小批量数据上进行迭代来训练这些框架,这让你能够处理任意大小的数据集。
使用这些框架的深度学习模型也非常通用和可重用。它们可以在不从头开始的情况下用额外的数据进行训练。因此,可以轻松地“解冻”一个现有模型,添加更多训练数据来更新权重,然后再次“冻结”它,用于你的新任务。
因此,你可以在一个领域(如图像分类)预训练一个模型,然后针对不同问题(如视频分割)调整其训练。
我认为向你展示一个使用 Keras 定义简单数字识别器的具体示例会很有趣。这个例子来自 François Chollet 的《Python深度学习》一书。
一个 Keras 脚本通常有四个部分。
第一部分是一些准备数据的代码行。在这个例子中,我们使用 MNIST 数据集。因此,这里有一些代码来加载数据集,并进行一些重塑操作,使其为后续的神经网络准备好正确的格式。
第二部分,在准备好数据之后,你定义模型。这里我们将实现一个非常简单的模型:输入是一个数字,我们让它通过一个有 512 个单元的密集神经网络层,然后是一个有 10 个单元的第二层。
你可以看到,Keras 程序中顺序模型的定义与这里模型的图形描述之间有非常清晰的对应关系。因此,在 Keras 中创建这些模型非常容易:你定义想要的模型类型,添加这些层(每添加一层只需一行代码)。当然,你为每一层指定它有多少个隐藏单元、使用什么激活函数等等。
一旦定义了模型的层,你就进行所谓的“编译”,在这里指定一些重要参数,如使用哪个优化器、使用哪个损失函数等等。这为下一步(即训练步骤)在内部准备好代码。
训练步骤是使用带有标签的训练图像来拟合网络的地方,在这里你可以指定参数,如要运行多少个周期、在训练过程中每个批次要放入多少训练图像等等。
然后,在模型训练步骤之后,最后一步是评估模型。Keras 有非常简单的方法,你只需在网络上调用这个 evaluate 方法,使用测试数据,它就会为你提供数据上的损失以及像准确率这样的评估指标。
这很好地说明了使用高级框架来构建一个能完成有趣任务的非平凡神经网络是多么容易。
在本节课中,我们一起学习了深度学习的基本概念,包括其自动特征提取的核心优势、多层架构的工作原理,以及面临的挑战。我们还了解了主流的深度学习框架(如 TensorFlow、PyTorch 和 Keras)及其软件栈结构,并通过一个 Keras 代码示例直观地看到了构建和训练一个简单神经网络模型的便捷性。
88:数据泄露问题 🔍

在本节课中,我们将要学习数据科学中的一个关键概念——数据泄露。我们将了解数据泄露是什么、它为何重要、它如何发生,以及如何在你的应用中检测和避免它。
什么是数据泄露?
数据泄露,有时简称为“泄露”,描述的是这样一种情况:你用来训练机器学习算法的数据,恰好包含了关于你试图预测的目标的、意料之外的额外信息。
本质上,泄露发生在训练期间引入了关于目标标签或值的、在实际使用中本不应合法获得的信息的任何时候。
也许最简单的数据泄露例子是,如果我们将数据实例的真实标签作为一个特征包含在模型中。模型将学会类似这样的规则:如果这个对象被标记为苹果,就预测它是苹果。
我们之前见过的另一个明显的泄露例子是,测试数据意外地被包含在训练数据中,这会导致过拟合。然而,数据泄露也可能因许多其他原因发生,而且方式通常相当微妙,难以察觉。
当数据泄露确实发生时,它通常会导致模型开发阶段的结果过于乐观,随后在预测模型实际部署到新数据上并评估时,会带来令人失望的糟糕结果。换句话说,泄露可能导致你的系统学习到一个次优模型,该模型在实际部署中的表现远不如在无泄露环境下开发的模型。
为什么数据泄露很重要?
数据泄露在现实世界中可能产生重大影响,范围从对实际上无效的事物进行糟糕的货币和工程投资所带来的财务成本,到损害客户对你系统质量的看法或影响公司品牌的系统故障。
由于这些原因,数据泄露是数据挖掘和机器学习中最严重和最普遍的问题之一,也是作为机器学习从业者必须时刻警惕的事情。
数据泄露的微妙示例
现在,我们来看看数据泄露问题的一些更微妙的例子。
一个典型的情况是,训练数据中包含了在实际使用中本不应合法获得的关于未来的信息。
假设你正在开发一个零售网站,并构建一个分类器来预测用户是可能留下并查看另一个页面,还是离开网站。如果分类器预测他们即将离开,网站可能会弹出一个提供激励以继续购物的窗口。
一个包含泄露信息的特征例子是用户的总会话时长或他们在访问网站期间浏览的总页面数。这个总数通常在访问日志数据的后处理阶段被添加为新列。这个特征包含了关于未来的信息,即用户将要进行多少次访问,这在实际部署中是不可能知道的。一个解决方案是用“会话中已访问页面数”特征来替换“总会话时长”特征,该特征只知道会话中到目前为止访问的总页面数,而不知道还剩下多少。
第二个泄露示例可能涉及尝试预测银行网站上的客户是否可能开设账户。如果用户的记录包含一个账户号字段,对于仍在探索网站的用户,该字段通常为空,但一旦用户确实开设了账户,它最终会被填写。显然,用户账户字段在这种情况下不是一个合法的特征,因为在用户仍在探索网站时,它可能不可用。
另一个未来信息泄露到过去的例子可能是,如果你正在开发一个诊断测试来预测特定的医疗状况。现有的患者数据集可能包含一个二元变量,该变量恰好标记了患者是否因该状况接受了手术。显然,这样的变量对该医疗状况具有高度预测性。
还有许多其他方式可以使预测信息泄露到这个特征集中。可能存在某种缺失诊断代码的组合,非常能表明该医疗状况。但同样,这些信息在患者状况仍在研究时不可用,因此使用它们是不合法的。
或者,最后,同一个患者数据集中的另一个例子可能涉及患者ID的形式。ID的分配可能取决于特定的诊断路径。换句话说,如果是因为访问了专科医生(初始医生确定该医疗状况可能性很大)的结果,ID可能会不同。
这最后一个例子很好地说明了,在训练集中,数据泄露可能以多种不同方式发生。事实上,通常不止一个泄露问题同时存在。有时,修复一个泄露特征可能会揭示第二个泄露特征的存在。
数据泄露的主要类型
作为指导,以下是数据泄露的一些额外示例。我们可以将泄露分为两种主要类型:
- 训练数据中的泄露:通常是测试数据或未来数据混入了训练数据。
- 特征中的泄露:关于真实标签的某些高度信息性的内容以某种方式被包含为一个特征。
数据泄露的一个非常重要的原因是在整个数据集上执行某种预处理,其结果影响了训练期间看到的内容。这可能包括以下场景:
- 计算用于归一化和重新缩放的参数。
- 查找最小和最大特征值以检测和移除异常值。
- 使用变量在整个数据集上的分布来估计训练集中的缺失值或执行特征选择。
另一个需要特别谨慎的情况是在处理时间序列数据时,未来事件的记录被意外地用于计算特定预测的特征。我们看到的会话时长例子就是这种情况的一个实例。但如果数据收集或缺失值指示器存在错误,则可能发生更微妙的影响。如果一个特征与在某个时间跨度内收集至少一条记录有关,那么错误的存在可能会泄露关于未来的信息,换句话说,表明不再期望有进一步的观察。
泄露特征包括我们有一个像患者记录中的诊断ID这样的变量,我们将其移除,但却忽略了移除其他包含相同或相似信息的变量,这些变量被称为代理变量。患者ID的例子(其中ID号码由于入院过程而包含关于患者诊断性质的线索)就是这种情况的一个例子。
在某些情况下,数据集记录被有意随机化,或者包含用户特定信息(如姓名、位置等)的某些字段被匿名化。根据预测任务,撤销这种匿名化可能会泄露用户或其他敏感信息,这些信息在实际使用中本不应合法获得。
最后,我们在这里讨论的任何上述示例都可能出现在第三方数据集中,这些数据集作为附加特征源被连接到训练集。因此,要始终注意此类外部数据中的特征及其解释和来源。
如何检测和避免数据泄露?
那么,如何在你的应用中检测和避免数据泄露呢?
在构建模型之前,探索性数据分析可以揭示数据中的意外情况。例如,寻找与目标标签或值高度相关的特征。来自医疗诊断示例的一个例子可能是那个指示患者是否因该状况接受了特定外科手术的二元特征,它可能与特定诊断高度相关。
构建模型后,寻找拟合模型中令人惊讶的特征行为,例如极高的特征权重或与变量相关的非常高的信息增益。
接下来,寻找整体上令人惊讶的模型性能。如果你的模型评估结果远高于相似数据集上相同或类似问题的结果,那么请仔细查看对模型影响最大的实例或特征。
另一个更可靠的泄露检查方法(但也可能成本较高)是对训练好的模型进行有限的真实世界部署,以查看模型训练和开发结果所建议的估计性能与实际结果之间是否存在巨大差异。这种检查模型是否能很好地泛化到新数据的方法是有用的,但可能无法立即深入了解泄露是否发生、发生在哪里,或者任何性能下降是否是由于其他原因(如经典的过拟合)造成的。
让我给你一个例子,说明特征重要性分析如何帮助识别数据泄露的存在。
在这个例子中,我们使用拟合随机森林时计算的 feature_importances_ 属性。其思想是使用特征重要性分析来检查特征是否存在可疑的高重要性值。在这个例子中,假设我们有一个医疗数据集,其中包含关于患者、他们的健康状况、药物、位置等的各种特征,以及他们在系统中的患者ID。我们训练一个分类器来预测患者未来是否会被诊断出患有某种疾病,这是一个医疗分类任务。我们训练分类器,然后检查特征重要性值。你会期望与健康问题或人口统计相关的特征在此分析中具有较高的特征重要性权重,而像患者ID或社会安全号码这样的通用特征预计不会包含任何关于某人健康状况的预测信息。但当你查看特征重要性数字时,你会发现虽然社会安全号码确实具有较低的特征重要性,但患者ID却被列为信息量最大的特征,这非常令人惊讶。因此,值得更仔细地研究以理解原因。
这种情况实际上在真实数据集中发生过。在一个案例中,患者ID包含一个子ID,该子ID指示患者是由全科医生添加的,还是在预测的问题被识别后由专科医生添加到系统中的。换句话说,通过知道患者ID,系统得到了提前警告,即一个人已经看过专科医生,因此更有可能患有该疾病,从而为这个分类问题贡献了一种形式的数据泄露。
减少数据泄露的实践方法

你可以遵循一些实践方法来帮助减少应用中数据泄露的机会。
一条重要规则是确保在每个交叉验证折叠内分别执行任何数据准备。换句话说,如果你正在缩放或归一化特征,为此估计的任何统计量或参数应仅基于交叉验证分割中可用的数据,而不是整个数据集。你还应确保在相应的保留测试折叠上使用这些相同的参数。
如果你正在处理时间序列数据,请跟踪与处理特定数据实例(如用户在网页上的点击)相关的时间戳,并确保用于计算此实例特征的任何数据不包括时间晚于截止值的记录。这将有助于确保你没有在当前特征计算或训练数据中包含来自未来的信息。
如果你有足够的数据,甚至在开始处理新数据集之前,考虑划分出一个完全独立的测试集,然后仅在最后一步在此测试数据上评估你的最终模型。这里的目标类似于进行真实世界部署,以检查你的训练模型是否能合理地很好地泛化到新数据。如果性能没有显著下降,那很好。但如果有下降,泄露可能是一个促成因素,此外还有像经典过拟合这样的常见嫌疑。
总结
在本节课中,我们一起学习了数据泄露的概念。我们了解到,数据泄露发生在训练数据包含了关于预测目标、但在实际部署中本不应获得的信息时。它会导致模型在开发阶段表现过于乐观,而在真实应用中表现不佳。我们探讨了泄露的多种形式和原因,包括未来信息混入、代理变量以及预处理不当等。最后,我们学习了通过仔细的探索性数据分析、特征重要性检查、正确的交叉验证流程以及保留独立的测试集等方法来检测和避免数据泄露。作为机器学习从业者,始终保持对数据泄露的警惕至关重要。
89:无监督机器学习概述 🧠

在本节课中,我们将要学习无监督机器学习的基本概念、主要任务及其应用场景。我们将了解它与监督学习的核心区别,并初步认识两种主要的无监督学习方法:数据变换和聚类分析。
无监督机器学习包含多种多样的任务。与监督学习不同,无监督学习没有需要预测的目标值。相反,无监督学习算法的任务是处理原始数据,并从中捕捉一些有趣的结构。
这在许多场景中非常有用。例如,探索和可视化复杂数据集中的结构。进行密度估计,以预测事件的概率。压缩数据。在应用监督学习算法之前提取更有效的特征。或者发现重要的结构,例如数据中相似对象的聚类或不寻常的个别离群值。
所有这些以及其他无监督学习任务都有一个共同属性:没有目标值、标签或输出来学习或预测。相反,我们只有数据集中未标记的样本作为输入。
以下是聚类这一无监督方法的示例。假设你负责运营一个允许人们从你公司购买产品的网站,该网站每天有数千次访问。当人们通过点击产品链接或输入搜索词来访问网站时,他们的交互行为会被网络服务器记录,从而生成一个大型日志文件。
对于你的业务而言,通过根据用户的购物行为将他们分组来理解谁在使用你的网站可能很有用。例如,可能有一组更专业的用户,他们使用更高级的功能来寻找非常具体的商品。而另一组非专业用户可能只是喜欢浏览更广泛的商品集合。
通过将用户聚类成组,你可能会对你的典型客户是谁,以及不同类型的用户认为哪些网站功能重要有所洞察。你可以利用从用户聚类中获得的见解来改进针对不同群体的网站功能,或者向更有可能购买产品的特定群体推荐产品。
因此,在本讲中,我们将简要概述无监督学习方法,并将其分为两大类。首先,我们将看一类称为“变换”的无监督方法,因为它们本质上只是将原始数据通过某种有用的过程运行,以提取或计算某种信息。然后,我们将看另一大类无监督学习方法,即聚类方法。就像我们网站的例子一样,聚类方法在数据中寻找组,并将数据集中的每个点分配到其中一个组。
数据变换方法 🔄
上一节我们介绍了无监督学习的整体概念,本节中我们来看看第一类方法:数据变换。这些方法通过对原始数据进行处理,提取出更有价值的信息或结构。
以下是几种重要的数据变换方法:
密度估计
密度估计用于当你有一组测量值散布在一个区域中,并且你想创建一个可以视为覆盖整个区域的平滑版本时,该版本可以给出在该空间某个区域观察到特定测量值的一般可能性估计。
例如,在与诊断糖尿病相关的医疗应用中,单变量密度估计可用于估计特定测试分数的分布,即血液检测中的血浆葡萄糖浓度值,针对患有特定形式糖尿病的人群。利用这个密度估计,我们可以估计任何患有该医疗状况的人具有特定葡萄糖分数的概率,即使该特定分数未在原始数据集中出现。然后,我们可以将其与没有该病症的人的葡萄糖水平范围(此处由红线表示)进行比较。通常,密度估计随后用于进一步的机器学习阶段,作为为分类或回归提供特征的一部分。
更技术性的说法是,密度估计在特征空间上计算连续概率密度,给定该特征空间中的一组离散样本。利用这个密度估计,我们可以估计任何给定特征组合出现的可能性。在 Scikit-learn 中,你可以使用 sklearn.neighbors 模块中的 KernelDensity 类来执行一种广泛使用的密度估计形式,称为核密度估计。核密度估计在创建地理空间数据的热图时尤其受欢迎。
聚类方法 🧩
在了解了数据变换方法之后,本节我们将探讨另一大类无监督学习方法:聚类。聚类方法的核心目标是在数据中发现内在的组别结构。

聚类方法,如我们网站的例子所示,在数据中寻找组,并将数据集中的每个点分配到其中一个组。其核心思想是将相似的数据点归为一类,将不相似的点分开。
总结 📝

本节课中我们一起学习了无监督机器学习的基础知识。我们明确了它与监督学习的核心区别在于没有预设的目标标签。我们探讨了无监督学习的两大主要类别:数据变换方法(如密度估计)和聚类方法。数据变换旨在从原始数据中提取或计算新的信息表示,而聚类则致力于发现数据中内在的组别结构。理解这些基本概念是后续深入学习具体算法和应用的基础。
90:降维与流形学习 📉

在本节课中,我们将要学习无监督学习中的一个重要类别:降维算法。我们将探讨如何将高维数据转换为低维表示,以便于可视化和理解数据的内在结构。我们将重点介绍主成分分析(PCA)和几种流形学习方法。
概述
降维算法属于转换类无监督学习方法。这类转换将原始数据集(可能包含数百个特征)转换为一个仅使用少量维度(例如10个)的近似版本。一个常见的需求是在首次探索数据集时,通过二维散点图可视化来理解样本如何分组或相互关联。
主成分分析(PCA)🔍
上一节我们介绍了降维的基本概念,本节中我们来看看最经典的方法:主成分分析。
直观地说,PCA所做的是获取原始数据点云,并找到一个旋转,使得维度在统计上不相关。PCA随后通常会丢弃除最能捕捉原始数据集变化的最具信息量的初始维度之外的所有维度。
以下是一个使用合成二维数据集的简单示例。如果我们有两个高度相关的原始特征,由这个点云表示,PCA将旋转数据,使得方差最大的方向(称为第一主成分,沿着点云的长轴方向)成为第一个维度。然后,它会找到与第一主成分成直角、能最大程度捕捉剩余方差的方向,即第二主成分。在二维空间中,只有一个这样的方向与第一主成分垂直,但在更高维度中,会有无限多个。对于超过两个维度的情况,寻找与前一个主成分垂直的连续主成分的过程将持续进行,直到达到所需的主成分数量。
应用PCA的一个结果是,我们现在知道了原始二维数据的最佳一维近似。换句话说,我们可以取任何之前使用X和Y两个特征的数据点,并仅使用一个特征(即其在第一主成分上的投影位置)来近似它。
以下是使用Scikit-learn将PCA应用于更高维数据集(乳腺癌数据集)的示例。
要执行PCA,我们从sklearn.decomposition导入PCA类。首先,转换数据集以使每个特征的值范围具有零均值和单位方差,这一点很重要。我们可以使用StandardScaler类的fit和transform方法来实现,如下所示。
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# 假设 X 是原始数据
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 创建PCA对象,指定保留前两个主成分
pca = PCA(n_components=2)
pca.fit(X_scaled)
X_pca = pca.transform(X_scaled)
然后,我们创建PCA对象,指定我们只想保留前两个主成分以将维度减少到仅两列,并使用我们的归一化数据调用fit方法。这将设置PCA,使其学习数据集的正确旋转。然后,我们可以应用这个准备好的PCA对象,将原始输入数据集中的所有点投影到这个新的二维空间。
请注意,由于我们不是在进行监督学习或针对测试集评估模型,因此我们不必将数据集拆分为训练集和测试集。如果我们查看从PCA返回的数组的形状,它会将我们具有30个特征的原始数据集转换为一个只有两列的新数组,本质上是用两个新特征表示每个原始数据点,代表数据点在这个新的二维PCA空间中的位置。
然后,我们可以创建一个使用这两个新特征的散点图,以查看数据如何形成聚类。在这个例子中,我们使用了一个带有监督学习标签(即癌细胞的恶性和良性标签)的数据集,以便我们可以看到PCA在寻找数据聚类方面的效果如何。
以下是使用PCA计算的两个新特征绘制所有30个特征数据样本的结果。我们可以看到,恶性和良性细胞在这个空间中确实倾向于聚集成两组。事实上,我们现在可以将线性分类器应用于原始数据集的这个二维表示,并且可以看到它可能会表现得相当好。这说明了像PCA这样的降维方法的另一个用途:寻找信息丰富的特征,然后可以在后续的监督学习阶段使用。
我们可以创建一个热图,可视化乳腺癌数据集的前两个主成分,以了解每个成分与哪些特征分组相关联。请注意,我们可以获取表示定义PCA空间的两个主成分轴的数组,使用在数据上使用PCA的fit方法后填充的PCA.components_属性。
我们可以看到,第一主成分全是正值,显示了所有30个特征之间的一般相关性。换句话说,它们倾向于一起上下变化。第二主成分有正负符号的混合,但特别是,我们可以看到一组负号特征共同变化,并且与剩余特征的方向相反。查看名称,这个子集共同变化是有道理的。我们看到“平均纹理”和“最差纹理”这对,以及“平均半径”和“最差半径”这对一起变化,等等。
流形学习 🗺️
上一节我们介绍了PCA,它是一种探索数据集的好工具,但对于更复杂的数据集,可能无法找到产生更好可视化的更细微的分组。本节中我们来看看另一类算法:流形学习。
有一类称为流形学习算法的无监督算法,非常擅长在高维数据中找到低维结构,并且对可视化非常有用。高维空间中低维子集的一个经典例子是这个三维数据集,其中所有点都位于一个形状有趣的二维薄片上。这个高维空间内的低维薄片被称为流形。PCA不够复杂,无法找到这种有趣的结构。
一种广泛使用的流形学习方法称为多维缩放(MDS)。MDS有很多变体,但它们都有相同的总体目标:可视化高维数据集并将其投影到低维空间(在大多数情况下是二维页面),以一种保留原始数据空间中点之间接近程度信息的方式。这样,您可以在高维数据中找到并可视化聚类行为。
在Scikit-learn中使用像MDS这样的技术与使用PCA非常相似。与PCA一样,每个特征都应归一化,使其特征值具有零均值和单位方差。从sklearn.manifold导入MDS类并转换输入数据后,您创建MDS对象,指定组件的数量(通常设置为二维以进行可视化)。然后,您使用转换后的数据拟合对象,这将学习映射,然后您可以将MDS映射应用于转换后的数据。
以下是应用MDS到水果数据集的示例,您可以看到它在可视化不同水果类型确实倾向于聚集成组的事实方面做得相当好。
from sklearn.manifold import MDS
mds = MDS(n_components=2)
X_mds = mds.fit_transform(X_scaled)
T-SNE:强大的可视化工具 ✨
一种特别强大的用于可视化数据的流形学习算法称为T-SNE。T-SNE找到数据的二维表示,使得二维散点图中点之间的距离尽可能接近原始高维数据集中相同点之间的距离。特别是,T-SNE更重视保留关于相邻点之间距离的信息。

以下是T-SNE应用于手写数字数据集图像的示例。您可以看到,这个二维图保留了在像素方面相似的图像之间的邻居关系。例如,大多数数字8样本的聚类更接近数字3和5的聚类(在手写中可能看起来更相似),而不是距离更远的数字1的聚类。
以下是应用T-SNE到水果数据集的示例。代码与应用MDS非常相似,本质上只是用TSNE替换MDS。有趣的是,T-SNE在这个相当小且简单的水果数据集上在寻找结构方面表现不佳,这提醒我们,在使用流形学习可视化数据时,应该至少尝试几种不同的方法,看看哪种方法对特定数据最有效。T-SNE倾向于在具有更明确局部结构的数据集上效果更好。换句话说,具有更清晰定义的邻居模式。
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
X_tsne = tsne.fit_transform(X_scaled)
总结
本节课中我们一起学习了降维与流形学习。我们首先介绍了降维的概念及其在数据探索和可视化中的重要性。然后,我们深入探讨了主成分分析(PCA)的原理和应用,学习了如何使用它来减少数据维度并发现潜在结构。接着,我们介绍了流形学习,特别是多维缩放(MDS)和T-SNE,这些方法能够更好地捕捉复杂数据集中的非线性结构,并生成有意义的二维可视化。最后,我们通过实际代码示例演示了如何在Python中应用这些技术,并讨论了不同方法在不同类型数据集上的适用性。掌握这些工具将帮助您更有效地理解和呈现高维数据。
91:聚类分析 🧩

在本节课中,我们将学习无监督学习中的另一类重要方法:聚类分析。我们将探讨三种主流的聚类算法:K均值聚类、凝聚层次聚类和DBSCAN密度聚类,并了解它们的工作原理、应用场景以及如何用Python实现。
除了数据变换,无监督学习的另一大类方法是聚类方法。
聚类的目标是将数据集划分成若干个称为“簇”的组,使得相似的数据实例被分配到同一个簇中,而非常不相似的对象则被分配到不同的簇。
如果随着时间的推移有新的数据点加入,一些聚类算法也能预测新数据实例应该被分配到哪个簇。这类似于分类任务,但聚类模型无法预先使用带标签的示例进行训练。
K均值聚类 🎯
K均值聚类是最广泛使用的聚类算法之一。它的核心思想是在特征空间的不同区域找到K个簇中心,这些中心被认为代表了不同的数据组。
你需要预先指定K的值,这是K均值算法的一个缺点。对于某些问题,我们可能知道数据应该分为几类,但对于许多其他任务,我们可能并不知道。
K均值算法的运行过程如下:首先,随机选取K个簇中心的位置。然后,它在两个步骤之间反复迭代。
以下是算法的两个核心步骤:

- 分配步骤:给定现有簇中心的位置,根据每个数据点到中心的距离,将其分配给最近的簇中心。
- 更新步骤:调整每个簇中心的位置。具体做法是将新的簇中心设置为该簇内所有数据点位置的平均值。
经过一段时间的迭代,连贯的簇开始形成,簇中心以及每个数据点对应的簇分配最终会稳定下来。
K均值的一个特点是,簇中心不同的随机起始点通常会导致非常不同的聚类结果。因此,在scikit-learn中,K均值算法通常会以10个不同的随机初始化运行,并选择出现次数最多的那个解。
在Scikit-learn中应用K均值
K均值聚类在scikit-learn中应用简单。你需要从sklearn.cluster导入KMeans类,创建KMeans对象时通过n_clusters参数指定K值,然后在数据集上调用fit方法来运行算法。
这里需要区分能够预测新数据点应分配到哪个中心的聚类算法和不能进行此类预测的算法。K均值支持predict方法,因此我们可以分别调用fit和predict方法。我们稍后将看到的凝聚层次聚类等方法则不支持,必须在一个步骤中执行拟合和预测。
以下是应用于水果数据集的笔记本代码输出结果,在这个例子中我们预先知道K值。请注意,K均值对特征值的范围非常敏感。因此,如果你的数据特征具有非常不同的范围,使用最小-最大缩放进行归一化非常重要,就像我们对一些监督学习方法所做的那样。
由于K均值聚类中的每个簇完全由其中心点定义,因此它只能捕获相当简单的簇类型。当数据点形成大小大致相同、形状简单且分离良好的球形组时,K均值聚类往往效果很好。如果数据形成长而不规则的簇,K均值的效果往往不佳。
此外,我们在这里看到的K均值版本假设数据特征是连续值。然而,在某些情况下,我们可能具有分类特征,此时计算平均值没有意义。在这种情况下,存在K均值的变体,可以使用更通用的距离定义,例如能够处理分类特征的K中心点算法。
凝聚层次聚类 🌳
凝聚层次聚类指的是一类通过迭代自底向上方法工作的聚类方法。首先,每个数据点被放入自己的单一项簇中。然后进行一系列聚类,在每个阶段将最相似的两个簇合并成一个新簇,重复此过程直到满足某个停止条件。在scikit-learn中,停止条件是簇的数量。
以下是凝聚层次聚类如何在样本数据集上进行的可视化示例,直到达到三个簇。
在阶段1,每个数据点都在自己的簇中,由点周围的圆圈表示。在阶段2,两个最相似的簇(在此阶段相当于找到最近的点)被合并。这个过程继续进行,如表示每个簇的不断扩大的封闭区域所示。
你可以通过指定几种可能的连接标准之一,来选择凝聚层次聚类算法如何确定最相似的簇。在scikit-learn中,提供了以下三种连接标准:Ward、average和complete。
以下是三种连接标准的说明:
- Ward方法:选择合并能使所有簇内总方差增加最小的两个簇。
- 平均连接:合并两个点之间平均距离最小的簇。
- 完全连接:也称为最大连接,合并两个点之间最大距离最小的簇。
通常,Ward方法在大多数数据集上效果良好,是我们通常选择的方法。在某些情况下,如果你期望簇的大小差异很大(例如,一个簇远大于其他簇),也值得尝试平均连接和完全连接标准。
在Scikit-learn中执行凝聚层次聚类
要在scikit-learn中执行凝聚层次聚类,你需要从sklearn.cluster导入AgglomerativeClustering类。初始化对象时,指定n_clusters参数,该参数使算法在达到该簇数时停止。你使用数据集作为输入调用fit_predict方法,它会返回数据点的一组簇分配,如下所示。
凝聚层次聚类的一个优点是,作为算法的效果,它会自动将数据排列成一个层次结构,反映了每个数据点被分配到连续簇的顺序和簇距离。这种层次结构可以使用称为树状图的工具进行可视化,即使对于高维数据也很有用。
以下是前一个数据集示例的Ward方法聚类对应的树状图。数据点在底部并编号,Y轴代表簇距离,即两个簇在数据空间中的距离。数据点构成树底部的叶子,随着每对连续簇的合并,树中添加一个新的父节点。父节点沿Y轴的高度捕获了两个簇在合并时的距离,向上的分支代表新合并的簇。请注意,你可以通过树中每个分支的长度来判断合并的簇相距多远。
树状图的这个特性可以帮助我们找出正确的簇数。通常,我们希望每个簇内的项目高度相似,但与其他簇相距甚远。例如,我们可以看到从三个簇变为两个簇发生在一个相当高的Y值处,这意味着被合并的簇相距甚远。我们可能希望避免选择两个簇,而坚持使用三个簇,这样就不会迫使包含非常不相似项目的簇强行合并。
Scikit-learn不提供绘制树状图的功能,但SciPy可以。SciPy处理聚类的方式与scikit-learn略有不同,但这里有一个示例。我们首先从scipy.cluster.hierarchy模块导入dendrogram和ward函数。ward函数返回一个数组,指定了凝聚层次聚类期间跨越的距离。这个ward函数返回一个连接数组,然后可以传递给dendrogram函数来绘制树。
通常,当基础数据本身遵循某种层次过程,使得树易于解释时,利用这种层次结构最有用。例如,层次聚类对于遗传学和其他生物学数据特别有用,其中层级代表突变或进化的阶段。
DBSCAN密度聚类 🌌
但是,有些数据集K均值聚类和凝聚层次聚类都表现不佳。因此,我们现在将概述第三种聚类方法,称为DBSCAN。
DBSCAN是一个缩写,代表“基于密度的含噪声应用空间聚类”。DBSCAN的一个优点是你不需要预先指定簇的数量。另一个优点是它能很好地处理具有更复杂簇形状的数据集。你还可以找到不应合理分配给任何簇的离群点。DBSCAN相对高效,可用于大型数据集。
DBSCAN背后的主要思想是,簇代表数据空间中数据点更密集的区域,同时被空的或至少人口密度低得多的区域分隔开。
DBSCAN的两个主要参数是min_samples和eps。所有位于更密集区域的点都称为核心样本。对于一个给定的数据点,如果在距离eps内有min_samples个其他数据点,则该给定数据点被标记为核心样本。然后,所有彼此距离在eps单位内的核心样本被放入同一个簇中。
除了被归类为核心样本的点之外,最终不属于任何簇的点被视为噪声。而距离核心点eps单位内,但本身不是核心点的点被称为边界点。
以下是DBSCAN应用于样本数据集的示例。与其他聚类方法一样,DBSCAN从sklearn.cluster模块导入。就像凝聚层次聚类一样,DBSCAN不会为新数据进行簇分配,因此我们使用fit_predict方法一步完成聚类并获取簇分配。
参数设置注意事项
如果你的特定数据集没有正确设置eps和min_samples参数,可能导致DBSCAN返回的簇成员资格全部被分配标签-1,这表示噪声。基本上,eps设置隐式地控制了找到的簇的数量。

使用DBSCAN时,如果你已经使用StandardScaler或MinMaxScaler缩放数据以确保特征值具有可比范围,则更容易找到合适的eps值。最后一点需要注意的是,当你使用DBSCAN的簇分配时,请检查并适当处理-1这个噪声值,因为这个负值可能会引发问题,例如,如果簇分配稍后被用作另一个数组的索引。
聚类评估的挑战 ⚖️
与监督学习不同,在监督学习中我们有现有的标签或目标值可用于评估学习方法的有效性,而无监督学习算法通常很难自动评估,因为通常没有真实情况可供比较。
在某些情况下,例如在乳腺癌示例中,我们可能有现有的标签,可以通过比较数据点到簇的分配与同一数据点的标签分配来评估聚类的质量。但在许多情况下,标签并不可用。
此外,在聚类的情况下,存在模糊性,因为通常可以有多个看似合理的聚类分配给给定的数据集,除非我们有额外的标准(例如在具有客观评估基础的特定应用任务上的性能),否则没有一个明显优于另一个。例如,在聚类结果用作监督学习的特征的情况下,我们可以使用添加这些基于聚类的特征所带来的整体分类器准确率提升,作为底层聚类成功与否的衡量标准。
评估聚类算法的另一个问题是,很难自动解释或标记所找到簇的含义。这仍然是一个需要人类专业知识来判断的步骤。
总结 📝
在本节课中,我们一起学习了三种核心的聚类算法。我们首先介绍了K均值聚类,它通过迭代寻找簇中心来划分数据,适合处理球形、分离良好的簇,但需要预先指定簇数K。接着,我们探讨了凝聚层次聚类,它通过自底向上合并最相似的簇来构建层次结构,并可通过树状图可视化,适合探索数据的自然分组层次。最后,我们学习了DBSCAN密度聚类,它基于数据点的密度分布来发现任意形状的簇,并能有效识别噪声点,无需预先指定簇数。
每种方法都有其适用场景和局限性:K均值简单高效但对初始值和数据尺度敏感;层次聚类能提供丰富的层次信息但计算成本较高;DBSCAN能处理复杂形状和噪声但对参数设置敏感。在实际应用中,需要根据数据特性和分析目标选择合适的算法,并理解评估无监督学习结果通常需要结合领域知识进行判断。
92:机器学习基础与实践 🎯

在本课程中,我们共同探索了机器学习的基础概念、核心算法及其在实际问题中的应用。通过理论讲解与实践练习,我们旨在为你构建一个坚实的起点,帮助你理解这一快速发展的领域,并激发你进一步深入学习的兴趣。
课程内容回顾 📚
通过本课程涵盖的多样主题,你可能已经了解到,机器学习领域包含了一系列广泛的思想与方法,并且该领域本身正在飞速发展。
因此,像本课程这样的概论课,其目标是覆盖最重要的基础概念,并涉足一些关键且有趣的领域,希望能在你完成课程后,激发你继续探索的热情。
我精心设计了这门课程,使其不仅是一个讲座和作业的资源库。
同时,它也是一个精选的阅读材料集合。这些材料基于我在该领域的经验挑选而成,可以作为你在机器学习不同方面继续学习的良好起点。
核心学习成果 ✨
到目前为止,我们已经覆盖了相当多的内容。
首先,我们从理解应用型机器学习的一般概念和工作流程开始。
随后,我们接触了用于分类和回归等重要任务的各种学习算法。
同时,我们也理解了控制这些算法模型复杂度的关键参数。
现在,你应该对过拟合和数据泄露等关键问题有了更深入的理解。
并且,你也掌握了一些可用于检测和避免这些问题的策略。
我们探讨了如何评估机器学习算法,以及如何调整其参数,以优化适用于不同任务的各种评估标准。
最后,我们借助Notebook示例和作业应用了这些理念。
从而在一个非常强大的Python机器学习库——scikit-learn中获得了更多实践经验。
延伸学习方向 🔮
我想简要提一下我们在本课程中涉及但未重点关注的另一类重要机器学习任务,即涉及文本的机器学习。
这包括诸如电子邮件垃圾邮件检测和按主题对网页进行分类等问题。
我们数据科学系列中紧随本课程的下一门课,将专门聚焦于文本挖掘和文本机器学习。
因此,如果你对结合机器学习进行自然语言处理有特别的兴趣,我鼓励你去学习那门课程。
结语与致谢 🙏
感谢你对本课程的兴趣。我非常享受这次带领大家游览迷人机器学习领域的旅程。
我希望本课程能以某种方式,在你可能选择的任何探索道路上对你有所帮助。
一如既往,我们欢迎你针对本课程的任何方面提出反馈。
谢谢你,并祝愿你在未来的学习中一切顺利。
本节课中我们一起学习了:机器学习的基础工作流程、核心算法(如分类与回归)、模型评估与调参、以及使用scikit-learn进行实践。我们认识到机器学习领域的广阔性,并为后续在文本挖掘等方向的深入学习奠定了基础。


浙公网安备 33010602011771号