UCSD-DSC10-数据科学原理笔记-全-

UCSD DSC10 数据科学原理笔记(全)

1:数据科学导论与 Python 基础 📘

在本节课中,我们将要学习数据科学的基本概念、课程的整体结构以及如何使用 Python 进行最基础的计算。课程将从“什么是数据科学”开始,介绍其核心组成部分,并带你初步体验如何通过编程来探索和分析数据。

🎯 什么是数据科学?

数据科学是一个融合了多个学科的领域。一种常见的定义方式是将其视为三个领域的交集:编程数学与算法以及领域知识

  • 编程:使用计算工具处理和分析数据的能力。
  • 数学与算法:理解数据背后的统计原理和计算方法。
  • 领域知识:将技术应用于特定领域(如生物、金融、政治)的背景知识。

数据科学的定义是:通过计算从数据中得出有用结论。计算能力的提升使得我们能够处理大规模数据并回答复杂问题,这是数据科学作为一个新兴领域兴起的关键。

本课程的教学哲学是“从交集开始”。我们不会先分别深入学习编程、统计和领域知识,而是从一开始就让你接触实际的数据科学项目,在实践中同时学习这些技能。随着课程的深入,你的能力圈会逐渐扩大。

📊 课程结构概览

本课程为期十周,节奏紧凑,旨在让你快速上手。课程结构分为三个主要部分:

  1. 数据探索(第1-4周):学习 Python 编程基础,重点是使用代码来探索和分析数据集。
  2. 统计推断(第5-8周):学习如何从样本数据推断总体特征。我们将主要使用模拟而非公式来建立直观理解。
  3. 预测建模(第9-10周):介绍机器学习的基础,重点是回归这一预测方法。

🛠️ 课程运行与资源

上一节我们介绍了课程的整体框架,本节中我们来看看课程的具体运行方式和可用资源。

所有课程材料、作业和公告都发布在课程网站 dsc10.com 上。网站结构清晰,包含以下关键部分:

  • 主页:列出所有讲座、作业和讨论课链接。
  • 教学大纲:详细说明了课程政策、评分标准和学术诚信要求。
  • 日历:显示所有课程会议、办公时间和截止日期。
  • 资源页:包含补充学习材料、参考手册和往届学生制作的视频。
  • 调试页:针对常见技术问题的解答。

课程的主要交互平台是 Ed 讨论区,所有课程相关问题都应在此提问,而非发送邮件。作业通过 Gradescope 提交和评分。

以下是开学初需要立即完成的任务清单:

  • 加入 Ed 讨论区。
  • 确保能访问 Gradescope。
  • 阅读教学大纲并完成大纲确认测验。
  • 完成欢迎调查(用于收集信息和安排测验时间)。
  • 完成学前测试(评估必要的数学和逻辑基础)。

第一个涉及课程内容的作业是 Lab 0,截止日期为10月3日(周四)。

💻 访问课程材料与工具

我们将使用 Jupyter Notebook 进行编程学习和实践。你可以通过两种方式访问讲座内容:

  1. 点击讲座标题旁的 “代码” 链接,在 DataHub(UCSD 提供的在线平台)中打开一个可交互的 Notebook,你可以跟随讲解并运行代码。
  2. 点击 “预览” 链接,查看讲座的静态 HTML 版本,适合打印或在平板电脑上做笔记。

作业(Labs 和 Homeworks)也以 Jupyter Notebook 的形式发布,点击链接即可在 DataHub 中打开并开始工作。

⚖️ 学术诚信与合作政策

保持学术诚信至关重要。我们的合作政策如下:

  • 鼓励就作业问题进行讨论,但仅限于口头交流思路。
  • 严禁共享代码或查看他人的代码。你不能将自己的代码发给别人,也不能复制他人的代码。
  • 项目可以两人合作完成,但合作意味着共同完成所有部分,而非简单分工。
  • 禁止任何形式的代写或抄袭,包括从网上搜索现成解决方案。

关于人工智能工具(如 ChatGPT)的使用:

  • 允许在作业中谨慎使用,主要用于调试语法错误或理解概念。
  • 禁止让其直接解答作业问题,因为这会阻碍你的学习。
  • 在测验和考试中禁止使用。
  • 请注意,AI 生成的答案可能不准确,应以课程提供的官方资源为主要学习依据。

🆘 寻求帮助与成功策略

本课程富有挑战性且节奏快,但提供了大量支持资源帮助你成功:

  • 办公时间:由教授、研究生助教和本科生辅导员主持。这是一个开放的学习空间,欢迎随时带着问题前来,或只是在那里学习。
  • Ed 讨论区:公开提问一般性问题,私有提问涉及代码或具体解决方案的问题。
  • 丰富的学习资源:包括课程笔记、参考手册、辅导视频、图表库和历年考试练习题。

来自往届学生的成功建议包括:尽早开始作业、预留充足的学习时间、积极参加办公小时、在测验前一日进行复习、与同学组队学习并互相帮助。

🧪 课程演示:《小妇人》数据分析

现在,让我们通过一个简单的演示来感受数据科学的魅力。我们将分析小说《小妇人》的文本数据,而不需要阅读整本书。

首先,我们将小说文本读入 Python。初始的文本格式对计算机友好但不易读。通过 print 命令,我们可以用更人性化的格式查看它。

我们的目标是分析书中主要角色名字在各章节的出现频率。我们通过按“章节”分割文本来创建按章节组织的表格(数据框)。然后,我们统计四位姐妹及其伴侣“劳里”的名字在每一章的出现次数。

仅仅看数字表格难以发现模式,因此我们将其可视化。下图显示了每个角色名字在全书中的累计提及次数:

(此处原为可视化图表描述:横轴为章节,纵轴为累计提及次数,不同角色用不同颜色的线条表示)

通过分析图表,我们可以回答关于书的问题:

  1. 谁是主角? 线条最高的“乔”显然是主角。
  2. 当乔在第32章搬去纽约后,她与哪位姐妹的关系受影响最大? 观察线条的斜率(即名字被提及的速率),“梅格”的线条在乔离开后明显变得平缓,说明她的提及率下降最多。
  3. 劳里最终娶了哪一位姐妹? “艾米”和“劳里”的线条从某一点开始几乎完全同步上升和下降,这表明在书中他们经常被一同提及,暗示了他们最终成为伴侣。

这个演示展示了如何通过计算(统计名字频率)、分析(创建数据表格)和可视化来从数据中提取有意义的结论,这正是数据科学的核心。

🐍 Python 与 Jupyter Notebook 入门

在演示中我们使用了代码,现在让我们正式介绍它们。代码是给计算机的精确指令。Python 是一种流行且易学的编程语言,特别适用于数据科学。

我们将在 Jupyter Notebook 中编写和运行 Python 代码。它的优点是可以将代码、文本说明和可视化结果整合在一个文档中,并能立即看到代码运行的结果。

最基础的 Python 用法是将其当作计算器。你可以输入数学表达式,Python 会计算并返回它的

以下是基本的算术运算符:

  • 加法:+
  • 减法:-
  • 乘法:*
  • 除法:/
  • 求余数(模运算):%
  • 指数运算:**

Python 遵循标准的运算顺序(PEMDAS/BEDMAS)。你可以使用括号 () 来改变运算顺序。

📝 本节总结

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

  1. 数据科学的定义及其多学科交叉的特性。
  2. DSC 10 课程的整体结构、资源和运行方式。
  3. 学术诚信政策以及如何有效获取帮助。
  4. 通过一个文本分析演示,直观感受了数据科学的工作流程:从数据中提出问题、进行计算分析、通过可视化解读结果。
  5. Python 和 Jupyter Notebook 的最基础入门,学会了如何将其用作计算器。

下节课,我们将更深入地学习 Python 表达式和变量。请记得完成本周末截止的欢迎任务,并开始着手 Lab 0。

2:Python 编程基础与数据类型 📘

在本节课中,我们将学习 Python 编程的基础知识,包括如何使用变量存储数据、如何调用函数进行计算,以及理解 Python 中不同的数据类型。这些是进行数据科学分析前必须掌握的核心技能。

🏠 课程网站与资源

如果你错过了第一节课,请务必了解课程网站的位置,因为那里有你所需的一切信息。我们不使用 Canvas,所有内容都在课程网站上,包括教学大纲和从今天开始的办公时间安排。我们有一个庞大的助教团队,全天提供支持。

你可以通过点击 CodeWrite 来获取讲义材料。点击 Code 可以跟随我一起动手实验,而点击 Write 则适合使用平板电脑等设备做笔记。

当你点击 Code 后,可能需要登录,然后选择 DSC 10, Fall 24 并启动。如果你希望以幻灯片模式查看(就像我演示的那样),只需点击那个看起来像条形图的按钮即可。

📢 课程公告

我通常会在每节课开始时宣布一些事项。

今天是周一,有讨论课。本周的讨论课与未来不同,主要是关于 Jupyter Notebook 和基础 Python 的入门介绍,也会有一些小组合作。未来的讨论课将主要是小组合作,练习过去的考试题目。

关于课程表中周三的“实验课”时间,需要澄清一下:这个时间段是为本课程预留的,但我们通常不会使用,除了偶尔的周三测验。本学期大约有四次测验,每次约20分钟,大部分在周三进行(感恩节周除外)。如果你在课程表中有这个时间段且没有其他安排,可以将测验偏好时间选在其中。这样安排是为了确保每个人都有至少一小时的空闲时间用于本课程,避免测验时间冲突。例如,本周三就没有测验。

感恩节那周,测验时间与平时相同,但会改在周一进行,而讨论课则改在周三。如果有人对此有疑问,或者需要修改 Wecome 调查中的偏好,仍然可以操作。

除了昨晚提交的入门任务外,我们的第一项作业是 Lab 0,截止日期是周四。我推荐观看作业末尾的视频,它将帮助你熟悉 Data Hub 和 Jupyter Notebook。

最后,从今天开始,欢迎来参加办公时间。

🔢 回顾与变量引入

上一节课结束时,我们尝试编写一个与给定数学表达式匹配的 Python 表达式。解决方案使用了三对括号,但只有最后一对是必需的。答案是一个漂亮的整数:100。

在 Python 中,使用双星号 ** 表示幂运算。计算平方根的一个技巧是使用 ** 0.5。一个常见的错误是写成 100 ** 1 / 2,这会导致结果为 50,因为根据运算顺序,它会先计算 100 ** 1 得到 100,再除以 2。

今天,我们将学习一些基础的 Python 知识。如果你有编程经验,今天的内容会很简单;如果你是新手,今天会接触到大量信息。本周的前三节课都是入门编程材料。从本周五开始,我们将深入处理数据集,即使是有经验的同学也会发现数据分析方面的新内容,这也是最有趣的部分。

💾 变量与赋值语句

让我们从一个类似实验作业的问题开始:计算一年中的秒数。假设一年有 365 天,计算过程是 60 * 60 * 24 * 365

如果你想在之后用这个数字计算 12 年有多少秒,你可以复制粘贴这个表达式或它的结果,但这很麻烦且容易出错。更好的方法是将需要重复使用的值存储起来

在 Python 中,这通过赋值语句实现。赋值语句的格式是:变量名 = 值。这就像给一个值贴上名字标签。

例如:

zebra = 23 - 14

这行代码会先计算右侧表达式 23 - 14 得到 9,然后将名字 zebra 赋予数字 9。之后,zebra 就代表 9,而无法得知它来自 23 - 14

运行赋值语句的单元格时,通常没有输出,因为它不进行计算,只是改变“名字标签”的归属。

你可以改变变量的值:

a = 1
a = 2

这就像把名字标签 a 从数字 1 移到了数字 2。从此,a 指代 2,而不是 1。Python 不会保留变量的历史值。

你可以创建多个变量指向同一个值:

a = 2
b = 2

现在数字 2 有两个名字:ab。在未来的代码中,你可以用 ab 来代表数字 2。

变量名可以自由选择,但必须满足一些约束条件,并且最好使用有意义的名称。例如,seconds_per_hourchocolate 要好得多。

在 Python 中,等号 = 表示“赋值给”或“得到”,而不是数学中的“等于”。因此,3 = 2 + 1 在 Python 中会导致错误,因为不能给数字 3 赋值。

🔄 变量赋值的特点

你可以在一个代码单元格中定义多个变量,这很方便,因为赋值语句不需要输出。

例如:

uc = 2
sd = 3 + uc

这里,uc 被赋值为 2,然后 sd 被赋值为 3 + uc 即 5。

但赋值语句不是永恒的承诺。如果你之后改变 uc 的值:

uc = 7

sd 的值不会自动更新,它仍然是 5。变量只在被用于赋值语句时才会改变其值。如果你想更新 sd,必须再次执行赋值:

sd = 3 + uc  # 现在 sd 是 10

一个常见的模式是:先执行赋值,然后显示变量的值以确认操作正确。

triton = 10
triton

第二行只是为了让你作为程序员确认一切按计划进行。

🛠️ 使用函数

到目前为止,我们只是用 Python 作为计算器进行计算和存储。接下来,我们将学习如何使用 Python 调用函数

函数接受输入(称为参数)并返回一个输出。在数学中,你写 f(x);在 Python 中,你写 函数名(参数)

Python 有一些内置函数,例如:

  • abs():求绝对值。abs(-23) 返回 23
  • max():求最大值。max(4, -8) 返回 4max 可以接受多个参数,但不能只给一个数字。

函数参数本身可以是表达式,Python 会先计算这些表达式,再将结果传递给函数。这称为嵌套求值。理解代码时,应从内向外计算。

了解函数功能的最佳方式是查阅文档。在 Jupyter Notebook 中,你可以在函数名后加一个问号并运行单元格来查看帮助信息。例如,round? 会显示 round 函数的用法,它可以四舍五入到指定的小数位数。

📦 导入模块

Python 没有内置所有你可能需要的函数。你可以通过导入模块来使用他人编写的、公开可用的函数集合。

例如,math 模块包含许多数学函数。使用前需要导入:

import math

导入后,使用模块中的函数需要用到点标记法模块名.函数名()
例如:

  • math.sqrt(16) 计算 16 的平方根。
  • math.pow(2, 5) 计算 2 的 5 次方。
  • math.log(10) 计算 10 的自然对数。

使用 math. 后按 Tab 键,可以查看 math 模块中所有可用的函数和常量(如 math.pi)。

🧮 数据类型:整数与浮点数

在 Python 中,每个值都以特定的方式存储,这称为其数据类型。对于数字,有两种基本类型:

  • int:整数。
  • float:浮点数,即带小数点的数字。

你可以使用 type() 函数查看任何值的类型。

type(2)    # 输出:int
type(2.0)  # 输出:float

整数运算(加、减、乘、幂)的结果通常是整数,并且可以精确表示非常大的数。浮点数用于表示小数,但有时无法精确表示所有实数(如 0.1 + 0.1 + 0.1 可能不等于 0.3),这是计算机表示无限实数集的固有限制。在本课程中,我们通常不需要担心这种微小的精度误差。

当整数和浮点数混合运算时,结果通常是浮点数。整数除法 (/) 的结果也总是浮点数。

可以使用 int()float() 函数在类型间转换:

int(6.0)   # 输出:6 (转换为整数)
float(5)   # 输出:5.0 (转换为浮点数)
int(6.7)   # 输出:6 (直接截断小数部分,注意这可能不是你想要的结果)

📝 数据类型:字符串

文本数据在 Python 中用字符串表示,类型名为 str。字符串是任意长度的文本片段。

通过使用引号(单引号 ' 或双引号 ")来创建字符串。

type('woof')  # 输出:str

字符串可以包含字母、数字、标点符号和表情符号。数字也可以作为字符串,例如 '1998',但此时它只是文本,不能直接进行数学运算。

字符串支持两种特殊操作:

  1. 拼接:使用加号 + 将字符串连接起来。
    'Hello' + 'World'  # 输出:'HelloWorld'
    'Hello' + ' ' + 'World'  # 输出:'Hello World' (需要手动添加空格)
    
  2. 重复:使用乘号 * 将一个字符串重复多次。
    'Hi' * 3  # 输出:'HiHiHi'
    

字符串有许多有用的方法(即专属于字符串的函数)。调用方法的语法是:字符串.方法名()

  • .upper(): 将字符串转为大写。
  • .title(): 将每个单词的首字母大写。
  • .replace(old, new): 将字符串中的 old 部分替换为 new
    my_string = "My favorite class is DSC 10"
    my_string.replace("favorite", "❤️❤️❤️")
    

内置函数 len() 可以返回字符串的长度(字符数)。

len("Hello")  # 输出:5

可以使用 str() 函数将其他数据类型转换为字符串。但反过来,只有看起来像数字的字符串才能被 int()float() 转换。

str(3)           # 输出:'3'
int('42')        # 输出:42
float('3.14')    # 输出:3.14
int('hello')     # 这会报错

🎯 总结

本节课我们一起学习了 Python 编程的核心基础。我们了解了如何使用变量来存储和引用数据,掌握了调用内置函数和从模块导入函数的方法。最重要的是,我们探讨了三种基本数据类型:用于整数的 int、用于小数的 float 和用于文本的 str,并学习了它们之间的转换和基本操作。

掌握这些概念是后续进行数据操作和分析的基石。下节课我们将开始学习如何处理实际的数据集。

3:数据科学原理 - 第3讲

在本节课中,我们将要学习字符串的进一步操作、数据集的均值与中位数,以及如何在Python中组织数据序列,包括列表、数组和范围。


字符串回顾与数据类型转换

上一节我们介绍了字符串的基本概念。字符串是用于表示文本的数据类型,通过引号(单引号或双引号)来定义。

字符串拥有特殊的功能,称为方法。方法与普通函数的调用方式不同。普通函数使用 function_name(input) 的格式,而字符串方法使用点表示法:string.method_name()。例如,.upper() 方法可以将字符串转换为大写。

Python中有不同的数据类型,如整数(int)、浮点数(float)和字符串(str)。可以使用特定的函数在这些类型之间进行转换。

以下是转换函数:

  • int():将输入转换为整数。
  • float():将输入转换为浮点数。
  • str():将输入转换为字符串。

任何值都可以转换为字符串,但并非所有字符串都能转换为数字。包含字母、表情符号或标点的字符串无法转换为数字,尝试转换会导致错误。

以下是数据类型转换的示例:

str(3)          # 将整数3转换为字符串'3'
float('5.6')    # 将字符串'5.6'转换为浮点数5.6
int('42')       # 将字符串'42'转换为整数42
float('baby panda') # 错误:'baby panda'不是有效的数字
int('4.3')      # 错误:'4.3'不是有效的整数表示

一个可行的间接转换方法是先将字符串转换为浮点数,再转换为整数:

int(float('4.3')) # 结果为4,浮点数转换为整数会截断小数部分

均值与中位数:数据集的中心度量

本节中我们来看看如何用均值和中位数来概括数据集。它们是衡量数据集中趋势的两种方法。

均值(或平均值)的计算方法是将所有数值相加,然后除以数值的个数。公式为:
均值 = (所有数值之和) / (数值个数)

均值具有以下性质:

  • 均值不一定等于数据集中的任何一个实际值。
  • 均值不一定为整数,即使所有数据都是整数。
  • 均值介于数据集的最小值和最大值之间。
  • 均值与原始数据具有相同的单位。

中位数是另一种中心度量。计算中位数时,首先将数据按顺序排列,然后选取位于中间位置的数值。如果数据个数为偶数,则中位数是中间两个数的平均值。

对于同一组数据,均值和中位数可能相同,也可能不同。它们是从不同角度对数据中心的概括。

以下是几个思考练习,请尝试构造简单的数据集(例如两三个数)来满足以下条件:

  1. 两个数据集的均值相同,但中位数不同。
  2. 两个数据集的中位数相同,但均值不同。
  3. 两个数据集的均值和中位数都相同。

这些练习表明,仅凭均值或中位数无法了解数据的全貌。几周后,我们将学习数据的分布,例如使用直方图进行可视化,它能更完整地展示数据的形态。


列表:存储多个值的序列

当处理像一周气温这样的数据集时,为每一天创建一个变量非常繁琐。列表提供了一种在单个变量中存储多个值的方法。

列表是一种有序的值序列。在Python中,使用方括号 [] 创建列表,值之间用逗号分隔。

temperature_list = [68, 73, 70, 74, 68, 72, 74]

列表有长度,表示其中包含多少个值,可以使用 len() 函数获取。

列表的优势在于可以方便地对整个序列进行计算。例如,计算平均气温:

average_temp = sum(temperature_list) / len(temperature_list)

如果数据量很大,这种方法的代码也无需改变,而使用多个变量则需修改代码。

列表的数据类型是 list。列表非常灵活,其元素可以是不同的数据类型,甚至包含其他列表。

mixed_list = [42, 3.14, 'hello', [1, 2, 3]]
len(mixed_list) # 结果为4

然而,列表的灵活性也带来了一个主要缺点:当处理大量数据(如数百万或数十亿条)时,列表的速度会非常慢。


数组:更高效的数据序列

为了解决列表在处理大数据时速度慢的问题,我们使用数组。数组可以看作是列表的更快、更高效的版本。

数组来自 numpy 包(Numerical Python)。我们导入该包并通常使用缩写 np

import numpy as np

使用 np.array() 函数可以将列表转换为数组。

temperature_array = np.array(temperature_list)

也可以直接从数值创建数组:

temperature_array = np.array([68, 73, 70, 74, 68, 72, 74])

注意,如果尝试将包含混合数据类型(如数字和字符串)的列表转换为数组,numpy 会尝试将所有元素转换为同一种类型(通常是字符串)。

与列表不同,数组要求所有元素必须是相同的数据类型。这在实际数据科学应用中通常不是问题,因为数据集中的一列数据(如温度、姓名)通常是同质的。


访问序列元素与广播操作

要访问列表或数组中的单个元素,需要使用方括号 [] 并提供位置索引。关键点在于,Python中的索引从0开始计数。

例如,在 temperature_array 中,索引0对应第一个值68,索引1对应73,依此类推。也可以使用负索引从末尾开始访问,如索引-1对应最后一个值。

temperature_array[2]   # 访问索引为2(第三个)的元素,结果为70
temperature_array[-1]  # 访问最后一个元素,结果为74

数组的一个强大特性是广播。可以对整个数组执行算术运算,该运算会自动应用于数组中的每个元素。

temperature_array + 3          # 每个温度值加3
temperature_array * 1.8 + 32   # 将摄氏温度转换为华氏温度(假设原数据为摄氏度)

需要注意的是,上述运算表达式本身并不会改变原始数组 temperature_array 的值。要更新变量,必须使用赋值语句:

temperature_array = temperature_array * 1.8 + 32

广播也适用于两个相同长度的数组之间的元素级运算(如加、减、乘、除)。

A = np.array([4, 5, -1])
B = np.array([2, 3, 2])
A + B          # 结果为 [6, 8, 1]
A / B          # 结果为 [2.0, 1.666..., -0.5]
A**2 + B**2    # 结果为 [20, 34, 5]

数组还拥有许多有用的方法,使用点表示法调用,例如:

temperature_array.max()   # 最大值
temperature_array.min()   # 最小值
temperature_array.mean()  # 平均值
temperature_array.sum()   # 总和

范围:创建规律序列

有时我们需要创建具有规律模式的数字序列(如1到31的日期)。手动输入每个数字很麻烦,这时可以使用 np.arange() 函数来生成一个等差数组(范围)。

np.arange() 函数最多接受三个参数:起始值、终止值和步长。其中起始值和步长是可选的。

  • np.arange(stop):从0开始,步长为1,生成直到但不包含 stop 的序列。
  • np.arange(start, stop):从 start 开始,步长为1,生成直到但不包含 stop 的序列。
  • np.arange(start, stop, step):从 start 开始,以 step 为步长,生成直到但不包含 stop 的序列。

示例如下:

np.arange(8)             # 结果: [0, 1, 2, 3, 4, 5, 6, 7]
np.arange(5, 10)         # 结果: [5, 6, 7, 8, 9]
np.arange(3, 32, 5)      # 结果: [3, 8, 13, 18, 23, 28]

步长可以是小数或负数,用于生成分数序列或递减序列。


总结

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

  1. 字符串方法的调用方式(点表示法)以及不同数据类型(int, float, str)之间的转换。
  2. 如何使用均值和中位数来概括数据集,并理解它们是两种不同的中心度量。
  3. 如何使用列表在单个变量中存储有序的值序列,并了解其灵活性及在处理大数据时的局限性。
  4. 如何导入 numpy 并使用数组,数组是更高效、要求元素类型一致的数据序列。
  5. 如何通过索引(从0开始)访问序列中的元素,以及如何利用数组的广播特性进行高效的批量运算。
  6. 如何使用 np.arange() 函数快速生成具有规律数字范围的数组。

下一讲,我们将开始学习如何处理表格形式的数据,这将是本课程的核心内容之一。

4:数据科学原理 - 数据框入门与基础操作 📊

在本节课中,我们将开始学习数据科学的核心工具之一:数据框。我们将了解如何加载数据、查看数据、进行计算,以及如何从数据框中提取特定信息。课程将使用一个关于美国各州信息的真实数据集,通过解决“计算各州人口密度”等问题,来掌握数据框的基本操作。


📢 课程公告与提醒

以下是关于课程安排的重要公告。

希望各位同学已经在昨晚提交了实验零。虽然你们有宽限日,但请尽量将它们留到作业或项目中,因为那些任务可能更需要宽限日。

下一个实验已发布,截止日期是一周后的下周四。建议尽早开始完成,原因之一是下周三将有一次测验,而完成实验是准备测验的最佳方式。

我们将在周日发布第一次作业,截止日期是下周日。请尽可能多地完成这些任务,以帮助你们为测验做准备。

测验将在下周三进行。昨天已通过邮件为各位分配了测验部分。如果未收到或找不到邮件,请在 Ed 上给我们发送消息。

测验是 20 分钟的纸质测验。在我们的练习网站 DSC10.com 上,可以找到往年的练习测验或测验题目。网站角落有一个名为“练习”的链接,可以访问预测试、讨论以及往年的考试和测验。通过练习这些往年的测验题目,是准备讨论部分的好方法。

我们也会在课堂上讲解其中一些问题。测验是纸质的,不允许使用电脑或笔记。题目设计时已考虑到这一点,因此不需要进行过于复杂的计算。

但你们需要掌握我们在课堂上介绍的所有函数和方法。测验涵盖前四讲的内容,请复习这些课程,记住例如 absround 以及字符串的 upper 方法的作用。

测验的具体题目数量不会透露,但所有往年的测验都是 20 分钟,可以参考其长度。


📚 课程资源介绍

上一节我们介绍了课程安排和测验信息,本节中我们来看看本课程提供的一些核心学习资源。

以下是几个重要的学习资源,请确保了解。

从 DSC10 主页可以看到,每节课程旁边都有蓝色的关联阅读材料,即“Baby Pandas”课程笔记。

我们称之为课程笔记,但它们更像是一本教科书。它基本上涵盖了我们在课堂上讲解的所有内容,例如什么是表达式、函数、不同的数据类型等。它包含了课程前几周要讲解的大部分内容。这是一个很好的资源,可以从不同角度学习我在课堂上讲解的内容,由他人用文字和新例子进行解释,是学习材料的另一种方式。

此外,还有一份 DSC10 参考手册,链接在顶部“参考”处。这有点像我们在课堂上讲解内容的“小抄”版本。未来几周要教的所有不同的 Baby Pandas 方法和函数都在这里。左侧的菜单可以展开为不同的下拉菜单。在课程后期进行统计推断时,也有类似的菜单项。这个手册旨在快速查阅,例如需要快速查找某个函数的语法和示例时。


🐼 认识数据框与 Baby Pandas

上一节我们介绍了学习资源,现在让我们正式开始学习数据框。

数据框来自一个名为 Pandas 的包。Pandas 是人们在 Python 中用来处理数据的包,这些数据通常以电子表格形式存在,例如 CSV 文件。CSV 代表逗号分隔值。如果在 Excel 或 Google Sheets 中打开此类文件,它会将其分开并以漂亮的网格形式显示,包含列和行。像 Excel 和 Google Sheets 这样的程序有自己处理数据和从中提取信息的函数和方法。Pandas 是 Python 中与之等效的工具。它可以做 Excel 或 Google Sheets 能做的事情,但功能更多、更可定制,因此更通用。

但 Pandas 包本身并不那么“友好”。因此,在 UCSD,我们教授一种称为“Baby Pandas”的工具。Baby Pandas 可以看作是 Pandas 包的一个更小、更友好的版本。Baby Pandas 包中的所有内容也都在更大的 Pandas 包中,但 Baby Pandas 并不包含所有内容,它只包含我们认为需要了解的核心部分,没有许多其他实现方式。Baby Pandas 的一个好处是它有更好的错误信息,因此更容易学习。

但 Baby Pandas 中的所有代码仍然是有效的 Pandas 代码,所以你们学习的是真正的 Pandas 代码,只是没有学习 Pandas 的全部内容,因为它内容太多,对初学者来说可能难以承受。我们的理念是教授核心部分,在这门课程之后,你们将从核心知识中掌握足够的内容,从而可以扩展知识,直到掌握所有 Pandas。

数据框只是一个表格。这是在 Pandas 或 Baby Pandas 中的名称。

要使用数据框或表格,我们必须导入 Pandas 包或我们的 Baby Pandas 包。在使用完整的 Pandas 包时,人们通常将其导入为 pd。我们使用 Baby Pandas 包,因此将其导入为 bpd。这是一个输入快捷方式,这样就不必在需要时输入 babypandas.,而只需输入 bpd.

然后,我们将使用 Baby Pandas 来分析表格中的数据。因此,我们需要找到一些以表格形式存在的数据,并将其保存在文件中。


📂 加载数据与查看数据框

上一节我们介绍了数据框和 Baby Pandas 的概念,本节中我们来看看如何将数据加载到 Python 环境中。

加载 CSV 文件的命令来自 Baby Pandas 包,因此以 bpd. 开头,然后是该包中的函数 read_csv。它的输入是一个字符串,用引号括起来,该字符串指示如何找到 CSV 文件。它是一个路径,即文件相对于笔记本所在位置的“方向”。例如,我的笔记本在 lecture4 文件夹中,该文件夹内有一个 data 文件夹。我告诉它进入 data 文件夹,然后从该文件夹获取 CSV 文件。

这就是加载数据并漂亮地显示它所需的全部操作。这是你看到的第一个数据框。注意底部显示了关于数据框的摘要信息,包括行数和列数。CSV 文件的第一行是标题信息,它以粗体漂亮地显示在顶部。它自动为行添加了一些阴影,交替颜色以便阅读,并自动为每一行编号,从 0 开始。正如我们上次学到的,计算机从 0 开始计数,因此 0 到 49 为每个州分配了一个数字。

让我们了解一下数据。大部分数据不言自明,但有几件事需要知道。对于每个州,人口来自最近的人口普查,每 10 年进行一次,因此这是 2020 年的人口。土地面积以平方英里为单位测量。

“区域”列将每个州划分为这四个区域之一。美国人口普查局确定了这些区域:西部、中西部、东北部、南部。

还有一个名为“党派”的列,表示每个州的政治党派。这并不容易衡量,正如你可能从选举新闻中了解到的那样。这是一个完整的数据科学问题,即将每个州分类为民主党、共和党或可能的中立。一种方法称为库克党派选民指数。你可以点击此链接了解更多关于他们如何为每个州分配颜色的方法。他们有一个基于数据的系统来确定每个州被认为是民主党还是共和党。实际上,在这张地图上,有不同深浅的蓝色和红色。库克党派选民指数实际上是一个量表,衡量你在自由-保守量表上的位置。例如,加利福尼亚州是非常蓝的,因为它非常民主党,而其他州则更红、不那么红或更中立。对于我们的数据,我们只看颜色,是民主党还是共和党,是蓝色还是红色,但我们不看深浅程度。因此,“党派”列只有两个条目:民主党或共和党。

数据框将数据排列在列和行中。将每一列视为一个数组,就像你有一堆所有州的人口值,这就像一个数组,因为它是一堆相同数据类型的值。或者,其中一列是首府城市,这也像一个数组,因为每个人都有一个表示其首府名称的字符串。

列有标签,以粗体显示。将标签本身视为字符串。

行也有标签,显示在左侧。这些标签一起称为数据框的索引,它不被视为一列。例如,这个数据框有 50 行和 6 列,这 6 列就是这些。索引并不真正是一列,它只是标记每一行的一种方式。可以将索引视为行的标签,将列名视为每列的标签。


🔢 计算新列:各州人口密度

现在我们有了数据,我们想开始回答关于数据的问题。我们的第一个问题将是计算不同州的人口密度。在这个过程中,我们将学习一些关于如何与数据框交互的关键概念,这些概念将被反复使用。

问题是:每个州每平方英里的人口密度是多少?

你有计算所需的相关信息,因为有一个“人口”列告诉你每个州有多少人居住,还有一个“土地面积”列告诉你每个州的平方英里数。

本质上,对于每个州,你需要取人口数除以土地面积数。

策略如下:我们将提取整个“人口”列,将其视为一个数组,一个值序列。然后,我们将提取整个“土地面积”列,这也是一个数组。它们长度相同,因为它们对应 50 个州。我们上节课学到,如果在两个长度相同的数组之间进行操作,它会按元素配对。因此,如果将人口数组除以土地面积数组,第一个结果将代表阿拉巴马州的人口除以阿拉巴马州的土地面积,第二个结果将是阿拉斯加州的人口除以阿拉斯加州的土地面积,这正是你需要的所有密度值。

然后,我们将得到一整个密度值序列。但它只是一组数字,州名不会与之关联。因此,我将把这个计算出的数字列添加到数据框中,以便可以读取每个州及其密度。

以下是实际操作的方法。今天要介绍的大部分新 Python 内容都是数据框方法。这些是专门用于数据框的特殊函数。我将使用点表示法访问它们。我的数据框名为 states,我要使用的方法是 get。因此,我将输入 states.get,然后指定列名,它会提取该列。

这是完整的 states 数据框。这是 states.get('Population')。第一个数字大约是 502 万,就是这个数字。列名区分大小写,这意味着如果“Population”以大写 P 开头,我必须用大写 P 请求它,否则它会说没有名为“population”的列。如果你看到这个错误,几乎肯定是你拼写错误或大小写错误。

结果包含所有人口值,但注意左侧也有这些数字,0 到 49。这些来自索引,左侧的粗体数字在你提取列时会被保留。因此,结果并不完全是一个数组,因为当我们有数组时,它只显示所有用逗号分隔的数字。结果类似于数组,但称为 Series。它看起来像一个单列数据框,只有一组数字和索引。Series 基本上是一个带有索引的数组。

这是当我提取“人口”列时得到的 Series。Series 是一种数据类型。当我请求这个提取出的列的类型时,它说它是一个 Baby Panda Series。现在我们知道了几种不同的序列数据类型:列表、数组和 Series。Series 的特殊之处在于它们也有一个与之关联的索引。

同样地,我可以提取“土地面积”。然后我将有两个 Series。但 Series 进行算术运算是按元素进行的,就像数组一样。因此,我们的除法计划仍然有效。

这个数字代表索引 0 对应的人口,即第一个州的人口除以索引 0 对应的土地面积,即第一个州的土地面积。第一个州是阿拉巴马州,阿拉巴马州平均每平方英里 99 人。第二个州是阿拉斯加州,每平方英里约 1 人。加利福尼亚州平均每平方英里 253 人。但看着这些数字,很难通过在心里默唱州名来找出哪个数字对应哪个州。如果它能显示州名和旁边的数字,会更容易。

因此,我们想将其添加到数据框中。将新列分配给数据框的命令是 .assign。同样,这是一个数据框方法,我首先指定要添加内容的数据框名称,然后输入 .assign.assign 需要如下形式的内容:新列的名称,然后是等号,然后是应放入该新列的值。我正在创建一个名为“密度”的列,然后要放入“密度”列的值是我刚刚计算并在上一张幻灯片上显示的值。

有点奇怪的是,列名将是一个字符串,但对于这个特定方法,你不需要用引号括起来。这是一个特例,你不需要在列名周围加引号。一个后果是,你不能在列名中使用空格,它不会喜欢那样。你可以用下划线替换空格,或者如果需要,可以使用新单词的大写。目前,我们保持简单,使用一个单词的标题。

这将把计算出的值的新列添加到我的数据框中,这样我就可以快速查找加利福尼亚州的人口密度是每平方英里 253 人。这解决了我们关于每个州人口密度的问题,现在我已经将计算出的信息添加到我的数据框中。

需要非常小心的是,数据框方法实际上并不改变数据框。就像我们看到的字符串方法一样,我们有一个字符串并执行 .upper(),但字符串本身没有改变。数据框就像字符串和其他变量一样,只有当你实际有意为其赋值时才会改变。因此,你可能觉得刚刚向 states 添加了一列,但 states 实际上仍然只有六列,新列不在那里。这是因为如果我想真正向 states 添加一列,我必须将 assign 的结果保存到变量 states 中。这是作业中常见的错误。人们基本上会做我在这里做的事情。他们会添加一列,然后测试失败。测试会说我们期望有七列,但你的数据框有六列,学生会说,不,它有七列。但你没有实际改变变量。测试的方法是查看变量 states,你会发现变量只有六列,这就是问题所在。一旦我将这里的结果赋值给 states,现在 states 实际上有七列。


📈 数据摘要与排序

上一节我们学习了如何计算并添加新列,本节中我们来看看如何对数据进行摘要统计和排序。

现在我们有了人口密度,让我们了解一下。哪个州的人口密度最高?加利福尼亚州似乎相当拥挤,但它是最拥挤的吗?所有州的平均人口密度是多少?还记得我们学过的数组方法吗,比如数组的 .min().max(),Series 也有类似的方法。它们基本相同,Series 也有 .min().max() 等方法。

如果我想知道哪个州的单州人口密度最高,我要做的是提取“密度”列,这是一个 Series,然后询问该 Series 中的最大值。这是一个方法,注意它使用点表示法,Series 在前,然后是点和方法名,后跟括号和所需的任何输入。这里不需要输入,因为它所做的只是查看该 Series,它拥有所需的一切。

最大值是 1,263 人每平方英里,这比加利福尼亚州拥挤约五倍。这是哪个州?这并没有告诉我答案。这只是对该 Series 中数字的操作,我已经丢失了它对应哪个州的信息。因此,我们需要一些更高级的方法来找出州名,但我们将很快看到如何做到。

以下是你可以计算的其他内容:最小密度。我们已经看到了这个,是阿拉斯加州。所有州的平均密度。中位数密度。平均值和中位数都是某种意义上的典型每平方英里人数。

另一个对于快速理解数据很有用的 Series 方法是 .describe()。它显示你可能想知道的关于数据的一堆信息,如平均值、标准差、最小值、最大值以及各个百分位数,以便了解数字的分布情况。

现在让我们回答我们提出的问题:人口密度最高的州是哪个?有一个州比加利福尼亚州拥挤五倍,我们如何找出是哪个州?策略将涉及对数据进行排序。最初,州是按字母顺序给出的。但我们有一个名为“密度”的列,我们想做的是重新排列行,使它们不是按字母顺序,而是按密度顺序。

执行此操作的方法是 .sort_values.sort_values 接受一个名为 by 的参数,即你想按哪一列排序。我想按“密度”排序值。注意它们按升序或递增顺序排序,人口最少的阿拉斯加州排在前面,顶部是小数字。因此,阿拉斯加州、怀俄明州、蒙大拿州是人口稀少的州,而人口稠密的州在底部。我们可以滚动查看它们是什么。这次比赛的获胜者是新泽西州,那是我长大的地方,很拥挤。

关于数据框的另一件事是,我看不到所有 50 行,中间的行被遮挡,只显示省略号。通常,看到省略号是好的做法,因为你不想通过滚动数据来寻找答案,你希望 Python 为你完成。因此,仅仅想看到答案是很诱人的,但重点是要学习如何从无法看到所有内容的大数据集中提取答案。

关于 .sort_values 需要注意的是,by 是可选的,如果不加它,它也会做同样的事情,但我们通常喜欢加上它,因为它使阅读和理解它在做什么更自然。我正在按密度排序。

我们知道问题的答案是新泽西州,但我们想在代码中提取它,而新泽西州在底部,我们希望它出现在顶部,以便立即看到答案。你可以通过添加一个额外的参数来改变排序顺序,该参数称为 ascending。如果我说 ascending=False,这基本上是说,我们不希望它是升序,我们希望它是降序。因此,我们希望大的在顶部。如果我说 ascending=True,那将按升序或递增顺序排序。

.sort_values 与你可能见过的其他方法有点不同,因为输入有名称。我们不只是说 sort_values('density', True),而是给密度分配名称 by,给 True 分配名称 ascending。这就是我们所说的关键字参数或命名参数。ascending=Trueascending=False,如果你想指定排序顺序,必须使用单词 ascending。不能省略 ascending=,那样不行。因此,省略 by= 是可行的,但省略 ascending= 不行。我们之前看到 ascending 是可选的,如果你不写它,它将按升序排序。但如果你想指定排序顺序,必须使用这个词。你可以将其设置为 TrueFalse,因此将其设置为 True 基本上没什么作用,因为如果你根本不写这个,也会发生这种情况,所以你通常不会看到 ascending=True,因为反正是默认的,何必写出来。但如果你想改变它,那么你必须说 ascending=False

我将这个排序后的结果保存在一个名为 ordered_states 的变量中。现在 ordered_states 看起来是这样。我的问题是哪个州的人口密度最高,我现在可以通过查看看到答案是新泽西州,但如何让 Python 提取它,以便将来可以使用它?方法如下:我们必须提取包含所需数据的整个数据列。在我们的例子中,如果我希望答案是“新泽西州”,新泽西州在“州”列中。因此,我必须提取“州”列,我用 .get('State') 来做到。然后这里有很多州,我只想要其中一个。我必须指定我想要哪个州。我指定想要哪个州的方式是通过计数。新泽西州是第一个,因此我们称之为州编号 0。罗德岛州是下一个,因此将是州编号 1,马萨诸塞州是 2。我们总是从 0 开始计数。有一种从 Series 访问单个元素的方法,称为 .iloc。然后你给出你想要的行的位置。我想要位置 0 的行,那就是新泽西州。如果我想要人口密度第二高的州,我会做 `.iloc[

5:数据框查询与分组 🧮

在本节课中,我们将学习两种强大的数据框操作技术:查询分组。我们将继续使用美国各州的数据集,通过编写代码来回答更复杂的问题。


概述

上一节课我们学习了多种数据框操作方法。本节课中,我们将重点学习如何从数据框中筛选出特定的行(查询),以及如何根据某一列的类别对数据进行汇总分析(分组)。这些是数据分析中的核心技能。


查询数据框:筛选特定行

查询(或称过滤)是指根据特定条件,从一个较大的数据集中筛选出符合条件的行。例如,我们想找出所有位于“西部”地区的州。

布尔值与比较运算

在Python中,比较两个值的结果是 True(真)或 False(假)。这种数据类型称为 布尔型

  • 比较单个值5 == 6 的结果是 False
  • 比较序列与值:我们可以将整个数据列与一个值进行比较,Python会对序列中的每个元素逐一进行比较,这个过程称为“广播”。

以下是获取“西部”地区各州的步骤:

  1. 获取“地区”列:states.get(‘region’)
  2. 将该列与字符串 ’West’ 进行比较:states.get(‘region’) == ‘West’
  3. 上述操作会生成一个由 TrueFalse 组成的序列,其中 True 对应地区为“西部”的行。

执行查询

得到布尔序列后,我们可以用它来查询数据框。语法是:在数据框名称后加上方括号 [],并在括号内放入布尔序列。

# 查询所有位于“西部”地区的州
is_west = states.get(‘region’) == ‘West’
western_states = states[is_west]

方括号内的布尔序列就像一个过滤器:只保留对应值为 True 的行,过滤掉对应值为 False 的行。

获取数据框的形状

查询后,我们常需要知道结果有多少行。可以使用 .shape 属性。

# 获取数据框的行数和列数
western_states.shape
# 输出类似 (13, 7),表示13行,7列

# 仅获取行数
western_states.shape[0]

.shape 是一个属性,访问时不需要加括号。


组合多个条件进行查询

有时我们需要同时满足多个条件。例如,找出所有“南部”的“共和党”州。

我们可以使用逻辑运算符组合多个布尔序列:

  • & (与):两个条件必须同时为真。
  • | (或):至少一个条件为真。

注意:每个条件需要用括号括起来。

# 找出南部且为共和党的州
is_south = states.get(‘region’) == ‘South’
is_rep = states.get(‘party’) == ‘Republican’
southern_rep_states = states[is_south & is_rep]

分组与聚合:按类别汇总数据

分组是一种强大的操作,它允许我们根据某一列的值(如“地区”)将数据分成若干组,然后对每组内的数据进行汇总计算(如求和、求平均)。

理解分组过程

假设我们有一个宠物数据框 pets,包含物种、颜色、体重和年龄。

# 按物种分组,并计算每组的平均体重和年龄
pets.groupby(‘species’).mean()

这个过程分为两步:

  1. 分组groupby(‘species’) 将数据框按“物种”列的不同值(猫、狗、仓鼠)拆分成多个子数据框。
  2. 聚合.mean() 对每个子数据框中的数值列(体重、年龄)分别计算平均值。

结果是一个新的数据框,索引是分组的类别(物种),列是各数值列聚合后的结果。非数值列(如颜色)会被自动忽略。

常用的聚合方法

除了 .mean(),还有其他聚合方法:

  • .sum():求和
  • .max():最大值
  • .min():最小值
  • .median():中位数
  • .count():计数(每组有多少行)

应用于州数据:找出人口最多的地区

现在,让我们用分组来解决“哪个地区人口最多”的问题。

# 按地区分组,并计算每个地区的总人口
region_pop = states.groupby(‘region’).get(‘population’).sum()

# 按总人口降序排序,并获取排名第一的地区
most_populous_region = region_pop.sort_values(ascending=False).index[0]

代码解释

  1. states.groupby(‘region’):按“地区”分组。
  2. .get(‘population’):只选取“人口”这一列进行后续操作。
  3. .sum():对每个地区组内的人口求和。
  4. .sort_values(ascending=False):将求和结果按降序排列。
  5. .index[0]:获取排序后索引(即地区名称)的第一个值,即人口最多的地区。

总结

本节课我们一起学习了数据框的两个核心操作:

  1. 查询:我们学会了如何使用布尔比较创建条件,并通过 df[boolean_series] 的语法筛选出满足条件的行。我们还学习了如何使用 &| 运算符组合多个条件。
  2. 分组:我们掌握了 groupby() 方法,它能够根据某一列的值对数据进行分组,然后结合 .sum(), .mean() 等聚合方法,高效地对每个组进行统计分析。这使我们能轻松回答诸如“每个地区的总人口是多少”或“哪个类别平均值最高”等问题。

掌握查询和分组,你将能够从数据中提取出更具洞察力的信息,这是进行数据探索和分析的基础。

6:数据框操作与数据可视化 📊

在本节课中,我们将学习如何调整数据框的列,并介绍数据可视化的基本概念和几种核心图表类型。我们将使用babypandas库来实践这些操作。


🛠️ 数据框列操作

上一节我们介绍了数据分组和聚合。有时,聚合操作会产生冗余或命名不佳的列。本节中,我们来看看如何调整数据框的列名、顺序和数量。

以下是调整数据框列的三个核心方法:

  • .assign():用于添加新列或重新分配现有列的值。
  • .get():用于选择并保留特定的列,返回一个仅包含这些列的新数据框。当传入一个列名列表时,返回的是数据框;传入单个列名字符串时,返回的是序列。
  • .drop():用于删除指定的列。

例如,我们有一个按地区分组并计数后的数据框states_by_region,其列名不具描述性且内容冗余。我们希望将其调整为仅包含一列名为count的数据框,用于显示每个地区的州数量。

# 步骤1:添加新列‘count’,其值来自任意一个冗余列(例如‘party’)
states_by_region = states_by_region.assign(count=states_by_region.get('party'))

# 步骤2:使用.get()方法仅保留‘count’列,并得到一个整洁的数据框
states_by_region = states_by_region.get(['count'])

通过组合使用.assign().get().drop()方法,我们可以有效地重命名、重新排序和筛选数据框的列。


📈 数据可视化简介

数据可视化通过图形呈现数据,能帮助人们更直观、快速地理解信息。我们根据变量的类型(数值型或分类型)来选择不同的可视化图表。

变量分为两种类型:

  • 数值型变量:其值可以进行算术运算(例如:薪资、利润)。
  • 分类型变量:其值属于不同的类别,类别间可能有顺序(例如:电影类型、邮政编码)。即使类别用数字表示(如邮政编码),只要不用于算术比较,就属于分类型。

本节课我们将介绍三种图表:

  1. 散点图:用于展示两个数值型变量之间的关系。
  2. 折线图:通常用于展示一个序列型变量(如时间)与一个数值型变量之间的关系。
  3. 条形图:用于展示一个分类型变量和一个数值型变量之间的关系。

🔵 散点图

散点图适用于探索两个数值型变量之间是否存在关系。

我们将使用系外行星数据集。例如,我们想探究行星的距离星等(亮度,数值越小越亮)之间是否存在关联。

以下是创建散点图的代码:

# 绘制距离(x轴)与星等(y轴)的散点图
exo.plot(kind='scatter', x='distance', y='magnitude')

如果数据点过于分散,我们可以先通过查询筛选数据,再绘制图表,以便更清晰地观察主要数据区域的关系。


📉 折线图

折线图常用于展示数据随时间或其他序列变化的趋势。

例如,我们想了解新发现系外行星的平均星等如何随时间变化。由于原始数据中每年有多个发现,我们需要先按年份分组并计算平均星等,得到一个每年一行的数据框。

以下是创建折线图的代码:

# 按‘year’分组并计算均值
yearly_avg = exo.groupby('year').mean()

# 绘制折线图:x轴默认使用索引(年份),y轴使用‘magnitude’列
yearly_avg.plot(kind='line', y='magnitude')

折线图清晰地显示,随着时间的推移,我们发现的行星平均星等变高(即亮度变低),这表明我们的探测技术不断进步,能够发现更暗的天体。


📊 条形图

条形图用于比较不同类别在某个数值指标上的差异。

例如,我们想比较不同类型系外行星的平均半径。首先,我们按行星类型分组并计算平均半径。

以下是创建水平条形图的代码(水平条形图通常更便于阅读标签):

# 按‘type’分组并计算均值
types = exo.groupby('type').mean()

# 绘制水平条形图:x轴为数值(平均半径),y轴为类别(行星类型)
types.plot(kind='barh', x='type', y='radius')

在条形图中,条形的长度代表数值大小,条形的顺序本身通常没有意义,但按数值大小排序可以使图表更易于解读。

注意:对于水平条形图,x参数对应的是数值列y参数对应的是类别列,这与图表上坐标轴的直观显示是相反的,这是由barh(水平)属性决定的。


🎨 叠加图表

我们可以在同一坐标系中叠加绘制多个数据系列。

例如,我们想在同一个条形图中同时展示每种行星类型的平均半径和平均星等。我们首先创建一个只包含所需列的数据框,然后进行绘制。

# 从分组数据中仅选择‘radius’和‘magnitude’列
to_plot = types.get(['radius', 'magnitude'])

# 绘制图表。不指定y参数,将使用数据框中的所有列
to_plot.plot(kind='barh')

通过这种方式,我们可以在一张图上直观地比较多个指标在不同类别上的表现。


🎯 课程总结

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

  1. 使用.assign().get().drop()方法调整数据框的列。
  2. 理解了数值型变量和分类型变量的区别。
  3. 掌握了三种基本图表的用途和绘制方法:
    • 散点图kind='scatter')用于两个数值变量。
    • 折线图kind='line')用于展示序列趋势。
    • 条形图kind='barh')用于分类变量与数值变量的比较。
  4. 学会了如何通过筛选数据和选择特定列来创建更清晰、更有针对性的图表。

这些技能是进行数据探索和有效传达数据见解的基础。

7:直方图与数据分布 📊

📖 概述

在本节课中,我们将学习一种新的数据可视化方法——直方图。我们将了解它与之前学过的条形图的区别,学习如何创建和解读直方图,并理解其核心概念“密度”和“面积”的含义。


🔍 回顾与引入

上一节我们介绍了涉及数据框中两列数据的可视化方法:散点图、折线图和条形图。本节中,我们将探讨第四种可视化方法——直方图。直方图仅涉及数据框中的一列数据,用于展示一个变量的分布情况。

今天的课程是迄今为止理论性最强的一节,重点不在于学习代码操作,而在于理解直方图所代表的含义以及如何基于它进行计算。

我们将继续使用上节课的NASA系外行星数据集,尝试回答一个问题:一个变量的分布是怎样的?


📊 理解“分布”

当我们谈论一个变量(即数据框中的单列数据)的分布时,指的是该列中所有可能的值以及它们出现的频率。

分布可以是数值型变量的(例如掷骰子的点数),也可以是分类型变量的(例如班级中学生的年级)。了解分布有助于回答诸如“哪些值常见,哪些值不常见”这类问题。

我们将从分类型变量开始,查看行星的“类型”分布。


📈 分类型变量的分布:条形图

以下是查看行星类型分布的方法。我们需要按类型分组并计数,以了解每种类型有多少颗行星。

# 按‘type’列分组并计数,结果是一个名为type_counts的数据框
type_counts = planets.groupby(‘type’).count()

得到的type_counts数据框会告诉我们有多少颗气态巨行星、类海王星行星、超级地球和类地行星。

为了直观展示每种类型的常见程度,我们可以绘制条形图。绘图时,我们可以使用数据框中的任意数值列(如distancemagnitude),因为计数结果在各列是相同的。通常选择最短的列名以简化代码。

在绘图代码中,我们使用了一些参数:

  • kind=‘bar’:指定绘制条形图。
  • y=‘distance’:指定使用哪一列作为条形的高度(数值)。
  • title:为图表添加标题。
  • legend=False:在此处隐藏图例,因为列名(如year)没有实际意义,显示图例会误导观众。
  • figsize:控制图表输出尺寸的参数。

🪐 聚焦类地行星

由于类地行星较为稀有,且平均半径最小,我们将创建一个仅包含类地行星的较小数据框进行后续分析。

# 筛选出类型为‘Terrestrial’的行星
terr = planets.query(‘type == “Terrestrial”’)

现在我们有了一个包含193行数据的子集。接下来,我们将尝试查看一个数值列(如radius)的分布。

对于数值列,我们可以使用序列的.describe()方法获取一些统计信息(如数量、平均值、标准差、百分位数等),但这并非全部信息。可视化通常能提供更直观的理解。


❓ 为何条形图不适用于数值型变量?

对于radius这样的数值型变量,条形图并不是合适的工具。让我们尝试一下,看看会出现什么问题。

首先,我们需要按相同的半径值分组并计数:

# 按‘radius’分组计数,并重命名列以便理解
terr_radius_counts = terr.groupby(‘radius’).count()
terr_radius_counts = terr_radius_counts.rename(columns={‘distance’: ‘count’})
terr_radius_counts = terr_radius_counts.get([‘count’])

绘制出的条形图会显示许多非常接近的数值(如0.89和0.9)各自拥有独立的条形。这带来两个主要问题:

  1. 舍入误差:这些数值可能来自不同的舍入精度,实际值可能非常接近,却被分到不同组。
  2. 缺乏比例感:条形在x轴上的位置仅代表类别顺序,而非数值大小。例如,代表半径1.0和0.997的条形紧挨着,而代表1.8的条形也紧邻1.0。在数轴上,0.997和1.0非常接近,而1.8则远大于1.0,但条形图无法直观体现这种数值关系。

因此,我们需要一种能将相近数值分组,并且x轴能体现数值大小和比例的可视化方法。


📉 数值型变量的分布:密度直方图

用于展示数值型变量分布的可视化工具是密度直方图。它解决了条形图面临的问题。

直方图通过“分箱”过程创建。分箱是指将整个数值范围划分为连续的区间(箱),然后统计每个区间内数据点的数量。

在Python中,我们使用DataFrame.plot方法并指定参数来绘制密度直方图:

terr.plot(y=‘radius’, kind=‘hist’, density=True, ec=‘w’, title=‘Distribution of Radii of Terrestrial Exoplanets’)
  • kind=‘hist’:指定绘制直方图。
  • y=‘radius’:指定要分析的数据列。
  • density=True:关键参数,表示绘制密度直方图(稍后解释含义)。
  • ec=‘w’:为条形添加白色边框,使其更易区分。
  • 与条形图不同,直方图没有x参数,因为它只分析单列数据。

🧮 理解直方图的分箱

分箱的方式可以调整,这会影响直方图的呈现细节:

  • 默认分箱:如果不指定,pandas会创建大约10个等宽的箱。
  • 指定箱数:通过bins=20等参数,可以增加或减少箱的数量,从而获得更精细或更粗略的分布图。
  • 自定义箱边界:可以精确指定每个箱的起点和终点,甚至可以让箱的宽度不同。

使用np.arange函数可以方便地生成等间距的分箱边界。需要注意的是,在Python中,区间通常是“左闭右开”的(最后一个箱除外)。

改变分箱方式会改变直方图的形状和条形的高度,但分布的整体模式(例如峰值位置)应保持一致。


💡 密度直方图的核心:面积代表比例

密度直方图最核心、最重要的概念是:图中所有条形的总面积之和为1

这意味着:

  • y轴上的“高度”值本身不易直接解释,它被称为“密度”。
  • 每个条形所代表的面积(宽度 × 高度)才有明确意义:它表示落在该箱内的数据点占总数据点的比例
  • 例如,一个条形面积为0.2,就意味着20%的数据位于该箱对应的数值范围内。

因此,当我们想了解“半径在0.5到0.75之间的行星占多大比例”时,我们只需要计算对应条形的面积即可。

公式比例 = 条形面积 = 箱宽 × 条形高度

这个性质使得密度直方图在概率统计中非常有用,因为总面积1对应着100%的概率。


🔁 对比总结:条形图 vs. 直方图

本节课我们对比了两种相似但用途迥异的图表:

特征 条形图 直方图
适用变量 分类型变量 数值型变量
数据需求 通常需要两列(类别和数值) 仅需一列数值数据
X轴 代表不同类别,无顺序意义 代表数值范围,有连续性和比例意义
核心解读 条形的长度代表数量或数值大小 条形的面积代表数据在该区间的比例
关键性质 条形的长度直观易读 所有条形总面积恒为1

在本课程中,我们将始终使用密度直方图,因为它具有“总面积和为1”这一在概率推断中非常宝贵的特性。


🎯 课程总结

本节课我们一起学习了:

  1. 分布的概念:一个变量所有可能值及其出现频率的展现。
  2. 分类型变量分布:使用条形图进行可视化,通过分组计数实现。
  3. 数值型变量分布:使用密度直方图进行可视化,通过“分箱”过程实现。
  4. 密度直方图的精髓:理解其面积代表比例,且总面积恒为1。这是解读直方图的关键。
  5. 两者的根本区别:认识到条形图与直方图在适用场景、构建方式和解读方法上的不同。

通过掌握直方图,你现在拥有了描述和探索数值数据分布的有力工具。

8:自定义函数与数据框应用 📘

在本节课中,我们将学习如何编写自己的函数,以及如何将这些函数应用到数据框(DataFrame)的列上,以处理和转换数据。我们将通过一个有趣的案例——分析课程中所有学生的名字——来实践这些概念。


🧮 第一部分:编写自定义函数

我们已经使用过许多Python内置函数(如 maxlen)以及来自包(如 numpymath)的函数。我们还使用过方法,这是为特定对象设计的特殊类型函数。现在,我们将学习如何创建自己的函数。

为什么需要自定义函数?

考虑一个数学问题:你开车去洛杉矶的一家餐厅,距离100英里。前50英里你以80英里/小时的速度行驶,后50英里以60英里/小时的速度行驶。你的全程平均速度是多少?

直觉上,你可能会认为平均速度是70英里/小时(即80和60的平均值)。但这是不正确的。平均速度的计算公式是总距离除以总时间。

总距离是100英里。总时间需要分段计算:第一段的时间是 50 / 80 小时,第二段的时间是 50 / 60 小时。因此,总时间为 (50/80) + (50/60) 小时。

平均速度公式为:

\[\text{平均速度} = \frac{100}{\frac{50}{80} + \frac{50}{60}} \]

简化后,公式变为:

\[\text{平均速度} = \frac{2}{\frac{1}{80} + \frac{1}{60}} \]

这个公式可以推广到任何两个正数 ab,计算它们的调和平均数(Harmonic Mean):

\[\text{调和平均数} = \frac{2}{\frac{1}{a} + \frac{1}{b}} \]

我们不想每次计算时都复制粘贴这个公式,因此我们创建一个函数来完成这个任务。

如何定义函数

在Python中,我们使用 def 关键字来定义函数。函数定义包括函数名、参数(输入)和函数体(执行的操作)。

以下是计算两个数调和平均数的函数定义:

def harmonic_mean(a, b):
    """返回两个正数 a 和 b 的调和平均数。"""
    return 2 / (1/a + 1/b)

函数定义解析:

  • def: 定义函数的关键字。
  • harmonic_mean: 函数名。
  • (a, b): 函数的参数(输入)。
  • :: 冒号表示函数头结束。
  • """...""": 文档字符串,描述函数功能(可选但推荐)。
  • return: 关键字,指定函数的输出。

定义函数后,我们可以像使用内置函数一样调用它:

print(harmonic_mean(80, 60))  # 输出约 68.57
print(harmonic_mean(20, 40))  # 输出约 26.67

函数术语

  • 参数(Parameters): 函数定义中声明的变量(如 ab),代表通用的输入。
  • 实参(Arguments): 调用函数时传递给参数的具体值(如 8060)。
  • 函数体(Body): 缩进的代码块,定义了函数执行的操作。
  • 返回值(Return Value): return 语句后面的表达式结果,是函数的输出。

作用域(Scope)

函数内部定义的变量(包括参数)是局部变量,只在函数执行期间存在。函数外部无法访问它们。即使外部有同名变量,函数内部使用的也是传递给它的实参值。

x = 15  # 全局变量 x

def triple(x):  # 参数 x 是局部变量
    return 3 * x

print(triple(12))  # 输出 36,使用局部变量 x (值为12)
print(x)           # 输出 15,全局变量 x 未改变

无参数函数与常见错误

函数可以有零个、一个或多个参数。

def greeting():
    """一个简单的问候函数。"""
    return "Hi! 👋"

print(greeting())  # 输出: Hi! 👋

一个常见的混淆点是 printreturn 的区别。print 只是在屏幕上显示信息,而 return 则是将值从函数中传递出来,以便后续使用(例如赋值给变量)。

def pythagorean_print(a, b):
    """计算斜边并打印,但不返回值。"""
    c = (a**2 + b**2)**0.5
    print(c)

result = pythagorean_print(3, 4)  # 屏幕会打印 5.0
print(result)                     # 输出: None,因为函数没有返回值

def pythagorean_return(a, b):
    """计算斜边并返回值。"""
    c = (a**2 + b**2)**0.5
    return c

result = pythagorean_return(3, 4)  # 屏幕不打印
print(result)                      # 输出: 5.0
print(result + 10)                 # 输出: 15.0

关键点return 语句会立即结束函数的执行,其后的代码不会运行。


🐼 第二部分:将函数应用于数据框

上一节我们介绍了如何编写自定义函数。本节中,我们来看看如何将这些函数应用到Pandas数据框的列上,以实现高效的数据处理。

我们将使用一个包含本课程所有学生姓名(为保护隐私,姓氏已替换为随机字符串)的数据集。我们的目标是找出班级中最常见的名字。

问题拆解

我们的数据框 roster 有一个 "name" 列,包含全名(如 "Jacob Ercuga")。要按名字分组统计,我们首先需要从全名中提取出名字(First Name)。

我们已经知道如何为单个名字实现这个功能:

def first_name(full_name):
    """从全名中提取名字(第一个空格前的部分)。"""
    return full_name.split(" ")[0]

# 测试函数
print(first_name("Pradeep Khosla"))  # 输出: Pradeep
print(first_name("Barack Obama"))    # 输出: Barack

但我们希望对数据框中的每一行都应用这个函数。手动操作442次是不现实的。

使用 .apply() 方法

Pandas Series 有一个强大的 .apply() 方法,它可以将一个函数应用到该Series的每一个元素上。

# 从数据框中获取“name”列(这是一个Series)
name_series = roster.get("name")

# 将 first_name 函数应用到该Series的每个元素
first_names_series = name_series.apply(first_name)

# 查看结果(前几个)
print(first_names_series.head())

.apply() 方法接收函数名作为参数(注意不是函数调用,所以不加括号 ())。它会自动遍历整个Series,将每个元素(全名)作为输入传递给 first_name 函数,并收集所有输出,形成一个新的Series。

现在,我们可以将这个新的Series作为新列添加到原始数据框中:

roster = roster.assign(first_name = first_names_series)

分析名字分布

有了 first_name 列,我们现在可以轻松找出最常见的名字:

# 按 first_name 分组并计数
name_counts = roster.groupby("first_name").count()
# 按计数降序排序
name_counts = name_counts.sort_values("name", ascending=False)
print(name_counts.head())

结果显示,“Ryan”是班上最常见的名字,有9位同学叫Ryan。

实践活动:可视化共享名字

创建一个条形图,展示被多人共享的名字(即出现次数大于1的名字)及其出现频率。然后,计算班上拥有“名字伙伴”(共享名字)的学生比例。

以下是实现步骤:

  1. 筛选重复名字:从 name_counts 中筛选出计数大于1的行。
  2. 创建条形图:使用筛选后的数据框创建水平条形图。
  3. 计算比例:统计共享名字的总人数,除以班级总人数。
# 1. 筛选重复名字
shared_names = name_counts.query("name >= 2")

# 2. 创建条形图 (为使图表更美观,先按计数排序)
shared_names_sorted = shared_names.sort_values("name")
shared_names_sorted.plot(kind="barh", figsize=(10, 8))
plt.xlabel("Number of Students")
plt.title("Shared First Names in Class")
plt.show()

# 3. 计算拥有共享名字的学生比例
total_shared_students = shared_names.get("name").sum()
total_students = roster.shape[0]
proportion_shared = total_shared_students / total_students
print(f"拥有共享名字的学生比例: {proportion_shared:.1%}")

更复杂的应用:查找同课同名者

现在,我们提出一个更具体的问题:对于每位学生,其所在的小节(section)中,有多少人与他同名?

策略如下(以学生"Jacob Ercuga"为例):

  1. 找出 Jacob Ercuga 所在的小节。
  2. 在整个花名册中,查询 first_name == "Jacob"section == Jacob的小节 的所有行。
  3. 统计行数。

我们可以将这个逻辑封装成一个函数,然后使用 .apply() 为每位学生计算。

def shared_in_section(full_name):
    """返回与给定全名学生在同一小节中具有相同名字的人数。"""
    # 1. 找到该学生的行,获取其名字和小节
    student_row = roster.query("name == @full_name")
    stu_first_name = student_row.get("first_name").iloc[0]
    stu_section = student_row.get("section").iloc[0]

    # 2. 查询同小节同名的所有学生
    same_name_section = roster.query("first_name == @stu_first_name and section == @stu_section")
    # 3. 返回人数
    return same_name_section.shape[0]

# 测试函数
print(shared_in_section("Jacob Ercuga"))  # 输出可能是 3
print(shared_in_section("Runong Zpvmwy")) # 输出可能是 1

# 为数据框的每一行应用此函数,创建新列
roster = roster.assign(shared_in_my_section = roster.get("name").apply(shared_in_section))
print(roster.head())

新列 shared_in_my_section 中的数字表示每位学生在自己所在的小节中有多少位同名者。大多数值是1(表示没有同名伙伴),大于1的值则表明有同名伙伴在同一小节。


📝 总结

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

  1. 自定义函数:使用 def 关键字定义函数,理解参数、返回值和作用域的概念。
  2. .apply() 方法:这是将自定义函数高效应用于Pandas Series(数据框列)每个元素的强大工具。
  3. 数据操作流程:通过编写解决特定问题的函数,并结合 .apply() 和分组聚合,我们可以从原始数据中提取有意义的洞察,例如分析名字的流行度或查找特定群体。

这些技能是数据科学工作流的核心,它们使你能够将复杂的数据处理任务分解为可重用、可读性高的代码块。

9:多列分组与数据合并 📊

在本节课中,我们将学习两种更高级的数据框操作技术:基于多列进行分组以及合并不同的数据框。我们将继续使用上节课的学生名册数据,并引入新的数据集来练习这些技巧。


回顾:提取名字与单列分组

上一节我们介绍了如何从全名中提取名字,并使用groupby进行单列分组。让我们快速回顾一下。

我们首先定义了一个函数来提取单个全名中的名字:

def first_name(full_name):
    return full_name.split()[0]

然后,我们将这个函数应用到数据框的name列,为每个人创建了一个新的first名字列:

roster['first'] = roster.get('name').apply(first_name)

接着,我们按first名字列分组,并统计了每个名字出现的次数,以找出最常见的名字:

name_counts = roster.groupby('first').count()
name_counts_sorted = name_counts.sort_values('name', ascending=False)

我们发现“Ryan”是最常见的名字,共有9个。如果我们想进一步了解这9个Ryan是如何分布在各个讲座部分的,我们可以先筛选出所有Ryan,然后按section分组计数。


多列分组 🔄

但是,如果我们想一次性了解所有名字在各个部分的分布情况呢?例如,对于每一个名字,有多少人在9AM部分,多少人在10AM部分?

目前的方法无法直接回答这个问题。单按first名字分组,会把所有同名的人放在一起,但不会区分他们来自哪个部分。单按section分组,则不会区分组内的名字是否相同。

解决方案是:基于多列进行分组。我们希望分组依据是first名字和section部分的组合。例如,所有在10AM部分的“Ryan”属于一组,所有在1PM部分的“Ryan”属于另一组。

以下是具体语法。我们向groupby方法传入一个列名列表,而不是单个列名:

counts = roster.groupby(['section', 'first']).count()

这行代码的意思是:首先按section分组,然后在每个section组内,再按first名字进行子分组。对于每个(section, first)组合,它会统计有多少行数据(即多少名学生)。

执行后,结果数据框的索引会变成由sectionfirst两列组成的多重索引。为了便于处理,我们通常使用.reset_index()将其恢复为普通列:

counts_reset = counts.reset_index()

关于分组顺序:分组['section', 'first']与分组['first', 'section']在逻辑上是等价的,都会创建相同的分组集合(即所有(section, first)组合)。区别在于结果数据框中行和列的显示顺序会不同。前者先按section排序,后者先按first名字排序。


练习:找出拥有最多Kevin的部分

假设我们已经有了按sectionfirst分组计数的数据框counts,目标是找出哪个讲座部分拥有最多的“Kevin”。

以下是解决步骤:

  1. counts中筛选出first名字为“Kevin”的行。
  2. 按计数列(通常是name)降序排列。
  3. 取排序后第一行的section值。

# 步骤1:筛选
kevin_rows = counts[counts.get('first') == 'Kevin']
# 步骤2:排序
kevin_sorted = kevin_rows.sort_values('name', ascending=False)
# 步骤3:获取结果
section_with_most_kevins = kevin_sorted.get('section').iloc[0]


应用:海洋温度数据分析 🌡️

现在,让我们将多列分组技术应用到一个新的数据集:拉霍亚斯克里普斯码头自1916年至今的每日海面温度数据。

数据框ct包含以下列:year(年)、month(月)、day(日)、surface_temp(海面温度,摄氏度)。

问题:如何找出平均海面温度最高的单个月份(例如“1998年11月”)?

我们需要将所有属于同一年、同一月的日期数据分组,然后计算该组内surface_temp的平均值。因此,正确的分组依据是['year', 'month']

monthly_avg_temp = ct.groupby(['year', 'month']).mean()

分组后,surface_temp列的值就是每个年月组合的平均温度。我们可以据此进行排序,找到温度最高的月份。

重要提示:在解释此类数据时,务必注意数据的完整性。例如,如果数据集只更新到某年6月,那么该年的年平均温度仅基于上半年(较冷月份)的数据,不能代表全年情况,在分析趋势时需要谨慎。


数据合并 (Merging) 🤝

接下来,我们学习第二种关键技术:合并。当我们需要的信息分散在多个数据框中时,合并操作可以将它们组合起来。

考虑以下场景:你有一些旧衣服想要转卖。一家二手店根据衣物类型提供不同的回收价格比例。你有一个数据框offer_pct列出了每种clothing_type(衣物类型)对应的offer_percentage(回收比例)。你还有另一个数据框clothes,列出了你拥有的每件item(物品)及其retail_price(原价)。

问题:如果你卖掉所有衣服,能赚多少钱?

要回答这个问题,需要将两个数据框的信息结合起来:对于你拥有的每件衣服,找到其类型对应的回收比例,然后用原价乘以这个比例。

我们使用merge方法进行合并。基本语法是:

merged_df = left_df.merge(right_df, left_on='left_column_name', right_on='right_column_name')

在我们的例子中:

  • left_dfoffer_pct
  • right_dfclothes
  • 用于匹配的列:left_df中的clothing_typeright_df中的item
combined = offer_pct.merge(clothes, left_on='clothing_type', right_on='item')

合并过程如下:

  1. 从左数据框(offer_pct)的第一行开始。
  2. 查看其clothing_type的值(例如“shirt”)。
  3. 在右数据框(clothes)的item列中寻找相同的值(“shirt”)。
  4. 如果找到匹配项,就将左右两行的所有列组合成一个新行。
  5. 如果左表的一行在右表找到多个匹配(例如“shoes”对应两双鞋),则会为每个匹配创建一行。
  6. 如果左表的一行在右表没有匹配(例如“shorts”,但你没有短裤),则该行不会出现在结果中。

合并后,combined数据框包含了计算所需的所有信息:offer_percentageretail_price在同一行。然后我们可以进行计算:

# 将百分比转换为小数
earnings_series = (combined.get('offer_percentage') / 100) * combined.get('retail_price')
# 求和得到总收入
total_earnings = earnings_series.sum()

关于合并顺序:与分组类似,交换左右数据框的顺序(clothes.merge(offer_pct, ...))会产生一个包含相同信息但行、列顺序可能不同的数据框。


练习:预测合并结果

理解合并操作的关键是能够预测结果的行数。这取决于两个数据框在匹配列上的值如何对应。

考虑两个数据框:

  • nice_weather_cities: 包含city(城市)和weather_score(天气评分)。
  • schools: 包含school(学校名)、city(城市)和state(州)。

问题1:如果按city列合并这两个数据框,结果会有多少行?
你需要检查左表的每个city值在右表的city列中出现了多少次。每次匹配都会产生一行。例如,如果左表的“San Diego”在右表出现了2次,那么它就会贡献2行。将所有城市的匹配行数相加即为总行数。

问题2:如果按state列合并呢?
原理相同,但现在是基于州进行匹配。左表的每个state值(如“CA”)会与右表中所有具有相同state值的行进行匹配并组合。


总结 📝

本节课我们一起学习了两个强大的数据框操作技术:

  1. 多列分组:通过向groupby()传入列名列表,可以基于多个条件创建更精细的分组,从而进行更复杂的聚合分析,例如分析海面温度在不同年月的变化。
  2. 数据合并:使用merge()方法,可以根据一个或多个共同列将来自不同数据框的信息连接起来,这对于组合相关数据集以进行综合分析至关重要,例如计算转卖衣物的总收益。

掌握这些技术将极大地增强你处理和分析复杂、多维度真实世界数据的能力。

10:编程基础与迭代 🧑‍💻

在本节课中,我们将学习一些基础的编程概念,这些概念并非专门针对数据处理或Python,而是通用的编程技能。我们将在未来几周使用这些技能来分析数据。

布尔值与逻辑运算

上一节我们介绍了数据类型,本节中我们来看看布尔值。布尔数据类型只有两个值:TrueFalse。我们通常通过比较运算来获得布尔值。

比较运算包括:

  • 相等比较:==
  • 不等比较:!=
  • 数学比较:><>=<=

例如,我们定义两个变量:

department = "DSC"
course = 10

我们可以进行如下比较:

course < 20  # 结果为 True

这个操作就像在提问:“课程编号小于20吗?”,其结果是布尔值。

in 操作符

in 操作符用于检查某个元素是否属于一个序列(如列表或数组)。在Python中,字符串也被视为序列。

对于字符串,in 操作符检查的是子字符串,且子字符串必须是连续的。

"DS" in "DSC"  # 结果为 True
"DS" in "data science"  # 结果为 False(D和S不连续)

布尔运算符

有三个布尔运算符允许我们对布尔值进行类似算术的运算:notandor

  • not:放在一个布尔值前,将其值从 True 变为 False,或从 False 变为 True
  • and:放在两个布尔值之间,只有当两边都为 True 时,结果才为 True
  • or:放在两个布尔值之间,只要至少一边True,结果就为 True

与算术运算有运算顺序(PEMDAS)一样,布尔运算也有优先级顺序:not > and > or。建议使用括号来明确意图。

布尔序列运算

之前我们在用布尔序列查询数据框时,已经接触过“与”和“或”的概念,但那里使用的是符号 &(与)和 |(或)。区别在于:

  • 关键字 andor 用于两个单独的布尔值之间。
  • 符号 &| 用于两个布尔值序列之间,进行逐元素运算。

条件语句(if 语句)

我们学习布尔值的真正原因,是为了编写能根据不同情况执行不同操作的代码。if 语句允许我们实现这一点。

if 语句的写法是:if 后面跟一个条件(一个能计算出 TrueFalse 的表达式),如果条件为 True,则执行其下方缩进的代码块。

else 语句

通常,当条件不满足时,我们可能想做点别的事情。这时可以使用 else 语句。else 必须跟在 if 语句后面,它指定了当 if 条件为 False 时要执行的代码。else 是可选的,但你不能单独使用 else

elif 语句

有时逻辑可能更复杂,不止两个选项。你可以使用 elif(即 “else if”)来检查多个条件。

elif 的工作方式可以想象成一系列筛子:

  • 第一个 if 筛子孔最大,符合条件的会被它“捕获”并执行相应代码。
  • 只有没被前一个筛子捕获的,才会进入下一个 elif 筛子,它的孔更小一些。
  • 可以有很多个 elif
  • 最后的 else 就像地面,所有通过前面所有筛子的最细颗粒都会掉到这里执行。

一个重要的结果是:当你评估某个 elif 条件时,你已经知道它前面所有的 ifelif 条件都是 False

如果将所有的 elif 都换成 if,代码结构会改变,因为每个 if 都会被独立检查,可能导致多个代码块被执行。

函数中的条件语句与返回

在函数中使用条件语句时,return 语句会立即结束函数的执行。这意味着,如果某个条件满足并执行了 return,函数就不会继续检查后面的条件。

因此,在函数中,有时即使将所有 elif 换成 if,只要每个分支都使用 return,结果看起来也可能一样,因为函数在第一次 return 后就结束了。但如果将 return 换成 print,结果就会不同,因为 print 不会终止函数。

迭代与 for 循环

接下来我们谈谈迭代。迭代是指重复一个过程。for 循环允许我们实现迭代。

for 循环告诉Python为某个变量的不同值重复执行一段代码块。其结构是:for 一个变量 in 一个序列,然后执行缩进的代码体。

你可以这样理解:对于给定序列中的每个元素,重复执行这段代码。变量会自动依次取序列中的每个值。

for 循环通常用于压缩相似或相同的代码。没有 for 循环,你总是可以通过多次重复代码来达到目的,但 for 循环让代码更简洁。

遍历序列

字符串在Python中也被视为序列,因此你也可以用 for 循环遍历字符串中的每个字符。

使用索引遍历

有时,我们需要根据元素在序列中的位置(索引)来遍历。可以使用 np.arange(len(sequence)) 来生成索引序列(0, 1, 2, ...),然后在循环中使用这个索引来访问序列中的元素。这在需要同时遍历两个或多个序列时特别有用。

循环变量

循环变量不一定非要在循环体中使用。你可以仅仅利用循环来重复执行某段代码固定的次数,而不关心循环变量的具体值。

缩进的重要性

只有被缩进的代码才会作为循环体的一部分被重复执行。循环体外的代码只执行一次。

累积模式与模拟

我们在本课程中使用 for 循环的主要场景是累积模式,用于模拟随机事件并记录结果。

累积结果:np.append

np.append 函数用于向数组末尾添加元素。需要注意的是,你必须将 np.append 的结果重新赋值给原数组变量,数组才会被真正修改和扩展。这就像在纸上记录实验结果,每做一次实验,就在列表末尾添加一个新结果。

模拟示例:抛硬币

通过模拟抛硬币多次,并记录每次得到正面的次数,我们可以了解这个随机过程的典型结果。我们使用一个 for 循环来重复实验(例如10,000次),每次实验后,使用 np.append 将结果(正面次数)累积到一个数组中。最后,我们可以通过直方图等方式查看结果的分布。

另一种累积:计数器

另一种累积结果是使用一个整数变量作为计数器。例如,在模拟彩票时,你可以将计数器初始化为0,如果中奖则将其加1,最后计数器就告诉你中奖的次数。

总结

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

  1. 布尔逻辑:包括比较运算、in 操作符以及 notandor 运算符。
  2. 条件语句:使用 ifelifelse 来根据条件执行不同的代码路径,并理解了它们在函数中的行为。
  3. 迭代与 for 循环:使用 for 循环来重复执行代码,可以遍历序列元素或单纯重复固定次数。
  4. 累积模式:这是本课程中使用 for 循环的核心模式,用于通过模拟来理解随机过程,主要工具是 np.append 和计数器。

请注意,在处理数据框、序列或数组时,我们通常有更高效的方法(如分组、查询),因此不会用 for 循环来逐元素处理数据。我们主要将 for 循环用于上述的模拟和累积任务。

11:概率基础 🎲

在本节课中,我们将学习概率论的基本概念。概率是数据科学中用于理解和量化不确定性的核心工具。我们将从最简单的等可能情况入手,逐步学习事件、条件概率、乘法规则以及独立事件等核心概念。

概述与基本概念

概率研究的是看似随机的过程,例如抛硬币或掷骰子。我们首先需要理解几个基本术语。

  • 实验:产生随机结果的过程,例如掷一次骰子。一个实验可以包含多个步骤,例如连续抛两次硬币。
  • 结果:实验发生的具体情形。例如,掷一次骰子有6种可能的结果。
  • 事件:一个或多个结果的集合。例如,“掷出偶数”这个事件对应结果集合 {2, 4, 6}。
  • 概率:一个介于0和1之间的数字,表示事件发生的可能性。概率为0表示永远不会发生,概率为1表示总是会发生。我们用 P(A) 表示事件A的概率。

等可能结果的概率

概率最简单的情况是所有可能结果发生的可能性都相等,例如掷一个公平的骰子或抛一枚公平的硬币。

在这种情况下,事件A的概率计算公式为:
P(A) = (事件A包含的结果数) / (所有可能的结果总数)

例如,抛两次公平硬币,求“至少出现一次正面”的概率。所有可能的结果是:{正正,正反,反正,反反},共4种。“至少一次正面”包含 {正正,正反,反正},共3种结果。因此,概率为 3/4

重要提示:此公式仅在所有结果等可能时成立。如果某些结果更可能发生,则不能简单使用此公式。

条件概率

条件概率是指在已知某个事件(A)发生的情况下,另一个事件(B)发生的概率。记作 P(B | A)

其计算公式为(在等可能前提下):
P(B | A) = (同时满足A和B的结果数) / (满足A的结果总数)

可以理解为:在已知事件A发生的“世界”里,事件B也发生的比例。

例如,掷一个公平的六面骰子,已知点数小于等于3,求点数为偶数的概率。

  • 已知事件A(点数≤3)包含结果 {1, 2, 3}。
  • 事件B(点数为偶数)包含结果 {2, 4, 6}。
  • 同时满足A和B的结果只有 {2}。
    因此,P(偶数 | 点数≤3) = 1/3

事件的组合:“与”和“或”

上一节我们介绍了单一事件和条件概率,本节中我们来看看如何计算多个事件组合的概率。

事件“与”的概率

事件“A与B同时发生”的概率记作 P(A and B)。在等可能前提下,它等于同时满足A和B的结果数除以总结果数。

例如,掷骰子得到“点数≤3且为偶数”的概率。同时满足两个条件的结果只有 {2},总结果有6个,所以概率为 1/6

“与”概率可以通过乘法规则与条件概率联系起来:
P(A and B) = P(A) * P(B | A)

这表示,要使A和B都发生,可以先让A发生,然后在A发生的条件下再让B发生。此公式在结果非等可能时也成立。

事件“或”的概率

事件“A或B至少发生一个”的概率记作 P(A or B)。在等可能前提下,它等于满足A或B(或同时满足)的结果数除以总结果数。

例如,掷骰子得到“偶数或点数≥5”的概率。

  • 偶数结果:
  • 点数≥5结果:
  • 满足至少一个的结果是:{2, 4, 5, 6},共4个。
    因此,概率为 4/6 = 2/3

注意:不能简单地将P(A)和P(B)相加,因为同时属于A和B的结果(如数字6)会被重复计算。只有当A和B互斥(即没有重叠结果)时,才有 P(A or B) = P(A) + P(B)

独立事件与乘法规则的特例

当两个事件独立时,即一个事件的发生不影响另一个事件发生的概率,条件概率 P(B | A) 就等于 P(B)

此时,乘法规则简化为:
P(A and B) = P(A) * P(B)

例如,抛一枚有偏硬币(正面概率0.7),每次抛掷独立。连续抛5次都得到正面的概率为:
0.7 * 0.7 * 0.7 * 0.7 * 0.7 = 0.7^5

补事件概率

一个非常有用的规则是:事件不发生的概率等于1减去它发生的概率。
P(not A) = 1 - P(A)

这常用于简化“至少发生一次”这类问题的计算。例如,打电话给奶奶,每次接通的概率是1/3且独立。打3次电话,至少接通一次的概率,可以先计算一次都没接通的概率:(2/3)^3 = 8/27,然后用1减去它:1 - 8/27 = 19/27

总结

本节课中我们一起学习了概率论的基础知识:

  1. 所有结果等可能的简单情况下,概率是“有利结果数”除以“总结果数”。
  2. 条件概率 P(B | A) 是在已知A发生的情况下B发生的概率。
  3. 计算“与”概率可以使用乘法规则:P(A and B) = P(A) * P(B | A)。当事件独立时,简化为 P(A) * P(B)
  4. 计算“或”概率需要小心重叠部分。仅当事件互斥时,才能直接相加:P(A or B) = P(A) + P(B)
  5. 利用补事件规则 P(not A) = 1 - P(A) 可以巧妙解决“至少一次”问题。

这些概念是理解数据中随机性和不确定性的基石,将在后续的统计推断和机器学习中反复应用。

12:概率与模拟 🎲

在本节课中,我们将学习如何将编程与概率知识结合起来。我们将通过编写代码来模拟随机过程,从而估算那些难以直接计算的概率。课程将从复习循环开始,然后介绍如何使用循环来运行模拟实验。

复习:For循环与累加器模式

上一节我们介绍了概率的基本概念,本节中我们来看看如何用编程来辅助概率计算。首先,我们需要回顾一下用于重复执行代码的 for 循环。

for 循环会为序列中的每个元素重复执行一段代码。序列可以是列表、数组或字符串。

以下是一个 for 循环的例子:

for x in ["my boyfriend", "a god", "the breeze", "a relaxing thought"]:
    print("Karma is", x)

这段代码会依次将 x 设置为列表中的每个值,并执行打印语句。

如果我们需要将循环的结果保存下来,而不是直接打印,就需要使用累加器模式。累加器模式是指:我们首先创建一个初始为“空”的变量,然后在每次循环中向其添加内容。

以下是累加器模式的几种常见形式:

  • 累加为字符串:从空字符串开始,使用 + 进行拼接。
    lyrics = ""
    for x in ["my boyfriend", "a god", "the breeze", "a relaxing thought"]:
        lyrics = lyrics + "Karma is " + x + "\n"
    
  • 累加为整数(计数):从0开始,当满足条件时增加计数。
    count = 0
    for number in some_array:
        if number % 2 == 0: # 判断是否为偶数
            count = count + 1
    
  • 累加为数组:从空数组开始,使用 np.append 添加元素。
    results = np.array([])
    for i in np.arange(10):
        new_result = np.random.choice(["Heads", "Tails"])
        results = np.append(results, new_result)
    

练习:统计元音字母数量

为了巩固对循环和字符串操作的理解,我们来尝试一个练习:编写一个函数,统计给定字符串中元音字母(A, E, I, O, U)的数量。

以下是实现该函数的一种策略:

def count_vowels(s):
    vowels_seen = 0
    for vowel in "AEIOU":
        how_many = s.count(vowel)
        vowels_seen = vowels_seen + how_many
    return vowels_seen

该函数的核心思路是:初始化一个计数器 vowels_seen 为0。然后遍历每个元音字母,使用字符串的 .count() 方法计算该元音在输入字符串 s 中出现的次数,并将次数累加到计数器上。

通过模拟估算概率

当我们遇到一个复杂、难以直接计算概率的问题时,可以通过模拟来估算概率。模拟的核心思想是:用计算机重复实验很多次,然后统计目标事件发生的频率,这个频率就近似于概率。

模拟随机选择

要进行模拟,我们首先需要让计算机能够做出随机选择。这可以通过 np.random.choice 函数实现。

np.random.choice 的基本用法是从一个序列中随机选取一个元素:

np.random.choice(["Heads", "Tails"]) # 模拟抛一次硬币
np.random.choice([1, 2, 3, 4, 5, 6]) # 模拟掷一次骰子

你可以通过 size 参数一次性进行多次选择:

np.random.choice(["Heads", "Tails"], size=10) # 模拟抛10次硬币

你还可以通过 replace 参数指定是否允许重复选择(即有放回或无放回):

# 无放回地选择3所不同的学院
colleges = ["Revelle", "Muir", "Marshall", "Warren", "Roosevelt", "Sixth", "Seventh"]
np.random.choice(colleges, size=3, replace=False)

示例1:估算抛硬币的概率

现在,让我们用模拟来估算一个概率问题:抛掷100枚均匀硬币,得到60次或更多次正面朝上的概率是多少?

我们的计划分为三步:

  1. 编写能模拟一次实验(抛100次硬币并统计正面次数)的代码。
  2. 将实验重复很多次(例如10,000次),并记录每次的结果。
  3. 计算所有实验中,正面次数大于等于60的实验所占的比例。

首先,实现单次实验的函数:

def coin_experiment():
    coins = np.random.choice(["Heads", "Tails"], size=100)
    heads_count = np.count_nonzero(coins == "Heads") # 统计"Heads"的数量
    return heads_count

这里,coins == "Heads" 会生成一个布尔值数组(True表示正面)。在Python中,True等价于1,False等价于0。因此,对这个布尔数组求和或使用 np.count_nonzero 都能得到正面的数量。

接着,重复实验多次并收集结果:

repetitions = 10000
head_counts = np.array([]) # 初始化累加器数组

for i in np.arange(repetitions):
    hc = coin_experiment() # 进行一次实验
    head_counts = np.append(head_counts, hc) # 将结果累加

最后,计算估算的概率:

# 计算正面次数>=60的比例
successes = np.count_nonzero(head_counts >= 60)
estimated_probability = successes / repetitions
# 或者等价地:estimated_probability = (head_counts >= 60).mean()

通过10,000次模拟,我们估算出的概率约为2.8%,这与通过数学公式计算出的精确概率非常接近。模拟次数越多,估算通常就越准确。

示例2:蒙提霍尔问题

蒙提霍尔问题是一个著名的概率谜题:假设你参加一个游戏节目,面前有三扇门,其中一扇后面有汽车(奖品),另外两扇后面是山羊。你选择一扇门后,知道答案的主持人会打开另一扇有山羊的门。然后问你:是坚持原来的选择,还是换到剩下的那扇门?哪种策略赢的概率更大?

我们可以通过模拟来找出答案。思路同样是模拟游戏很多次,分别测试“坚持”和“更换”策略的胜率。

以下是模拟“更换”策略一次游戏的函数核心逻辑(简化版):

def simulate_switch_strategy():
    # 奖品随机放在一扇门后
    prizes = ["Car", "Goat", "Goat"]
    # 参赛者随机初选一扇门
    original_choice = np.random.choice([0, 1, 2])
    # 主持人打开一扇有山羊且未被选择的门
    # ... (此处省略具体的主持人逻辑代码)
    # 参赛者更换到剩下的那扇门
    # ... (此处省略具体的更换逻辑代码)
    # 返回最终得到的奖品
    return final_prize

当我们重复这个游戏成千上万次后,统计得到汽车的比例。模拟结果清晰地显示:更换策略的获胜概率约为2/3,而坚持策略的获胜概率仅为1/3。因此,更换是更优的策略。

一个直观的解释是:如果你最初选中山羊(概率2/3),主持人必定会打开另一扇山羊门,此时更换一定会赢得汽车。如果你最初选中汽车(概率1/3),更换则会输掉。所以更换策略的胜率就是最初选中山羊的概率,即2/3。

总结

本节课中我们一起学习了如何将Python编程与概率论结合。我们复习了for循环和累加器模式,并利用np.random.choice函数引入随机性。通过“抛硬币”和“蒙提霍尔问题”两个实例,我们掌握了通过计算机模拟来估算复杂概率的完整流程:定义单次实验、重复多次并收集数据、最后分析结果计算频率。这种方法在理论计算困难时非常强大,也是数据科学中理解随机现象的重要工具。

13:概率分布、抽样与统计推断 📊

在本节课中,我们将学习概率分布与经验分布的区别,了解抽样在数据科学中的核心作用,并探索如何利用样本数据推断总体特征。我们将通过掷骰子和航班延误数据的例子,直观地理解大数定律以及样本统计量如何逼近总体参数。


🎲 概率分布与经验分布

上一节我们介绍了模拟的基本概念,本节中我们来看看如何描述随机事件的可能性。

概率分布描述了一个随机变量所有可能取值及其对应的理论概率。例如,掷一枚均匀骰子时,每个面(1到6)出现的概率都是1/6。

公式表示
对于一个均匀骰子,其概率分布可以表示为:
P(X = x) = 1/6,其中 x ∈ {1, 2, 3, 4, 5, 6}

经验分布则基于实际观察到的数据。它描述了在多次实验中,每个观测值出现的比例。例如,如果你实际掷骰子25次,记录每个数字出现的次数,由此计算出的比例就构成了一个经验分布。经验分布会因实验者和实验次数不同而变化,而概率分布是固定不变的理论值。


🔄 大数定律

从掷骰子的经验中,我们观察到随着实验次数增加,经验分布会越来越接近理论上的概率分布。这一现象由大数定律所描述。

大数定律指出:在相同条件下独立重复进行一个随机试验,某个事件发生的频率会随着试验次数的增加而逐渐稳定,并趋近于该事件的理论概率。

这意味着,当我们进行大量模拟(如上一节课所做)时,用频率来估计概率是合理且可靠的。


📋 总体与样本

在数据科学中,我们常常无法获取研究对象的全部数据(即总体),这时就需要进行抽样。

  • 总体:我们想要研究的全部个体或观测值的集合。例如,所有UCSD学生的身高。
  • 样本:从总体中选取的一个子集。例如,随机调查的100名UCSD学生的身高。

我们的目标是通过分析样本数据来推断总体的特征。样本的选取方式至关重要,糟糕的抽样方法(如只调查篮球队成员)会导致结论存在偏差。


🎯 简单随机抽样

为了获得对总体有代表性的样本,黄金标准是进行简单随机抽样

简单随机抽样满足两个条件:

  1. 均匀性:总体中的每个个体被选中的概率相同。
  2. 无放回:同一个个体不会被重复选中。

在代码中,我们可以使用 np.random.choice(..., replace=False) 或DataFrame的 .sample() 方法(默认无放回)来实现简单随机抽样。


✈️ 实例分析:航班延误数据

让我们通过一个真实数据集来理解这些概念。我们有一个包含2015年夏季从旧金山机场起飞的近14,000个联合航空公司航班的总体数据,其中包含“延误时间”这一数值变量。

首先,我们查看总体中延误时间的分布直方图。我们发现大部分航班基本准点,但存在一个长长的右尾,意味着有少数航班延误非常严重。

核心操作代码

# 从总体数据框中抽取一个简单随机样本
sample_of_100 = flights.sample(100)
# 绘制样本中延误时间的分布
sample_of_100.get('Delay').plot(kind='hist')

当我们只有样本(例如100个航班)时,其延误分布直方图与总体分布相似,但不会完全一致。随着样本量增大(如1000个、10000个航班),样本的分布会越来越接近总体分布。这再次印证了大数定律的思想:更大的样本能提供关于总体更准确的信息。


📐 参数与统计量

为了进行定量推断,我们引入两个关键概念:

  • 参数:描述总体特征的固定数值。例如,所有航班的平均延误时间。参数通常未知,是我们希望通过样本去推断的目标。
  • 统计量:基于样本数据计算得出的数值。例如,样本中100个航班的平均延误时间。统计量是随机的,因为不同的样本会计算出不同的值。

一个常用的记忆法是:Parameter(参数)对应 Population(总体),Statistic(统计量)对应 Sample(样本)。

我们的目标是:使用样本统计量(如样本均值)来估计总体参数(如总体均值)。


🎰 统计量的经验分布

由于抽样具有随机性,基于不同样本计算出的统计量(如样本均值)会有所不同。为了理解这种变异性,我们可以通过模拟来研究统计量的经验分布。

以下是研究样本均值经验分布的方法:

  1. 从总体中重复抽取大量相同大小的样本(例如,抽取2000次,每次抽100个航班)。
  2. 对每个样本计算其统计量(例如,计算2000个样本各自的平均延误时间)。
  3. 将这些统计量的值收集起来,绘制直方图。

这个直方图展示了样本均值可能取值的分布情况。我们发现:

  • 当样本量较大(如1000)时,样本均值的分布非常集中,紧密围绕在总体均值(约17分钟)周围,形状对称且规则。
  • 当样本量较小(如100)时,样本均值的分布则分散得多,这意味着基于小样本的估计结果可能很不稳定,有时会严重偏离真实值。

这直观地说明了:更大的样本能产生更精确、更可靠的估计


📝 总结

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

  1. 概率分布(理论)与经验分布(观测)的区别。
  2. 大数定律如何保证频率趋近于概率。
  3. 数据科学中通过样本推断总体的基本框架。
  4. 获得代表性样本的关键方法——简单随机抽样
  5. 参数(总体特征)与统计量(样本特征)的核心概念。
  6. 通过模拟理解统计量的经验分布,并认识到样本量越大,统计量作为估计量通常越优

理解这些概念是进行统计推断的基石,能帮助我们在只有部分数据的情况下,对更广阔的世界做出合理的结论。

14:期中考试复习 📘

📋 概述

在本节课中,我们将通过分析一份往期期中考试试卷(2024年冬季学期)来复习关键概念。我们将模拟考试环境,专注于问题解决,而不运行代码。课程将涵盖数据框操作、条件筛选、概率计算和直方图解读等核心主题。


🎲 考试背景与数据说明

首先,我们来看考试的数据信息表。仔细阅读数据信息表非常重要,因为它包含了考试所需的所有细节。

本次考试围绕棋盘游戏《妙探寻凶》(Clue)展开。这是一个谋杀推理游戏,玩家通过排除法推断犯罪细节。游戏前提是:在一所大宅中发生了一起谋杀案,凶手是六名嫌疑人之一,使用了七种武器之一,发生在九个房间之一。

游戏共有22张卡牌(6张嫌疑人 + 7张武器 + 9张房间)。确定凶手的方法是:从每个类别中随机抽取一张卡牌(一名嫌疑人、一种武器、一个房间),放入一个信封。信封中的内容代表了谋杀的细节。这意味着有三张卡牌被放入信封。

剩下的19张卡牌被洗匀并分发给玩家。玩家查看自己手中的卡牌,可以知道这些卡牌不可能是信封中的卡牌(即不是凶器、凶手或案发房间)。这是一个通过排除法进行的游戏。玩家有机会看到其他玩家的手牌,进一步排除嫌疑人、武器和房间。第一个将可能性缩小到唯一的嫌疑人、武器和房间组合的玩家提出指控并获胜。

我们拥有的实际数据是关于一场特定游戏实例的信息。玩家是Janine、Henry和Page。Janine和Page各分到6张卡牌,Henry分到7张卡牌,总计19张。

数据框 clue 有22行,对应游戏中的每张卡牌,它代表了我对每张卡牌持有者的了解。开始时,我对许多卡牌持有者并不清楚,选项包括“unknown”或玩家姓名。category 列包含“suspect”、“weapon”或“room”。cardholder 列包含“Janine”、“Henry”、“Page”或“unknown”。数据框的索引是卡牌名称。

需要记住的是,clue 数据框代表我在游戏任意时间点的当前知识,不一定是游戏开始时的状态。在整个考试中,我们已经运行了 import babypandas as bpdimport numpy as np


🔢 问题一:表达式求值

以下每个表达式都会求值为一个整数。请确定该整数的值,如果无法确定,请圈出“信息不足”。

以下是每个问题的解析:

A. (clue.get(‘cardholder’) == ‘Janine’).sum()

  • 这部分求值结果是什么?它是一个布尔值序列(True和False)。
  • 对布尔值序列求和会得到True的数量。
  • 这基本上就是cardholder列等于“Janine”的行数。
  • 根据游戏设定,我(Janine)被发了6张牌,并且我知道自己持有这6张牌。因此,答案是6

B. np.count_nonzero(clue.get(‘category’).str.contains(‘p’))

  • 我们获取category列,并检查哪些值包含字母‘p’。
  • category列包含三个特定值:“suspect”、“weapon”、“room”。哪些包含‘p’?“suspect”和“weapon”包含,‘room’不包含。
  • 因此,这对应的是嫌疑人和武器卡牌的行数,即6 + 7 = 13行。
  • str.contains(‘p’)生成一个布尔序列,np.count_nonzero计算其中非零(即True)的数量,所以答案是13

C. clue[clue.get(‘category’) == ‘suspect’].query(‘cardholder == “Janine”’).shape[0]

  • 这表示:在category为“suspect”的行中,查询cardholder为“Janine”的行,然后获取其行数(shape[0])。
  • 用中文解释就是:我手中有多少张嫌疑人卡牌?
  • 我们知道我总共有6张牌,但这些牌可能是任意类别(嫌疑人、武器、房间)的组合。我们无法确定其中有多少张是嫌疑人卡牌。因此,答案是信息不足

D. len(clue.take(np.arange(5, 20, 3)).index)

  • np.arange(5, 20, 3) 生成序列:5, 8, 11, 14, 17。
  • .take 方法根据位置索引(从0开始计数)获取数据框的特定行。因此,它获取的是第6行(索引5)、第9行(索引8)等。
  • 最终会得到一个包含5行的新数据框。
  • .index 获取这个新数据框的索引,len() 计算其长度,即行数。所以答案是5

E. clue.query(‘category >= “t”’).index.shape[0]

  • 这是一个查询,条件是 category 列的值大于或等于字符串 “t”。
  • Python对字符串进行字母顺序比较。
  • “suspect” (s) < “t” -> False
  • “weapon” (w) > “t” -> True
  • “room” (r) < “t” -> False
  • 因此,只有“weapon”类别的行会被保留,共有7行。
  • 查询后得到一个7行的数据框,其索引长度为7

F. clue.groupby(‘cardholder’).count().get(‘category’).sum()

  • clue.groupby(‘cardholder’).count() 会按cardholder的不同值(Janine, Henry, Page, unknown)进行分组,并计算每组中每列的非空值数量。由于所有列在每组中都有值,count()的结果列都表示该持有者拥有的卡牌数量(根据我的当前知识)。
  • 例如,Janine对应的数字是我知道自己持有的卡牌数(6),Page和Henry对应的数字是我目前所见到的他们持有的卡牌数,unknown对应的数字是我尚不知道持有者的卡牌数。
  • 然后,我们取出category列(选择哪一列并不重要,因为count()后所有列的值都相同)并求和。
  • 这个和必须是22,因为总共有22张卡牌,每张卡牌都被某个持有者(包括unknown)所“拥有”。

🕵️ 问题二:判断指控时机

当玩家将可能性缩小到唯一的未知嫌疑人、武器和房间时,就意味着可以提出指控并赢得游戏。这部分定义了一些通过操作clue数据框得到的新数据框(groupedfiltered),问题是在空白处填上代码,使得当我有足够信息提出指控时,会打印“ready to accuse”。

我们需要理解对数据框进行操作后会发生什么。

首先,回忆一下clue.reset_index()后的样子:它会让card从索引变成一列,并引入默认索引0, 1, 2…。数据框将有三列:card, category, cardholder

接着,我们按categorycardholder进行分组(groupby([‘category’, ‘cardholder’]))。当按多列分组时,会为这些列值的每个组合生成一行。例如,一行可能代表category为“suspect”且cardholder为“Page”的组合,另一行可能代表category为“weapon”且cardholder为“unknown”的组合。

然后,.count()会告诉我这个新数据框的列中会有什么数据。它计算的是每个组合中有多少张卡牌。例如,“嫌疑人 & Page”组合下的数字表示我知道Page有多少张嫌疑人卡牌;“武器 & unknown”组合下的数字表示有多少张武器卡牌对我来说还是未知的。

grouped = clue.reset_index().groupby([‘category’, ‘cardholder’]).count().reset_index() 这行代码执行了上述操作并重置索引以便于处理。

filtered = grouped[grouped.get(‘cardholder’) == ‘unknown’] 这行代码从grouped中筛选出cardholder为“unknown”的行。结果数据框会有3行,分别对应“weapon-unknown”, “suspect-unknown”, “room-unknown”。各列的值都相同,代表未知的武器、嫌疑人、房间的数量。

为了能够提出指控,我需要每个类别的未知数量都恰好为1(不能是0,因为信封中的卡牌永远不会被任何人持有,所以总是至少有一个未知项)。当每个未知数都变为1时,就意味着通过排除法,剩下的那个就是信封中的卡牌(即凶手、凶器、案发房间)。

因此,问题在于如何检查filtered数据框中的数字是否全为1。

A部分:代码是 if filtered.get(‘card’)._____ == 3:。我们需要选择一个Series方法填入空白,使得当可以指控时条件为真。

  • max():如果最大值是1,由于未知数不可能小于1(信封里总有一张),这意味着所有值都是1。所以 filtered.get(‘card’).max() == 1 是有效的检查。
  • min() == 1 无效,因为如果值是1, 2, 3,最小值也是1,但并非所有类别都已被缩小到唯一。
  • sum() == 3 也有效,因为如果每个都是1,和就是3。
  • count() 不是Series方法,它是GroupBy后的聚合方法。
  • shape[0] 是DataFrame的属性,不适用于Series。即使对filtered使用filtered.shape[0] == 3,也总是成立,因为filtered总是有3行(三个类别各一行)。关键不是行数,而是行中的数值。

所以,对于A部分,sum()max() 是可行的。题目中A部分用的是 sum() == 3

B部分:代码是 if filtered.get(‘card’)._____ == 1:。根据上面的分析,这里应该填入 max()。因为最大值等于1意味着所有值都是1。


🔁 问题三:构建指控语句

当玩家准备提出指控时,他们会说类似“It was Miss Scarlet with the dagger in the study.”的句子。总是这种形式:“It was [嫌疑人] with the [武器] in the [房间].”

题目定义了两个数组:

  • words = np.array([‘It was ‘, ‘ with the ‘, ‘ in the ‘])
  • answers = np.array([suspect, weapon, room]),例如 [‘Miss Scarlet’, ‘dagger’, ‘study’]

目标是编写一个for循环,将这两个数组的内容交错组合,最终生成字符串 “It was Miss Scarlet with the dagger in the study.”

我们需要遍历索引。words[0](’It was ‘)需要与answers[0](嫌疑人)组合,words[1]需要与answers[1]组合,依此类推。

因此,循环应该遍历索引0, 1, 2。

空白A:需要定义循环变量i遍历的序列。可以是[0, 1, 2],或者np.arange(3),或者np.arange(len(words))等。

空白B:在循环体内,我们需要使用累加器模式构建最终的指控语句accusation。初始时accusation是空字符串,每次迭代我们添加words[i] + answers[i]。所以代码是 accusation = accusation + words[i] + answers[i]


🎯 问题四:概率计算

回忆一下,《妙探寻凶》有22张卡牌。其中各一张(嫌疑人、武器、房间)被随机放入信封。剩下19张卡牌(5张嫌疑人、6张武器、8张房间)被洗匀并随机分发。我将获得其中的6张。

A部分:卡牌一张一张发出。我获得的第一张卡牌是武器卡的概率是多少?

  • 总共有19张卡牌正在分发。
  • 其中6张是武器卡。
  • 因此,概率是 6/19。发牌过程可以理解为随机获得6张卡牌,第一张是武器卡的概率与此相同。

B部分:我的所有六张卡牌都是武器卡的概率是多少?

  • 这是一个连续事件:第一张是武器,并且第二张也是武器,…,第六张也是武器。
  • 概率为:(6/19) * (5/18) * (4/17) * (3/16) * (2/15) * (1/14)
  • 答案以未简化形式写出即可。

C部分:确定我的前两张卡牌中恰好有一张是武器卡的概率。概率可以表示为 \(\frac{k \times (k+1)}{m \times (m+1)}\) 的形式,其中k和m是整数。求k和m的值。

  • 策略:分成两种情况。
    1. 第一张是武器,第二张不是武器。
    2. 第一张不是武器,第二张是武器。
  • 情况1概率:(6/19) * (13/18)。因为第一张后,剩下18张牌,其中非武器牌为19-6=13张。
  • 情况2概率:(13/19) * (6/18)。因为第一张非武器后,所有6张武器牌都还在。
  • 总概率 = (613)/(1918) + (136)/(1918) = (2 * 6 * 13) / (19 * 18) = (12 * 13) / (18 * 19)。
  • 对比形式 \(\frac{k \times (k+1)}{m \times (m+1)}\),可得 k=12, m=18。

📊 问题五:直方图解

直方图展示了两人和三人《妙探寻凶》游戏时长的分布(分钟),每个分布代表1000场游戏。这是一个叠加直方图。

A部分:游戏时间至少50分钟的三人游戏比两人游戏多多少场?答案取整到最近的10的倍数。

  • 聪明方法:直接计算直方图中在50分钟以上区域,两人和三人分布之间的面积差。
  • 观察50-60分钟区间:三人游戏的比例高度约为0.08,两人游戏的比例高度约为0。该区间宽度为10。
  • 面积差代表的比例是 0.08 * 10 = 0.8?注意:密度轴的单位是“每分钟的比例”,所以矩形面积=高度(密度)宽度(分钟数)。高度需要从纵轴读取。假设50-60分钟区间,三人游戏密度约为0.008(需要根据实际图形刻度确认,这里假设刻度为0.002, 0.004…),宽度为10,则面积=0.00810=0.08。这代表8%的游戏。
  • 1000场游戏的8%是80场。因此,答案大约是80。题目要求取整到10的倍数,所以是80。

B部分:两个直方图重叠部分的面积是多少?

  • 聪明方法:每个直方图的总面积都是1。重叠面积等于1减去不重叠部分的面积。
  • 不重叠部分(即只有一个分布覆盖的区域)的面积更容易计算。通常,这些区域位于分布的两端。
  • 计算这些不重叠区域的面积(每个区域的面积=密度差*区间宽度),然后用1减去它,即可得到重叠面积。

📝 总结

本节课我们一起分析了一份期中考试样卷,重点复习了以下内容:

  1. 对数据框进行布尔索引、字符串操作、分组聚合,并理解其结果的数值含义。
  2. 通过操作和筛选分组后的数据框,来判断游戏中的特定状态(如是否可以提出指控)。
  3. 使用循环和累加器模式构建字符串。
  4. 应用概率论知识,计算在不放回抽样中的各种事件概率,包括利用分类讨论。
  5. 解读叠加直方图,并通过计算面积差或利用总面积恒为1的特性来高效解题。

希望这次复习能帮助你为考试做好准备。祝你好运!

15:数据科学原理 - 考试指南 📝

在本节课中,我们将学习如何为数据科学考试做准备,并了解考试过程中的关键步骤和注意事项。我们将从考试前的准备开始,逐步介绍考试当天的具体流程。


概述

考试是检验学习成果的重要环节。一个有序的考试环境有助于每位考生发挥出最佳水平。本节将指导你如何开始考试,并确保整个过程顺利进行。

考试开始前的准备

上一节我们概述了考试的重要性,本节中我们来看看考试开始前需要完成的具体步骤。

考试开始时,监考人员会发出明确指令。你需要按照以下步骤操作,以确保考试顺利进行。

以下是考试开始时的关键步骤列表:

  1. 打开你的试卷。
  2. 撕下试卷的最后一页。
  3. 将最后一页从试卷上分离。
  4. 首先阅读这一页的内容。
  5. 阅读完毕后,即可开始答题。

考试环境与纪律

在了解了开始步骤后,维持良好的考试环境同样至关重要。

由于人员离场可能造成混乱,因此在最后10分钟请所有考生留在座位上。此外,请确保关闭手机或将手机调至静音模式,以免打扰他人。

总结

本节课中我们一起学习了数据科学考试的启动流程和考场纪律。关键点包括:按时开始、遵循拆封和阅读说明的步骤、以及维护安静的考试环境。记住,充分的准备和遵守规则是成功完成考试的基础。

16:统计推断与自助法 (Bootstrapping) 📊

在本节课中,我们将学习统计推断的核心概念,特别是如何使用自助法来估计总体参数,并构建置信区间来量化估计的不确定性。我们将通过一个关于圣地亚哥市员工薪资的真实数据集来演示这些概念。


🏙️ 数据集介绍

我们今天将使用一个新的数据集:圣地亚哥市员工薪资。这是每年公开的完整人口数据,包含了为圣地亚哥市工作的每一位员工的信息。

这是2023年的最新数据。数据框预览显示有近14,000名员工和29列数据。由于列数太多,无法全部显示,我们可以使用 .columns 属性来查看所有列名。

# 查看数据框的所有列名
df.columns

我们只关心其中的一列:total_wages(总薪资)。因此,我们创建一个只包含该列的小型数据框。

数据按降序排列。最高收入者年薪约为433,000美元,同时也有一些非常小的数字(例如2美元),这些可能不是全职员工。

通过查看总薪资的分布,我们可以了解典型的薪资水平。分布图显示有一个很高的第一个条形(代表极低薪资),其余数据大致呈峰值在略低于100,000美元,然后逐渐下降的形状。这被称为长右尾,在收入分布中很常见,因为薪资可以非常高但越来越罕见,而左侧受限于零(薪资不能为负)。


🎯 总体参数与样本统计量

我们首先查看总体中位数薪资,它可以代表员工的典型薪资。计算总薪资列的中位数:

population_median = df['total_wages'].median()

结果是 80,492美元。这是一个总体参数,因为它描述了整个总体的单一数值。

然而,在现实中,我们通常无法获得整个总体的数据。这时,我们会采取抽样的方法。我们将总体视为完整的薪资数据框。当我们只抽取500人的薪资时,就得到了一个样本

我们感兴趣的总体参数是总体中位数。由于无法直接计算,我们使用样本中位数作为替代,这是一个统计量(从样本中计算出的单一数值)。我们希望样本中位数与总体中位数相近。

让我们看看样本中位数的结果。我们使用 .sample 方法进行简单随机抽样(无放回抽样)。为了确保结果可重现,我们设置了随机种子。

my_sample = df.sample(500, random_state=123)
sample_median = my_sample['total_wages'].median()

样本中位数是 82,508美元,比总体中位数(80,492美元)高出约2,000美元。这种差异源于我们随机抽到的特定样本。


❓ 核心问题:估计的可靠性

我们得到了一个估计值(82,508美元),但这个估计值依赖于我们抽到的随机样本。如果抽到不同的样本,估计值会如何变化?我们对这个单一估计值的信心,取决于这个问题的答案。

如果不同的500人组的中位数薪资差异很大,那么我们对任何一组样本的估计都不会有太大信心。反之,如果所有组的中位数都非常接近,那么任何一组样本的估计都相当可靠。

为了回答这个问题,我们需要理解样本中位数的分布。如果这个分布的直方图很窄,意味着不同的样本中位数都大致相同,我们对估计就更有信心。如果直方图很宽,意味着不同的估计值可能差异很大,我们对单一估计的信心就会降低。


🔄 理解样本中位数分布的第一种方法(不切实际)

第一种方法是直接查看其他样本的中位数。这需要我们从总体中抽取大量样本。虽然不切实际,但可以演示概念。

我们进行一个模拟:从总体中抽取1,000个样本(每个样本500人),计算每个样本的中位数,并查看这些中位数的分布。

sample_medians = np.array([])
for i in np.arange(1000):
    # 从总体中抽取一个样本
    one_sample = df.sample(500)
    # 计算该样本的中位数
    median = one_sample['total_wages'].median()
    # 将中位数存入数组
    sample_medians = np.append(sample_medians, median)

这个直方图显示了样本中位数的经验分布。它告诉我们,当你抽取一个500人的样本并用其薪资中位数估计总体中位数时,你可能会偏差大约10,000美元(基于直方图的宽度)。

然而,这种方法的问题在于,在现实中我们无法反复从总体中抽样。如果我们能轻易获得总体数据,就直接计算总体参数了,无需抽样。


🚀 自助法:一个实用的解决方案

我们需要一个更实用的解决方案,能在不返回总体抽取新样本的情况下,获得类似的分布。关键思路在于:利用我们已有的样本

比较总体分布(红色,14,000名员工)和我们抽取的样本分布(蓝色,500名员工)。虽然条形高度有差异,但蓝色样本的分布形状与红色总体大致相似。

既然样本分布与总体分布相似,我们可以用样本分布来近似替代总体分布。这就是自助法背后的核心思想。

自助法是一种捷径,它允许我们理解像样本中位数这样的统计量在不同样本中可能的表现,而无需返回总体抽取更多样本。这个捷径就是:从样本本身中进行重抽样

过程如下:

  1. 你获得一个初始样本(例如我们的500人,称为 my_sample)。
  2. 你想知道其他样本的中位数会如何。
  3. 你无法返回总体获取新样本,但你可以从你的初始样本中进行重抽样(即,从样本中抽样)。
  4. 你从初始样本中抽取许多重抽样样本,并计算每个重抽样样本的中位数。
  5. 这些重抽样中位数的分布,近似于从总体中抽取新样本得到的中位数分布。

之所以可行,是因为我们的初始样本看起来很像总体。这个过程被称为“自助法”,源于“靠自己的力量振作起来”这个古老说法,意指非常谨慎地利用所有资源。


🔁 重抽样:有放回 vs 无放回

每次抽样时,我们都要问:是有放回还是无放回?在我们的情境中,我们总是讨论固定大小的样本组(500人),以进行公平比较。

为了理解重抽样时是否需要放回,我们看一个简单例子。假设初始样本只有三个薪资:$7, $9, $4。

  • 无放回重抽样:从{7, 9, 4}中抽取3个,不允许重复。你只会得到这三个数字的不同排列,例如[9,7,4]。每个重抽样的中位数总是相同的(排序后的中间数)。这意味着所有重抽样本质上都是同一个样本,没有提供新信息。
  • 有放回重抽样:从{7, 9, 4}中抽取3个,允许重复。你可能得到[7,7,4]或[9,4,9]等。这些重抽样的中位数会有所不同(例如[7,7,4]的中位数是7)。

我们的目标是模拟从总体中抽取的不同样本组。无放回重抽样得到的样本都太相似,无法反映这种差异性。因此,我们必须使用有放回重抽样。这样,重抽样样本才能与原始样本略有不同(有些人被重复,有些人被省略),从而产生统计量(如中位数)的多样性。


💻 自助法代码实现

现在,我们来看完整的自助法代码。我们通过从初始样本 my_sample 中进行有放回重抽样,来模拟收集新样本的过程。

# 设置随机种子以保证结果可重现
np.random.seed(123)

# 创建一个数组来存储每个重抽样样本的中位数
boot_medians = np.array([])

# 进行5000次重抽样
for i in np.arange(5000):
    # 从初始样本中抽取500个(有放回)
    resample = my_sample.sample(500, replace=True)
    # 计算该重抽样样本的中位数
    median = resample['total_wages'].median()
    # 将中位数存入数组
    boot_medians = np.append(boot_medians, median)

这段代码与之前不切实际方法的唯一区别在于抽样来源:之前是从总体 df.sample(500) 中抽样,现在是从初始样本 my_sample.sample(500, replace=True) 中抽样。其余部分(计算中位数并存储)完全相同。

通过这种方式,我们仅利用最初的一组500人数据,就得到了大量(5000个)可能的中位数估计值,从而“榨取”出尽可能多的信息。


📈 自助分布与点估计

让我们查看这个自助法生成的样本中位数分布。这个分布与我们直接从总体抽样得到的分布并不完全相同,但它仍然让我们对样本中位数可能的变化范围有了大致了解。

在这个分布图中,蓝色的点代表总体中位数(80,492美元),即我们试图估计的“正确答案”。

自助法的目的是从你初始样本的中位数(例如82,508美元)出发,去估计这个总体参数。仅凭一个样本,你只能给出一个点估计(“我认为总体中位数大约是82,500美元”),但无法说明这个估计有多接近真实值。

而通过自助法,你得到了一个完整的分布。这个分布显示,你的估计可能偏差高达10,000美元。因此,你可以做出更有信息的陈述,例如:“我认为总体中位数可能在70,000美元到88,000美元之间”,因为大部分数据落在这个范围内。


📊 百分位数

在构建区间之前,我们需要理解百分位数的概念。百分位数常用于描述数据位置。

  • 非正式定义:对于数值数据集,第p百分位数是一个值,使得大约p%的数据小于或等于这个值。
  • 例子:SAT考试的75百分位数是1400分,意味着75%的考生分数低于1400分,25%的考生分数高于1400分。

NumPy提供了计算百分位数的函数 np.percentile

data = np.array([3, 1, 6, 8, 4])
fiftieth_percentile = np.percentile(data, 50) # 计算第50百分位数(即中位数)

第50百分位数(中位数)是数据排序后处于中间位置的值。对于数组 [1, 3, 4, 6, 8],中位数是4。


🎯 构建置信区间

我们使用自助法创建了样本中位数的分布。为了量化估计的不确定性,我们想要给出一个区间,一个我们认为参数(总体中位数)可能落入的数值范围。

这个区间被称为置信区间。我们特别构建一个95%置信区间,它旨在捕获自助法直方图中95%的数据面积。

我们的目标是找到两个点 xy,使得:

  • 直方图中 x 左侧的面积约为2.5%。
  • 直方图中 y 右侧的面积约为2.5%。
  • 这样,xy 之间的面积就是中间的95%。

用百分位数表示:

  • x第2.5百分位数(2.5%的数据小于它)。
  • y第97.5百分位数(97.5%的数据小于它,即2.5%的数据大于它)。

我们使用NumPy的 percentile 函数来计算这些端点:

left_endpoint = np.percentile(boot_medians, 2.5)
right_endpoint = np.percentile(boot_medians, 97.5)
confidence_interval_95 = (left_endpoint, right_endpoint)

这样得到的区间 (left_endpoint, right_endpoint) 就是我们对总体中位数的95%置信区间估计。它表示,基于我们的自助分布,我们有95%的信心认为总体中位数落在这个区间内。


🤔 置信区间的解释与局限性

将这个置信区间(例如70,000美元到86,000美元)画在自助分布直方图上,它会覆盖中间95%的区域。

蓝色的点(总体中位数)落在这个区间内吗?在这个演示中,因为我们知道总体答案,可以看到它确实落在区间内。然而,在现实中,你永远无法确切知道你的置信区间是否包含了真实的总体参数。这是统计推断中一个令人不安但必须接受的事实。

我们提供的是一个估计过程。我们将在后续课程中讨论如何从理论上保证这个过程在长期是有效的。但对于任何一次具体的估计,它可能成功也可能失败。

失败的主要原因在于你的初始样本可能不具有代表性。即使是大样本,也可能由于随机巧合而无法反映总体。自助法尤其容易受此影响,因为它严重依赖于初始样本的质量。如果初始样本有偏差,那么基于它的所有重抽样和结论都会有偏差。


🔧 调整置信水平

我们构建的是95%置信区间。如果我们想要一个80%置信区间呢?

80%置信区间意味着我们只想保留中间80%的数据,需要从两侧各截去10%的数据。

因此,代码需要修改端点百分位数:

  • 左端点:第10百分位数 (np.percentile(boot_medians, 10))
  • 右端点:第90百分位数 (np.percentile(boot_medians, 90))

与95%置信区间相比,80%置信区间会更(例如从75,000美元到85,000美元)。这体现了置信水平区间精度之间的权衡:

  • 更高的置信水平(如95%):区间更宽,你更有可能捕获真实参数,但陈述的精确性较低。
  • 更低的置信水平(如80%):区间更窄,你的估计更精确,但捕获真实参数的信心(概率)更低。

“置信”这个词很贴切:你对一个更精确(更窄)的猜测信心较低,而对一个更保守(更宽)的猜测信心较高。


📝 本节课总结

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

  1. 核心问题:如何评估基于单一样本的参数估计的可靠性。
  2. 自助法原理:通过从已有样本中进行有放回重抽样,来模拟从总体中多次抽样的过程,从而近似统计量的抽样分布。
  3. 置信区间构建:利用自助法得到的分布和百分位数的概念,构建特定置信水平(如95%)的区间估计,以量化估计的不确定性。
  4. 关键权衡:置信区间的宽度(精度)与置信水平(可靠性)之间存在权衡关系。

自助法是一个强大的工具,它允许我们仅利用单一样本数据就对估计的变异性做出推断。然而,它的有效性依赖于初始样本的质量,因此获取一个具有代表性的大样本至关重要。

17:置信区间解读与中心/离散度测量 📊

在本节课中,我们将学习如何解读通过自助法(Bootstrapping)创建的置信区间,理解这种方法的适用场景与局限性。随后,我们将介绍衡量数据分布的两个关键概念:中心趋势(如均值和中位数)和离散度(如标准差)。


置信区间的解读

上一节我们介绍了如何使用自助法创建置信区间来估计总体参数(如中位数)。本节中,我们来看看如何正确解读这些区间,并理解“95%置信度”的确切含义。

我们通过以下三步流程创建了一个置信区间:

  1. 从总体中获取一个样本。
  2. 对该样本进行自助重采样,生成大量重采样样本。
  3. 基于这些重采样样本的统计量(如中位数)分布,取中间95%的范围作为置信区间。

我们得到的区间(例如70,000到86,000美元)包含了自助重采样中位数的中间95%。由于样本统计量应该与总体参数相似,我们相当确信真实的总体中位数也会落在这个区间内。

以下是关于置信区间含义的关键点:

  • 置信度在于过程,而非单个区间: “95%置信度”意味着,如果我们能多次重复上述三步流程(获取新样本、自助法、构建区间),那么大约有95%这样构建出来的区间会包含真实的总体参数。
  • 任何单个区间都可能出错: 我们无法保证基于手头这一个样本构建的特定区间一定包含真值。它可能属于那“幸运”的95%,也可能属于“不幸”的5%。我们无法知晓。
  • 类比说明: 想象蒙眼玩套圈游戏。柱子(真实参数)是固定的。每次扔出的圈(基于一个样本构建的区间)可能套中,也可能套不中。95%置信度意味着,如果你以这种方式扔很多次圈,大约有95%的圈会套中柱子。但这不保证你下一次扔出的特定圈一定能套中。

为了直观展示,我们模拟了200次从总体中抽样并构建置信区间的过程。结果显示,大约97%(本例中略高于95%)的区间包含了真实的总体中位数(蓝色竖线)。这验证了该流程的可靠性。


自助法的适用性与局限性

自助法功能强大且实用,因为它仅需一个样本就能模拟统计量的可能分布。然而,它并非万能,存在一些局限性。

以下是自助法的主要局限性:

  1. 不适用于敏感统计量: 对于最大值、最小值这类对样本构成极其敏感的统计量,自助法效果很差。因为重采样不会引入原始样本之外的数据,导致自助分布严重低估或高估总体中的极端值。
    • 示例: 尝试用自助法估计城市员工的最高薪水。由于原始样本的最高薪水可能远低于总体最高薪水,导致所有重采样样本的最高薪水都受限于此,最终置信区间会严重偏离真实值。
    • 代码示例: 若将计算中位数的代码改为计算最大值,会得到误导性的结果。
      # 错误示例:对最大值使用自助法
      boot_max = np.array([])
      for i in np.arange(5000):
          resample = original_sample.sample(500, replace=True)
          resample_max = resample.get('Salary').max()
          boot_max = np.append(boot_max, resample_max)
      
  2. 依赖于原始样本的质量: 自助法假设原始样本是总体的良好近似。如果原始样本存在偏差(例如,采用方便抽样而非随机抽样),那么自助法只会放大这种偏差,导致构建的区间也不准确。因此,确保采用良好的抽样方法至关重要。

中心趋势的度量:均值与中位数

现在,我们切换话题,讨论如何描述数据分布的特征。首先来看中心趋势的度量,即“典型值”是什么。我们已经熟悉了均值(Mean)和中位数(Median)。

  • 中位数: 将数据排序后位于正中间的值。在直方图上,中位数是使左右两侧面积各占50%的那个点。
  • 均值: 所有数据的平均值。在直方图上,均值是分布的“平衡点”。

理解均值与中位数在对称和偏态分布中的关系非常重要:

  • 在对称分布中: 均值与中位数相等。
  • 在右偏分布(长尾在右)中: 均值 > 中位数。右侧的极端值会拉高均值。
  • 在左偏分布(长尾在左)中: 均值 < 中位数。左侧的极端值会拉低均值。

示例: 航班延误时间通常是右偏分布。大多数航班准点或稍有延误(数据集中在左侧),但少数航班延误非常久(长尾在右侧)。这使得均值(受极端值影响大)通常大于中位数(对极端值不敏感)。


离散度的度量:标准差

除了中心,我们还需要度量数据的离散或分散程度。一个简单的方法是使用全距(最大值减最小值),但它只关注两个极端值,无法描述大部分数据的分布情况。

标准差(Standard Deviation) 解决了这个问题,它衡量的是数据点相对于均值的“典型”距离。计算步骤如下:

  1. 计算数据集的均值。
  2. 计算每个数据点与均值的差,称为“偏差”(Deviation)。
  3. 由于偏差有正有负,直接求平均会相互抵消为0。因此,我们将每个偏差平方以消除负号。
  4. 计算这些平方偏差的平均值,得到方差(Variance)
  5. 对方差取平方根,将其单位还原到与原始数据相同,这个结果就是标准差

公式表示
对于一个数据集 $ x_1, x_2, ..., x_n $,其均值 $ \mu $ 和标准差 $ \sigma $ 的计算如下:

\[\mu = \frac{1}{n} \sum_{i=1}^{n} x_i \]

\[\sigma = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (x_i - \mu)^2} \]

标准差越大,表示数据点越分散;标准差越小,表示数据点越集中在均值附近。


总结

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

  1. 置信区间的正确解读: 95%置信度描述的是重复抽样和构建区间这一过程的长期成功率,而非特定区间包含真值的概率。
  2. 自助法的局限性: 自助法不适用于估计最大值、最小值等敏感统计量,且其效果严重依赖于原始样本的代表性。
  3. 中心趋势的度量: 比较了均值和中位数,理解了在偏态分布中均值会偏向长尾方向。
  4. 离散度的度量: 引入了标准差的概念,了解了它作为衡量数据点相对于均值典型距离的用途。

这些概念为我们后续学习数据分布的形状(如正态分布)以及更深入的统计推断打下了基础。


18:切比雪夫不等式与正态分布 📊

在本节课中,我们将学习一个重要的不等式——切比雪夫不等式,并深入了解一种在自然界和数据科学中极为常见的分布形态:正态分布。


📈 回顾:方差与标准差

上一节我们介绍了方差和标准差的概念。它们用于衡量数据集的离散程度。

对于一个数据集,我们首先计算每个数据点与均值(平均值)的差值,这称为“偏差”。偏差有正有负,直接求平均会相互抵消。因此,我们先将所有偏差平方,再求平均值,这个结果就是方差

方差公式
方差 = (所有偏差的平方和) / (数据点数量)

方差衡量了数据的离散程度,但其单位是原始单位的平方。为了得到一个与原始数据单位相同的度量,我们对方差取平方根,得到标准差

标准差公式
标准差 = sqrt(方差)

标准差越大,意味着数据越分散。我们可以使用Python的NumPy库轻松计算标准差:

import numpy as np
data = [2, 3, 3, 9]
std_dev = np.std(data)  # 计算标准差

🎯 切比雪夫不等式

我们定义了标准差这个数字,它的意义何在?其中一个关键作用是,它可以告诉我们,需要从数据中心(均值)向外移动多远,才能“捕获”大部分数据。

无论数据分布的形状如何(直方图是何种奇怪形状),均值是它的平衡点。标准差就像一个合理的“跳跃”距离,你只需要向外跳跃几次(几个标准差),就能收集到大部分数据。

例如,比较期中考试成绩和薪水。考试成绩的标准差可能在5到10分之间,而薪水的标准差可能高达10000美元。这是因为数据本身所处的尺度不同。

我们可以将上述直觉表述得更精确,这就是切比雪夫不等式

切比雪夫不等式
在任何数值分布中,落在区间 [均值 - z * 标准差, 均值 + z * 标准差] 内的数据比例至少1 - 1/z²,其中 z 是任意大于1的数。

这个不等式给出了一个保证的下限。例如:

  • z = 2 时,至少有 1 - 1/4 = 75% 的数据落在均值±2个标准差的范围内。
  • z = 3 时,至少有 1 - 1/9 ≈ 88.9% 的数据落在均值±3个标准差的范围内。
  • z = 5 时,至少有 1 - 1/25 = 96% 的数据。

注意,z=1 时不等式成立但无意义(下限为0),因此通常使用 z >= 2。这个不等式非常通用,适用于任何形状的分布。


✈️ 实例:航班延误数据

让我们在航班延误数据上应用切比雪夫不等式。该数据集的平均延误时间约为16分钟。

我们首先计算延误时间的标准差:

std_delay = np.std(flight_data[‘delay’]) # 结果约为40分钟

根据切比雪夫不等式:

  • 至少有75%的航班延误落在 [16 - 2*40, 16 + 2*40],即 [-64, 96] 分钟区间内。
  • 至少有88.9%的航班延误落在 [16 - 3*40, 16 + 3*40],即 [-104, 136] 分钟区间内。

我们可以通过查询实际数据来验证:

# 计算落在红色条(±2个标准差)内的航班比例
within_two_std = flight_data.query(‘delay >= @(mean_delay - 2*std_delay) and delay <= @(mean_delay + 2*std_delay)’)
proportion = within_two_std.shape[0] / flight_data.shape[0] # 实际比例约为95.6%

实际比例(95.6%)高于切比雪夫不等式保证的下限(75%),这完全符合预期,因为不等式给出的是最坏情况下的保证。如果我们对数据的分布形状有更多了解,就可以做出更精确的断言。接下来,我们就来看一种特殊的分布。


🔔 正态分布

现在让我们转向一种新的数据集:5000名成年男性的身高和体重。分别绘制它们的直方图,会发现一个有趣的现象:两者具有相似的形状——都是中间高、两边低,呈单峰对称的“钟形”曲线。

尽管形状相似,但两个分布并不相同:

  • 中心位置不同:身高的均值约69英寸,体重的均值约187磅。
  • 离散程度不同:身高的数据更集中,体重的数据更分散。

这种形状的分布被称为正态分布高斯分布,其曲线被称为钟形曲线。自然界中许多现象(如身高、体重、睡眠时间)都近似服从正态分布。

正态分布由一个均值和一个标准差唯一确定。均值决定中心位置,标准差决定曲线的“胖瘦”或分散程度。因此,存在无数种不同的正态分布。


📏 标准化

虽然身高和体重的原始分布不同,但它们的形状相同。我们可以通过一种称为标准化的转换,将它们调整到同一个尺度和中心上。

标准化的过程如下:对于数据中的每一个原始值 x_i,我们减去整个数据集的均值 μ,再除以标准差 σ

标准化公式
标准单位值 = (x_i - μ) / σ

这个计算结果的解释是:

  • (x_i - μ):该值高于(或低于,如果为负)均值的原始单位数。
  • 除以 σ:将单位从“原始单位”转换为“标准差个数”。结果是一个无量纲的数字,表示该值高于均值多少个标准差。

例如,某人体重225磅。已知平均体重 μ = 187 磅,标准差 σ ≈ 19 磅。

  1. 计算高于均值的磅数:225 - 187 = 38 磅。
  2. 转换为标准差个数:38 / 19 = 2
    这意味着此人的体重比平均体重高2个标准差

对整个数据集的列进行标准化后,新数据具有两个重要性质:

  1. 均值为0:因为我们减去了原始均值。
  2. 标准差为1:因为我们除以了原始标准差。

如果我们分别绘制标准化后的身高和体重分布,会发现它们几乎完全重合!这是因为标准化过程消除了原始数据在中心和尺度上的差异,只保留了共同的“形状”。这个均值为0、标准差为1的特殊正态分布,称为标准正态分布


📊 标准正态分布与面积计算

标准正态分布是一条完美的、光滑的钟形曲线。与直方图类似,曲线下的总面积等于1。曲线在任意两点之间的面积,代表了数据落在该区间的比例。

那么,如何计算这条光滑曲线下的面积呢?这通常需要微积分,但我们有一个更简单的方法:使用Python函数 scipy.stats.norm.cdf()CDF 是“累积分布函数”的缩写,对于标准正态分布,norm.cdf(z) 给出了曲线下在 z左侧的面积(比例)。

核心函数

from scipy.stats import norm
area_left = norm.cdf(z)  # 计算标准正态曲线下,z点左侧的面积

利用这个函数和一些技巧,我们可以计算各种面积:

  • 右侧面积1 - norm.cdf(z)
  • 区间面积norm.cdf(b) - norm.cdf(a),即 [a, b] 区间内的面积。
  • 利用对称性:标准正态分布关于0对称。因此,norm.cdf(-z) 等于 z 点右侧的面积。

🔄 应用:估计比例

假设我们想知道体重在200磅到225磅之间的男性比例。我们已知体重近似正态分布,均值 μ = 187,标准差 σ = 19

解决步骤:

  1. 标准化:将边界值转换为标准单位。
    • 200 磅转换为:(200 - 187) / 19 ≈ 0.68
    • 225 磅转换为:(225 - 187) / 19 ≈ 2.00
  2. 问题转化:原问题等价于求标准正态分布在区间 [0.68, 2.00] 下的面积。
  3. 面积计算
    area_between = norm.cdf(2.00) - norm.cdf(0.68) # 结果约为23%
    

因此,我们估计大约有23%的男性体重处于200-225磅之间。通过查询原始数据计算的实际比例与此非常接近,这验证了体重分布非常近似于正态分布。

重要提示:标准化本身不会将一个分布变成正态分布。它只是进行平移和缩放。上述方法仅在原始数据本身近似正态分布时才有效。


⚖️ 正态分布与切比雪夫不等式

现在我们可以联系起本节课开头的两个主题。切比雪夫不等式对任何分布都给出了一个保守的(较低的)比例下限。但对于正态分布这种特定形状,我们可以得到更精确、更高的比例。

以下是两者的对比:

标准差倍数 (z) 切比雪夫不等式 (至少) 正态分布 (约)
1 0% 68%
2 75% 95%
3 88.9% 99.7%

对于正态分布,大约有68%的数据落在均值±1个标准差内,95%落在均值±2个标准差内,99.7%落在均值±3个标准差内。这些精确的比例来自于标准正态分布CDF的计算。正态分布的数据更集中于中心,因此当偏离同样距离时,能捕获的数据比例远高于切比雪夫不等式给出的通用下限。


📍 拐点

最后介绍一个有助于直观理解标准差的概念:拐点。拐点是曲线改变弯曲方向的点(例如从“微笑”的凸形变为“皱眉”的凹形)。

对于标准正态分布,其拐点恰好位于 z = ±1 的位置。这意味着,对于任何正态分布曲线,其拐点总是位于均值 ± 1个标准差处。

这个性质可以帮助我们目测标准差。例如,在身高分布图中,均值约69英寸,目测拐点大约在66英寸和72英寸,两者与均值相差约3英寸,这提示我们身高的标准差大约就是3英寸。


📝 总结

本节课我们一起学习了两个核心内容:

  1. 切比雪夫不等式:一个强大的通用工具,它告诉我们,对于任何分布,只需从均值向外移动几个标准差,就一定能捕获大部分数据(有明确的比例下限保证)。
  2. 正态分布:一种在自然界中常见的钟形分布。我们学习了如何通过标准化将任何正态分布转化为标准正态分布(均值为0,标准差为1)。利用标准正态分布的累积分布函数,我们可以计算数据落在任意区间的比例。与切比雪夫不等式的通用下限相比,正态分布允许我们做出更精确的比例估计。

理解这些概念,将为我们后续学习统计推断(如置信区间)打下坚实的基础。

19:中心极限定理与置信区间 📊

在本节课中,我们将要学习中心极限定理,并了解它如何帮助我们理解样本均值的分布,以及如何利用它来构建置信区间,而无需进行大量的自助法重采样计算。

回顾:标准单位

上一节我们介绍了标准单位的概念。标准单位用于衡量一个数值相对于其数据集的平均值有多少个标准差。其公式为:

标准单位 = (数值 - 平均值) / 标准差

对于任何数值型数据集,标准单位计算的是该数值高于平均值多少个标准差。我们可以将其可视化:平均值是中心点,一个标准单位对应一个标准差的跳跃。标准单位也可以是负值,表示数值低于平均值。

中心极限定理介绍

现在,我们来看一个重要的定理——中心极限定理。为了理解它,我们使用之前分析过的航班延误分布数据。这个分布本身并非正态分布(例如,呈右偏态),但当我们从中抽取大量随机样本并计算每个样本的均值时,这些样本均值的分布会呈现出有趣的规律。

样本均值的分布

如果我们从一个总体(如所有航班延误数据)中反复抽取相同大小的随机样本(例如,每次抽取500个航班),并计算每个样本的均值,那么这些样本均值的分布会如何?

通过模拟这个过程(抽取2000个样本,每个样本大小为500),我们得到了样本均值的分布直方图。这个分布呈现出明显的钟形曲线形状,即近似正态分布。有趣的是,尽管原始总体分布(航班延误)不是正态的,但样本均值的分布却是正态的。

这个现象就是中心极限定理的核心内容。

中心极限定理的表述

中心极限定理指出:当从总体中抽取大样本(样本量足够大)并计算其均值(或总和)时,这些样本均值的分布将近似服从正态分布。无论原始总体本身的分布形状如何,这个结论都成立。

在我们的例子中,总体是严重右偏的航班延误分布,但样本均值的分布却变成了漂亮的钟形曲线。

分布的中心与展布

这个正态分布(样本均值的分布)有两个关键特征:

  1. 中心:它以总体均值为中心。这是因为随机抽样是无偏的,样本均值有时会偏高,有时会偏低,但平均而言会集中在总体均值附近。

  2. 展布(标准差):其展布(标准差) 由以下公式决定,这被称为平方根法则

    样本均值分布的标准差 = 总体标准差 / √样本量

    这意味着,样本量越大,样本均值分布就越狭窄,样本均值作为总体均值的估计就越精确。

重要区分:这里讨论的“标准差”是指样本均值这个统计量的分布的标准差,而不是单个样本内部数值的标准差。单个大样本本身可能包含更多极端值,其内部标准差可能更大。

从理论到实践:利用单一样本

上述理论很完美,但依赖于我们知道总体参数(总体均值、总体标准差)。在实践中,我们通常只有一个样本。如何应用中心极限定理呢?

思路与自助法类似:我们假设一个大的随机样本可以很好地代表总体。因此,我们可以用样本统计量来近似替代未知的总体参数

具体来说:

  • 样本均值 (sample_mean) 来估计中心极限定理分布的中心(即总体均值)。
  • 样本标准差 (sample_sd) 来替代平方根法则中的总体标准差。

由此,我们可以预测,如果对原始样本进行多次自助重采样,这些重采样样本的均值分布将近似服从以下正态分布:

  • 中心sample_mean
  • 标准差sample_sd / √n (其中 n 是原始样本量)

通过对比可以发现,根据这个公式绘制的正态曲线与通过实际自助法重采样5000次得到的样本均值分布直方图几乎完全重合。这证明了中心极限定理为我们提供了一种无需大量计算即可估计样本均值分布的有效捷径。

构建置信区间

我们研究样本均值分布的主要目的之一是构建总体均值的置信区间。之前我们使用自助法:通过重采样得到统计量的分布,然后取中间95%的范围作为置信区间。

利用中心极限定理,我们可以更直接地计算置信区间。我们知道样本均值的分布近似为正态分布 N(sample_mean, sample_sd/√n)。对于正态分布,约有95%的数据落在“均值 ± 2个标准差”的范围内。

因此,总体均值的一个近似95%置信区间可以通过以下公式计算:

置信区间 = [ sample_mean - 2 * (sample_sd / √n), sample_mean + 2 * (sample_sd / √n) ]

对比自助法与中心极限定理

以下是两种方法的对比:

  • 自助法:通过模拟重采样得到经验分布,通用性强(也适用于中位数等),但结果略有随机性,计算量较大。
  • 中心极限定理法:利用公式直接计算,仅适用于样本均值(或总和),结果确定且计算快捷。

在实践中,对于均值问题,推荐使用中心极限定理法;对于其他统计量(如中位数),则需使用自助法。

调整置信水平

上述公式中的数字“2”对应95%的置信水平。如果需要不同置信水平(例如80%或99%)的区间,需要调整这个乘数(通常记为z)。置信水平越低,所需的z值越小(区间越窄);置信水平越高,所需的z值越大(区间越宽)。可以通过标准正态分布表来查找特定置信水平对应的z值。

总结

本节课我们一起学习了中心极限定理及其在推断统计学中的应用。

  1. 中心极限定理:无论总体分布如何,大样本均值的分布近似服从正态分布。
  2. 定理的关键:该正态分布以总体均值为中心,其标准差为 总体标准差 / √样本量
  3. 实践应用:通过单一样本,我们可以用样本均值和样本标准差来估计上述分布,从而绕过复杂的自助法模拟。
  4. 构建置信区间:利用该正态分布的性质,我们可以使用公式 样本均值 ± z * (样本标准差 / √样本量) 来快速计算总体均值的置信区间。

中心极限定理是统计学中一个强大而实用的工具,它使我们对样本均值的变异性有了深刻的理解,并提供了进行统计推断的坚实理论基础。

20:中心极限定理、样本量选择与统计模型入门 📊

在本节课中,我们将要学习中心极限定理(CLT)的实际应用,特别是如何利用它来确定满足特定精度要求的样本量。随后,我们将引入“统计模型”的概念,学习如何通过数据来检验一个假设或模型是否合理。


🔍 中心极限定理回顾

上一节我们介绍了中心极限定理的基本思想。本节中,我们来看看如何将其与样本量选择联系起来。

中心极限定理描述了从总体中抽取样本时,样本均值的分布规律。具体来说:

  • 我们有一个总体(分布A)。
  • 从中抽取一个样本(分布B)。
  • 如果我们抽取许多样本,并计算每个样本的均值,这些样本均值会形成一个新的分布(分布C)。

中心极限定理指出,无论原始总体是什么形状,只要样本量足够大,这个样本均值的分布(分布C)就会近似于正态分布(钟形曲线)

关于这三个分布,我们知道:

  • 中心位置:分布A(总体)、分布B(样本)和分布C(样本均值分布)的均值大致相同。
  • 离散程度:分布A和B的离散程度(标准差)相似。但分布C的离散程度要小得多。
  • 计算公式:分布C的标准差等于分布A的标准差除以样本量的平方根。公式表示为:
    SD(分布C) = SD(总体) / sqrt(样本量)
    实践中,我们不知道总体的标准差,因此用样本的标准差来近似。

中心极限定理为我们提供了一种理解样本均值分布的捷径,无需进行大量的自助法(Bootstrap)重采样计算。


🎯 利用CLT选择样本量

现在,我们来看看如何利用中心极限定理来解决一个实际问题:为了达到特定的估计精度,我们需要多大的样本量?

问题场景

假设我们想估计UCSD学生中使用语言学习App“Duolingo”的比例(一个总体参数)。我们计划随机调查一部分学生(是/否问题),并希望构建一个95%置信区间。我们要求这个置信区间的宽度不超过0.06

这意味着,如果我们最终估计比例在21%到27%之间(宽度0.06),这个精度是可以接受的。但如果区间是21%到28%(宽度0.07),则被认为信息不够精确。

从置信区间宽度出发

根据中心极限定理,一个95%置信区间的构造公式为:
样本均值 ± 2 * (样本标准差 / sqrt(样本量))

因此,置信区间的总宽度为:
宽度 = 4 * (样本标准差 / sqrt(样本量))

我们希望这个宽度 ≤ 0.06,即:
4 * (样本标准差 / sqrt(样本量)) ≤ 0.06

解决“先有鸡还是先有蛋”的困境

我们需要解出“样本量”,但公式中的“样本标准差”在真正抽取样本之前是未知的。为了解决这个问题,我们考虑样本标准差所有可能取值中的最大值。使用最大值是保守的做法,能确保计算出的样本量一定足够大。

我们的数据是0(不使用)和1(使用)的集合。对于这种0/1数据,其标准差有一个确定的上限。

以下是0/1数据标准差的计算公式及其特性:

  • 公式:对于一组0和1的数据,其标准差 = sqrt(比例_0 * 比例_1)
  • 特性:当数据中0和1各占一半(比例均为0.5)时,标准差达到最大值0.5。如果数据全为0或全为1,则标准差为0。

因此,在调查之前,我们可以保守地使用0.5作为样本标准差的最大可能值。

计算所需样本量

将样本标准差的最大值0.5代入不等式:
4 * (0.5 / sqrt(样本量)) ≤ 0.06

解这个不等式:

  1. 两边乘以 sqrt(样本量)4 * 0.5 ≤ 0.06 * sqrt(样本量)
  2. 两边除以0.06:(4 * 0.5) / 0.06 ≤ sqrt(样本量)
  3. 计算左边:2.0 / 0.06 ≈ 33.33
  4. 两边平方:样本量 ≥ (33.33)^2 ≈ 1111.11

由于样本量必须是整数,我们需要向上取整。因此,最小的所需样本量是1112

这意味着,为了有95%的信心使我们的估计区间宽度不超过0.06,我们需要至少调查1112人。

理解公式中的数字

  • 4:源于构建95%置信区间时,需要在均值两侧各延伸约2个标准误。
  • 0.5:0/1数据标准差的最大可能值。
  • 0.06:我们期望的置信区间最大宽度。
  • 平方:源于中心极限定理公式中的平方根,解方程时需要进行平方运算。

如果需要更高的精度(例如,宽度要求更小),所需的样本量会急剧增加。例如,如果要求宽度为0.03(精度提高一倍),根据公式计算,所需样本量将是原来的4倍,即至少需要4445人。这体现了提高估计精度需要付出巨大的数据收集成本。


🤔 引入统计模型:提出“是/否”问题

到目前为止,我们专注于参数估计(如使用自助法或CLT构建置信区间)。现在,我们将转向另一类统计推断问题:回答关于样本与总体之间关系的“是/否”问题

以下是两个我们将要研究的例子:

  1. 某个陪审团名单是否是从符合条件的陪审员总体中随机抽取的?
  2. 一系列硬币抛掷结果是否看起来来自一枚公平的硬币?

这些问题都属于统计推断,因为它们都使用样本数据来对总体或数据生成过程进行推断。

什么是统计模型?

一个统计模型是一套关于数据来源的假设。

  • 例如,对于一枚在地上捡到的硬币,我们的模型(假设)是“这是一枚公平的硬币”。
  • 正如一句名言所说:“所有的模型都是错的,但有些是有用的。”模型是对现实世界的简化,一个好的近似模型非常有价值。

我们的新任务是评估一个模型。我们有一些先验的假设(模型),然后通过收集数据来检验这些假设是否与事实相符。

  • 如果数据与模型假设高度一致,我们可能维持原假设。
  • 如果数据与模型假设严重矛盾,我们就需要拒绝原假设,因为数据代表现实。

伽利略挑战“重物下落更快”的旧观念就是一个经典例子。他通过实验(数据)证明了旧模型(假设)是错误的。


⚖️ 案例研究:Swain诉阿拉巴马州案(1965年)

让我们通过一个真实的法律案例来实践模型评估。

背景

罗伯特·斯温(Robert Swain)是一名黑人男性,在1965年被判有罪。他上诉至最高法院,理由是所在县系统性地将黑人排除在陪审团名单之外,导致陪审团不能代表社区人口构成。

陪审团筛选过程

  1. 合格总体:有资格担任陪审员的人群(当时主要是21岁以上的男性公民)。
  2. 陪审团名单:从合格总体中随机抽取的人员,被传唤参加陪审团选拔。
  3. 最终陪审团:从名单中经过询问等(非完全随机)过程选出的人员。

斯温案的关键在于第一步。在他所在的县,合格总体中约有26%是黑人。如果随机抽取,陪审团名单中也应有约26%的黑人。

数据与模型

  • 观察到的数据:斯温的陪审团名单上有100名男性,其中只有8名黑人(8%)
  • 我们的模型(假设):陪审团名单是从一个26%为黑人的总体中随机抽取的。

核心问题:我们观察到的数据(8/100)是否与我们的模型相符?或者说,在模型成立的假设下,观察到如此少黑人的情况是常见的还是极不寻常的?

通过模拟进行评估

我们的方法是模拟。我们假设模型成立(即随机抽取),然后重复模拟抽取100人陪审团名单的过程成千上万次,每次记录抽到的黑人数。

以下是使用Python进行单次模拟的核心代码:

# 假设总体中黑人的比例是0.26
# 使用多项分布模拟抽取100人
demographics = [0.26, 0.74] # [黑人比例, 非黑人比例]
结果 = np.random.multinomial(100, demographics)
黑人数量 = 结果[0]

我们重复此过程多次(例如10,000次),并统计所有结果。

模拟结果与结论

模拟结果显示,在10,000次随机抽取中,从未得到过像8这么少的黑人数(最少也有10人左右)。典型的黑人数在20-35人之间。

这意味着,如果陪审团名单真是随机抽取的,观察到仅有8名黑人的概率极低。因此,数据强烈反驳了“随机抽取”的模型假设。实际情况更可能是存在系统性的种族排除。这个分析表明,当年最高法院认为“8%与26%的总体差异不大”的结论在统计上是站不住脚的。

这种模拟方法是一种强大的工具,它允许我们用数据来挑战和检验任何假设或模型的合理性。


📝 总结

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

  1. 中心极限定理的应用:理解了样本均值分布的规律,并学会了如何利用它来构建置信区间。
  2. 样本量选择:掌握了如何根据期望的置信区间宽度和置信水平,计算所需的最小样本量。关键在于理解0/1数据标准差的最大值,并保守地用于计算。
  3. 统计模型入门:引入了统计模型的概念,即关于数据生成过程的假设集。
  4. 模型评估:学习了通过模拟方法来评估一个模型是否与观察数据相符。我们以Swain诉阿拉巴马州案为例,展示了如何用数据有力地挑战一个关于“随机性”的假设。

这些工具将帮助我们不仅能够估计未知参数,还能对关于世界的具体假设进行严格的检验。

21:假设检验入门 🧪

在本节课中,我们将学习假设检验的基本框架。我们将通过两个具体例子——陪审团选拔和硬币公平性检验——来理解如何使用数据评估模型假设,并学习如何选择和使用检验统计量。


回顾:什么是统计模型? 🔍

上一节我们介绍了统计模型的概念。模型是我们对数据生成方式的假设。

在罗伯特·斯旺(Robert Swain)的陪审团案例中,我们的模型假设是:陪审团是从符合条件的总人口中随机抽取的,而该人口中黑人比例为26%。我们收集到的实际数据是:一个100人的陪审团中只有8名黑人。我们的目标是判断,实际数据是否与模型假设一致。

我们的方法是进行模拟。如果模型为真(即确实是从26%为黑人的总体中随机抽取),我们模拟抽取成千上万个100人的陪审团,并计算每个陪审团中的黑人数。然后,我们将模拟结果与实际观察到的数字(8)进行比较。


模拟工具:np.random.multinomial 🛠️

以下是模拟抽取单个陪审团的具体方法。我使用了一个名为 np.random.multinomial 的新函数。

该函数帮助我们从已知的分类分布中进行随机抽样。它适用于我们不知道总体具体大小,但知道各类别比例的情况。

其用法如下:

np.random.multinomial(样本大小, 总体分布)

参数 总体分布 是一个数组或序列,表示每个类别的概率,总和应为1。

该函数的作用是:根据给定的概率分布,有放回地随机抽取个体。

对于陪审团例子,总体分布是 [0.26, 0.74](26%黑人,74%非黑人)。运行以下代码:

demographics = [0.26, 0.74]
np.random.multinomial(100, demographics)

输出是一个包含两个值的数组,例如 [24, 76]。第一个值代表样本中属于第一个类别(黑人)的数量,第二个值代表属于第二个类别(非黑人)的数量。由于我们抽取了100个样本,数组中的值总和总是100。

我们真正感兴趣的是第一个数字(黑人数),可以通过索引 [0] 提取。


案例一:陪审团选拔的假设检验 ⚖️

我们编写了以下代码来模拟抽取一个陪审团,并计算其中的黑人数:

def count_black_in_panel():
    demographics = [0.26, 0.74]
    sample = np.random.multinomial(100, demographics)
    return sample[0]

然后,我们通过一个循环重复此过程数千次,将所有结果存入一个名为 counts 的数组,并绘制了直方图。

模拟结果显示,在10000次试验中,黑人数最少为11,从未出现过8。我们观察到的实际数据(8)在模拟中极其罕见。

因此,我们的结论是:模拟表明,从该总体中随机抽取100人,得到像罗伯特·斯旺陪审团那样(仅8名黑人)的结果可能性极低。既然真实数据确实如此,我们认为关于数据来源的假设(随机抽样)很可能是错误的。有其他因素(而非偶然性)导致了陪审团中黑人数量过少。


引入假设检验框架 📐

现在,我们将使用相同的框架来分析另一个例子,并在此过程中引入一些术语。

这个例子是:你在地上发现一枚硬币,不确定它是否公平。你通过多次抛掷来检验。

假设你抛掷了400次,得到了212次正面和188次反面。问题是:基于这个数据,我们认为硬币是公平的吗?

这引出了假设检验的概念。假设检验是一种基于样本数据,在两个不同的世界观(即假设)之间做出选择的框架。

两个假设分别称为原假设备择假设。其中一个假设(通常是原假设)必须是一个定义明确的概率模型,说明数据是如何生成的。“定义明确”意味着它必须有具体的数字,因为我们需要用它进行模拟。

在硬币例子中:

  • 原假设 (H₀):硬币是公平的(正面概率=50%,反面概率=50%)。这是一个精确的假设。
  • 备择假设 (H₁):硬币是不公平的。这是一个不精确的假设,因为它包含多种可能性(正面概率可以是任何非50%的值)。

案例二:检验硬币是否公平 🪙

我们的方法是:首先假设原假设为真(即硬币公平)。然后模拟抛掷一枚公平硬币400次,记录正面次数。我们重复此模拟多次。

我们最初选择的检验统计量正面次数。在实际数据中,我们观察到的正面次数是188,这是我们的观察统计量

我们通过模拟生成数千个“在公平假设下抛掷400次得到的正面次数”,并绘制其分布直方图。我们发现,观察到的188次正面位于分布的中间区域,并不罕见。

为了更清晰地进行决策,我们希望将分布划分为两个区域:一个区域的值支持“硬币公平”,另一个区域的值支持“硬币不公平”。然而,正面次数的分布是对称的,中间值(接近200)支持公平,而极高或极低的值都支持不公平。

解决方案是“折叠”分布。我们不再使用“正面次数”作为检验统计量,而是使用“正面次数与期望值200的绝对差值”。即:

test_statistic = abs(number_of_heads - 200)

对于观察数据,这个新的检验统计量值为 abs(188 - 200) = 12

我们再次进行模拟,但这次计算并绘制这个新统计量的分布。这个分布是右偏的,大部分值集中在0附近(即接近200次正面),值越大表示越极端。

在模拟分布中,我们观察统计量12的位置。我们发现,出现12或更大差值的情况并不少见。因此,我们观察到的数据与原假设(硬币公平)是相容的。


假设检验的决策与术语 🧮

假设检验的决策基于比较观察统计量与模拟统计量的分布。

  • 如果观察统计量落在模拟分布的极端区域(即,在模拟中,出现像观察值一样极端或更极端值的情况非常罕见),那么我们就有证据反对原假设。我们拒绝原假设
  • 如果观察统计量落在模拟分布的常见区域(即,出现像观察值一样极端或更极端值的情况很常见),那么数据与原假设是相容的。我们未能拒绝原假设。注意,我们不说“接受”原假设,只是说目前没有足够证据拒绝它。

在硬币公平性检验中,观察到的绝对差值为12,这属于常见情况,因此我们“未能拒绝”硬币公平的原假设。


检验统计量的选择取决于备择假设 🎯

检验统计量的选择至关重要,它取决于我们想要区分的具体备择假设。

之前,我们的备择假设是“硬币不公平”(任何方向的偏差)。我们使用了绝对差值 abs(number_of_heads - 200),因为大的差值(无论正负)都指向“不公平”。

现在,考虑一对新的假设:

  • 原假设 (H₀):硬币是公平的。
  • 备择假设 (H₁):硬币偏向反面。

此时,绝对差值就不再适用了,因为它无法区分偏差的方向(正面太多还是反面太多)。我们需要一个能特异性检测“偏向反面”的统计量。

一个合适的检验统计量是正面次数。因为如果硬币偏向反面,我们预期会看到较少的正面。所以:

  • 极低的正面次数 → 支持备择假设(偏向反面)。
  • 较高的正面次数 → 支持原假设(公平)或至少不支持“偏向反面”。

我们再次进行模拟,这次检验统计量就是正面次数。观察统计量仍是188。在公平硬币的模拟分布中,188次正面并不极端(不是特别低)。因此,我们再次“未能拒绝”原假设。如果我们观察到像172这样极低的正面次数,我们可能就会拒绝原假设,认为硬币偏向反面。


总结 📝

本节课我们一起学习了假设检验的基本原理。

  1. 核心思想:通过比较实际观察数据与在原假设下模拟生成的数据分布,来评估原假设的合理性。
  2. 关键步骤:建立原假设和备择假设 → 选择检验统计量 → 在原假设下模拟数据并计算统计量分布 → 将观察统计量与模拟分布比较并做出决策(拒绝或未能拒绝原假设)。
  3. 统计量选择:检验统计量的选择直接依赖于备择假设。我们需要确保统计量能有效区分两种假设。对于检测任何偏差(双尾检验),可以使用像绝对差值这样的统计量;对于检测特定方向的偏差(单尾检验),则需要选择能反映方向的统计量(如正面次数本身)。
  4. 决策术语:当数据极端时“拒绝原假设”;当数据不极端时“未能拒绝原假设”。

通过陪审团选拔和硬币公平性两个案例,我们实践了从模拟到决策的完整假设检验流程。理解如何根据问题背景选择合适的检验统计量,是掌握假设检验的关键。

22:假设检验与P值详解 📊

在本节课中,我们将深入学习假设检验的核心流程,并通过三个具体案例(硬币公平性检验、期中成绩分析和陪审团种族构成研究)来掌握如何设定假设、选择检验统计量、进行模拟计算P值,并基于此做出统计推断。


🪙 案例一:硬币公平性检验

上一节我们介绍了假设检验的基本框架。本节中,我们来看看如何将其应用于判断一枚硬币是否公平。

假设设定

首先,我们需要明确两种对立的观点:

  • 零假设 (H₀):硬币是公平的。即每次抛掷出现正面的概率为 0.5。
  • 备择假设 (H₁):硬币是不公平的。

零假设必须是具体、可模拟的。备择假设通常是你试图证明的情况。

检验统计量的选择

为了区分这两种假设,我们需要选择一个检验统计量。在模拟零假设(抛掷公平硬币)时,我们将反复计算这个统计量。

对于“公平 vs. 不公平”的检验,一个常用的统计量是正面次数与期望次数(200次)的绝对差值
| 观测到的正面次数 - 200 |

这个统计量的值域是 0 到 200。值越小(接近0),越支持硬币公平;值越大(接近200),越支持硬币不公平。

模拟与P值计算

接下来,我们在零假设(硬币公平)下进行模拟。例如,模拟抛掷一枚公平硬币400次,重复此过程10,000次,每次记录上述统计量的值,从而得到该统计量在零假设下的经验分布。

P值的定义是:在零假设为真的前提下,得到与观测数据同样极端或更极端结果的概率。

在我们的例子中,如果我们观测到170次正面,那么“更极端”意味着正面次数小于或等于170(因为更少的正面更支持“不公平”的备择假设)。P值就是模拟结果中,统计量值 ≤ 170 的比例。

决策与显著性水平

如何根据P值做决策?统计学中有一个常用但人为设定的阈值——显著性水平,通常设为 0.05。

  • 如果 P值 ≤ 0.05,我们认为观测结果在零假设下过于罕见,因此拒绝零假设,结论具有“统计显著性”。
  • 如果 P值 > 0.05,我们没有足够证据拒绝零假设,因此不拒绝零假设

例如,若某硬币抛400次得到188次正面,计算出的P值为0.125 > 0.05,我们则结论为“没有足够证据认为该硬币不公平”。

核心要点:P值计算的方向(左尾、右尾或双尾)取决于备择假设的方向。选择检验统计量时,应确保其极端值方向与备择假设一致。


📝 案例二:期中成绩分析

现在,我们将假设检验应用于现实教育数据,分析不同班级的期中平均分差异是否纯属偶然。

问题背景

在DSC10课程某学期中,四个班级参加了同一场考试。数据显示,C班的平均分(46.17)显著低于其他班级(约50分)。C班讲师想知道:这种差异是否可能只是随机抽样的结果?

假设设定

  • 零假设 (H₀):C班学生的成绩是从整个课程学生群体中随机抽样得到的。班级间无真实差异。
  • 备择假设 (H₁):C班的平均分低于随机抽样下的预期水平。

模拟与检验

我们通过模拟来评估零假设:

  1. 模拟过程:从全体学生名单中,无放回地随机抽取108名学生(C班实际人数),计算他们的平均分。
  2. 重复:将此过程重复大量次数(如10,000次),得到在零假设下“随机C班”平均分的分布。
  3. 检验统计量:直接使用样本平均分
  4. 计算P值:比较C班的实际平均分(46.17)与模拟分布。P值等于模拟中平均分 ≤ 46.17 的比例。

结果与解读

模拟结果显示,实际观测到的46.17分位于模拟分布的极端左尾。计算出的P值极小(<0.01),属于“高度统计显著”。

因此,我们拒绝零假设。有强有力的统计证据表明,C班的低平均分不太可能仅由随机抽样导致。可能的原因包括上课时间差异、学生选课偏好等非随机因素,但假设检验本身并不能告诉我们具体是哪种原因。


⚖️ 案例三:陪审团构成差异检验

最后,我们探讨一个更复杂的案例:如何量化比较两个分类分布之间的整体差异。

问题背景

美国公民自由联盟调查了某县陪审团名单的种族构成。他们拥有两份数据:

  1. 合格人口分布(该县居民的种族比例)。
  2. 实际陪审团名单的分布。

数据显示,某些种族在陪审团名单中的比例与合格人口比例存在肉眼可见的差异(例如亚裔比例偏高,非裔比例偏低)。问题是:这种差异是否超出了随机波动的范围?

假设设定

  • 零假设 (H₀):陪审团名单是从合格人口中随机抽取得到的。
  • 备择假设 (H₁):陪审团名单的种族构成不是从合格人口中随机抽取的结果。

检验统计量:总变异距离

挑战在于如何用一个数字来概括两个分布(五个种族类别)的整体差异。我们引入一个新的统计量——总变异距离

以下是计算总变异距离的步骤:

  1. 计算每个种族类别在“合格人口”与“陪审团名单”中的比例之差。
  2. 取这些差值的绝对值(避免正负抵消)。
  3. 将所有绝对差值相加
  4. 将总和除以2

公式表示为:
TVD = (1/2) * sum(| 合格人口比例_i - 陪审团名单比例_i |)

总变异距离的值介于0到1之间。值越大,表示两个分布差异越大;值为0表示两个分布完全相同。

后续检验思路

在下一节课中,我们将完成这个检验:

  1. 模拟:在零假设(随机抽样)下,从合格人口分布中反复生成与观测陪审团名单同大小的模拟样本。
  2. 计算:对每个模拟样本,计算其种族分布与合格人口分布之间的总变异距离。
  3. P值:将观测到的实际总变异距离与模拟得到的分布进行比较。P值等于模拟中总变异距离 观测值的比例(因为更大的距离更支持备择假设)。
  4. 决策:根据P值判断差异是否统计显著。

📚 本节课总结

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

  1. 假设检验的完整流程:从设定假设(H₀, H₁)、选择检验统计量,到模拟零假设分布、计算P值并做出推断。
  2. P值的核心概念:P值是零假设为真时,得到观测结果或更极端结果的概率。它是衡量证据强度的连续指标。
  3. 统计显著性:使用0.05或0.01等阈值进行二元决策是一种惯例,但报告P值本身能提供更多信息。
  4. 三类应用案例
    • 硬币检验:演示了方向性(单尾)检验。
    • 成绩分析:展示了如何检验样本均值是否来自已知总体。
    • 陪审团构成:引入了总变异距离,用于量化两个分类分布的整体差异,为比较分布提供了有力工具。

通过这三个案例,我们看到了假设检验如何帮助我们在数据存在随机性的情况下,对世界做出有理有据的判断。

23:假设检验进阶与置换检验 🧪

在本节课中,我们将学习如何比较两个分类分布,探索置信区间与假设检验之间的联系,并初步了解一种特殊的假设检验方法——置换检验。


总变异距离:比较分类分布 📊

上一节我们讨论了如何检验单一类别(如种族)的分布差异。本节中,我们来看看当数据涉及多个类别时,如何量化两个分类分布之间的整体差异。

总变异距离(Total Variation Distance, TVD)是一种用于比较两个分类分布相似性的方法。其核心思想是计算两个分布在每个类别上比例差的绝对值之和,然后除以2。

以下是计算TVD的步骤:

  1. 对于每个类别,计算两个分布的比例差。
  2. 取每个差的绝对值。
  3. 将所有绝对差相加。
  4. 将总和除以2。

公式表示为:
TVD = (1/2) * Σ |p_i - q_i|
其中 p_iq_i 分别是两个分布在类别 i 上的比例。

TVD可以解释为一个分布相对于另一个分布的“总超额量”或“总不足量”。例如,在陪审团案例中,TVD为0.14意味着某些族裔在陪审团中的比例比合格人口中的比例总共超出了14%。


置信区间与假设检验的联系 🔗

我们已经分别学习了置信区间和假设检验。现在,我们来看看当假设检验的零假设涉及总体参数等于某个特定值时,两者如何联系起来。

当零假设为“总体参数 = 某个值”,备择假设为“总体参数 ≠ 某个值”时,我们可以使用置信区间来进行检验。

方法如下:

  1. 根据样本数据,构建一个 (100 - p)% 的置信区间(例如,使用5%的显著性水平 p,则构建95%置信区间)。
  2. 观察零假设中声称的那个特定值是否落在这个置信区间内。
    • 如果该值不在置信区间内,则拒绝零假设。这意味着数据不支持参数等于该值的说法。
    • 如果该值置信区间内,则无法拒绝零假设。这意味着该值是参数的一个合理可能值。

例如,在人体体温案例中,我们检验平均体温是否为98.6华氏度。通过计算样本均值的95%置信区间(约为98.12至98.37),发现98.6远在该区间之外,因此我们拒绝零假设,认为平均体温并非98.6度。


引入A/B测试与置换检验 🧬

到目前为止,我们的假设检验都是回答“这个样本是否来自某个已知总体”的问题。现在,我们考虑一种新问题:我有两个样本,我想知道它们是否来自同一个总体(或分布)。

这类问题在比较两种处理效果时非常常见,例如:

  • 吸烟母亲与非吸烟母亲所生婴儿的体重分布是否相同?
  • 两个不同版本的网页设计,哪个带来更多的用户注册?

这种方法被称为A/B测试。我们将学习一种解决A/B测试问题的强大方法——置换检验

置换检验的核心思路是:如果两个样本真的来自同一个总体,那么将它们的观测值完全混合后随机重新分配成两组,所计算出的组间差异(如均值差)应该与我们原始观测到的差异类似。通过大量重复这种随机重排并计算差异,我们可以评估原始观测差异是否“异常”到足以拒绝“两组来自同一总体”的零假设。

在下一讲中,我们将详细学习置换检验的步骤,并将其应用于婴儿体重和“放气门”足球等实际案例。


总结 📝

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

  1. 总变异距离(TVD):用于量化两个分类分布整体差异的统计量。
  2. 置信区间与假设检验的联系:当零假设为参数等于某特定值时,可以通过检查该值是否落在置信区间内来进行假设检验。
  3. A/B测试与置换检验的引入:介绍了如何检验两个样本是否来自同一总体的问题框架,并引出了下节课将深入讲解的置换检验方法。

这些工具将帮助我们更系统地分析比较数据,并得出更可靠的统计结论。

24:置换检验 🧪

在本节课中,我们将学习一种名为“置换检验”的假设检验方法。我们将通过两个具体案例来理解其应用:一是探究母亲吸烟是否影响新生儿体重,二是分析著名的“放气门”足球丑闻中,两支球队的足球气压下降是否存在显著差异。

假设检验的两种类型

上一节我们介绍了标准的假设检验,本节中我们来看看置换检验有何不同。

在标准的假设检验中,我们有一个已知的总体和一个样本。我们的问题是:这个样本看起来像是从这个总体中抽取的吗?例如,总体可能是“一半正面一半反面的硬币”,样本是我们实际抛掷得到的结果。

在置换检验中,我们不知道总体。相反,我们有两个样本。我们的基本问题是:这两个样本是否来自同一个源头?它们是否是从同一个总体中抽取的?尽管我们并不具体说明这个总体是什么。

因此,标准假设检验针对的是已知的总体分布,而置换检验针对的是未知的总体。这意味着我们需要一种略有不同的模拟策略。

案例一:吸烟与新生儿体重 🍼

首先,我们来看一个关于母亲吸烟与新生儿体重的研究数据。数据记录了每位母亲是否吸烟以及其婴儿的出生体重(以盎司为单位)。

如果我们将吸烟母亲(蓝色)与非吸烟母亲(红色)的婴儿体重绘制在同一坐标轴上的直方图中,会发现吸烟母亲的婴儿体重似乎普遍较轻。

计算两组体重的均值,会得到一个明显的差异:大约9.25盎司。

我们的问题是:这9.25盎司的差异是显著的吗?还是说任何两个随机分组都可能产生这样的差异?

建立假设

  • 零假设 (H₀):所有婴儿来自同一个总体。在这个总体中,婴儿的体重分布相同,与母亲是否吸烟无关。
  • 备择假设 (H₁):吸烟母亲与非吸烟母亲的婴儿体重来自不同的分布。具体来说,吸烟母亲婴儿的平均体重更低(这反映了我们在样本中观察到的现象)。

选择检验统计量

我们使用两组均值的差异来量化两组之间的不同。检验统计量为:
检验统计量 = 非吸烟组平均体重 - 吸烟组平均体重

注意,这里没有取绝对值。因为备择假设是有方向的(吸烟组体重更低)。如果备择假设成立,非吸烟组均值会更大,吸烟组均值会更小,因此非吸烟组均值 - 吸烟组均值会是一个正数。这个数字越大,支持备择假设的证据就越强。如果它接近零或为负数,我们则没有证据支持备择假设。

我们观察到的统计量是 9.25

模拟零假设下的情况

这是置换检验的核心步骤。在零假设下,两组样本来自同一个总体。但我们不知道这个总体是什么。

聪明的解决方案是:将我们手头的两个样本合并,构造一个“总体”。这符合零假设的精神——吸烟状态不影响体重,所有婴儿体重可以看作来自一个大池子。

以下是模拟步骤:

  1. 将吸烟组和非吸烟组的所有婴儿体重数据合并,形成一个大的“构造总体”。
  2. 从这个构造总体中,随机抽取两个样本,其样本量分别等于原始的吸烟组和非吸烟组。
  3. 计算这两个随机分组的均值差。
  4. 重复此过程多次,得到在零假设(即分组是随机的)下,检验统计量的经验分布。

如何实现“随机抽取两个指定大小的样本”呢?一个等价且方便的方法是:随机打乱(置换)样本的“组标签”

在我们的数据中,“母亲是否吸烟”这一列就是组标签(例如,True=吸烟,False=不吸烟)。如果我们随机打乱这个标签列,但保持体重数据不变,那么每个体重数据所关联的“组别”就变成了随机的。由于只是打乱顺序,打乱后“True”和“False”的数量与原始数据相同,因此新形成的两个“随机组”大小也与原组相同。

代码实现

以下是使用Python进行置换检验的关键步骤代码示意:

import numpy as np
import pandas as pd

# 假设 df 是包含‘maternal_smoker’(布尔值)和‘birth_weight’(数值)两列的DataFrame
observed_diff = df[df[‘maternal_smoker’] == False][‘birth_weight’].mean() - df[df[‘maternal_smoker’] == True][‘birth_weight’].mean()

def difference_of_means(df, group_col, value_col):
    """计算按group_col分组后,value_col的组间均值差(False组 - True组)"""
    group_means = df.groupby(group_col)[value_col].mean()
    return group_means[False] - group_means[True]

# 模拟
num_repetitions = 500
differences = np.array([])

for i in np.arange(num_repetitions):
    # 1. 打乱标签列
    shuffled_labels = df[‘maternal_smoker’].sample(frac=1, replace=False).reset_index(drop=True)
    # 2. 创建包含打乱标签的新DataFrame
    shuffled_df = df.copy()
    shuffled_df[‘shuffled_smoker’] = shuffled_labels
    # 3. 基于打乱的标签计算均值差
    sim_diff = difference_of_means(shuffled_df, ‘shuffled_smoker‘, ’birth_weight‘)
    # 4. 存储结果
    differences = np.append(differences, sim_diff)

结果与结论

模拟完成后,我们会得到一个differences数组,它代表了在“分组与吸烟无关”的零假设下,可能出现的组间均值差分布。绘制其直方图,通常会看到一个以0为中心、向两侧逐渐减少的分布。

然后,我们将观察到的统计量 9.25 作为一条黑线标在直方图上。可以看到,9.25远远落在模拟分布的右侧尾端。

P值 的计算方法是:在模拟中,出现“差值大于等于9.25”的次数占总模拟次数的比例。在这个例子中,P值几乎为0。

由于P值极小(远小于常用的0.05阈值),我们拒绝零假设。有显著的统计证据表明,吸烟母亲与非吸烟母亲的婴儿平均体重不同,且吸烟母亲婴儿的体重更轻。

需要强调的是:假设检验只能告诉我们关联是否显著,不能证明因果关系。体重较低可能由吸烟直接导致,也可能与其他潜在因素(如吸烟母亲更可能摄入咖啡因)有关。

概念检查:打乱哪一列? 🔀

在婴儿体重的例子中,我们打乱了“母亲是否吸烟”这个标签列。那么,打乱“出生体重”这个数值列是否可行?

答案是肯定的。置换检验的核心思想是打破数据点与原始组标签之间的关联,从而创建随机的分组。无论是打乱标签列(保持数据不动),还是打乱数据列(保持标签不动),都能达到“随机重新分配数据点到两个组”的效果。两种方法是等价的。

案例二:“放气门”足球丑闻 🏈

现在我们来看一个更有趣的例子:2015年美式足球“放气门”事件。新英格兰爱国者队被指控在季后赛中故意给比赛用球放气,以便于接球。

关键背景:

  • 每支球队自带12个足球用于本方进攻。
  • 规定球压必须在12.5至13.5 psi(磅每平方英寸)之间。
  • 指控是爱国者队在中场时球压过低。

我们拥有的数据是:中场时测量了部分足球的气压(爱国者队11个,小马队4个,共14个)。我们已知两队赛前的大致初始球压(爱国者~12.5 psi,小马~13.0 psi)。

核心问题:我们不应直接比较中场时的绝对球压,因为爱国者队起始球压就较低。我们应该问:两队足球的气压下降值是否有显著差异? 指控是爱国者队的足球放气更多。

建立假设与统计量

  • 零假设 (H₀):两队足球的气压下降值来自同一个分布。观察到的差异仅是随机波动。
  • 备择假设 (H₁):爱国者队足球的气压下降值大于小马队(即放气更多)。
  • 检验统计量气压下降均值差 = 爱国者队平均下降值 - 小马队平均下降值
    备择假设有方向(爱国者队下降更多),因此统计量取正值表示支持备择假设。我们观察到的差值约为 0.74 psi

进行置换检验

流程与婴儿体重例子完全相同:

  1. 我们有两列数据:pressure_drop(气压下降值)和 team(球队标签)。
  2. pressure_drop列随机打乱,生成一个新列shuffled_drop
  3. 使用原始team标签和打乱后的shuffled_drop值,计算两个“随机分组”(实际上分组标签未变,但关联的下降值随机了)的均值差。
  4. 重复多次模拟,得到零假设下的统计量分布。

结果与结论

模拟得到的统计量分布依然以0为中心。将观察到的 0.74 标在分布图上,它位于右侧尾部。

计算P值(模拟中统计量 >= 0.74 的比例),结果大约为 0.002,这是一个非常小的值(< 0.01)。

因此,我们拒绝零假设。有高度显著的统计证据表明,爱国者队足球的气压下降值异常地高于小马队,这不太可能仅由随机因素导致。

再次提醒:统计显著性并不能直接等同于“作弊”的结论。可能存在其他解释(例如,足球质量、测量时的温度差异等),但在给定数据下,随机巧合的概率极低。NFL后续的调查也得出了类似的统计结论。

置换检验步骤总结 📝

本节课中我们一起学习了置换检验的完整流程:

  1. 明确问题:判断两个样本是否可能来自同一个总体。
  2. 建立假设
    • H₀:两组数据来自同一分布(组别标签无意义)。
    • H₁:两组数据来自不同分布(通常指定方向,如A组均值 > B组均值)。
  3. 选择检验统计量:通常使用两组均值之差。根据备择假设决定是否取绝对值。
  4. 计算观察统计量:基于原始数据和原始分组标签进行计算。
  5. 模拟零假设
    • 将两组数据合并,构造一个“总体”。
    • 通过随机打乱组标签随机打乱数据值的方式,重复创建许多次“随机分组”。
    • 每次计算随机分组下的检验统计量。
  6. 计算P值:在模拟得到的统计量分布中,计算出现“等于或比观察到的统计量更极端”(方向与备择假设一致)结果的比例。
  7. 得出结论:如果P值很小(例如 < 0.05),则拒绝零假设,认为两组差异显著;否则,没有足够证据拒绝零假设。

置换检验是一种强大且直观的非参数检验方法,它不依赖于总体分布的具体形式,只依赖于手头的数据,非常适合用于比较两个样本的分布差异。

25:相关性与预测入门 📊

📝 概述

在本节课中,我们将要学习数据科学中的一个新主题:预测。我们将从回顾统计推断的知识开始,然后重点介绍线性回归的基础——相关性。我们将学习如何通过散点图观察变量间的关系,如何计算和解释相关系数,并初步了解如何利用相关性进行简单的线性预测。


🔄 课程回顾与过渡

在课程的第二部分,我们学习了统计推断。我们通过样本去了解总体。当对总体有所预期时,我们使用标准的假设检验,询问样本是否看起来来自这个总体。置换检验用于比较两个样本是否来自同一个未知的总体分布。自助法和中心极限定理都是估计参数的方法,它们都能给出置信区间,即对总体中某个未知参数值的猜测。

我们还看到了自助法、中心极限定理与假设检验的结合应用,例如在人体体温的例子中,我们通过构建参数的置信区间来检验假设。

以上是我们学到的核心统计技术。在接下来的三节课中,我们将进入一个全新的主题:预测。给定一个样本,我们如何预测不在该样本中的数据?我们将聚焦于线性回归,这是一种最直接的预测技术,也是许多更复杂机器学习算法的基础。


🎯 预测问题

假设我们有一个至少包含两个数值变量的数据集,我们想根据其中一个变量的值来预测另一个变量。

例如:

  • 给定某人的教育水平(量化为数值,如受教育年限),预测其收入。
  • 给定我的身高,预测我孩子成年后的身高。
  • 给定我的年龄,预测我去过的国家数量。

只有当变量之间存在某种关系模式时,进行预测才有意义。如果变量之间没有关联(例如,根据身高预测去过的国家数量),预测就缺乏依据。

因此,当我们希望进行预测时,首先要做的就是通过散点图来检查变量间是否存在模式。


📈 散点图与关联

当我们绘制散点图时,我们寻找的是变量之间的任何关系,这被称为关联

  • 正关联:一个变量增加时,另一个变量也增加。
  • 负关联:一个变量增加时,另一个变量减少。

关联可以是线性的,也可以是非线性的。任何模式都有助于我们根据一个变量预测另一个变量。

我们将使用一个早期混合动力汽车的数据集进行演示。数据集包含年份、价格、加速度和每加仑英里数等数值变量。

以下是绘制散点图的示例观察:

  • 价格 vs. 加速度:呈现正关联。加速更快的汽车通常更昂贵。
  • 价格 vs. 每加仑英里数:呈现负关联。燃油经济性更好(更环保)的汽车反而更便宜。这似乎有悖常理,但结合第一个图可以发现,加速性能强的汽车(价格高)往往油耗也高(每加仑英里数低),这反映了当时消费者对混合动力汽车加速性能的看重。

通过散点图,我们可以直观判断变量间是否存在关联以及关联的方向。但我们还需要一个更具体的度量来衡量这种关联的强度。


🔢 相关系数

为了更具体地度量两个变量之间线性关联的强度,我们使用相关系数,通常用字母 r 表示。

相关系数 r 衡量的是两个变量之间线性关系的强弱,即散点图中的点围绕一条直线的紧密程度。

r 的计算公式如下:
r = (x和y标准单位值的乘积)的平均值

其中,标准单位的转换方法为:标准单位值 = (原始值 - 均值) / 标准差

r 的值始终在 -11 之间,这为我们提供了一个解释尺度。


📊 相关系数示例与解释

以下是一些不同数据集的相关系数示例图:

  • r = 1:所有点完全落在一条向上倾斜的直线上(完美正线性关联)。
  • r = 0.66:点大致围绕一条向上倾斜的直线分布,但更为分散。
  • r = 0.33:向上的趋势更弱,点更分散。
  • r = 0:点呈云状随机分布,没有明显的线性方向(不相关)。
  • r = -0.33:点大致围绕一条向下倾斜的直线分布。
  • r = -0.66:向下的趋势更强,点更紧密。
  • r = -1:所有点完全落在一条向下倾斜的直线上(完美负线性关联)。

符号(正/负) 表示关联的方向。绝对值大小 表示线性关联的强度,越接近1或-1,点围绕直线的聚集程度越高。

在我们的汽车数据示例中:

  • 价格与加速度的 r ≈ 0.69,表明较强的正线性关联。
  • 价格与每加仑英里数的 r ≈ -0.5,表明中等强度的负线性关联,且其散点图呈曲线状,这也解释了为何其线性相关系数的绝对值不是特别高。

🤔 为何使用标准单位?

计算 r 时转换为标准单位,是一种线性变换(即对每个值进行加减乘除常数操作)。线性变换会改变图形的刻度、位置(如平移)和缩放(如拉伸压缩),但不会改变散点图形状本身

因此,转换为标准单位:

  1. 不会改变变量间的线性关系模式。
  2. 其目的是消除量纲影响,使结果不依赖于变量原始的单位(如美元 vs. 日元),让我们只关心变量关系的形状。
  3. 使得 r 具有对称性:变量x和y的相关系数与y和x的相关系数相同。

🧮 相关系数的几何理解

将两个变量转换为标准单位后绘制散点图,坐标原点 (0, 0) 代表两个变量的平均值点。

  • 正关联:数据点主要分布在第一象限(x正,y正)和第三象限(x负,y负)。在这两个象限中,标准单位值的乘积为正。因此,大多数数据点的乘积为正,其平均值 r 为正。
  • 负关联:数据点主要分布在第二象限(x负,y正)和第四象限(x正,y负)。在这两个象限中,标准单位值的乘积为负。因此,大多数数据点的乘积为负,其平均值 r 为负。
  • 无关联:数据点随机分布在各个象限,正负乘积相互抵消,其平均值 r 接近零。

这种基于乘积平均的计算方法,巧妙地用一个数字概括了变量间的线性关系信息。


⚠️ 关联与相关的区别

重要术语区分

  • 关联:指变量间存在任何模式或关系,可以是线性的,也可以是非线性的(如曲线)。
  • 相关:特指线性关联。相关系数 r 只度量线性关系的强度。

例如,对于关系 y = x² 的数据,散点图呈抛物线形,存在明显的关联(模式),但其相关系数 r 为 0,因为不存在线性关系。


🔮 从相关性到预测

我们现在来看相关性如何与预测问题联系起来。我们将使用弗朗西斯·高尔顿在19世纪收集的关于父母与子女身高的历史数据集(注:高尔顿收集数据的初衷涉及优生学,我们在此仅将其作为统计学发展的一个历史案例使用)。

我们关注其中母亲与儿子身高的数据子集。散点图显示存在正关联:母亲较高,儿子也倾向于较高,但并非紧密围绕直线,关联强度中等。

预测目标:找到一条简单的规则,根据母亲身高预测儿子身高。

  1. 最简单(朴素)的预测:如果忽略母亲身高的信息,对所有人的最佳单一预测值是儿子的平均身高(在标准单位下为0)。
  2. 更好的预测:利用母亲与儿子身高之间的线性关联。我们的预测线应该穿过原点 (0, 0),即“身高处于平均水平的母亲,其儿子身高也处于平均水平”,这很合理。
  3. 最佳斜率:这条预测线的最佳斜率,恰好就是母亲身高与儿子身高之间的相关系数 r

在这个数据集中,r ≈ 0.33。因此,在标准单位下,根据母亲身高 (x_su) 预测儿子身高 (y_su) 的直线方程为:
预测的 y_su = r * x_su
预测的 y_su = 0.33 * x_su

这条线被称为回归线。它提供了一种基于另一个变量进行线性预测的简单方法。


📚 总结

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

  1. 预测问题的引入,其基础是变量间存在的关联
  2. 通过散点图可视化并判断关联的方向(正/负)。
  3. 使用相关系数 r 量化两个数值变量间线性关联的强度和方向。r 介于 -1 与 1 之间,通过变量标准单位值的乘积平均值计算得出。
  4. 理解了关联(任何模式)与相关(特指线性模式)的区别。
  5. 初步探索了如何利用相关系数 r 作为斜率,构建一条穿过原点的直线,进行简单的线性预测,这为下一讲深入探讨回归线奠定了基础。

下次课,我们将进一步学习如何解释和使用这条回归线进行预测。

26:回归分析进阶 📊

在本节课中,我们将深入学习回归分析,特别是回归线的概念、计算方法及其性质。我们将探讨如何利用回归线进行预测,理解其数学原理,并了解异常值对回归分析的影响。


回顾:相关系数与回归线

上一节我们讨论了如何利用直线进行预测,并在最后学习了回归线。

我们使用的数据来自高尔顿,他试图通过研究人类特征进行优生学实验。这是一个关于母亲身高与成年儿子身高的数据集。

散点图显示,母亲身高与儿子身高之间存在一定的关系。图形从左到右呈略微上升的趋势,这意味着身高较高的母亲往往有身高较高的儿子。

我们可以通过相关系数来量化这种线性关系的强度。相关系数的定义是:当两个变量都以标准单位衡量时,其乘积的平均值。

以下是计算相关系数的函数:

def standard_units(col):
    return (col - np.mean(col)) / np.std(col)

def calculate_r(x, y):
    x_su = standard_units(x)
    y_su = standard_units(y)
    return np.mean(x_su * y_su)

对于母亲和儿子的身高数据,相关系数约为三分之一。这表明变量之间存在线性关系,且呈上升趋势,但关系并非特别强。

我们讨论了不同的预测方法。如果不考虑母亲身高,我们可能会预测所有儿子的身高都等于平均身高。但如果允许将母亲身高作为预测因素,我们就需要一条有斜率的直线。

一个很好的方法是让这条直线的斜率等于相关系数 R。同时,这条直线会经过原点 (0, 0)。原点代表平均 x 和平均 y 的点。这意味着,如果一位母亲身高处于平均水平,我们预测她的儿子身高也处于平均水平。如果母亲身高高于平均水平,我们预测儿子身高也高于平均水平。


回归线的公式与应用

回归线的公式是:当变量处于标准单位时,预测的 y 值等于 x 值乘以相关系数 R。

公式为:预测的 y (标准单位) = R * x (标准单位)

例如,如果母亲身高比平均值高 0.5 个标准差,相关系数为 0.32,那么我们预测她的儿子身高将比平均值高 0.5 * 0.32 = 0.16 个标准差。

这引出了高尔顿对回归线的失望。他发现,无论母亲的身高多么极端,预测的儿子身高总是更接近平均值。他将这种现象称为“回归”,意为“倒退”,因为后代的身高似乎会向平均值“倒退”,而不是朝着他期望的优生学目标“进步”。

需要明确的是,回归预测的是更接近平均值的趋势,但这并不意味着每个人的身高都会变得平均。个体之间仍然存在很大的差异,预测值只是基于线性关系的最佳估计。


将回归线转换到原始单位

使用标准单位的回归线公式进行预测需要三步转换,这很不方便。我们希望有一个直接从原始单位(如英寸)到原始单位的公式。

我们可以通过代数推导或几何直观来得到这个公式。

从几何上看:

  • 在标准单位中,回归线经过原点 (0, 0)。在原始单位中,对应的点是 (x的平均值, y的平均值)。
  • 在标准单位中,斜率为 R(即 x 变化 1 个单位,y 变化 R 个单位)。在原始单位中,x 变化 1 个标准差,y 变化 R 个标准差。

因此,在原始单位中,回归线的斜率 m 和截距 b 公式如下:

  • 斜率 m = R * (y的标准差 / x的标准差)
  • 截距 b = y的平均值 - m * x的平均值

回归线方程为:预测的 y = m * x + b

以下是计算斜率和截距的函数:

def slope(x, y):
    r = calculate_r(x, y)
    return r * np.std(y) / np.std(x)

def intercept(x, y):
    m = slope(x, y)
    return np.mean(y) - m * np.mean(x)

对于母亲和儿子的身高数据,计算出的斜率约为 0.36,截距约为 45。现在,预测儿子身高只需将母亲身高乘以 0.36 再加上 45。


异常值对回归的影响

异常值会对回归线和相关系数产生巨大影响。

考虑一个数据集,其中大部分数据点清晰地落在一条直线上,但有一个点远离其他点。即使只有一个这样的异常值,也可能使相关系数变得接近于零,并使回归线严重偏离大多数数据点的趋势。

如果移除这个异常值重新计算,相关系数会变得接近于 1,回归线也能很好地拟合剩余的数据。

因此,在进行回归分析前,检查数据中是否存在异常值非常重要。需要根据实际情况判断是否应该将其包含在分析中。


为什么回归线是“最佳”直线?

回归线看起来能很好地拟合数据,但为什么说它是最佳直线?衡量一条直线拟合好坏的标准是预测误差。

对于每个数据点,误差等于实际值减去预测值。一个好的预测线应该使所有数据点的误差尽可能小。

由于误差有正有负,直接求平均会相互抵消。因此,我们像计算标准差一样,先求误差的平方,再求平均值,最后开方。这个指标称为均方根误差 (RMSE)

RMSE 越小,说明直线拟合得越好。回归线就是在所有可能的直线中,能使 RMSE 达到最小的那一条。因此,回归线也被称为最佳拟合线最小二乘线

我们可以通过优化算法来验证这一点。以下函数计算给定斜率和截距的直线的 RMSE:

def rmse(slope, intercept, x, y):
    predicted = slope * x + intercept
    errors = y - predicted
    return np.sqrt(np.mean(errors**2))

使用优化函数寻找使 RMSE 最小的斜率和截距,得到的结果与之前用公式计算的回归线斜率和截距完全相同。这证实了回归线在最小化预测误差意义上的最优性。

需要注意的是,回归线是“最佳的直线”,但这并不意味着直线就是最好的预测模型。如果数据本身是非线性的,那么即使是最好的直线,其预测效果也可能不如一个简单的曲线模型。回归线是在我们限定使用线性模型 (y = m*x + b) 进行预测时,所能找到的最佳选择。


总结

本节课我们一起深入学习了回归分析。
我们回顾了相关系数与回归线的基本概念,学会了如何在标准单位和原始单位下表达和使用回归线进行预测。
我们理解了回归向均值“倒退”的现象及其含义。
我们认识到异常值对回归结果的显著影响,以及检查数据的重要性。
最后,我们探讨了回归线之所以被称为“最佳拟合线”的原因:它在所有直线中,能够最小化预测的均方根误差 (RMSE)。这为我们使用回归分析提供了坚实的理论基础。

27:回归分析与统计推断 📊

在本节课中,我们将学习回归分析的深入概念,特别是残差分析,以及如何通过自助法将回归与统计推断联系起来。我们将探讨回归线的局限性,学习如何评估线性模型的适用性,并理解预测结果的不确定性。


🔍 回顾:回归线

上一节我们介绍了回归线。回归线是描述两个变量之间线性关系的最佳直线。它由以下公式定义的斜率和截距决定:

斜率公式m = r * (SD_y / SD_x)
截距公式b = avg_y - m * avg_x

这条线在所有可能的直线中,能够最小化均方根误差。均方根误差衡量的是预测值与实际数据点之间的平均偏差,其值越小,说明直线对数据的拟合越好。

然而,无论数据形态如何,我们总能计算出一条回归线。但这并不意味着它总是适合用于预测。只有当变量之间的关系确实是线性时,使用回归线进行预测才有意义。


📈 理解残差

为了评估回归线的拟合质量,我们需要引入残差的概念。残差是当预测值来自回归线时,每个数据点的预测误差。具体来说,它是实际 y 值减去回归线预测的 y 值:

残差公式residual = actual_y - predicted_y

以下是计算一组数据预测值和残差的示例代码:

def residuals(df, x_col, y_col):
    # 计算回归线的斜率和截距
    m = slope(df, x_col, y_col)
    b = intercept(df, x_col, y_col)
    # 计算预测值
    predicted = m * df.get(x_col) + b
    # 计算残差
    resids = df.get(y_col) - predicted
    return resids

每个数据点都有一个对应的残差。正残差表示实际值高于预测值(点在线上方),负残差则表示实际值低于预测值(点在线下方)。


📊 解读残差图

残差图是一种强大的可视化工具,它以自变量 x 为横轴,以残差为纵轴绘制散点图。通过观察残差图的形态,我们可以判断线性模型是否合适。

以下是解读残差图的关键点:

  • 理想情况(无模式):残差随机、均匀地分布在横轴(残差为0)上下,形成一个无特定模式的“云团”。这表示线性模型是合适的,没有系统性的预测偏差。
  • 存在曲线模式:如果残差图显示出明显的曲线形态(例如,先正后负再正),则表明变量间的真实关系可能是非线性的。使用线性回归可能不是最佳选择,或许应考虑多项式回归等其他方法。
  • 不均匀的垂直分布:如果残差的离散程度随着 x 的变化而改变(例如,在 x 值较小时残差分布很广,在 x 值较大时分布很窄),这种现象称为异方差性。这并不意味着回归线不好,但它提示我们:对于不同范围的 x,预测的可靠性是不同的。在数据变异大的区域,预测误差可能更大。

⚠️ 回归的陷阱:安斯库姆四重奏

仅仅依靠相关系数、均值、标准差等数值,无法判断数据是否适合线性回归。著名的“安斯库姆四重奏”完美地阐释了这一点。

以下是四个截然不同的数据集:

  1. 一个理想的线性关系数据集。
  2. 一个明显的非线性(抛物线)关系数据集。
  3. 一个包含强影响力异常值的数据集。
  4. 一个几乎所有 x 值都相同,仅靠一个离群点决定回归线的数据集。

令人惊讶的是,这四个数据集的五个关键统计量(avg_x, avg_y, SD_x, SD_y, r)完全相同,因此它们的回归线也完全一致。这个例子强有力地说明:在进行回归分析前,必须可视化数据(绘制散点图和残差图),而不能仅仅依赖数字摘要。


🔗 回归与统计推断

到目前为止,我们基于一个样本数据计算了回归线。但在现实中,我们的数据只是从更大总体中抽取的一个样本。这就引出了统计推断的核心问题:如果换一个样本,我们的回归线和预测结果会有多大不同?

为了解决这个问题,我们再次请出强大的工具:自助法

自助法的思路如下:

  1. 从原始样本数据中有放回地重复抽样,生成一个与原始样本量相同的“新样本”(自助样本)。这个过程会重复很多次(例如5000次)。
  2. 对每一个自助样本,计算其回归线的斜率和截距。
  3. 现在,我们不再只有一条回归线,而是拥有了由许多斜率和截距定义的“一堆”回归线。

当我们想预测一个特定 x 值(例如,母亲身高为68英寸)对应的 y 值时,我们可以将这 x 值代入每一条自助回归线,从而得到一系列预测值。取这些预测值的中间95%,就得到了一个预测区间。这个区间给出了基于不同可能样本时,预测值的大致范围。

以下是通过自助法计算预测区间的核心代码逻辑:

# 假设 slopes_array 和 intercepts_array 存储了自助法得到的斜率和截距
x_input = 68
# 利用数组运算,一次性计算所有回归线在 x=68 处的预测值
predictions = slopes_array * x_input + intercepts_array
# 计算95%预测区间
lower_bound = np.percentile(predictions, 2.5)
upper_bound = np.percentile(predictions, 97.5)

🍝 “意面图”与预测区间

将所有自助回归线绘制在同一张图上,会形成一个在中心收紧、向两端发散的图形,常被称为“意面图”。

这个图形揭示了一个重要规律:预测区间在自变量均值附近最窄,在远离均值时变宽。这是因为所有回归线都必然经过其自身样本的 (avg_x, avg_y) 点附近。由于不同自助样本的均值非常接近,这些线在中心区域被“束缚”在一起,差异很小。然而,随着向两端移动,斜率上的微小差异被放大,导致预测值出现更大的波动。

这意味着,对于身高接近平均水平的母亲,我们对其儿子身高的预测更加精确(区间窄);而对于身高特别高或特别矮的母亲,预测的不确定性更大(区间宽)。


📝 总结

本节课中我们一起学习了回归分析的深入内容。

我们首先学习了残差残差图,这是诊断线性回归模型是否适用的重要工具。通过识别残差图中的模式(如曲线或异方差性),我们可以判断是否需要更复杂的模型。

接着,我们通过安斯库姆四重奏认识到,不能仅凭汇总统计量就决定使用线性回归,可视化检查至关重要。

最后,我们将回归与统计推断相结合,使用自助法来量化预测的不确定性。我们了解到,基于单一样本的预测只是一个点估计,而通过自助法可以构建预测区间。并且,预测的精度取决于预测点在数据分布中的位置,越接近中心,预测越可靠。

这标志着我们新课学习部分的结束。接下来的课程将进入复习阶段,为期末考试做准备。

28:期末考试复习 📊

在本节课中,我们将复习春季学期期末考试中的一些关键题目。我们将重点讲解数据处理、假设检验、置信区间和回归分析等核心概念,帮助大家为即将到来的期末考试做好准备。


🏢 题目一:公寓租金数据分析

题目提供了一个名为APS的数据集,包含800套待租公寓的信息。数据列包括:租金(rent)、卧室数量(bed)、浴室数量(bath)、是否有洗衣机(laundry)、面积(sqft)、社区(neighborhood)、公寓楼(complex)和租期(lease_term)。

我们的目标是创建一个名为studio_average的数据框,其中每个提供单间公寓(studio)的公寓楼出现一次,并包含该楼所有单间公寓的平均租金。

解决方案分析

以下是几种可能的解决方案,我们需要判断其是否正确:

  1. 标准解决方案
    studio_average = (aps.query('bed == "studio"')
                         .groupby('complex')['rent']
                         .mean()
                         .reset_index())
    
    • 分析:此代码首先筛选出所有单间公寓,然后按公寓楼分组并计算平均租金。这完全符合题目要求。

  1. 分组后取最小值

    studio_average = (aps.groupby('complex')['rent']
                         .min()
                         .reset_index())
    
    • 分析:此代码计算每个公寓楼的最低租金,而非单间公寓的平均租金。即使单间公寓恰好是最便宜的,这也只是一个数据点,而非平均值。代码能运行,但结果错误。
  2. 按卧室类型和公寓楼双重分组

    studio_average = (aps.groupby(['bed', 'complex'])['rent']
                         .mean()
                         .reset_index()
                         .query('bed == "studio"'))
    
    • 分析:此代码先计算每个公寓楼内每种卧室类型的平均租金,然后筛选出单间公寓的行。虽然多计算了信息,但最终结果正确。
  3. 先分组平均再筛选

    studio_average = (aps.groupby('complex')['rent']
                         .mean()
                         .reset_index()
                         .query('bed == "studio"'))
    
    • 分析:在按complex分组并求均值后,数据框中将只包含数值列(如rent)。bed列是字符串类型,在求均值时已被丢弃,因此后续无法根据bed == "studio"进行筛选。此代码会报错。

上一节我们分析了如何正确计算单间公寓的平均租金。接下来,我们看一个相关的深入问题。

深入问题:替代方法的含义

考虑以下替代方法生成的alternate_approach数据框:

grouped = aps.groupby(['bed', 'complex'])['rent'].mean().reset_index()
alternate_approach = grouped.groupby('complex')['rent'].min().reset_index()

题目指出,如果alternate_approach的结果与第一部分(标准解决方案)的结果完全相同,这说明了关于公寓的什么事实?

分析

  • grouped数据框包含每个公寓楼内每种卧室类型的平均租金。
  • alternate_approach取每个公寓楼中所有卧室类型平均租金的最小值。
  • 要使这个最小值等于该公寓楼单间公寓的平均租金,意味着在每个公寓楼中,单间公寓的平均租金必须不高于(即小于或等于)其他任何卧室类型的平均租金。换句话说,单间公寓是各个公寓楼中最便宜的房型(按平均租金计算)。

📈 题目二:置信区间计算

题目给出关于圣地亚哥Mission Hills社区400套公寓押金的样本信息:

  • 样本均值(mean_deposit) = $2300
  • 样本标准差(sd_deposit) = $500
  • 已知标准正态分布的CDF在-0.8处的值约为0.21。

要求利用这些信息,构建总体平均押金的58%置信区间。

核心概念与计算

95%置信区间的公式为:
样本均值 ± 2 * (样本标准差 / √样本数量)

对于58%的置信区间,我们需要找到对应的“倍数”(而不是2)。已知CDF(-0.8) = 0.21,这意味着正态曲线下,有21%的面积在均值左侧0.8个标准差以外。根据对称性,也有21%的面积在均值右侧0.8个标准差以外。因此,均值±0.8个标准差范围内的面积是100% - 21% - 21% = 58%。

因此,58%置信区间的公式为:
样本均值 ± 0.8 * (样本标准差 / √样本数量)

代入数值计算:
2300 ± 0.8 * (500 / √400) = 2300 ± 0.8 * (500 / 20) = 2300 ± 0.8 * 25 = 2300 ± 20

所以,置信区间的端点是 $2280$2320


🔬 题目三:假设检验与模拟

题目希望使用公寓数据检验以下两对假设:

  1. 第一对
    • H₀: 一居室数量 = 二居室数量
    • H₁: 一居室数量 > 二居室数量
  2. 第二对
    • H₀: 一居室数量 = 二居室数量
    • H₁: 一居室数量 ≠ 二居室数量

已知数据中一居室或二居室的公寓总数为467套。在零假设下进行了模拟,生成了两个统计量数组:

  • prop_1br:一居室公寓的比例(相对于一居室和二居室总数)。
  • abs_diff:上述比例与0.5的绝对差值。

模拟结果显示prop_1br的分布是正态的,均值为0.5,标准差约为0.05,并给出了其几个百分位数。

问题解析

以下是基于模拟结果需要回答的问题:

  • prop_1br的均值:根据零假设(数量相等),比例应为0.5。
  • prop_1br的标准差:从给出的百分位数(如2.5%分位数是0.4,97.5%分位数是0.6)可知,均值±2个标准差覆盖95%数据,因此标准差为 (0.6 - 0.5) / 2 = 0.05
  • abs_diff的95%分位数abs_diffprop_1br分布“折叠”后的结果。prop_1br中2.5%的数据小于0.4,2.5%的数据大于0.6。当取绝对值差时,这两部分(共5%)的数据都对应着大于0.1的值。因此,abs_diff的95%分位数是 0.1
  • 应使用的检验统计量
    • 检验第一对假设(有方向性,>)应使用 prop_1br 本身。
    • 检验第二对假设(无方向性,)应使用 abs_diff
  • 观察结果分析:题目指出,在5%显著性水平下,根据第一对假设拒绝了零假设,但根据第二对假设未能拒绝零假设。求观察到的prop_1br可能值。
    • 第一对假设(拒绝):拒绝意味着观察到的prop_1br值很大,落在了prop_1br分布右尾的5%区域内。根据给出的95%分位数是0.58,这意味着观察值必须 > 0.58
    • 第二对假设(未拒绝):未拒绝意味着观察值对应的abs_diff没有落在abs_diff分布右尾的5%区域内。根据计算出的95%分位数是0.1,这意味着|prop_1br - 0.5| 必须 ≤ 0.1,即观察值在 0.4 到 0.6 之间
    • 综合:观察值必须同时满足 > 0.58在[0.4, 0.6]之间。因此,可能的范围是 0.58 到 0.6 之间(例如0.59)。


📉 题目四:回归分析应用

题目给出了租金(rent)和面积(sqft)的汇总统计:

  • 平均租金 = $3000, 租金标准差 = $400
  • 平均面积 = 2000 sqft, 面积标准差 = 100 sqft
  • 两者的相关系数 r = 0.9

问题解析

  1. 正态分布问题:假设租金服从正态分布,求84%的公寓租金低于哪个值?
    • 根据经验法则,均值右侧1个标准差范围内的面积约为84%(50% + 34%)。因此,该值为 均值 + 1个标准差 = 3000 + 400 = $3400

  1. 标准化单位(Standard Units)转换:Sophie的公寓租金为$5000,将其转换为标准单位。
    • 标准单位公式:(值 - 均值) / 标准差
    • (5000 - 3000) / 400 = 5
    • 她的租金比平均租金高5个标准差。

  1. 回归预测:基于Sophie的租金,预测其公寓面积。

    • 在标准单位下,回归方程是:y_su = r * x_su
    • 已知 x_su(租金标准单位)= 5, r = 0.9。
    • 因此,预测的面积标准单位 y_su = 0.9 * 5 = 4.5
    • 将标准单位转换回原始单位:预测面积 = 面积均值 + y_su * 面积标准差 = 2000 + 4.5 * 100 = 2450 sqft
  2. 计算残差:若Sophie公寓的实际面积是2300 sqft,预测的残差是多少?

    • 残差 = 实际值 - 预测值 = 2300 - 2450 = -150 sqft

  1. 反向预测:CC的公寓面积为1800 sqft,预测其租金。
    • 先将面积转换为标准单位:(1800 - 2000) / 100 = -2
    • 使用回归方程预测租金标准单位:y_su(租金) = r * x_su(面积) = 0.9 * (-2) = -1.8
    • 将租金标准单位转换回原始单位:预测租金 = 租金均值 + y_su * 租金标准差 = 3000 + (-1.8) * 400 = $2280

🎯 总结

本节课我们一起复习了期末考试中的几个核心题型:

  1. 数据分组与聚合:重点在于理解groupby、筛选和聚合操作的顺序与逻辑。
  2. 置信区间构建:关键在于根据给定的分布信息(如CDF值)确定合适的乘数,并代入公式计算。
  3. 假设检验与模拟:核心是理解不同备择假设对应不同的检验统计量,并能够根据模拟结果的分布解读观察值的意义。
  4. 回归分析应用:熟练掌握利用均值、标准差和相关系数进行预测,包括在原始单位和标准单位之间的转换,以及残差的计算。

希望本次复习能帮助大家巩固这些重要的数据科学概念。祝大家在期末考试中取得好成绩!

29:数据科学原理 - 期末复习与课程总结 📊

在本节课中,我们将回顾一些过往的考试题目,并总结整个课程的核心内容。课程分为两部分:首先,我们将通过解决具体的考试问题来复习关键概念;其次,我们将探讨课程结束后的学习路径,并展示一些更高级的数据可视化技巧。


📝 期末考试安排与课程反馈

期末考试将于明天举行,时间为11:30至14:30。考试地点分为两个教室,具体安排已由助教Eric通过邮件通知。参加周日补考的同学无需座位安排,到场后在大厅登记即可。

今天是填写课程期末调查的最后一天。完成调查对课程未来的改进非常重要,并且在明天早上8点前完成可以获得一次参与分。

关于成绩,我们将在下周中段完成期末项目和期末考试的评分,并计算总成绩。成绩报告将更新,包含所有作业、已丢弃的作业和延期天数等信息。请注意,本课程不设成绩曲线,请专注于你能控制的部分,即明天的期末考试。


🔍 过往考题复习

本节课前半部分将用于复习过往的考试题目。我们将从合并数据、解读直方图和理解P值等主题开始。

合并数据框问题

上一节我们介绍了考试安排。本节中,我们来看看一个关于合并数据框的具体问题。这个问题来自Winter 24的第9题,涉及一个关于“霹雳舞”比赛的数据集。

问题背景:2024年奥运会首次引入霹雳舞(Breaking)项目。16名运动员参加单败淘汰赛。我们有一个名为 breaking 的DataFrame,它记录了每位运动员在每一轮比赛中的表现。每位参赛者在其参加的每一轮中都会有一行记录。

以下是关于数据框 breaking 的关键信息:

  • 冠军会出现4次(参加了所有4轮)。
  • 亚军也会出现4次。
  • 有4名运动员的名字恰好出现2次(进入了第二轮但未进入第三轮)。
  • 有2名运动员的名字恰好出现3次(进入了第三轮但未进入决赛)。
  • 有8名运动员的名字只出现1次(在第一轮被淘汰)。

现在,如果将 breaking 数据框与自身在 name 列上进行合并,结果数据框将有多少行?

解决方案
合并操作的本质是,对于左表中的每一行,都会与右表中具有相同键值的所有行进行配对。因此,一个名字在结果中产生的行数,等于该名字在数据框中出现次数的平方。

以下是计算过程:

  1. 名字出现1次的8人:每人贡献 1 * 1 = 1 行,共 8 * 1 = 8 行。
  2. 名字出现2次的4人:每人贡献 2 * 2 = 4 行,共 4 * 4 = 16 行。
  3. 名字出现3次的2人:每人贡献 3 * 3 = 9 行,共 2 * 9 = 18 行。
  4. 名字出现4次的2人:每人贡献 4 * 4 = 16 行,共 2 * 16 = 32 行。

总行数为:8 + 16 + 18 + 32 = 74 行。

核心公式
如果某个键值在数据框中出现 n 次,那么在自合并后,它将产生 行。

# 概念性代码,说明合并后的行数计算逻辑
total_rows = sum(count**2 for count in appearance_counts.values())

直方图解读问题

接下来,我们来看一个关于解读直方图的问题,来自Fall 23的第13题。

问题背景:Ashley有一个包含400条交易记录的随机样本,并绘制了两幅描述amount列分布的直方图(A和B),它们使用了不同的箱宽。直方图A的箱宽为30,但其中两个条形的高度数据丢失了。已知直方图A和B描述的是相同的数据。

第一个问题是:哪个箱体大约包含60笔交易?
由于总共有400笔交易,60笔交易占总数的比例为 60/400 = 0.15。在直方图A中,每个箱体的宽度为30。面积代表比例,所以我们需要找到高度满足 宽度 * 高度 = 0.15 的箱体。计算高度:高度 = 0.15 / 30 = 0.005。观察直方图A,高度约为0.005的箱体对应的是(60, 90]这个区间。

第二个更有趣的问题是:已知直方图B中前两个箱体(宽度为45)的高度分别为 yz,如何用 yz 表示直方图A中(60, 90]箱体的高度(设为 u)?
关键点在于,两个直方图中从0到90这个数据区间的总面积必须相等。

计算逻辑

  1. 直方图A中[0, 90]区间的总面积 = 30*w + 30*x + 30*u
  2. 直方图B中[0, 90]区间的总面积 = 45*y + 45*z
  3. 两者相等:30*(w + x + u) = 45*(y + z)
  4. 解出 uu = (45*(y + z) / 30) - w - x = (3/2)*(y + z) - w - x

P值方向判断

最后,我们简要回顾一下假设检验中P值方向的选择。这取决于你所使用的检验统计量及其含义。

核心思路:P值计算的是,在原假设成立的前提下,得到与观察结果同样极端或更极端结果的概率。“极端”的方向由备择假设决定。

决策方法

  1. 绘制一条数轴,表示检验统计量可能的取值。
  2. 标记出你的观察到的统计量值。
  3. 确定哪个方向的取值更支持备择假设(即更“极端”)。
  4. 计算P值时,就计算统计量向那个方向取极端值(大于等于或小于等于观察值)的概率。

示例

  • 如果检验统计量是“正面次数与200的绝对差值”,用于检验硬币是否公平。大的差值支持“不公平”的备择假设。因此,P值应计算为 P(统计量 >= 观察值)
  • 如果检验统计量是“正面次数”,用于检验硬币是否偏向反面。少的正面次数支持“偏向反面”的备择假设。因此,P值应计算为 P(统计量 <= 观察值)

🚀 课程结束后的学习路径

在复习了关键考题后,我们来看看课程结束后你可以如何继续你的数据科学之旅。

开展个人项目

本课程为你提供了开展个人数据科学项目所需的技能。你可以寻找感兴趣的数据集进行分析。

以下是一些寻找数据的途径:

  • 数据仓库:如Kaggle,拥有大量社区上传的数据集和项目。
  • 政府与机构数据:各国政府、世界银行等国际组织公开的数据。
  • 特定领域数据:体育、音乐、交通、新冠疫情等。
  • 网络抓取:可以从维基百科等网站提取表格数据。
  • 自行收集:通过谷歌表单等工具创建调查并收集数据。

技术准备

  • 保存你的工作:如果你下个季度不再使用DataHub,请记得从DataHub下载你的笔记本和数据文件。具体方法已发布在课程网站的调试页面上。
  • 从BabyPandas到Pandas:本课程使用的BabyPandas是Pandas的简化版。所有BabyPandas代码在标准Pandas中同样有效。只需将导入语句 import babypandas as bpd 改为 import pandas as pd,并将代码中的 bpd 替换为 pd 即可。Pandas提供了更多功能和参数选项。

进阶数据可视化演示

本课程使用了基础的绘图功能。数据可视化本身是一个广阔的领域。这里使用一个更高级的库Plotly进行演示,其代码逻辑与我们所学的相似。

我们使用gapminder数据集,它记录了1952年至2007年间世界各国的人口、人均GDP和预期寿命等数据。

示例1:动态散点图
我们可以绘制人均GDP与预期寿命的散点图,并用颜色区分大洲,用点的大小表示国家人口。Plotly的强大之处在于可以创建动画,展示这些指标随时间的变化。例如,可以观察到中国和印度随着经济发展(向右移动),预期寿命显著提高(向上移动),而非洲许多国家的发展相对滞后。

示例2:动态直方图
可以绘制预期寿命的分布直方图,并制作动画展示该分布如何从1952年到2007年整体向右移动(即全球预期寿命普遍提高)。

示例3:热力图
可以绘制世界地图热力图,用颜色深浅表示各地区的预期寿命,直观展示全球健康水平的差异。

这些演示表明,在掌握了本课程的基础后,你已有能力通过学习Plotly等新工具库的文档,创建出复杂且富有洞察力的可视化作品。


🎓 课程核心回顾与总结

在本节课中,我们一起回顾了数据合并、直方图解读和假设检验等关键考试题型,并展望了课程结束后的学习方向。

现在,让我们总结一下整个DSC 10课程的核心旅程,这与第一讲的目标相呼应:

  1. 使用Python探索数据:这是前四周的重点,我们学习了数据操作、清洗和初步可视化。
  2. 对总体进行推断:在随后的几周里,我们通过自助法和假设检验,学习了如何利用样本数据对总体做出统计推断。
  3. 进行预测:在课程最后,我们学习了回归分析,这是最基本的机器学习算法之一,用于根据变量之间的关系进行预测。

未来的数据科学课程将会在这些主题上分别进行更深入、更专门的探讨。

衷心感谢所有为本课程顺利运行付出努力的教学团队成员(教授、助教、辅导员等)。 tutoring是一个由修读过本课程的学生申请的带薪职位,是回馈社区的好方式。

如果你对数据科学专业的发展路径有疑问,我们将在Ed讨论区发布帖子,欢迎你向助教们咨询。

最后,请为明天的期末考试做好充分准备。保证充足的休息和饮食,以最佳状态迎接挑战。祝大家好运!


posted @ 2026-03-26 12:19  布客飞龙III  阅读(38)  评论(0)    收藏  举报