UCD-ECS032a-Python-数据分析笔记-全-

UCD ECS032a Python 数据分析笔记(全)

001:Python入门与基础语法

概述

在本节课中,我们将学习Python编程语言的基础知识。课程目标是让你能够在Python控制台和Jupyter Lab中运行代码,创建变量,调用函数,以及保存和运行代码文件。我们还将学习如何与文件系统交互,并最终能够执行文件中的代码。

为什么选择Python?

上一节我们介绍了课程目标,本节中我们来看看为什么在众多编程语言中选择Python。

首先,Python代码具有可复现性。如果你将数据文件和代码分享给他人,他们理论上可以运行完全相同的代码,复现你的所有结果。这在协作环境和共享项目中至关重要。

其次,可复现的代码能够自动化分析中的重复任务。例如,在处理大量原始序列文件时,使用Python可以避免手动重复操作。

另一个好处是可以在不同分析之间复用代码。如果你为一个项目编写了代码,在另一个类似项目中可以轻松复制粘贴使用。

此外,你可以将你认为有用的代码打包,并在更广泛的范围内分发。例如,我们今天使用的Jupyter Lab就是由他人编写并发布的。

Python是一种通用语言,适用于化学、遗传学、人类学、社会学等多个学科领域。

它之所以如此流行,是因为其语法比其他一些语言更易于阅读,几乎就像用简单的英语编写代码一样直观。

Python是交互式的,有许多在线调试器可以逐行描述你的操作。由于它在众多领域中被广泛使用,有大量可供参考和使用的直接、明确的代码。

因为使用广泛,当你运行Python时,有大量的函数和包可以使用。虽然R语言也很有用,但Python确实是一种非常强大的语言,如果你选择深入学习Python,你的数据分析体验将会很愉快。

Python数据科学包简介

上一节我们讨论了选择Python的原因,本节中我们将介绍一些用于数据科学的核心包。

Python有大量可用的包。仅PyPI上就发布了超过60万个包。对于数据科学,有许多工具可供选择,具体取决于你的工作内容。

以下是一些对数据分析有帮助的包:

  • NumPy: 提供N维数组(如向量和矩阵)和一系列数学函数,帮助你以数据框的形式处理数据。它是使用最广泛的包之一。
  • Pandas: 用于处理数据框,可以读取CSV文件,帮助你很好地操作数据。它可以将数据集(如字典)转换为CSV格式保存。但Pandas在处理大型数据框时 notoriously 内存效率不高。
  • Polars: 与Pandas非常相似,但效率更高,可以快速处理非常大的数据集。我们今天将主要使用Polars。
  • DuckDBIbis: 它们在底层使用SQL(结构化查询语言),如果你处理类似数据库的数据(而不仅仅是CSV文件)会很有帮助。

除了数据处理包,我们还将使用以下包:

  • Jupyter: 提供交互式运行的笔记本风格界面,可以混合代码和文本。
  • SciPy: 提供许多数学函数。
  • Matplotlib: 创建图形、绘图和可视化的常用包。
  • Statsmodelsscikit-learn: 用于机器学习和统计的包。

请注意,这并非详尽列表,只是为了让你意识到,无论你做什么,很可能都有一个对应的包。

设置与Jupyter Lab界面

上一节我们介绍了核心的数据科学包,本节中我们来看看如何设置环境并启动Jupyter Lab。

首先,请确保你已经按照指引运行了Pixi初始化,并安装了Jupyter、NumPy和Polars。

有多种方式可以运行Python代码:可以在控制台运行,可以在终端运行脚本,也可以在Jupyter中运行。Jupyter Lab界面非常友好,因为它提供了一个集成开发环境(IDE),这有助于使你的代码更易于理解。你可以编写代码、添加文本部分、插入图片,将代码和你想要运行的内容很好地结合在一起。

接下来,我们将打开Jupyter Lab。在命令行(Windows用户使用Git Bash)中运行以下命令:

pixi run jupyter lab

运行后,应该会弹出一个链接。将该URL复制并粘贴到浏览器中,即可打开Jupyter Lab界面。

在界面中,你应该能看到一个部分,在“Console”下有一个写着“Python 3”的选项。点击它,会为你打开一个Python控制台。

Python控制台基础操作

上一节我们打开了Jupyter Lab,本节中我们将在Python控制台中开始实际操作。

在控制台底部,你会看到一组方括号,这称为提示符。在Jupyter中,提示符是一对方括号,它会根据括号内的数字告诉你之前是否运行过代码。如果括号是空的,则表示尚未运行任何代码。

现在,让我们输入 2 + 2,然后按住 Shift 键并按下 Enter 键。你应该会看到输出结果。这表明它运行了你给出的代码。

再试试 3 - 1,然后按 Shift + Enter

现在,你应该看到提示符括号的颜色发生了变化。一种颜色显示你输入的内容,另一种颜色显示返回的结果。一种代表你提示的代码,另一种是你运行代码的返回值。这就是你与Python提示符交互、输入Python代码的方式。

正如你所注意到的,我们使用了加号和减号。Python的好处在于它很直观。很多操作符都和我们想的一样:加法用加号,减法用减号。但也有一些特殊的,比如乘法用星号 *,除法用正斜杠 /。Python已经内置了许多基本的数学函数。如果你想求幂,可以使用两个星号 **。例如,2 ** 2 会计算2的2次方。

这些都是与数字交互的方式,在你开始处理和数据操作时非常重要。

我们还可以赋值并进行更复杂的表达式运算。例如,假设我们要计算三角形的面积。公式是 1/2 * 底 * 高。假设底为3,高为4。你可以输入 1/2 * 3 * 4,它会给出输出结果,告诉我们这个三角形的面积是6。

Python的优点是你可以用多种方式编写计算过程,其中有些方式更符合惯例。例如,计算 1/2 * 3 * 4,你也可以写成 3 * 4 / 2

你还记得运算顺序吗?是的,这里使用PEMDAS(括号、指数、乘、除、加、减),和你做代数时完全一样。如果你想用括号明确指定运算顺序,它会先计算括号内的内容。例如,如果我们改变括号的位置,可能会得到不同的结果。因此,始终要确保你计算的是你期望的结果。你可以通过尝试一些数字来验证。

正如我提到的,人们在运行Python代码时有一些惯例,其中之一就是使用空格,这样代码看起来更清晰。比较一下有空格和没空格的代码,有空格的那个更容易阅读。当数字、字母和符号混杂在一起时,很快就会变得难以理解。因此,在元素之间添加空格非常有帮助。

你应该像写文本一样编写代码,把它组织得整洁美观。好的代码就是整洁的代码。

变量与命名

上一节我们进行了基础运算,本节中我们来看看如何存储和重用计算结果。

如果我们想存储某些内容,比如我们如何处理这些数字?我们可以直接输入它们,但它们只存在于那个实例中。

在Python中,我们有称为变量的命名值。这很像数学中的变量,可以代表你分配给它的任何东西,包括整个数据框、列表、字典、单词、数字等。我们可以将任何我们想要的东西赋值给一个变量。

但变量的命名需要遵循一些小的规则。

一般来说,变量名可以由字母、下划线和数字的任意组合构成,但不能以数字开头。例如,geese_top_50dogs_and_nine(nine拼写出来)都是完全没问题的变量名。像 goose space teeth 这样的名字会被解释为两个不同的项,所以变量名中不要有空格。tropical.dot.fish 在Python中,点号通常表示调用函数或方法,所以也不要这样用。9lives 会让Python认为你在谈论数字,所以以数字开头会让它很困惑。通常,你应该使用描述性的单词或单词组合,如果想分隔单词,惯例是使用下划线。

例如,我可以把三角形的面积赋值给一个变量。我可以称它为 areaarea_of_triangle,或者任何我想要的名称,但为了简单起见,我称它为 area。我将这个三角形的面积赋值给变量 area

运行 area = 3 * 4 / 2。注意,现在没有输出。为什么现在没有输出?因为没有输出是因为我们只告诉Python将一个变量设置为某个值。我们没有告诉它对变量做任何其他事情,除了赋值。变量的赋值是通过等号 = 进行的。惯例通常是先写变量名,然后是等号,再是你想赋给它的值。

一般来说,如果你使用变量,建议遵循风格指南,使用小写字母。当你使用大量大写字母时,在Python中通常代表其他东西,比如全局变量或常量(不应该改变的值)。所以,如果一个变量在任何时候都可能改变,你应该使用小写字母。这不是特别严格的规定,但这是Python通用风格指南的建议。

在上面,我们分配了变量,area 等于 3 * 4 / 2。但现在,当我们直接输入变量名时,它会打印出该值,因为我们将该值存储在了变量中。继续打印出变量,你应该会看到它输出了我们赋给它的方程式的计算结果。

那么,使用变量的目的是什么?为什么我们需要它们,而不是在需要计算时直接进行计算?

原因之一是它使我们的代码更具可重用性。通过采用变量并在此命名,如果我只是写 area = 3 * 4 / 2 而没有解释,你不知道我在指什么。但是,如果我分配变量,比如 base = 3height = 4area = base * height / 2,这使我们的代码更具描述性,也意味着我们可以更轻松地更改代码中的变量,这非常有帮助。例如,如果我将代码分享给某人,而他们的文件名与我的略有不同,如果我将该文件名存储在顶部的变量中并只使用该变量,他们只需要更改一行。否则,他们必须遍历脚本并更改每一个出现的地方。因此,变量对于组织良好、可重用的代码非常有用。

这里我们将 base 赋值为3,height 赋值为4,area 等于这个方程 base * height / 2。注意,这些颜色也略有不同,base 是黑色的,2 是绿色的,这是因为它们是不同的数据类型。Python知道当你说 base 时,你实际上指的是这个变量,而当你说 2 时,你只是在使用一个数字。

这就是为什么拥有描述性代码非常好。这也是为什么在Jupyter中运行代码比仅在控制台中运行更好的原因。这称为语法高亮,它根据代码的不同部分进行着色。

现在我们已经分配了变量,我们可以运行 area,看到它为我们计算结果。

假设我们想把高度改为10。我们不需要重新输入整个表达式,只需更改这一个变量。我们可以重新运行我们的小段代码,现在它重新计算了面积。这是一个使代码可重用的好例子。

一般来说,当你分配变量时,你希望名称具有描述性。我几乎可以把任何变量命名为任何我想要的东西。我可以把它命名为 asdf。但是,如果我把我的代码分享给你,你看了可能会想,这是什么?因此,拥有描述性的变量名非常有帮助,因为它使查看他人的代码更容易,而且根据经验,当你未来的自己查看你的代码时,也会更容易理解。我曾经有过看着旧代码想,我到底在做什么的时候。所以,拥有描述性的代码非常有帮助。因此,在编码时,请始终记住,你自己和其他人将来可能会查看它。

数据类型:字符串与布尔值

上一节我们介绍了变量,本节中我们来看看变量可以存储哪些类型的数据。

Python有许多不同的数据类型,其中最常用的一种,尤其是在数据框中,称为字符串。

字符串是任何一段字面文本。任何在单引号或双引号内的内容都被视为字符串。例如,输入 'hi'"hi",无论是单引号还是双引号,都应该可以工作,但你不能混合使用两者。例如,'hi" 会让它有点困惑。它会知道这是一个字符串,但会提示字符串未正确终止。

我们可以使用单引号或双引号。我们可以添加感叹号。我们甚至可以使用数字。例如,如果我们输入 "1" + "1",如果你在数字周围加上引号,它会认为它是一个字符,不会将其解释为数字。所以你会认为一加一等于二,但它所做的只是将两个东西粘在一起,字面意思上的粘在一起,因为它将数据类型解释为字符,即字符串,而不是整数。

数据类型非常重要,因为假设你正在处理一个数据框,其中有几列,在第一千列的地方,数据类型实际上是字符串而不是整数,而你并不知道。现在你试图执行数学函数,它会向你抛出错误,因为它说我不能把这些东西加在一起,或者我不能除这些东西,我不知道你在说什么。因此,了解你正在处理的不同数据类型非常有帮助。这也是为什么我们编写代码时使用的符号(语法)有非常具体的规则。这就是为什么字符串用引号表示。所以,任何时候你在Python中看到引号,你就知道那是一个字符串。

除了那些数学运算符,还有一些逻辑运算符,比如小于、大于、小于等于,所有这些都相当直观,直到等于和不等于,它们有点特别。

在Python中,当你给变量赋值时,你使用一个等号 =。所以,如果你想表示某物等于某物,如果你使用一个等号,你所做的只是赋值。因此,在Python中,如果你想问某物是否等于某物,你实际上需要两个紧挨着的等号 ==,这些是比较运算符。如果你想表示不等于,通常“不”用感叹号表示,所以你会写 !=,表示不等于。

我们可以这样使用,比如 1.5 < 3,这会告诉我们这个表达式为真。我们可以说 'a' > 'b',它会说假。有趣的是,我相信在Python中,字母数字字符根据它们的ASCII值被分配了一个数值,这就是为什么 'a' 不大于 'b',即使它们是字母。这也是另一个例子,说明了解你正在处理的数据类型非常重要,这是你应该注意的事情,因为即使你使用了比较运算符,但你的数据是字符串,你实际上可能会得到一个你不期望的输出。因此,你总是要检查你的代码,确保它做了你认为它应该做的事情。

这是一个使用等于运算符的例子,3 == 3.14,它会告诉我们这是假,这符合我们的预期。你也可以对字符串进行这种操作。所以,如果你想检查字符串的身份是否相同,如果字符串彼此相等,它将返回真或假。

这些真和假的值称为布尔值,它们有几种不同的表示方式。你可以用单词 TrueFalse 表示,也可以用 10 表示。这回到了真值表的概念,其中1代表真,0代表假。布尔值只是一个真或假的值,当你尝试处理数据时,这将非常重要,因为你可以以不同的方式对数据进行子集划分,并说如果满足这个条件,就对它做这个操作。例如,假设我正在处理对数变换,我不想计算0的对数。我可以说如果这个等于0,就不要把它包含在计算中。因此,它允许你评估条件是真还是假,并据此进行处理。这让你可以剖析和处理你的数据。

布尔值除了在你的条件之后打印出来,你也可以直接打印它们。所以你可以说 True == True。如果你想评估 True == False,它们不相等,所以它会告诉你 True 不等于 False,这个表达式是假的。

另一件事是Python实际上支持链式比较。如果我们赋值 x = 1.5,我们可以说 1 < x and x < 2,如果我们输入这个,这是真的。这种表示法实际上称为语法糖,它基本上是一个快捷方式,试图将两个不同的短语结合起来。它把 1 < xx < 2 合并成一个更简单的表达式,但当我们评估这个时,这也是真的。

你可以在这里使用不同的逻辑运算符。所以,快速尝试一下,写一些表达式,看看它是输出真还是假。也许试试不等于、等于、小于等于,看看程序的行为是什么样的。

需要注意的是,当你使用这种简写表示法时,Python在幕后插入了一个 and,所以它说这是一个条件,这是另一个单独的条件。

这涉及到整个概率情况,比如“与”和“或”,条件一与条件二,或者条件一或条件二。这些条件运算符或比较运算符基本上就是这些有趣的方式,你可以比较数据中的不同事物。更重要的是你理解如何使用这些符号处理数据,而不是知道这被称为运算符。如果你在阅读某些内容时知道这个很好,但如果你感到不知所措,只需专注于理解如何使用实际的代码本身。

函数与参数

上一节我们讨论了逻辑运算,本节中我们来看看函数。

函数是Python的特性。人们引入了函数,这些是执行操作的代码段。你可以把函数想象成一台机器:你给它一个输入,它会给你一个输出。你给它的那些输入通常称为参数,你给它一个参数,它返回给你的输出称为返回值。

每当你执行一个函数时,这称为调用函数。例如,我们可以说我们将调用 round 函数。让我们试试这个,输入 round(2.7),看看它做了什么。

你应该会看到,仅仅通过输入 round 并给它一些数字,它会四舍五入到最接近的整数。这是因为很多时候函数实际上有隐藏的默认设置。

对于 round 函数,实际上有一个可选参数,表示你想四舍五入到多少位小数。如果你不提供这个参数,它会默认选择最接近的整数,但我们实际上可以提供这个参数并改变它。所以,重新运行你刚才运行的内容,但现在添加另一个参数,你放置它的位置很重要。也许试试像 12,然后运行看看,你的输出现在应该看起来有点不同。

在这些函数中,正如我提到的,你输入的部分称为参数。但它们也有默认值。正如我提到的,默认的四舍五入是最接近的整数,但你可以将其更改为你想要的任何值。这些位置之所以特别重要,假设我们给它一个数字。让我们实际调换一下顺序,看看会发生什么。如果我们输入 round(1, 8.153),它会报错,说这个对象(这是一个浮点数)不能被解释为整数。它之所以报错,是因为描述的位置对函数来说非常关键。函数的编写方式是,你给它的第一个东西被分配给一个名为 number 的底层变量(这是参数),第二个位置被分配为你想要四舍五入到的小数位数。

所以,如果我们把 ndigits 放在这里,这是函数固有的。它将能够为我们四舍五入。我们可以指定。只要我们保留位置,我们可以删除这个,这不会改变你的答案。我们可以显式地定义它们,所以我们可以说我们的数字是 8.153,位数等于 1。它会进行计算。

我提到这些位置很重要,但注意这里我实际上调换了 ndigits 参数和 number 的位置。但这次,它没有像之前那样报错。那是因为这次我们非常明确地告诉了它参数是什么。我们告诉它 1 应该代表 ndigits8.153 应该代表 number

稍后我们会更多地讨论如何确定这些参数是什么,但对于每个函数,都有一种方法可以看到输入是什么以及这些东西叫什么。是的,谢谢。正如黑板上写的,你可以输入 help() 然后加上函数名,它实际上会告诉你那些东西,我们稍后会更详细地探讨。

但这会给你一些关于它期望什么输入的想法,它也可以给你一些关于你可能期望什么输出的信息,这取决于函数。

这里我想演示一下,我提到对于这个参数,round 函数的一个参数是 number,所以如果我们在函数外部定义 number,然后尝试运行它,它会告诉我们参数 number 缺失,这是因为这些变量只存在于该函数的上下文中。所以,即使我们在代码的其他地方说 ndigits,它也不会工作。我们可以通过直接说 number 作为变量来规避这个问题。所以,如果我们赋值 4.755 给变量 number,然后我们给它这个变量,这个变量代表 4.755。如果是这种情况,那么这将起作用,因为这背后实际上有意义。我们可以继续运行那个。

你也可以写得更明确,所以我们可以使用那种表示法,我们精确地分配变量名是什么,变量,然后我们也可以运行那个。

花大约30秒时间,尝试一些参数。numberndigits 是两个参数,所以稍微尝试一下,也许调换一下顺序,看看你的输出是什么。

对象、方法与属性

上一节我们介绍了函数,本节中我们来看看Python中对象、方法和属性的概念。

函数是代码中非常非常有用的方面。一般来说,我们导入的很多模块都有函数,这就是我们导入它们的原因。我们试图使用别人编写的函数供自己使用。round 函数已经是Python的一部分。我们可以调用许多其他不同的函数,其中很多函数与对象和属性的概念有关。

Python将数据表示为对象,我们讨论过的所有东西——数字、字符串、函数——实际上都只是Python的对象。属性是附加到另一个对象上的对象。这就像是说,对于三角形,底和高可能是一个属性。这个想法只是说对象有特定的特征,但这些对象本身也是对象,这听起来有点复杂,但当我们向你展示它是如何工作时,会变得容易一些。

这里我们有一个字符串,"snakes everywhere!",注意它根本没有大写。这个字符串是一个对象,字符串有一个属性。当属性是一个函数时,我们称之为方法。所以字符串有一个名为 capitalize 的方法。当我们把它附加到字符串的末尾时,它将执行那个函数。所以,如果我们有 "snakes everywhere!" 小写,然后我们将其大写,现在我们看到它实际上使我们的字符串首字母大写。所以 "snakes" 中的 "s" 现在是大写的。

如果你想查看附加到对象的所有属性的列表,你实际上可以检查那是什么。这有点回到我认为Nick提到的内容,你可以在环境中检查变量。但这里我说 "hi",这是一个字符串。让我们检查与字符串相关的属性。如果我们看到它以两个下划线开头,这对Python来说是必要的,但对我们来说不是那么重要。其他没有那两个下划线的东西,这些是有用的信息,以及我们可以调用的函数或方法或属性。capitalize 会将我们正在处理的内容大写。isalphaisdigitisdecimalisnumeric,所有这些都是我们可以调用或执行以尝试处理对象的函数或方法。所有这些都是特定于字符串的。例如,lower 使所有内容小写。所以,你实际上可以探索与这些对象相关的不同类型的属性。

假设我们确实想使用 lower。我们执行一个新函数或执行一个新方法的方式是,我们可以给它一个字符串。所以,继续用大写字母输入你的名字,然后在末尾加上 .lower()。那个 .lower() 应该有括号,因为这是我们正在调用的方法或函数。然后看看它输出什么。你应该看到我们已经将这个方法应用到了我们给它的数据或对象上,所以我们现在已经能够操作这个对象了。

是的,有很多文档,通常这可能有点让人不知所措。但我发现有帮助的是,在这里逐个案例地进行。如果我正在处理一个字符串,我想,哦,我需要这个大写,我有时会直接谷歌搜索“如何使这个大写”。它会告诉我函数名,会说,哦,使用 .upper().capitalize(),取决于你想要什么。这非常有帮助,因为并非所有事情都超级直接。实际上,谷歌可以是你学习编码时最好的朋友,只需搜索“如何做这件事”,然后在末尾加上“Python”,你通常会得到一些非常有用的东西。我们稍后会更多地讨论这个。

代码注释

上一节我们介绍了对象的方法,本节中我们来看看如何通过注释使代码更清晰。

正如我提到的,拥有整洁的代码非常有帮助,但并非所有代码都容易阅读,所以我们可以做的是在代码中添加注释,使我们的代码更具信息性。

注释是你添加的内容,它以井号 # 开头。看看现在这一切是如何高亮显示为绿色的。当我运行它时,什么也没有发生。这里甚至没有输出。发生这种情况是因为注释是Python固有的部分,你可以在代码中添加一些信息性的片段。例如,我可以说 # 这个变量代表我的宠物,然后我说 pet = "Shadow"。现在,当有人看这个时,我把这段代码给了别人。他们看了看,哦,好的。所以这就是这个变量应该代表什么。或者如果我有一个晦涩的函数,比如 base * height / 2 * 10,我不知道这到底是什么,我们可以继续解释这是什么。所以,通过向代码添加注释,你让自己、未来的自己以及查看你代码的其他人更容易理解代码在做什么,而Python只是忽略它。如果你在这里前面放任何东西,Python都会忽略它。

让我们做这个计算,random_expression = 5 * 19 + 3,然后打印 random_expression,这样我们可以看到那个值,我们得到98。但是,如果我把井号放在这里,看,现在它把这一部分注释掉了,我重新运行它。它完全忽略了那个 + 3。它现在表现得好像它不存在一样。所以,任何时候你以井号开头,它会注释掉该行的其余部分。所以,你可以把它放在行的末尾作为注释,也可以让它单独成行。但是,该行之后出现的任何内容都不会被运行。

是的,这些就是注释,如果你查看别人分发或编写的代码,看到一大堆这样的小注释,这真的很常见。人们会像这样写:这是我在这里做的,这是我在这里做的,它们的范围从极其信息丰富到有点有趣,取决于谁写的代码。

使用帮助与排查错误

上一节我们介绍了注释,本节中我们来看看如何获取帮助以及如何处理代码中的错误。

我已经演示过这个了,但很多时候,当你开始学习如何编码时,真的很难,你会有点不知所措,因为你甚至不知道从哪里开始。你不知道需要什么工具,你不知道如何格式化,甚至不知道如何读入你需要读入的内容。

所以,最好的做法是,首先,如果你遇到麻烦,就查一下。网上有很多全面的教程,会向你展示非常基础的东西,比如“如何在Python中读入CSV?”

但也有内置的帮助函数,所以如果你在使用一个函数,你实际上可以检查它在说什么。例如,如果我们输入 help(round),它会告诉我们 round 接受两个数字,numberndigits,这回到了我们所说的,你可以定义 number=某个值ndigits=某个值。这里显示的是,ndigits 的默认值是 None

所以,如果你在处理某些东西时遇到麻烦,你实际上可以通过在Python控制台中运行 help 来检查函数在做什么。每当你尝试运行某些东西,但不确定它需要什么输入、默认值是什么,或者是否需要更多参数时,这真的很有帮助,因为有时我运行的东西有,比如说,三个参数,而我只给了它两个。它会告诉我缺少一个参数。我就想,缺什么?然后我去查看帮助函数,哦,我忘了指定,比如,我希望这个输出的格式。

这对于获得你需要的帮助和更深入地理解你正在运行的东西真的很有帮助。

你也可以对操作符这样做。我们可以输入 help("+") 并运行。它会输出这个非常庞大的表达式解释,包含各种信息,如果你想了解更多,了解这些会很有帮助。我不会读给你听,但这对你询问帮助的内容进行了非常详细的说明。需要注意的是,如果你使用的不是字母数字(我认为只是字母),如果是像加号、减号这样的符号,你需要在它周围加上引号,否则Python会认为你试图做某事,并会给你一个错误。例如,如果我运行 help(abs),它说返回参数的绝对值,所以我可以不加打印或不加引号地输入这个。对于正斜杠 /,如果我运行 help("/"),它会说恰好接受一个参数。当我尝试处理这个时,我不……它只是一个提示。

是的,如果函数只是字母,你可以直接写名字,它会按原样执行。所以我们看到,尽管一个在引号里,一个不在,但我们得到了相同的输出。

如果你使用函数的在线文档,请确保你查看的版本与你安装的版本相同。我记得有一次,我因为试图使用一个叫做f-string的东西而挣扎,你不需要知道那是什么意思,但我没有意识到我落后于它被发明和纳入的Python版本一个版本,我当时想,为什么这不起作用?直到最后我意识到是版本问题。这就是为什么,如果你查看函数版本,这也是为什么提到检查版本,它可能是一个过时的版本,或者版本之类的东西。版本是一件大事,即使它似乎漂浮在背景中。版本是那些真正影响你编码和工作能力的事情之一。所以,如果你查找文档,请确保它与你正在使用的版本相关。通常,这表现为所谓的已弃用函数。你会尝试运行某些东西,它就像,哦,这个函数已弃用,这意味着它不再能够使用了,不再支持了,然后你很难过,因为你必须阅读新的文档。是的,所以只需确保版本匹配。

现在,我们将介绍当出现问题时该怎么办。我喜欢告诉实验室里的一些人的一个笑话是,如果我第一次运行所有东西都正常,我就不会有工作了。所以,迟早,可能今天,可能每次你编码时,你都会遇到某种错误。这可能是任何情况,从语法错误(因为你忘了在某个东西后面加逗号,忘了关闭括号),到你不小心删掉了一行并移动了它,以至于你在第一次调用变量之后才给它赋值,诸如此类的事情,遇到错误真的很常见。大多数时候,Python会打印出一些信息性的错误。例如,看看我能不能搞砸一些东西。是的,无效语法,所以我们知道这与语法有关,这意味着像我们的括号、逗号之类的,引号,这些是我们正在寻找的问题。

通常,当你阅读错误时,它会很有帮助,不幸的是,它并不总是超级有帮助,因为有时它会给你一个行号,有时它只会对你大喊大叫。在这种情况下,只需检查你的代码,检查它提到的任何行,有时它就像第173行无效语法,然后你去看,哦,我忘了一个括号,你加上你的括号,然后收工。另一件事是,如果你真的很难找到错误在哪里,一次运行一行代码,直到找到导致问题的原因,你可以通过注释掉大块代码来缩小范围,然后说,好吧,这就是问题所在。

网上还有其他资源,比如Stack Overflow,这是一个人们常去提问的网站。我认为,如果你更偏向生物学,有像Biostars这样的网站,它提供很多生物包的支持。在阅读器中,有一个关于如何提出好问题的链接。一般来说,你总是想加上你正在使用的语言,所以你会说“Python中的这个错误”,你几乎要逐字地写出它给你的错误,比如语法错误之类的。如果你不知道错误是什么,比如有一次我得到一个奇怪的数字错误,比如错误9372,我想我不知道那是什么,所以我查了一下,就像,哦,你内存不足,它杀死了你的工作。就像,好的。所以,一定要知道,如果你遇到错误,这不是你的问题,这是一个非常常见的问题。所以,只需继续学习如何正确地进行故障排除,查找你正在运行的内容的文档,查找代码,确保使用论坛。我的意思是,ChatGPT,如果那是你的方式,那可以帮助解决一些语法错误。

但很多时候,编程最难的部分就是试图进行故障排除,一旦你缩小了导致问题的原因,你可能会意识到修复起来并没有那么糟糕。

运行Python脚本文件

上一节我们讨论了错误排查,本节中我们来看看如何创建和运行Python脚本文件。

现在我们已经一直在控制台中摆弄了,我们将让你打开一个Python文件并运行一个文件。

你要做的是转到文件,在你的笔记本中点击“新建”,然后选择“Python文件”。这应该会打开一个文件,这应该是某种未命名的Python文件,你可以直接叫它 hello.py。然后输入 area = 2 + 2,然后 print(area)。确保保存那个文件。

此时,我相信你应该有多个标签页打开,一个是你有Python控制台的地方,一个是你有文件的地方。让我看看。我稍等一下测试这个,因为我用的是Jupyter Notebook而不是Jupyter Lab。每个人都创建文件了吗?好的。

你们有一个和我稍微不同的屏幕,你应该能够转到那个Jupyter Lab屏幕,那里写着控制台。由于某些原因,我们无法打开控制台。嗯。没关系。为了简单和节省时间,我们将跳过运行它,但一般来说,一旦你有了这个Python文件,我只需在我的终端中打开它来展示那会是什么样子。

我会输入像 python3 这样的东西。我的意思是。滚动。不,好的,那看起来很奇怪。所以 python3,然后是我要运行的任何文件。如果你在Pixi中运行,我相信你可以说 pixi run python,然后你会说像 python3 和你的文件名。这是保存。这是一个有趣的故障排除,我不知道为什么我的文件没有保存。嗯。哦,它回到了一个目录。好的,所以现在我们会说像 python3 hello.py 这样的东西,它会打印出我们的面积是4。这只是一个演示,因为很多时候你要运行的东西可能非常非常长,你不会想一行一行地运行,所以你只需把它放在文件中,使用一个命令来运行这个东西,这有点复杂,所以如果你没弄明白也不要担心,这只是为了展示运行Python有不同的方式,你可以运行脚本本身。

导入模块

上一节我们运行了脚本文件,本节中我们来看看如何导入和使用外部模块。

当我们谈论导入模块时。你知道我们安装了pandas、Polars、NumPy,这些都是模块。这些都是别人编写的软件,本质上是由别人编写的Python文件,里面定义了函数。所以当你安装它时,你是在下载他们的代码。当你说导入像NumPy这样的东西时,你实际上是在读入他们的代码,以便你可以使用他们编写的东西。

就像我把它做成一个函数一样,我实际上可以从我的文件中导入 area 并使用那个函数,所以将Python代码保存在文件中真的很有用。你可以有很长的代码,可以轻松地分享代码,所以如果你试图编写脚本之类的东西,这真的很好。

如果你有经常重用的代码,这也很好。例如,如果我经常处理FASTA文件(这些文件有一些序列名和DNA序列),如果我做很多这方面的工作,也许有一个函数 read_fasta 会很有帮助,因为我可以读入它并轻松地分离我需要的序列。所以,如果你有很多经常做的事情,手头有这些东西真的很有帮助。

当你导入一个模块时,实际上非常简单,假设你已经安装了它。既然我们确实使用Pixi安装了,你只需要说 import,然后是你导入的东西的名称。每个人都应该安装了NumPy,所以让我们继续尝试运行 import numpy

现在你所做的是保存了NumPy的代码,无论谁写了这个包,你已经读入了他们的代码,并且能够访问他们编写的函数。所以现在我们有了NumPy,如果我们想使用函数,是的,那是在控制台或文件中。是的,文件只是为了演示一种不同的使用Python的方式。

是的,所以在控制台中,继续导入NumPy。如果你想从NumPy读入一个函数,你必须指定该函数来自NumPy,所以你会说 numpy.round,因为这是一个属性。所以 numpy.round(3.3),意识到它返回3,但它也告诉你比普通 round 函数多一点的信息,因为它不是普通的 round 函数。它是NumPy中某人编写的不同函数。

有很多包,包名有非常常见的惯例,NumPy实际上是其中之一。所以很多时候,当你阅读某些东西时,NumPy被导入为叫做 np 的东西。想象一下,你正在运行大量使用NumPy的代码,你每次尝试调用函数时都想输入 numpy 吗?不,过一段时间后,这有点令人望而生畏。所以NumPy有更短的常规名称,是 np。所以通常,如果有人运行像 np.round 这样的东西,那意味着他们实际上在使用NumPy。这是惯例,通常你可以在网上找到。有几个包这是常见的。我提到过用于处理数据框的pandas,pandas是另一个你会说像 import pandas as pd 这样的东西,这样你就不必每次使用它时都输入pandas。

所以你可以写 import numpy as np。然后运行 np.round 并给它一个数字。注意,当我们用NumPy运行它时,我们得到了相同的输出,因为我们刚刚告诉Python np 实际上代表NumPy模块。

你可能会想,为什么有两个 round 函数,为什么我需要这个函数?让我们演示一下。是的,你说 np 只是一个惯例,但我可以选择任何首字母吗?或者它可以是,所以我可以导入,我可以说 as asdf,然后把它当作 asdf 使用,但通常有惯例的原因是,如果我编写我的代码并分享给你,你可以看到我是否使用了NumPy,因为任何使用它的人通常都使用那种表示法,所以它只是让分享更容易。

好的,所以这里有一个演示。当我们使用内置的 round 函数将 4.755 四舍五入到2位小数时,它给我们 4.75,但当我们使用NumPy的 round 函数处理 4.755 时,它实际上给我们 4.76。所以你使用什么很重要,包与包之间有一些细微的差别。这就是为什么阅读函数文档很重要,因为你需要理解你究竟在运行什么,以及它的行为是什么,因为如果你期望当这个四舍五入时,它会是 4.76,但你使用了这个版本的函数,它没有做你期望它做的事情。

它们期望的行为或方式,就像有人在计算机上工作一样。不同的算法会产生这些不同的舍入结果。实际上,根据文档,iPhone 1 由 iPhone 驱动的实际上在大多数情况下应该更准确,但速度较慢。所以在这种情况下,NumPy 给了我们实际上会做的结果。是的,如果你需要像非常精确的舍入,有特殊的舍入方式。

是的,所以注意 roundnumpy.round 是不同的函数,所以你必须指定你使用的是哪个函数,以及你使用的是从导入的模块中使用的函数,还是只是使用Python内置的函数。

Jupyter Notebook 基础

上一节我们导入了外部模块,本节中我们来看看Jupyter Notebook的基本操作。

Jupyter Notebook 是我在这里运行的美丽的东西,我混合了文本、代码和图片。所以它比仅仅一个文件或脚本或只是一个控制台更全面,更有组织。

Jupyter本身代表Julia、Python、TeX和R,所以它像一个混合起来的缩写。Julia是另一种编程语言,R是一种编程语言。

我们将继续让你们创建一个笔记本,这样你们就可以开始尝试运行一些东西。

所以

002:数据结构与控制流

在本节课中,我们将学习Python中的核心数据结构(如列表、元组、字典、集合)以及Polars库中的Series和DataFrame。理解这些数据类型及其操作是进行有效数据分析的基础。我们还将探讨Python中的特殊值(如None、NaN)和基本的数据筛选方法。

概述

上一节我们介绍了如何设置环境并加载数据。本节中,我们将深入探讨Python和Polars中的数据结构,理解数据类型的转换,并学习如何对数据进行基本的筛选操作。

数据类型的重要性

在统计学和数据分析中,我们通常将数据分为两大类:数值型数据分类型数据。数值型数据可以是离散的(如整数)或连续的(如小数)。分类型数据可以是无序的(名义变量)或有序的(序数变量)。

在编程中,数据类型决定了我们可以对数据执行哪些操作。例如,尝试将数字与字符串相加会导致错误。

# 这将导致TypeError
1 + "hi"

我们可以使用 type() 函数来检查任何对象的类型。

type(4)    # <class 'int'>
type(4.0)  # <class 'float'>
type("hi") # <class 'str'>

Python内置数据类型

Python提供了多种内置数据类型来存储和操作数据。

列表

列表是一种有序的容器,可以存储任意类型的元素,使用方括号 [] 创建。

my_list = [5, 6, 7]
type(my_list) # <class 'list'>

以下是列表的一些基本操作:

  • 获取元素:使用索引,索引从0开始。
    my_list[0] # 获取第一个元素,结果为 5
    
  • 获取长度:使用 len() 函数。
    len(my_list) # 结果为 3
    
  • 修改元素:通过索引进行赋值。
    my_list[0] = 10
    
  • 列表推导式:一种简洁地创建或转换列表的方法。
    squared = [x**2 for x in my_list]
    filtered = [x for x in my_list if x > 6]
    

重要提示:在Python中,简单的赋值 (y = x) 创建的是对原始列表的引用,而不是副本。修改 x 也会影响 y。如果需要独立的副本,请使用 .copy() 方法。

x = [1, 2, 3]
y = x       # y 是 x 的引用
z = x.copy() # z 是 x 的副本
x[0] = 99
print(y) # [99, 2, 3]
print(z) # [1, 2, 3]

元组、集合与字典

除了列表,Python还有其他有用的容器类型:

  • 元组:使用圆括号 () 创建。与列表类似,但元素不可更改(不可变)。通常用于表示固定的一组值。
    my_tuple = (1, 2, "a")
    
  • 集合:使用花括号 {} 创建,存储唯一、无序的元素。支持集合运算(如并集、交集)。
    my_set = {1, 2, 2, 3} # 结果为 {1, 2, 3}
    
  • 字典:使用花括号 {} 创建,存储键值对。通过键来访问对应的值。
    my_dict = {"name": "Alice", "age": 30}
    my_dict["name"] # 结果为 'Alice'
    

Polars 中的 Series

在Polars中,Series 是一个核心数据结构,可以看作是一个带标签的一维数组。它类似于Python的列表,但所有元素必须是相同的数据类型,并且支持高效的向量化操作(广播)。

从DataFrame中获取一列,就得到一个Series。

import polars as pl
turns = pl.read_csv("data/least_terns.csv")
year_series = turns["year"]
type(year_series) # <class 'polars.series.series.Series'>

以下是Series的一些关键特性:

  • 数据类型:使用 .dtype 属性查看。
    year_series.dtype # Int64
    
  • 广播:数学运算会自动应用于每个元素,无需循环。
    squared_series = year_series ** 2
    
  • 创建Series:使用 pl.Series() 函数。
    new_series = pl.Series("my_numbers", [1, 2, 3])
    

Polars 数据类型

Polars定义了自己的数据类型(如 pl.Int64, pl.Float64, pl.Utf8 等),以优化内存使用和性能。例如,Int8 使用8位存储,范围是-128到127。默认的整数类型 Int64 范围大得多,通常足够使用。

将数据转换为正确的类型(特别是分类数据)是数据分析的重要步骤。

# 将字符串列转换为分类类型,可以节省内存并提高分组操作效率
turns = turns.with_columns(pl.col("site_name").cast(pl.Categorical))

特殊值

在数据分析中,我们需要处理一些特殊值。

  • None:Python内置的空值,表示“无”或“空”。常用于函数无返回值时。
    result = print("Hello") # print函数返回None
    
  • null (Polars):在Polars中表示缺失数据,类似于R中的 NA。出现在DataFrame或Series中。
    # 假设 ‘non_fd_eggs’ 列有缺失值
    missing_val = turns["non_fd_eggs"][2] # 可能得到 None
    
  • NaN (Not a Number):表示未定义的数学运算结果,如 0.0 / 0.0
  • Inf (Infinity):表示无穷大,如 10.0 / 0.0

这些值具有传播性,任何涉及它们的运算结果通常也是特殊值。

数据筛选与选择

现在,让我们应用这些概念来处理我们的数据框。

选择列

使用 .select() 方法选择特定的列。返回结果是一个新的DataFrame。

# 选择单列或多列
selected = turns.select("year")
selected_multi = turns.select("year", "site_name")

要在选择列的同时对其进行计算,需要使用 pl.col() 创建表达式。

# 对‘year’列进行计算,并选择‘site_name’列
transformed = turns.select(
    (pl.col("year") / 10),
    pl.col("site_name")
)

添加或转换列

使用 .with_columns() 方法。它在保留所有原有列的同时,添加新的列或替换已有的列。

# 创建新列‘year_decade’,并保留所有其他列
turns_with_new_col = turns.with_columns(
    (pl.col("year") / 10).alias("year_decade")
)

筛选行

使用 .filter() 方法,根据条件筛选行。组合条件时,使用 & 表示“且”,| 表示“或”。

# 筛选 total_nests 小于10 且 fl_max 大于5 的行
filtered_data = turns.filter(
    (pl.col("total_nests") < 10) & (pl.col("fl_max") > 5)
)

可以链式调用这些方法,Polars会优化执行顺序。

result = (turns
          .filter(pl.col("year") > 2010)
          .select("site_name", "total_nests")
          .with_columns((pl.col("total_nests") * 2).alias("double_nests"))
         )

总结

本节课我们一起学习了Python和Polars中的核心数据结构。我们了解了列表、元组、字典、集合等Python内置类型的特性和用法,重点掌握了Polars中Series和DataFrame的创建、类型转换以及基本的列选择与行筛选操作。理解数据类型和这些基本操作是构建复杂数据分析流程的基石。下一节中,我们将利用这些知识进行数据可视化。

003:pandas入门与表格数据处理 📊

在本节课中,我们将学习如何使用Python进行数据可视化,特别是通过plotnine库(一个ggplot2的Python克隆)来创建美观且信息丰富的图表。我们将从理解“图形语法”开始,逐步构建一个完整的图表,并学习如何调整其样式和保存结果。


概述 📋

数据可视化是数据分析中至关重要的一步,它能将复杂的数据转化为直观、易于理解的图形。在本节课程中,我们将重点介绍如何使用plotnine库,基于“图形语法”的概念来创建图表。我们将学习如何设置数据层、几何图形层和美学映射层,并最终生成可用于报告或发表的图表。


可视化包介绍 📦

在开始之前,我们先了解一些流行的Python可视化包。根据你的需求和偏好,有多种选择:

  • Matplotlib:一个非常流行且灵活的Python基础绘图库。它功能强大,但创建复杂图表可能需要更多代码。
  • Plotnine:这是我们将要使用的库。它是R语言中著名绘图库ggplot2的Python版本,语法几乎相同,非常适合熟悉ggplot2的用户。
  • Seaborn:基于Matplotlib,提供了更高级的接口和美观的默认样式,特别适合制作统计图表和热图。

选择哪个包取决于你的具体需求和个人习惯。今天,我们将专注于plotnine


图形语法 🎨

plotnine(以及ggplot2)的核心思想是“图形语法”。它认为图表是由多个图层叠加构建而成的。每个图表至少包含三个基本图层:

  1. 数据层:指定要绘制的数据集。
  2. 几何图形层:决定数据的视觉表现形式,例如散点图、折线图或条形图。
  3. 美学映射层:将数据中的变量映射到图形的视觉属性上,如颜色、形状、大小等。

此外,还可以添加坐标轴、标题、图例等可选图层来完善图表。


环境设置与导入 🔧

首先,我们需要导入必要的库并设置绘图环境。

# 导入plotnine库,并允许使用类似ggplot2的语法
from plotnine import *
# 导入matplotlib用于调整图形显示
import matplotlib.pyplot as plt

# 设置Jupyter Notebook中图形的默认显示大小
plt.rcParams['figure.figsize'] = (10, 6)

数据准备与第一层:数据层 📁

在绘图前,确保你的数据是“整洁的”。整洁数据意味着:

  • 每个变量占一列。
  • 每个观测值占一行。
  • 每个值占一个单元格。

这有助于代码的读取和索引。我们的示例数据已经处理为整洁格式。

现在,我们加载数据并创建图表的基础——数据层。

# 假设我们已经有一个名为'turns'的DataFrame,包含了我们的数据
# 创建图表对象,仅指定数据层
ggplot(turns)

运行这行代码,你会得到一个空白的画布。这是因为我们只告诉了图表要使用什么数据,但还没有指定如何展示它。


第二层:几何图形层 🔵

接下来,我们添加几何图形层,告诉图表我们想要绘制哪种类型的图形。例如,要绘制散点图,我们使用geom_point()

ggplot(turns) + geom_point()

运行这段代码会报错,提示缺少xy美学映射。这是合理的,因为我们还没有指定用数据中的哪些变量来分别代表x轴和y轴。


第三层:美学映射层 🎭

美学映射层连接了数据和几何图形。它定义了如何将数据变量映射到图形的视觉属性上。我们使用aes()函数来设置。

ggplot(turns) + geom_point() + aes(x='bp_min', y='total_nests')

现在,图表成功绘制了bp_mintotal_nests两个变量的散点图。每个点代表数据中的一行观测值。


添加更多美学属性与分组 🎨

我们可以通过美学映射,用颜色和形状来区分数据中的另一个变量,例如event(气候事件)。

ggplot(turns) + geom_point() + aes(x='bp_min', y='total_nests', color='event', shape='event')

这样,不同event类别的点就会以不同的颜色和形状显示,图例也会自动生成。同时使用颜色和形状可以提高图表的可访问性。


调整图形细节与保存 📸

为了使图表更专业,我们可以添加轴标签、标题,并调整颜色。

# 构建一个完整的图表对象
my_plot = (ggplot(turns)
           + geom_point()
           + aes(x='bp_min', y='total_nests', color='event', shape='event')
           + labs(x='Minimum Reported Breeding Pairs',
                  y='Total Nests',
                  color='Climate Event',
                  shape='Climate Event',
                  title='California Least Terns: Breeding Pairs vs. Nests')
           + theme_bw() # 使用黑白主题,更简洁
          )

# 显示图表
print(my_plot)

# 将图表保存为图片文件
my_plot.save('my_plot.png', dpi=300)

通过labs()函数,我们可以自定义所有标签。theme_bw()应用了一个更简洁的主题。最后,使用save()方法将图表保存为PNG格式的图片文件。


创建其他类型的图表:条形图 📊

图形语法同样适用于其他图表类型。例如,要创建堆叠条形图来展示不同区域每年雏鸟数量的总和:

# 筛选我们关注的区域
regions_to_keep = ['SF Bay', 'Central', 'Southern']
turns_filtered = turns.filter(pl.col('region3').is_in(regions_to_keep))

# 创建堆叠条形图
bar_plot = (ggplot(turns_filtered)
            + aes(x='year', y='min_fledglings', fill='region3')
            + geom_bar(stat='identity') # 使用原始值而非计数
            + labs(x='Year',
                   y='Minimum Reported Fledglings',
                   fill='Region',
                   title='California Least Terns Fledglings by Region')
            + scale_fill_brewer(type='qual', palette='Set2') # 使用Set2调色板
           )
print(bar_plot)

这里,aes()中的fill参数指定了用于填充颜色的分组变量。geom_bar(stat='identity')表示直接使用y值(min_fledglings)作为条形高度,而不是对行进行计数。scale_fill_brewer()允许我们使用预设的、对色盲友好的调色板。


数据聚合与分组 📈

在可视化前后,经常需要对数据进行汇总分析。polars库提供了强大的聚合与分组功能。

简单聚合:计算整个数据集的统计量。

# 计算捕食性鸟蛋死亡率的中位数
median_predation = turns['predatory_eggs'].median()

分组聚合:按类别计算统计量,信息更丰富。

# 按区域(region3)分组,计算每个区域总巢数的中位数
median_nests_by_region = turns.group_by('region3').agg(pl.col('total_nests').median())

复杂聚合:同时对多列进行多种计算,并重命名输出列。

# 按区域分组,同时计算总巢数和最大繁殖对数的中位数及最大值
summary_stats = (turns.group_by('region3')
                 .agg([
                     pl.col('total_nests').median().suffix('_median'),
                     pl.col('total_nests').max().suffix('_max'),
                     pl.col('bp_max').median().suffix('_median'),
                     pl.col('bp_max').max().suffix('_max')
                 ]))

保留原始结构的计算:使用over()函数可以在不改变数据形状(不聚合)的情况下为每一行添加分组计算结果。

# 计算每年的平均总巢数,但结果添加到原始数据的每一行中
turns_with_avg = turns.select([
    '*', # 选择所有原有列
    pl.col('total_nests').mean().over('year').alias('yearly_avg_nests')
])

优秀可视化实践准则 ✅

在创建图表时,请遵循以下准则以确保其清晰有效:

  • 始终添加标题和轴标签:使用描述性文字,并包含单位。
  • 考虑色盲用户:使用区分度高的颜色,并辅以形状或纹理差异。
  • 包含图例:清晰地解释图表中使用的所有颜色、形状和大小。
  • 撰写说明:为图表配上一段简短的文字说明,解释其展示的内容。
  • 谨慎比较:当并排展示多个图表时,确保它们使用相同的比例尺,以便进行公平比较。

记住,创建高质量的图表是一个迭代过程。从简单的草图开始,逐步添加和调整元素,直到得到清晰、美观且信息丰富的结果。


总结 🎉

本节课我们一起学习了数据可视化的核心概念和技能:

  1. 图形语法:理解了图表是由数据层、几何图形层和美学映射层等叠加构建的。
  2. 使用Plotnine:掌握了如何使用plotnine库,通过叠加图层(+)的方式来创建散点图和条形图。
  3. 美学映射:学会了如何将数据变量映射到颜色、形状等视觉属性,并对图表进行标注和美化。
  4. 数据聚合:了解了如何使用polars对数据进行分组和汇总计算,这是理解和准备可视化数据的关键步骤。
  5. 最佳实践:认识到创建易于理解且专业的图表需要遵循一定的设计原则。

通过将数据操作与可视化相结合,你现在可以更有效地探索数据、发现模式,并向他人清晰地传达你的分析结果。

004:代码组织、函数与迭代

在本节课中,我们将要学习如何组织代码以提高效率和可读性。主要内容包括:深入理解Polars的延迟计算机制、如何编写自定义函数、使用条件语句(if)控制程序流程,以及通过迭代(循环)自动化重复性任务。这些技能是编写高效、可维护数据分析代码的基础。


Polars表达式:延迟计算

上一节我们介绍了Polars的基本操作,本节中我们来看看Polars一个核心概念:表达式和延迟计算。这有助于理解为何有时需要使用 pl.col()

在Polars中,pl.col() 用于创建一个表达式,它描述了你想要对数据进行的计算,但并不会立即执行。计算只有在表达式被用于 select()with_columns() 等方法时才会发生。

代码示例:

import polars as pl

# 加载数据
turns = pl.read_csv('turns_data.csv')

# 创建一个表达式:计算 ‘total_nests’ 列的均值
mean_expr = pl.col('total_nests').mean()
# 此时 mean_expr 只是一个表达式对象,并未计算

# 在 select 中使用表达式,此时才进行计算
result1 = turns.select(mean_expr)
print(result1)

# 对数据子集使用同一个表达式
result2 = turns.filter(pl.col('year') == 2020).select(mean_expr)
print(result2)

这种方式的好处是:

  1. 代码复用:可以预先定义复杂的计算逻辑,然后应用到不同的数据子集上。
  2. 避免错误:确保对多个数据集或子集执行完全相同的计算,避免因复制粘贴导致的笔误。
  3. 性能优化:Polars可以优化整个表达式链的执行计划。

与之相对,如果提前计算出一个具体的值(如 turns['total_nests'].mean()),那么这个值就是一个固定的数字,不会随数据子集的变化而改变。


编写自定义函数

当你发现某段代码需要重复使用时,就应该考虑将其封装成函数。这能减少错误,并使代码更易于维护和测试。

函数基础

一个简单的函数包含以下部分:def 关键字、函数名、参数、冒号以及缩进的函数体。使用 return 语句返回结果。

代码示例:创建一个判断数值是否为负的函数

def is_negative(x):
    """
    判断输入值是否为负数。
    参数:
        x: 数值
    返回:
        bool: 如果x为负则返回True,否则返回False
    """
    result = x < 0
    return result

# 测试函数
print(is_negative(-5))  # 输出: True
print(is_negative(3))   # 输出: False
print(is_negative(0))   # 输出: False

编写更实用的函数

让我们编写一个更实用的函数,用于获取一个序列中最大的N个值。

代码示例:获取序列中最大的N个值

def top_values(data, n=3):
    """
    返回序列中最大的n个值。
    参数:
        data: 列表或Polars Series
        n: 要返回的最大值的数量,默认为3
    返回:
        Polars Series: 包含最大n个值的Series
    """
    # 将输入转换为Polars Series以确保方法可用
    s = pl.Series(data)
    # 降序排序并取前n个
    result = s.sort(descending=True).head(n)
    return result

# 测试函数
test_data = [1, -1, 7, 2, 0]
print(top_values(test_data))      # 输出: shape: (3,)
                                  # Series: '' [i64]
                                  # [
                                  #     7
                                  #     2
                                  #     1
                                  # ]
print(top_values(test_data, n=2)) # 输出前2个最大值

编写函数的建议步骤:

  1. 先用简单的测试数据写出能工作的代码。
  2. 确定哪些部分会随着调用而改变,将它们设为函数的参数。
  3. def 关键字定义函数,将代码放入函数体,并用 return 返回结果。
  4. 使用不同的输入测试函数。

使用条件语句(if)

程序常常需要根据不同的条件执行不同的操作。在Python中,我们使用 ifelifelse 语句来实现条件分支。

代码示例:一个简单的问候函数

def greet(name):
    """
    根据不同的名字返回特定的问候语。
    """
    if name == "Vicky":
        print("Hello Vicky! Thanks for co-instructing.")
    elif name == "Tim":
        print("Hi Tim! Thanks for coming.")
    else:
        print(f"Hi {name}, have we met?")

![](https://github.com/OpenDocCN/cs-notes-pt2-zh/raw/master/docs/ucd-ecs032a-pyda-intro/img/69a09d07f08094801ba73f70ff27b113_1.png)

![](https://github.com/OpenDocCN/cs-notes-pt2-zh/raw/master/docs/ucd-ecs032a-pyda-intro/img/69a09d07f08094801ba73f70ff27b113_3.png)

# 测试函数
greet("Vicky")  # 输出: Hello Vicky! Thanks for co-instructing.
greet("Alice")  # 输出: Hi Alice, have we met?
greet("Tim")    # 输出: Hi Tim! Thanks for coming.

关键点:

  • if/elif 后面的条件必须能计算为 TrueFalse
  • 缩进定义了属于每个条件分支的代码块。
  • else 是可选的,用于处理所有未匹配的情况。
  • 应谨慎使用条件语句,因为过多的分支会使代码复杂。有时使用字典映射或 match 语句(Python 3.10+)可能是更清晰的选择。

迭代:自动化重复任务

迭代是编程的核心,用于对一系列元素执行相同或类似的操作。Python提供了多种迭代方式,在灵活性和效率之间各有权衡。

迭代方式概览

以下是几种常见的迭代方式,从最简洁高效到最灵活:

  1. 广播(Broadcasting):直接对整个Series进行操作。最简洁、最快,但要求操作是Polars支持的内置向量化操作。

    s = pl.Series([1, 2, 7])
    result = s - 10  # 广播:每个元素减10
    
  2. 列表推导式(List Comprehension):对可迭代对象(如列表)中的每个元素应用表达式。比广播灵活,可以调用任何Python函数。

    my_list = [1, 2, 7]
    result = [x - 10 for x in my_list]
    
  3. 循环(Loops):使用 for 循环。最灵活,可以在循环体内执行任意多行代码,尤其适用于前后迭代相互依赖的情况(如时间序列模拟)。

    results = []
    for x in [1, 2, 7]:
        new_value = x - 10
        # 这里可以添加更复杂的逻辑
        results.append(new_value)
    

选择建议:优先选择能满足需求的最简单、最高效的方式。如果广播可行,就用广播;如果不行,尝试列表推导式;只有当迭代步骤复杂或相互依赖时,才使用 for 循环。

循环实战:模拟迭代计算

以下是一个使用 for 循环的典型场景:每次计算都依赖于前一次的结果。

代码示例:重复计算正弦值

import numpy as np
import polars as pl

# 初始化
value = 1.0
iterations = 100
results = []  # 用于存储每次迭代的结果
iteration_numbers = list(range(iterations))  # 迭代次数列表

for i in iteration_numbers:
    value = np.sin(value)  # 计算当前值的正弦,并作为下一次的输入
    results.append(value)

# 将结果转换为DataFrame以便分析或绘图
df_sim = pl.DataFrame({
    "iteration": iteration_numbers,
    "result": results
})
print(df_sim.head())
# 输出示例:
# shape: (5, 2)
# ┌───────────┬──────────┐
# │ iteration ┆ result   │
# │ ---       ┆ ---      │
# │ i64       ┆ f64      │
# ╞═══════════╪══════════╡
# │ 0         ┆ 0.841471 │
# │ 1         ┆ 0.745624 │
# │ 2         ┆ 0.67843  │
# │ 3         ┆ 0.627572 │
# │ 4         ┆ 0.587181 │
# └───────────┴──────────┘

案例预告:合并多个数据文件

一个常见的循环应用场景是处理多个结构相似的数据文件。例如,加州医院床位利用率数据每年一个独立文件。我们可以:

  1. 编写一个函数 clean_hospital_data(filepath),用于读取并清洗单个文件。
  2. 使用 for 循环遍历所有年份的文件路径列表。
  3. 在循环中调用清洗函数,并将结果收集到一个列表中。
  4. 最后将所有年份的数据合并成一个大的DataFrame。

这避免了为每个文件重复编写几乎相同的代码,极大地提高了效率和代码的健壮性。


总结

本节课中我们一起学习了组织Python代码的核心技能。

  • 我们深入了解了Polars的表达式和延迟计算机制,明白了 pl.col() 在构建可复用计算逻辑中的作用。
  • 我们学会了如何编写自定义函数来封装重复代码,使程序更模块化、更易调试。
  • 我们掌握了使用 if 条件语句来控制程序流程,根据不同条件执行不同操作。
  • 我们探讨了多种迭代方法(广播、列表推导式、循环),理解了它们在不同场景下的适用性,并特别练习了在迭代步骤相互依赖时如何使用 for 循环。

掌握这些概念将使你能够编写出更清晰、更高效、更易于维护的数据分析脚本。在接下来的案例研究中,我们将综合运用这些技能来解决一个实际的数据处理问题。

005:代码组织、可重复性与综合实践

在本节课中,我们将通过一个医院数据的案例研究,学习如何组织代码、确保分析的可重复性,并进行综合实践。我们将从读取原始数据开始,逐步进行数据清洗、转换,最终完成一个简单的可视化分析。


概述与目标

我们的目标是读取加州医院2023年的数据文件,并将其整理成一个可用于分析的数据框。输入是八个Excel文件,输出是一个包含所有相关列和行的统一数据框。

导入必要的库

首先,我们需要导入处理数据所需的库。我们将使用Polars进行数据操作。

import polars as pl
from pathlib import Path
from plotnine import ggplot, aes, geom_bar

读取单个Excel文件

我们将从读取2023年的单个文件开始。Polars的read_excel函数可以读取Excel文件,但需要指定文件路径和工作表。

# 尝试读取文件
path = "data/CA_hospitals/H_23.xlsx"
sheet = pl.read_excel(path, sheet_id=2)

上一节我们介绍了如何读取文件,本节中我们来看看读取后的数据结构。我们发现,数据的第一行是描述信息,并非实际的医院数据。因此,我们需要移除这些行。

清理数据行

数据的前四行和最后一行不是医院记录,我们需要将其删除。我们将使用Python的切片语法来选择所需的数据行。

# 删除前四行和最后一行
sheet_clean = sheet[4:-1, :]

选择相关列

原始数据有355列,我们只需要其中一部分进行分析。以下是我们要保留的列:

  • 医院元数据列:用于识别和分组医院信息。
    • fac_name: 医院名称
    • fac_city: 所在城市
    • fac_zip: 邮政编码
    • fac_operated_this_yr: 当年是否运营
    • facility_level: 医院级别
    • t_hospital: 是否为教学医院
    • county: 所在县
    • prim_service_type: 主要服务类型
  • 床位相关列:包含以tt_开头的总计列。
  • 呼吸科相关列:包含respiratory关键词的列。

我们将使用Polars的select方法和正则表达式来选择这些列。

# 定义要选择的列
facility_cols = ["fac_name", "fac_city", "fac_zip", "fac_operated_this_yr",
                 "facility_level", "t_hospital", "county", "prim_service_type"]

# 选择列并转换数值列类型
sheet_selected = (sheet_clean
                  .select(
                      facility_cols,
                      pl.col("^tt_.*$"),  # 选择所有以tt_开头的总计列
                      pl.col(".*respiratory.*")  # 选择所有包含respiratory的列
                  )
                  .with_columns(
                      pl.col("^tt_.*$").cast(pl.Float64),  # 将总计列转换为浮点数
                      pl.col(".*respiratory.*").cast(pl.Float64)  # 将呼吸科列转换为浮点数
                  )
                 )

将清洗步骤封装为函数

为了使代码可重复用于其他年份的数据,我们将上述步骤封装成一个函数。

def read_hospital_data(path, year):
    """
    读取并清洗单个年份的医院数据。
    参数:
        path: Excel文件路径
        year: 数据年份
    返回:
        清洗后的Polars数据框
    """
    # 读取指定工作表
    sheet = pl.read_excel(path, sheet_id=2)
    # 清理行
    sheet_clean = sheet[4:-1, :]
    # 选择并转换列
    facility_cols = ["fac_name", "fac_city", "fac_zip", "fac_operated_this_yr",
                     "facility_level", "t_hospital", "county", "prim_service_type"]
    result = (sheet_clean
              .select(
                  pl.lit(year).alias("year"),  # 添加年份列
                  facility_cols,
                  pl.col("^tt_.*$"),
                  pl.col(".*respiratory.*")
              )
              .with_columns(
                  pl.col("^tt_.*$").cast(pl.Float64),
                  pl.col(".*respiratory.*").cast(pl.Float64)
              )
             )
    return result

# 测试函数
df_2023 = read_hospital_data("data/CA_hospitals/H_23.xlsx", 2023)
print(df_2023.head())

批量处理多个文件

现在我们需要处理2018年及以后的所有文件。我们将使用pathlib模块来获取文件路径列表,并循环调用清洗函数。

首先,创建一个辅助函数从文件名中提取年份。

def get_year_from_path(path):
    """从文件路径中提取四位数的年份。"""
    # 例如,从 'H_23.xlsx' 中提取 '23'
    year_short = path.name[2:4]
    # 转换为四位数年份,如 2023
    year = int(year_short) + 2000
    return year

然后,批量读取所有符合条件的数据文件。

# 获取所有Excel文件路径
data_dir = Path("data/CA_hospitals")
paths = list(data_dir.glob("*.xlsx"))

# 循环读取2018年及以后的数据
all_hospitals_list = []
for p in paths:
    year = get_year_from_path(p)
    if year >= 2018:  # 只处理2018年及以后的格式
        df = read_hospital_data(p, year)
        all_hospitals_list.append(df)

# 将所有数据框合并为一个
all_hospitals = pl.concat(all_hospitals_list)
print(f"总数据行数: {all_hospitals.shape[0]}")
print(f"总列数: {all_hospitals.shape[1]}")

数据可视化分析

有了整理好的数据,我们可以进行简单的可视化分析。例如,我们可以查看不同年份医院总占用床日数的变化。

# 绘制每年总占用床日数的条形图
plot_total = (ggplot(all_hospitals, aes(x='year', weight='tt_census_days'))
              + geom_bar()
             )
print(plot_total)

我们也可以专门查看急性呼吸科护理占用床日数的变化,这可能与特定事件(如疫情)相关。

# 绘制每年急性呼吸科护理占用床日数的条形图
plot_resp = (ggplot(all_hospitals, aes(x='year', weight='acute_respiratory_care_census_days'))
             + geom_bar()
            )
print(plot_resp)

总结

本节课中我们一起学习了数据分析的完整流程。我们从读取杂乱的原始Excel数据开始,通过删除无关行、选择关键列、转换数据类型等步骤清洗数据。为了提高代码的复用性,我们将清洗逻辑封装成函数。接着,我们批量处理了多个年份的数据文件,并将结果合并。最后,我们利用整理好的数据进行了简单的可视化分析,观察了医院床位使用情况随时间的变化。这个过程涵盖了数据获取、清洗、整合和分析的核心步骤,是进行可重复数据分析的重要实践。

006:使用Python的API进行网络爬虫 🕸️

在本节课中,我们将要学习什么是网络爬虫和API,以及如何使用Python与网络服务(特别是Twitter)进行交互,从而获取和分析公开的网络数据。

概述:网络、文本与数据

互联网上的所有网页本质上都是文本文件。你的浏览器下载这些包含HTML指令的文本文件,并将其渲染成你看到的交互式页面。因为网络是公开的,所以这些文本文件中的数据也是公开可用的。网络爬虫就是让计算机自动读取这些文本文件,并从中提取我们感兴趣的部分。

什么是API?🚪

API(应用程序编程接口)可以看作是网站为计算机程序准备的“前门”。与爬虫(通过解析人类可读的网页来获取数据,有时像是“走后门”)不同,API直接提供计算机可读格式的数据,使得数据获取更加规范和高效。

例如,一个提供随机猫咪图片的网站 placekitten.com 就是一个简单的API。你可以通过改变URL中的数字(如 /400/400)来请求特定尺寸的图片。这种通过参数与服务器交互的方式,就是API的核心。

代码示例:请求猫咪图片

# 这是一个伪代码示例,展示API调用的概念
image_url = "https://placekitten.com/400/400"
# 程序会访问这个URL并获取图片数据

为什么API存在?三种网站态度

网站对数据抓取的态度大致分为三类:

  1. 请勿抓取我:数据等于金钱的公司(如某些社交媒体、金融数据提供商)。它们会设置障碍,如混淆网页结构,防止自动化抓取。
  2. 请抓取我:具有公共使命的机构(如政府网站、维基百科)。它们通常会提供友好的公共API,方便人们获取数据。
  3. 按我的规则抓取我:大多数社交媒体平台(如Twitter)。它们提供API,但会施加严格限制(如调用频率、数据量),并要求注册获取密钥(API Key),以便管理和控制数据的使用。

实战:使用Python与Twitter API交互 🐦

上一节我们介绍了API的基本概念,本节中我们来看看如何具体使用Python来调用一个真实的API——Twitter API。

准备工作:认证与密钥

要与Twitter这类私有API交互,你需要注册开发者账号并获取API密钥。这个密钥是一串独特的字符,相当于你的密码,用于标识你的身份。务必妥善保管,不要公开分享。

代码示例:设置认证(概念性)

# 导入必要的库
import tweepy

# 使用你的密钥进行认证
auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
auth.set_access_token(access_token, access_token_secret)

# 创建API对象
api = tweepy.API(auth)
# 现在可以使用 `api` 对象调用各种功能了

获取数据:从推文到用户信息

通过认证后的 api 对象,我们可以轻松获取各种数据。

获取用户时间线推文:

# 获取某个用户最近的20条推文
tweets = api.user_timeline(screen_name='username', count=20)
for tweet in tweets:
    print(tweet.text)

搜索特定关键词或话题标签:

# 搜索包含特定关键词的推文
search_results = api.search(q='#datascience', count=10)
for tweet in search_results:
    print(f"{tweet.user.screen_name}: {tweet.text}")

获取用户的关注者列表:

# 获取某个用户的关注者(有限数量)
followers = api.followers(screen_name='username')
for follower in followers:
    print(follower.screen_name)

理解数据结构:推文对象

从API返回的推文(Tweet)不仅仅是一段文字。它是一个包含丰富信息的对象(在Python中通常表现为字典或具有属性的对象)。

一个推文对象可能包含以下信息:

  • id: 推文的唯一标识符。
  • text / full_text: 推文内容。
  • created_at: 创建时间。
  • user: 发布者信息(本身又是一个包含 screen_name, followers_count 等字段的对象)。
  • retweet_count: 转发数。
  • favorite_count: 点赞数。
  • entities: 包含提到的用户、话题标签、链接等。

你可以像操作字典一样访问这些属性:

tweet = api.home_timeline(count=1)[0] # 获取一条最新推文
print(f"语言: {tweet.lang}")
print(f"发布者: {tweet.user.screen_name}")
print(f"粉丝数: {tweet.user.followers_count}")

重要限制与应对策略 🚧

使用Twitter API(尤其是免费层)时,你会遇到诸多限制,这是学习过程中的主要挑战之一。

以下是核心限制:

  • 频率限制:每15分钟窗口内只能进行有限次API调用(例如15次)。
  • 数量限制:单次请求最多获取200条推文;通过时间线最多能回溯获取3200条推文;搜索功能通常只能回溯约一周的数据。
  • 流式API限制:流式传输(实时获取)通常只提供1%的随机样本。

应对策略:

  1. 遵守规则,耐心规划:对于长期项目,提前开始缓慢收集数据。
  2. 使用分页(Pagination):通过 page 参数分批获取数据,以突破单次请求的数量限制。
  3. 寻找替代数据源:使用如 pushshift.io 等第三方存档数据。
  4. 考虑非官方库:在了解风险(可能违反服务条款)后,使用如 twint 等非官方抓取工具。
  5. 付费获取:对于研究项目,可以将商业API费用纳入预算。

总结与练习 💻

本节课中我们一起学习了网络爬虫和API的核心概念,并重点演练了如何使用Python通过Twitter API获取社交数据。我们了解了API的便利性,也认识了其种种限制以及可能的应对方法。

现在,你可以尝试以下练习来巩固所学:

  • 修改参数:在提供的代码笔记本中,尝试更改用户名、搜索关键词或地理位置坐标,观察不同的结果。
  • 数据提取:写一段代码,从你获取的推文列表中,提取出所有用户的屏幕名称(screen name)并计算唯一数量。
  • 简单分析:获取某个话题标签下的推文,尝试统计这些推文中最常出现的三个单词(需简单处理文本,如转为小写、去除常见停用词)。
  • 探索限制:尝试连续快速运行多个API调用,观察触犯频率限制时返回的错误信息。

记住,熟练使用API的关键在于阅读文档、大胆尝试并妥善处理错误和限制。祝你探索愉快!

007:Jupyter与AWS入门 🚀

在本节课中,我们将学习如何结合使用Jupyter Notebook与亚马逊云服务(AWS)进行数据分析。我们将了解AWS的核心概念,并通过实际操作,学习如何在云端启动Jupyter Notebook、创建存储空间、配置网络环境以及启动计算实例。


概述

大家好,我是Pamela Reynolds,加州大学戴维斯分校数据科学与信息学实验室的副主任。我们的实验室致力于促进数据科学方法、技术和最佳实践的应用,以增强加州大学戴维斯分校及更广泛社区的研究与学习。我们提供研究服务、培训活动(如今天的研讨会)以及社区建设支持,以提升数据科学能力。

今天的研讨会将介绍使用亚马逊云服务(AWS)进行云计算的基础知识。我们将学习AWS的基础设施和服务,并探讨在该平台上进行研究的适用场景。在贯穿全天的动手实验环节中,我们将启动一个SageMaker上的Jupyter Notebook,并使用API来启动一个应用程序。我们还将讨论如何获取更多学习资源。


AWS简介与安全

上一节我们概述了课程内容,本节中我们来看看AWS的基本概念及其安全模型。

AWS是一个功能广泛的平台,它允许用户无需管理办公室内的物理服务器或计算集群。研究人员可以利用它进行跨平台协作、共享数据和基础设施,使用自动化工具重现研究工作流,并利用其先进的分析和机器学习工具。此外,AWS通常能提供比大多数校园环境更高的安全性。

AWS拥有一个全球网络,包含24个地理区域和多个可用区。一个区域是一组地理位置相近的数据中心集合,而一个可用区则是区域内相互隔离、具备独立基础设施(如不同电网)的数据中心。这种设计确保了高可用性和数据冗余。

AWS提供从基础架构服务到机器学习工具在内的广泛功能。对于初学者,AWS的规模可能令人望而生畏,解决方案架构师的角色就是帮助用户梳理这个过程。

安全责任共担模型

在AWS中,安全是共同责任。AWS负责云本身的安全,包括物理设施、底层计算、存储和网络基础设施。而用户(客户)则负责云内部内容的安全,包括自身的数据、访问控制、在实例上安装的软件以及数据加密。

仅仅使用AWS并不意味着默认安全,用户有责任确保其数据和应用的安全。因此,许多校园都构建了安全研究环境,为校园用户提供额外的安全层。

AWS在不同层级提供服务:

  • 基础设施即服务(IaaS):例如Amazon EC2。AWS管理物理设备和虚拟化层,用户负责操作系统、应用及网络安全配置。
  • 容器服务:例如Amazon ECS/EKS。AWS管理更多的控制平面组件。
  • 无服务器计算:例如AWS Lambda。AWS管理几乎所有底层资源,用户只需关注代码。

使用无服务器组件的一大优势是,用户无需维护和管理不擅长的软硬件,可以将精力集中在研究代码本身。

AWS符合众多联邦和非联邦合规标准。如果研究工作涉及受控非密信息(CUI)、个人身份信息(PII)或HIPAA等规范的数据,AWS提供了相应的支持环境。

用户始终拥有自己的数据主权。数据存储在用户选择的特定地理区域内,不会自动跨境传输。用户可以加密、移动或删除自己的数据。即使是AWS的解决方案架构师也无法访问用户账户内的数据。


基础设施即代码与可重复性

上一节我们介绍了AWS的安全模型,本节中我们来看看如何利用自动化提升研究的可重复性。

可重复性是当前研究领域的重点。通过代码自动化部署基础设施,比手动操作更可靠、更可审计。基础设施即代码的理念允许用户:

  1. 重复部署相同的环境。
  2. 与他人共享整个研究基础设施,使他人能够复现相同的计算或模型。
  3. 在不需要时销毁资源以控制成本,并在需要时随时重建。

动手实验:启动Jupyter Notebook并创建IAM角色

理论部分告一段落,现在让我们开始动手实验。实验将通过您的浏览器完成,您将获得临时账户凭证。

以下是实验的主要步骤:

  1. 创建IAM角色。
  2. 启动Jupyter Notebook实例。
  3. 创建S3存储桶并复制文件。
  4. 创建网络环境(VPC)。
  5. 启动EC2计算实例。
  6. (可选)清理所有资源。

步骤1:访问控制台与创建IAM角色

首先,我们需要了解身份与访问管理(IAM)。IAM是用于控制AWS环境内访问权限的系统。它管理谁可以在什么条件下对什么资源执行什么操作。

在IAM中:

  • 用户:代表个人或应用程序,拥有独立的凭证。
  • 策略:定义允许或拒绝的操作(Action)和资源(Resource)。
  • 角色:一种可被AWS服务(如SageMaker Notebook)代入的身份,允许该服务代表您调用AWS API。

我们需要为即将创建的Jupyter Notebook赋予一个角色,使其有权代表我们执行操作。

以下是操作步骤:

  1. 登录AWS管理控制台。
  2. 在服务搜索栏中输入 IAM 并进入IAM控制台。
  3. 在左侧导航栏点击“角色”,然后点击“创建角色”。
  4. 选择“AWS服务”作为可信实体类型,并在用例中查找并选择 SageMaker
  5. 点击“下一步”,直到权限策略页面。此时可以先不附加策略,点击“下一步”。
  6. 为角色命名(例如 SageMakerAdminRole),然后点击“创建角色”。
  7. 角色创建后,在角色列表中点击其名称进入详情页。
  8. 点击“添加权限” -> “附加策略”。
  9. 搜索并选择 AdministratorAccess(管理员访问)策略,然后点击“附加策略”。

注意:在生产环境中,授予管理员权限可能过于宽松。应根据最小权限原则,只授予必要的权限。本实验为简化操作使用了该策略。


动手实验:启动SageMaker Notebook实例

上一节我们创建了IAM角色,本节我们将使用它来启动一个Jupyter Notebook环境。

AWS提供广泛的机器学习服务栈,其中Amazon SageMaker是一项完全托管的机器学习服务。它涵盖了从数据准备、模型训练、调优到部署的整个流程。今天,我们将使用它的Notebook实例功能,快速获得一个预配置的Jupyter Notebook环境。

以下是操作步骤:

  1. 在AWS控制台服务搜索栏中输入 SageMaker 并进入。
  2. 在左侧导航栏点击“Notebook实例”。
  3. 点击“创建Notebook实例”。
  4. 为实例命名(例如 MyFirstNotebook)。
  5. 在“IAM角色”下拉菜单中,选择刚才创建的 SageMakerAdminRole
  6. 其他设置保持默认,点击页面底部的“创建Notebook实例”。

实例创建需要几分钟时间。状态变为 InService 即表示就绪。

在等待期间,请下载本次实验将要使用的Jupyter Notebook文件。您无需在本地打开它,稍后我们将把它上传到云端Notebook中。


核心服务介绍:Amazon S3 存储

在Notebook实例启动的同时,我们来了解第一个核心AWS服务:Amazon S3。

Amazon S3(简单存储服务) 是一个可扩展的、高耐久性的对象存储服务。它提供11个9(99.999999999%)的数据持久性。

S3的特点包括:

  • 存储类别:提供从频繁访问(标准)到长期归档(如S3 Glacier Deep Archive,每TB每月约1美元)的多级存储选项,用户可根据数据访问模式选择以优化成本。
  • 性能与权限:支持并行读写,性能高。可以通过IAM策略精细控制桶(Bucket)和对象(Object)级别的访问权限。
  • 其他功能:支持数据加密、静态网站托管、生命周期策略(自动将旧数据转移到更便宜的存储层)等。
  • 数据分析:可与 Amazon Athena 服务结合,使用标准SQL直接查询存储在S3中的CSV、JSON等格式的数据,无需加载到数据库中。

对于需要长期保存但很少访问的研究数据,S3 Glacier Deep Archive等归档存储类别是一个极具成本效益的选择。


动手实验:在Notebook中操作AWS服务

现在,我们的SageMaker Notebook实例应该已经准备就绪。

  1. 在SageMaker控制台的“Notebook实例”页面,找到状态为 InService 的实例,点击“打开Jupyter”。
  2. 在打开的JupyterLab界面中,点击“上传”按钮,选择之前下载的笔记本文件(例如 datascience-lab.ipynb)。
  3. 上传完成后,点击该文件打开它。

这个笔记本的目标不是进行复杂的计算,而是演示如何使用Python代码(通过boto3库)调用AWS API来创建和管理云资源。我们将通过运行其中的代码单元来完成实验。

创建S3存储桶

在笔记本中找到创建S3存储桶的代码单元并运行。这段代码会生成一个全局唯一的桶名(因为S3桶名必须在所有AWS账户中唯一),并创建它。

创建虚拟私有云(VPC)

接下来,我们创建网络环境。Amazon VPC(虚拟私有云) 允许您在AWS中预配置一个逻辑隔离的虚拟网络。

在笔记本中运行创建VPC的代码单元。这段代码将:

  • 创建一个VPC。
  • 在指定可用区内创建子网(Subnet)。
  • 配置路由表(Route Table)以引导网络流量。

通过代码创建这些基础设施的好处是可重复和可共享。您可以将这段代码分享给合作者,他们能在自己的账户中一键部署完全相同的网络环境。

创建安全组并启动EC2实例

最后,我们将启动一个虚拟服务器。Amazon EC2(弹性计算云) 是AWS的核心计算服务。

在启动实例前,需要配置安全组,它充当虚拟防火墙,控制进出实例的网络流量。我们将创建允许HTTP(端口80)和HTTPS(端口443)流量的规则。

笔记本中的代码将:

  1. 查找最新的Amazon Linux 2系统镜像(AMI)。
  2. 编写一个用户数据脚本(user_data),用于在实例启动时自动安装一个简单的Web服务器。
  3. 调用EC2 API,使用指定的AMI、实例类型(如 t2.micro)、用户数据脚本、子网和安全组来启动一台实例。
  4. 等待实例进入运行状态,并输出其公共IP地址。

运行这些代码单元后,您就成功在AWS云端部署了一个完整的、可访问的Web服务器。您可以随时在EC2控制台查看和管理运行的实例。

资源清理

实验结束后,运行笔记本末尾的“清理”代码单元非常重要。该代码会删除本次实验创建的所有资源(S3桶、EC2实例、VPC等),避免产生不必要的费用。

重要提示:AWS按实际使用量计费。不使用资源时,请务必将其停止或删除。


基础设施即代码工具对比

上一节我们通过Python脚本完成了资源创建,本节我们来对比不同的基础设施管理方式。

  1. 手动控制台操作:通过Web界面点击完成。简单直观,但难以重复、易出错、效率低。
  2. 脚本化(使用SDK,如boto3):如我们刚才所做的。使用Python等编程语言调用API。灵活、可编程、易于集成到现有工作流中。
  3. 声明式模板(如AWS CloudFormation, Terraform):使用YAML或JSON等模板文件描述您想要的最终资源状态,由引擎负责创建和管理。易于共享和版本控制,但模板语法可能较复杂,且缺乏编程逻辑。
  4. AWS Cloud Development Kit (CDK):使用熟悉的编程语言(Python, TypeScript等)来定义云资源。它提供了高级别的抽象组件,使得像创建VPC这样的复杂操作只需几行代码。CDK在后台会生成CloudFormation模板,结合了编程的灵活性和声明式管理的便利性。

选择哪种工具取决于您的具体需求、团队技能和项目复杂度。


AWS在研究中的适用场景

了解技术细节后,我们来看看AWS在学术研究中的典型应用场景。

  1. 扩展计算容量

    • 当本地电脑、实验室服务器或校园HPC集群的计算能力不足时。
    • 可以快速获取特定配置(如大内存、多GPU)的实例,使计算资源匹配研究任务需求,而非让任务适应现有资源。
    • 可以通过如Slurm等工具,将本地HPC集群无缝扩展到AWS,形成混合云,按需使用海量计算资源。
  2. 使用托管式AI/ML与分析服务

    • Amazon SageMaker:托管整个机器学习生命周期,无需手动搭建和管理集群。
    • AI服务:直接使用预训练的模型,无需机器学习专业知识。例如:
      • Rekognition:图像和视频分析(物体识别、人脸检测)。
      • Transcribe:将音频转换为文本。
      • Textract:从扫描文档或图像中提取文本和结构化数据。
    • 分析服务:如Amazon Athena,可直接用SQL查询S3中的数据文件,快速分析海量半结构化数据。
  3. 数据就近处理

    • 当研究数据已存储在AWS(例如,从公共数据集注册表下载)或合作方使用AWS时,可以将计算任务直接部署到数据所在区域,避免耗时且昂贵的大规模数据传输。

后续学习资源与支持

课程接近尾声,本节将为您提供继续探索AWS的路径和资源。

  • UC Davis Aggie Cloud:由校园IT中心运营,提供集中账单和用校园CAS单点登录,并享有约11%的服务折扣。
  • NIH STRIDES计划:为NIH资助的研究人员提供AWS积分折扣,并专注于构建可共享的大型数据集。
  • NSF CloudBank:为获得NSF云资源分配的研究人员提供资金管理和支持服务。
  • AWS Research & Technical Teams:专门面向研究人员的支持页面,包含快速入门、培训路径和博客。
  • AWS开放数据注册表:托管众多公开可用的数据集,存储和下载均免费。欢迎贡献数据集。
  • AWS Educate:面向学生、教职员工的计划,提供自定进度的学习内容、每年$100的AWS积分以及职业资源。
  • AWS云积分研究计划:如果您有明确的研究项目,可以申请AWS积分资助。
  • 全球数据出口费用豁免:对于符合条件的学术研究,AWS可以豁免数据传出到互联网的费用。
  • 更多动手实验:GitHub上提供了涵盖数据分析、TensorFlow、批处理(HPC)等主题的更多Jupyter Notebook教程。

总结

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

  1. AWS云计算平台的基本概念、全球架构和安全责任共担模型。
  2. 基础设施即代码对于研究可重复性的重要性。
  3. 如何通过IAM角色管理访问权限。
  4. 如何使用Amazon SageMaker快速启动托管的Jupyter Notebook环境。
  5. 核心AWS服务:对象存储服务 Amazon S3、虚拟网络 Amazon VPC、计算服务 Amazon EC2 的基本用途。
  6. 如何通过Python脚本(boto3)自动化创建和管理这些云资源。
  7. AWS在学术研究中的多种适用场景,包括计算扩展、托管AI/ML服务和数据就近处理。
  8. 为UC Davis师生提供的多种AWS学习资源、支持计划和积分申请途径。

希望本教程能帮助您开始利用云计算的强大能力,更高效地开展数据科学研究。

posted @ 2026-03-29 09:28  布客飞龙II  阅读(7)  评论(0)    收藏  举报