ScipyCon-2017-笔记-全-
ScipyCon 2017 笔记(全)

课程 P1:Pandas 数据分析入门教程 🐼
在本课程中,我们将学习如何使用 Python 的 Pandas 库进行数据分析。Pandas 是一个强大的工具,可以看作是支持异构数据的 NumPy,或者类似于 R 语言中的数据框。它提供了一种编程方式来操作类似 Excel 表格的数据。
准备工作与数据加载
首先,请确保您已下载课程资料库。资料库中包含本课程将使用的所有数据集和讲义。
为了开始工作,我们需要导入 Pandas 库并加载数据。通常,我们会为 Pandas 设置一个别名 pd 以方便使用。
import pandas as pd
接下来,我们使用 read_csv 函数来加载数据。如果数据是制表符分隔的(TSV文件),我们需要指定分隔符参数。
df = pd.read_csv('../data/gapminder.tsv', delimiter='\t')
加载数据后,我们可以使用 head 方法查看前几行,以确认数据加载正确。
df.head()


要了解数据集的整体结构,我们可以使用 shape 属性获取行数和列数,或使用 info 方法查看每列的数据类型和非空值数量。



df.shape
df.info()
数据子集选择:行与列
上一节我们介绍了如何加载和查看数据,本节中我们来看看如何从数据框中选择特定的行和列。
选择列
选择单列有两种常见方法:使用方括号或点号表示法。点号表示法更简洁,但要求列名不包含空格。


# 方法一:方括号
country_df = df['country']


# 方法二:点号表示法(仅适用于无空格列名)
country_df = df.country
如果需要选择多列,则需要提供一个列名列表。
subset = df[['country', 'continent', 'year']]
subset.head()
选择行

选择行主要使用 loc 和 iloc 属性。loc 通过行标签选择,而 iloc 通过行位置(索引)选择。



# 使用 loc 选择标签为 0 的行
row_loc = df.loc[0]
# 使用 iloc 选择位置为 0 的行(第一行)
row_iloc = df.iloc[0]
# 使用 iloc 选择最后一行
last_row = df.iloc[-1]
我们还可以同时选择特定的行和列。
# 选择第 0, 99, 999 行,以及 'country', 'lifeExp', 'gdpPercap' 列
subset = df.loc[[0, 99, 999], ['country', 'lifeExp', 'gdpPercap']]
布尔子集选择
布尔子集选择允许我们根据条件筛选行。例如,筛选出寿命期望高于平均值的所有行。
life_exp_mean = df['lifeExp'].mean()
above_average = df.loc[df['lifeExp'] > life_exp_mean, :]
分组与聚合统计



在查看了如何选择数据子集后,一个常见的分析任务是计算分组统计量。Pandas 的 groupby 功能非常强大。
分组操作遵循“拆分-应用-合并”的模式:先将数据按指定键拆分成组,然后在每个组上应用函数,最后将结果合并。
例如,计算每年的平均寿命期望:


# 按年份分组,计算 lifeExp 列的平均值
lifeExp_by_year = df.groupby('year')['lifeExp'].mean()
lifeExp_by_year.head()
我们可以对多个列进行分组和聚合。
# 按年份和大陆分组,计算 lifeExp 和 gdpPercap 的平均值
grouped = df.groupby(['year', 'continent'])[['lifeExp', 'gdpPercap']].mean()
grouped.head()
分组后,结果可能是一个具有多层索引的 Series 或 DataFrame。我们可以使用 reset_index 方法将其展平为普通的 DataFrame。
flat_df = grouped.reset_index()


数据可视化与保存
Pandas 内置了简单的绘图功能,可以快速可视化数据。例如,绘制平均寿命期望随年份变化的趋势图。
import matplotlib.pyplot as plt
%matplotlib inline
lifeExp_by_year.plot()
plt.title('Average Life Expectancy Over Time')
plt.ylabel('Life Expectancy')
plt.xlabel('Year')
plt.show()
分析完成后,我们通常需要将结果保存到文件。Pandas 提供了 to_csv 等方法。
# 将展平后的 DataFrame 保存为 CSV 文件,不保存行索引
flat_df.to_csv('../output/life_expectancy_by_year.csv', index=False)
总结
在本课程中,我们一起学习了 Pandas 数据分析的基础知识。我们从数据加载和查看开始,然后学习了如何选择数据的行和列,包括使用 loc、iloc 和布尔索引。接着,我们探索了强大的分组聚合功能,使用 groupby 来计算分组统计量。最后,我们简要介绍了如何使用 Pandas 进行数据可视化以及如何将结果保存到文件。

掌握这些基础操作是进行更复杂数据分析的第一步。Pandas 的功能远不止于此,但这些核心概念将为您后续的学习打下坚实的基础。
课程 P10:Jupyter交互式控件生态系统 🎛️
在本课程中,我们将学习Jupyter交互式控件(Widgets)的核心概念、使用方法以及如何利用它们来创建高度交互的数据分析和可视化工作流。我们将从基础概念开始,逐步深入到控件创建、样式布局、事件处理,并最终探索基于此生态系统的强大可视化库。
概述:控件的核心理念 💡
我的名字是Matt Craig,我在明尼苏达州立大学物理与天文学系任教。今天和我一起的还有Jason Grout(在Bloomberg工作)和Sylvain Corlay(在Quantstack工作)。我们还有几位助手:Mark和Paul。
首先,请确保您获取了教程Notebook的最新版本。如果打开索引文件时,顶部从零介绍开始,那么您就拥有了最新版本。
控件的核心理念是让Notebook中的交互式计算变得更加互动。为了说明这一点,我们来看一个非常简单的例子:计算一个数的平方。
最初,我们可能通过手动输入 9 * 9 来计算。然后为了探索,我们尝试 10 * 10,11 * 11。虽然我们开始注意到一些模式,但反复输入数字变得非常繁琐。
因此,工作流中的下一步通常是编写函数来自动化这个过程。我们可以定义一个打印数字平方的函数 f(x)。这样,工作流就变成了 f(9),f(10),f(11)。这比之前快了一些,但仍然不够快。
原因在于,当我们执行代码时,整个流程的瓶颈在于“输入代码”这个环节。特别是对于简单的工作流,输入占用了大量时间。不仅如此,输入行为还会将你从分析数据和思考问题的高层次认知中拉下来,陷入打字的机械操作中。当你得到输出后,又需要重新回到高层次去理解结果的意义。这种上下文切换带来了摩擦,降低了效率。
Notebook和内核并没有拖慢我们,是我们自己(的输入界面)拖慢了自己。这正是控件大放异彩的地方。我们想要让这个循环变得和你思考一样快,而不是和你打字一样快。
IPython控件提供了一个简单的 interact 函数。例如,interact(f, x=(0, 100))。执行后,一个GUI控件会自动为我们创建。当我们改变这个控件时,函数会自动在每次参数值变化时运行。这样,我就可以非常快速地探索参数空间(81, 100),真正地以思考的速度来解决问题。这就是IPython控件的天才之处和应用场景。
我们拥有许多不同类型的控件。如果系统没有自动猜测出合适的控件类型,你可以创建特定的控件,例如一个浮点数滑块。你可以精确控制我们构建的控件类型。实际上,我们展示的控件共享同一个模型,但可以多次显示,并且它们是链接在一起的。这只是同一个控件的两个不同视图。
我们可以在后端(内核端)询问控件的值。例如,我可以从用户那里获取输入,然后从Python端询问那个值是什么。我也可以基于该值的变化来触发其他操作。每当滑块变化时,我可以运行任何想要的Python代码。
我还可以将两个控件链接在一起。例如,将文本框与滑块链接,当我移动滑块时,文本框会自动更新。
随着课程的深入,我们会更详细地探讨这些内容。这里只是对控件的一些能力和核心理念做一个简要概述。
控件不仅仅是我们提供的一套控件(如滑块、文本框等),它更是一个在Notebook中编写交互式控件的框架。基于此,有许多库(如BQplot、ipyleaflet、pythreejs、ipyvolume等)使用相同的基础设施来提供和构建交互式控件。
这些库提供了自定义控件:BQplot用于2D图表,ipyleaflet用于构建可交互的地图,pythreejs用于3D绘图,ipyvolume也用于3D绘图。同样,这些库中的每一个元素都是用户可以与之交互的,并且会将消息发送回内核,让内核端的代码响应用户的交互。
重申一下,核心理念是:我们希望让你的计算保持在高级别,并消除繁琐的输入过程。同时,我们也希望提供一个框架,让人们能够编写任意复杂度的GUI控件,并将它们连接到Python端,这样你就可以在Notebook端与高级控件交互,而Notebook端的任何交互都会自动发送到Python端,在那里函数可以运行,你可以调查用户的操作,并基于前端的用户交互触发任何类型的操作。这是一个非常强大的库。
接下来,让我们进入 interact 函数的介绍部分。
第一部分:interact 函数入门 🚀
上一节我们介绍了控件的核心理念,本节中我们来看看最常用的控件生成函数 interact。
首先,让我们了解一下听众的背景。有多少人以前使用过Notebook?很好,几乎是所有人。有多少人使用过控件?哦,太棒了。有多少人编写过控件?我们甚至有一些这样的人,在讲到更复杂的部分时,我们可能会请你帮助周围的人。有多少人可以说你广泛地使用过控件?好的。有多少人主要使用过 interact?有多少人实际使用过控件本身(即不是通过 interact,而是直接构建浮点滑块并连接等)?好的,我们对听众有了更好的了解。
当你想要使用控件时,首先要问自己的问题是:是否需要自己编写任何控件代码。interact 提供了一种无需编写太多控件代码就能生成控件的方法。
让我们跳转到相应的Notebook。索引中的概述部分包含了一些我们在介绍中提到的包的引用和链接。
如果我们切换到 interact 笔记本,我希望大家花几分钟时间,完成笔记本中的内容,直到到达“使用 fixed 固定参数”这一部分。
看起来几乎每个人都使用过Jupyter Notebook,按 Shift+Enter 可以执行单元格中的代码。前几个单元格将定义一个函数,并引导你了解使用 interact 可以生成的不同类型的控件。
请大家花几分钟时间完成这个部分。当你到达“使用 fixed 固定参数”这一点时,请在你的电脑上贴一张便签,以便我们了解大家的进度。
当你到达这个标题“使用 fixed 固定参数”时,请在你的电脑上贴上便签,让我们知道你到达了那里。
好的,我们都到了。正如你所看到的,生成的控件类型由参数的类型决定。如果你提供了一个默认参数,并且你有一个带有几个变量的函数,希望其中一个生成控件,另一个保持固定,请使用 fixed。
例如,我教宇宙学课程时,有一个使用 interact 的例子,他们将宇宙的宇宙学模型拟合到超新星数据,在某个阶段,固定哈勃常数的值会很有用。
到目前为止,我们所做的实际上是一种生成控件的简写方式。当你将 x=10 作为关键字参数输入函数时,IPython控件在底层会将其转换为一个整数滑块,最小值为你输入值的负数,最大值为该值的三倍,步长为1。如果你愿意,你也可以显式地提供滑块,我们稍后会讲到这一点。
下一个笔记本(可能是接下来的几个)将逐一介绍所有的控件。为了方便参考,我们提供了一个表格,列出了几种不同的参数输入方式,以及每种参数类型会被转换成什么类型的控件。
现在,让我们继续完成笔记本,直到“禁用连续更新”部分。同样,如果你已经到达那里,请放下你的标志(便签),完成后再把标志拿起来。当我们到达那一点时,我会看看你们是否有任何问题。
我们将在接下来的几个笔记本中继续解决安装问题。
你可能已经注意到,在尝试使用 interact 滑块时,它们往往会闪烁。让我向上跳转几个单元格。我定义的函数将其输入值乘以三。所以当苹果变成三时……(开个玩笑,我是老师,我喜欢人们带苹果来)。
让我们向下滚动到绘图部分。根据你的笔记本电脑性能,闪烁现象可能会很明显,或者即使不闪烁,也会相当滞后。
有几种方法可以禁用连续更新。你可以做的两件事是:一是添加一个按钮,这样直到用户按下按钮时,interact 才会运行。你可以使用 interact_manual 或 interactive 来实现。使用 interactive 的语法有点特别,你必须将一个字典作为第二个参数传递给 interactive,其中包含特定的键值对,但效果是一样的。
你也可以在滑块上设置 continuous_update=False。如果你这样做,那么在你松开滑块之前什么都不会发生。这样做的好处是使浏览器中的动画看起来更流畅。缺点是,如果你正在使用类似的功能让某人探索线条斜率变化时如何变化,拥有交互性是非常好的。在这个具体案例中,如果我试图优化,我可能会在斜率中添加一个步长,这样在我拖动滑块时就不会经历那么多变化。
这里有一个简短的练习。关于使用 interactive(它会生成一个存储在变量中的控件对象,而不是像 interact 那样直接显示)的一个好处是,你可以在Notebook后面修改这个控件。所以这里的任务是修改这个交互式绘图,使得滑块只在松开时才更新。我已经为你修改了其中一个滑块,请填写第二个。给你大约一分钟时间,这应该不需要很长时间。如果你仍然遇到安装问题,请举手,我们会提供帮助。
有人有问题吗?同样,一旦你更新了绘图控件,请在电脑上贴上便签。
好的,第二行代码很简单。我们只需修改第二个控件(即第二个滑块),然后更新就会按我们想要的方式工作。
这里有一个链接指向一个示例。我们不会深入探讨其他示例,但在IPython控件源代码仓库中,有几个使用 interactive 的其他示例。
现在,让我们继续学习控件的基础知识。这部分我将和大家一起过一遍。
第二部分:控件基础 📚
上一节我们学习了 interact 函数,本节我们来深入了解控件对象本身的基础知识。
和Notebook中的许多其他东西一样,控件有一个表示形式,允许它们直接显示在Notebook中。因此,你可以多次显示同一个控件。如果我拖动其中一个控件,另一个也会改变。这是因为在底层,在Python内核中,我有一个控件对象;在JavaScript中,也有该控件的模型。每当我显示这个控件时,我都是在生成同一个对象的另一个视图。你还可以使用 close() 方法关闭它们。
几乎所有的控件都有一个 value 属性。如果我拖动滑块,然后再次打印它的值,变量的值会反映出来。我也可以通过编程方式设置这个值。
对于每个控件,都有一个键的列表。这些是同步的属性,如果你设置它,会影响GUI;如果你改变GUI,控件的属性也会相应改变。不同控件之间的属性有所不同。稍后今天我们会向仓库中添加一个我整理的表格,该表格为每个控件类列出了包含哪些属性的矩阵。但几乎所有的控件都会有 value 属性(我们会在几个笔记本后遇到一个例外)。
你可以在初始化控件时设置其值,也可以在之后设置(我们上面做了一个例子)。
有几种不同的链接控件的方法。你可以在Python端使用 link 链接,也可以在JavaScript端使用 jslink 链接。在这个特定的例子中,我们使用的是 jslink,我们稍后会再讨论这两者之间的区别。
我更新滑块,文本框移动;如果我在文本框中输入,滑块移动。如果你创建了一个链接后又想断开它,你可以这样做。现在它们又是独立的控件了。
这为接下来几个小时我们要做的事情奠定了基础:逐一学习控件列表,学习如何组合控件。我们将不再使用 interact。
现在让我们继续学习控件列表。
第三部分:控件列表 📋
上一节我们介绍了控件的基础属性,本节我们将系统地浏览所有可用的控件类型。
与其我读给你听,不如让你花大约十分钟时间自己运行一遍控件列表。如果你完成得很快,下面有几个练习建议。大约五到十分钟后,我会打断你们,因为中间有几件事我想讨论一下。但我认为让你了解不同对象是什么的最快方法就是亲自尝试一下。
所以开始运行吧。
关于单选按钮示例的问题:描述在样式笔记本中被截断了,我们接下来会讨论这个问题。默认情况下,标签有固定的宽度,这样如果你使用相对较短的标签,一切都会对齐得很好。但如果你想要更长的标签,也是可以的。
还有其他问题吗?
关于图标列表的问题?我稍后会把它加进去。
其他问题?
我想再花一两分钟在这个上面。看起来还不是每个人都完成了。如果你能快速执行完剩下的单元格。
关于DOM更新导致的闪烁问题?看起来像是空白,没有数字,所以看起来是向上的。这意味着你可以告诉DOM不要更新。你可以关闭模型中的某些东西。是的,这是后台的MOP之一。
好的,我想提醒大家注意控件列表中的几件事。
一是字符串有几种不同的表示方式。Text 和 Textarea 都用于输入,唯一的区别是你可以输入字符的空间大小。Label 最初是作为一种在控件上编写自定义描述的方式。所以,如果你的控件描述被截断了,一种处理方法是组合一个 Label 控件和一个其他控件。
HTML 控件允许你显示格式化的文本,Math 和 Label 控件可以接受LaTeX并自动将其转换为排版方程。
Output 控件是版本7中相对较新的添加。它可以接受任何你可以在Notebook中显示的内容,并将其放入一个控件中。我们将在教程末尾看到一个例子,我使用它将别人编写的图像查看器嵌入到Notebook中。但本质上,任何你以前能在Notebook中显示的东西,有时要把它放进控件里有点麻烦,Output 控件为你做到了这一点。
随着我们继续,容器控件将变得很重要。当你编写自己的控件时,几乎总是从 Box、HBox 或 VBox 子类化,这取决于你想要的布局,并将你想要的部件放入那个盒子中。
还有其他问题吗?关于控件列表还有什么问题吗?
为什么有 IntWidget 和 FloatWidget,而不是只有一个 NumericWidget(无论是用于滑块还是文本框)?这实际上可能很快就会改变,以便拥有一个 NumberWidget。没有真正的原因,我可以说,主要是历史原因。它们基本上共享实现,不同之处在于读数格式的默认值,对于 IntWidget 不会去除任何数字。
Box 和 HBox 之间没有太大区别,我认为它们实际上是相同的。历史上,在早期版本的包中,HBox 和 VBox 实际上不是类,而是函数,它们返回一个实例,所以是有区别的。
接下来,我们……哦,Ty,请说。
关于进度条和其他一些元素,你可以设置 bar_style。对于浅蓝色的那个,bar_style 是 'info';对于深蓝色的那个,没有设置 bar_style。如果你查看 IntProgress,它会列出你可以使用的不同样式。如果我把它从 'info' 改为 'warning',我会得到黄色而不是浅蓝色。
其他问题吗?
是的,问题是:是否有所有可用样式的列表?对于像 bar_style 这样的特定样式,是否有选项列表?你可以检查控件。对于这个,IntProgress 的 description 是一个 trait 类型,可能有一个预定义的列表,所以这有点风险。实际上,那是 bar_style。你可以通过另一种方式获取。哦,它们以前是有效的。你只需将其设置为一个错误的值,它就会告诉你有效值是什么。我有一个尚未放入教程的笔记本,其中列出了哪些控件有哪些可用的样式。例如,对于一些滑块控件,有一个你可以设置的 handle_color。有几个控件具有这样的样式属性,原则上你可以用 Tab 键发现,但这……我已经基本整理好了表格,稍后我会把它添加到笔记本中。
我看到,所以让我们把它变成一个字符串。
继续前进。我们这里有一组练习。我们计划在10分钟后或2:30休息,所以你可能没有时间完成所有这些练习。对于每个练习,让我展示一件事。对于每个练习(除了最后一个),我们确实有示例解决方案。所以,如果你取消注释带有 %load 的那一行(它以 %load 开头),然后按 Shift+Enter,它会用我们的解决方案替换单元格中已有的内容。所以,如果你更喜欢直接加载解决方案看看它们是如何工作的,请继续这样做。如果你尝试了一段时间后卡住了,可以加载解决方案;或者如果你没有时间完成,想稍后再回来做,也可以这样做。
让我们花大约10分钟时间来做这些练习,我们会继续四处走动回答你们的问题。
实际上,我要再打断你们一次,因为昨天它让我困惑了好几次。在代码加载到单元格之后,你需要再次运行该单元格才能实际运行代码。
有些人,房间里的多个人,在安装时遇到了问题。问题是他们在用户环境中有一个预先存在的控件安装,并且他们进行了用户安装。实际上,用户安装对所有环境都是全局的,并且优先于你可能正在运行的任何环境,因此最终导致你的环境所需的JavaScript与你在浏览器中加载的JavaScript不匹配。所以,请永远不要使用 --user 或类似选项安装Notebook扩展。是的,用户安装是邪恶的,不要使用它。如果你曾经这样做过,并且你运行的是Linux或任何形式的Unix,你可以检查主目录下的 ~/.local/share/jupyter 目录,并删除所有内容。这解决了很多麻烦。将来安装Notebook扩展或JupyterLab扩展时,一个好的建议是使用 --sys-prefix 参数将其安装到当前运行的环境中,这将使其成为该环境本地的。我认为这实际上应该成为默认设置,并且可能会在Jupyter的下一个版本中成为默认设置。
问题是:如果我想在多个环境中安装同一套Notebook扩展和控件扩展,该怎么办?我的回答是:不要这样做,而是在每个环境中分别安装它们。但你可以全局启用它们,使用 --user 会不那么邪恶。或者另一件事是定义一个环境,并让每个人都使用相同的清单,即声明你的环境的依赖项。
在我们短暂休息之前还有其他问题吗?
你能用装饰器禁用连续更新吗?你必须调用函数吗?答案是否定的,你不能使用装饰器,我想这就是答案。
让我们休息10分钟。现在我的时钟是2:33,所以我们将在2:43重新开始。走廊里应该有零食。
第四部分:样式与布局 🎨
上一节我们完成了控件列表的学习,本节我们将专注于控件的样式和布局,这是构建复杂界面的关键。
在开始之前,基于帮助房间里的人的经验,有几点注意事项。
第一,强烈、强烈、强烈建议你遵循当前的安装说明。特别是,在安装所有这些包之前创建一个新环境。我们在房间里看到的很多问题都与控件版本不正确或包未正确安装等有关。很多问题仅仅是因为没有遵循说明或遵循了旧的说明集。所以强烈建议你遵循说明创建一个新环境,并安装IPython控件的预发布版本等。

另一个注意事项是,你需要在 widgets-2017 环境中启动你的Notebook服务器。原因是Notebook服务器提供JavaScript,它与后端的IPython控件版本相匹配。你希望这个JavaScript是预发布版本的JavaScript。所以请在 widgets-2017 环境中启动Notebook服务器。

问题?假设你正在尝试创建一个广泛分发的Notebook,如何确保我们有正确版本的包等?是的,这是一个非常广泛的问题,关于如何确保你的目标受众安装了正确的软件等。我在学术环境中教学,你可能可以回答这个问题。当我在学术环境中时,你知道,在学年开始时,这是我们的环境,让每个人都设置到同一个环境,然后这就是我们全年坚持使用的环境。就像我们在这个教程中所做的一样:这是一套安装说明,使用这个。这就是我们测试的依据。关于分发还有其他意见吗?对我来说,问题在于我想做一次还是多次。如果是一次,beta.mybinder.org 或类似的服务可以让你设置一个云托管环境。我们设置一个
课程 P11:使用 Python 与 Tableau 构建交互式美观的数据可视化 📊
在本节课中,我们将学习如何结合 Python 的强大分析能力与 Tableau 的便捷可视化功能,来创建引人入胜且交互性强的数据报告。我们将重点介绍一个名为 TabPy 的工具,并探讨三个核心设计原则,以确保你的可视化作品能被用户反复使用。
引言:可视化面临的挑战
作为一名数据分析师,我热衷于创建数据可视化,以便利益相关者使用数据、理解数据并基于数据做出业务决策。
但有一件事一直让我感到困扰和沮丧。
我创建了许多数据可视化,希望我的利益相关者能参与其中并经常使用。但我也很好奇他们实际使用了多少次我的可视化报告。
当我查看服务器数据时,我注意到很多仪表板只有一次浏览记录。这意味着在我创建并分享可视化报告后,他们可能只使用了一次就再也没有回来。
因此,在本节课中,我将分享一些有趣的数据可视化技巧,帮助你创建真正具有吸引力的可视化作品,确保你的利益相关者会反复使用它。
第一部分:什么是 TabPy?🔗
上一节我们提到了创建可视化报告的挑战,本节中我们来看看一个能结合 Python 与 Tableau 优势的强大工具。
TabPy 本质上允许你在 Tableau 中非常方便地使用 Python。因此,你能够利用 Python 丰富的分析库来创建功能强大的可视化。
以下是几种常用可视化工具的优缺点分析:
- Python:主要用于内部探索性分析,因为它拥有强大的分析库(如情感分析、聚类、机器学习)。它提供了出色的可视化库,如
matplotlib、seaborn、Bokeh。- 优点:分析能力强,库丰富。
- 缺点:默认可视化样式可能不够美观;在 Jupyter Notebook 中创建的报告对非技术用户不够友好。
- D3.js:常用于面向公众的可视化,因为它非常交互式、用户友好且视觉冲击力强。
- 优点:交互性强,视觉效果好,体验类似网页。
- 缺点:代码量大,创建简单图表也很复杂,不适合探索数据。
- Tableau:介于两者之间。它是一个拖放式可视化工具,可以非常快速、轻松地创建可视化。
- 优点:易于使用,创建速度快,界面对业务用户友好。
- 缺点:分析功能相对较弱,数据准备能力有限。
Python 拥有强大的高级分析工具,而 Tableau 是进行数据可视化的便捷方式。TabPy 正是将这两大优势结合起来,允许你在 Tableau 内部使用 Python 库来创建可视化。
TabPy 的工作原理
如果你熟悉 Tableau 界面,你可以在 Tableau 的计算字段中直接编写 Python 代码。代码会被传递到外部的 TabPy 服务器执行,并将输出结果直接返回给 Tableau。你不再需要在 Tableau 外部单独运行 Python。
代码示例:在 Tableau 计算字段中使用 Python 进行情感分析
# 这是一个简单的Python代码片段,可在Tableau计算字段中运行
# 假设返回情感分析得分
return sentiment_score
此外,通过 TabPy,你还可以直接部署和调用机器学习模型。
资源:如果你对 TabPy 感兴趣,可以访问其 GitHub 页面获取安装和使用的详细文档:github.com/tableau/TabPy

第二部分:三大设计原则 ✨

现在你已经学会了如何创建功能强大的可视化,接下来我们看看如何设计出吸引人的可视化,让你的观众愿意反复使用。
我将介绍三个核心设计技巧:
- 属性:使用哪些视觉属性来引导用户浏览信息。
- 受众:如何识别正确的受众并为其选择合适的可视化类型。
- 关联:使用哪些视觉线索来帮助用户记忆你的可视化。
1. 属性:引导用户注意力
当你在可视化中展示大量信息时,观众很难在一两秒内处理所有内容。
因此,你必须利用前注意属性来引导观众的注意力,让他们聚焦于你认为重要的部分,而不会被信息淹没。
以下是你可以使用的一些前注意属性:
- 不同形状
- 不同色调
- 不同大小
通过有策略地使用这些属性,你可以让观众轻松地注意到关键信息。
2. 受众:了解你的观众
创建数据可视化就像与朋友建立关系。你必须真正了解你的朋友,才能建立良好的关系。同样,你必须真正了解你的受众,才能为他们创建合适的数据可视化。
在工作场所,你需要为不同类型的受众创建可视化:
- 高管和经理:他们非常忙碌,没有太多时间处理数据。因此,创建解释性可视化至关重要,让他们能在一秒钟内获得你希望传达的见解或故事。这更像是静态报告。
- 分析师和产品经理:他们通常希望查看原始数据,与数据互动并进行探索。在这种情况下,你需要为他们创建更像分析工具的可视化。
- 公众:介于两者之间。你可以创建带有故事性的信息图,也可以创建高度交互的可视化供他们探索或获取灵感。
在创建可视化之前,必须考虑受众的类型。

3. 关联:利用视觉线索增强记忆

关联是可视化中一个非常重要的概念,它能帮助你记忆和识别可视化。
研究表明,在可视化中使用强烈的视觉关联可以极大地帮助人们记忆。
你可以在可视化中使用以下类型的视觉线索来增强关联性:
- 照片
- 标注
- 趋势线
- 图标
这些元素视觉冲击力强,易于让大脑关联和记忆。例如,在关于“鸡肉消费”的可视化中使用鸡肉的图片,在关于“酒精”的可视化中使用啤酒瓶的形状,都能让主题更容易被记住。
第三部分:实战演示 🚀
上一节我们介绍了三大设计原则,本节我们将通过两个实例,结合 TabPy 和这些原则来创建数据可视化。
我今天有两个例子:
- 在 TabPy 中使用 Python 脚本查询数据。
- 在 TabPy 中调用已部署的模型。

示例演示

本例的受众是 Airbnb 的产品经理。作为产品经理,他们希望提供更好的用户体验。因此,识别哪些房源持续获得较低的情感评分及其原因非常重要。基于此,我为他们创建了一个数据可视化来探索数据并发现问题。
在这个 Tableau 可视化中,我应用了设计原则:

- 属性:我使用了醒目的黄色来高亮显示有 292 个房源持续获得低情感评分,首先吸引观众的注意力。接着引导他们查看这些房源在旧金山地图上的位置分布,然后是底部显示情感评分随时间变化的柱状图(观察季节性),最后才是按原因细分的详细信息(用较小的白色区域表示)。
- 关联:在顶部,我使用了一些图标作为视觉关联,让用户可以轻松地识别和交互。
- 交互:我创建了适当的菜单,鼓励用户与数据进行更多互动和探索。
通过结合 TabPy 的计算能力和精心的设计,我创建了一个既能提供深度分析,又对用户友好、易于理解和记忆的可视化仪表板。

总结 📝

在本节课中,我们一起学习了:
- TabPy 工具:它如何桥接 Python 的分析能力与 Tableau 的可视化便捷性,让你能在 Tableau 内部直接运行 Python 代码。
- 三大设计原则:
- 属性:利用颜色、形状、大小等前注意属性引导用户视线。
- 受众:根据受众类型(如高管、分析师、公众)选择创建解释性报告或探索性工具。
- 关联:使用图片、图标等强烈的视觉线索,帮助用户更好地记忆和识别你的可视化。
- 实战应用:看到了如何将这些原则与 TabPy 结合,为一个具体业务场景(分析 Airbnb 房源评价)构建交互式、有重点且易于记忆的数据仪表板。

希望你能将这些概念应用到自己的数据可视化项目中,创建出不仅功能强大,而且真正吸引人、能被用户反复使用的出色作品。
课程 P12:Python 中的现代优化方法 🚀
在本节课中,我们将学习 Python 中的现代优化方法。优化是科学和工程中广泛使用的工具,但传统的优化工具栈自计算机诞生以来变化不大。近年来,随着并行计算和非线性问题处理能力的发展,优化方法正朝着处理大规模物理、工程和化学问题的方向演进。我们将从优化基础开始,介绍一些现代方法,并重点讲解 Mystic 包的使用。
优化基础介绍
上一节我们概述了课程内容,本节中我们来看看优化的基本组成部分。优化通常涉及一个目标函数或成本函数。你可以将其想象成一个表面,优化的目标是找到这个表面上的最低点(最小值),无论是局部最小值还是全局最小值。

在回归分析中,优化用于最小化数据点与拟合线之间的距离。在分类问题中,优化则是为了找到能最好地区分不同组别的边界线。本质上,优化就是理解目标函数并试图找到其最小值。
以下是优化问题的标准形式:
def objective(x):
return 1.3*x**2 - 4*x + 6
我们通常使用优化器(如 Nelder-Mead)来寻找最小值。优化器将目标函数和一个初始解向量作为输入,然后尝试“滚下山坡”找到最低点。
from scipy.optimize import minimize
result = minimize(objective, x0=[0.0], method='Nelder-Mead')

然而,优化器通常是一个“黑盒”。你提供输入,它返回一个结果,但中间过程是未知的。你无法轻易判断是否找到了真正的全局最小值,尤其是在高维或计算成本高昂的问题中。


应用约束:边界约束



上一节我们介绍了基本的无约束优化,本节中我们来看看如何通过约束来融入物理信息。最常见的约束类型是边界约束(或“盒子约束”),它将搜索限制在变量的特定范围内。

例如,我们可能只想在区间 [2, 4] 内寻找一阶贝塞尔函数的最小值。

from scipy.optimize import minimize_scalar
from scipy.special import j1
result = minimize_scalar(j1, bounds=(2, 4), method='bounded')
通过应用边界约束,优化器不会在指定区域之外进行搜索,这有助于缩小解空间并融入先验知识。
非线性测试函数与可视化
为了理解和测试优化器,我们经常使用标准非线性测试函数,如 Rosenbrock 函数。Mystic 包内置了许多这样的函数。
from mystic.models import rosenbrock
# 获取函数信息和绘图范围
print(rosenbrock.__doc__)
可视化目标函数的表面对于理解优化问题和诊断结果至关重要。对于低成本函数,我们可以绘制其三维图像来观察山谷和峰值。
局部优化与梯度信息
许多优化算法是局部优化器。它们从一个初始点开始,沿着梯度下降方向寻找最近的局部最小值。提供目标函数的导数(梯度)通常可以帮助优化器更快、更准确地收敛。
from scipy.optimize import minimize
from mystic.models import rosenbrock, rosenbrock_derivative
x0 = [0.5, 0.5, 0.5, 0.5, 0.5] # 5维初始猜测
result_without_deriv = minimize(rosenbrock, x0, method='Nelder-Mead')
result_with_deriv = minimize(rosenbrock, x0, jac=rosenbrock_derivative, method='BFGS')
然而,即使有梯度信息,局部优化器也可能陷入错误的局部最小值。对于复杂问题,通常需要从多个随机初始点运行优化器,并选择最佳结果。
传统约束方法:惩罚函数
上一节我们看了边界约束,本节中我们来看看另一种融入约束的传统方法:惩罚函数法。惩罚函数通过向目标函数添加一个项来工作,当约束被违反时,该项会显著增加目标函数值,从而在违反约束的区域形成“屏障”。
考虑一个带有约束的优化问题:
- 最小化目标函数:
f(x) = 2*x[0]*x[1] + 2*x[0] - x[0]**2 - 2*x[1]**2 - 约束条件:
x[1] >= 1x[0]**3 - x[1] == 0
在 scipy.optimize 中,我们可以使用 SLSQP 等支持约束的方法。
import numpy as np
from scipy.optimize import minimize
def objective(x):
return 2*x[0]*x[1] + 2*x[0] - x[0]**2 - 2*x[1]**2
def derivative(x):
return np.array([2*x[1] + 2 - 2*x[0], 2*x[0] - 4*x[1]])
# 约束定义
cons = ({'type': 'ineq', 'fun': lambda x: x[1] - 1},
{'type': 'eq', 'fun': lambda x: x[0]**3 - x[1]})
result_unconstrained = minimize(objective, [0, 0], jac=derivative, method='SLSQP')
result_constrained = minimize(objective, [0, 0], jac=derivative, constraints=cons, method='SLSQP')
惩罚函数和这类约束求解器(如线性/二次规划)通常要求目标函数和约束是线性或二次的,这对于高度非线性问题是一个限制。
优化方法概览与凸优化
SciPy 的 optimize 模块提供了从传统方法到现代方法的各种优化器。大多数是无约束的,而约束优化器往往局限于线性或二次规划。
对于严格的凸优化问题(目标函数和约束均为凸),可以使用专门的工具如 CVXOPT。它要求将问题表述为矩阵形式:
最小化: (1/2) * x.T * P * x + q.T * x
约束条件: G * x <= h
A * x = b
CVXOPT 的一个优点是它通常提供求解过程的诊断信息,例如目标函数值随迭代的变化轨迹,这增加了对解的置信度。
全局优化方法
对于存在多个局部最小值的问题,局部优化器可能不足。这时需要使用全局优化方法,如差分进化或遗传算法。
from scipy.optimize import differential_evolution
from mystic.models import rosenbrock
bounds = [(-10, 10)] * 5 # 5维问题,每维边界为[-10, 10]
result = differential_evolution(rosenbrock, bounds)


全局优化器的代价是通常需要大量的函数评估,计算成本很高。它们通过在整个搜索空间内随机“跳跃”来寻找更好的解,直到满足终止条件(例如,多次迭代后没有改进)。





特殊用途优化




优化还有许多特殊用途:
- 最小二乘拟合:用于参数估计和获取协方差矩阵(误差估计)。
from scipy.optimize import curve_fit - 整数规划:变量被限制为整数的优化,用于密码学等领域。
- 求根:寻找使一组方程等于零的解向量。
from scipy.optimize import root
优化诊断的挑战
传统上,验证优化结果是一项挑战,更像一门艺术而非精确科学。常见做法包括:
- 可视化解(对于低维问题)。
- 从多个随机起点运行优化器,选择最佳结果。
- 检查求解器提供的收敛轨迹日志(如果可用)。
- 对于拟合问题,检查协方差矩阵。



由于许多优化器是“黑盒”,缺乏内部状态信息,很难确定是否找到了全局最优解,或者求解过程是否正常终止。



引入 Mystic:更透明的优化



Mystic 库旨在解决传统优化中的一些痛点,提供更透明、可定制和诊断友好的优化环境。









与 SciPy 兼容的接口
Mystic 提供了与 scipy.optimize 类似的单行函数接口,易于上手。





from mystic import fmin
from mystic.models import rosenbrock

result = fmin(rosenbrock, [0, 0, 0], retall=True)
# retall=True 返回所有迭代的解向量,便于分析轨迹
回调函数与监控器
Mystic 允许在优化过程中插入回调函数,例如动态绘制参数变化或记录日志。


from mystic.monitors import VerboseLoggingMonitor



monitor = VerboseLoggingMonitor(1, 1) # 每1次迭代打印/记录
result = fmin(rosenbrock, [0,0,0], itermon=monitor)


日志文件可以通过 mystic 的日志阅读器进行可视化,绘制成本函数和参数随迭代的变化,直观判断收敛情况。

面向对象的求解器接口
对于高级定制,Mystic 提供了面向对象的接口。你可以创建求解器实例,并独立配置其各个部分:终止条件、种群初始化、变异策略等。

from mystic.solvers import DifferentialEvolutionSolver
from mystic.termination import VTR, ChangeOverGeneration
from mystic.monitors import VerboseMonitor



solver = DifferentialEvolutionSolver(ndim=9, npop=90) # 9维问题,90个个体
solver.SetRandomInitialPoints(min=[-100]*9, max=[100]*9)
solver.SetEvaluationLimits(generations=2000)




# 设置复合终止条件:达到目标值或长时间无改进
term = VTR(1e-8) | (ChangeOverGeneration() & VTR(0.0))
solver.SetTermination(term)


solver.Solve(rosenbrock)
solution = solver.bestSolution

灵活的初始点设置
你可以从任意概率分布中抽取初始点。
from mystic.math import Distribution
import numpy as np
dist = Distribution(np.random.normal, 5, 1) # 均值为5,标准差为1的正态分布
solver.SetSampledInitialPoints(dist)


核心特性:核变换与约束算子
Mystic 最强大的特性之一是能够使用“核变换”或“约束算子”来施加复杂的非线性约束。这与传统的惩罚函数不同。惩罚函数是在违反约束的区域修改目标函数表面(添加惩罚项)。而核变换是将整个搜索空间映射到满足约束的子空间上,优化器只在这个有效的子空间内工作。


这允许将复杂的物理或工程约束(如等式、不等式、微分约束)直接融入优化过程,大大提高了处理现实世界非线性约束问题的能力。




# 概念性示例:使用 mystic 的约束
from mystic.constraints import *

# 定义约束函数
def constraints(x):
x[0] = x[1]**2 # 例如,一个非线性等式约束
return x
# 创建约束算子
@with_constraints(constraints)
def constrained_objective(x):
return rosenbrock(x)
# 现在优化 constrained_objective,解将自动满足约束
总结



本节课中我们一起学习了 Python 中优化方法的基础和现代进展。我们从优化问题的基本定义开始,探讨了局部优化、全局优化以及传统的约束处理方法(如边界约束和惩罚函数)。我们指出了传统优化工具在诊断性、约束处理能力和透明度方面的局限性。



随后,我们深入介绍了 Mystic 优化库,它通过提供详细的求解监控、灵活的求解器配置、可定制的终止条件以及强大的核变换约束处理能力,旨在使优化过程更加透明和强大。Mystic 既兼容 SciPy 的简单接口,也提供了面向对象的高级接口,适用于从快速原型到复杂非线性约束问题求解的各种场景。



现代优化方法正朝着融入更多物理信息、利用并行计算以及提供更好诊断工具的方向发展,Mystic 是这一趋势中的一个代表性工具。
课程 P13:Python 中的并行数据分析 🚀
在本课程中,我们将学习 Python 中的并行编程。课程不会专注于某个特定工具,而是介绍通用的并行编程范式,帮助你理解在不同场景下应选择何种工具和范式。我们将从最简单的并行映射开始,逐步深入到更灵活的任务调度和大型数据集合处理。

概述
大家好,欢迎来到并行数据分析教程。我是 Matthew Rocklin,这位是 Aaron Amadea,这位是 Ben Zaitlin。今天我们将一起探讨 Python 中的并行编程。

今天的教程不针对任何特定工具,而是关于通用的并行编程和 Python。我们将介绍几种不同的编程范式,帮助你理解在何种情况下应使用何种范式或工具。



课程的前半部分主要在个人笔记本电脑上进行,后半部分我们将在 Google Cloud 上运行一个集群。首先由我讲解几个部分,然后由 Aaron 和 Ben 接手。如果你有任何问题,请随时举手示意,我们会提供帮助。


准备工作
在开始之前,请访问 GitHub 页面 github.com/pydata/paraltutorials,按照说明下载教程仓库。这将下载一系列 Jupyter Notebook 到你的机器上,我们将使用 Jupyter Notebook 服务器来完成课程。
课程的第一部分将使用你的本地机器。如果本地环境无法工作,可以立即切换到集群。课程的第二部分将使用集群,届时请访问指定链接并按提示操作。

我们还将使用 Slack 房间进行交流,我和其他讲师会留意其中的问题。

第一部分:并行映射
上一节我们介绍了课程的整体安排,本节中我们来看看最简单的并行编程范式:并行映射。


当你有一个函数和一批数据,并希望将该函数应用于数据的每一部分时,通常会使用 for 循环、列表推导式或 Python 内置的 map 函数。map 函数的好处在于,我们可以重写它以并行运行。许多编程框架都提供了并行版本的 map。
例如,concurrent.futures 模块提供了 ThreadPoolExecutor 或 ProcessPoolExecutor。我们创建一个执行器,向其传递函数和输入列表,它就会并行执行并返回输出列表。这是非常常见的模式,许多库都遵循这个系统。
以下是使用 ProcessPoolExecutor 进行并行映射的基本代码结构:
from concurrent.futures import ProcessPoolExecutor
def slow_increment(x):
# 模拟一些工作
time.sleep(1)
return x + 1
with ProcessPoolExecutor() as executor:
results = list(executor.map(slow_increment, range(10)))
实践练习:转换数据格式
我们首先生成一些模拟的股票数据。数据以 CSV 和 JSON 格式存储,每个文件代表一只股票的时间序列数据。我们的任务是将所有 JSON 文件加载并转换为更高效的格式(例如 HDF5)。由于每个文件的操作是独立的,这非常适合使用 map 进行并行化。
以下是顺序执行的代码:
import json
import pandas as pd
import glob
def convert_json_to_hdf5(filename):
with open(filename) as f:
data = json.load(f)
df = pd.DataFrame(data)
hdf5_filename = filename.replace('.json', '.h5')
df.to_hdf(hdf5_filename, key='df', mode='w')
file_names = glob.glob('data/*.json')
for fn in file_names:
convert_json_to_hdf5(fn)
使用性能分析工具 snakeviz,我们发现大部分时间花费在 json.loads 函数上。在考虑并行化之前,理解性能瓶颈非常重要。
你的目标:将上述顺序代码使用 ProcessPoolExecutor.map 进行并行化改写。
解决方案与性能分析
以下是使用 ProcessPoolExecutor 并行化的解决方案:
from concurrent.futures import ProcessPoolExecutor
import glob
def convert_json_to_hdf5(filename):
# ... 与之前相同的函数体 ...
file_names = glob.glob('data/*.json')
with ProcessPoolExecutor() as executor:
results = list(executor.map(convert_json_to_hdf5, file_names))
在四核机器上,并行版本运行时间从约13秒减少到约4.7秒,获得了近2倍的加速。但请注意,并行化并非总是最佳方案。我们发现 json.loads 是主要瓶颈,因此也可以尝试使用更快的 JSON 库(如 ujson)来加速顺序执行。结合两者(使用 ujson 和并行化)可以获得最佳性能。

第二部分:使用 Submit 进行灵活任务调度
上一节我们介绍了简单的并行映射,本节中我们来看看更灵活的任务调度方法:submit。
有时,你的代码结构比简单的 map 更复杂。例如,你可能有两个嵌套的 for 循环,根据某些条件调用不同的函数 F 和 G。这些调用可以并行执行,但不容易转换为 map 操作。这时可以使用 submit 方法。
submit 允许你将函数及其参数提交给执行器,在后台运行,并立即返回一个 Future 对象作为结果的引用。你可以在提交任务后继续做其他工作,然后在需要结果时调用 future.result() 来获取。
以下是使用 ThreadPoolExecutor.submit 的基本示例:
from concurrent.futures import ThreadPoolExecutor
import time
def slow_add(x, y):
time.sleep(1)
return x + y
with ThreadPoolExecutor() as executor:
future = executor.submit(slow_add, 1, 2)
# 在此期间可以做其他事情
result = future.result() # 等待并获取结果
实践练习:寻找相关性最高的股票对
我们使用第一部分生成的 HDF5 文件。每个文件包含一只股票的时间序列数据。我们的目标是找出所有股票对中相关性最高的一对。

顺序执行的代码包含一个嵌套循环来计算每对股票的相关性:
import pandas as pd
import glob
file_names = glob.glob('data/*.h5')
series = {}
for fn in file_names:
series[fn] = pd.read_hdf(fn)['close']
best = (None, None, -2)
for name_a, ts_a in series.items():
for name_b, ts_b in series.items():
if name_a == name_b:
continue
corr = ts_a.corr(ts_b)
if corr > best[2]:
best = (name_a, name_b, corr)
你的目标:使用 ThreadPoolExecutor.submit 并行化上述代码中计算相关性的嵌套循环部分。
解决方案与注意事项
以下是使用 submit 并行化的解决方案:
from concurrent.futures import ThreadPoolExecutor
import pandas as pd
import glob
def compute_corr(pair):
name_a, name_b, ts_a, ts_b = pair
if name_a == name_b:
return (name_a, name_b, -2) # 跳过自相关
corr = ts_a.corr(ts_b)
return (name_a, name_b, corr)
file_names = glob.glob('data/*.h5')
series = {fn: pd.read_hdf(fn)['close'] for fn in file_names}
futures = []
with ThreadPoolExecutor() as executor:
for name_a, ts_a in series.items():
for name_b, ts_b in series.items():
future = executor.submit(compute_corr, (name_a, name_b, ts_a, ts_b))
futures.append(future)
results = [f.result() for f in futures]
best = max(results, key=lambda x: x[2])
在这个例子中,并行版本可能没有获得巨大的加速,因为计算本身不重,而任务创建和协调的开销可能成为主导。此外,需要注意线程安全。例如,某些 HDF5 库的旧版本不是线程安全的,使用多线程读取 HDF5 文件可能导致崩溃。幸运的是,新版本的 HDF5 已经解决了这个问题。

关于执行器的选择:
ThreadPoolExecutor:任务在同一个进程的不同线程中运行,可以轻松共享数据(如内存中的数组),但受限于 Python 的全局解释器锁,纯 Python 计算可能无法充分利用多核。ProcessPoolExecutor:任务在不同的进程中运行,可以绕过 GIL 充分利用多核进行纯 Python 计算,但进程间通信(序列化和传输数据)成本较高。
一般经验法则:使用 NumPy、Pandas 或 scikit-learn 等库时,使用线程;进行纯 Python 计算(如处理字典、列表、文本解析)时,使用进程。
第三部分:使用大型数据集合
上一节我们介绍了灵活但底层的 submit 方法,本节中我们来看看更高级的抽象:大型数据集合。
有时,你会使用提供受限 API 的框架,例如 Spark 的 map、filter、groupBy、join,或者数组编程系统的矩阵运算,或者类似 SQL 的查询语言。只要你能将计算约束在这些 API 内,框架就会自动为你处理并行性。这对于符合这些模式的问题来说非常高效。
我们将看看典型的 Spark RDD(弹性分布式数据集)风格的 API。你可以创建来自 Python 可迭代对象的 RDD,然后在其上应用一系列转换操作(如 map、filter、cartesian),最后通过一个动作(如 collect 或 max)触发计算并返回结果。
以下是使用 PySpark 的基本示例:
from pyspark import SparkContext
sc = SparkContext()
data = [1, 2, 3, 4, 5]
rdd = sc.parallelize(data)
squared_rdd = rdd.map(lambda x: x**2)
even_rdd = squared_rdd.filter(lambda x: x % 2 == 0)
result = even_rdd.collect()
cartesian 操作可以生成两个 RDD 所有元素对的笛卡尔积,类似于嵌套循环。
实践练习:使用 Spark 寻找相关性最高的股票对
我们将使用 Spark 来重写之前寻找最高相关性股票对的练习。
你的目标:使用 Spark RDD 操作(如 parallelize、map、cartesian、filter、max)来实现相同的计算。
解决方案与性能分析
以下是使用 Spark 的解决方案:
from pyspark import SparkContext
sc = SparkContext()
file_names = sc.parallelize(glob.glob('data/*.h5'))
series_rdd = file_names.map(lambda fn: (fn, pd.read_hdf(fn)['close']))

# 生成所有股票对(笛卡尔积),并过滤掉自相关的对
pairs_rdd = series_rdd.cartesian(series_rdd) \
.filter(lambda x: x[0][0] != x[1][0])
def compute_corr(pair):
(name_a, ts_a), (name_b, ts_b) = pair
corr = ts_a.corr(ts_b)
return (name_a, name_b, corr)
corr_rdd = pairs_rdd.map(compute_corr)
best = corr_rdd.max(key=lambda x: x[2])
有趣的是,在这个例子中,Spark 版本(约8秒)可能比顺序版本(约1.5秒)还要慢。原因在于通信开销。cartesian 操作需要将每个股票的时间序列数据移动到所有其他任务所在的节点,这个数据移动的成本远高于实际计算相关性的成本。在本地笔记本电脑这样的共享内存环境中,这种开销尤其明显。
这个例子说明了并行化并不总是能带来加速。设置成本、任务调度开销,特别是数据移动(通信)成本,都可能抵消甚至超过并行计算带来的收益。像 Spark 或 Dask 这样的分布式系统通常提供 Web UI 等诊断工具,帮助你可视化任务执行和数据移动情况,这对于理解和优化性能至关重要。

第四部分:数据科学应用:超参数调优

现在,让我们看一个更贴近实际数据科学工作的例子:机器学习模型的超参数调优。我们将使用手写数字数据集和一个支持向量机分类器。
我们的目标是调整模型的三个参数:C(误差容忍度)、gamma(核函数影响范围)和 tol(迭代容忍度)。我们将在一个参数网格上进行搜索,为每一组参数训练模型并进行交叉验证,找出在验证集上表现最好的参数组合。

顺序执行的代码涉及在参数网格上进行三重循环,训练和评估每个模型,这非常耗时。
你的目标:使用你学到的任何并行编程技术(map、submit、Spark 等)来并行化这个超参数搜索过程。
解决方案示例
以下是使用 ThreadPoolExecutor.submit 的一种解决方案:
from concurrent.futures import ThreadPoolExecutor
from sklearn.model_selection import train_test_split
from sklearn import datasets, svm
import numpy as np
# 加载数据,划分训练集和测试集
digits = datasets.load_digits()
X_train, X_test, y_train, y_test = train_test_split(...)
# 定义参数网格
param_grid = {'C': np.logspace(-10, 3, 14),
'gamma': np.logspace(-10, 3, 14),
'tol': np.logspace(-4, -1, 4)}
futures = []
with ThreadPoolExecutor() as executor:
for C in param_grid['C']:
for gamma in param_grid['gamma']:
for tol in param_grid['tol']:
# 提交任务
future = executor.submit(evaluate_params, C, gamma, tol, X_train, y_train)
futures.append(future)
# 收集结果
results = [f.result() for f in futures]
best_params = max(results, key=lambda x: x[0]) # 假设返回 (score, params)
并行化可以显著减少搜索大量参数组合所需的总时间。你也可以尝试使用更智能的搜索算法(如随机搜索或贝叶斯优化),它们通常比网格搜索更高效,并且也容易并行化。


总结

在本课程中,我们一起学习了 Python 中并行编程的几种核心范式:
- 并行映射:适用于对一批独立数据应用相同函数的简单场景。使用
concurrent.futures.Executor.map可以轻松实现。 - 任务提交:通过
submit方法提供了更大的灵活性,可以处理更复杂的依赖关系和任务图。它返回Future对象用于异步获取结果。 - 大型数据集合:使用像 Spark 这样的高级抽象,通过受限但富有表现力的 API(如
map、filter、join)自动处理并行性。适合符合其计算模式的问题,但在数据移动成本高时需要谨慎。 - 实际应用:我们将这些技术应用于数据格式转换、股票相关性分析和机器学习超参数调优等实际问题。
记住,并行化不是万能的银弹。在尝试并行化之前,务必:
- 使用性能分析工具(如
snakeviz)识别瓶颈。 - 考虑是否有更优的算法或更高效的库。
- 评估并行化带来的开销(任务启动、通信、数据序列化)是否会超过收益。
- 根据任务类型(I/O 密集型、CPU 密集型、纯 Python 计算)选择合适的执行器(线程 vs 进程)。

希望本教程能帮助你在未来的项目中更有效地利用 Python 的并行计算能力。
课程 P14:scikit-image - Python 图像处理 🖼️
在本课程中,我们将学习如何使用 scikit-image 库进行图像处理。我们将从图像在 Python 中的基本表示开始,逐步深入到三维数据处理和机器学习应用。课程内容旨在让初学者能够轻松理解并上手实践。
概述 📋
scikit-image 是一个基于 NumPy 和 SciPy 构建的图像处理库,旨在提供高级、易用的接口,用于科学研究和实验。本教程将涵盖图像表示、三维数据处理、机器学习应用以及与其他库的交互。
图像表示:NumPy 数组 📊



在 scikit-image 中,图像使用标准的 NumPy 数组来表示。本节我们将学习如何创建和操作这些数组。

灰度图像
灰度图像是一个二维数组,每个元素代表一个像素的亮度值。
import numpy as np
import matplotlib.pyplot as plt
# 生成一个 500x500 的随机灰度图像
image = np.random.random((500, 500))
plt.imshow(image, cmap='gray')
plt.show()
彩色图像
彩色图像是一个三维数组,包含红、绿、蓝三个通道。
from skimage import data
# 加载彩色测试图像
color_image = data.chelsea()
print(color_image.shape) # 输出: (300, 451, 3)
图像数据类型
scikit-image 支持不同的数据类型,如 uint8(0-255)和浮点数(0-1)。内部处理通常使用浮点数。
from skimage import img_as_float, img_as_ubyte
# 转换图像数据类型
float_image = img_as_float(color_image)
ubyte_image = img_as_ubyte(float_image)
图像显示与颜色映射 🎨
使用 Matplotlib 可以方便地显示图像。选择合适的颜色映射对图像可视化至关重要。
显示图像
plt.imshow(image, cmap='viridis')
plt.show()
避免使用 Jet 颜色映射
Jet 颜色映射可能引入虚假的轮廓,建议使用 viridis 等更优的颜色映射。
# 创建示例数据
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2) / 15)
# 使用不同颜色映射显示
fig, axes = plt.subplots(1, 2)
axes[0].imshow(Z, cmap='jet')
axes[1].imshow(Z, cmap='viridis')
plt.show()
加载与保存图像 💾
scikit-image 提供了简单的接口来从磁盘加载和保存图像。
加载单个图像
from skimage import io
image = io.imread('images/balloon.jpg')
print(type(image), image.dtype, image.shape)
加载图像集合
ImageCollection 可以高效地加载多个图像,仅在需要时加载到内存。

from skimage import io
# 加载目录中的所有 PNG 图像
images = io.ImageCollection('images/*.png')
print(images.files)
# 显示所有图像
fig, axes = plt.subplots(1, len(images))
for i, img in enumerate(images):
axes[i].imshow(img)
plt.show()
练习:操作 NumPy 数组 ✏️
以下是三个练习,帮助您熟悉图像数组的基本操作。
练习 1:在图像上绘制字母 H
在给定图像上绘制一个指定颜色和大小的字母 “H”。
def draw_H(image, start_row, start_col, color, inplace=True):
if not inplace:
image = image.copy()
# 绘制垂直条
image[start_row:start_row+24, start_col:start_col+3] = color
image[start_row:start_row+24, start_col+17:start_col+20] = color
# 绘制水平条
image[start_row+10:start_row+13, start_col:start_col+20] = color
return image
练习 2:绘制 RGB 强度图
提取彩色图像中某一行的红、绿、蓝通道强度并绘制成线图。
def plot_rgb_intensity(image, row):
red = image[row, :, 0]
green = image[row, :, 1]
blue = image[row, :, 2]
plt.plot(red, color='red')
plt.plot(green, color='green')
plt.plot(blue, color='blue')
plt.show()
练习 3:将彩色图像转换为灰度图像
使用加权公式将 RGB 图像转换为灰度图像。
def rgb_to_gray(image):
weights = np.array([0.2, 0.7, 0.07])
gray_image = np.dot(image, weights)
return gray_image
三维图像处理 🔬
三维图像在生物医学等领域广泛应用。本节我们将学习如何处理三维图像数据。
三维图像表示
三维图像是一个三维数组,通常由多个二维切片堆叠而成。
# 加载三维测试数据
from skimage import data
volume = data.cells3d()
print(volume.shape) # 输出: (60, 256, 256)
图像间距
三维图像中,不同维度的像素间距可能不同,需要在处理时考虑。
original_spacing = np.array([0.29, 0.065, 0.065])
rescaled_spacing = original_spacing * [1, 4, 4]
normalized_spacing = rescaled_spacing / rescaled_spacing[1]
图像增强与去噪 🌟
为了提高图像质量,我们通常需要进行对比度增强和去噪处理。
对比度增强
gamma 校正和直方图均衡是常用的对比度增强方法。
from skimage import exposure
# Gamma 校正
gamma_corrected = exposure.adjust_gamma(volume, gamma=0.5)
# 直方图均衡
equalized = exposure.equalize_hist(volume)
去噪
高斯滤波、中值滤波和双边滤波可用于去除图像噪声。
from skimage.filters import gaussian, median
from skimage.restoration import denoise_bilateral
# 高斯滤波(考虑间距)
sigma = 3 / normalized_spacing
smoothed = gaussian(volume, sigma=sigma, multichannel=False)
# 中值滤波(逐平面应用)
denoised = np.zeros_like(volume)
for i in range(volume.shape[0]):
denoised[i] = median(volume[i])
图像分割与特征提取 🧩
图像分割是将图像划分为不同区域的过程,特征提取则用于量化这些区域。
阈值分割
使用阈值将图像分为前景和背景。
from skimage.filters import threshold_li
# Li 最小交叉熵阈值
threshold = threshold_li(denoised)
binary = denoised > threshold
形态学操作
形态学操作可用于填充孔洞和去除小物体。
from skimage import morphology
# 填充孔洞
filled = morphology.remove_small_holes(binary, area_threshold=64)
# 去除小物体
cleaned = morphology.remove_small_objects(filled, min_size=128)
分水岭算法
分水岭算法能有效分离相互接触的物体。
from skimage.segmentation import watershed
from skimage.feature import peak_local_max
# 计算距离图并查找局部最大值
distance = morphology.distance_transform_edt(binary)
local_maxi = peak_local_max(distance, min_distance=10)
markers = morphology.label(local_maxi)
labels = watershed(-distance, markers, mask=binary)
特征提取
使用 regionprops 可以提取每个区域的多种特征。
from skimage.measure import regionprops
# 清除边界标签
from skimage.segmentation import clear_border
labels_cleared = clear_border(labels)
# 提取区域属性
props = regionprops(labels_cleared, intensity_image=volume)
for prop in props:
print(prop.area, prop.mean_intensity)

总结 🎓
在本课程中,我们一起学习了 scikit-image 库的基本用法。我们从图像在 Python 中的 NumPy 数组表示开始,逐步深入到图像加载、显示、增强、分割和特征提取。特别是,我们还探讨了三维图像处理的特殊考虑和方法。希望这些知识能帮助您在图像处理项目中更加得心应手。
注意:本教程基于原始讲座内容整理,删除了所有语气词,并按照要求格式进行了优化,以确保内容清晰、结构完整、易于理解。

课程 P15:面向地球科学的Python API 🌍
在本节课中,我们将学习Descartes Labs公司如何构建一个统一的Python API,用于高效访问和处理海量地球空间数据。我们将了解其背后的动机、核心功能,并通过一个实际案例演示其强大能力。
公司简介与数据挑战 🏢
Descartes Labs是一家成立于2014年的初创公司,源自洛斯阿拉莫斯国家实验室。公司的核心目标是整合全球地理空间数据,并应用机器学习和计算机视觉技术,对地球表面即将发生的事件进行预测。
我们面临的核心挑战是地理空间数据的“大”。例如,Landsat卫星用了44年才积累了约1PB的影像数据,而欧空局的Sentinel-2星座仅用2年就能收集1PB数据,像Planet这样的私营公司每年更是会收集数PB的数据。这个领域的数据在规模、速率和类型上都在爆炸式增长。
多元化的地理空间数据 🌐
在Descartes Labs,我们整合了多种卫星影像和数据。这些数据在空间分辨率、时间分辨率和光谱分辨率上差异巨大。
以下是主要的数据类型:
- MODIS:提供约300米分辨率的数据,时间分辨率高,适合制作高质量的无云镶嵌图。
- Landsat:提供更高分辨率的多光谱数据,包含多个不同波段。
- 国家航空影像计划:提供约1米分辨率的数据,但每年仅更新一次。
- Sentinel-1:合成孔径雷达卫星,主动发射5GHz辐射并测量其反射的极化。它不受云层影响,能清晰捕捉船只等金属物体。
- Sentinel-2:与Landsat类似的光学卫星,其红边波段对植被监测非常有用。
我们的核心理念是:通过一个统一的API整合所有这些数据集,可以更轻松、更快速地利用它们,从而迭代和构建复杂的产品模型。
平台应用实例 🚜
利用这些影像和遥感数据集,我们开发了一些应用。
上一节我们介绍了多元化的数据,本节中我们来看看这些数据的具体应用。

农业产量预测
我们去年展示了美国玉米产量的预测模型。这需要整合大量前述数据集,并快速迭代模型。我们并非农学背景,主要通过物理洞察和海量影像数据来推导。

全球合成影像
我们制作了三套全球合成影像:
- Landsat 8地表反射率全球合成图。
- Sentinel-1合成孔径雷达全球合成图(据我们所知是首例)。
- Sentinel-2红边波段全球合成图,对农业监测尤其有力。
地理视觉搜索
这是一个演示项目,概念源于卡内基梅隆大学。其核心思想是:给定一张卫星影像片段,在全球范围内寻找与之相似的其他地点。
其技术流程可以概括为:
# 概念性流程
影像数据 -> 深度卷积神经网络处理 -> 生成特征空间 -> 在高维空间中进行快速相似性比对 -> 输出最相似的结果
我们将其应用于Planet公司提供的中国地区影像,成功实现了对太阳能电池板等特定地物的搜索。
Python API 设计与演示 🐍
经过两年半的发展,我们内部API已能支持大规模的全球分析。现在,我们正将部分能力对外开放。
我们的平台旨在提供统一、快速、可扩展的卫星影像数据访问接口,避免用户分别向不同数据提供商学习复杂的FTP接口。
平台目前处于测试阶段,其核心功能包括:
- 定义感兴趣区域。
- 跨所有卫星星座搜索所需数据。
- 创建镶嵌图和时序合成图。
- 提供基础遥感运算,更复杂的算法则由用户自行实现。

平台主要通过REST API暴露功能,并提供了Python客户端库。

以下是基本的Python使用模式:
import descarteslabs as dl
# 1. 在特定区域(如爱荷华州)搜索Landsat数据
scenes, ctx = dl.scenes.search(aoi, products=["landsat:8"], start_time="2017-01-01", end_time="2017-12-31")
# 2. 获取数据为NumPy数组
arr = scenes.stack(bands=["red", "green", "blue"], ctx=ctx) # 获取原生波段
ndvi = scenes.stack(bands=["ndvi"], ctx=ctx) # 直接获取NDVI等衍生指数
scenes.stack 方法返回标准的NumPy数组,并附带元数据对象,方便用户了解所下载数据的信息。
实战演示:拉森C冰架崩解 🧊

接下来,我们通过一个实际案例来演示API的便捷性。最近,南极洲的拉森C冰架发生了崩解。



我们可以利用Sentinel-1的合成孔径雷达数据来观察这一过程,因为雷达数据不受极地黑暗和云层影响。
操作步骤如下:
- 在 GeoJSON.io 上找到拉森C冰架位置并绘制多边形。
- 使用Python API搜索过去一个月内的Sentinel-1 GRD数据。
- 筛选出降轨数据,并按日期分组。
- 请求120米分辨率的VV(垂直发射垂直接收)极化波段数据。
- 将获取的影像按时间顺序叠加,生成GIF动图。
通过短短几行代码和几分钟的处理,我们就能清晰地看到冰架在2017年6月至7月间崩解、分离的过程。这充分展示了该API在快速验证想法、进行科学探索方面的能力。
平台访问与总结 📚
我们的平台目前提供测试访问权限,面向学术界、非营利组织和工业界用户。用户可以访问所有公开数据集,我们也可以协助接入私有数据源。
关键信息:
- 分析运行位置:目前,分析在用户下载数据的位置进行。我们不对用户使用的算法或软件包做限制。
- 性能提示:数据存储在Google云上,在Google云平台内访问速度最快。用户可以通过多进程和并发请求来优化下载速度。
- 资源:
- 文档:请访问我们的官网。
- Python客户端库:已在GitHub上开源,支持Python 2和Python 3。
本节课中我们一起学习了Descartes Labs如何构建一个面向地球科学的Python API。我们从其处理海量、多元地理空间数据的挑战出发,了解了该API的设计理念、核心功能,并通过拉森C冰架崩解的实例,看到了它如何让科学家和开发者能够快速、便捷地访问和分析全球卫星数据,从而更高效地探索我们的星球。
感谢观看。如有兴趣参与测试或了解更多,欢迎会后交流。
课程 P16:nbgrader - Jupyter Notebook 中的作业创建与评分工具 📚

在本节课中,我们将学习一个名为 nbgrader 的工具。它是一个专门为 Jupyter Notebook 环境设计的系统,用于创建、分发、自动评分和手动批改作业。我们将了解其核心概念、两种主要工作流程,并通过一个简单的例子来演示其基本操作。
概述:为什么在课堂中使用 Notebook?
Jupyter Notebook 在教育领域的应用越来越广泛。教师们用它进行课堂练习、编写讲义、演示复杂概念,当然,还有布置和批改作业。Notebook 之所以特别适合用于作业,是因为它可以将说明文字、编程练习、数学公式和概念性问题(如自由回答)全部整合在一个文档中。
然而,使用 Notebook 进行作业评分也带来了一些独特的挑战:
- 如何维护独立的教师版(含答案)和学生版(不含答案)作业?
- 如何在 Notebook 中实现自动评分?
- 如何对自由回答题进行手动评分?
- 如何清晰地向学生提供反馈和评论?
nbgrader 正是为了解决这些问题而开发的。


nbgrader 工作流程总览
nbgrader 涵盖了从作业创建到反馈生成的完整工作流程。一个典型流程包含以下步骤:
- 创建教师版本:编写包含题目、说明和标准答案的 Notebook。
- 生成学生版本:使用 nbgrader 自动移除答案,生成供学生使用的版本。
- 分发作业:将学生版作业文件发送给学生。
- 收集提交:接收学生完成后的 Notebook 文件。
- 自动评分:使用 nbgrader 运行预定义的测试,自动给出分数。
- 手动评分:对需要人工评判的部分(如自由回答)进行评分。
- 生成反馈:创建包含详细分数和评论的 HTML 报告,返回给学生。
其中,第3步(分发)和第4步(收集)可以根据是否使用 JupyterHub 而有所不同,我们稍后会详细讨论。
核心概念与单元格类型
在 nbgrader 中,我们通过为 Notebook 中的单元格添加特殊“类型”来定义其角色。以下是主要的单元格类型:
1. 自动评分答案单元格
这是学生编写代码解决方案的地方。在教师版本中,你需要用特殊的标记来包裹标准答案。
### BEGIN SOLUTION
def squares(n):
return [x**2 for x in range(1, n+1)]
### END SOLUTION
当生成学生版本时,### BEGIN SOLUTION 和 ### END SOLUTION 之间的内容会被移除,并替换为“在此编写你的代码”之类的占位符。

2. 自动评分测试单元格
这是定义如何评估学生代码的地方。你可以在其中编写单元测试。

# 这是一个测试单元格,值2分
assert squares(1) == [1]
assert squares(5) == [1, 4, 9, 16, 25]
### BEGIN HIDDEN TESTS
assert squares(12) == [1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144]
### END HIDDEN TESTS
- 普通测试:学生会看到,用于自我检查。
- 隐藏测试:位于
### BEGIN HIDDEN TESTS和### END HIDDEN TESTS之间。生成学生版本时会被移除,仅在自动评分时执行,用于最终评分。
3. 手动评分单元格
用于自由回答题或难以自动评分的代码题。你需要手动为这些单元格打分。
- 手动评分答案:可以是文本(如解释一个概念)或代码。
- 在教师版本中,你同样需要提供参考答案或评分标准。



工作流程一:独立使用 nbgrader(无 JupyterHub)
上一节我们介绍了 nbgrader 的核心概念,本节中我们来看看如何在不依赖 JupyterHub 的情况下独立使用它。这个流程更灵活,适合通过电子邮件、学习管理系统(LMS)或 GitHub 分发作业的课程。
以下是具体操作步骤:
- 创建教师版本:在 Jupyter Notebook 中,启用 “Create Assignment” 工具栏,为每个单元格设置正确的类型(如自动评分答案、测试等),并编写好答案和测试。
- 生成学生版本:在 nbgrader 的表单界面或使用命令行,点击“生成”按钮。nbgrader 会创建一份移除了所有答案和隐藏测试的 Notebook。
- 分发作业:你需要手动将生成的学生版
.ipynb文件通过电子邮件、课程网站等方式发送给学生。 - 收集作业:学生完成后,你需要手动收集他们提交的
.ipynb文件。 - 组织提交文件:将收集到的文件按照
submitted/{学生ID}/{作业名称}/的目录结构存放,以便 nbgrader 识别。 - 自动评分:在 nbgrader 界面中,对收集的作业运行自动评分。系统会执行测试单元格并给出基础分数。
- 手动评分:对于手动评分单元格,你需要逐一点开学生作业,根据参考答案进行评判,并可以添加文字评论和部分分数。
- 生成反馈:使用
nbgrader feedback命令,为每份作业生成一个 HTML 报告。报告中会清晰展示每个单元格的得分、自动测试的错误信息以及教师添加的评论。 - 返回反馈:最后,你需要手动将这些 HTML 反馈文件发送回给学生。
优点:无需搭建和维护 JupyterHub 服务器。
缺点:分发、收集和返回反馈需要手动操作,在大班教学中可能比较繁琐。


工作流程二:结合 JupyterHub 使用 nbgrader
如果你已经为课程部署了 JupyterHub,那么 nbgrader 可以与它无缝集成,使作业分发和收集过程自动化。

以下是结合使用时的步骤:


- 创建与生成:前两步与独立流程相同:创建教师版本并生成学生版本。
- 发布作业:在 nbgrader 界面中,点击“发布”按钮。作业会自动出现在已关联课程的所有学生的 JupyterHub 界面的“作业”选项卡中。
- 学生获取与提交:学生登录 JupyterHub 后,可以“获取”作业,在 Notebook 中直接完成,并使用“提交”按钮交回。学生可以多次提交,系统会记录最新版本。
- 收集作业:教师在 nbgrader 界面点击“收集”按钮,系统会自动从所有学生账户下抓取最新的提交版本。
- 评分与生成反馈:自动评分、手动评分和生成反馈的步骤与独立流程完全一致。
- 返回反馈:目前,将反馈文件返回给学生仍需手动操作(例如,将 HTML 文件复制到学生的 JupyterHub 主目录)。


优点:极大简化了作业分发和收集流程,体验更流畅。
缺点:需要事先设置和运维 JupyterHub 环境。


总结


本节课我们一起学习了 nbgrader 这个强大的工具。我们了解到它通过定义特殊的单元格类型(自动评分答案、自动评分测试、手动评分),帮助教师在 Jupyter Notebook 中高效管理作业的全生命周期。
我们重点探讨了两种主要工作流程:
- 独立使用:适合灵活的小型课程或已有固定文件分发渠道的场景。
- 结合 JupyterHub 使用:适合希望实现作业管理自动化的中大型课程。

无论选择哪种方式,nbgrader 的核心价值在于它统一了作业创建、评分和反馈的流程,并为学生提供了清晰、详细的评估结果,有助于提升学习效果。
课程 P17:使用 SymPy 进行自动代码生成 🚀

在本课程中,我们将学习如何利用 SymPy 的代码生成功能,将符号数学表达式自动转换为多种编程语言的代码。我们将从基础概念开始,逐步深入到高级应用,包括自定义代码打印器和性能优化技术。
概述

SymPy 是一个强大的符号数学库,它不仅能进行符号计算,还能将复杂的数学表达式自动转换为高效的数值计算代码。本教程将引导你了解 SymPy 的代码生成工作流程,从简单的表达式转换到生成完整的、可编译的函数,并最终集成到科学计算流程中。



1. SymPy 表达式基础回顾 🔄






在深入代码生成之前,我们需要确保对 SymPy 的基本操作有清晰的理解。SymPy 的核心是符号表达式,它允许我们以数学形式而非数值形式处理问题。
首先,我们导入 SymPy 并设置漂亮的打印输出,以便更直观地查看表达式。
import sympy as sp
sp.init_printing()
创建符号
符号是构建表达式的基本单元。我们使用 symbols 函数来创建它们。
x, y, z = sp.symbols('x y z')
alpha_1, omega_2 = sp.symbols('alpha_1 omega_2')
构建表达式
有了符号,我们就可以像书写数学公式一样构建表达式。
expr = sp.sin(x) + sp.cos(y)
expr
第一个练习:创建正态分布表达式
以下是你的第一个练习。请尝试使用 SymPy 符号重新创建以下正态分布的概率密度函数表达式:
提示:你需要创建符号 sigma 和 mu,并使用 sp.sqrt 表示平方根,sp.exp 表示指数函数。
当你完成时,请贴上蓝色便签。
上一节我们回顾了如何创建基本的 SymPy 表达式。本节中,我们将看看在使用 SymPy 时需要注意的几个常见问题。
使用 SymPy 时的注意事项
以下是三个初学者常犯的错误,了解它们可以避免很多麻烦。
-
整数除法问题:在 Python 中,
1/2的结果是浮点数0.5。但在符号计算中,我们通常希望保持精确的有理数形式。# 错误做法:得到浮点数 expr_wrong = x + 1/2 # 正确做法:得到有理数 expr_correct1 = x + sp.S(1)/2 expr_correct2 = x + sp.Rational(1, 2) -
幂运算符:在 Python 和 SymPy 中,幂运算符是
**,而不是^。# 错误做法:^ 是异或运算符 expr_wrong = x^2 # 正确做法 expr_correct = x**2 -
表达式不可变性:所有 SymPy 表达式都是不可变的。任何操作都会返回一个新的表达式,而不会修改原表达式。
expr = x + 1 new_expr = expr.subs(x, 2) # 返回新表达式 3 # expr 仍然是 x + 1
数值计算与任意精度
SymPy 可以进行精确的符号计算,也能转换为高精度的数值。
# 精确的 sqrt(2)
expr_exact = sp.sqrt(2)
# 转换为浮点数
float_val = expr_exact.evalf()
# 获取100位精度
high_precision = expr_exact.evalf(100)
练习:请计算圆周率 π 的 100 位精度数值。
未定义函数与微分
在建模动态系统或微分方程时,我们需要使用未定义的函数。
# 创建一个未定义的函数 f
f = sp.Function('f')
# 创建包含 f 的表达式
expr_with_f = f(x) + x**2
SymPy 可以轻松计算符号导数。
expr = sp.sin(x+1) * sp.cos(y)
# 对 x 求导
sp.diff(expr, x)
# 对 y 求导
sp.diff(expr, y)
# 对未定义函数求导
sp.diff(f(x), x)
练习:请使用 SymPy 写出以下波动方程的符号形式:


提示:u 是 t 和 x 的函数。使用 sp.Eq 创建等式。
矩阵与雅可比矩阵
SymPy 支持符号矩阵运算,这对于多变量系统建模至关重要。
# 创建一个矩阵
M = sp.Matrix([[1, 2], [3, 4]])
# 创建一个列向量
v = sp.Matrix([x, y, z])
# 矩阵乘法
M * v
雅可比矩阵计算也非常直接。
# 假设 F 是一个向量值函数
F = sp.Matrix([x*y, y*z, z*x])
# 计算雅可比矩阵
J = F.jacobian([x, y, z])
练习:
- 创建矩阵
M = [[1,0,1], [-1,2,3], [1,2,3]]。 - 创建列向量
v = [x, y, z]。 - 计算
M * v。 - 计算结果向量关于
[x, y, z]的雅可比矩阵。你发现了什么?
2. 代码打印机初探 🖨️
现在我们已经熟悉了 SymPy 表达式,让我们开始探索代码生成的核心工具:代码打印机。代码打印机负责将 SymPy 表达式转换为特定编程语言的代码字符串。
SymPy 支持多种语言,包括 C、C++、Fortran、Julia、JavaScript 等。
expr = sp.Abs(sp.sin(sp.pi * x**2))
# 生成 C 代码
print(sp.ccode(expr))
# 生成 Fortran 代码
print(sp.fcode(expr))
# 生成 Julia 代码
print(sp.julia_code(expr))
# 生成 JavaScript 代码
print(sp.jscode(expr))

每个打印机都有可配置的选项,例如指定 C 语言标准。
# 使用 C89 标准
printer_c89 = sp.printing.ccode.C89CodePrinter()
print(printer_c89.doprint(expr))
# 使用 C99 标准
printer_c99 = sp.printing.ccode.C99CodePrinter()
print(printer_c99.doprint(expr))
练习:请选择一个你感兴趣的编程语言,用不同的数学表达式(包含三角函数、指数、对数等)测试其代码打印机,观察输出有何不同。你遇到了任何错误或意外行为吗?


上一节我们看到了如何将单个表达式转换为代码片段。本节中,我们将学习一个更高级、更便捷的函数:lambdify,它能直接将表达式转换为可调用的数值函数。
3. 简单方式:使用 lambdify 进行代码生成 ⚡
lambdify 函数是 SymPy 中进行代码生成最直接的方法。它接收一个 SymPy 表达式和一组符号,然后返回一个高效的、用于数值计算的 Python 函数。
为什么需要 lambdify?
直接使用 subs() 和 evalf() 进行数值计算虽然可行,但速度很慢,因为它每次都要进行符号替换和任意精度计算。
expr = x**2 + sp.sin(y)
# 慢速方法
slow_val = expr.subs({x: 1.5, y: 0.3}).evalf()
lambdify 通过将表达式编译为底层代码(默认使用 NumPy)来大幅提升速度。
# 使用 lambdify 创建快速函数
fast_func = sp.lambdify((x, y), expr, ‘numpy’)
# 现在可以快速计算
fast_val = fast_func(1.5, 0.3)
控制函数签名
有时我们需要函数具有特定的参数结构,例如与 SciPy 的求解器兼容。
# 创建一个函数,其第三个参数是一个包含三个值的元组
args = (x, (y, z, x)) # y, z, x 被打包成一个元组作为第二个参数
special_func = sp.lambdify(args, expr)
result = special_func(1.0, (2.0, 3.0, 4.0)) # 调用方式
练习:给定函数 \(f(x, y) = \sin(x^2 + y^2)\)。
- 使用
lambdify创建一个数值函数。 - 计算该函数在点 \((0.5, 0.5)\) 处关于 \(x\) 和 \(y\) 的混合偏导数 \(\frac{\partial^2 f}{\partial x \partial y}\) 的数值。
- (可选)使用 Matplotlib 绘制该函数在区域 \(x, y \in [-2, 2]\) 上的图像。
应用实例:化学动力学建模 🧪
让我们将 lambdify 应用于一个实际问题:模拟化学反应动力学。我们考虑亚硝酰溴(NOBr)的形成与分解反应。
反应方程式为:
根据质量作用定律,我们可以建立常微分方程组(ODE)来描述各物质浓度的变化。
# 定义符号:浓度 C1=[NO], C2=[Br2], C3=[NOBr], 速率常数 kf, kb
C1, C2, C3, kf, kb, t = sp.symbols(‘C1 C2 C3 kf kb t’)
# 根据质量作用定律建立 ODE
dC1_dt = -2*kf*C1**2*C2 + 2*kb*C3**2
dC2_dt = -kf*C1**2*C2 + kb*C3**2
dC3_dt = 2*kf*C1**2*C2 - 2*kb*C3**2
为了使用 SciPy 的 odeint 求解器进行数值积分,我们需要一个函数来计算 ODE 的右侧(RHS)。
# 手动编写 RHS 函数(容易出错)
def rhs_manual(y, t, kf, kb):
C1, C2, C3 = y
dC1 = -2*kf*C1**2*C2 + 2*kb*C3**2
dC2 = -kf*C1**2*C2 + kb*C3**2
dC3 = 2*kf*C1**2*C2 - 2*kb*C3**2
return [dC1, dC2, dC3]
使用 SymPy 和 lambdify,我们可以自动、无误地生成这个函数。
# 状态向量
state_vec = sp.Matrix([C1, C2, C3])
# RHS 向量
rhs_vec = sp.Matrix([dC1_dt, dC2_dt, dC3_dt])
# 使用 lambdify 自动生成函数
# 注意函数签名匹配 odeint 的要求:(y, t, ...args)
rhs_func = sp.lambdify((state_vec, t, kf, kb), rhs_vec)
练习:使用上面生成的 rhs_func,结合 SciPy 的 odeint,模拟从初始浓度 [C1, C2, C3] = [2.0, 1.0, 0.0] 开始,在 t=[0, 10] 时间范围内的反应过程。假设 kf=1.0, kb=0.5。绘制三种物质浓度随时间的变化曲线。
进阶:生成解析雅可比矩阵
对于刚性 ODE 问题,向求解器提供解析的雅可比矩阵可以显著提高求解效率和稳定性。SymPy 可以轻松计算雅可比矩阵。
# 计算 RHS 向量关于状态向量的雅可比矩阵
jacobian_mat = rhs_vec.jacobian(state_vec)
# 使用 lambdify 生成雅可比矩阵函数
jacobian_func = sp.lambdify((state_vec, t, kf, kb), jacobian_mat)
现在,你可以将 rhs_func 和 jacobian_func 一起传递给 odeint(通过 Dfun 参数)。
练习:对一个更复杂的化学反应系统(例如 Robertson 问题,一个经典的刚性 ODE)重复上述过程:构建符号 ODE,生成 RHS 函数和雅可比矩阵函数,并使用 odeint 进行求解。比较提供和不提供雅可比矩阵时求解器的性能和结果。
4. 深入方式:自定义打印机与 CSE 🛠️
上一节我们使用了高级函数 lambdify。本节中,我们将深入底层,学习如何直接控制代码打印过程,并引入公共子表达式消除(CSE)来优化生成的代码。
使用代码打印机类
lambdify 背后使用的是代码打印机。我们可以直接实例化并使用它们。
from sympy.printing.ccode import C99CodePrinter
printer = C99CodePrinter()
# 打印一个简单表达式
expr = sp.sin(x)**2 + sp.cos(x)**2
print(printer.doprint(expr))
然而,直接打印表达式通常不足以生成有效的 C 代码,因为 C 代码需要赋值语句。我们需要使用矩阵符号来指定赋值目标。
# 创建一个矩阵符号作为赋值目标
result = sp.MatrixSymbol(‘result’, 3, 1) # 3x1 的矩阵符号
# 要计算的表达式向量
expr_vec = sp.Matrix([x**2, sp.sin(y), sp.log(z)])
# 打印赋值代码
print(printer.doprint(expr_vec, assign_to=result))
输出将是类似 result[0] = pow(x, 2); result[1] = sin(y); result[2] = log(z); 的代码。
练习:对于之前化学动力学模型的 RHS 向量 rhs_vec,使用 C99CodePrinter 和矩阵符号,生成将其赋值给一个名为 dy 的数组的 C 代码。
自定义代码打印机
有时默认的打印方式不符合我们的需求。例如,我们可能想改变变量名,或者使用特定库中的函数。这可以通过子类化现有的打印机并重写其 _print_* 方法来实现。
所有打印机都有许多以 _print_ 开头的方法,每个方法负责打印一种特定类型的 SymPy 对象(如 _print_Symbol, _print_Pow, _print_Matrix)。
class MyCustomCPrinter(C99CodePrinter):
def _print_Symbol(self, expr):
# 将所有符号 ‘y’ 打印为 ‘state[0]’,‘z’ 打印为 ‘state[1]’ 等。
# 这里是一个简单示例:将所有符号名转为大写
return self._print(expr.name.upper())
custom_printer = MyCustomCPrinter()
print(custom_printer.doprint(x + y))
# 输出: X + Y
练习:在化学动力学 ODE 中,状态变量是 C1, C2, C3。创建一个自定义打印机,将它们分别打印为 state[0], state[1], state[2]。
进阶练习:创建一个自定义打印机,将小整数次幂(如 x**2, x**3)展开为乘法形式(如 x*x, x*x*x),而对其他次幂保持使用 pow 函数。
公共子表达式消除
在大型表达式中,相同的子表达式可能会重复计算多次。公共子表达式消除(CSE)是一种优化技术,它识别这些重复部分,计算一次并将结果存储在临时变量中,然后在主表达式中引用这些变量。
这可以减少计算量和代码大小,有时还能提高数值稳定性。
from sympy import cse
# 一个含有公共子表达式的例子
expr1 = sp.sin(x) * sp.log(x)
expr2 = sp.cos(x) * sp.log(x) # log(x) 是公共子表达式
# 应用 CSE
replacements, reduced_exprs = cse([expr1, expr2])
print(“替换(临时变量):”)
for var, subexpr in replacements:
print(f” {var} = {subexpr}“)
print(“简化后的表达式:”)
for expr in reduced_exprs:
print(f” {expr}“)
练习:对化学动力学模型的 RHS 向量 rhs_vec 应用 CSE。观察生成了多少个临时变量。然后,使用自定义打印机,生成包含这些临时变量定义和最终赋值语句的完整 C 代码块。
终极挑战:将以上所有内容结合起来:
- 为化学动力学 ODE 系统计算 RHS 和雅可比矩阵。
- 对这两个表达式列表一起应用 CSE,找出它们共有的子表达式。
- 创建一个自定义打印机,生成一个完整的 C 函数。该函数应:
- 定义所有临时变量。
- 计算 RHS 向量的值并填入输出数组
dy。 - 计算雅可比矩阵的值并填入输出数组
jac(按行展开成一维数组)。
- 将生成的代码嵌入一个 C 程序模板,编译并运行它,验证结果是否正确。
5. 集成与自动化:autowrap 与外部库 🌉
上一节我们手动处理了代码生成和打印。本节中,我们将介绍 autowrap 工具,它能将代码生成、编译和包装成 Python 可调用模块的过程自动化。我们还将学习如何集成外部 C 库。
使用 codegen 生成 C 文件
sympy.utilities.codegen 模块中的 `code

机器学习教程 P18:使用 scikit-learn 第二部分
在本节课中,我们将要学习如何处理文本数据、进行模型评估与参数调优、深入了解线性模型与决策树,并简要介绍异常检测与高级聚类方法。
文本数据处理 📝
上一节我们介绍了分类数据和连续型特征。本节中我们来看看如何处理长文本数据。

文本数据(如电子邮件、产品评论)与数值数据有很大不同。文本是字符串,长度不一,且长度本身通常不直接反映内容。我们需要一种方法将字符串数据转换为数值表示。
词袋模型
基本思想是将文本分解为单词(或标记),然后统计每个单词在文档中出现的频率。
以下是实现步骤:
- 分词:将文本按空格分割并转换为小写,得到标准化单词(标记)。
- 构建词汇表:收集训练数据中所有独特的单词。
- 编码:对于每个文档,创建一个长度等于词汇表大小的向量,记录每个单词的出现次数。

由于向量中大部分元素为零,我们使用稀疏矩阵表示以节省空间。这种方法称为词袋模型,因为它忽略了单词的顺序。


在 scikit-learn 中,可以使用 CountVectorizer 实现:





from sklearn.feature_extraction.text import CountVectorizer
X = ["Some say the world will end in fire",
"Some say in ice"]
vectorizer = CountVectorizer()
X_bow = vectorizer.fit_transform(X)

TF-IDF 编码












词袋模型的一个变体是 TF-IDF 编码。它通过降低常见单词的权重来改进表示。











- TF 代表词频。
- IDF 代表逆文档频率,用于惩罚在所有文档中频繁出现的单词。
其核心思想是:一个单词在越多的文档中出现,其重要性就越低。公式可以简化为对计数进行重新缩放和归一化。

from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer()
X_tfidf = vectorizer.fit_transform(X)

N-gram 特征
为了捕捉单词顺序信息,我们可以使用 N-gram。N-gram 是文本中连续的 N 个单词序列。

- 1-gram:单个单词。
- 2-gram:相邻的单词对。
- 3-gram:相邻的三个单词。

使用 CountVectorizer 或 TfidfVectorizer 的 ngram_range 参数可以轻松提取 N-gram 特征。

# 提取 1-gram 和 2-gram
vectorizer = CountVectorizer(ngram_range=(1, 2))
X_ngram = vectorizer.fit_transform(X)
字符 N-gram
对于短文本或名称,字符 N-gram 可能更有效。它将文本视为字符序列,并提取连续的 N 个字符片段。
vectorizer = CountVectorizer(analyzer='char', ngram_range=(2, 2))
X_char_ngram = vectorizer.fit_transform(X)





文本分类实战:垃圾短信检测 📱

现在,让我们应用所学知识解决一个实际问题:垃圾短信检测。
我们使用一个包含正常短信和垃圾短信的数据集。以下是处理流程:

- 加载数据:数据
X是短信文本列表,y是标签列表。 - 划分数据集:将数据分为训练集和测试集。
- 特征提取:使用
CountVectorizer将文本转换为数值特征。 - 训练模型:由于特征维度可能很高,线性模型(如逻辑回归)通常效果很好。
- 评估模型:在测试集上评估模型性能。
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction.text import CountVectorizer
# 假设 X_text, y 已加载
X_train, X_test, y_train, y_test = train_test_split(X_text, y, stratify=y)
vectorizer = CountVectorizer()
X_train_bow = vectorizer.fit_transform(X_train)
X_test_bow = vectorizer.transform(X_test)
model = LogisticRegression()
model.fit(X_train_bow, y_train)
print("Test accuracy:", model.score(X_test_bow, y_test))
我们可以通过检查模型系数(每个单词的权重)来理解哪些单词对预测垃圾短信最重要。

模型评估与交叉验证 📊

之前我们使用单一的训练/测试分割来评估模型,但这可能不稳定且浪费数据。K折交叉验证 是一种更稳健的评估方法。
K折交叉验证原理
其思想是将数据随机分成 K 个大小相似的子集(称为“折”)。然后进行 K 次训练和评估:
- 第1次:使用折1作为测试集,其余折作为训练集。
- 第2次:使用折2作为测试集,其余折作为训练集。
- ...
- 第K次:使用折K作为测试集,其余折作为训练集。
最终,我们得到 K 个性能评估分数,可以计算其均值和标准差,从而更可靠地估计模型性能。
在 scikit-learn 中,可以使用 cross_val_score 函数轻松实现:
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
scores = cross_val_score(KNeighborsClassifier(), X, y, cv=5)
print("CV scores:", scores)
print("Mean CV accuracy:", scores.mean())
分层交叉验证



对于分类问题,为了确保每个折中各类别的比例与原始数据集一致,应使用分层K折交叉验证,这是 cross_val_score 在分类任务中的默认行为。
模型复杂度与参数调优 ⚙️













模型可能欠拟合(过于简单)或过拟合(过于复杂)。我们需要找到最佳平衡点。





验证曲线


验证曲线展示了模型在训练集和验证集上的性能如何随某个超参数(如 KNN 中的邻居数 n_neighbors)变化。这有助于我们可视化欠拟合和过拟合的区域。


网格搜索

当模型有多个超参数时,我们可以使用 网格搜索 来系统地寻找最佳参数组合。GridSearchCV 类可以自动进行交叉验证并选择最佳参数。
以下是使用网格搜索为 K近邻分类器寻找最佳 n_neighbors 的示例:



from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier






param_grid = {'n_neighbors': [1, 3, 5, 10, 50]}
grid_search = GridSearchCV(KNeighborsClassifier(), param_grid, cv=5)
grid_search.fit(X_train, y_train)
print("Best parameters:", grid_search.best_params_)
print("Best cross-validation score:", grid_search.best_score_)
print("Test set score:", grid_search.score(X_test, y_test))
重要提示:使用网格搜索找到最佳参数后,在最终报告模型性能时,应在一个独立的、未参与参数搜索的测试集上进行评估,以避免乐观偏差。





深入线性模型与决策树 🌲


线性模型与正则化


线性模型(如线性回归、逻辑回归)通过特征的线性组合进行预测。为了防止过拟合,可以引入正则化项:
- 岭回归:使用 L2 正则化,使系数趋向于零但通常不为零。
- Lasso回归:使用 L1 正则化,可以使某些系数恰好为零,从而实现特征选择。
- 弹性网络:结合 L1 和 L2 正则化。
正则化强度由参数 alpha(回归)或 C(分类,C 是 alpha 的倒数)控制。
决策树与随机森林









决策树通过一系列“是/否”问题(基于特征阈值)对数据进行递归划分。它们非常灵活,但容易过拟合。




随机森林是解决过拟合问题的强大方法。它构建许多决策树,每棵树在训练数据的自助采样子集上训练,并且在每个节点分裂时只考虑特征的随机子集。最终的预测是所有树预测的平均(回归)或投票结果(分类)。



随机森林的优点包括:
- 通常具有很高的准确性。
- 对特征缩放不敏感。
- 可以提供特征重要性度量。


from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, max_depth=5)
rf.fit(X_train, y_train)
print("Feature importances:", rf.feature_importances_)













异常检测与高级聚类 🎯

异常检测
异常检测旨在识别与大多数数据显著不同的样本。常用方法包括:
- 孤立森林:基于随机划分,异常点容易被“孤立”。
- 一类SVM:在只有正常数据的情况下,学习一个边界。
- 局部离群因子:基于邻居密度的比较。




高级聚类方法





除了 K-Means,还有其他聚类算法:
- DBSCAN:基于密度进行聚类,可以发现任意形状的簇,并能识别噪声点。它需要设定邻域半径
eps和最小样本数min_samples。 - 层次聚类:通过连续合并或分割簇来构建树状结构(树状图)。有单连接、全连接、Ward 等方法。








总结




本节课中我们一起学习了:
- 文本处理:使用词袋模型、TF-IDF 和 N-gram 将文本转换为特征。
- 模型评估:使用 K折交叉验证获得更可靠的性能估计。
- 参数调优:使用网格搜索寻找模型最佳超参数。
- 核心模型:深入了解了线性模型的正则化以及决策树/随机森林的工作原理。
- 进阶主题:简要介绍了异常检测的常用方法和 DBSCAN 等高级聚类算法。

这些工具和方法构成了使用 scikit-learn 进行机器学习实践的核心部分。
课程 P19:Dask 高级技巧 🚀
在本课程中,我们将学习 Dask 分布式调度器的高级功能,包括实时任务调度、异步操作、多客户端协作以及如何构建复杂的数据处理管道。我们将通过一个科学计算(同步辐射光束线数据处理)的实例来演示这些概念。
概述
Dask 不仅适用于大数据处理,其分布式调度器还提供了强大的实时控制和诊断功能。本节课将重点介绍如何利用 concurrent.futures 接口、动态计算图、多客户端工作负载以及工作进程协调原语(如队列和共享变量)来构建灵活、高效的并行应用。

分布式调度器的优势
上一节我们概述了课程内容,本节中我们来看看为何要使用 Dask 的分布式调度器,即使是在单机上。
Dask 的分布式调度器最初为分布式计算设计,但在单机环境下同样高效。它提供了一个中心化的调度器来协调多个工作进程。这些工作进程可以位于不同机器、同一机器的不同进程,甚至是同一进程的不同线程中。
与传统的单机多线程调度器相比,分布式调度器提供了更丰富的功能和诊断信息。
以下是使用分布式客户端的基本方式:
from dask.distributed import Client
client = Client() # 在本地自动创建调度器和工作进程
核心优势:
- 丰富的诊断信息:通过 Web 仪表板实时可视化任务执行、数据传输和资源使用情况。
- 状态保持:调度器可以保留中间计算结果,供后续计算重复使用,避免重复计算。
- 轻量级:启动和销毁集群的开销极小(约数十毫秒),易于集成到各种应用中。
调度器性能对比
了解了分布式调度器的基本优势后,我们来看看在不同场景下如何选择调度器。
对于不同的计算类型,选择合适的调度器至关重要:
- Dask Array:涉及大量数值计算(如线性代数),通常继续使用多线程调度器(通过
scheduler='threads'指定)性能更佳,因为它能避免进程间通信开销。 - Dask DataFrame / Dask Bag:进行字符串处理等 Python 密集型操作时,分布式调度器(多进程)通常能提供更好的性能,因为它能绕过 Python 的全局解释器锁(GIL)。
经验法则:如果你的计算不是纯粹的数值计算(非“Guilt-free”),尝试分布式调度器可能会获得性能提升。
构建实时处理管道
在比较了调度器性能之后,我们将目光转向一个更复杂的应用场景:构建实时数据处理管道。
我们将以一个同步辐射光束线的数据处理为例。光子击中探测器后生成图像(NumPy 数组),这些图像需要经过一系列复杂处理(如使用 scikit-image),最终将原始和处理后的图像存入数据库。
系统需求:
- 实时性:科学家需要实时调整参数并看到反馈。
- 资源弹性:数据处理可能超出本地两台工作站的能力,需要能扩展到附近的数据中心集群。
- 复杂性:处理流程涉及多个步骤和自定义计算。
我们将使用 Dask 来构建这个管道系统,但请注意,Dask 本身不是一个图像处理管道系统,而是一个可以包裹在你自己的问题之上的通用工具。
并发 Futures 接口
要构建实时管道,我们需要一个灵活的任务提交接口。这就是 concurrent.futures 接口。
concurrent.futures 是 Python 的标准异步执行接口,Dask 对其进行了实现和扩展。它类似于 dask.delayed,但允许实时、动态地控制任务。
以下是其基本用法:
from dask.distributed import Client
client = Client()
# 提交任务,立即返回一个 Future 对象
future = client.submit(lambda x: x * 2, 10)
# Future 是一个指向尚未完成结果的引用
print(future.status) # 查看状态
result = future.result() # 阻塞并获取结果
关键特性:
- 动态依赖:可以提交依赖于其他
Future结果的新任务。 - 实时控制:可以随时取消任务、收集结果、或基于已完成任务的状态提交新任务。
- 异步收集:使用
as_completed()方法可以按照任务完成的顺序处理结果,这对于实现“最先完成”或“找到足够好的解就停止”等模式非常有用。
高级工作模式
掌握了基础的任务提交后,我们可以利用这些特性实现一些高级的工作模式。

利用 as_completed 和动态任务提交,可以实现自适应算法。例如,在优化问题中,我们可以:
- 同时测试多个初始点。
- 一旦某个点返回了较好的结果,立即在其附近区域提交更多测试任务。
- 逐渐缩小搜索范围,快速收敛到最优解。
这种模式使得计算能够根据中间结果进行动态引导,提高了搜索效率。
任务内提交任务与多客户端协作
当管道变得复杂时,一个任务可能需要在执行过程中派生出更多子任务。这引出了任务内提交任务和多客户端协作的概念。
通常,一个客户端控制调度器。但在 Dask 中,工作进程内部也可以创建客户端,从而向集群提交更多任务。这实现了多生产者-多消费者的复杂工作流模式。
在工作进程内部,可以使用以下函数:
get_client(): 获取一个连接到当前调度器的客户端对象。get_worker(): 获取当前工作进程对象,用于操作本地数据。
示例:一个计算斐波那契数列的函数,其中递归调用通过提交新任务实现:
def fib(n):
if n < 2:
return n
else:
client = get_client()
a = client.submit(fib, n - 1)
b = client.submit(fib, n - 2)
return a.result() + b.result()
此外,Dask 还提供了分布式队列(Queue)和分布式变量(Variable),供多个客户端之间协调元数据和传递消息。
光束线实例代码解析
最后,让我们将以上所有概念整合起来,看一个简化的光束线处理管道实例。
假设我们已有以下组件:
get_data_from_detector(): 模拟从探测器获取图像数据(返回随机数组)。process_image(img): 模拟处理图像的函数。save_to_database(img): 模拟将图像保存到数据库的函数(无返回值)。
我们的目标是:持续从两个光束线获取数据,分别进行处理,并保存结果。
架构设置:
- 在远程启动一个调度器。
- 在两个光束线工作站(
beam1,beam2)上分别启动工作进程并连接到调度器,通过name参数标识。 - 在数据中心启动一组通用的工作进程池。
管道逻辑:
# 简化的核心循环逻辑
while True:
# 从探测器获取数据
data = get_data_from_detector()
# 提交处理任务,并指定在对应的光束线机器上运行
processed_future = client.submit(process_image, data, workers={'beam1'})
# 提交保存任务,不关心返回值(fire-and-forget)
save_future = client.submit(save_to_database, processed_future)
save_future.add_done_callback(lambda f: f.exception()) # 可选:处理异常
time.sleep(sleep_interval) # 控制数据获取频率
通过调整 sleep_interval,可以模拟数据产生速率的变化。当速率加快时,Dask 会自动将溢出的任务分配给数据中心的其他工作进程,实现弹性伸缩。
总结与未来方向
本节课我们一起学习了 Dask 分布式调度器的高级技巧。
核心收获:
- 分布式调度器提供了强大的诊断、状态保持和新特性,值得在单机复杂任务中尝试。
- 对于不同计算类型(Array vs DataFrame),应选择合适的调度器以获得最佳性能。
- 通过
concurrent.futures接口,可以动态构建和修改计算图,实现实时、自适应的算法。 - 利用任务内提交任务和多客户端协调原语,可以构建复杂的工作流(如多生产者-消费者管道)。
- Dask 是一个通用框架,可以灵活地应用于各种领域(如科学仪器数据流处理),而不仅仅是“大数据”。
鼓励参与:
Dask 是一个服务于整个 PyData 生态的库。我们鼓励大家:
- 在自己的项目中使用 Dask,解决实际问题。
- 参与贡献,无论是修复 bug、添加新功能,还是完善文档。
- 探索诸如地理空间数据处理、流式 DataFrame 等正在发展的新方向。
备注:本教程中的所有代码示例均可在关联的演讲幻灯片和在线资源中找到。Web 诊断仪表板在启动分布式调度器时会自动运行,提供宝贵的运行时洞察。

课程 P2:计算统计学 SciPy 2017 教程 🧮


在本节课中,我们将要学习计算统计学的核心概念,包括效应量估计、置信区间计算和假设检验。我们将通过Python和Jupyter Notebook进行实践操作,理解如何用计算的方法替代传统的数学分析来解决统计推断问题。

概述 📋


统计推断的核心是从一个总体的小样本出发,试图利用样本信息来推断总体的特性。这通常涉及三个主要部分:估计效应大小、量化估计的精确度以及进行假设检验。传统上,这些问题通过数学分析解决,但本课程将展示如何通过计算(例如重采样和模拟)来更灵活、更直观地处理它们。
第一部分:效应量 📏



上一节我们介绍了统计推断的整体框架,本节中我们来看看如何量化效应大小。效应量是衡量观察到的差异或效应实际大小的指标,它比单纯的“是否显著”更为重要。


绝对差异与相对差异

首先,我们处理一个简单的例子:男性和女性的平均身高差异。我们可能得到一个以厘米为单位的绝对差异,但将其转换为百分比形式的相对差异通常更有意义。

以下是计算相对差异的几种方法:

- 基于男性均值:
(男性均值 - 女性均值) / 男性均值 - 基于女性均值:
(男性均值 - 女性均值) / 女性均值 - 基于总均值:
(男性均值 - 女性均值) / ((男性均值 + 女性均值)/2)
选择哪种方法取决于上下文,例如是否有一个明确的对照组。

误分类率与优势概率
除了相对差异,我们还可以用更实际的方式解释均值差异。
- 误分类率:如果我们根据身高阈值(如两组均值的中间点)来猜测一个人的性别,被错误分类的样本比例。在NumPy中,可以通过比较数组与阈值并求和来计算。
threshold = (mean_male + mean_female) / 2 misclassified = np.sum(male_sample < threshold) + np.sum(female_sample > threshold) misclassification_rate = misclassified / len(total_sample) - 优势概率:随机抽取一名男性和一名女性,男性身高更高的概率。可以通过配对样本或逻辑数组计算。
# 方法一:使用zip配对 pairs = zip(male_sample, female_sample) prob_superiority = sum(1 for m, f in pairs if m > f) / len(male_sample) # 方法二:使用NumPy广播 prob_superiority = np.mean(male_sample[:, None] > female_sample)

科恩效应值
科恩效应值(Cohen‘s d)是一种标准化的效应量度量,它表示两组均值差异相当于多少个合并标准差。这使得不同研究间的比较成为可能。



公式为:
d = (mean1 - mean2) / pooled_std

其中合并标准差 pooled_std 的计算考虑了每组的样本量和方差:
pooled_std = sqrt(((n1-1)*std1^2 + (n2-1)*std2^2) / (n1 + n2 - 2))



科恩效应值是一个无量纲数。根据经验,d=0.2 被视为小效应,d=0.5 为中等效应,d=0.8 为大效应。许多心理学中报告的性别差异效应值在0.5或以下,其实际重要性可能有限。
比例差异:以花生过敏研究为例
当处理比例数据(如花生过敏研究:避免花生的儿童过敏率为17%,食用花生的为3%)时,有多种方式表达效应大小。
以下是表达比例差异的不同方式及其特点:
- 绝对风险差:
17% - 3% = 14%。简单直观,但难以在不同基线风险的研究间比较。 - 相对风险:
17% / 3% ≈ 5.67或3% / 17% ≈ 0.176。表示倍数关系,但方向不对称,且公众可能难以理解“百分比的百分比”。 - 优势比:
(0.17/(1-0.17)) / (0.03/(1-0.03)) ≈ 6.6。在医学中常用,但同样存在不对称性,且解释需要专业知识。 - 对数优势比:
log(6.6) ≈ 1.89。具有优良的数学性质(对称性、可加性),并与贝叶斯因子相关,但最不为大众所熟悉。

选择哪种度量需要在数学严谨性和与受众的沟通有效性之间取得平衡。
第二部分:量化精确度 🎯
上一节我们学习了如何测量效应大小,本节中我们来看看如何评估这种测量的精确度。我们主要关注由随机抽样引起的误差(随机误差),尽管在实践中,抽样偏差和测量误差可能更为重要。
抽样分布与标准误

如果我们知道总体的真实分布(例如,假设成年人体重服从对数正态分布),我们可以通过模拟来理解估计的变异性。


核心思想是进行多次“模拟实验”:每次从已知总体中抽取一个样本(大小为 n),计算我们关心的统计量(如均值),然后观察这些估计值的分布。这个分布称为抽样分布。
- 标准误:抽样分布的标准差。它量化了由于随机抽样导致的估计值波动范围。
- 置信区间:抽样分布的一个区间,例如去除最低和最高5%的值后剩下的90%区间,称为90%置信区间。
随着样本量 n 增大,抽样分布变窄,标准误减小,置信区间宽度变窄。这体现了大样本带来的更高精确度。
自助法

然而,我们通常并不知道总体分布。自助法提供了一种解决方案:我们利用手头已有的单个样本,通过有放回地重采样来模拟总体,并生成许多新的“自助样本”。

自助法的框架如下:
- 原始数据与估计:从原始样本计算统计量(如均值),得到点估计。
- 构建模型:将原始样本视为“总体”的代理。
- 生成模拟数据:从原始样本中有放回地抽取多个自助样本(每个样本大小与原始样本相同)。
- 计算抽样分布:对每个自助样本计算相同的统计量,形成该统计量的自助抽样分布。
- 总结不确定性:计算自助抽样分布的标准误或百分位数置信区间。
这种方法的美妙之处在于,它可以应用于任何可以计算的统计量(均值、中位数、标准差等),而无需复杂的数学推导。
关于置信区间的重要说明
一个常见的误解是“真实值有90%的概率落在90%置信区间内”。在频率学派的框架下,真实值是固定的(非随机的),置信区间是随机的。更准确的解释是:如果重复进行多次实验,每次计算一个90%置信区间,那么这些区间中大约有90%会包含真实值。


更重要的是,置信区间只量化了随机抽样误差。当样本量很大时,标准误和置信区间会非常小,但这并不意味着估计非常准确,因为抽样偏差和测量误差等其他误差源可能占主导地位。一个很窄的置信区间只是排除了随机误差作为主要担忧,研究者应将注意力转向其他可能的误差来源。
第三部分:假设检验 ❓

上一节我们讨论了如何量化估计的不确定性,本节中我们来看看如何检验一个效应是否可能只是随机产生的。虽然效应大小和置信区间更为重要,但假设检验(特别是p值)在科学文献中广泛使用,因此需要正确理解和应用。

零假设显著性检验框架



假设检验的逻辑类似于数学中的反证法:
- 定义检验统计量:通常是效应大小的度量(如两组均值之差)。
- 建立零假设:通常假设效应不存在(例如,两组均值没有差异)。
- 模拟零假设世界:在零假设成立的条件下生成模拟数据。对于比较两组均值,通常将两组数据合并,然后随机打乱并重新分成两组(保持原样本量),这称为置换检验。
- 构建零分布:对大量模拟数据计算相同的检验统计量,得到在零假设下该统计量的可能分布(零分布)。
- 计算p值:计算在零分布中,出现大于或等于实际观察到的效应量(或更极端情况)的概率。即
p = (# of simulated stats >= observed stat) / (# of simulations)。
如果p值很小(例如小于0.05),意味着如果零假设为真,观察到如此大效应的可能性很低,从而提供证据反对零假设,支持效应是真实存在的。

p值的解释与局限

- 解释:p值是在零假设为真的前提下,观察到当前数据(或更极端数据)的概率。它不是零假设为真的概率,也不是效应大小的度量。
- 阈值:不应僵化地使用0.05作为“显著”的魔法阈值。建议采用数量级解释:p > 0.1 表示结果可能由偶然产生;0.01 < p < 0.1 是灰色区域,需要更多数据;p < 0.01 则提供较强证据反对零假设。
- 多重检验问题:如果进行多次检验,即使所有零假设都为真,也有机会得到一些小p值。应对方法包括调整显著性水平(如Bonferroni校正)或预先明确主要假设。
- 唯一检验:上述置换框架是通用的。传统的t检验、Z检验等都是针对特定统计量和假设,用分析方法近似p值的特例。计算框架允许你轻松检验任何自定义的统计量(如标准差的差异、分布形态的差异等)。

假设检验的定位
假设检验(p值)的主要作用是排除随机误差作为观察到的效应的合理解释。一个很小的p值让我们可以暂时将“随机巧合”从怀疑列表中划掉,转而关注其他更可能的问题,如测量误差或研究设计缺陷。它本身并不能证明效应是真实或重要的。
总结 🎓


本节课中我们一起学习了计算统计学的三个核心支柱:

- 效应量:量化观察到的差异的实际大小。这是最重要的信息,应优先报告。科恩效应值、优势比等标准化度量有助于跨研究比较。
- 置信区间:量化估计值的精确度,反映随机抽样带来的不确定性。自助法是一种强大且通用的计算工具,可用于估计任何统计量的置信区间。
- 假设检验与p值:评估观察到的效应是否可能仅由随机因素导致。应正确理解p值的含义,避免滥用,并记住它只是评估研究可靠性的众多方面之一。
记住,统计推断的目标是理解世界。效应大小告诉我们发生了什么,置信区间告诉我们对此有多确信,而p值则帮助我们排除一种特定的错误可能性。始终将效应大小放在首位,并意识到所有统计结论都依赖于模型假设,且可能受到抽样偏差、测量误差等未量化因素的影响。

推荐资源:想深入了解这些内容,可以参考Allen Downey的书籍《Think Stats》(O‘Reilly出版,也有免费在线版),其中详细介绍了探索性数据分析、可视化以及本教程涉及的统计推断方法。
🐍 P20:面向数据科学家的Cython教程


在本节课中,我们将学习Cython的基础知识。Cython是Python的一个超集,它允许我们轻松地访问C和C++级别的结构。我们将通过比较纯Python代码和Cython代码的性能,来了解Cython如何加速我们的程序,特别是在数值计算密集型任务中。
1️⃣ 为什么Python在某些情况下“慢”?
上一节我们介绍了Cython的基本概念。本节中,我们来看看为什么纯Python代码在某些计算密集型任务中可能表现不佳。Python本身并不慢,但对于大量重复的简单操作(如加法),其解释型特性和动态类型检查会带来显著的开销。
Python执行一个简单的加法操作 a + b 时,解释器需要执行多个步骤:
- 解释字节码:将
BINARY_ADD这样的字节码指令转换为具体的C API调用。 - 动态类型查找:由于Python的强多态性,解释器在运行时必须检查操作数
a和b的类型,并查找正确的加法函数(例如,整数加法、浮点数加法或字符串连接)。 - 错误检查和内存管理:进行类型检查、引用计数增减等安全操作。
- 结果包装:将C级别的计算结果重新包装成Python对象返回。
这些步骤对于单次操作来说开销很小,但在循环中执行数百万次时,累积的开销就会变得非常可观。

以下是一个纯Python加法函数及其执行时间的例子:
def foo(a, b):
return a + b
# 使用IPython的 %timeit 魔法函数计时
%timeit foo(1, 2)
# 输出示例:95.7 ns ± 0.96 ns per loop

2️⃣ Cython初体验:从编译中获益
上一节我们看到了Python解释器的开销。本节中,我们来看看仅仅将Python代码编译成C扩展,能带来多少性能提升。
Cython是一个将Python代码(.pyx 文件)首先“转译”为C代码,然后再编译为机器码(.so 或 .pyd 文件)的工具。即使不添加任何静态类型声明,这个编译步骤本身也能消除部分解释开销。
以下是使用Cython编译相同加法函数的步骤:
%%cython
def cyfoo(a, b):
return a + b
# 计时编译后的函数
%timeit cyfoo(1, 2)
# 输出示例:82.8 ns ± 0.5 ns per loop
性能对比:
- 纯Python
foo: ~95.7 纳秒 - Cython
cyfoo: ~82.8 纳秒 - 提升: 约 13.5%
这个提升主要来自于跳过了字节码解释步骤,直接调用编译后的函数。然而,真正的性能飞跃来自于添加静态类型声明。
3️⃣ 静态类型声明:性能飞跃的关键
上一节我们看到编译带来的小幅提升。本节中,我们来看看如何通过声明C级别的静态类型来获得巨大的性能提升。
在Cython中,我们可以使用 cdef 关键字来声明变量、函数参数和返回值的C数据类型(如 int, double)。这允许Cython生成高效的、直接操作内存的C代码,避免了Python对象的动态类型检查和操作开销。


让我们以计算阶乘的递归函数为例:
1. 纯Python版本:
def py_fact(n):
if n <= 1:
return 1
return n * py_fact(n-1)
%timeit py_fact(20)
# 输出示例:3 µs ± 15.3 ns per loop
2. Cython版本(无类型声明):
仅编译,逻辑不变。

%%cython
def cy_fact(n):
if n <= 1:
return 1
return n * cy_fact(n-1)

%timeit cy_fact(20)
# 输出示例:1.25 µs ± 4.37 ns per loop
提升: 约 2.4倍
3. Cython版本(添加参数类型声明):
我们声明参数 n 为C的 double 类型。
%%cython
def cy_fact_double(double n):
if n <= 1:
return 1
return n * cy_fact_double(n-1)
%timeit cy_fact_double(20)
# 输出示例:1 µs ± 2.22 ns per loop
提升: 约 3倍
4. Cython版本(cpdef 与完整类型声明):
使用 cpdef 创建同时可供C和Python调用的函数,并声明返回类型。

%%cython
cpdef double cp_fact(double n):
if n <= 1:
return 1
return n * cp_fact(n-1)
%timeit cp_fact(20)
# 输出示例:80 ns ± 0.156 ns per loop
提升: 约 37.5倍 (相比最初的3µs)
为什么使用 double 而不是 int?
阶乘结果增长极快,使用C的 int 或 long 类型很容易导致整数溢出,得到错误结果。double(双精度浮点数)提供了更大的数值范围,虽然可能有精度损失,但在此类计算中更安全。

迭代 vs 递归:
Python对递归优化不佳。将递归改为迭代通常能进一步提升性能。
%%cython
cpdef double cp_fact_loop(int n):
cdef double r = 1
cdef int i
for i in range(2, n+1):
r *= i
return r




%timeit cp_fact_loop(20)
# 输出示例:65 ns ± 0.091 ns per loop

4️⃣ Cython中的数据类型

上一节我们看到了类型声明的威力。本节中,我们系统地了解一下Cython中可用的数据类型。
Cython是Python的超集,因此所有Python类型(如 list, dict, str)依然可用。此外,它引入了完整的C类型系统。
C类型声明 (cdef)
使用 cdef 声明的变量是纯粹的C变量,只能在Cython代码内部访问,不能直接从Python交互。


基本数值类型:
%%cython
cdef int a = 10 # 有符号整数
cdef unsigned long b = 5 # 无符号长整数
cdef long long c = 100 # 长整型
cdef float d = 3.14 # 单精度浮点数
cdef double e = 2.718 # 双精度浮点数(常用)
cdef long double f = 1.0 # 扩展精度浮点数
复数类型:
%%cython
cdef float complex fc = 1+2j
cdef double complex dc = 3+4j
字符串/字节类型:
在Python 3中需区分Unicode字符串和字节。
%%cython
cdef str s = "Hello" # Python Unicode 字符串对象
cdef bytes b = b"World" # Python 字节对象
# C级别的字符/字符串(谨慎使用,可能破坏Python的不可变性保证)
cdef char c = 'A'
cdef char *c_buffer = "dangerous"
导入C级别的模块:
使用 cimport 将C结构导入到Cython的C命名空间,用于高性能操作。
%%cython
from libc.math cimport log # 导入C标准库的log函数,而不是Python的math.log
cimport numpy as cnp # 导入NumPy的C接口,用于高效数组操作

5️⃣ 函数定义:def, cdef, cpdef
上一节我们了解了数据类型。本节中,我们来看看在Cython中定义函数的三种不同方式,它们决定了函数的可访问性和性能。




1. def 函数
- 使用普通的Python
def关键字定义。 - 接受Python对象作为参数,返回Python对象。
- 可以从Python代码中正常调用。
- 性能提升有限,主要来自编译优化,但内部操作仍涉及Python对象开销。




%%cython
def add_def(a, b):
return a + b
# 可从Python调用: add_def(1, 2)



2. cdef 函数
- 使用
cdef关键字定义。 - 接受和返回C类型或Python类型。
- 只能在Cython模块内部或其他Cython代码中调用,无法直接从Python代码中访问。
- 性能最高,因为避免了与Python交互的大部分开销。
- 可用于内部计算,作为
cpdef函数的高性能核心。



%%cython
cdef int add_cdef(int a, int b):
return a + b
# 无法从Python直接调用 add_cdef(1, 2)






3. cpdef 函数
- 使用
cpdef关键字定义。 - 兼具两者优点:Cython会生成两个版本——一个高效的C函数(供Cython内部调用)和一个Python包装器(供Python代码调用)。
- 参数和返回类型必须是能转换为Python类型的C类型。
- 是提供高性能接口的推荐方式。


%%cython
cpdef int add_cpdef(int a, int b):
return a + b
# 可从Python调用: add_cpdef(1, 2)
# 也可在Cython内部被高效调用
异常处理:
在 cdef 或 cpdef 函数中,可以使用 except 关键字将C级别的错误转换为Python异常。

%%cython
cpdef int divide(int a, int b) except? -1:
if b == 0:
raise ZeroDivisionError("division by zero")
return a / b
















6️⃣ 性能分析与代码注解
上一节我们学会了如何定义函数。本节中,我们学习如何分析和优化Cython代码的性能。
使用Python分析器 (%prun)
IPython的 %prun 魔法命令可以分析代码执行时间。但对于 cdef 函数,Python分析器默认无法跟踪,因为它们本质上是C函数。
启用Cython函数分析:
在Cython代码块顶部添加指令 %%cython --profile True,或在 .pyx 文件开头添加 # cython: profile=True。这会注入额外的代码,使Python分析器能够跟踪 cdef 函数的调用。
%%cython --profile True
def outer(n):
cdef int i, res = 0
for i in range(n):
res += inner(i)
return res
cdef int inner(int i):
return i * i
# 然后使用 %prun 分析
%prun outer(1000)
注意:开启分析会引入额外开销,仅用于开发调试,不应在生产中使用。
使用注解报告 (-a 标志)
%%cython -a 命令会生成一个HTML注解报告,直观显示每一行Cython代码对应的生成C代码量。
- 白色背景:该行代码被翻译成高效的、少量的C代码。
- 黄色背景:该行代码产生了大量C代码,通常意味着存在Python对象交互、类型未知或函数调用开销,是潜在的优化点。


优化目标就是通过添加类型声明、使用C函数等方式,尽可能让代码行“变白”。




%%cython -a
def sum_squares(list lst):
cdef int total = 0
cdef int i
cdef int val
for i in range(len(lst)):
val = lst[i] # 这行可能是黄色的,因为lst[i]返回Python对象
total += val * val
return total
查看报告后,你可能会尝试将 list 参数改为类型化的内存视图或NumPy数组以获得更好性能。



7️⃣ 与NumPy高效交互



上一节我们学习了分析工具。本节中,我们来看一个数据科学中的常见场景:优化NumPy数组操作。





Cython通过 cimport numpy 提供了与NumPy C API的直接接口,可以绕过Python调用开销,直接访问数组底层数据。
示例:计算香农熵
公式:\(H(p) = -\sum_{i} p_i \log(p_i)\)
1. 纯NumPy版本(基线):
import numpy as np
def entropy_numpy(p):
return -np.sum(p * np.log(p))

2. 初级Cython版本(提升有限):
仅编译和简单声明。
%%cython
import numpy as np
cimport numpy as cnp


def entropy_cy_v0(cnp.ndarray p):
return -np.sum(p * np.log(p)) # 仍调用Python的np.sum和np.log
3. 优化版本1(使用C函数和循环):
%%cython
import numpy as np
cimport numpy as cnp
from libc.math cimport log
def entropy_cy_v1(cnp.ndarray[double, ndim=1] p):
cdef double total = 0.0
cdef Py_ssize_t i
for i in range(p.shape[0]):
if p[i] > 0:
total += p[i] * log(p[i])
return -total
cnp.ndarray[double, ndim=1]:声明p是一维双精度浮点数组。from libc.math cimport log:导入C的log函数,而非Python的。- 手动循环避免调用
np.sum。
4. 优化版本2(使用内存视图,更通用):
内存视图语法更简洁,且不限于NumPy数组。
%%cython
from libc.math cimport log

def entropy_cy_v2(double[:] p_view):
cdef double total = 0.0
cdef Py_ssize_t i
# 可选的:关闭边界检查以获取极致性能(需确保代码安全)
# with nogil:
for i in range(p_view.shape[0]):
if p_view[i] > 0:
total += p_view[i] * log(p_view[i])
return -total



# 调用时可以直接传递NumPy数组
import numpy as np
arr = np.random.rand(1000)
arr = arr / arr.sum()
result = entropy_cy_v2(arr)

关键点:
- 将
np.log替换为C的log。 - 将通用的
ndarray声明为具体的ndarray[double, ndim=1]或使用double[:]内存视图。 - 用C循环替代
np.sum。 - 内存视图(
double[:])是推荐的方式,它语法干净且支持多种缓冲区协议对象。



🎯 总结

本节课中我们一起学习了Cython的核心概念和使用方法:
- 动机:Cython通过将Python代码编译为C扩展,并允许声明静态C类型,可以显著提升计算密集型任务的性能(通常可达数十倍甚至数百倍)。
- 关键步骤:从
.pyx文件到.c文件(转译),再到.so/.pyd文件(编译),最后被Python导入。 - 类型声明:使用
cdef声明C类型(如int,double,cnp.ndarray)是获得性能提升的关键。 - 函数类型:
def:供Python调用,性能提升有限。cdef:纯C函数,性能最高,但无法从Python直接调用。cpdef:两者结合,是创建高性能接口的推荐方式。
- 工具:使用
%%cython -a生成注解报告来定位性能瓶颈,使用--profile参数使cdef函数可被Python分析器跟踪。 - 与NumPy集成:通过
cimport numpy和类型化声明(如double[:]内存视图),可以直接操作NumPy数组的底层数据,避免Python层开销。
Cython让你能够以接近Python的语法编写高性能的C扩展,特别适合优化循环密集型、数值计算密集型的代码段,是数据科学家和科学计算工程师工具箱中的重要工具。
课程 P21:使用 HoloViews 与 Bokeh 进行交互式数据可视化 🎨
在本节课中,我们将学习如何使用 HoloViews 和 Bokeh 这两个强大的 Python 库来创建交互式数据可视化应用。HoloViews 提供了一个高级、语义化的接口,让你专注于数据本身的意义,而不是繁琐的绘图细节。Bokeh 则负责将这些语义对象渲染成美观、可交互的图表。我们将从基础概念开始,逐步构建一个复杂的交互式仪表板。
环境设置与介绍 🛠️
首先,我们需要确保开发环境已正确设置。本教程基于一个特定的 Git 代码库和 Conda 环境。

以下是设置步骤:


- 克隆指定的 Git 代码库。
- 使用提供的 YAML 文件创建 Conda 环境:
conda env create -f environment.yml。 - 进入环境后,更新到最新版本的库(例如
taulivis 1.8.1)。 - 进入
notebooks目录,使用命令jupyter notebook --NotebookApp.iopub_data_rate_limit=1.0e10启动 Jupyter Notebook,该参数用于提高数据传输限制,以便处理大型数据集。 - 打开
00_welcome.ipynb笔记本文件开始学习。

如果在设置过程中遇到问题,可以通过课程提供的 Slack 频道寻求帮助。
理解 HoloViews 核心:元素 📦

上一节我们完成了环境设置,本节中我们来看看 HoloViews 的核心抽象——元素。

元素是数据的容器,它们代表你的数据并拥有丰富的视觉表示,但其设计初衷是让你关注数据的语义信息(如“距离”、“高度”),而非视觉样式(如线条粗细、颜色)。你可以将元素视为带有可视化能力的“数据对象”。
HoloViews 提供了多种元素类型,例如 Curve(曲线)、Scatter(散点图)、Area(面积图)和 Image(图像)。你可以在 HoloViews 官网的参考图库中查看所有可用的元素类型。
创建第一个元素
让我们从一个简单的 Curve 元素开始。
import holoviews as hv
hv.extension('bokeh') # 指定使用 Bokeh 进行渲染
# 创建数据
x_values = [1, 2, 3, 4, 5]
y_values = [1, 4, 9, 16, 25]
# 创建 Curve 元素
simple_curve = hv.Curve((x_values, y_values))
simple_curve
运行上述代码,你将看到一个用 Bokeh 渲染的抛物线图。你可以使用 Bokeh 的工具进行缩放、平移等交互操作。
元素对象不仅具有可视化表示,还有文本表示。打印 simple_curve 对象可以看到其结构。通过 .data 属性可以访问元素内部的数据,对于上面的列表数据,它通常会被转换为 Pandas DataFrame。

探索不同元素类型
以下是几种相似但视觉表现不同的元素:
hv.Curve((x, y)):绘制连接数据点的曲线。hv.Area((x, y)):绘制曲线下方的填充区域。hv.Scatter((x, y)):绘制独立的散点。

你可以尝试将上面代码中的 hv.Curve 替换为 hv.Area 或 hv.Scatter,观察其变化。

为数据添加语义信息
目前我们的数据只是简单的数字列表。我们可以为其添加维度标签,以说明数据的实际意义。


# 添加关键维度 (kdims) 和值维度 (vdims) 标签
trajectory_curve = hv.Curve((x_values, y_values), kdims='distance', vdims='height')
trajectory_curve



现在,图表的坐标轴标签变成了“distance”和“height”。kdims 通常对应自变量(如X轴),vdims 对应因变量(如Y轴)。这些维度信息被封装在丰富的 Dimension 对象中。

元素间的转换
由于元素关注数据语义,因此在数据结构相似的元素间可以轻松转换。
# 将 Curve 转换为 Scatter
trajectory_scatter = hv.Scatter(trajectory_curve)
trajectory_scatter
转换后,新的 Scatter 元素会保留原 Curve 元素的所有维度标签信息。
使用不同数据格式 📊

上一节我们使用列表创建了元素,本节中我们来看看 HoloViews 如何支持 NumPy 数组和 Pandas DataFrame。



使用 NumPy 数组


对于二维数组,对应的元素是 Image。



import numpy as np


# 创建一个二维数组(例如,正弦余弦组合的图案)
x = np.linspace(0, 10, 200)
y = np.linspace(0, 10, 200)
X, Y = np.meshgrid(x, y)
array_data = np.sin(X) * np.cos(Y)



# 创建 Image 元素
simple_image = hv.Image(array_data)
simple_image

Image 元素将数组值映射为颜色,直观展示数据结构。你可以尝试修改数组生成函数(例如 np.sin(X)**2 * np.cos(Y)),创建不同的图案。

和 Curve 一样,你也可以为 Image 添加维度标签。Image 有两个 kdims(用于索引数组的行和列)和一个 vdim(数组值本身)。
labeled_image = hv.Image(array_data, kdims=['longitude', 'latitude'])
labeled_image
使用 Pandas DataFrame


当数据已存在于 DataFrame 中时,可以直接将其传入元素,并通过列名指定维度。


import pandas as pd
# 假设 `economics_df` 是一个包含‘year’, ‘growth’, ‘unemp’等列的DataFrame
# 选取美国的数据
us_data = economics_df[economics_df['country'] == 'United States']
# 绘制 GDP 增长曲线
gdp_growth_curve = hv.Curve(us_data, kdims='year', vdims='growth')
gdp_growth_curve
通过更改 vdims 参数,可以轻松绘制不同的指标,例如失业率:

unemp_curve = hv.Curve(us_data, kdims='year', vdims='unemp')
unemp_curve

完善维度标签

为了生成更专业的图表,我们需要更详细的轴标签和单位信息。


# 为维度设置更友好的标签和单位
gdp_growth_labeled = gdp_growth_curve.redim.label(growth='GDP Growth').redim.unit(growth='%')
gdp_growth_labeled


redim.label 方法用于修改维度的显示名称,redim.unit 用于添加单位。这些信息会体现在最终的图表轴上。
组合与布局元素 🧩


在数据分析中,我们经常需要并排比较多个图表。HoloViews 使用 + 和 * 运算符来组合元素。

使用 Layout(+ 运算符)

+ 运算符将元素排列在不同的子图中,创建一个布局。
# 创建同一个数据的不同视图
curve_view = hv.Curve(trajectory_curve)
scatter_view = hv.Scatter(trajectory_curve)
area_view = hv.Area(trajectory_curve)
spikes_view = hv.Spikes(trajectory_curve)



# 使用 + 创建布局
layout = curve_view + scatter_view + area_view + spikes_view
layout


默认情况下,共享相同维度的子图会链接其交互(如缩放、平移),这是一个有用的特性。布局对象可以通过属性或字典方式访问其子元素(例如 layout.Curve_I 或 layout[‘Curve’, ‘I’])。
为元素添加组和标签
为了更好地组织布局中的元素,可以为每个元素设置 group 和 label。
# 为元素设置组和标签
cannonball = trajectory_curve.relabel(group='Trajectory', label='Cannonball')
filled = hv.Area(trajectory_curve).relabel(group='Trajectory', label='Filled')
labeled_layout = cannonball + filled
labeled_layout


设置后,布局中每个子图的标题会显示这些信息,并且在访问子图时也可以使用这些标签(例如 labeled_layout.Trajectory.Filled)。

使用 Overlay(* 运算符)

* 运算符将元素叠加在同一个坐标系中,创建一个覆盖层。
# 在同一坐标系中叠加曲线和钉状图
overlay = trajectory_curve * hv.Spikes(trajectory_curve)
overlay


覆盖层适用于需要同时显示多个数据序列的场景,并且会自动处理颜色循环和图例。和布局一样,覆盖层也支持通过组和标签进行索引。
克隆元素以创建变体
clone 方法可以基于现有元素创建一个新实例,并允许修改其属性(如数据或标签),而不影响原元素。
# 克隆一个元素并修改其标签
tennis_ball = cannonball.clone(label='Tennisball')
comparison_overlay = cannonball * tennis_ball
comparison_overlay
你可以尝试使用 clone 方法修改数据(例如将 y 值减半以模拟“手抛球”轨迹),并将其添加到覆盖层中进行比较。



总结 📝




本节课我们一起学习了使用 HoloViews 和 Bokeh 进行交互式数据可视化的基础。


我们首先了解了 HoloViews 元素 作为语义化数据容器的核心概念,学会了如何创建 Curve、Scatter、Image 等基本元素,并为它们添加有意义的维度标签和单位。

接着,我们探索了如何将 NumPy 数组 和 Pandas DataFrame 这两种常见数据格式无缝转换为可视化元素。
最后,我们掌握了组合元素的强大技巧:使用 + 运算符创建并排的 布局,以及使用 * 运算符创建叠加的 覆盖层。我们还学习了通过 relabel 和 clone 方法来更好地组织和区分不同的数据视图。

通过这些基础构建模块,你已经可以开始创建结构清晰、富有表现力的静态和交互式图表。在接下来的课程中,我们将深入探讨如何定制图表样式、处理更大规模的数据集以及构建完整的交互式应用程序。
课程 P22:软件工匠科学Python课程第一部分 SciPy 2017 教程 🐍
在本课程中,我们将学习Python编程语言的基础知识,特别是如何将其应用于科学计算。课程将从命令行(Shell)的基本操作开始,然后过渡到Python的核心概念,包括变量、数据类型、数据结构、函数以及如何使用NumPy和Matplotlib等强大的库进行数据处理和可视化。
课前准备与介绍 👋
大家好。我是Maxim Belkin,今天将由我来教授这个教程。


这个教程遵循软件工匠(Software Carpentry)的标准,旨在让大家通过亲手实践来学习。通常,这类研讨会会有很多助教,但今天我们只有一两位助教,Heather Rose会尽力为大家提供帮助。




由于我们现场有超过70位学员,而通常软件工匠的标准是每位助教对应10-15位学员,这对我们来说是一个挑战。因此,我希望大家能互相帮助,共同学习。


为了了解大家的基础,我创建了一个Slack频道用于互动和投票。请大家加入,以便我能更好地掌握大家的学习进度和遇到的问题。


第一部分:为什么需要Shell? 💻

在开始Python学习之前,我们需要掌握一些命令行(Shell)的基础知识。虽然现代操作系统都有图形用户界面(GUI),但在处理大量科学数据文件时,命令行提供了更高效、可扩展的操作方式。

命令行界面(CLI)允许我们通过输入命令来与计算机交互,这与图形用户界面(GUI)形成对比。例如,当你有数百万个数据文件时,在图形界面中滚动查找特定文件会非常困难,而使用命令行工具则可以快速定位和处理。
基本Shell命令
首先,打开你的终端(在Mac或Linux上)或Git Bash(在Windows上)。你会看到一个命令提示符,通常是$符号,表示系统正在等待你输入命令。
你可以通过输入PS1='$ '来简化提示符,这只改变显示,不影响系统。

要查看当前所在目录,使用pwd命令(Print Working Directory)。
要查看当前目录下的文件和文件夹,使用ls命令(List)。
ls -l命令可以以详细列表形式显示信息,包括文件权限、所有者、大小和修改日期。
要获取命令的帮助信息,可以使用--help选项(例如ls --help)或man命令(例如man ls)。在Mac上,ls --help可能无效,需使用man ls。按q键退出帮助页面。
下载和处理数据
为了练习,我们需要下载一些示例数据。使用以下命令(Mac用户使用curl,Linux用户使用wget):
curl -O http://software-carpentry.org/v4/shell/data-shell.zip
或者直接在浏览器中打开该链接下载。
下载后,使用unzip命令解压文件:
unzip data-shell.zip
解压后,使用cd命令进入新创建的目录:
cd data-shell
目录和文件操作
使用mkdir命令创建新目录,例如mkdir thesis。
使用cat命令查看文件内容,例如cat creatures/basilisk.dat。
使用wc命令统计文件的行数、单词数和字符数,例如wc -l *.pdb。
通配符和重定向
通配符*可以匹配多个文件。例如,ls b*会列出所有以b开头的文件。
使用>符号可以将命令的输出重定向到文件,例如wc -l *.pdb > results.dat。如果文件已存在,>会覆盖它。
使用>>符号可以将输出追加到文件末尾,而不覆盖原有内容。
管道
管道|可以将一个命令的输出作为另一个命令的输入。例如,以下命令会统计所有.pdb文件的行数,然后排序,最后显示前三行:
wc -l *.pdb | sort -n | head -n 3
查找文件
使用find命令可以搜索文件。例如,在当前目录及其子目录中查找所有扩展名为.pdb的文件:
find . -type f -name "*.pdb"
-type f表示只查找文件,-name后面跟匹配模式。
删除文件
使用rm命令删除文件。为了安全,可以使用-i选项进行交互式删除,系统会询问你是否确认:
rm -i results.dat
第二部分:Python入门 🚀
上一节我们介绍了Shell的基础操作,本节我们将开始学习Python编程语言。Python以其简洁易读的语法和强大的库生态系统而闻名,非常适合科学计算。
首先,我们需要启动Python。在终端中输入python或python3(如果你安装了Anaconda Python 3)。你应该会看到类似>>>的提示符,这表明你已进入Python交互式解释器。



基本数据类型

Python支持多种基本数据类型:
- 整数:例如
1 - 浮点数:例如
2.3 - 字符串:用单引号或双引号括起来的文本,例如
'hello'或"world" - 布尔值:
True或False - 复数:例如
1+2j
你可以使用type()函数来查看任何值的类型。
变量和赋值






使用等号=可以将值赋给变量:



a = 1
b = 2.3
c = 'hello'
使用print()函数可以输出变量的值:
print(a)
基本运算符
Python支持常见的数学和比较运算符:
+,-,*,/:加、减、乘、除**:幂运算//:整数除法%:取模==,!=,>,<,>=,<=:比较运算符,返回布尔值
注意:在Python 3中,/运算符执行浮点除法。要执行整数除法,需使用//。
字符串可以使用+进行拼接,使用*进行重复:
'hello' + 'world' # 结果是 'helloworld'
'ha' * 3 # 结果是 'hahaha'
列表(Lists)
列表是一种有序的集合,可以包含不同类型的元素。列表用方括号[]表示:
fruits = ['apple', 'banana', 'cherry']
可以通过索引访问列表元素,索引从0开始:
fruits[0] # 'apple'
fruits[-1] # 'cherry' (负数索引表示从末尾开始)
可以使用切片获取子列表:
fruits[0:2] # ['apple', 'banana'] (包含起始索引,不包含结束索引)
列表是可变的,可以使用append()方法添加元素,使用pop()方法移除元素:
fruits.append('date')
last_fruit = fruits.pop()
重要:当将一个列表赋值给另一个变量时,两个变量会指向同一个列表对象。修改其中一个会影响另一个。如果需要复制列表,请使用copy()方法或list()函数。
元组(Tuples)
元组与列表类似,但它是不可变的(创建后不能修改)。元组用圆括号()表示:
coordinates = (10, 20)
字典(Dictionaries)
字典是一种键值对集合。字典用花括号{}表示,键和值之间用冒号:分隔:
conversions = {'inches': 12, 'feet': 30.48}
可以通过键来访问值:
conversions['inches'] # 12
可以添加新的键值对:
conversions['yards'] = 0.9144


函数(Functions)

函数是可重用的代码块。使用def关键字定义函数:
def multiply(x, y):
"""返回两个数的乘积。"""
result = x * y
return result


函数可以包含文档字符串(docstring),用于说明函数的功能。可以使用help()函数或?(在Jupyter中)查看文档字符串。



第三部分:使用NumPy和Matplotlib进行数据分析 📊

上一节我们学习了Python的核心语法,本节我们将利用NumPy和Matplotlib这两个强大的库来处理和可视化科学数据。
首先,我们需要导入这些库:
import numpy as np
import matplotlib.pyplot as plt
使用NumPy加载数据
NumPy的loadtxt函数可以方便地从文本文件(如CSV)加载数据:
data = np.loadtxt(fname='data/inflammation-01.csv', delimiter=',')
加载的数据是一个NumPy数组(ndarray)。你可以使用shape属性查看数组的维度,使用索引和切片访问数据:
print(data.shape) # 例如 (60, 40),表示60行40列
print(data[0, 0]) # 访问第一行第一列的元素
print(data[0:4, 0:10]) # 访问前4行、前10列的数据
NumPy数组支持向量化操作,这意味着数学运算会应用到每个元素上,速度非常快:
double_data = data * 2.0
你可以沿特定轴(axis)计算统计值,例如平均值、最大值、最小值:
mean_per_patient = data.mean(axis=1) # 沿行(axis=1)计算每位患者的平均值
max_per_day = data.max(axis=0) # 沿列(axis=0)计算每天的最大值
使用Matplotlib可视化数据
Matplotlib是一个绘图库。在Jupyter Notebook中,我们使用%matplotlib inline魔法命令让图表直接显示在笔记本中。
plt.imshow()函数可以显示二维数组(如图像或矩阵):
plt.imshow(data)
plt.show()
你可以创建包含多个子图的图形:
fig = plt.figure(figsize=(10.0, 3.0)) # 创建图形,设置大小
# 创建三个并排的子图
axes1 = fig.add_subplot(1, 3, 1)
axes2 = fig.add_subplot(1, 3, 2)
axes3 = fig.add_subplot(1, 3, 3)
# 在每个子图中绘制不同的数据
axes1.set_ylabel('average')
axes1.plot(data.mean(axis=0))
axes2.set_ylabel('max')
axes2.plot(data.max(axis=0))
axes3.set_ylabel('min')
axes3.plot(data.min(axis=0))
fig.tight_layout() # 自动调整子图参数,使之填充整个图像区域
plt.show()
通过观察平均值、最大值和最小值随时间(天数)变化的图表,我们可能会发现数据中的异常模式,从而引导进一步的分析。
总结 🎯

在本课程中,我们一起学习了:


- Shell基础:包括导航文件系统、操作文件和目录、使用通配符、重定向和管道,这些是高效管理数据文件的必备技能。
- Python核心概念:包括变量、基本数据类型(整数、浮点数、字符串、布尔值)、数据结构(列表、元组、字典)以及如何定义和调用函数。
- 科学计算库:我们介绍了如何使用NumPy加载和处理数组数据,以及如何使用Matplotlib创建数据可视化图表。

这些工具构成了使用Python进行科学计算的基础。通过将小的、可重用的函数与强大的库相结合,你可以有效地分析数据、自动化任务并清晰地展示你的研究成果。


课程 P23:使用 Dask 并行化科学 Python 🚀
概述
在本课程中,我们将学习如何使用 Dask 框架来并行化 Python 科学计算任务。Dask 是一个灵活的并行计算库,它允许我们处理超出单机内存限制的大型数据集,并利用多核或分布式集群进行高效计算。我们将从基础概念开始,逐步探索 Dask 的核心组件,包括 delayed、DataFrame、Array 和 Bag,并最终了解如何利用分布式调度器进行大规模计算。
1. 使用 Dask Delayed 进行并行化
上一节我们介绍了课程概述,本节中我们来看看 Dask 最基础的并行化工具:delayed。delayed 是一个装饰器或函数,它能使任何函数或操作变得“惰性”(lazy),即不会立即执行,而是构建一个任务图(task graph),稍后统一调度执行。
我们首先定义两个简单的函数,模拟耗时操作:
import time
from dask import delayed
def inc(x):
time.sleep(1)
return x + 1
def add(x, y):
time.sleep(1)
return x + y
如果串行执行这些函数,总耗时将是 3 秒。
# 串行执行
x = inc(1) # 耗时 1 秒
y = inc(2) # 耗时 1 秒
z = add(x, y) # 耗时 1 秒
# 总耗时约 3 秒
使用 delayed 进行并行化:
# 使用 delayed 包装函数,构建任务图
x = delayed(inc)(1)
y = delayed(inc)(2)
z = delayed(add)(x, y)
# 此时尚未执行任何计算,只是构建了图
# 调用 compute() 开始并行执行
result = z.compute() # 总耗时约 2 秒
为什么耗时是 2 秒而不是 3 秒? 因为 inc(1) 和 inc(2) 这两个任务之间没有依赖关系,Dask 可以将其调度到不同的工作线程上同时执行。我们可以通过可视化任务图来理解:
z.visualize()
任务图将显示两个独立的 inc 任务,最后汇聚到一个 add 任务。这体现了基本的并行模式。
练习:并行化一个 for 循环


以下是需要并行化的串行代码:
data = [1, 2, 3, 4, 5, 6, 7, 8]
results = []
for x in data:
results.append(inc(x))
total = sum(results)
任务:使用 delayed 重写以上代码,使其能够并行执行。
解决方案思路:
- 使用
delayed(inc)创建一个惰性版本的函数。 - 在循环中应用这个惰性函数,得到一系列
delayed对象。 - 使用
delayed(sum)来惰性地对结果列表求和。 - 最后调用
compute()。
delayed_inc = delayed(inc)
delayed_results = [delayed_inc(x) for x in data]
total = delayed(sum)(delayed_results)
result = total.compute() # 并行执行
在这个例子中,8 个 inc 任务可以并行执行,因此总耗时接近 1 秒(假设有足够多的 CPU 核心)。
引入控制流
当代码中包含条件判断时,并行化需要稍加注意。考虑以下函数:
def double(x):
time.sleep(1)
return x * 2
def is_even(x):
return x % 2 == 0
串行逻辑如下:
data = [1, 2, 3, 4, 5, 6, 7, 8]
results = []
for x in data:
if is_even(x):
results.append(double(x))
else:
results.append(inc(x))
total = sum(results)
重要原则:delayed 对象在构建图时,其具体值是未知的,因此不能直接用于 if 判断。我们需要在图构建阶段就确定执行路径。
解决方案:只对实际耗时的计算函数(double, inc)和最终的聚合函数(sum)使用 delayed。条件判断 is_even 作用于原始数据,应即时执行。
delayed_double = delayed(double)
delayed_inc = delayed(inc)
results = []
for x in data: # x 是原始数据,不是 delayed 对象
if is_even(x): # 即时判断
results.append(delayed_double(x))
else:
results.append(delayed_inc(x))
total = delayed(sum)(results)
result = total.compute()
这样构建的图,会根据每个数据点的奇偶性,选择不同的计算分支,并且所有分支可以并行执行。
真实案例:并行处理多个 CSV 文件
假设我们有一组 CSV 文件(例如,1990-1999 年纽约的航班数据),需要计算每个机场的平均起飞延误时间。串行处理代码如下:

import pandas as pd
import glob

files = glob.glob('nyc_flights/*.csv')
sums = {}
counts = {}
for f in files:
df = pd.read_csv(f)
grouped = df.groupby('origin')['dep_delay'].agg(['sum', 'count'])
for origin in grouped.index:
sums[origin] = sums.get(origin, 0) + grouped.loc[origin, 'sum']
counts[origin] = counts.get(origin, 0) + grouped.loc[origin, 'count']
# 计算最终平均值
mean_delay = {origin: sums[origin]/counts[origin] for origin in sums}
使用 delayed 并行化:
我们需要了解两个新知识点:
delayed对象支持大多数运算符和方法调用。例如,如果df是一个delayed对象(代表一个未来的 Pandas DataFrame),那么df.groupby(...)会自动返回一个新的delayed对象。dask.compute函数可以同时计算多个delayed对象,并自动合并共享子任务,避免重复计算。
from dask import compute

delayed_results = []
for f in files:
# 延迟读取 CSV
df = delayed(pd.read_csv)(f)
# 延迟执行分组聚合
grouped = df.groupby('origin')['dep_delay'].agg(['sum', 'count'])
delayed_results.append(grouped)

# 同时计算所有中间结果
computed_results = compute(*delayed_results)
# 后续的聚合在本地进行
sums, counts = {}, {}
for grouped in computed_results:
for origin in grouped.index:
sums[origin] = sums.get(origin, 0) + grouped.loc[origin, 'sum']
counts[origin] = counts.get(origin, 0) + grouped.loc[origin, 'count']
mean_delay = {origin: sums[origin]/counts[origin] for origin in sums}
性能考虑:由于 Python 的全局解释器锁(GIL),Pandas 的某些字符串操作可能无法充分利用多线程。在这个例子中,我们可能只能获得约 2 倍的加速,而不是理想的 8 倍(对应 8 个文件)。对于计算密集型任务,使用进程池或分布式调度器是更好的选择。
2. 使用 Dask DataFrame 处理表格数据 📊
上一节我们使用 delayed 手动构建了并行处理流水线,本节中我们来看看更高级的抽象:Dask DataFrame。它模仿了 Pandas DataFrame 的 API,但能自动对超出内存的数据进行分块并行处理。
创建 Dask DataFrame
Dask DataFrame 可以从多个 CSV 文件创建,底层是多个 Pandas DataFrame 的集合。
import dask.dataframe as dd
# 读取所有 CSV 文件,惰性操作
df = dd.read_csv('nyc_flights/*.csv')
read_csv 会查看第一个文件的一部分来推断数据类型。如果推断错误(例如,后续文件中某列的数据类型与首文件不同),可能会导致后续操作(如 tail)失败。解决方法是指定 dtype 参数。
dtype = {'tail_num': str, 'arr_time': float, 'cancelled': bool}
df = dd.read_csv('nyc_flights/*.csv', dtype=dtype)
基本操作
Dask DataFrame 支持许多常见的 Pandas 操作,但它们是惰性的。
# 查看前几行(这是立即执行的少数操作之一)
print(df.head())
# 计算行数(惰性操作)
n_rows = len(df) # 返回一个 delayed 对象
n_rows_computed = n_rows.compute() # 触发实际计算
# 布尔索引
early_flights = df[df['dep_delay'] < 0]
n_early = len(early_flights).compute()
分组聚合
分组聚合是数据分析中的常见操作,Dask DataFrame 可以高效地并行执行。
# 计算每个机场的平均延误
mean_delay = df.groupby('origin')['dep_delay'].mean().compute()
在底层,Dask 会对每个数据块执行分组聚合,然后对中间结果进行合并(reduce),这与我们之前用 delayed 手动实现的过程类似,但代码简洁得多。
共享中间结果
如果要计算同一数据集的多个统计量(如均值和标准差),分别调用 compute() 会导致数据被重复读取和计算。更高效的方式是使用 dask.compute 一次性计算所有需要的量。
from dask import compute
mean = df['dep_delay'].mean()
std = df['dep_delay'].std()
# 低效:分别计算
mean_result = mean.compute()
std_result = std.compute()
# 高效:同时计算
mean_result, std_result = compute(mean, std)
Dask 会自动优化任务图,共享 df['dep_delay'] 这个子任务,避免重复工作。
设置索引以提高性能
与 Pandas 类似,为 Dask DataFrame 设置索引可以加速基于该列的查询、合并和分组操作。更重要的是,如果数据已经按索引列自然分区(例如,我们的航班数据按年份分区存储),设置索引能让 Dask 知道数据分布,从而避免全表扫描。
# 设置索引。这是一个需要洗牌(shuffle)的昂贵操作,通常只在工作流开始时执行一次。
df = df.set_index('year')
# 设置后,Dask 知道了数据的分区信息(divisions)
print(df.divisions)
# 基于索引的查询会更快,因为它知道只需要读取特定分区
df_1990 = df.loc[1990].compute()
3. 使用 Dask Array 处理数值数据 🔢
上一节我们处理了表格数据,本节中我们来看看用于处理大型多维数值数组的 Dask Array。它提供了与 NumPy 类似的接口,但将数据分块存储,支持大于内存的数据集并行计算。
创建 Dask Array
可以从现有的类数组对象(如 HDF5 数据集)创建 Dask Array,并指定分块大小。
import h5py
import dask.array as da
# 假设有一个 HDF5 文件
with h5py.File('random.hdf5', 'r') as f:
dataset = f['/x']
# 创建 Dask Array,指定分块大小为 (1000, 1000)
x = da.from_array(dataset, chunks=(1000, 1000))
也可以直接生成随机数据等。
# 生成一个 20000x20000 的随机数组,分块大小为 (1000, 1000)
big_array = da.random.normal(10, 1, size=(20000, 20000), chunks=(1000, 1000))
执行计算
Dask Array 支持大多数 NumPy 操作,并且是惰性的。
# 计算全局均值(惰性)
mean_lazy = big_array.mean()
# 触发实际计算
mean_computed = mean_computed.compute()
# 沿特定轴聚合
mean_along_axis0 = big_array.mean(axis=0).compute()
分块大小的选择
分块大小对性能至关重要:
- 分块过大(如
chunks=(20000, 20000)):相当于只有一个块,无法并行,且可能超出单个工作线程的内存。 - 分块过小(如
chunks=(25, 25)):会产生海量任务,任务调度开销将远大于计算本身,导致性能急剧下降。 - 理想分块:通常使每个块的大小在 MB 到 GB 量级,并且块的数量与 CPU 核心数处于同一数量级。例如,对于 20000x20000 的浮点数组(约 3.2 GB),
chunks=(1000, 1000)会产生 400 个约 8 MB 的块,这是一个合理的起点。





4. 使用 Dask Bag 处理非结构化数据 🎒
Dask Bag 适用于处理半结构化或非结构化的 Python 对象序列,例如 JSON 文本行。它提供了函数式编程接口(如 map, filter, foldby)。
创建与处理
import dask.bag as db
import json
# 从文本文件创建 Bag(每行一个 JSON 字符串)
lines = db.read_text('accounts.*.json.gz')
# 将每行解析为 Python 字典
js = lines.map(json.loads)
# 使用函数式操作
alice_transactions = js.filter(lambda record: record['name'] == 'Alice')
counts = alice_transactions.map(lambda record: (record['name'], len(record['transactions'])))
result = counts.take(5) # 取前5个结果查看
Bag 的适用场景
Dask Bag 非常适合数据清洗和预处理阶段,尤其是处理嵌套、结构不一致的数据。一旦数据被清理并扁平化,通常可以转换为 Dask DataFrame 以进行更高效的分析。对于分组聚合,foldby 操作通常比 groupby 在 Bag 上性能更好。
5. 调度器与分布式计算 🌐


到目前为止,我们的计算默认在单机的线程池或进程池中运行。Dask 的真正威力在于其分布式调度器,它可以将任务分配到多台机器组成的集群上执行。
本地调度器
Dask 提供了几种本地调度器,可通过 dask.set_options 或 compute(scheduler=...) 指定:
threads(默认):线程池。适用于大部分 NumPy/Pandas 操作(它们会释放 GIL)。processes:进程池。适用于纯 Python 代码或受 GIL 限制的操作。synchronous:单线程。用于调试和分析。
分布式调度器
分布式调度器(dask.distributed)功能最强大,即使只在单机上使用,也能提供比 processes 调度器更好的性能和诊断功能。
from dask.distributed import Client
# 在本地启动一个分布式集群(默认使用所有CPU核心)
client = Client()
# 或者连接到已有的集群
# client = Client('tcp://scheduler-address:8786')
创建 Client 后,它会自动成为全局默认调度器。之后所有的 compute() 调用都会将任务提交到这个集群。
持久化数据
在分布式环境中,一个关键优势是可以将中间数据集持久化到集群各节点的内存中。这样,后续的多次查询可以避免重复的磁盘 I/O,实现交互式分析速度。
# 从远程存储(如S3)读取数据
df = dd.read_csv('s3://bucket/nyc-flights/*.csv')
# 将数据载入集群内存
df = df.persist()
# 后续操作会非常快
result = df.groupby('origin')['dep_delay'].mean().compute()
诊断仪表板
分布式调度器提供了一个强大的 Web 诊断仪表板,可以实时可视化任务执行、 worker 状态、内存使用等情况,是性能调优和问题排查的利器。启动客户端后,其地址通常会打印出来(例如 http://localhost:8787/status)。
6. 高级分布式 API:Futures
除了高级集合(DataFrame, Array, Bag),分布式客户端还提供了底层的 Futures API,它类似于 Python 标准库的 concurrent.futures,但运行在集群上。这允许动态地、异步地构建和提交任务。
from dask.distributed import Client
client = Client()

def inc(x):
time.sleep(1)
return x + 1


# 提交单个任务,立即返回一个 Future 对象(不阻塞)
future_x = client.submit(inc, 1)
# 可以继续做其他事情...
# 需要结果时,调用 result() (会阻塞直到完成)
print(future_x.result())
# 构建任务依赖
future_a = client.submit(inc, 10)
future_b = client.submit(inc, 20)
# future_c 依赖 future_a 和 future_b 的结果
future_c = client.submit(add, future_a, future_b)
print(future_c.result())
Futures API 非常适合实现自定义的迭代算法、异步工作流或需要细粒度任务控制的场景。


总结


在本课程中,我们一起学习了 Dask 并行计算框架的核心内容:
- Dask Delayed:用于并行化任意 Python 代码的基础工具,通过构建任务图实现惰性求值和并行调度。
- Dask DataFrame:为处理大型表格数据提供了类 Pandas 的 API,支持分块、并行操作。
- Dask Array:为处理大型多维数值数组提供了类 NumPy 的 API。
- Dask Bag:用于处理半结构化数据序列的函数式工具。
- 调度系统:了解了线程、进程、同步以及功能强大的分布式调度器。
- 分布式计算:学习了如何设置分布式集群、持久化数据以及利用诊断工具。
- Futures API
课程 P24:HDF5 进阶 - h5py 与 PyTables 🗂️
概述
在本课程中,我们将学习如何使用 Python 中的两个主要库——h5py 和 PyTables 来处理 HDF5 文件。HDF5 是一种高性能的数据存储格式,特别适合存储和管理大规模科学数据。我们将从基础概念开始,逐步深入到数据组织、类型、分块、压缩、查询、索引以及与 Pandas 的集成等高级主题。
1. HDF5 简介与 Python 生态
HDF5 是一个 I/O 库,针对规模和速度进行了优化。更重要的是,它是一个自文档化的容器,可以包含数据集、属性和元数据,确保研究数据能够自我描述。
在 Python 中,有两个主要的包支持 HDF5:
- h5py: 提供 Pythonic 的、类似 NumPy 的接口,暴露了 HDF5 库的底层 API。
- PyTables: 提供更高级的 API,主要基于表格数据集,并增加了快速索引、核外计算和高级压缩等特性。
这两个包是互补的,但都依赖于底层的 HDF5 C 库。目前的发展趋势是让 h5py 作为底层绑定,PyTables 构建在其之上,而 Pandas 则可以进一步利用 PyTables 来访问 HDF5。

2. HDF5 文件结构:组、数据集与属性
上一节我们介绍了 HDF5 的基本概念,本节中我们来看看 HDF5 文件内部的具体结构。一个 HDF5 文件主要包含三种类型的对象:
- 数据集: 数值或其他数据的数组。
- 组: 类似于文件系统中的文件夹,用于组织数据集和其他组。
- 属性: 附加在数据集或组上的小型键值对,用于存储元数据。
每个 HDF5 文件都有一个根组 (/),组和数据集可以形成层次结构,类似于 Unix 文件路径。
2.1 使用 h5py 操作结构
以下是使用 h5py 创建组和数据集的示例:
import h5py
# 创建文件并写入数据
with h5py.File('struct_data.h5', 'w') as f:
# 创建一个组
grp = f.create_group('my_group')
# 在组中创建一个数据集
data = np.arange(10, dtype='i1')
grp['my_array'] = data
# 读取文件结构
with h5py.File('struct_data.h5', 'r') as f:
print(list(f.keys())) # 输出: ['my_group']
print(list(f['my_group'])) # 输出: ['my_array']
2.2 使用 PyTables 操作结构
以下是使用 PyTables 实现相同功能的示例:
import tables as tb
import numpy as np
# 创建文件
f = tb.open_file('struct_data_pytables.h5', 'w')
# 创建组
group = f.create_group(f.root, 'a_group')
# 在组中创建数组(数据集)
f.create_array(group, 'my_array', obj=np.arange(10))
# 查看文件结构
print(f)
f.close()
3. 属性:为数据添加元数据
属性是附加在组或数据集上的小型命名数据片段,用于存储描述性信息,如单位、作者、创建日期等。
3.1 h5py 中的属性操作
在 h5py 中,可以像访问字典一样访问对象的 attrs:
with h5py.File('attrs_demo.h5', 'w') as f:
grp = f.create_group('experiment')
grp.attrs['temperature_unit'] = 'Celsius'
grp.attrs['measurement_date'] = '2023-10-27'
# 甚至可以存储数组
grp.attrs['calibration_coefficients'] = np.array([1.02, 0.98, 1.05])
3.2 PyTables 中的属性操作
PyTables 使用 get_node_attr 和 set_node_attr 来操作属性。它的一个强大功能是能够透明地序列化并存储整个 Python 对象(如字典、类实例),只要其大小不超过 64KB。
f = tb.open_file('attrs_pytables.h5', 'w')
node = f.create_group(f.root, 'my_group')
# 定义一个简单的类
class MyClass:
def __init__(self, value):
self.value = value
def describe(self):
return f"Value is {self.value}"
# 存储类实例
my_obj = MyClass(42)
f.set_node_attr(node, 'my_object', my_obj)
# 读取类实例
retrieved_obj = f.get_node_attr(node, 'my_object')
print(retrieved_obj.describe()) # 输出: Value is 42
f.close()
注意: 这种序列化依赖于 Python 的 pickle 模块,在 Python 2 和 Python 3 之间可能存在兼容性问题。
4. 数据集与数据类型
上一节我们学习了如何组织数据和添加元数据,本节中我们来看看如何存储实际的数据集,并理解不同的数据类型。
4.1 使用 h5py 存储数据
h5py 的 API 非常 NumPy 化,存储和读取数据非常直观。
import h5py
import numpy as np
with h5py.File('data_types.h5', 'w') as f:
# 方法1:直接赋值(推荐)
data = np.arange(10, dtype='i1')
f['my_data'] = data
# 方法2:使用 create_dataset(更详细的控制)
dtype = np.dtype([('x', 'i4'), ('y', 'f8'), ('label', 'S5')])
structured_data = np.array([(1, 2.5, b'hello'), (3, 4.2, b'world')], dtype=dtype)
dset = f.create_dataset('my_structured_data', data=structured_data, dtype=dtype)
# 读取数据
with h5py.File('data_types.h5', 'r') as f:
print(f['my_data'][:]) # 读取整个数据集
print(f['my_structured_data']['x'][:]) # 读取结构化数组的特定列
4.2 使用 PyTables 存储数据
PyTables 将数据集分为几种高级对象:Array(同质数组)、CArray(分块数组)、EArray(可扩展数组)和 Table(表格,异质数据)。
import tables as tb
import numpy as np
f = tb.open_file('data_types_pytables.h5', 'w')
# 创建同质数组
f.create_array(f.root, 'my_array', obj=np.arange(10, dtype='i1'))
# 创建表格(异质数据)
# 方法1:使用 NumPy dtype
dtype = np.dtype([('momentum', 'f4'), ('charge', 'i1')])
table = f.create_table(f.root, 'my_table', description=dtype)
particle = table.row
for i in range(100):
particle['momentum'] = np.random.rand()
particle['charge'] = np.random.choice([-1, 0, 1])
particle.append()
table.flush()
# 方法2:使用 PyTables 的 IsDescription 类(更清晰)
class Particle(tb.IsDescription):
momentum = tb.Float32Col()
charge = tb.Int8Col()
table2 = f.create_table(f.root, 'my_table2', Particle)
f.close()
列访问器: PyTables 的 Table.cols 访问器允许你高效地访问特定列,而无需将整个列读入内存,这对于大型数据集至关重要。
f = tb.open_file('data_types_pytables.h5', 'r')
table = f.root.my_table
# 低效:先读取整个列到内存再切片
# column_data = table.col('momentum')[20:30]
# 高效:直接读取所需切片
column_slice = table.cols.momentum[20:30]
print(column_slice)
f.close()
5. 分块与性能优化
当数据集非常大时,如何高效地读写数据就变得非常重要。HDF5 的分块技术是优化性能的关键。
5.1 什么是分块?
一个连续存储的数据集在文件中是线性排列的。而一个分块数据集则被分割成固定大小的、多维的“块”。这些块在文件中独立存储,并通过一个 B-树索引来定位。
- 为什么需要分块? 为了实现数据集的可扩展性(调整大小)和压缩。
- 原子性操作:读写数据时,总是以整个块为单位。即使你只修改块中的一个值,也需要将整个块读入内存,修改后再写回磁盘。
5.2 分块对访问模式的影响
访问模式决定了分块策略的性能:
- 连续访问(如按行顺序读写):适合连续存储或行方向较长的块。
- 随机访问(如按列访问或随机点访问):如果块设计不当(例如列方向太宽),可能导致需要读取大量不相关的块,性能急剧下降。
5.3 在 h5py 中设置分块
import h5py
import numpy as np
shape = (10000, 10000) # 大型二维数组
chunk_shape = (1000, 100) # 分块形状
with h5py.File('chunked_data.h5', 'w') as f:
# 创建可分块、可扩展的数据集
dset = f.create_dataset('big_data',
shape=shape,
maxshape=(None, 10000), # 第一个维度可扩展
chunks=chunk_shape,
dtype='f4')
# 写入数据(会按块写入)
dset[:, :] = np.random.randn(*shape)
5.4 块缓存的重要性
HDF5 库有一个块缓存,用于在内存中保留最近访问的块。如果您的访问模式需要频繁用到多个块,但缓存大小不足以容纳它们,就会导致缓存颠簸,性能下降。
- 默认缓存大小通常为 1MB。
- 在 PyTables 中,可以在打开文件时调整
chunkshape和缓存参数(尽管 API 有待改进)。 - 在 h5py 中,调整缓存需要使用底层 API,较为复杂。
核心建议:对于大多数应用,使用库自动选择的块大小即可。只有当遇到特定性能瓶颈,并且分析确认与分块策略有关时,才需要手动调整。
6. 压缩:节省空间与提升速度
压缩可以显著减少存储空间,在某些情况下,由于减少了 I/O 数据量,甚至能提升读写速度。
6.1 HDF5 的过滤管道
HDF5 支持在数据写入磁盘前通过一个“过滤管道”进行处理,压缩就是最常用的过滤器。压缩是在每个块上独立进行的,因此分块是使用压缩的前提。
6.2 在 PyTables 中使用压缩
PyTables 支持多种压缩算法:
zlib:标准算法,兼容性好。blosc:元压缩器,速度极快,支持多线程。它包含多种子算法:blosc:blosclz(默认)blosc:lz4(速度最快)blosc:lz4hc(高压缩比)blosc:zstd(Facebook 算法,速度与压缩比平衡性好)blosc:snappy(Google 算法)
import tables as tb
import numpy as np
# 创建过滤器对象
filters = tb.Filters(complevel=5, # 压缩级别 0-9
complib='blosc:zstd', # 使用 zstd 算法
shuffle=True) # 启用字节洗牌,通常能提高压缩率
f = tb.open_file('compressed_data.h5', 'w', filters=filters)
# 创建表格时会自动应用压缩
table = f.create_table(f.root, 'my_table', description=MyDescription)
# ... 填充数据
f.close()
6.3 性能权衡
选择压缩算法时需要在压缩速度、解压速度、压缩比和CPU占用之间权衡:
lz4:压缩/解压速度极快,压缩比一般。zstd:压缩比和速度平衡得很好,是很好的通用选择。zlib:压缩比较高,但速度较慢。- 对于查询密集型操作,压缩数据有时更快,因为需要从磁盘读取的数据量更少。
7. 查询与索引
对于表格数据,我们经常需要根据条件查询特定的行。PyTables 为此提供了强大的支持。
7.1 查询方法比较
假设我们有一个存储电影评分的大表 ratings,我们想查找评分 >= 4 的记录。
import tables as tb
f = tb.open_file('movie_lens.h5', 'r')
table = f.root.ratings
# 方法1:读入内存后用 NumPy 查询(内存消耗大)
data = table[:]
result = data[data['rating'] >= 4]
print(len(result))
# 方法2:使用 PyTables 的 `Table.where()` 迭代器(核内查询,较慢)
count = 0
for row in table.where('rating >= 4'):
count += 1
print(count)
# 方法3:使用 PyTables 的 `Table.read_where()` (读取所有匹配行到内存)
matches = table.read_where('rating >= 4')
print(len(matches))
# 方法4:使用 `Table.itersequence()` 或条件表达式(更高效)
# 但对于复杂查询,索引才是关键
f.close()
7.2 创建索引以加速查询
对于经常查询的列,创建索引可以带来巨大的速度提升。
f = tb.open_file('movie_lens_indexed.h5', 'w')
# ... 创建表 my_table ...
table = f.root.my_table
# 在 ‘title’ 列上创建完全排序的索引
table.cols.title.create_index(optlevel=9, _filters=tb.Filters(complevel=5))
f.close()
# 使用索引进行查询
f = tb.open_file('movie_lens_indexed.h5', 'r')
table = f.root.my_table
# 此查询将利用索引,速度极快
result = table.read_where('(title == b"Inception") & (rating > 4)')
f.close()
注意:创建索引(尤其是完全排序索引)可能很耗时,并且会增加文件大小。它适用于读多写少的场景。
8. 与 Pandas 集成
Pandas 是数据分析的利器,它可以通过 HDFStore 类直接使用 PyTables 作为后端存储引擎。
8.1 使用 HDFStore 存储 DataFrame
import pandas as pd
import numpy as np
# 创建一个 DataFrame
df = pd.DataFrame({'A': np.random.rand(5),
'B': np.random.rand(5),
'C': ['foo', 'bar', 'baz', 'qux', 'quux']})
# 方法1:使用 to_hdf (Pandas 0.20.2+)
df.to_hdf('pandas_store.h5', key='my_df', mode='w', format='table')
# 方法2:使用 HDFStore 对象
store = pd.HDFStore('pandas_store2.h5')
store.put('my_df', df, format='table') # 格式设为 ‘table’ 以便查询
store.append('my_df', another_df) # 追加数据
store.close()
8.2 从 HDFStore 中查询
HDFStore 支持类似 SQL 的查询语法,并且是核外操作的,即使数据大于内存也能处理。
store = pd.HDFStore('large_dataset.h5')
# 执行查询,只将结果读入内存
result = store.select('df', where='A > 0.5 & B < 0.2')
print(result)
store.close()
重要提示:Pandas 为了存储 DataFrame 的索引和数据类型等元信息,会在 HDF5 文件中添加许多额外属性。如果你计划用纯 h5py 或 PyTables 读取这些文件,可能会感到混乱。如果互操作性很重要,建议直接用 PyTables 创建标准表格。
9. 并行 HDF5

对于超大规模数据或高性能计算需求,可能需要并行读写 HDF5 文件。
9.1 并行 HDF5 简介
并行 HDF5 依赖于 MPI 库和编译时启用了并行支持的 HDF5 版本。它允许多个进程同时读写同一个文件。
- 需求:需要 MPI 库、并行 HDF5 库、以及支持并行 I/O 的文件系统(如 Lustre, GPFS)。
- 模式:
- 独立 I/O:每个进程读写文件的不同部分。
- 集体 I/O:所有进程协同操作(如共同创建数据集),通常用于元数据操作。
9.2 使用 h5py 进行并行 I/O (MPI)
from mpi4py import MPI
import h5py
import numpy as np
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
# 必须所有进程一起以并行模式打开文件
with h5py.File('parallel_test.h5', 'w', driver='mpio', comm=comm) as f:
# 集体操作:所有进程共同创建数据集
dset = f.create_dataset('data', (size, 100), dtype='i4')
# 独立操作:每个进程写入自己那部分
dset[rank, :] = rank * np.ones(100)
# 注意:运行此脚本需要使用 mpirun 或 mpie
# 课程 P25:奇科苏卢布撞击坑钻探研究 🚀🌍
在本节课中,我们将学习关于奇科苏卢布撞击坑的钻探项目。这是一个研究大型撞击坑形成过程、其对地球生命灭绝的影响以及撞击坑内生命可能性的科学探索。我们将跟随科学家的视角,了解项目的目标、方法、关键发现以及这些发现如何帮助我们理解地球和其他行星的历史。
---
首先,我要感谢埃里克、恩索特以及SIPI的邀请,让我来做这次主题演讲。
在这种场合演讲总是很有趣,因为你们可能比我更了解其中的某些部分,反之亦然,我可能在其他方面了解更多。
但我非常乐意回答问题,特别是如果我使用了任何让人困惑的术语。
如果有人没听懂,请随时要求快速澄清。我们可以现场进行定义。
当我面对普通观众演讲时,我尽量去“去术语化”,但正如你们所知,完全“去术语化”是不可能的。
好的,这只是一张艺术家描绘的撞击效果图,它实际上是有比例的。
但展示这类图片总是很有趣。
我必须感谢很多人,我看到我的幻灯片顶部和底部被稍微裁剪了。
这个项目由国际大洋发现计划资助,这是一个由大约30个国家组成的联盟,负责在全球进行科学钻探。
它也得到了国际大陆钻探计划的共同资助,因为我们当时在非常浅的水域,几乎是在陆地上,所以他们从1000万美元中给了我们大约100万美元。
然后恩索特和威瑟福德实验室联合起来,看起来底部的标签消失了,但无论如何,恩索特和威瑟福德实验室联合起来,能够以非常低廉的价格帮助我们进行CT扫描,还捐赠了大量软件和开发人员时间等,使我们能够在计算机平台上直接分析这些岩芯,创建虚拟岩芯。
在这个空间里,我们可以与整个科学团队完全共享数据,让全球的研究人员都能访问这些岩芯,分辨率大约在0.3毫米级别,这确实令人兴奋。
我将向你们展示一些例子,说明这在科学上是如何结出果实的。
好的,闲话少说,这个项目到底是关于什么的?这个项目本质上是一种比较行星学。
我们所说的比较行星学,是指通过研究一颗行星来理解所有行星,或者在这种情况下,研究一颗岩石行星以及发生在岩石行星上的一个过程——即在整个太阳系历史中,撞击体与行星的碰撞——来从根本上理解撞击是如何工作的。
如果你要选择一个撞击坑来研究,选择一个不仅作为改变行星地壳的过程,而且对我们有直接影响的是很好的,而这个撞击坑,你或许可以论证,如果它没有发生,我们可能就不会在这里。
所以,这是一个值得研究的对象。
这就是我们的提案内容,令人惊讶的是,这个提案花了15年才获得资助,但最终我们成功了。
也许你们知道这种长远的愿景,但它确实奏效了。
那么,我到底在谈论什么?我基本上是在谈论这样一个事实:撞击坑形成是重塑除地球外所有其他行星表面的最基本、最主要的过程。
在地球上,我们有水的循环,有板块构造,我们以非常高的速度改变着地球的表面,比如每次飓风过境都会改变地表。


但在其他行星上,最主要的过程是撞击,并且自太阳系诞生以来一直如此。
那么,这些过程从根本上是如何运作的?谢谢。
刚才修复了问题的朋友,太棒了。通过研究地球上保存完好的撞击坑,我们能学到什么?
需要提醒的是,这不仅仅是过去的事情。左边展示的是我们太阳系早期历史的一个阶段,称为晚期重轰炸期,当时太空中有很多物体飞来飞去,所有天体都经常被撞击。
有趣的是,地球上最早的生命出现在37亿年前,正好在这个阶段结束时,所以也许存在某种联系,也许撞击太多,或者也许撞击帮助了生命的起源。
这些都是很好的问题,但撞击在太阳系早期非常重要,包括在地球上。
右边,每一个点都是一个近地天体,这是今天的太阳系。抱歉,我们地球的轨道是这个浅蓝色的圆圈。
所以,我们正穿行在一个充满物体的空间中,因此从灾害的角度来看,思考撞击可能对我们造成的影响也是值得的。
这张图可能已经过时了大约五年,我想我们现在又发现了大约一百万个天体。
那么,关于地球的科学故事是从哪里开始的呢?它真正始于这两个人:沃尔特和路易斯·阿尔瓦雷斯。
沃尔特是伯克利的地质学家,现在已经退休。路易斯·阿尔瓦雷斯是他的父亲,一位获得诺贝尔奖的物理学家。
他们有一个很酷的想法,去意大利古比奥观察一层粘土层,它位于两层石灰岩之间。
我的光标在哪里?在这里,就在这两层石灰岩之间。
这一层来自白垩纪,结束于6600万年前。这一层来自古近纪,开始于6600万年前。
中间有一层有趣的粘土层,他们想知道沉积这层只有几厘米厚的粘土需要多长时间。
为什么不研究一些恒定的东西呢?比如宇宙射线总是从外太空降落。
如果我们简单地测量这层粘土中有多少宇宙射线元素,做一个估算,我们或许就能说出它花了多长时间。
然而,他们恰好选错了层来提出这个问题,因为他们没有找到像十亿分之零点一那样的恒定背景值,而是发现了八十个十亿分比的异常值。
所以,从他们研究的宇宙成因元素铱的角度来看,这个数值高得离谱,这意味着这不可能是背景辐射,而必须是从外太空涌入的物质导致了它的存在。
因此,在1980年诞生了一篇科学论文,指出这一刻可能是一次撞击事件。
当时还没有发现撞击坑,只是在一层地层中发现了铱异常。
但这激发了人们的想象力,出现了科学书籍,比如关于恐龙灭绝的儿童读物《T-Rex and the Crater of Doom》等等。
然后,科学界发生了一个奇妙的巧合。在同一年,下面这位名叫莱昂·史密特的科学家也在研究突尼斯的一层地层,并得出了完全相同的发现。
只是在他的案例中,他把样本送到实验室,实验室回信说:“抱歉,你的样本有问题,被污染了。”
所以,他直到一年后才发表论文,当时他说:“那不是污染,那是信号。”然后他在那之后一年发表了论文。
不幸的是,他没有因此获得应有的荣誉,他本应与阿尔瓦雷斯父子共享荣誉。
快进30年,他去年实际上加入了我们的考察队,这真是太酷了,算是圆满的结局。
好的,我们谈论的是一个由撞击形成的全球边界层,而撞击坑是在那之后十年才被发现的。
我稍后会展示这一点,但重要的是,从这个角度思考这层地层的样子,因为我将在钻探岩芯中展示给你们看。
基本上,如果你在地球的另一侧,它是一层厘米厚的层,其中确实含有像铱这样的撞击证据。

当你靠近撞击点时,情况变得更复杂一些。
当你非常接近撞击点时,情况就非常混乱了,充满了海啸和地震等证据。
每个人都知道这一点,但确切理解形成这些地层的过程是什么,以及随着它向墨西哥湾方向增厚,人们知道撞击坑一定在墨西哥湾的某个地方。
这里有一个很好的例子,来自美国东海岸钻取的岩芯,由国际大洋发现计划的前身——大洋钻探计划完成。
你可以看到一个很好的例子,展示了其中的特征。在撞击之前,海洋表面的生物多样性较低,但存在非常大的生物。
这些基本上是当时生活在海洋中的浮游生物。然后你看到了这个疯狂的边界层,里面有像玻璃球(称为球粒)的东西,有经历过数百万psi压力的冲击矿物,顶部还有火球层,含有铱、尘埃、灰烬等来自撞击的各种令人兴奋的物质。
之后,生命发生了彻底改变,下面95%的生物灭绝了,只有四个物种幸存下来,其中只有两个物种后来繁衍开来,占据了全球海洋。
这是一个巨大的变化。
这是实际的撞击坑,最终通过重力模拟被发现。撞击引起的重力异常使圆形结构显现出来。
作为地球物理学家,我们已经进行了几次成像工作。其中一条测线用白色标出,这是一条地震测线,向下观察地表以下约10公里。
你看到的是所有这些大断层,使白垩纪地层发生位移,落入坑中。你可以在坑中看到这个大凸起。
这个凸起在这里显示为一个发亮的环,这实际上是埋在坑中心的一个山脉环。
这很令人兴奋,因为只有最大的撞击坑才有这些,它们被称为峰环,我们过去不知道它们是如何形成的,直到去年我们进行了钻探。
这是地球上保存最完好的大型撞击坑,另外两个同等大小的撞击坑已有20亿年历史。
我们认为在6600万年前,一个直径约14公里(或10英里宽)的撞击体击中了尤卡坦半岛,当时那里是一片相对较浅的海域,水深几百米。
全球75%的生命灭绝,海洋表面的灭绝比例更高,达到约90%,但河流中可能只有5%,所以变化很大。
地球上所有重量超过约60磅的生物都灭绝了,这很有趣。我们的祖先属于幸存者之列。
快进6600万年,我们有了一个科学团队,可以去研究它,这很好,对吧?
那么,我们的目标是什么?这是一张全波形地震成像图,属于地震成像的高科技领域。
基本上,当人们从空气枪等能量源发出能量时,我们收集所有返回的能量,通过拖在船后的灵敏接收设备接收,然后创建图像,告诉我们岩石的密度(实际上是声波在岩石中的速度,但两者是等效的),并将其叠加在一张制作精良的图像上。
你可以看到我们决定钻探地点的一些真实细节。这就是那个峰环,那个山脉环。
注意它有一个非常平坦光滑的顶部。注意这里有一个浅蓝色的层,它的速度和密度都非常低。那是什么?
然后你可以看到这上面的所有地层。我的光标又丢了,在这里。
你可以看到峰环本身几乎是均质的,你看不到任何密度对比。
但在它上面,你有所有这些地层,这些单独的地层是在随后的6600万年里掩埋撞击坑的石灰岩层。
我们的目标是从大约500米深处开始钻探,向下钻取并采集岩芯,收集覆盖在其上的石灰岩层,以了解生命是如何恢复的。
然后钻入峰环,穿过那个浅蓝色的物质,进入峰环本身,以提出三个非常基本的问题:峰环是如何形成的?撞击过程从根本上如何运作?是什么导致了75%地球生命灭绝的环境变化?撞击对地下(地壳内)的生态系统有什么影响?
我们知道地球表面之下存在着一个巨大的生态系统,据估计它比生活在地表的生态系统大得多,主要是细菌、古菌等有趣的生命形式。它们如何受到这次撞击的影响?
我们提出这个问题的原因与生命起源有关:生命是否可能因为撞击驱动的能量和化学交换而开始,而不是目前流行的理论——生命起源于大洋中脊?这是我们的第三个目标。
好的,我将快速浏览这些目标,然后向你们展示一些结果,以及我们如何实际使用CT成像并与这个社区合作。
首先要看的是月球上峰环型撞击坑的图片。这是月球上的薛定谔盆地。
在左边,你可以看到一个基本平坦的盆地,可能充满了撞击熔岩和碎屑等各种物质。
但在盆地中央,你可以看到这个山脉环。这个东西就是峰环。
关于如何形成这样一个看起来低而平的撞击坑,以及如何形成山脉环,曾经有两种相互竞争的理论。
为了让你了解规模,在地球上,这些山脉高1到2公里。撞击坑最初形成时,边缘可能有撒哈拉到毛里求斯那么高,然后才坍塌。
所以这些不是小山。两种理论是:一切都基本上由熔体驱动,这就是左边的理论,称为撞击熔体坑或嵌套熔体假说,即撞击使物质真正熔化(这里的熔化不一定指温度,也指压力,如果你撞击得足够猛烈,可以使物质熔化,需要60吉帕斯卡或960万psi的压力,但撞击确实有那么多能量)。
这个想法是,有如此多的熔体,以至于它实际上阻碍了任何反弹过程,形成撞击坑的方式基本上是物质从侧面坍塌,山脉环将由从侧面落入的浅层物质构成。
另一种理论是,撞击如此猛烈,以至于地壳中更大范围的物质暂时表现得像一种流体,一种缓慢移动的粘性流体。
在这个世界里,你可以把它想象成向池塘里扔一块石头,撞击时基本上会形成一个洞,然后立即开始向上反弹。

顶部有一些撞击熔体,但基本上不足以阻止这个反弹过程。它可能上升到地球表面以上10公里,然后向后坍塌,使得山脉环实际上由来自深处的物质构成,现在位于地表。
这很重要,因为如果这是它的工作方式,那么行星的地表会随着时间的推移被撞击重塑,它们就像一个巨大的园艺系统,行星表面不断被撞击更新,如果右边的理论是正确的。
那么,到底是哪一种呢?如何测试呢?如果你钻入峰环,在尤卡坦的情况下,如果它是浅层物质,那么它将由石灰岩构成;如果它是深层物质,那么它将由石灰岩以下的物质构成。
先不剧透,我们去看它是由什么构成的。
下一个问题:大灭绝。你知道,像那样的大撞击在当地是糟糕的一天,对整个墨西哥湾也是糟糕的一天。

这里用颜色绘制的是所有沉积物(所有岩石)的厚度图,这些岩石因为撞击而坍塌进入墨西哥湾,这是地球上最大的单一事件沉积层。
估计涉及的能量相当于10级地震,比构造地震产生的能量更大,海啸高达500米以上,飓风级风力,冲击波将1000公里内的一切烧成灰烬,这确实是糟糕的一天。

但它为什么杀死了全球各地的生物?这就是研究问题:为什么地球另一侧的生命会受到影响?
这是我们想要研究的事情之一。这里只是一个例子,底部是白垩纪,描绘了动植物群。
由于这个事件,生物灭绝,新的生物开始出现。如果你测量全球生命生产力,无论是通过方解石(即石灰石)的数量还是碳同位素,一切都在崩溃。
这确实是一件大事,你可以看到它正好与撞击时刻重合。
另外需要澄清的是,当时德干地盾有一次主要的火山喷发,但规模并不比其他火山喷发大,而且它们发生在撞击前后,所以与这次事件没有巧合,与撞击有巧合。
那么,潜在的致死机制是什么?也许是由于从坑中喷射出的快速物质加热了地球表面;也许是由于空气中的碎片导致黑暗;也许是由于释放了像硫这样的物质,导致某种核冬天情景,就像坦博拉火山喷发导致“无夏之年”一样,大型火山可以做到,那么撞击呢?它能持续多久?
也许由于所有碳进入大气导致海洋酸化,这听起来像今天的情况吗?也许只是说说而已,或者我们甚至向海洋中注入了大量金属中毒。
这些都是一些想法。这里只是一个有趣的模型,展示了欧洲被喷射物击中的情况。
压力波穿过大气层,喷射物从奇科苏卢布6000公里外飞来。红色是较小的颗粒,黄色是较大的颗粒。
你会看到小的黄色颗粒实际上会很快落下,而红色的颗粒会卡在大气层中,大约在平流层底部50公里高处。
这足以导致类似右下角艺术家描绘的情景吗?当然,另一个问题是,当这些东西开始落下时,会产生多少热效应?那实际上是在加热东西吗?
有很多证据表明与撞击相关的烟灰和分散的野火,估计从几个小时的烤面包机温度到几个小时的披萨烤箱温度,但足以烧毁大量可燃材料。
如果你体型大,你就有麻烦了,你无法逃脱;如果你体型小,也许你可以躲在海洋里;也许生活在溪流底部的生物会没事。
这些是我们想在微体化石世界中研究的一些想法,通过观察生活在海洋中的浮游生物,我们看到在全球范围内这些生物的巨大差异。
撞击后3万年是我们第一次看到浮游生物世界全新进化的时间。如果你看撞击后300万年,你可以看到完全是不同的生命进化。
所以我们可以从岩芯尺度上观察生命的进化,以浮游生物作为地球上生命的代表。
最后一个目标是研究生命是否喜欢撞击坑,这里指的是生活在地下的生命。
问题是:当你投入那么多能量,进行那么多化学交换,穿过可能因撞击、破裂和移动而增强了孔隙度的地壳时,你是否实际上为生命或特殊生命在地下占据一席之地创造了条件?
我们有证据表明过去发生过这种情况。这是切萨皮克湾撞击坑,我不知道有多少人知道切萨皮克湾下面有一个巨大的撞击坑,宽85公里,发生在3500万年前。
他们钻探了它,击中了一个掉入其中的巨大岩块,在那个岩块中,他们发现了一个自那时以来进化但为撞击坑所独有的生态系统。
所以它是由3500万年前的撞击能量启动的,现在仍然生活在那里,这使它成为一个非常有趣的问题:撞击能否启动地球上的生命?
另外,这里的图片只是为了说明峰环可能是钻探的好地方,因为你可以想象流体向上流入这样一个突出部位。
那么,我们做了什么?我们带着一艘升降船出去,在普罗格雷索(世界上最长的码头,长6公里,水深7米,以便停靠游轮)附近钻探。
我们大约在离岸25公里、水深约20米(60英尺)的地方,将船用支腿升起,钻探了两个月。
这是一张照片。令人惊讶的是,这是你上船的方式,在西班牙语中他们称之为“寡妇制造者”。
你实际上就是抓住它,他们把你吊起来,然后把你放到船上,这很有趣,我喜欢,但其他人并不兴奋。
我们在那里呆了两个月。这些是集装箱,兼作我们的实验室,我们六个人住一个房间,所以海上科学家的生活有时并不奢华。
每个集装箱都有桌子、实验室、显微镜以及测量岩芯物理性质的方法等等。
我们这样做了两个月。岩芯没有在海上劈开,因为没有空间这样做,它们保持完整。
我稍后会展示期间发生了什么,但一旦它们被送回陆地,最终送到了德国不来梅,那里建立了一套完整的实验室来研究岩芯。
整个32人的科学团队加上许多技术帮助人员聚集在一起,我们劈开岩芯,为研究取样,描述它们等等,对这些岩芯进行全套初步测量。
事实上,它们没有在海上劈开,需要大约三个月才能到达不来梅,这创造了一个机会窗口。
通过恩索特的合作,我们能够去用最先进的医用风格CT扫描仪(在这种情况下是双能量CT)扫描这些岩芯,以便也能对这些岩芯进行虚拟分析。
这是一个绝佳的机会,得益于这些物流安排,并且因为威瑟福德给了我们一个难以置信的价格,恩索特为项目捐赠了他们的时间,这真是太棒了。
我将在几分钟后展示一些结果。
当我们从大约500米深处开始钻探时,我们得到了什么?抱歉,开始取芯。我们先钻进了500米,然后开始从500米深处取芯,一直钻到大约1335米,接近一英里。
我们看到了大约115米掩埋撞击坑的石灰岩,然后看到了大约130米那个浅蓝色的物质(我稍后会展示图片,现在称之为撞击角砾岩),然后我们实际上获得了600米的峰环本身(实际上是580米),我将展示这些图片。
向下移动,这些只是石灰岩的图片。结果发现大部分属于始新世,这是过去6600万年中最热的时期,所以人们对气候研究感兴趣。
我将快速跳过这部分。实际上,我们在向下钻探时得到了所谓的黑色页岩。
研究黑色页岩非常重要,因为这是海洋环境恶化的时期:变得非常热,海洋酸化占据主导,海洋分层,许多生物死亡。
所以这是我们未来可能面临的噩梦情景,人们希望通过研究这个事件(称为古新世-始新世极热事件,发生在大约5500万年前)来了解它。
我们确实捕获了这一点,所以这对气候研究很有用。
在那之下,我们进入了石灰岩的末端,遇到了这个有趣的棕色层,我稍后会详细研究。
然后我们进入了那个浅蓝色的物质。
# 课程 P26:使用 pomegranate 进行快速灵活的 Python 概率建模 🍈


在本节课中,我们将学习一个名为 **pomegranate** 的 Python 库。该库旨在实现快速、灵活的概率建模。我们将了解它的核心设计理念、主要功能、性能优势以及一些实际应用案例。
## 概述与核心设计理念

pomegranate 是一个用于概率建模的 Python 库,其设计目标是比现有方案更灵活、更快速、更直观,并能并行处理所有任务。
该库目前涵盖六种主要模型和两种支持模型。主要模型包括:
* 概率分布
* 混合模型
* 马尔可夫链
* 隐马尔可夫模型
* 贝叶斯分类器
* 贝叶斯网络

pomegranate 的一个核心原则是,尽管这些模型在形式上不同,但它们本质上都表示**概率分布**。例如,混合模型是一个复杂的分布,隐马尔可夫模型是序列上的分布,而贝叶斯网络则是沿图结构分解的分布。这一核心见解贯穿了整个库的设计。
## 模型的灵活组合 🧩

基于“万物皆分布”的理念,pomegranate 允许以其他库不支持的方式灵活地堆叠模型。
以下是模型堆叠的示例矩阵,其中行代表可被嵌入的模型,列代表可容纳其他模型的容器模型:


* **深蓝色**:pomegranate 已支持的方式。
* **浅蓝色**:计划未来支持的方式。
* **深红色**:据我所知,其他 Python 包均不支持的方式。
例如,你可以将正态分布放入贝叶斯分类器中,得到高斯朴素贝叶斯。但 pomegranate 的灵活性远不止于此,你还可以:
* 将混合模型作为发射分布放入隐马尔可夫模型,得到 GMM-HMM。
* 将隐马尔可夫模型放入分类器中,创建隐马尔可夫模型贝叶斯分类器。
* 为不同特征使用不同类型的分布(如指数分布、伯努利分布),而不仅仅是高斯或多项式分布。

## 统一的 API 接口

由于所有模型本质上都是概率分布,pomegranate 为它们提供了统一的 API。

所有模型都支持以下核心方法:
* `.probability()` / `.log_probability()`:计算(对数)概率。
* `.sample()`:从生成模型中采样。
* `.fit()`:像 scikit-learn 一样拟合模型。
* `.summarize()` / `.from_summaries()`:支持核心外学习 API。
* `.predict()` / `.predict_proba()` / `.predict_log_proba()`:对于组合模型(除基本分布外的所有模型),提供与 scikit-learn 类似的预测方法。
* `.from_samples()`:直接从数据推断并创建模型,这是与 scikit-learn 的 `.fit()` 有所区别的初始化方式。
## 支持的概率分布 📊
pomegranate 采用模块化结构,支持多种概率分布,这使得创建复杂模型变得简单。

库中包含了广泛的分布类型,最常见的用红色标出,例如:
* 伯努利分布
* 正态分布
* 多元高斯分布

由于其模块化设计,你可以通过“拖放”这些简单模型来创建复杂模型。例如,只需实现一个对数正态分布,你就可以创建对数正态混合模型和对数正态隐马尔可夫模型,而无需更改底层代码。
## 模型初始化与性能优势 ⚡
pomegranate 提供了两种初始化模型的方式。


**方式一:已知参数**
如果你已知模型参数,可以直接传入。例如,创建正态分布只需传入 `mu` 和 `sigma`。创建高斯核密度估计(一种非参数分布)也只需传入数据点。
**方式二:从数据推断**
如果你知道模型类型并拥有数据,可以使用 `.from_samples()` 方法直接从数据推断模型,这类似于 scikit-learn 的 `.fit()`。
现在,让我们看看 pomegranate 如何实现其速度优势。
**比 NumPy 更快**
例如,用大约1000个样本拟合一个正态分布,pomegranate 的速度大约是 NumPy 的两倍。这是因为 NumPy 追求通用性,需要分别计算均值和标准差,而 pomegranate 针对特定分布进行了优化,能更高效地一次性计算所需数值。
对于更大的模型,如用1000万条10维数据拟合多元高斯分布,pomegranate 仍比 NumPy 快约20%。
**GPU 加速**
pomegranate 已合并 GPU 支持,为所有模型带来了约4倍的加速。例如,在拟合多元高斯分布和计算样本概率时,都能观察到显著的性能提升。这种加速也延伸到了混合模型。
这种加速得益于 pomegranate 的模块化设计:只需为多元高斯分布添加 GPU 支持,所有依赖它的模型便自动获得了 GPU 支持。该功能后端基于 `cupy` 库,可自动检测并使用 GPU。

**高效计算与核心外学习**
pomegranate 通过计算**充分统计量**来高效更新参数。例如,对于正态分布,只需计算权重的和、加权点的和以及加权点平方的和,就能精确得出均值和方差。这意味着只需遍历一次数据。
这种可加性更新自然支持**核心外学习**:无需将所有数据载入内存,可以分批加载数据、计算充分统计量并累加,最后更新模型参数。
**并行处理**
可加性更新也使并行化变得简单:将数据分块,分配给不同进程并行计算充分统计量,然后汇总并计算新参数。pomegranate 主要使用多线程(释放了 GIL)而非多进程,避免了内存复制,在数值计算上更高效。

## 半监督学习能力 🎓
pomegranate 天然支持半监督学习,即同时利用大量未标记数据和少量已标记数据。
其原理是:先用已标记数据初始化模型,然后使用期望最大化算法,同时结合已标记数据的监督信号和未标记数据的无监督统计量,共同更新模型。这种方法在理论上合理,并且比 scikit-learn 的标签传播算法快约10倍。

效果上,仅使用已标记数据得到的决策边界可能比较尖锐,而结合未标记数据后,边界会更平滑,类间过渡更自然,通常能减少约一半的错误。

## 比 SciPy 更快的概率计算
在计算大量高维样本(如2000维2000个样本)的多元高斯对数概率时,pomegranate 比原生方法快约2倍。如果已经创建了分布对象,由于**积极的缓存策略**,速度可达 SciPy 的8倍。
缓存原理:在正态分布的对数概率公式中,包含 `log(σ√(2π))` 的项与数据无关。在创建分布对象时,pomegranate 会预先计算并缓存这个值,从而在后续计算每个数据点的概率时,避免了耗时的对数或指数运算,只需进行少量的加法和乘法。


## 实际应用案例:揭秘“绯闻女孩” 🕵️
为了说明概率建模的实用性,我们来看一个有趣的例子:分析美剧《绯闻女孩》中神秘人物“Gossip Girl”的真实身份。
**问题建模**
我们将“Gossip Girl”发送的每条爆料短信编码为一个向量,其中每个角色对应一个分量。如果爆料在当下有利于该角色的议程,则记为+1;如果不利,则记为-1。假设“Gossip Girl”会始终推动自己的议程。

**简单求和的局限**
最初,我们尝试对每个角色在所有爆料中的得分求和,分数最高者嫌疑最大。但结果发现所有角色的总分都是负数,且多人并列,无法得出明确结论。这说明我们需要更精细的概率模型。

**使用贝塔分布建模**
我们转向**贝塔分布**,它常用于建模介于0和1之间的概率(如事件发生率)。在这个案例中,我们将+1视为“成功”(有利于该角色),-1视为“失败”。

随着剧情推进(数据增多),每个角色对应的贝塔分布会逐渐收敛(方差减小)。第一季结束时,丹(Dan)的分布最偏向正面(最可能是Gossip Girl),而珍妮(Jenny)和塞雷娜(Serena)最不可能。
四季之后,所有角色的分布更加集中。有趣的是,根据这个模型,丹仍然是可能性最高的人选,而这与剧集最终揭晓的真相一致。
这个案例展示了概率建模如何从带有噪声的数据中提取洞察。
## 超越朴素贝叶斯:灵活的分类器
现在,我们回到一个更常见的模型——朴素贝叶斯分类器,来展示 pomegranate 的灵活性。
**朴素贝叶斯的局限**
朴素贝叶斯使用贝叶斯定理计算后验概率,但它假设所有特征相互独立。这通常会导致决策边界呈椭球形。在 scikit-learn 中,你通常只能使用相同类型的分布(如所有特征都是高斯分布)。


**pomegranate 的灵活性**
假设我们有一段时序信号,想将其片段分类为两类。其特征(如均值、标准差、偏度)可能分别服从不同的分布(如正态分布、对数正态分布、指数分布)。用单一的高斯分布对所有特征建模效果可能不佳。

在 pomegranate 中,你可以轻松地为不同特征指定不同的分布。通过为均值、标准差、偏度分别选用正态、对数正态、指数分布,我们可以在不增加数据、不进行复杂清洗的情况下,获得约4-5%的准确率提升,且计算成本没有增加。
**更一般的贝叶斯分类器**
pomegranate 更进一步,实现了**通用贝叶斯分类器**。它不再要求特征独立,允许使用具有任意协方差矩阵的似然函数。例如,对于特征间存在相关性的数据,使用全协方差矩阵的多元高斯分布作为似然函数,比朴素贝叶斯能减少约一半的错误。
你甚至可以使用**混合模型**作为每个类别的似然函数。对于由多个子成分构成的复杂数据,使用混合模型(如每个类别用两个多元高斯混合)比使用单一多元高斯分布能获得更好的分类边界和性能。
## 总结与资源
回顾开篇,pomegranate 实现了其设计目标:
* **更灵活**:支持模型自由堆叠、特征独立分布、通用贝叶斯分类器。
* **更快速**:通过优化算法、GPU加速、缓存和并行处理实现。
* **直观易用**:统一的 API 和模块化设计。
* **支持并行与核心外学习**:基于可加的充分统计量。
**资源**
* **文档与API参考**:可在 Read the Docs 上找到。
* **教程**:GitHub 仓库的 `tutorials` 文件夹中提供了所有模型的简明教程。
感谢大家的聆听。现在进入问答环节。
# 课程 P27:为科学与创新而编程 🧪💻
在本节课中,我们将学习Gaël Varoquaux在SciPy 2017大会上分享的核心观点。他将探讨如何通过编程来推动科学发现与技术创新,并分享构建科学软件库的设计原则与最佳实践。
---
## 演讲开场与背景
SciPy社区对我而言如同一个大家庭,它在许多方面都对我产生了深远的影响,这种影响难以言表。Prebrii提到我曾从事量子物理研究,而正是SciPy促使我发生了巨大的转变。无论是在这里,还是通过互联网与世界各地的朋友远程协作,SciPy都为我带来了大量的朋友。来到这里,我见到了像Prebrii这样的朋友,我希望你们也有同样的感受,因为能与这些人相聚是莫大的快乐。这远不止是工作,意义重大。
非常感谢给我这个机会回到这里,与朋友们相见,这真的非常棒。
现在,进入我的演讲主题。我想分享一些关于我们如何编程,以及我如何尝试为科学和创新而编程的思考,其目标当然是改变世界。
---
## 定义科学与创新
当我说“科学”时,我指的是发现知识或机制的过程。这不仅限于学术界,也存在于工业界。如今,计算在这一过程中处于核心地位。
让我们做个简单的调查。在座有多少人来自学术界?有多少人来自工业界?看起来大约各占一半。我知道我应该问有多少人不喜欢举手,这个点子我是从别人那里“偷”来的。
将科学与计算机结合,这种情况非常普遍,通常被称为“计算科学”。例如,我们听说过它在核物理中的应用。现场有核物理学家吗?太好了。还有计算流体动力学。太棒了,有做计算流体动力学的吗?计算化学家呢?心理学领域的?别害羞。太棒了!你们可能会觉得在计算科学会议上问有没有心理学家有点奇怪,但我并不这么认为。想想市场营销,过去几年对市场营销影响最大的就是数据科学,即通过分析客户数据来更好地理解他们。
因此,计算科学的传统领域正在扩展。美国科学促进会首席执行官最近不得不提醒国会:“科学不是一种政治建构或信仰体系。科学进步依赖于开放性、透明度以及思想和人员的自由流动。”很明显,我们从事科学的方式正成为一个重要议题,因为它已在国会中被讨论。
问题是,这种进步并不总是如预期般顺利。

---
## 科学中的信任危机与可重复性

让我举个例子。几年前,《经济学》杂志发表了一篇题为《债务时期的增长》的论文,该论文认为高额债务会阻碍经济增长。这篇论文被用作公共政策的基础,例如将金融危机归咎于公共债务。但如今,这篇论文被认为是错误的。这个结论是通过分析支撑该论文结论的Excel电子表格发现的。

另一个例子是自闭症与疫苗。需要强调的是,**疫苗不会导致自闭症**。没有科学证据表明疫苗会导致自闭症。曾有一项发表在《柳叶刀》上的研究,事后被证明是伪造的。然而,其后果是,在一些教育水平很高的国家,疫苗接种率下降,导致本应在地球上被根除的疾病再次爆发。这可以直接追溯到那个被伪造的、声称疫苗导致自闭症的研究。
我想说明的是,**对科学失去信任的代价是极其高昂的**。我认为我们目前正经历着这种情况。
---
## 创新的多维性
谈到创新,我想说创新是关于将正确的技术用于正确的用途。以电灯泡为例,你知道它实际上是由林赛在1835年发明的吗?但要使其可行,还需要额外的技术进步,比如需要好的真空泵,这花了一些时间。真正的突破发生在它经济上可行时,这意味着电力输送的普及。那时,我们熟知并都归功于他的那家公司才真正让电灯泡普及开来。
另一个有趣的故事是关于Adbox公司。这家总部位于奥斯汀的公司致力于将实体邮件数字化,他们会扫描你收到的邮件。但问题是,美国邮政系统告诉Adbox,美国邮政服务的客户不是公民,而是寄信的人,包括垃圾邮件发送者。因此,Adbox干扰了美国邮政的业务,最终导致其倒闭。

我的观点是,创新和技术远不止技术本身,还涉及经济学和权力平衡。我们作为技术专家倾向于关注技术,但我们需要理解技术是嵌入在更广阔的世界中的。

---
## 计算:新时代的电力 ⚡
回到我演讲的标题“为科学与创新而编程”,我想断言:**计算是新时代的电力**。它是当前变革的强大驱动力。借助新的数据源,它能够触及物理学和工程学之外的广阔领域。这就是如今被称为“数据科学”的领域,我认为它正在对社会产生巨大影响。

因此,我想首先从科学家的角度看待我对编程的感受,然后从构建软件的人的角度,最后审视我们的生态系统。
---
## 作为科学家的编程实践
过去几年,我一直对数据对脑科学的影响感兴趣。脑科学关注我们的精神世界,例如认知、情感或相应的病理学,如自闭症、抑郁症。历史上,这主要通过言语互动来研究,这基本上是心理学。过去几十年发生的一个变化是,我们现在可以成像大脑功能,获得大脑活动的定量记录。这对心理学和认知科学产生了缓慢但深远的影响。因此,我认为这是一个非常有趣的领域。


我们一直在研究数据处理能对这些领域做什么,以及如何处理医学图像。

---
### 一个具体的研究案例:自闭症的生物标志物
举个例子,我们一直对自闭症的生物标志物感兴趣。问题是:你能通过观察一个人的大脑,推断出这个人是否可能患有自闭症谱系障碍吗?

我们的工作方式基本上是:比较许多不同受试者的大脑活动,然后使用**监督机器学习**,让计算机学习如何区分自闭症患者和对照组。
在实践中,为了让你了解这背后的工程工作,我们首先要做的是提取大脑网络。我们不知道这是什么,但对计算而言真正重要的是,我们正在进行**无监督特征学习**,并将一个相当复杂的模型(比逻辑回归复杂得多)拟合到1TB的数据上。因此,这里存在一些计算挑战。
完成这一步后,我们将研究如何参数化每个受试者的大脑连接性。这时我就可以假装自己是个数学家,谈论信息几何和李代数之类的东西。如果你感兴趣,我们可以线下讨论。然后,我们在此基础上进行监督学习。我们直接使用 **`scikit-learn`** 并应用它,因为这对我们来说就是监督学习。
---
### 科学实践中的挑战与局限
这种方法有效,但我们对心理学或精神病学的影响存在很大的局限性。首先,从设计上,我们无法超越定义自闭症或对照组的临床医生,因为是他们给了我们定义。所以,我们无法比他们做得更好。但问题是,精神病学家对他们目前的定义并不满意。
脑部疾病是由症状定义的,这是一个不令人满意的状况。你希望根据组织损伤或机制来定义病理学。然而,他们还不准备接受我们黑箱算法的定义。我们可以告诉他们:“我能预测某人是否是自闭症患者或对照组。”但他们不会接受。也许他们是对的,也许他们不应该信任黑箱算法,因为它有很多活动部件。
因此,我们在这个领域长期面临的挑战是:**我们能否让从业者将这些工具变成他们自己的?** 如果我们能做到这一点,我认为我们将对精神病学产生深远影响。如果我们做不到,这个问题就悬而未决。
正如我所说,科学面临着对信任的追求。这通常被称为“可重复科学”。Victoria Stodden说过:“如果它不开放且无法被他人验证,那就不是科学或工程。”因此,该领域和计算科学领域的人们一直在大量讨论计算可重复性。其理念是,如果你自动化一切并控制环境,事情应该是可重复的,对吧?

---

### 自动化与交互的张力

那么,让我们自动化一切。正如我的老板曾经说过的:“你知道,这只是编程的一个简单问题。”当然,这更具挑战性。原因在于,有些操作在有“人在回路”时效果更好,比如标记数据、获得对数据的洞察。科学本身就有人参与。另一个相关的挑战是,科学研究是一个迭代过程。
因此,我们面临着对快速、简单交互的渴望与对自动化和重放的需求之间的张力。我认为这个社区的每个人都感受到了这一点。

很久以前,当我和Prabhu在Mayavi上研究3D数据可视化时,我认为我们解决这个问题的方式是:Mayavi的代码库在其内部对象模型和它暴露的对话框之间建立了紧密的联系。所以,代码看起来像对话框,而不是可视化。我们认为这很好,但还不够。后来,Prabhu添加了一个很棒的功能:记录模式。你可以点击记录按钮,然后在可视化上更改东西,相应的代码行就会弹出。这是一种方法,试图记录人们正在做的事情。


另一个有趣的发展是围绕Jupyter笔记本发生的,在代码和交互之间的空间中,混合小部件和代码的想法。我认为这里将会有大量的实验,我不确定最终会产出什么,但也许这将有助于调和交互需求与自动化需求。
---
### 可重复性的局限与更广阔的目标

如果我们让每一个计算步骤都可重复,好的科学就会出现,对吗?最近有一项研究评估了心理科学的可重复性。这项研究的标题发表在《科学》杂志上,他们发现只有36%的已发表效应是可重复的。只有36%。如果你看看问题所在,首要问题是统计挑战。分析数据的方法有很多种。如果你用所有可能的方式分析数据,然后选择你最喜欢的结论,你将无法控制你的错误率。这是基本的统计学知识。当你认为自己是对的时候,很容易忘记这一点。我总是认为我是对的。所以我会分析数据,并在某个时刻确认我是对的。
再加上学术界存在相当薄弱的激励。你必须发表才能生存。即使是最诚实的人也会被此愚弄,我也曾被愚弄过。最终的问题是:在已发表的文献中,你拥有的错误阳性(错误发现)比你想象的要多,这只是因为存在选择偏差。这是“赢家诅咒”的一个实例。如果你看看这里的问题,很少是计算可重复性问题。我们喜欢挑出少数计算可重复性问题的案例,但科学的问题不仅仅是计算问题。我想这并不令你惊讶。

因此,我认为“可重复性”是一个用词不当,或者可能只是一个太狭隘的概念。对于计算科学而言,重要的是操作是**可验证的**,以建立信任,并且理想情况下是**可重用的**。
---

### 提升科学研究的核心:正确的工具

话虽如此,如果我们想改进研究,改进科学,最好的方法是使用正确的工具,无论是正确的软件工具还是正确的概念工具,并让人们使用正确的工具。顺便说一句,这是一个难题。

原因在于,研究环境从社会学和技术角度来看都非常复杂。人们面临的实际障碍是**认知负荷**,因为研究人员需要结合来自许多不同领域的概念。如果在此基础上,他们还需要结合所有不同的工具,比如shell脚本、MATLAB、Python、Stan、BUGS,你可能不了解所有这些工具,我也不了解。所以,如果你身处这个领域,试图结合所有这些复杂的工具和相应的复杂理论概念,那么你正确完成的机会很小,找到适合工作的正确工具的机会就更小了。
---

### 小结:科学家视角下的编程
总结一下,作为科学家进行编程,我认为最终的代码应该以某种方式**可审计**(这很难),并且理想情况下**可重用**(这也很难)。很明显,交互式计算与自动化之间存在张力,我认为主要的敌人是**认知超载**。

这与工业界有很大不同吗?我并不这么认为。硅谷有一种态度:“嘿,如果它有效,你并不真的需要知道它是如何工作的。重要的是它解决了问题。”这在一定程度上是对的,但并非适用于所有应用领域。而且这也正在损害硅谷,因为对硅谷的怨恨越来越多。我不想过多讨论这个,但也许选择定律与此有关。
工业界有而学术界没有的一点是,当遇到问题时,一些行业可以投入更多资金来解决。所以他们可以掩盖一些问题,但问题仍然存在。
---
## 科学家的迭代工作流程

为了让你了解我作为使用代码的科学家是如何思考工作的,我有一个非常迭代的工作方式。以下是我依次应用的步骤列表:
以下是科学家迭代工作流程的关键步骤:
1. **使用好的编辑器和代码检查工具**:为什么很少人这么做?如果你在一个要运行24小时的脚本中打错字,而错误发生在保存结果时,你就损失了24小时的计算时间。在编辑器中添加代码检查工具成本极低,所以我认为你应该这么做。
2. **遵循编码规范**:你需要养成习惯,但我尽量不再用单个字母(如x和y)命名变量。如果你用scikit-learn,你可能注意到我们用了很多x和y,这是历史原因。抱歉。
3. **使用版本控制**:成本相当低。你应该看看我科学代码仓库的提交信息,我想99%都叫“iter”。但这比没有版本控制好。
4. **进行代码审查**:如果你想扩大规模,在实验室层面构建好的工具或好的科学,那么阅读彼此的代码非常重要。代码审查,这是众所周知的。
5. **进行单元测试**:再进一步,单元测试。因为确实,正如Spring所说,未经测试的东西要么今天坏,要么明天坏。这是事实。
6. **打包分享**:如果你想更进一步分享你的结果,那么你可以制作一个包含依赖控制和编译等内容的包。
这里的成本是递增的。**避免过早的软件工程**非常重要。正如Katie在昨天的演讲中提到的,我不认为一切都应该被重复。我想衡量我的愚蠢想法与好想法的比例,但我不认为这是个好比例。你不想重复我的愚蠢想法。所以这是在过度工程和工程不足之间的权衡。

---
## 从探索到巩固:Python的优势

第四个目标是产生洞察,或者如果你致力于创新,则是进入新的领域。实验对于产生洞察和开发新想法的概念验证非常有用。随着这些想法变得清晰,你需要进行巩固。好消息是Python在这方面非常出色。Python允许你编写非常混乱的代码,也可以编写非常清晰和健壮的代码。重要的是你需要逐步推进。
我说“逐步”是因为我需要数年时间才能知道我的想法是好是坏。真的需要。所以我基本上做的是让事情越来越稳固。随着它们变得稳固,我更好地理解它们。随着我更好地理解,我知道我是想抛弃它们,还是把它们变成一个包。
当一切顺利,这些想法最终变得稳固时——这大约只占我们尝试事情的百分之一——那么我认为我们需要分享它们。我们需要让它们可重用,并把它们变成一个库。

---
## 构建科学软件库 🛠️
这时,我就戴上了我的另一顶帽子:软件开发者的帽子,我关心的是为科学构建软件。
库非常重要,因为它们使我们能够扩展。首先,**抽象降低了认知负荷**,这至关重要;其次,它们**实现了代码重用**。所以我可以使用你写的、而我不够聪明去理解的算法,这至关重要。
我参与过的库的例子包括 **`scikit-learn`**。我们的目标是让机器学习研究对不理解模型和算法的人可用。我认为我们部分成功了。另一个我参与过的、较小的库是 **`Nilearn`**,我们的目标是让人们易于理解如何用机器学习解决新的成像问题或脑成像问题。
`scikit-learn`面临的挑战是机器学习模型的空间巨大且极其多样化。另一个挑战是编码概念是简单的,而统计和机器学习概念是困难的。在`Nilearn`中,我们面临一个非常具体且有趣的挑战:我的目标是让技术接受度低的用户能够上手。这对我来说是一个强烈的目标,因为我认为如果我想对脑科学产生影响,我们需要实现这个目标。这非常重要。
---
## 软件库的设计原则


因此,我们需要设计能够**降低认知开销**的工具。这确实是一个设计问题。我所说的“设计”是指工业设计,即设计汽车、自行车、叉子。有一个叫做“工业设计”的专业领域,这些人基本上坐下来关心日常工具的外观。
你有没有意识到苹果公司的第四号人物乔纳森·艾夫就是一个工业设计师?他在去苹果之前设计微波炉或椅子。所以苹果非常清楚地理解了工业设计的重要性,你在使用产品时能感受到。我认为实际上SciPy社区也很好地理解了这一点。所以我想说,SciPy的代码与众不同。

设计是一个难题,一个非常难的难题。因此,我想为SciPy技术栈提出一些设计原则。这是我在尝试玩“Python之禅”的把戏,所以很可能会失败。但无论如何,我要试一试。
---
### 设计原则详解
以下是我总结的为科学软件库设计的一些关键原则:

1. **一致性**:我学到的第一件事,实际上是和Prabhu一起学到的,就是一致性、一致性、一致性。我记得有一个拉取请求,我写了一个两个单词的参数,中间没有下划线。Prabhu说:“这与我们的编码指南不一致。”我当时想,这家伙太注重细节了。但这真的很重要。我举例子不是为了抨击其他库。当我遇到问题时,不是为了抨击库。我在scikit-learn中提不出这些问题,因为我们已经解决了它们并全部移除了。但如果你看SciPy,例如,一些SciPy优化函数使用`maxiter`(没有下划线),而另一些使用`max_iter`(有一个下划线)。我基本上需要把这个记在脑子里,或者每次都要查。我的记忆力很差,所以这行不通。这就是认知超载的一个例子。

2. **函数优于类**:对我来说非常重要的一点是,函数比类更容易理解。对象有隐藏状态。在某些情况下这非常有用。如果你在做图形用户界面工具包,你从设计上就有隐藏状态。但这种隐藏状态是用户需要记在心里的。另一件事是对象没有通用接口。它们没有清晰的入口点和清晰的输出。所以函数实际上比对象可重用得多。如果我对一个软件工程人员说这个,那个人会立即告诉我,我不知道如何进行面向对象编程,因为任何知道如何正确进行面向对象编程的人都知道对象比函数更可重用。所以我声称我知道,因为我和Prabhura Mshandran一起工作过。所以我确实知道如何进行面向对象编程。问题是,如果你试图跨越库的边界,对象很难穿越,因为对象带有它们自己的一套约定和概念。

3. **围绕少量核心概念构建**:相应地,一个库应该只依赖于少数几个概念。我的意思是,如果你理解了如何使用库的一端,你能将这种理解转移到库的另一端吗?这是一个困难的设计问题。我不认为如果我开始一个新库,第一次就能做对。我需要迭代。问题是我需要找出哪些是领域概念,那些少数几个非常有用的、通用的领域概念,我需要依赖它们并围绕它们展开。
4. **使用通用的数据容器**:我想强调,**通用的数据容器**使生态系统更强大。所以,如果一个函数接受一个`Earth-factory`模型对象作为输入,而你试图使用这个东西,你根本不知道`Earth-factory`模型是什么。也许它不过是一个字典,映射着地球数据。如果它被写成“这是一个字典,输入是这个,输出是那个”,或者一个pandas DataFrame之类的,但如果一个库定义了自己的数据容器,切换到使用这个库的成本就很高。
5. **单一职责原则**:每个函数应该**只有一个且仅有一个目的**。最危险的事情是当行为根据输入类型而改变时。对我来说,最明显的例子是`git checkout`。根据传给`git checkout`的是分支名还是文件名,它的行为截然不同,这造成了非常令人惊讶的行为。
6. **为接口编程**:为接口编程非常重要。我的意思是,你编码时期望对象暴露一组属性和方法,这定义了它们的接口,这就是你在库内或其他库之间传递和使用对象的方式。话虽如此,**不要过度使用鸭子类型**也很重要。一个很好的例子是NumPy矩阵。如果你编写一个函数,期望得到数组,并且该函数对数组进行乘法运算,而有人向这个函数传入一个矩阵,它可能以某种定义“工作”。基本上你会得到错误的答案。所以我认为这是一个很好的例子,说明在拥有接口但背后行为不同这方面走得太远了。
7. **谨慎使用属性**:我认为
# 课程 P28:SciSheets - 结合编程能力与电子表格的简洁性 📊➡️🐍
在本节课中,我们将学习一个名为 SciSheets 的创新项目。该项目旨在为电子表格用户提供编程的强大功能,同时保持电子表格的直观和易用性。我们将探讨传统电子表格的局限性、SciSheets 的核心特性,以及它如何通过使用 Python 作为公式语言来解决表达性、复用性和性能等问题。

## 传统电子表格的现状与挑战
上一节我们介绍了课程的主题。本节中,我们来看看传统电子表格的运作方式及其面临的挑战。


电子表格非常普遍,通常包含带列标题的数据行。当需要输入公式时,用户会使用类似 Excel 的坐标系统,例如 `A2` 代表 A 列第 2 行。一个典型的公式可能是 `=B2/A2`,其优点是可以立即看到计算结果。
若要进行迭代计算,用户通常会使用复制和粘贴功能。然而,查看这些公式背后的实际代码时,会发现它类似于汇编语言,可读性较差。

以下是传统电子表格如此流行的几个原因:
* **专注于计算而非编程**:用户无需担心数据依赖关系,系统会自动处理。
* **无需流程控制**:迭代通过复制粘贴完成,无需编写 `for` 循环。
* **简化数据结构**:避免了编程中处理复杂数据结构的麻烦。
编程的历史悠久,而电子表格相对较新。据统计,全球约有 2000 万专业程序员,但使用公式的电子表格用户却高达 10 亿。这表明电子表格是全球更流行的计算环境。


## SciSheets 的目标用户与设计理念
上一节我们了解了电子表格的普及性。本节中,我们来分析 SciSheets 所针对的不同用户群体及其需求。
在规划功能时,我们将用户分为三类:
1. **计算新手**:他们希望评估公式,会使用计算器,但电子表格更好用。他们构建的是线性的计算“食谱”。
2. **脚本编写者**:他们可以在 MATLAB 或 R 控制台中交互式工作,了解 `for` 循环和 `if` 语句,但可能不保存文件,或保存为一个大文件。他们通常不习惯使用函数。
3. **程序员**:他们可以使用宏语言,如 Visual Basic 或 Google Sheets 中的 App Script。


SciSheets 主要关注前两类用户。其设计目标是:让使用 SciSheets 进行传统电子表格操作同样简单,同时提供更多功能以解决诸多问题;并且帮助用户从新手进阶为脚本编写者或程序员。


## 传统电子表格的核心问题
上一节我们明确了目标用户。本节中,我们来深入探讨 SciSheets 旨在解决的传统电子表格的几个核心问题。
第一个问题是**表达性**,即算法或计算的编写与阅读能力。例如,生物化学中常见的米氏方程参数计算,涉及一系列步骤。在 Excel 中实现时,虽然能看到 `1/S`、`1/V` 等列,但底层的公式网格非常难以解读。用户只能从列的位置推断计算步骤,这很危险,因为电子表格中的数据依赖关系并非由位置隐式决定。
第二个关键问题是**复用性**,这是软件的基础。用户投入时间创建的计算,希望能在其他场景中复用。例如,将单个数据集的计算扩展到多个数据集。传统做法是向列中添加值、插入或删除行,这非常笨拙。此外,很难将一个电子表格的计算嵌入到另一个计算中,或者让使用 Python 的软件工程师直接复用电子表格中的计算逻辑。
除了表达性和复用性,还存在性能和复杂数据处理等问题。但更重要的是,计算机科学领域长期以来忽视了为进行计算的广大用户群体提供创新工具。

## SciSheets 的解决方案与核心特性


上一节我们指出了传统电子表格的痛点。本节中,我们来看看 SciSheets 如何通过一系列特性来解决这些问题。
SciSheets 的标语是“**编程的能力,电子表格的简洁**”。它专注于以下几个核心特性来应对表达性、复用性、性能和复杂数据等挑战:
1. **使用 Python 作为公式语言**:取代晦涩的电子表格公式,允许使用任何 Python 表达式或语句,包括 `import` 和 `eval`。
2. **将电子表格导出为 Python 函数**:可以将整个电子表格计算逻辑导出为一个 Python 模块中的函数。这带来了双重好处:程序员可以直接使用(因为它是 Python),并且在其他计算中可以方便地复用。
3. **支持复杂数据结构**:通过允许**子表**(即列本身也可以是表)来提供更丰富的数据结构,例如用于表达多对多关系。

## 特性详解:Python 公式与代码生成

上一节我们概述了 SciSheets 的核心特性。本节中,我们将详细探讨前两个特性:Python 公式和导出功能。
SciSheets 的界面类似传统电子表格,标有星号的单元格表示背后有公式。公式仅允许在列中定义,这限制了结构的随意性,但带来了巨大优势:用户能清楚地知道代码在哪里。
当用户点击一个公式列时,会看到弹出的公式编辑器。其中的公式看起来就像 Python 表达式,列名在公式中作为 Python 变量使用。实际上,这些变量是 **NumPy 数组**。这使得用户能够以自然的方式进行向量化操作,并利用所有 NumPy 功能和各种 Python 包,极大地丰富了计算能力。
公式不仅可以是一个简单表达式,还可以是完整的脚本。例如,一个计算 `1/V` 的脚本,除了计算该列的值,还可以为其他列(如 `slope` 和 `intercept`)赋值,并导入 `SciPy` 包进行线性回归。这在一个地方集中体现了完整的计算“食谱”,大大提高了可读性和表达性。

接下来是导出功能。用户可以将一个电子表格(例如,输入为 `S` 和 `V`,输出为 `Vmax` 和 `Km`)导出为一个 Python 函数。通过点击表头并指定函数名、输入和输出,系统会在后台生成包含该函数的模块,甚至同时生成一个测试模块,因为系统已经拥有测试所需的输入输出用例。


代码生成的一个关键挑战是确定数据依赖关系的顺序。由于公式中可能使用 `eval`,无法通过解析树预先确定依赖关系。SciSheets 的解决方案是采用迭代求值:遍历所有包含公式的列,尝试对每列的代码进行求值。如果因依赖变量未赋值而失败,则记录异常并继续。在最坏情况下,每轮迭代只能成功求值一列,但经过最多 N 轮(N 为公式列数)后,所有列都能被成功求值。如果某轮迭代中所有列的值都不再变化,则表明计算已完成。

## 未来发展方向

上一节我们深入了解了 SciSheets 的现有核心功能。本节中,我们来看看项目正在规划或开发中的一些未来特性。

首先,是**子表命名作用域**。这将允许用户更新一个子表中的公式时,其他同名子表中的对应公式自动同步更新,从而消除大量的复制粘贴操作。

其次,是开发一种**直观的电子表格版本控制方法**。目标是让不熟悉编程的用户也能在不知不觉中使用版本控制功能。
第三,是为 SciSheets **添加可视化功能**。目前可以处理数值列,但无法直接可视化数据。计划整合 Python 中丰富的可视化工具箱。
关于版本控制,一个重要的方向是**与 GitHub 集成**,并使其对电子表格用户直观易懂。这主要涉及三个基本操作:分支、合并和差异比较。
* **分支**:允许多个用户基于初始数据并行工作或进行探索性修改,而不会破坏原始表格的列和公式。
* **合并**:将分支合并回主分支。当出现合并冲突(例如,不同用户对同一计算结果的列命名不同)时,系统可以让用户交互式地选择他们想要的列或名称。
* **历史与检出**:在电子表格界面中可视化 Git 提交历史。例如,新添加的列可以高亮为绿色(GitHub 用于表示新增的颜色),被删除的列可以显示为红色,使用户能直观地看到更改。
## 总结与项目现状
在本节课中,我们一起学习了 SciSheets 项目,它旨在弥合电子表格的易用性与编程的强大功能之间的鸿沟。
我们回顾了传统电子表格在表达性和复用性上的局限,探讨了 SciSheets 如何通过引入 **Python 作为公式语言**、支持将计算导出为**可复用的 Python 函数**、以及采用**基于子表的复杂数据结构**来解决这些问题。我们还展望了其在版本控制和可视化方面的未来发展方向。
目前,SciSheets 是一个基于 Web 的系统,使用 Django 和 Python 作为后端,浏览器作为前端。项目代码已在 GitHub 上开源,正处于积极开发阶段,正在评估部署选项(如集成到 Jupyter 环境中),并热忱欢迎更多的反馈和贡献者加入。
# 课程 P29:Spyder 插件生态系统 🚀
在本节课中,我们将学习 Spyder 集成开发环境的插件生态系统。我们将介绍四个由社区开发的第三方插件,它们为 Spyder 增添了新颖且强大的功能,包括集成 Jupyter Notebook、编写动态报告、运行系统终端以及执行单元测试。

## Spyder 简介


在深入了解插件之前,我们先简要回顾一下 Spyder 是什么。Spyder 是一个桌面应用程序,其界面设计类似于 MATLAB。它主要包含以下几个核心组件:
* **编辑器**:用于在左侧编写代码。
* **控制台**:用于在右侧运行代码。
* **变量资源管理器**:用于查看和管理工作空间中的变量。
* **帮助文档查看器**:用于查看代码对象的文档。
例如,在控制台中执行 `a = 10`,变量 `a` 就会出现在变量资源管理器中。变量资源管理器不仅支持数字,还支持列表、字典、元组、NumPy 数组和 Pandas DataFrame 等对象。要获取帮助,例如查看 `numpy.sin` 的文档,只需在编辑器或控制台中选中该对象并按 `Ctrl+I`,文档就会以网页形式呈现。

## 插件一:Spyder Notebook 📓
上一节我们介绍了 Spyder 的基础功能,本节中我们来看看第一个插件——Spyder Notebook。这个插件将 Jupyter Notebook 无缝集成到 Spyder 中,提供了类似桌面的体验,并在此基础上增加了一些新功能。
以下是使用 Spyder Notebook 的主要优势:

* **无需指定目录**:无需在特定目录下启动 Jupyter Notebook 服务器。
* **临时存储**:未命名的笔记本会保存在临时目录,避免污染文件系统。
* **灵活存取**:可以在文件系统的任何位置打开和保存笔记本。
* **变量检查**:笔记本中的变量会显示在 Spyder 的变量资源管理器中。
* **便捷切换**:可以使用 Spyder 的文件切换功能轻松在不同笔记本间跳转。
安装该插件后,Spyder 界面左侧会出现一个名为“Notebook”的标签页。打开后,会加载一个未命名的笔记本供你开始编写代码。例如,输入 `1 + 1` 并执行,结果会显示为 `2`。当你尝试关闭一个有内容的未命名笔记本时,Spyder 会提示你保存,你可以选择任意位置进行保存。
你可以同时打开多个笔记本,所有未命名的笔记本都保存在临时目录。如果这些笔记本没有内容,关闭时 Spyder 会自动关闭并终止其关联的内核。插件还提供了一个菜单,可以快速访问最近打开的笔记本。
最有趣的功能之一是,你可以打开一个连接到当前笔记本的控制台。这个控制台与笔记本共享同一个内核。例如,在笔记本中运行代码创建一个名为 `data` 的 DataFrame 后,你可以在变量资源管理器中查看和探索这个 DataFrame 的所有列和数据。这为不喜欢在网页浏览器中使用 Notebook 的用户提供了一个更丰富、更集成的交互界面。
## 插件二:Spyder Reports 📄
接下来,我们介绍第二个插件——Spyder Reports。这个插件允许你使用 Markdown 编写动态报告。
动态报告通常包含文本、代码、图形和数学公式。一些用户,特别是来自 R 社区的用户,习惯使用 R Markdown 等工具,当他们转向 Python 时,会怀念类似的功能。Spyder Reports 正是为了满足这一需求。

使用 Markdown 编写报告有以下优势:
* **易于版本控制**:Markdown 文件是纯文本文件,便于使用 Git 等工具进行版本管理。
* **线性执行**:报告在渲染时按线性顺序执行代码,避免了像在 Jupyter Notebook 中可能因单元格执行顺序混乱而导致的变量状态问题。
该插件基于 `pweave` 库,它能渲染 Markdown 文件,并将输出生成为 HTML 文件在 Spyder 中展示。需要注意的是,此插件仅支持 Python 3,因为 `pweave` 库仅兼容 Python 3。
使用方式很简单:在 Spyder 编辑器中编写一个包含特殊语法(如 `pweave` 项目定义的 Noweb 语法)的 Markdown 文件,然后通过“运行”菜单中的“渲染 HTML”选项,即可在 Spyder 中查看渲染后的报告。

## 插件三:Spyder Terminal 💻

现在,我们来看第三个插件——Spyder Terminal。这个插件允许你在 Spyder 内部运行命令行应用程序。

并非所有的命令行工具都适合在 Spyder 的 IPython 控制台中运行。例如,`git`、`htop`、`conda`、`pip` 等命令需要在系统终端中才能正常工作。为了避免用户为了执行这些命令而频繁切换出 Spyder,我们开发了这个插件。
该插件的工作原理是:使用 Tornado 启动一个服务,该服务运行 `xterm.js`(一个 JavaScript 终端模拟器库)。这与 Jupyter Notebook 嵌入系统终端的方式类似。但我们的插件有一个重要改进:它通过一个名为 `pywinpty` 的 Python 封装库,成功地在 Windows 系统上实现了这一功能,而 Jupyter 的终端仅支持 Unix 类操作系统。

在插件中,你可以像在普通终端里一样运行命令,例如 `htop` 或 `git status`。这为用户,尤其是 Windows 用户,提供了极大的便利,使他们无需离开 Spyder 就能完成各种命令行操作。
## 插件四:Spyder Unit Test ✅


最后,我们介绍第四个插件——Spyder Unit Test。顾名思义,这个插件允许你在 Spyder 内部运行和检查单元测试结果。


它支持两种流行的 Python 测试框架:`pytest` 和 `unittest`。要配置此插件,你需要点击“配置”按钮,选择你想要使用的测试框架以及测试文件所在的目录。

配置完成后,你可以直接运行测试,结果将在 Spyder 界面中显示。绿色行表示测试通过,并会显示测试名称和耗时。如果测试失败,则会显示失败的位置和具体的错误信息。这个插件旨在鼓励开发者在 IDE 内进行测试,减少切换到外部终端执行测试的需要。
## 总结与展望

本节课中我们一起学习了 Spyder 的四个核心插件:Spyder Notebook、Spyder Reports、Spyder Terminal 和 Spyder Unit Test。它们分别扩展了 Spyder 在交互式计算、动态报告生成、命令行操作和代码测试方面的能力。

这些插件目前仍处于早期开发阶段,计划在近期发布。它们的诞生离不开 Travis Oliphant 和 Continuum Analytics 对 Spyder 团队的支持。
最后,关于一个常见问题:目前 Spyder Notebook 主要使用为 Spyder 定制的特殊内核来实现与变量资源管理器等功能的数据交换。因此,虽然技术上可能支持连接外部服务器上的 Notebook,但将无法使用与 Spyder 深度集成的这些特有功能。

# 课程 P3:Dash - 构建技术计算用户界面的新框架 🚀

在本节课中,我们将学习一个名为 Dash 的框架。Dash 是一个用于在纯 Python 中构建 Web 应用程序的框架,无需编写任何 JavaScript、HTML 或 CSS。我们将了解 Dash 的核心概念、基本结构,并通过示例学习如何创建交互式数据可视化应用。
---
## Dash 应用概览 📊
Dash 应用是完全在 Web 浏览器中查看的 Web 应用程序。一个简单的 Dash 应用可能只需大约一百行代码。例如,一个应用可以包含一个下拉菜单,用于选择不同的股票代码。当用户选择一个股票代码时,应用程序会调用 Python 代码进行计算,并将生成的股票走势图发送回浏览器显示给用户。
以下是另一个 Dash 应用示例。当用户将鼠标悬停在散点图上的数据点时,左侧会更新显示该分子(数据点)的元信息、图像和相关链接。图表上方有一个下拉菜单,选择不同选项可以高亮显示散点图中的对应点,下方则会列出所有在下拉菜单中选中的分子。

整个应用仅用 Python 编写,无需任何 JavaScript、HTML 或 CSS。应用的所有交互性都是完全定制的。当用户在应用中悬停时,当前选中的数据点会被传递到 Dash 应用的后端,后端在 pandas DataFrame 中查找该点的信息,然后返回更新图表或左侧的元数据。
Dash 的强大之处在于,你可以用它构建非常自定义的用户界面。它不对数据格式或应用程序的外观做任何假设。整个应用大约只需 300 行 Python 代码,且都在一个文件中。

另一个 Dash 应用的例子被格式化为仪表板样式。当用户将鼠标悬停在左侧地图上的点时,应用代码会被调用,并传入当前悬停的值。代码在 pandas DataFrame 中查找数据并提取时间序列。随着悬停点的变化,右侧的时间序列图会相应更新。所有这些图表都由上方的一组输入控件控制,例如一个控制查看年份范围的滑块,以及用于筛选数据的不同下拉菜单。
所有筛选器和输入控件组合在一起,用于更新 Dash 在此示例中运行的基础数据分析代码。此示例大约有 400 行 Python 代码。
你可以用 Dash 构建各种类型的应用程序。它不仅仅用于构建我们刚才看到的传统仪表板。这是一个被格式化为报告样式的 Dash 应用示例。它具有横跨整个网页的漂亮全幅图表,并包含用 Markdown 编写的文本。这是在纯 Python 中重现《纽约时报》文章的一个尝试。
由于 Dash 应用在 Web 浏览器中查看,你可以利用 CSS 的全部功能来自定义应用程序的外观和感觉。Dash 应用的每个美学元素都是可定制的,包括颜色、元素位置、字体等。这是一个我们为客户制作的 Dash 应用示例,它被格式化为报告样式,看起来就像你可能收到的 PDF 报告,只不过是在浏览器中查看。这些图表现在是交互式的,你看到的表格是从数据生成的,它们来自 pandas DataFrame。因此,如果基础数据发生变化,这些图表和表格也会更新。此外,还有一个“打印 PDF”按钮,利用 Chrome 强大的 PDF 生成器来生成应用程序的高质量 PDF。
---
## 构建 Dash 应用教程 🛠️

接下来,我们将用大约 10 到 15 分钟创建一个 Dash 应用程序。这个应用大约有几百行代码,包含几个不同的元素。左侧有一个交互式图表,当鼠标悬停在左侧图表的数值上时,右侧的图表会更新显示该点的时间序列。还有一些下拉菜单用于更新 Y 轴绘制的数据,以及单选按钮用于切换以线性或对数格式查看数据。
### 应用布局:描述外观
Dash 应用的第一部分描述了应用程序的外观,称为应用的布局。
以下是一个非常简单的应用。我们有三个组件:一个标题元素 `h1`,一个包含 Markdown 的 `Markdown` 组件,以及一个硬编码了一些数据并绘制条形图的 `Graph` 组件。
Dash 附带两个组件库。第一个是 **dash-html-components**,它为所有可用的 HTML 元素及其属性提供了 Python 抽象。常见的 HTML 元素包括 `div`(通用容器)、标题 `h1`、`h2`,以及图像、段落、表格等。
第二个组件库是 **dash-core-components**,这是一组更高级的组件。它们在幕后结合了 JavaScript、CSS 和 HTML,用于创建更具交互性的控件,包括我们之前看到的下拉菜单,以及这里的 `dcc.Graph`(即 dash-core-components.graph)。
我们刚刚硬编码了一些数据,但你可以想象如何使应用程序更具数据驱动性。只需更改几行代码,不再硬编码数据,而是从 pandas DataFrame 导入数据。然后,不再将 `x` 和 `y` 设置为列表,而是从该 DataFrame 中提取列。例如,`x = df[‘life_expectancy’]`,`y = df[‘gdp_per_capita’]`。每个数据点上还有文本元素,悬停时会显示对应的国家名称。此外,还在下方添加了一个表格元素来显示该 pandas DataFrame。
在 Dash 中,如前所述,应用程序的每个元素都是可定制的,包括颜色、字体。你可以使用 CSS 的全部功能。在这个例子中,我更新了背景样式为深灰色,将文本颜色改为白色。最终,Dash 将这个 HTML `div` 渲染为浏览器中的一个 HTML 元素。因此,所有可用的 HTML 属性,如 `style`、`class`、`id`,都可以作为 Python 属性使用。在这种情况下,`style` 不再是你编写原始 HTML 时看到的字符串,而是一个包含键值对的字典。例如,我可以设置 `color: white`、`background-color: #222`,并在此处将文本居中。

### 应用交互性:描述行为
Dash 应用的第二部分描述了应用程序的交互性,即输入组件如何更新输出组件。
在这个例子中,输入组件是一个下拉菜单,我将根据这个输入来更新文本。当从这个下拉菜单中选择不同的项目时,我会更新上方的文本元素。
在 Dash 中,你通过回调函数来描述交互性。这些回调装饰器以声明式的方式告诉 Dash 输入元素应如何更新输出元素。第一个参数是输出元素,这里有一个字符串 `‘output-text’`,它对应于布局中一个 ID 为 `‘output-text’` 的元素。
这个装饰器表示:每当 ID 为 `‘my-dropdown-widget’` 的输入组件(即上面描述的 `my_dropdown_widget`)的 `value` 属性发生变化时(`value` 是此下拉菜单当前选中的值),就调用被此装饰器包装的函数,并传入新的当前选中值。
因此,在这种情况下,如果我将其更改为 “Austin”,我在这里编写的函数将被 Dash 自动调用,并传入该新值。然后,Dash 期望你返回的值格式正确,并将使用该值来更新我的 Dash 应用程序的另一个属性。在这里,它将更新我的 `div` 的 `children` 属性。
所以,我在这里所做的就是获取下拉菜单中选中的新值,将其格式化为字符串,然后更新那个文本元素。
你可以想象如何使这些应用程序更具数据驱动性。在这个例子中,我将把之前看过的数据集导入 pandas,而不仅仅是显示当前选中值的简单文本。我将基于该值运行一些计算。
这个下拉菜单列出了我的 DataFrame 中所有可用的国家。我根据 pandas DataFrame 中的唯一值动态更新了下拉菜单中的选项。这就是用 Python 编写所有标记的一个非常酷的地方:你可以用 Python 上下文中的值动态更新你的标记。在这里,我有一个包含数百个国家/地区的下拉菜单,我们只是动态地更新它。如果基础数据发生变化,下拉菜单中的可用选项也会随之改变。
然后,我稍微修改了函数以运行计算。我获取当前选中的国家,在此函数内部,我过滤 DataFrame 以仅返回对应于该国家的行,然后基于该数据子集计算 GDP 人均值的平均值。因此,当我更改下拉菜单中的值时,我的函数会被 Dash 自动调用,它计算统计量并将该统计量返回给 Web 浏览器。

这是一个相当简单的例子,我们只是更新一个文本元素。但让我们将下拉菜单改为滑块,并将文本元素改为图表。
在这种情况下,我们有一个 `dcc.Slider`(即 dash-core-components 滑块组件)和上方的一个 `Graph` 组件。当我在滑块中选择不同的值时,我的函数被自动调用。我根据当前选中的年份过滤我的 DataFrame 数据,然后基于过滤后的数据绘制散点图,将 X 轴数据设置为 GDP 人均值,Y 轴数据设置为过滤后的 DataFrame 中的预期寿命。
这只是单个输入元素更新单个输出元素的情况。在许多情况下,我们的数据分析代码中有多个参数,我们可能希望设置多个输入来更新单个输出。
在这个例子中,我有两个下拉菜单、两个单选项目和一个滑块,总共五个输入。我只是扩展了我的回调装饰器,以包含页面上显示的所有这些输入。因此,每当我更改这些元素中的任何一个,或者在下拉菜单中选择一个新值时,Dash 将收集我选择的所有输入的当前选中值,并将它们作为位置参数传递给我装饰的函数。然后,我可以自由地用它做任何我想做的事情。在这里,我将根据这些值过滤我的 DataFrame,并基于过滤后的数据返回一个新的 `figure` 属性。如果我选择“线性”或“对数”,我将更改坐标轴类型。
这是 Dash 的一个很酷的地方:即使一次只有一个输入在变化,而不是所有输入同时变化,Dash 也会完成所有工作,收集所有输入的当前选中值,并使它们对你可用。因此,你可以编写这些函数,并且知道你在函数内部获得的变量代表了应用程序的当前状态。

这只是一个具有五个输入元素的输出元素。如果你想要有多个输出,你只需编写具有不同装饰器的多个函数。这个例子可以很容易地扩展到我们拥有相同数量的输入元素(两个下拉菜单、两个单选项目和一个滑块),但我们基于它更新三个图表的情况。因此,我们有三个不同的函数被装饰。

这里的图表本身也是一个输入元素。当我在这个图表中悬停数值时,它会调用其他装饰器来更新右侧的时间序列图,只需使用 pandas 对该数据进行一些过滤即可。

---
## Dash 的技术基础与生态系统 ⚙️
简而言之,这就是 Dash。你可以扩展这些示例来创建更复杂的应用程序类型,就像我们之前展示的那样,具有多个输入和多个输出,并且可以将它们设计得非常美观。

Dash 的实现依赖于几个核心关键技术。Dash 本身是一个 Web 应用程序框架,我们使用的底层服务器是 Flask,并且作为 Dash 开发者,你可以使用它。例如,如果你想向 Dash 应用程序添加额外的路由,你可以访问该底层服务器实例并添加额外的路由。
实际上,Dash 主要是一个前端项目,在幕后主要是 JavaScript 项目。我们渲染的所有组件都是通过这个名为 React 的前端框架实现的。React 是一个由 Facebook 构建和维护的前端用户界面框架。我们在 Plotly 内部大量使用它来构建 Web 应用程序,它非常出色。
React 的一个真正优点是,有一个庞大的开发者社区,他们用 JavaScript 制作这些模块化组件,并在非常宽松的许可证(如 MIT 许可证)下开源,供所有人使用。
对于 Dash,我们所做的是创建了一个工具链,使得将现有的 React.js 组件转换为与 Dash 生态系统兼容、可在 Dash 应用程序中使用的组件变得非常容易。
例如,我们之前看到的滑块组件,实际上并不是我编写的,而是开源社区中的其他人编写和维护的。我碰巧找到了它,它非常棒。通过 Dash 的 React-to-Python 工具链,我能够只用大约 10 到 15 行额外的 JavaScript 就将这个组件转换为 Dash 兼容的组件。将这个组件转换为 Dash 兼容组件真的很容易。
因此,考虑到 Dash 的未来,我们已经可以在 React 生态系统中获得数千个高质量的前端组件。通过这个将 React 组件转换为 Dash 兼容组件的工具链,我认为我们很快将拥有一个非常丰富的组件集,这些组件不仅由我们或单个人维护,而是由社区维护。
我们在内部用于将这些 React 组件转换为 Python Dash 组件的工具链也是开源的,任何人都可以使用。因此,如果你想编写一个组件,或将现有的 React 组件转换为 Dash 兼容组件,你也可以这样做,并且使用的是我们内部使用的相同工具集。
如果 React 和 JavaScript 对你来说是新的,我们还在 academy.plot.ly 上编写了一个非常棒的 React 入门教程。这是我们用来培训自己员工的 React 教程,它会引导你从头到尾在 JavaScript 中创建一个 React 组件和 React 应用程序。将组件从 React 转换为 Python 的指南是用户指南中的一章,它会逐步引导你完成。
Dash 本身是开源的,采用 MIT 许可证。因此,你可以在自己的笔记本电脑上自由使用它,也可以在自己的基础设施或他人的基础设施上自由部署 Dash 应用。你部署和管理 Dash 应用就像部署和管理 Flask 应用一样。

Plotly 本身是一家私营公司,我们获得了风险投资支持。我们能够通过许可企业附加组件和企业平台来资助我们所做的所有开源工作,这些附加组件和平台使公司更容易采用开源软件。
对于 Dash,我们开发了一个部署服务器,用户可以安装在自己的基础设施上。这使得上传 Dash 代码并自动为你启动服务器变得非常容易。它还添加了诸如 LDAP 和 Active Directory 身份验证等功能。当然,你也可以自己实现这些功能。我们完全没有将你锁定在其中。我们只是创建了一些企业平台和企业附加组件,使这变得更容易一些。
最终,通过这种制作这些企业附加组件和平台以使其更易于使用该软件的商业模式,我们能够资助一个团队,其中超过一半的工程师直接从事每个人都可以使用的开源软件工作。这包括 Dash 本身,包括我们用于交互式图形的 JavaScript 绘图库 Plotly.js,以及所有使用 Plotly.js 的库,比如我们的 R 库和 Python 库以及 Julia 库。
因此,我们认为这是一个非常酷的模式,也是一种构建软件的方式,这种软件将长期存在,并将长期得到公司的支持。
---
## 总结与资源 📚

本节课中,我们一起学习了 Dash 框架。Dash 是一个用于在纯 Python 中构建交互式 Web 应用程序的强大工具,特别适合数据分析和可视化。我们了解了:
* Dash 应用的基本结构:**布局**(描述外观)和**回调**(描述交互性)。
* 核心组件库:**dash-html-components** 和 **dash-core-components**。
* 如何通过 Python 回调函数连接输入和输出,实现动态数据更新。
* Dash 基于 Flask 和 React 的技术栈,以及其强大的社区插件生态。
Dash 是开源的,你可以在 GitHub 上找到它。通过用户指南,你可以在大约半小时内入门。我们鼓励你尝试它,并参与到社区中。

**核心概念公式/代码表示:**
* **布局结构**:`app.layout = html.Div([dcc.Graph(...), dcc.Dropdown(...)])`
* **回调装饰器**:`@app.callback(Output(‘output-id’, ‘property’), [Input(‘input-id’, ‘value’)])`
* **更新函数**:`def update_output(input_value): return processed_data`
希望本教程能帮助你开始使用 Dash 构建自己的数据应用!
# 课程 P30:大规模科学分析 - 五个系统的比较 🧪🔬
在本节课中,我们将学习一篇关于在科学计算背景下,比较五种不同分布式系统处理大规模图像分析的研究。我们将探讨每个系统的特点、优势与挑战,并总结出对科学实践者有用的关键经验。
## 概述
本次分享基于一项跨学科合作研究,旨在探索在科学计算场景下,各种分布式系统的实际应用能力。研究重点关注图像分析工作流的扩展性,并使用了神经影像学和天文学两个案例进行测试。

## 系统介绍与评估
上一节我们介绍了研究的背景和目标,本节中我们来看看具体评估了哪五个系统,以及它们各自的特点。
以下是所评估的五个系统及其选择原因:
1. **SciDB**:专为处理密集、基于维度的数组数据而设计的数据库架构。
2. **Spark**:一个非常流行且支持良好的系统,拥有优秀的Python接口。
3. **Myria**:由研究团队开发,用于领域科学计算的“无共享”数据库管理系统。
4. **Dask**:Python社区当前应对可扩展科学计算的方案。
5. **TensorFlow**:一个广泛讨论的系统,尽管并非为此类任务设计,但作为有趣的对比点被纳入研究。

研究团队获取了由领域专家提供的Python参考实现工作流,然后由计算机科学背景的成员尝试在以上五个系统中复现这两个案例研究。
## 各系统详细分析
在了解了评估的系统列表后,我们逐一深入分析每个系统的实现方式、优点和面临的挑战。
### SciDB
SciDB内置了对密集数组的理解,理论上能高效处理图像数据。用户可以通过Python包装器以类似NumPy的语法进行操作。
**核心操作示例(伪代码)**:
```python
# 连接数据库并创建分布式数组对象
array = scidb.connect().create_array(data)
# 执行类似NumPy的操作,如在第三维计算均值
result = array.mean(axis=2)
优势:
- 原生支持数组数据模型。
- 支持通过流接口使用用户定义函数。
挑战:
- 缺乏执行卷积等基本数组操作的原语,需通过UDF实现,但数据库无法优化其内部逻辑。
- 所有数据在节点间以TSV格式传递,序列化/反序列化开销巨大。
Spark
Spark提供了一个功能性的Python API,允许直接插入Python UDF,并在集群中分布式执行。
优势:
- 拥有庞大用户社区,问题容易找到解决方案。
- 弹性分布式数据集格式便于使用任意Python对象作为键。
- 函数式编程风格易于使用。
挑战:
- 数据缓存和中间结果的决策需要手动调整以实现高效。
- 对工作流进行超初始实现的调优较为困难,缺乏自动调优机制。
Myria
Myria基于SQL,提供了一种混合声明式-命令式的数据库语言,专为与科学家合作而设计。
优势:
- 数据通过灵活的Blob格式传递(可包含NumPy数组),避免了序列化开销。
- 可直接利用现有科学工作流的参考实现。
- 语言支持显式循环,比传统SQL更灵活。
挑战:
- 为获得高效率,通常需要用Myria语言重新实现整个工作流,翻译过程复杂。
- 系统调优需要深入了解其内部机制。
Dask
Dask是一个纯Python系统,其设计考虑了Python用户的使用习惯。
优势:
- 部署和安装非常简单。
- 可以处理Python能处理的任何数据格式。
- 通过
delayed语法轻松集成任意UDF。
挑战:
- 用户需要手动在计算图中插入评估屏障以获取中间结果。
- 需要手动进行数据分区。
- 在
futures和delayed等选择上存在决策困难。 - 任务失败时可能难以调试,有时需要重启整个作业。

TensorFlow
TensorFlow并非为此类通用科学计算流水线设计,本研究旨在探索其边界。
挑战:
- 不支持Python UDF,所有操作都需用TensorFlow语言重新实现,且其功能不完整。
- 所有数据需通过主节点加载,然后分发给工作节点,造成巨大网络开销。
- 计算图有2GB的大小限制。
- 内置操作集有限,例如无法对张量进行逐元素赋值,这对掩码和过滤等操作是必需的。
结论:TensorFlow不适合用于扩展任意的科学计算流水线。
性能基准与关键发现
在分析了每个系统的特点后,我们来看看它们的性能表现和从中得出的核心结论。
对于神经科学用例的端到端基准测试发现:
- Dask、Myria和Spark 性能相当。
- SciDB和TensorFlow 则慢得多。
性能相似的原因在于,Dask、Myria和Spark本质上都在做同样的事情:传递Python函数并将其应用于以各自方式存储的数据。而SciDB和TensorFlow速度慢的主要原因是节点间数据传递的效率低下:SciDB需要反复进行TSV序列化,TensorFlow则需要通过主节点广播所有数据。
关键启示:开发分布式系统时,应确保工作节点能自行摄取数据,而非通过网络接收数据。
对科学实践者的核心建议
基于以上评估,以下是针对希望在领域科学中进行大规模分析的研究者的关键建议。
- 用户定义函数支持至关重要:没有一个系统能提供足够全面的原语来完全用其原生语言实现复杂科学流水线。因此,支持Python UDF(TensorFlow除外)是必须的,这允许你分发成熟的、经过测试的库函数(如scikit-learn, scikit-image)。
- 灵活的数据格式是效率关键:SciDB和TensorFlow的数据处理瓶颈凸显了这一点。高效的系统应能无缝处理科学领域常用的数据格式。
- 自动调优是未来方向:理想的系统不应要求用户具备其内部决策的百科全书式知识。目前所有系统在这方面都有欠缺,需要大量手动调整。
- 简化的安装流程能降低使用门槛:复杂的安装过程会阻碍工具的应用。
- 庞大的用户社区提供有力支持:在遇到问题时,能够从Stack Overflow等社区找到答案非常重要。
总结与展望
本节课中,我们一起学习了五种分布式系统(SciDB, Spark, Myria, Dask, TensorFlow)在处理大规模科学图像分析工作流时的表现。研究发现,目前没有一种工具在所有方面表现突出,但Dask、Spark和Myria在支持Python UDF和灵活数据格式方面更具实用性。
这项研究的意义在于,它向数据库研究社区指明了真实用户(而不仅仅是运行基准测试的研究生)所面临的实际问题。通过揭示这些挑战,希望能推动未来开发出更贴合科学家数据类型和 workflow 的可扩展计算系统。与华盛顿大学的数据库团队合作,正是为了朝这个方向努力,期待未来能有关于可扩展科学计算的更好解决方案。
论文与资源:研究预印本已发布,相关代码也计划在夏季晚些时候公开。
课程 P31:深度学习与地理空间数据 🧊🛰️
概述
在本节课中,我们将学习如何利用深度学习技术处理和分析地理空间数据,特别是针对冰川学中的冰裂隙识别问题。我们将跟随 Shane Grigsby 的研究,了解从原始激光测高数据到最终地表类型分类的完整流程,并探讨其中的挑战与解决方案。
科学背景与问题定义
我的名字是 Shane Grigsby。从我的标题幻灯片可以看出,我是一名冰川学家。我的主要工作是通过遥感技术从太空观测冰川融化。
我获得了一项 NASA 的资助,项目标题是“利用历史 ICESat 和机载激光测高数据评估格陵兰冰裂隙的范围与特征,为 ICESat-2 评估变化建立基线”。简而言之,我的研究目标是确定冰裂隙的位置,并观察它们是否在变化或移动。
这是一张冰裂隙的图片,以及计划于 2018 年发射的 ICESat-2 卫星的艺术构想图。我们关注冰裂隙,是因为它们是融水从格陵兰冰盖流入海洋的主要通道。当冰面上出现裂缝,融水流入其中,它会向下渗透,直至冰盖底部。到达底部后,融水会像冰块下的水一样,将冰盖托起,加速冰体向海洋移动,并冲刷冰川底部。
此外,学界还在争论冰裂隙是否会随着径流缓慢向上延伸。如果冰盖前缘加速移动,应力缺失会向上游传播,导致冰架上出现新的冰裂隙,从而形成正反馈循环。这个问题之所以重要,是因为全球正在变暖。2012 年,格陵兰冰盖曾出现罕见的全面融化现象,这引起了科学界的广泛关注。
为了追踪冰裂隙是否向上延伸,我们需要一个历史基线来评估其变化。因此,我需要从 2004 年的 ICESat-1 任务数据开始,绘制冰裂隙分布图。
数据挑战:ICESat-1 激光测高数据

ICESat-1 卫星于 2003 年发射,2009年结束任务。它使用激光测高仪进行测量,激光光斑直径约为 70 米,并记录反射能量的时间谱。
以下是不同地表反射波形的示意图:
- 平坦表面:产生高斯分布波形。
- 倾斜表面:产生更宽的高斯分布波形。
- 不连续表面(如冰裂隙):产生双峰分布波形。
处理这些数据最大的挑战是缺乏验证数据。我们没有与这些激光光斑同时期的光学影像。这意味着,我们只能通过分析这些“锯齿状”的波形曲线,来推断地表的类型(是冰裂隙、山脊还是凹地)。
这听起来简单,实则不然。我最初花了几个月时间来判断这是否可行。核心难题在于,这是一个从二维地表到一维时间序列的“多对一”映射问题,从第一性原理出发进行反演在数学上是不可能的。例如,一个向左倾斜的坡面和一个向右倾斜的坡面,可能产生完全相同的波形。许多不同类型的地表都可能产生相同的信号。科学家们将这类问题称为“非平凡问题”。
解决方案思路:从模拟到匹配

既然无法直接反演,我们换个思路:如果我能模拟出已知地表会产生什么样的波形,那么我是否可以将实测波形与模拟波形进行匹配,从而推断出可能的地表类型呢?
我的处理流程计划如下:
- 模拟波形:从已知地表模拟生成波形。
- 匹配波形:将 ICESat 的实测波形与模拟波形进行匹配。
- 关联地表:通过数据库查找,将匹配的波形与其对应的地表关联起来。
- 标记分类:对地表进行标记和分类。
这本质上是通过模拟来“制造”我自己的验证数据。
数据准备:源数据与预处理
我的源数据来自 NASA 的“冰桥行动”飞行任务。这些任务产生了高质量的光学影像和激光雷达点云数据,经过处理后生成了高精度的数字高程模型。
以下是预处理步骤:
1. 数据分块与归一化
原始数据是重叠的、像素大小略有不同的图块。我使用 GDAL 库进行双线性插值,将所有图块重采样为统一的 0.5米 分辨率。然后,使用 gdal_translate 将它们切割成 100米 x 100米 的小图块(即 200x200 像素)。我选择这个尺寸是为了让图块略大于激光光斑的70米直径,以提供一些边界缓冲。
2. 处理重叠与去重
数据块之间的重叠被我视为一个“特性”而非“缺陷”,因为它能提供同一特征(如冰裂隙)在不同位置和视角下的视图。为了去除可能存在的重复图块,我对数据数组进行哈希计算,并将输出文件名设为哈希值,这样重复的文件会被自动覆盖。这样做的一个额外好处是实现了数据集的自动随机打乱。
3. 波形模拟
我从“冰桥行动”数据中获得了约25万个图块。对于每个图块,我将其高程值转换为光飞行时间,然后与一个高斯函数(模拟 ICESat 传感器的响应)进行卷积,最终生成模拟波形。
然而,这些模拟波形是“完美”的,没有考虑大气衰减、云层、光电噪声等因素。而真实的 ICESat 波形存在噪声、幅度变化和时间偏移。
可行性验证与噪声处理
为了验证方法的可行性,我进行了初步测试。我使用局部敏感哈希算法,将波形视为高维空间中的点,进行最近邻查找。测试表明,对于一个未知的输入波形,我们确实能从模拟波形库中找到与之相似、且地表类型也相似的匹配项。
为了让模拟波形更接近真实情况,我需要为其添加噪声。我从真实的 ICESat 波形中提取了噪声、幅度缩放和时间偏移的分布。然后,我构建了一个一维卷积神经网络(CNN)自编码器。在训练时,每一批数据我都会为模拟波形随机添加不同组合的噪声、缩放和偏移。自编码器的目标是学会去除这些干扰,恢复出“干净”的波形。
通过这种方法,我可以将25万个模拟波形扩展成数十亿个带有不同扰动的变体,极大地增加了数据的多样性和真实性。
从匹配到分类:无监督学习
上述方法能为我提供与每个实测波形最匹配的多个模拟地表图块。但我仍然需要一种自动化的方式,将这些图块分类(例如,分为“冰裂隙”和“非冰裂隙”)。
我不想依赖人工标记(太枯燥且主观),因此选择了无监督学习方法。我使用了自组织映射(一种竞争型单层神经网络),它能将相似的输入图块在二维映射图上聚集在一起。
然而,第一次尝试效果不佳。网络更多地匹配了图块的方向和整体坡度等特征,而不是我关心的地表形态(如是否有裂隙)。
关键改进:图块归一化与降维
为了解决这个问题,我回头对输入图块进行了严格的归一化处理:
- 圆形掩膜:应用圆形掩膜,聚焦于激光光斑中心区域。
- 去趋势化:使用主成分分析对图块进行旋转,消除主要的倾斜趋势(注意,这里不降维,只是旋转坐标轴)。
- 降维:使用离散余弦变换对图块进行压缩。我移除了75%的数据,因为我的传感器只能探测到大于15厘米的变化,毫米级的细节误差是可以接受的。
经过这些步骤,我将输入向量的长度从 40,000 大幅减少到约 5,000。这不仅使自组织映射的训练时间从 10天 缩短到 4小时,更重要的是,归一化后的图块更能反映地表形态的本质特征,使得匹配和聚类结果更加鲁棒和准确。
结果与下一步计划
经过改进,自组织映射能够很好地将冰裂隙图块聚类在一起。例如,一个冰裂隙图块的最接近匹配项中,大部分都是其他冰裂隙图块。
下一步计划包括:
- 扩大规模:将当前在小数据集(1万个图块)上验证的方法,应用到完整的25万个图块数据集上。
- 聚类输出:对自组织映射的输出节点进行聚类,我计划使用 OPTICS 算法。这样我只需要标记几百个聚类,而不是几十万个单独的图块。
- 概率输出:最终,对于每个 ICESat 激光点,我将得到一个属于“冰裂隙”类别的概率(例如,82%)。通过将多年数据栅格化,我可以生成冰裂隙概率分布图,并通过时间序列对比来追踪其变化。

经验总结与要点回顾
本节课中,我们一起学习了利用深度学习处理地理空间数据解决科学问题的完整流程。回顾整个过程,有几点关键经验:
- 数据预处理至关重要:在机器学习中,调整输入数据往往比调整模型参数更能提升效果。
- 图块尺寸宜大不宜小:确保你的数据图块尺寸大于核心特征区域,以便后续进行裁剪和归一化。
- 尽可能去趋势和归一化:消除不相关的变异(如整体坡度、方向),能让模型更专注于你关心的特征。
- 利用模拟扩展数据:当真实标注数据稀缺时,通过物理模拟添加可控噪声来生成大量训练数据是一个有效策略。
- 无监督学习提供出路:在缺乏标签的情况下,自组织映射等无监督方法可以帮助发现数据中的自然类别。
通过结合地理空间数据处理、物理模拟和深度学习,我们能够从具有挑战性的遥感数据中提取出有价值的信息,以应对诸如冰川变化监测等重要科学问题。
课程总结:本节课我们深入探讨了如何利用 ICESat-1 激光测高数据,通过模拟波形、深度学习匹配以及无监督聚类的方法,来解决格陵兰冰裂隙识别这一非平凡的科学问题。核心在于通过巧妙的数据工程和模型设计,克服了数据反演的不确定性和标注数据缺失的挑战。
课程 P32:Sacred 🧪 - 如何停止担忧并爱上科研
在本课程中,我们将学习一个名为 Sacred 的开源框架。它旨在为计算实验提供基础架构,帮助研究人员摆脱日常实验中的琐碎烦恼,使科研工作更加流畅、可复现和有条理。
概述:一个科研同事的烦恼故事
为了理解 Sacred 旨在解决的问题,让我们想象一位同事的科研经历。
这位同事有一个很棒的项目想法。他建立了一个私有 GitHub 仓库,从配置文件加载参数,并将输出和结果记录在编号的目录中。初步结果看起来很有希望。
然而,他很快发现频繁编辑配置文件很繁琐。于是他构建了一个命令行界面。接着,他发现配置参数之间存在依赖关系(例如,alpha 参数通常希望设为 1 / sqrt(层数)),因此他为配置过程添加了后处理逻辑。
当导师要求他在几周内提交论文时,问题开始显现。他需要运行系统性的实验,但结果文件分散在众多目录中,难以筛选和分组。时间紧迫,他只能手动复制文件并用 Jupyter Notebook 进行分析。
在尝试改进方法时,他将配置过程改为使用全局变量。但重新运行旧配置后,结果反而更差了。由于 Git 提交不频繁,他不得不手动回退代码以定位问题。
随后,竞争压力迫使他使用新数据集并在多台服务器上运行实验。旧的目录编号方案导致文件被覆盖。他修复了存储方式,但新数据集上的结果不理想。
接着,他进行参数调优,编写脚本生成配置文件并分发运行。不幸的是,一个配置文件中的拼写错误导致大量实验时间被浪费。截止日期临近,他在恐慌中删除了旧文件并重新运行所有实验。
最终,在调试时他发现,由于使用了全局配置,一个关键的 metrics 参数在运行中被意外覆盖,导致所有结果都无效。
这个过程远非理想。在截止日期压力下,即使是最佳习惯也可能崩溃,让位于混乱和绝望。那么,我们能做些什么呢?
引入 Sacred:无忧计算实验框架
Sacred 是一个旨在为计算实验提供无忧基础架构的小型开源框架。其哲学是让计算实验变得有趣且可复现(在中期研究阶段),具有最少的样板代码和最大的便利性,并保持模块化和可扩展性。
在 Sacred 中,核心抽象是 实验(Experiment)。Experiment 类负责收集配置、要运行的函数以及观察者(Observers)。你可以从命令行或 Python 内部启动实验运行(Run)。
运行过程中,Sacred 会定期触发事件给观察者,以收集所有必要信息。主要有四种事件类型:
- 开始事件:包含配置、检测到的源代码、依赖项、主机和元信息。
- 心跳事件:定期更新捕获的标准输出和自定义实时信息。
- 完成/失败事件:包含结果或堆栈跟踪。
- 工件和资源事件:用于添加特定文件,这些文件将与运行记录一起存储。
快速上手:三行代码创建 Sacred 实验
假设你的项目有一个可作为入口点的函数,代码如下:
def my_main():
# 你的研究代码
print("Running experiment...")
if __name__ == '__main__':
my_main()
将其转换为 Sacred 项目只需添加三行代码:
from sacred import Experiment # 1. 导入 Experiment
ex = Experiment() # 2. 实例化实验
@ex.main # 3. 装饰主函数
def my_main():
# 你的研究代码
print("Running experiment...")



if __name__ == '__main__':
ex.run_command_line() # 改为使用 Sacred 的命令行运行器


实际上,如果你使用 @ex.automain 装饰器,可以省略最后的 if 块。添加这三行后,你就拥有了一个 Sacred 项目,可以从命令行执行,例如:
python my_experiment.py help
即使只做这些,也已获益匪浅。你立即获得了一个强大的命令行界面,并可以添加文件存储观察者,自动保存配置、输出、依赖项和源代码副本。此外,Sacred 会自动为实验添加一个随机种子(seed),并通过设置种子(如 python my_experiment.py with seed=1)使实验具备基本的可复现性,因为它会自动为 NumPy、random 和 TensorFlow(如果已安装)设置随机数生成器。
配置系统:强大而灵活
现在,让我们深入了解 Sacred 强大的配置系统。定义配置有多种方式,最优雅的一种是使用 @ex.config 装饰器:
@ex.config
def my_config():
# 此函数内的局部变量将成为配置项
num_layers = 7
optimizer = "adam"
# 配置项之间可以有依赖关系
alpha = 1.0 / (num_layers ** 0.5) # 公式:alpha = 1 / sqrt(num_layers)
# 根据其他配置项进行条件设置
if optimizer == "adam":
learning_rate = 0.001
else:
learning_rate = 0.01
添加此函数后,打印配置将显示所有条目。你可以从命令行轻松覆盖任何配置:
python my_experiment.py with num_layers=5 optimizer="sgd"
Sacred 会自动解析依赖关系(例如 alpha 会随之调整)。如果你拼写错了配置项名称,Sacred 会给出错误提示。
访问配置:通过注入而非全局变量
定义了配置后,如何在代码中访问它们?Sacred 采用了 配置注入(Configuration Injection) 的概念。你只需在需要使用配置的函数参数中声明它们:
@ex.capture
def train_model(num_layers, learning_rate, dataset="default"):
# 可以直接使用 num_layers 和 learning_rate
print(f"Training with {num_layers} layers and LR={learning_rate} on {dataset}")
# ... 训练逻辑 ...
@ex.main
def my_main():
train_model() # 无需传递参数,Sacred 会自动注入配置值
train_model(dataset="special") # 你也可以覆盖部分参数
配置注入的优先级是:显式传递的参数 > 配置值 > 函数默认值。这避免了在代码中到处传递配置的混乱,也消除了全局变量,提高了函数可重用性。
记录与存储:使用观察者和数据库
为了存储和分析结果,我们需要观察者。推荐使用 MongoDB 观察者,它是一个无模式数据库,非常适合存储结构可能变化的实验配置和结果。
你可以从命令行或脚本中添加它:
python my_experiment.py -m my_database
或者在代码中:
from sacred.observers import MongoObserver
ex.observers.append(MongoObserver(url='localhost:27017', db_name='my_database'))
所有实验的运行信息(配置、结果、标准输出、源代码等)都会存入 MongoDB。之后,你可以用几行 Python 代码轻松查询和分析:
from pymongo import MongoClient
client = MongoClient()
db = client.my_database.runs
# 查询所有 num_layers 为 7 的实验
runs = db.find({'config.num_layers': 7})
# 轻松转换为 Pandas DataFrame 进行分析和绘图
扩展工作流:参数调优与监控
当需要大规模运行实验(例如应对“俄罗斯实验室”的竞争)时,Sacred 的核心与两个扩展工具能提供帮助:
-
LabWatch:一个超参数优化接口(目前处于早期阶段)。它通过 MongoDB 协调优化过程,支持多种优化器。你定义一个搜索空间,LabWatch 会建议可能带来更好结果的参数组合。
-
Sacredboard:一个基于 Web 的仪表板,用于实时监控实验进展。它连接到 MongoDB,可以显示正在运行、已完成或中断的实验,查看详细配置,如果使用了特定的指标 API,还能实时显示收敛曲线。
这些工具共同构成了一个强大的工作流,帮助你从混乱走向高效、有序的科研状态。
总结与展望
本节课我们一起学习了 Sacred 框架。我们看到,琐碎的后勤问题可能累积成灾难,而拥有一个合适的基础架构工具至关重要。
Sacred 旨在填补这一空白,它通过以下方式支持计算实验工作流:
- 极简集成:几行代码即可将现有项目转换为 Sacred 实验。
- 强大配置:支持灵活、可互依赖的配置定义和注入。
- 全面记录:通过观察者自动捕获实验的方方面面。
- 便捷分析:与数据库(如 MongoDB)集成,便于查询和结果分析。
- 生态扩展:通过 LabWatch 和 Sacredboard 等工具支持超参数优化和实验监控。
Sacred 并非唯一此类工具(类似的有 Sumatra、Reprozip 等),但其在 Python 计算实验的便捷性和配置系统方面具有独特优势。我们希望 Sacred 能逐渐成为计算实验的默认工作流基础。
未来,Sacred 将继续发展,添加更多功能。记住,良好的工具习惯能让你更专注于科学本身,而非周围的混乱。
课程P33:使用scikit-dsp-comm进行信号处理与通信实践 🎛️📡
在本课程中,我们将学习如何使用Python的scikit-dsp-comm包进行信号处理和通信的实践操作。课程内容涵盖从基础信号概念到实际音频处理和软件定义无线电应用的完整流程。
概述
本教程旨在提供一个信号处理与通信的实践入门。我们将从基础概念开始,逐步深入到实际应用,包括音频处理、滤波器设计和软件定义无线电接收。课程内容设计简单直白,适合初学者理解。
第一部分:信号与系统基础 📈
首先,我们需要理解信号与系统的基本概念。信号可以是自然产生的,如心跳或风速,也可以是人为制造的,如通信信号。系统则是对这些信号进行操作和处理的部分。
连续时间与离散时间信号
连续时间信号在时间上是连续的,而离散时间信号则是通过采样得到的序列。在计算机中,我们主要处理离散时间信号。
连续时间余弦波公式:
x(t) = A * cos(2πf₀t + φ)
离散时间余弦波公式:
x[n] = A * cos(2πf₀n/fs + φ)
其中,A是振幅,f₀是频率,φ是相位,fs是采样率。
采样与混叠
采样是将连续时间信号转换为离散时间信号的过程。如果采样率不足,会导致混叠现象,即高频信号被错误地表示为低频信号。为了避免混叠,采样率必须大于信号最高频率的两倍。
基本脉冲信号
在信号处理中,脉冲信号是构建更复杂波形的基础。常见的脉冲信号包括矩形脉冲和三角脉冲。
矩形脉冲函数:
def rect_pulse(t, tau):
return 1 if abs(t) <= tau/2 else 0
三角脉冲函数:
def tri_pulse(t, tau):
return max(0, 1 - abs(t)/tau)
频域分析
频域分析是将信号从时间域转换到频率域的过程。通过傅里叶变换,我们可以分析信号的频率成分。
傅里叶变换公式:
X(f) = ∫ x(t) * e^(-j2πft) dt

在离散时间中,我们使用快速傅里叶变换(FFT)进行计算。
第二部分:系统与滤波器设计 🔧
系统对信号进行操作,例如滤波可以去除噪声。滤波器是信号处理中的核心组件,分为有限脉冲响应(FIR)滤波器和无限脉冲响应(IIR)滤波器。
差分方程
离散时间系统通常用差分方程描述。一个通用的差分方程形式如下:
差分方程公式:
y[n] = Σ b[k] * x[n-k] - Σ a[k] * y[n-k]
其中,x[n]是输入信号,y[n]是输出信号,b[k]和a[k]是系数。

滤波器设计
使用scikit-dsp-comm包可以方便地设计滤波器。例如,设计一个FIR带通滤波器:
from sk_dsp_comm import fir_design_helper
b = fir_design_helper.bandpass(f_pass1, f_pass2, fs, ripple_db, atten_db)
滤波器频率响应
滤波器的频率响应描述了滤波器对不同频率信号的增益。通过零极点图可以直观地理解滤波器的特性。
第三部分:音频处理实践 🎵
音频处理是信号处理的重要应用之一。我们将使用pyaudio进行实时音频处理。
实时音频流处理
pyaudio允许我们通过回调函数处理音频流。回调函数在每次音频帧到达时被调用,我们可以在其中进行信号处理。
示例回调函数:
def callback(in_data, frame_count, time_info, status):
# 处理输入数据
processed_data = process_audio(in_data)
return processed_data, pyaudio.paContinue
音频特效:镶边效果

镶边效果是一种音频特效,通过混合原始信号和经过时变延迟的信号实现。这可以产生类似多普勒效应的声音效果。

时变延迟实现:
def time_varying_delay(signal, delay_function):
# 根据延迟函数处理信号
return delayed_signal
第四部分:通信系统与软件定义无线电 📻

通信系统涉及信号的调制、传输和解调。软件定义无线电(SDR)允许我们通过软件实现无线电功能。


调频(FM)解调
调频是一种常见的模拟调制方式。解调过程涉及从接收信号中提取原始信息。
FM解调公式:
m(t) = dφ(t)/dt
其中,φ(t)是接收信号的相位。
RTL-SDR接收器
RTL-SDR是一种低成本的软件定义无线电设备。我们可以使用它接收和解析FM广播信号。
RTL-SDR捕获信号:
from rtlsdr import RtlSdr
sdr = RtlSdr()
sdr.sample_rate = 2.4e6
samples = sdr.read_samples(5 * sdr.sample_rate)
立体声FM解调
立体声FM广播包含左声道和右声道信息,以及一个19 kHz的导频信号。解调过程需要提取这些成分并重建立体声音频。
第五部分:数字通信与频移键控(FSK) 🔢

数字通信使用离散符号传输信息。频移键控是一种简单的数字调制方式,通过改变载波频率表示二进制数据。
FSK调制与解调
FSK调制将二进制数据映射到不同的频率。解调过程涉及检测接收信号的频率并还原原始数据。
FSK解调示例:
def fsk_demodulator(signal, f1, f2, fs):
# 检测信号频率并解码为二进制数据
return binary_data
位同步
在数字通信中,位同步是确保接收端正确采样数据的关键步骤。通过匹配滤波器和时钟恢复算法可以实现位同步。
总结
在本课程中,我们一起学习了信号处理与通信的基础知识,包括信号与系统概念、滤波器设计、音频处理以及软件定义无线电的应用。通过实践操作,我们掌握了使用scikit-dsp-comm包进行信号处理和通信系统仿真的基本技能。希望这些内容能为你的进一步学习和应用提供坚实的基础。


课程 P34:将通用制图工具引入 Python 🗺️
在本课程中,我们将学习如何通过一个名为 gmt 的 Python 库,将强大的通用制图工具(GMT)的功能引入 Python 环境。我们将了解该项目的背景、设计目标、核心实现方式,并学习如何使用它来创建地图。
概述
通用制图工具(GMT)是一个用于处理空间数据和制作高质量地图的命令行程序集合。它功能强大,但传统的命令行使用方式对于 Python 用户来说不够友好。本课程介绍的 gmt Python 库,旨在通过一个现代、Pythonic 的接口,让用户能够直接在 Python 脚本或 Jupyter Notebook 中使用 GMT 的全部功能。
上一节我们介绍了课程的整体目标,本节中我们来看看 GMT 是什么以及为什么需要 Python 包装器。
GMT 简介
GMT 是一个 C 语言编写的命令行程序集合,主要用于处理空间数据。这些数据包括地震震中、海底地形等。GMT 可以进行球面数学计算,例如计算大圆距离和旋转,但它最著名的是能够制作非常精美和专业的地图。
GMT 项目历史悠久,首个版本发布于 1988 年,因此非常成熟和稳定。其制图细节处理得极为出色,这是其图形质量高的原因。在 GMT 5 版本中,项目引入了 C API,其命令行程序即基于此 API 构建。引入 C API 的目的就是为了方便在其他语言(如 MATLAB、Julia)上构建包装器。
项目动机与现有方案
在开始构建新的 Python 包装器之前,作者回顾了现有的几个 Python GMT 包装器。
以下是当时存在的几个 Python GMT 包装器:
- GMT Pi:开发最为活跃,但自 2014 年起已停止更新。
- pi-gmt(小写):使用系统调用来运行命令行程序。
- PiGMT(大写):通过手写 Python C 扩展来连接 C API。
这些方案大多没有利用专为包装器设计的 C API,或者实现和分发过程非常复杂。因此,需要一个全新的、官方的解决方案。
项目目标
基于对现状的分析,新的官方 Python 包装器设定了明确的目标。
本项目的核心目标如下:
- 使用 C API:直接通过 C API 进行包装,避免系统调用、进程管理和重定向。
- 提供 Pythonic 接口:库的使用体验应像原生 Python 库,而不是在 Python 中编写命令行程序。
- 与 SciPy 技术栈集成:能够方便地与 NumPy 数组、通过 xarray 或 netCDF4 库加载的 NetCDF 网格等数据进行交互。
- 面向现代与未来:支持 Python 3.5+,并依赖即将发布的 GMT 6 版本,利用其新特性。
GMT 6 的现代模式


新包装器将充分利用 GMT 6 中引入的“现代执行模式”,这极大地简化了工作流程。
为了理解现代模式的优势,我们将其与经典模式进行对比。在经典 GMT 模式中,用户需要编写类似 Shell 的脚本,手动管理 PostScript 代码的重定向和文件拼接,并使用 psconvert 进行格式转换,过程繁琐且容易出错。
现代模式通过引入新命令改变了这一流程。以下是现代模式脚本的关键组成部分:
gmt begin figure_name png # 开始一个会话,并指定输出文件名和格式
gmt grdgradient ...
gmt makecpt ...
gmt grdimage ...
gmt pscoast ...
gmt psxy ...
gmt end # 结束会话,自动完成转换和输出
现代模式的主要改进包括:
- 自动会话管理:
gmt begin和gmt end管理整个绘图会话。 - 自动文件处理:用户无需关心 PostScript 重定向和
psconvert调用,GMT 会在后台自动处理。 - 更清晰的逻辑:移除了容易出错的
-K,-O等参数。
这种模式使得从 Python 调用 GMT 变得像使用 Matplotlib 一样直观:你执行绘图命令,然后在需要时显示或保存图形。
Python 包装器设计与演示
现在,让我们进入核心部分,看看如何将 GMT 的现代模式封装成 Python 库。
首先,需要导入库并初始化一个现代模式会话:
import gmt
导入 gmt 库会自动调用 gmt begin,在临时目录中创建一个 GMT 会话。当 Python 会话结束时,它会自动清理。
接着,可以创建一个新图形并设置其名称(输出格式由后续的 show 或 savefig 决定):
fig = gmt.Figure()
然后,可以调用 GMT 模块对应的函数。所有 GMT 模块都作为 gmt 库的函数暴露出来。例如,绘制海岸线的命令 pscoast 对应函数 gmt.pscoast。参数可以通过 GMT 传统的单字母选项传入:
gmt.pscoast(R="-30/30/-40/40", J="M6i", B="", G="chocolate")
但为了更符合 Python 风格,包装器提供了长格式的别名参数,使代码更易读:
gmt.pscoast(region="-30/30/-40/40", projection="M6i", frame=True, land="chocolate")
参数也支持 Python 原生类型转换,例如区域(region)可以用列表指定:
gmt.pscoast(region=[-30, 30, -40, 40], ...)
执行绘图命令后,图形并不会立即显示。需要调用 show() 方法在 Notebook 中显示,或调用 savefig() 方法保存为文件:
fig.show()
# 或
fig.savefig("my_map.png")
如何参与贡献

这个项目需要社区的帮助才能快速发展。目前有超过 60 个 GMT 模块需要包装,工作量巨大。



以下是几种参与贡献的方式:
- 包装模块:任务包括从 GMT 手册复制文档到函数文档字符串、定义参数别名、编写测试等。
- 构建 Conda 包:目前 GMT 的 Conda 包仅支持 Linux。需要熟悉 macOS 和 Windows 平台 Conda 包构建的开发者帮助。
- 提供反馈:项目处于早期阶段,非常欢迎社区就 API 设计、功能特性等提出建议。

总结

在本节课中,我们一起学习了将 GMT 引入 Python 的官方项目。我们了解了 GMT 的基本情况、现有包装器的不足,以及新项目的目标和设计原则。我们重点探讨了 GMT 6 的现代模式如何简化绘图流程,并演示了如何使用新的 gmt Python 库以更 Pythonic 的方式创建地图。最后,我们看到了这个开源项目需要社区的支持,并介绍了参与贡献的途径。通过这个库,地球科学和地理空间领域的 Python 用户将能更轻松地利用 GMT 强大的制图能力。
🧠 机器学习与 scikit-learn 教程(第一部分) - P4
在本节课中,我们将学习机器学习的基础概念,并了解如何使用 scikit-learn 库进行实践。课程将从机器学习的核心思想开始,逐步深入到数据表示、监督学习、无监督学习以及 scikit-learn 的统一 API。
📚 机器学习概述
机器学习的目标是从数据中学习并做出预测或决策。其核心思想是泛化,即模型不仅要在训练数据上表现良好,还要能够准确预测新的、未见过的数据。
一个标准的机器学习流程如下:你拥有一些带有标签(或称为目标值)的历史数据(训练集)。算法通过学习这些数据来预测标签。当遇到新的观测数据(测试集)时,算法需要能够正确预测其标签。
以下是机器学习的主要类别:
- 监督学习:数据带有标签。目标是学习从输入特征到标签的映射。
- 分类:预测离散的类别标签(例如,判断邮件是否为垃圾邮件)。
- 回归:预测连续的数值(例如,预测房屋价格)。
- 无监督学习:数据没有标签。目标是发现数据中的内在结构或模式。
- 聚类:将相似的数据点分组。
- 降维:将高维数据投影到低维空间以便可视化或压缩。
- 异常检测:识别与大多数数据显著不同的数据点。
🧮 数据表示与可视化
在 scikit-learn 中,数据通常被表示为一个二维的 NumPy 数组 X,其形状为 (n_samples, n_features)。每一行代表一个样本(观测值),每一列代表一个特征(描述符)。
我们将使用经典的 Iris(鸢尾花) 数据集作为示例。这个数据集包含 150 个样本,每个样本有 4 个特征(萼片长度、萼片宽度、花瓣长度、花瓣宽度),目标是将花分类为三个品种之一。
from sklearn.datasets import load_iris
iris = load_iris()
X = iris.data # 特征矩阵,形状 (150, 4)
y = iris.target # 目标标签,形状 (150,)
在开始建模前,可视化数据非常重要。例如,我们可以绘制特征之间的散点图来观察类别是否可分。
import matplotlib.pyplot as plt
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1])
🧪 训练集与测试集
为了评估模型的泛化能力,我们需要将数据划分为训练集和测试集。模型只在训练集上学习,然后在测试集上评估性能。这可以防止模型“死记硬背”(过拟合)。
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.3, random_state=42)
参数 random_state 用于固定随机种子,确保每次分割的结果可复现。对于分类问题,可以使用 stratify=y 参数进行分层抽样,确保训练集和测试集中各类别的比例与原始数据集一致。
🔍 监督学习:分类
上一节我们介绍了如何准备数据,本节中我们来看看如何使用 scikit-learn 构建一个分类器。
我们将使用一个简单的 k-最近邻(k-NN) 分类器。它的原理是:对于一个新样本,在训练集中找到 k 个最相似的样本(邻居),然后根据这 k 个邻居的标签来预测新样本的标签。
以下是使用 k-NN 分类器的基本步骤:
- 导入并实例化模型。
- 在训练集上拟合(训练)模型。
- 在测试集上进行预测。
- 评估模型准确率。
from sklearn.neighbors import KNeighborsClassifier
# 1. 实例化模型,设置邻居数 k=3
knn = KNeighborsClassifier(n_neighbors=3)
# 2. 在训练集上拟合模型
knn.fit(X_train, y_train)
# 3. 在测试集上预测
y_pred = knn.predict(X_test)
# 4. 评估准确率
accuracy = (y_pred == y_test).mean()
print(f"准确率: {accuracy:.2f}")
所有 scikit-learn 的估计器(模型)都遵循类似的 .fit() 和 .predict() API。拟合后,模型会学习到一些参数,这些参数通常存储在以下划线结尾的属性中(例如,对于线性模型,系数存储在 coef_ 中)。
📈 监督学习:回归
现在,让我们转向回归问题。回归的目标是预测一个连续值,而不是一个类别。
我们将使用一个合成的数据集来演示,其中 y 是 x 的正弦函数加上一些线性趋势和噪声。我们的目标是拟合这个关系。
首先,我们尝试最简单的 线性回归 模型。它试图找到一条直线(在高维空间中是超平面)来最好地拟合数据。





from sklearn.linear_model import LinearRegression
# 生成合成数据
X = np.linspace(0, 10, 100).reshape(-1, 1)
y = np.sin(X).ravel() + X.ravel() * 0.5 + np.random.randn(100) * 0.5



reg = LinearRegression()
reg.fit(X_train, y_train)
y_pred = reg.predict(X_test)
线性模型的形式是:y_pred = coef_ * X + intercept_。对于这个非线性数据,线性回归只能捕捉到整体的线性趋势,而无法捕捉周期性波动。
为了让线性模型处理非线性关系,我们可以进行特征工程,即手动创建非线性特征(例如,添加 sin(X) 作为一个新特征),然后让线性模型去学习这些特征的组合。
# 添加正弦波作为新特征
X_new = np.hstack([X, np.sin(4 * X)])
reg_new = LinearRegression()
reg_new.fit(X_new_train, y_train)
这样,模型就能同时捕捉线性和周期性的模式了。当然,更强大的模型(如 k-NN 回归器或基于树的模型)可以自动学习这些非线性关系。
🌀 无监督学习:数据预处理与变换

在深入更复杂的无监督学习之前,我们先看看一些基本的数据变换。许多机器学习算法对数据的尺度很敏感,因此标准化是一个常见的预处理步骤。

StandardScaler 会将每个特征缩放为均值为 0,方差为 1。关键点是:缩放器应该只在训练集上拟合(计算均值和方差),然后用同样的参数去变换训练集和测试集。

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(X_train) # 只在训练集上计算均值和标准差
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test) # 用训练集的参数变换测试集
🎯 无监督学习:降维(PCA)

主成分分析(PCA) 是一种常用的降维技术。它通过线性变换找到数据中方差最大的方向(主成分),并允许我们保留最重要的几个成分,丢弃信息量少的成分。
PCA 常用于数据可视化(将高维数据降至 2D 或 3D)或作为特征压缩/提取的前置步骤。
from sklearn.decomposition import PCA
pca = PCA(n_components=2) # 降至2维
X_pca = pca.fit_transform(X_scaled) # 拟合并变换数据
# X_pca 现在是一个二维数组,可以用于绘图
我们可以查看每个主成分所解释的方差比例,以决定保留多少成分。
print(pca.explained_variance_ratio_)
👥 无监督学习:聚类(K-Means)
聚类的目标是将数据点分组,使得同一组内的点彼此相似,而不同组的点彼此不同。K-Means 是最常用的聚类算法之一,它试图找到 k 个簇中心,并将每个点分配到最近的中心。
使用 K-Means 需要预先指定簇的数量 k。
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=3, random_state=42)
cluster_labels = kmeans.fit_predict(X) # 同时拟合和预测
# 可以查看簇中心
centers = kmeans.cluster_centers_
评估聚类质量是一个挑战,因为没有真实的标签。对于像手写数字这样的数据集,我们可以将聚类结果与真实标签比较(使用如 adjusted_rand_score 的指标),但在真正的无监督场景中,通常需要人工检查簇的含义。
K-Means 假设簇是球形的且大小相近,对于复杂形状的簇可能效果不佳。scikit-learn 还提供了其他聚类算法,如 DBSCAN 和 Agglomerative Clustering。
⚙️ scikit-learn API 统一接口回顾
scikit-learn 的所有估计器都遵循一个一致的接口,这大大简化了使用过程。主要的方法可以总结如下:
.fit(X, [y]): 在数据X(和标签y,如果是监督学习)上训练模型。这是学习的过程。.predict(X): 使用训练好的模型对新的数据X进行预测(用于分类、回归或聚类)。.transform(X): 使用训练好的模型将数据X转换到新的表示空间(用于预处理、降维等)。.score(X, y): 评估模型在给定数据(和真实标签)上的性能。对于分类器,默认是准确率;对于回归器,默认是 R² 分数。
许多模型还提供了便捷方法,如 .fit_predict()(用于聚类)和 .fit_transform()(用于变换)。
🚢 案例研究:泰坦尼克号生存预测
让我们将所学知识应用到一个更真实的数据集上:预测泰坦尼克号乘客的生存情况。这是一个二分类问题(生存/未生存)。
数据预处理是关键步骤,因为原始数据包含多种类型:
- 数值特征:如年龄、票价。可以直接使用,但可能需要处理缺失值(使用
SimpleImputer填充均值)。 - 分类特征:如性别、登船港口、客舱等级。需要转换为数值。
- 独热编码(One-Hot Encoding):为每个类别创建一个新的二进制特征。适用于线性模型、k-NN 等。可以使用
pandas.get_dummies或sklearn.preprocessing.OneHotEncoder。 - 标签编码(Label Encoding):为每个类别分配一个整数。适用于树模型(如随机森林)。
- 独热编码(One-Hot Encoding):为每个类别创建一个新的二进制特征。适用于线性模型、k-NN 等。可以使用
以下是处理流程的概要:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
# 1. 加载数据,选择特征
data = pd.read_csv('titanic.csv')
features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
X = data[features]
y = data['Survived']
# 2. 分割数据
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 3. 预处理:将分类特征转换为独热编码,并处理数值特征的缺失值
X_train = pd.get_dummies(X_train) # 自动处理分类列
imputer = SimpleImputer(strategy='mean')
X_train_imputed = imputer.fit_transform(X_train)
# 对测试集进行同样的转换(使用训练集拟合的imputer和get_dummies的列结构)
# ... (需要更细致的处理来保持列对齐)
# 4. 训练模型
logreg = LogisticRegression()
logreg.fit(X_train_imputed, y_train)
# 5. 评估(需要在测试集上应用相同的预处理步骤)
# ...
通过这个案例,你可以看到构建一个机器学习管道涉及数据清理、特征工程、模型选择和评估等多个环节。
🎯 总结
在本节课中,我们一起学习了:
- 机器学习的基本概念:监督学习(分类、回归)与无监督学习(聚类、降维)。
- 在 scikit-learn 中表示数据:使用形状为
(n_samples, n_features)的 NumPy 数组。 - 评估模型泛化能力的关键:将数据分割为训练集和测试集。
- 使用 scikit-learn 的统一 API(
.fit(),.predict(),.score())构建和评估分类器与回归器。 - 常见的数据预处理技术:标准化和独热编码。
- 无监督学习技术:PCA 用于降维和可视化,K-Means 用于聚类。
- 通过 泰坦尼克号数据集 的案例,实践了从原始数据到建模的完整流程。

这些基础知识为你使用 scikit-learn 解决实际问题奠定了坚实的基础。在接下来的课程中,我们将深入探讨模型选择、超参数调优、集成方法以及文本数据处理等更高级的主题。
课程 P5:NumPy 数值计算入门 🧮
在本节课中,我们将要学习 NumPy 库的基础知识。NumPy 是 Python 科学计算生态系统的核心库,它解决了 Python 在处理大规模数值计算时性能不足的问题。我们将从 NumPy 的核心概念、数据结构讲起,逐步学习如何创建、操作数组,并进行高效的数值运算。




课程结构与行为准则 📜
在开始学习 NumPy 之前,我们先了解一下本次课程的结构和一些重要的行为准则。












本次课程的结构如下:我会先进行讲解,期间会在白板上画图或在电脑上编写代码。在编写代码的部分,我强烈建议你跟着一起敲代码。学习这些内容最糟糕的方式就是只听我讲。我们会尽量减少单纯讲解的时间,只提供让你能成功完成练习所需的最少信息。














练习分为两种。第一种是幻灯片上的小练习,会要求你“创建一个这样的数组,用它做这个操作,再做那个操作”。另一种练习会更复杂一些,需要你打开一个 Python 程序,编辑它并查看结果,以完成一系列目标。到了这部分,我会简要介绍这些练习,然后让你们自己动手。我们会在教室里走动,回答你们可能遇到的问题。













另外,咖啡在 10:30 会消失。我们会在那之前休息一下,确保大家都有机会补充咖啡因。












对于这门课,我将使用 Canopy。它能提供展示 NumPy 所需的一切,而没有不需要的东西。如果你在终端工作,没问题。如果你在 notebook 中工作,也没问题。它们之间的语法会有细微差别,我会在遇到时尽量指出。















其中一个主要的区别是,在这些练习中会有很多需要绘制数组的附加部分。为了让绘图在像 IPython 终端这样的交互式会话中正确工作,我们需要输入一些 IPython 魔法命令:%matplotlib。如果你在 notebook 中而不是 IPython 会话中,你需要在后面加上 inline 这个词。














如果你听到一个没听过的词,请举手让我为你解释。如果你听过一个词,但我使用的方式和你理解的不同,你也应该问我。如果你觉得举手不舒服,我看到你们中有些人有软件 carpentry 或数据科学会议的小粉红便利贴,你们也可以用那些。
为什么需要 NumPy?⚡
我们在这里讨论一个名为 NumPy 的库。NumPy 是 Python 生态系统中的一个库,它将为我们解决一个非常大的问题。
我们喜欢用 Python 工作。Python 是一门神奇的语言。它让做常见的事情变得非常容易,也让做不常见的事情成为可能。它写起来很简洁,读起来和理解起来很容易,调试也相对简单。作为一种编程语言,它很安全,它在幕后为我们做了所有这些神奇的事情,以确保我们不会做诸如损坏计算机之类的事情。
Python 有一个主要弱点。Python 在数学计算方面不是特别好。我们将通过使用 NumPy 库来绕过这个弱点。
我们要做的第一件事是向你展示这看起来是什么样子。我想生成两个数据范围。一个将使用原生的 Python 数据结构。我们要做的就是计算这些数据的和。我们将用 NumPy 数组做同样的事情。我们将生成一个 NumPy 数据数组,并求其和。我们将看到完成这两个过程各需要多长时间。
首先,我可以创建一个像 test_list 这样的东西。这个 test_list 将包含,比如说,1000 个整数。Python 内置的 range 函数将生成一个从 0 到 999 的整数列表。请注意,我将其包装在 list() 强制转换中。如果你使用 Python 3,range 函数具有惰性行为。为了确保我们得到一个实际的数据结构,我们将使用 list 构造函数强制其立即求值。
接下来,我们要创建具有相同元素数量的 NumPy 数组。首先要做的是导入 NumPy 库。它的拼写是 numpy。你会看到我在这里使用了别名导入。我将其导入为另一个名字。这在 Python 世界不是法律,但这是一个非常强的惯例。当你查看别人的代码时,你几乎总是会看到 NumPy 以 np 的名字被导入。遵循这个惯例对你或任何其他人来说都很容易。如果他们查看 Python 模块中的某个函数,并在其中看到 np 这个名字,我们立刻就知道那是什么。
我已经导入了 NumPy 库。我将使用 NumPy 中与 range 对应的函数,它叫做 arange。同样,我们将创建一个包含 1000 个元素的对象。
现在我们要做的是对这两个数据结构求和,并使用 IPython 的魔法命令 %timeit 来计时。在 Python 世界中,我们有一整套以百分号开头的便捷函数,我们称之为魔法函数。我们这里想用的是 %timeit。我们将计时对原生 Python 数据结构 test_list 调用 Python 内置的 sum 函数需要多长时间。Python 将多次运行这个表达式,并给出它认为执行所需时间的最佳估计,忽略计算机正在做的所有其他事情,比如与投影仪通信、在后台运行 PowerPoint。IPython 对在我的计算机上执行所需时间的最佳估计大约是 5 微秒。在你的计算机上,时间可能不同,这没关系。

我们将做同样的事情,但这次不是对 Python 数据结构使用 Python 函数,而是对我们的 NumPy 数据结构使用 NumPy 的函数。再次使用魔法命令 %timeit。我们将使用 np.sum,这是 NumPy 的求和函数,并对我们的 test_array 进行操作。和之前一样,Python 将为我们多次运行这个操作,并给出它认为所需时间的最佳估计。
对于包含 1000 个元素的小型数据结构,我们看到使用 NumPy 使代码运行速度大约快了一倍。随着数据结构规模的增大,这种差异会变得更大。对于一个包含一百万个元素的数组,我们将看到使用 NumPy 会快 1 到 2 个数量级,也就是大约快 10 倍到 100 倍。为了给这个一点背景,这相当于等待一秒完成某事和等待几分钟的区别。这正是我们要讨论的内容。



问题是,速度性能的提升是否只适用于整数运算?答案是,对于你将进行的任何类型的数学运算,NumPy 版本总是比 Python 版本快。
这主要有两个原因。为了解释原因,我将开始在黑板上画一些图。




NumPy 速度背后的原理 🏎️
第一个原因与内存访问模式有关。我们来画出 Python 中列表的样子,以及我们如何从列表中获取项目。然后画出 NumPy 中数据结构的样子,以及 Python 如何从 NumPy 数组中提取数据。
如果我们考虑计算机中内存的样子,在 Python 列表中,我们会有一个数组。这个数组是动态调整大小的,并且由指针组成。你可以把每个指针看作一个地址,它知道该数组中对象的位置。例如,这个指针可能指向内存中其他地方存放字符串 "doing" 的位置。另一个指针可能指向内存中包含另一个数组(另一个 Python 列表)的位置。这意味着,当我检索一个项目时,如果该项目是像整数、浮点数或字符串这样的东西,每次我这样做时,我都在增加一次查找。我必须去内存中的其他地方获取东西。如果我的数据结构中有嵌套,我可能必须执行两次甚至三次查找。这些单独操作中的任何一个时间都非常短,大约在几十纳秒的尺度上,所以非常快。但如果只发生一次的话。



但通常,当我们构建 NumPy 数组时,不仅仅是因为我们想把 1 和 2 相加,而是因为我们有数百万个元素需要求和、计算标准差或找出最大值。给每个元素之间的二元操作增加 40 或 60 纳秒,累积起来就变得非常巨大。当你对数百万、数千万个事物进行这种操作时,就不是 20 纳秒了,而是达到了秒的尺度,是人类可察觉的减速。






我们使用 NumPy 数组时,内存中不是对引用的引用再引用。我们在内存中拥有的是一个未调整大小的密集数组,其中实际包含了我们感兴趣的数据。没有双重查找,没有三重查找。如果我想要数字 1,我就去我知道 1 所在的位置,那里就是我的数据。这是我们速度提升的一个来源。




不过,大部分速度提升来自于 Python 是一种我们称之为动态类型语言的事实。这意味着我们不必提前让 Python 解释器知道我们将给它什么类型的数据。这会是一个列表吗?一个字典?一个整数还是一个浮点数?这在我们用 Python 编码时非常好,因为我们可以摆脱很多样板代码。我们可以编写更容易组合、更容易扩展的代码。
缺点是,如果我在 Python 中有一个简单的操作,例如,我想求数字 0 和数字 1 的和,会发生很多中间转换和检查步骤。所以,在 Python 中执行 0 + 1 时,它首先要将那个加号运算符转换为该整数上的 __add__ 方法。它有很多检查。例如,这个整数的 __add__ 方法是否适用于作为参数传递给此方法调用的任何东西?我必须检查这个数字是否大到必须放入不同类型的数据结构和更大的内存中。我必须检查这个操作的左侧是否有显式的 NotImplemented 语句。然后我可以去检查右侧。同样,这些错误检查步骤、这些间接层中的每一个都只增加了一小部分时间。但是,如果你给 Python 一个对象列表,它无法知道该列表中的每个对象是否都是整数,甚至是否是数字。所以它必须每次都执行所有这些检查。同样,如果我们只做一次,这不是什么大问题。但如果我们必须这样做一百万次,这会大大增加我们程序的运行时间。


相比之下,当我们在 NumPy 中创建数值数据数组时,我们会提前告诉 NumPy:这些将是非常具体的 64 位整数,这些将是 32 位浮点数,这些将是 128 位复数。这样,当 NumPy 执行我们的加法时,它不必对这个数组中的每个对象进行这些类型检查。它知道对于其中的每一个,我总是进行 64 位整数加法。它将在用 C 编写的代码层面为我们执行所有这些操作,而不是用纯 Python 编写。所以,我们不仅避免了所有这些类型检查,还避免了 Python 解释器本身带来的大量开销。

但有时你必须进行检查。你只检查一次。假设你正在读取一个数据文件,其中有一个错误或一个数字,它有一个字符串,你试图将其加载到 NumPy 数组中。它会给你一个错误。所以问题是,如果我从磁盘读取一些数据,其中包含无法表示为数字的东西,我是否必须在进行加法时检查它?答案是,你会在读取时看到那个错误。所以,如果你从磁盘读取数据,并告诉 NumPy 这只会由 64 位浮点数组成,而其中有一些数据不符合该格式,你会立即看到那个异常。










因此,使用 NumPy 的好处是,我们将获得这一到两个数量级的性能提升。你必须付出的代价是,我们将远离 Python 提供给我们的所有良好保护。这意味着 NumPy 中的数字行为将与我们在 Python 中习惯看到的数字行为略有不同。我们还将看到,虽然数据结构本身支持许多我们在 Python 列表中习惯使用的相同方法,但我们对它们的使用方式有一些限制。






















NumPy 数组与 Python 列表的区别 🔄



数组看起来很像列表,但有一些非常重要的区别。

就数据结构本身而言,Python 列表允许是异构的,我的意思是它可以包含不同类型的数据。Python 列表也允许调整大小。因此,在 Python 中,列表的一个主要用途是在循环中累积数据。当你逐个生成数据时,你将其附加到该列表的末尾。
相比之下,我们的 NumPy 数组要求其数据类型是同质的。所以,如果我有一个我想用作某种表格数据结构的二维数组,我要求该表中的每个单独项目都具有相同的数据类型。它们可以都是整数,可以都是浮点数,如果我愿意,它们也可以都是字符串,但不能是这些的任意混合。另一个主要区别是 NumPy 数组是固定大小的。现在,有 99% 的把握你可以改变 NumPy 数组的大小,但这是一个非常昂贵的操作。
Python 有一个数组模块,你可以导入并使用数组数据结构。那不是 NumPy 数组。
创建和探索 NumPy 数组 🛠️
我们已经看到数据结构本身在一些关键方面会有所不同。数据结构内部的数据也会发生变化。为了向你展示这是什么样子,我们必须创建一些 NumPy 数组,以便实时查看它们的工作方式有何不同。
我想做的第一件事是创建一个数组。我们将创建一个整数数组。我们将使用 NumPy 内部的数组构造函数函数来做到这一点。我们将给它一个非常平淡无奇的名字,比如字母 a。这是我的名字。我有我的赋值运算符。是的?哦,当然。嗯。我有我的名字 a。我有我的赋值运算符。在右侧,我们将请求一个 NumPy 数组。所以是 np.array()。在括号内,我将用一些数据初始化我的数组。这里我们将有整数 -1、0、1 和 100。我们要再加一个东西。我稍后会解释这是做什么的。我们将使用可选参数 dtype 来指定我们希望此数组具有的数据类型。dtype 将是 int8。这是一个字符串字面量,告诉 NumPy 我想要一个 8 位整数。我可以按回车执行这行代码,然后这是我的 NumPy 数组。
我的第一个问题是,当我们在 Python 和基础 Python 中使用数字时,如果我试图将一个数字除以零会发生什么?完全正确。我得到一个零除错误。如果我说我想把 1 除以 0,Python 会告诉我这是警告我不要做的事情,所以它会引发一个异常来让我知道。
我们将做同样的事情,将一个数字除以零,但我们将除以 NumPy 数组 a 内部的数据。名称 a 然后除以 a[0],我看到零除错误了吗?我确实收到了一个警告,是的。没有引发异常。这个结果是一个有效的数据结构。它是一个充满零的 NumPy 数组。是的?你得到了无穷大。我敢打赌你所做的是你创建了一个浮点数数组,而不是一个整数数组。很好的直觉。我们接下来会讲到那里。原因是,当我们生活在 NumPy 内部时,我们不再处理 Python 数据类型。这些不是 Python 整数。这些是如果你自己用 C 编写代码时可以访问的那种整数。它们的行为遵循 IEEE 关于数字应如何行为的规范,而不是遵循 Python 关于不让你惊讶的理念。你可能会发现这是一个令人惊讶的结果:一个数字除以零等于零。控制数字的达特委员会已经决定这就是它的工作方式。是的。
我们还要做一件事。如果我问你一个问题,比如在 Python 中如何取一个数的平方,你会告诉我什么?是的。我可以把它乘以它自己。这是一个选项。或者我可以使用我的双星号运算符来要求 Python 对这个数字进行平方。所以这里我们有的是 Python 整数 100,我们正在对它进行平方。我们得到的结果是 10,000,这对我们来说并不奇怪。这是我们期望的。我可以把它取到任何幂。这个整数会变得越来越大。直到某个点,我会再次向上滚动。直到某个点,我们看到数字末尾有一个小写的 L。如果你在 Python 3 上,这是一个长整数,所以你不会再看到 L 了。但这是 Python 让我们知道这个整数现在太大,无法适应我们系统默认大小的方法。对你们大多数人来说,这将是 64 位。但在 Python 中,我们允许有任意大的整数。所以随着我们的整数变大,Python 将分配越来越多的内存来保存这些值。所以我们不必担心数字做奇怪的把戏,比如整数意外地变成负数。
在 NumPy 中,我们不受此保护。所以如果我取那个数组 a 并对它进行平方,我们看到 -1 的平方是 1,这是我期望的。0 的平方是 0。这也是我期望的。1 的平方是 1,感觉还不错。100 的平方是 16。对吧?我们从乘法表中记得 100 乘以 100 是 16。所以这里发生的是,当我创建这个数组时,我告诉 NumPy 我只想给每个整数 8 位。数字 10,000 无法在 8 位内表示。所以当我们达到该数字的最大范围时,我们从负端开始重新计数。这里就是 16。如果你以后想谷歌这个,我们刚刚目睹的技术名称叫做整数溢出。所以我们也不受此保护。
让我们尝试一些稍微不同的东西。我们一直在处理整数。但 NumPy 也支持浮点数,比如 4.5。它们不能表示为这些无限精度的整数。我们要做的是通过将我们的整数数组强制转换为浮点数数组来创建一个浮点数数组。我们可以给它起名叫 b。我们要说我希望我的数组 b 是那个数组 a。我们将使用一个名为 astype 的方法来强制转换它。a.astype。这里我们将创建一个 32 位浮点数。我将通过提供字符串字面量 float32 来做到这一点。这是我的浮点数组 b。你会注意到每个数字后面都有小点,让我知道我现在在处理浮点数。我们取这个数组 b,并对它做我们对整数做的同样的事情。我们把它们除以零。我们会看到一些更不同的东西。所以我们仍然没有得到零除错误。但这次,我们最终得到的不是一个零数组,而是一个包含两个 inf(代表无穷大)、一个负的 inf(负无穷大)以及中间这个写着 nan 的东西的数组。nan 代表“不是一个数字”。你可以把它大致理解为未定义的东西。我无法告诉你零除以零的结果是什么,所以我不会尝试。在 Python 的科学计算库世界中,你会看到这个特殊的对象 nan 经常被用来表示数据中的缺失。所以,如果我收到某个数组,其中某些值没有有效数据,这些值将被编码为 nan,即“不是一个数字”。
“不是一个数字”有一个特殊的规则。如果我们查看可以从 NumPy 命名空间 np.nan 访问的 nan 对象,并问 Python np.nan 是否等于它自己(使用双等号运算符),Python 告诉我们这不是真的。这背后的逻辑是,如果无法知道某物是什么,那么它就是未定义的。无法判断它是否与另一个东西相同。所以,与 np.nan 的相等比较总是返回 False。这带来的一个后果是,如果你习惯在能够过滤缺失数据的库中工作,选择所有数据等于缺失或 np.nan 的位置,这在 NumPy 中对你不起作用。在科学 Python 生态系统中,这对你也不起作用。实际上,你将使用的每个库都有自己内置的用于查找 nan 值的辅助函数。在 NumPy 的世界里,这叫做 isnan。所以我们可以问 Python np.nan 是 nan 吗?这确实返回 True。
所以,在 astype 的输出中,你能将 NumPy 数组的值赋给那个输出吗?那些无穷大、nan 和其他无穷大是相同类型吗?所以问题是,在第 18 行的输出中,那些无穷大和 nan 是相同类型的数据吗?答案是肯定的。在 NumPy 中,我们要求放入数组的数据是同质的。所以,无穷大(包括正负)和 nan 的编码本身是在如何使用零和一编码浮点数的规范中定义的。正因为如此,你永远不可能有一个内部包含 nan 值的整数数组,因为 nan 是专门在浮点数的标准中定义的,而不是在整数的标准中定义的。
是的,当然,好问题。所以,当我们在 Python 内部使用 NumPy 进行数值计算时,我们获得了惊人的性能提升
课程 P6:用于图像分割的全卷积网络 🧠

在本课程中,我们将学习全卷积网络在图像分割任务中的应用。我们将从基础的机器学习模型开始,逐步理解卷积神经网络的工作原理,并最终探讨如何将其改造用于像素级的密集预测任务,如图像分割。
从线性模型到多层感知机
上一节我们介绍了课程概述,本节中我们来看看图像分类的基础模型。
逻辑回归和线性回归是简单的线性模型。它们将图像(例如MNIST手写数字)的像素展平,并通过一个线性函数进行预测。其核心公式如下:
线性回归: y = Wx + b
逻辑回归(Softmax): P(class=i) = exp(z_i) / Σ_j exp(z_j)
这种模型的局限性在于无法建模输入变量之间的复杂交互关系,例如它无法表示异或(XOR)函数。
为了解决线性模型的局限性,我们引入了非线性激活函数,并将多个层堆叠起来,形成了多层感知机。以下是两种常见的非线性激活函数:
- Sigmoid:
σ(x) = 1 / (1 + exp(-x)) - 修正线性单元:
ReLU(x) = max(0, x)
MLP是一个非线性模型,能够表示非常复杂的函数,从而在MNIST等任务上获得更高的准确率。
计算机视觉的灵感:金字塔与层次特征
上一节我们了解了MLP如何引入非线性,本节中我们来看看计算机视觉中的经典思想如何启发神经网络的设计。
在计算机视觉研究中,高斯金字塔和拉普拉斯金字塔被用于多尺度图像分析。高斯金字塔通过不断下采样得到图像的不同尺度表示,用于处理不同大小的目标(如远近不一的人脸)。拉普拉斯金字塔则将图像分解为不同频率的子带,分别捕捉高频(如边缘)和低频(如整体形状)信息。
有趣的是,卷积神经网络的结构与这些金字塔模型有深刻的相似性。网络浅层的神经元激活响应类似于拉普拉斯金字塔的高频信息(捕捉边缘、纹理),而深层的神经元则响应更高级的语义特征(如物体的部件或整体)。
卷积神经网络的一个重要特性是它能分层学习特征。例如,一个用于人脸识别的网络,其第一层权重可能学习到边缘或斑点,中间层学习到眼睛、耳朵等部件,更深层则学习到整个面部的结构。
卷积神经网络算术

理解了CNN的灵感来源后,我们需要掌握其核心运算机制。
卷积操作有两个关键参数:填充和步长。
- 填充:在输入特征图边缘添加零值,以控制输出特征图的大小。
- 步长:卷积核在输入上移动的间隔,决定了下采样的程度。
通过堆叠卷积层和池化层,网络能够逐步提取特征并降低空间分辨率,实现从细节到语义的抽象。
密集预测任务
现在我们已经掌握了CNN的基础,本节中我们来看看本课程的核心目标——密集预测任务。
密集预测任务要求为图像的每个像素生成一个输出。主要包括:
- 语义分割:为每个像素分配一个类别标签(如人、马、背景)。
- 深度估计:从单张图像预测每个像素的深度值。
- 边缘检测:识别图像中物体的边界。
这些任务在计算机视觉中至关重要,并可作为更高级算法(如自动驾驶、医学图像分析)的组成部分。
从分类网络到全卷积网络
传统的CNN用于图像分类时,末端通常包含全连接层,最终输出一个固定维度的向量(如1000类的概率)。然而,全连接层要求输入特征图的尺寸是固定的,这限制了网络处理任意尺寸图像的能力。
为了将CNN应用于分割任务,我们需要进行一个关键改造:将全连接层转换为卷积层。具体做法是将全连接层的权重矩阵重塑为卷积核。例如,一个4096x7x7的全连接层可以看作是一个具有4096个7x7卷积核的卷积层。
经过这种转换后,网络就变成了全卷积网络。它可以接受任意尺寸的输入图像,并输出一个二维的特征图(而非一维向量)。在分类网络中,这个输出特征图的空间尺寸会因池化而下采样(例如缩小32倍)。
上采样与跳跃连接
上一节我们得到了一个全卷积网络,但其输出是下采样的粗糙预测图。本节中我们来看看如何恢复精细的预测。
为了得到与输入图像尺寸一致的分割图,我们需要对网络末端的特征图进行上采样。最直接的方法是使用双线性插值,并且可以将其设计为可微分的层,以便进行端到端训练。
然而,仅通过上采样得到的预测通常非常粗糙,丢失了大量细节。这是因为网络深层特征虽然语义信息丰富,但空间细节不足。
为了解决这个问题,我们引入了跳跃连接。其思想是将网络浅层(包含丰富的边缘、纹理等细节信息)的特征图,与深层(包含高级语义信息)的上采样特征图进行融合。这样,网络就能利用浅层的精确定位信息来“修饰”深层的粗糙语义预测,从而生成边界清晰、准确的分割结果。
训练时,我们使用逐像素交叉熵损失,即对输出特征图的每个位置计算分类损失,然后求和。这与图像分类中使用的损失函数本质相同,只是应用到了每个像素上。
改进、应用与总结
我们已构建了一个基本的全卷积分割网络。本节中我们快速浏览该领域的一些重要改进和实际应用。
该领域后续的重要改进包括:
- 空洞卷积:在不增加参数或降低分辨率的情况下,扩大卷积核的感受野,有助于捕捉更广泛的上下文信息。
- 条件随机场:作为后处理步骤,用于优化网络输出的分割边界,使其更加平滑和准确。
- Mask R-CNN:将目标检测与实例分割耦合,先检测物体,再为每个检测框预测精细的掩码。
这些技术已被成功应用于诸多领域,例如医学图像分析(如手术器械分割)、手机摄影的“人像模式”(背景虚化)和趣味性的图像编辑(如更换背景)。
总结:在本课程中,我们一起学习了全卷积网络用于图像分割的原理。我们从基础的线性模型和MLP出发,理解了CNN从计算机视觉金字塔模型中获得的灵感。通过将分类网络中的全连接层卷积化,我们得到了可以处理任意尺寸图像的全卷积网络。为了获得精细的分割结果,我们引入了上采样和跳跃连接来融合深层语义与浅层细节。最后,我们看到了该技术在持续改进并广泛应用于科研和工业场景中。
课程 P7:Numba - 告别C++ 📚
在本课程中,我们将学习如何使用Numba来加速Python代码,特别是数学密集型运算。我们将从性能分析开始,逐步探索Numba的JIT编译、不同编译模式、向量化操作以及并行计算。
概述
Numba是一个针对Python的即时(JIT)编译器。它利用LLVM将Python代码编译为高度优化的机器码,并与科学计算栈(如NumPy)良好集成。Numba主要针对数学运算进行优化,可以显著提升代码性能,而无需重写为C++或Fortran。


性能分析 🔍
在优化代码之前,首先需要识别性能瓶颈。性能分析帮助我们了解代码中哪些部分运行缓慢,从而有针对性地进行优化。
以下是三种常用的性能分析工具:
- cProfile:Python内置的性能分析器,提供函数级别的调用次数和时间统计。
- line_profiler:行级性能分析器,显示函数中每一行代码的执行时间。
- timeit:用于测量小段代码执行时间的工具,适合快速基准测试。
使用这些工具可以准确找到代码中的热点,避免在不重要的部分浪费时间。
Numba JIT 编译器 ⚡
上一节我们介绍了性能分析,本节中我们来看看如何使用Numba的JIT编译器来加速代码。
Numba的核心功能是通过@jit装饰器将Python函数编译为机器码。编译过程在函数首次运行时自动进行。
基本用法
以下是一个计算数组元素和的简单函数:
def array_sum(arr):
total = 0.0
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
total += arr[i, j]
return total
使用@jit装饰器加速:
from numba import jit
@jit
def array_sum_numba(arr):
total = 0.0
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
total += arr[i, j]
return total
性能对比
使用timeit比较加速效果:
import numpy as np
arr = np.random.rand(300, 300)
%timeit array_sum(arr) # 原始版本
%timeit array_sum_numba(arr) # Numba加速版本
Numba版本通常可以获得数十倍甚至上百倍的性能提升。
编译模式:对象模式 vs. nopython模式 🛠️
Numba有两种编译模式:对象模式(object mode)和nopython模式。nopython模式能提供最佳性能,但要求代码完全由Numba支持的操作组成。
对象模式
当代码包含Numba无法优化的操作(如字符串拼接)时,会自动回退到对象模式。对象模式速度较慢,因为它需要依赖Python对象系统。
nopython模式
使用@jit(nopython=True)或@njit装饰器强制启用nopython模式。如果代码中有不支持的操作,Numba会抛出错误。
from numba import njit
@njit
def add_numbers(a, b):
return a + b # 支持数值类型
# @njit
# def add_strings(a, b):
# return a + b # 字符串操作会引发错误
类型推断
Numba在编译时会推断输入和输出的数据类型。使用inspect_types()方法可以查看编译后的类型信息。
add_numbers.inspect_types()
实战应用:N体问题模拟 🌌
现在我们将Numba应用于一个更实际的场景:模拟N体问题中的直接求和计算。这是一个计算密集型问题,涉及所有粒子对之间的相互作用。
原始实现
使用Python类和列表实现:
class Particle:
def __init__(self, x, y, z, mass):
self.x = x
self.y = y
self.z = z
self.mass = mass
self.potential = 0.0
def direct_sum(particles):
for i, target in enumerate(particles):
for j, source in enumerate(particles):
if i != j:
dist = ((target.x - source.x)**2 +
(target.y - source.y)**2 +
(target.z - source.z)**2)**0.5
target.potential += source.mass / dist
使用Numba优化
由于Numba不能直接优化类方法,我们需要将数据结构转换为NumPy数组,并使用自定义dtype来保持代码可读性。
import numpy as np
from numba import njit
particle_dtype = np.dtype([
('x', np.float64),
('y', np.float64),
('z', np.float64),
('mass', np.float64),
('potential', np.float64)
])
@njit
def distance(p1, p2):
return ((p1['x'] - p2['x'])**2 +
(p1['y'] - p2['y'])**2 +
(p1['z'] - p2['z'])**2)**0.5
@njit
def direct_sum_numba(particles):
n = len(particles)
for i in range(n):
for j in range(n):
if i != j:
dist = distance(particles[i], particles[j])
particles[i]['potential'] += particles[j]['mass'] / dist
通过这种优化,通常可以获得数十倍的性能提升。
高级主题:向量化与并行计算 🚀
Numba的vectorize装饰器可以将标量函数转换为处理数组的通用函数(ufunc),并支持自动并行化。
创建向量化函数
from numba import vectorize
import math
@vectorize(['float64(float64, float64)'])
def trig_func(a, b):
return math.sin(a)**2 * math.exp(b)
# 现在可以处理数组
result = trig_func(np_array_a, np_array_b)
并行执行
通过设置target='parallel',Numba会自动并行化向量化函数的执行。
@vectorize(['float64(float64, float64)'], target='parallel')
def trig_func_parallel(a, b):
return math.sin(a)**2 * math.exp(b)
对于循环结构,也可以使用@jit(parallel=True)尝试自动并行化。
@jit(parallel=True)
def parallel_sum(arr):
total = 0.0
for i in range(arr.shape[0]):
for j in range(arr.shape[1]):
total += arr[i, j]
return total

总结
在本课程中,我们一起学习了:
- 性能分析的重要性:使用cProfile、line_profiler和timeit识别代码热点。
- Numba JIT编译:通过
@jit装饰器显著加速数学运算。 - 编译模式:理解对象模式与nopython模式的区别,并强制使用nopython模式以获得最佳性能。
- 实战优化:将面向对象的代码重构为使用NumPy数组和Numba兼容的循环,以解决N体问题。
- 向量化与并行:使用
vectorize创建通用函数,并利用并行目标进一步提升大规模数据处理的性能。
Numba是一个强大的工具,它让Python开发者能够在保持开发效率的同时,获得接近原生代码的性能。通过本课程介绍的技术,你可以有效地优化计算密集型任务,无需依赖C++或Fortran。
课程 P8:使用交互式 Jupyter 仪表板可视化数亿数据点 📊
在本节课中,我们将学习如何使用一系列 Python 库,通过大约 30 行代码,构建一个能够交互式可视化数亿乃至十亿数据点的仪表板。我们将从加载数据开始,逐步完成数据可视化、参数声明、控件绑定,最终部署为一个独立的应用程序。
第一步:获取数据 📥
我们首先需要获取数据。本节课将使用纽约出租车数据集的一个子集,其中包含约 1200 万个数据点。数据以 Parquet 格式存储,我们将使用 Dask 库将其加载到内存中。
以下是加载数据的代码:
import dask.dataframe as dd
# 从磁盘加载数据
df = dd.read_parquet('nyc_taxi_data.parquet')
# 将数据持久化到内存
df = df.persist()
加载过程大约需要两秒钟。数据包含五列:乘客数量、上车地点的 X 和 Y 坐标,以及下车地点的 X 和 Y 坐标。
第二步:在笔记本中构建原型图 📈
现在数据已加载到内存中,我们开始进行探索性可视化。为此,我们将使用 HoloViews 库。HoloViews 提供了一套简单、声明式的方法来为数据添加可视化注释,并包含一个庞大的元素库,每个元素都有其对应的视觉表现形式。
然而,我们的数据量(1200 万个点)太大,无法直接发送到浏览器进行渲染。因此,我们需要先使用 Datashader 库对数据进行栅格化处理。
以下是可视化数据的代码:
import holoviews as hv
from holoviews.operation.datashader import datashade
hv.extension('bokeh')
# 将数据包装为点元素
points = hv.Points(df, ['pickup_x', 'pickup_y'], 'passenger_count')
# 设置绘图选项
plot_opts = dict(width=600, height=400, bgcolor='black', show_grid=False)
# 应用 Datashader 进行栅格化,并指定色彩映射
shaded = datashade(points, cmap='viridis').opts(**plot_opts)
shaded
执行这段代码后,我们在一秒内就能得到一个交互式图表。缩放或平移时,图表会根据新的坐标轴范围重新进行栅格化渲染。
第三步:将数据置于地理背景中 🗺️
为了更直观地理解数据的地理位置,我们可以将可视化结果叠加在地图底图上。HoloViews 的地理扩展库 GeoViews 可以方便地集成在线地图瓦片服务。


以下是添加地图背景的代码:
import geoviews as gv
gv.extension('bokeh')
# 声明一个地图瓦片源
tiles = gv.tile_sources.OSM()
# 将出租车数据点叠加在地图上
overlay = tiles * shaded
overlay
现在,我们的数据点就显示在真实的网络地图上了,并且所有交互功能(如缩放、平移)依然保持流畅。
第四步:声明控制可视化的参数 ⚙️
为了能动态定制可视化效果,我们需要声明一些参数。这里使用 param 库,它可以让我们清晰地表达参数的意图、类型、取值范围和默认值等。
我们创建一个名为 NYCTaxiExplorer 的类,并为其添加一些参数:
import param
class NYCTaxiExplorer(param.Parameterized):
# 透明度参数,范围 0 到 1
alpha = param.Number(default=0.5, bounds=(0, 1))
# 选择可视化上车点还是下车点
column = param.ObjectSelector(default='pickup', objects=['pickup', 'dropoff'])
# 选择色彩映射
cmap = param.ObjectSelector(default='viridis', objects=['viridis', 'plasma', 'inferno'])
# 选择乘客数量范围
passengers = param.Range(default=(1, 4), bounds=(0, 10))
创建这个类后,我们就可以像访问普通属性一样访问这些参数。param 库会自动进行类型和范围校验。
第五步:将参数链接到可视化 🔗
接下来,我们需要将上一步声明的参数与我们的可视化图表连接起来。我们在 NYCTaxiExplorer 类中添加一个方法,根据参数值动态生成视图。
以下是链接参数与可视化的方法:
class NYCTaxiExplorer(param.Parameterized):
# ... 参数定义同上 ...
def make_view(self, x_range=None, y_range=None):
# 根据选择的列(上车/下车)筛选数据点
current_points = hv.Points(self.df, [f'{self.column}_x', f'{self.column}_y'])
# 根据乘客数量范围筛选
filtered_points = current_points.select(passenger_count=self.passengers)
# 应用 Datashader,并设置当前参数(如透明度、色彩映射)
shaded = datashade(filtered_points, cmap=self.cmap, alpha=self.alpha)
# 叠加地图瓦片
return gv.tile_sources.OSM() * shaded
现在,每当我们更改参数并调用 make_view 方法时,返回的可视化结果都会反映这些更改。
第六步:添加控件以交互控制图表 🎛️
我们希望用户界面上的控件(如滑块、下拉菜单)能够实时控制图表。param 库有对应的 UI 扩展,可以自动将参数转换为控件。
首先,我们可以使用 paramnb 库(基于 IPyWidgets)在 Jupyter 笔记本中生成控件:
import paramnb
# 创建类实例
explorer = NYCTaxiExplorer()
# 生成控件
widgets = paramnb.Widgets(explorer)
widgets
然而,Jupyter Dashboard Server 已不再维护。因此,我们更推荐使用基于 Bokeh 的 panel 库(原名 param-bokeh),它能够生成 Bokeh 控件并支持部署为独立应用。
使用 panel 生成控件的代码类似:
import panel as pn
pn.extension()
# 创建控件
widgets = pn.widgets.WidgetBox.from_param(explorer.param)
widgets
这些控件与我们的参数对象是双向绑定的,移动滑块或选择下拉选项会立即更新参数值。
第七步:创建动态地图并部署为仪表板 🚀
最后一步是将控件和动态更新的图表结合起来。HoloViews 的 DynamicMap 对象可以接受一个回调函数(即我们的 make_view 方法)和一组“流”(streams)。当流的值(例如参数值或视图范围)发生变化时,它会自动调用回调函数来更新图表。
以下是创建动态仪表板的代码:

from holoviews.streams import Params, RangeXY

# 将参数和视图范围定义为流
param_stream = Params(explorer.param, ['alpha', 'column', 'cmap', 'passengers'])
range_stream = RangeXY(source=overlay) # 捕获图表的可视范围
# 创建动态地图
dmap = hv.DynamicMap(explorer.make_view, streams=[param_stream, range_stream])
# 使用 panel 进行布局:左侧放控件,右侧放动态地图
dashboard = pn.Row(widgets, dmap)
dashboard

现在,一个完整的交互式仪表板就构建完成了。更改任何控件,图表都会实时更新。缩放图表时,RangeXY 流会捕获新的视图范围并触发重绘。

为了部署为独立的 Bokeh 应用,我们可以将上述代码保存到一个脚本文件中,并使用 panel 的服务器模式:
# 在脚本中,例如 app.py
import panel as pn
pn.extension()
# ... 重复上述仪表板构建代码 ...
# 将仪表板转换为可服务的 Bokeh 文档
app = dashboard.servable()
然后可以通过命令 panel serve app.py 来启动一个独立的 Web 服务器。
扩展与未来工作 🔮
上一节我们完成了基础仪表板的部署,本节我们来看看可以如何扩展以及未来的发展方向。
扩展可视化类型:HoloViews 支持多种图表元素(如曲线、散点图、热图),它们都能与 Datashader 和 Bokeh 后端协同工作。
更多参数与控件类型:param 库支持丰富的参数类型(如文本、字典、布尔开关、多选下拉框),panel 库能据此生成对应的复杂控件。

处理更大规模数据:对于超过单机处理能力的数据(例如本节课后面演示的包含 10 亿个 OpenStreetMap GPS 点的数据集),可以结合 Dask.distributed 将计算分布到集群上。
未来开发方向:团队正在积极开发 panel 库,以提供更灵活的布局选项,并致力于完善基于 Bokeh 服务器的部署体验,以期未来能替代旧的 Jupyter Dashboard Server 的拖放布局功能。
总结 📝
在本节课中,我们一起学习了构建高性能交互式数据仪表板的完整流程:
- 使用 Dask 高效加载大规模数据。
- 利用 HoloViews 进行声明式数据标注和构图。
- 通过 Datashader 对海量数据进行实时栅格化渲染。
- 使用 GeoViews 添加地理上下文背景。
- 通过 param 库声明和管理控制参数。
- 借助 panel 库将参数自动转换为 UI 控件,并构建交互逻辑。
- 最终将所有组件集成为一个可通过 Bokeh 服务器 部署的独立应用。

这套工具链使得用少量代码探索和展示超大规模数据集成为可能,并保持了良好的交互性能。
📊 Matplotlib 入门教程 P9:Matplotlib 剖析
在本节课中,我们将学习 Matplotlib 库的基础知识。Matplotlib 是一个用于创建高质量图形的 Python 库,广泛应用于科学计算和数据可视化领域。我们将从基本概念开始,逐步深入到更高级的功能。
🏛️ 历史背景与设计哲学
Matplotlib 始于 2001 年,由 John Hunter 创建。他当时对 Matlab 感到不满,于是决定自己编写一个库。Matplotlib 的设计初衷是模仿 Matlab 的 API,以便科学家们能够轻松上手。它能够生成像素完美的图像,适用于学术出版物。
Matplotlib 的成功部分归功于其跨平台兼容性。它支持 Windows、Linux 和 Mac 操作系统,并且可以与多种 GUI 工具包(如 WX、GTK、TK、QT)协同工作。文本渲染也是 Matplotlib 的一个重要特性,确保图形在出版物中看起来完美无缺。
然而,Matplotlib 也携带了一些历史包袱,这意味着它有一些独特的特性。一旦你理解了这些特性,它们就会变得合情合理。本教程旨在帮助你理解这些特性。
📚 学习资源
Matplotlib 提供了丰富的文档资源,包括常见问题解答、示例和直接的 API 文档。其中,图库(Gallery)是最重要的资源之一。图库中包含了许多预制的示例,涵盖了 Matplotlib 的各种功能。你可以浏览这些图像,找到你需要的功能,然后查看生成该图像的源代码。


此外,你还可以通过邮件列表、Stack Overflow 和 Gitter 等渠道获取帮助。Matplotlib 社区非常友好,欢迎任何问题。如果你有改进 Matplotlib 的想法,可以提交 Matplotlib 增强提案(MEP)。



🔧 后端与配置


Matplotlib 使用不同的后端来适应不同的平台。后端是 Matplotlib 与不同 GUI 工具包交互的组件。大多数情况下,你不需要关心后端,但在报告问题时,可能需要提供后端信息。



你可以通过以下代码查看 Matplotlib 的版本和当前使用的后端:


import matplotlib
print(matplotlib.__version__)
print(matplotlib.get_backend())
在 Jupyter Notebook 中,你可以使用 %matplotlib notebook 来启用交互式绘图。本教程将使用 nbagg 后端,以模拟编写 Python 脚本时的行为。


🖼️ 图形结构:Figure 与 Axes
在 Matplotlib 中,图形的基本结构包括 Figure(图形窗口)和 Axes(坐标轴)。Figure 是顶层的容器,而 Axes 是实际绘制图形的区域。



以下是创建 Figure 和 Axes 的基本步骤:


import matplotlib.pyplot as plt

# 创建 Figure
fig = plt.figure()
# 创建 Axes
ax = fig.add_subplot(111)


# 显示图形
plt.show()

Figure 可以包含多个 Axes,每个 Axes 可以独立绘制图形。你可以通过 add_subplot 方法添加多个子图。
📈 基本绘图:plot 与 scatter
Matplotlib 提供了多种绘图函数,其中最常用的是 plot 和 scatter。plot 用于绘制线图,而 scatter 用于绘制散点图。
以下是使用 plot 和 scatter 的示例:
import numpy as np
import matplotlib.pyplot as plt
# 创建数据
x = np.linspace(0, 10, 100)
y = np.sin(x)

# 创建 Figure 和 Axes
fig, ax = plt.subplots()
# 绘制线图
ax.plot(x, y, color='blue', linewidth=2)
# 绘制散点图
ax.scatter(x, y, color='red', marker='o')






# 设置坐标轴范围
ax.set_xlim(0, 10)
ax.set_ylim(-1, 1)






# 显示图形
plt.show()




需要注意的是,scatter 函数的主要用途是根据数据点的大小或颜色进行可视化,而不仅仅是绘制不带线的标记。如果你只需要绘制不带线的标记,可以使用 plot 函数并指定 linestyle='none'。




🧩 多子图布局





Matplotlib 支持在单个 Figure 中创建多个子图。你可以使用 subplots 函数轻松创建网格布局的子图。

以下是创建多个子图的示例:

import numpy as np
import matplotlib.pyplot as plt




# 创建 2x2 的子图网格
fig, axes = plt.subplots(2, 2)
# 在每个子图中绘制不同的图形
axes[0, 0].plot(np.random.rand(10))
axes[0, 0].set_title('Subplot 1')
axes[0, 1].scatter(np.random.rand(10), np.random.rand(10))
axes[0, 1].set_title('Subplot 2')
axes[1, 0].bar(range(5), np.random.rand(5))
axes[1, 0].set_title('Subplot 3')



axes[1, 1].hist(np.random.randn(100), bins=20)
axes[1, 1].set_title('Subplot 4')

# 调整布局
plt.tight_layout()
plt.show()
通过 subplots 函数,你可以轻松创建复杂的图形布局。



🎨 颜色、标记和线型


在 Matplotlib 中,你可以通过多种方式指定颜色、标记和线型。颜色可以通过名称、十六进制值或 RGB 元组来指定。标记和线型也有多种预定义选项。

以下是颜色、标记和线型的示例:
import matplotlib.pyplot as plt


# 创建数据
x = [1, 2, 3, 4, 5]
y = [1, 4, 9, 16, 25]


# 绘制带有不同颜色、标记和线型的图形
plt.plot(x, y, color='red', marker='o', linestyle='--', linewidth=2, label='Data')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.title('Sample Plot')
plt.legend()
plt.show()
你还可以使用简写形式来指定颜色、标记和线型。例如,'ro--' 表示红色、圆形标记和虚线。
🌈 色彩映射







色彩映射(Colormap)用于将数据值映射到颜色。Matplotlib 提供了多种预定义的色彩映射,包括顺序色彩映射、发散色彩映射和分类色彩映射。



以下是使用色彩映射的示例:


import numpy as np
import matplotlib.pyplot as plt
# 创建数据
data = np.random.rand(10, 10)




# 绘制热图
plt.imshow(data, cmap='viridis')
plt.colorbar()
plt.show()



在选择色彩映射时,应注意其适用性。例如,jet 色彩映射可能导致误导性的可视化结果,而 viridis 色彩映射在感知上更加均匀。


📝 数学文本与 LaTeX




Matplotlib 支持使用 LaTeX 语法渲染数学文本。你可以通过 rcParams 配置 LaTeX 支持,并在文本中使用数学表达式。

以下是使用数学文本的示例:
import matplotlib.pyplot as plt
# 启用 LaTeX 渲染
plt.rcParams['text.usetex'] = True



# 绘制图形并添加数学文本
plt.plot([1, 2, 3], [1, 4, 9])
plt.xlabel(r'$x$')
plt.ylabel(r'$y = x^2$')
plt.title(r'$\sigma_i = 15$')
plt.show()
如果未安装 LaTeX,Matplotlib 会使用内置的数学文本渲染器。


🧱 填充与阴影


Matplotlib 提供了填充和阴影功能,用于增强图形的可视化效果。你可以使用 fill_between 函数填充区域,或使用阴影样式区分不同的数据系列。
以下是填充和阴影的示例:


import numpy as np
import matplotlib.pyplot as plt
# 创建数据
x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
# 填充区域
plt.fill_between(x, y1, y2, color='gray', alpha=0.5, label='Fill')
plt.plot(x, y1, label='Sin')
plt.plot(x, y2, label='Cos')
plt.legend()
plt.show()








🔄 属性循环

属性循环允许你在绘制多个图形时自动循环不同的属性(如颜色、线型、标记)。你可以通过 cycler 对象自定义属性循环。
以下是属性循环的示例:
import matplotlib.pyplot as plt
from cycler import cycler



# 自定义属性循环
custom_cycler = cycler(color=['r', 'g', 'b']) * cycler(linestyle=['-', '--', ':'])
plt.rcParams['axes.prop_cycle'] = custom_cycler





# 绘制多个图形
for i in range(5):
plt.plot([i, i+1, i+2], label=f'Line {i+1}')

plt.legend()
plt.show()
🎭 艺术家对象
在 Matplotlib 中,所有可见的元素都是艺术家对象。艺术家对象分为容器类型(如 Figure、Axes)和原始类型(如 Line2D、Rectangle)。你可以直接操作艺术家对象来自定义图形。
以下是操作艺术家对象的示例:
import matplotlib.pyplot as plt





# 创建图形
fig, ax = plt.subplots()
line, = ax.plot([1, 2, 3], [1, 4, 9])



# 修改线条属性
line.set_color('red')
line.set_linewidth(3)
line.set_linestyle('--')



plt.show()




🗺️ 扩展工具包


Matplotlib 提供了一些扩展工具包,用于处理特定的可视化需求。例如:
- Basemap:用于绘制地图(已弃用,推荐使用 Cartopy)。
- mplot3d:用于绘制 3D 图形。
- AxesGrid1:用于创建复杂的图形布局。




以下是使用 mplot3d 绘制 3D 图形的示例:




import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
# 创建 3D 图形
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 生成数据
x = np.linspace(-5, 5, 100)
y = np.linspace(-5, 5, 100)
x, y = np.meshgrid(x, y)
z = np.sin(np.sqrt(x**2 + y**2))
# 绘制曲面
ax.plot_surface(x, y, z, cmap='viridis')
plt.show()


📦 总结


在本节课中,我们一起学习了 Matplotlib 的基础知识。我们从 Matplotlib 的历史背景和设计哲学开始,逐步介绍了图形结构、基本绘图函数、多子图布局、颜色与样式、色彩映射、数学文本、填充与阴影、属性循环、艺术家对象以及扩展工具包。通过本教程,你应该能够使用 Matplotlib 创建高质量的科学图形,并进一步探索其高级功能。



希望本教程对你有所帮助,祝你在数据可视化的道路上越走越远!


浙公网安备 33010602011771号